Update the media send flow with a persistent rail.
This commit is contained in:
parent
b58faf4fd1
commit
9f7bb69341
22 changed files with 1240 additions and 921 deletions
|
@ -13,7 +13,7 @@
|
|||
<item>
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="rectangle" >
|
||||
<corners android:radius="5dp" />
|
||||
<solid android:color="@color/transparent_black_70"/>
|
||||
<solid android:color="@color/transparent_white_40"/>
|
||||
</shape>
|
||||
</item>
|
||||
</ripple>
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="rectangle" >
|
||||
<corners android:radius="5dp" />
|
||||
<solid android:color="@color/transparent_black_70"/>
|
||||
<solid android:color="@color/transparent_white_40"/>
|
||||
</shape>
|
|
@ -1,32 +1,60 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<RelativeLayout
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:layout_gravity="end">
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<Button
|
||||
android:id="@+id/camera_capture_button"
|
||||
android:layout_width="80dp"
|
||||
android:layout_height="80dp"
|
||||
android:layout_centerHorizontal="true"
|
||||
android:layout_centerVertical="true"
|
||||
android:background="@drawable/ic_camera_shutter" />
|
||||
android:layout_marginEnd="24dp"
|
||||
android:background="@drawable/ic_camera_shutter"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintBottom_toBottomOf="parent"/>
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/camera_flip_button"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_above="@+id/camera_capture_button"
|
||||
android:layout_marginBottom="40dp"
|
||||
android:layout_centerHorizontal="true"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_marginTop="14dp"
|
||||
android:src="@drawable/ic_switch_camera_32"
|
||||
android:scaleType="fitCenter"
|
||||
android:background="?selectableItemBackgroundBorderless"
|
||||
android:visibility="gone"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
tools:visibility="visible" />
|
||||
|
||||
</RelativeLayout>
|
||||
<com.makeramen.roundedimageview.RoundedImageView
|
||||
android:id="@+id/camera_gallery_button"
|
||||
android:layout_width="36dp"
|
||||
android:layout_height="36dp"
|
||||
android:layout_marginBottom="42dp"
|
||||
app:layout_constraintBottom_toTopOf="@id/camera_capture_button"
|
||||
app:layout_constraintStart_toStartOf="@id/camera_capture_button"
|
||||
app:layout_constraintEnd_toEndOf="@id/camera_capture_button"
|
||||
app:riv_corner_radius="4dp"
|
||||
app:riv_border_color="@color/core_white"
|
||||
app:riv_border_width="2dp"/>
|
||||
|
||||
<include
|
||||
android:id="@+id/camera_count_button"
|
||||
layout="@layout/mediasend_count_button"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="11dp"
|
||||
android:layout_marginBottom="14dp"
|
||||
android:layout_gravity="bottom|end"
|
||||
android:visibility="gone"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintStart_toStartOf="@id/camera_capture_button"
|
||||
app:layout_constraintEnd_toEndOf="@id/camera_capture_button"
|
||||
tools:visibility="visible" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
|
|
|
@ -1,32 +1,61 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<RelativeLayout
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="16dp"
|
||||
android:layout_gravity="bottom">
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<Button
|
||||
android:id="@+id/camera_capture_button"
|
||||
android:layout_width="80dp"
|
||||
android:layout_height="80dp"
|
||||
android:layout_centerHorizontal="true"
|
||||
android:layout_centerVertical="true"
|
||||
android:background="@drawable/ic_camera_shutter" />
|
||||
android:layout_marginBottom="24dp"
|
||||
android:background="@drawable/ic_camera_shutter"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent" />
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/camera_flip_button"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_toStartOf="@+id/camera_capture_button"
|
||||
android:layout_marginEnd="40dp"
|
||||
android:layout_centerVertical="true"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:layout_marginTop="14dp"
|
||||
android:src="@drawable/ic_switch_camera_32"
|
||||
android:scaleType="fitCenter"
|
||||
android:background="?selectableItemBackgroundBorderless"
|
||||
android:visibility="gone"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
tools:visibility="visible" />
|
||||
|
||||
<com.makeramen.roundedimageview.RoundedImageView
|
||||
android:id="@+id/camera_gallery_button"
|
||||
android:layout_width="36dp"
|
||||
android:layout_height="36dp"
|
||||
android:layout_marginStart="32dp"
|
||||
app:layout_constraintTop_toTopOf="@id/camera_capture_button"
|
||||
app:layout_constraintBottom_toBottomOf="@id/camera_capture_button"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:riv_corner_radius="4dp"
|
||||
app:riv_border_color="@color/core_white"
|
||||
app:riv_border_width="2dp"/>
|
||||
|
||||
<include
|
||||
android:id="@+id/camera_count_button"
|
||||
layout="@layout/mediasend_count_button"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="11dp"
|
||||
android:layout_marginBottom="14dp"
|
||||
android:layout_marginEnd="32dp"
|
||||
android:layout_gravity="bottom|end"
|
||||
android:visibility="gone"
|
||||
app:layout_constraintTop_toTopOf="@id/camera_capture_button"
|
||||
app:layout_constraintBottom_toBottomOf="@id/camera_capture_button"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
tools:visibility="visible" />
|
||||
|
||||
</RelativeLayout>
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
|
|
|
@ -19,6 +19,8 @@
|
|||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_marginStart="2dp"
|
||||
android:layout_marginTop="2dp" />
|
||||
android:layout_marginTop="2dp"
|
||||
android:paddingBottom="@dimen/media_picker_rail_padding_affordance"
|
||||
android:clipToPadding="false" />
|
||||
|
||||
</LinearLayout>
|
|
@ -19,6 +19,8 @@
|
|||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_marginStart="2dp"
|
||||
android:layout_marginTop="2dp" />
|
||||
android:layout_marginTop="2dp"
|
||||
android:paddingBottom="@dimen/media_picker_rail_padding_affordance"
|
||||
android:clipToPadding="false"/>
|
||||
|
||||
</LinearLayout>
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
<FrameLayout
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:orientation="vertical"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
@ -11,59 +12,152 @@
|
|||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent" />
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/mediasend_count_button"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="32dp"
|
||||
android:layout_marginEnd="32dp"
|
||||
android:layout_gravity="bottom|end"
|
||||
android:padding="8dp"
|
||||
android:gravity="center"
|
||||
android:orientation="horizontal"
|
||||
android:background="@drawable/media_count_button_background"
|
||||
android:elevation="4dp"
|
||||
android:visibility="gone"
|
||||
tools:visibility="visible">
|
||||
<org.thoughtcrime.securesms.components.InputAwareLayout
|
||||
android:id="@+id/mediasend_hud"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:animateLayoutChanges="true">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/mediasend_count_button_text"
|
||||
android:layout_width="wrap_content"
|
||||
<LinearLayout
|
||||
android:id="@+id/mediasend_caption_and_rail"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:minWidth="28dp"
|
||||
android:paddingStart="7dp"
|
||||
android:paddingEnd="7dp"
|
||||
android:paddingTop="2dp"
|
||||
android:paddingBottom="2dp"
|
||||
android:gravity="center"
|
||||
android:background="@drawable/media_count_number_background"
|
||||
android:textColor="@color/signal_primary"
|
||||
android:textSize="18sp"
|
||||
tools:text="3" />
|
||||
android:layout_gravity="bottom"
|
||||
android:orientation="vertical"
|
||||
android:background="@color/transparent_black_70">
|
||||
|
||||
<org.thoughtcrime.securesms.components.emoji.EmojiEditText
|
||||
android:id="@+id/mediasend_caption"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:paddingTop="6dp"
|
||||
android:paddingBottom="6dp"
|
||||
style="@style/Signal.Text.Body"
|
||||
android:maxLines="3"
|
||||
android:maxLength="240"
|
||||
android:hint="@string/MediaSendActivity_add_a_caption"
|
||||
android:autoText="true"
|
||||
android:inputType="textAutoCorrect|textCapSentences|textMultiLine"
|
||||
android:background="@null"/>
|
||||
|
||||
<ImageView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="20dp"
|
||||
android:layout_marginStart="2dp"
|
||||
android:src="@drawable/ic_arrow_right"
|
||||
android:tint="@color/core_white"/>
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/mediasend_media_rail"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="2dp"
|
||||
android:layout_marginStart="2dp"
|
||||
android:layout_marginEnd="2dp"
|
||||
tools:layout_height="64dp"/>
|
||||
|
||||
</LinearLayout>
|
||||
<LinearLayout
|
||||
android:id="@+id/mediasend_compose_row"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="12dp"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:layout_marginBottom="12dp"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/mediasend_camera_button"
|
||||
android:layout_width="44dp"
|
||||
android:layout_height="44dp"
|
||||
android:layout_marginBottom="32dp"
|
||||
android:layout_marginStart="32dp"
|
||||
android:layout_gravity="bottom|start"
|
||||
android:padding="12dp"
|
||||
android:src="@drawable/ic_camera_filled_24"
|
||||
android:tint="@color/core_grey_60"
|
||||
android:background="@drawable/media_camera_button_background"
|
||||
android:elevation="4dp"
|
||||
android:visibility="gone"
|
||||
tools:visibility="visible"/>
|
||||
<LinearLayout
|
||||
android:id="@+id/mediasend_compose_container"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:paddingStart="8dp"
|
||||
android:paddingEnd="8dp"
|
||||
android:orientation="horizontal"
|
||||
android:background="@drawable/compose_background_camera">
|
||||
|
||||
<org.thoughtcrime.securesms.components.emoji.EmojiToggle
|
||||
android:id="@+id/mediasend_emoji_toggle"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="@dimen/conversation_compose_height"
|
||||
android:layout_gravity="bottom"
|
||||
android:paddingStart="4dp"
|
||||
android:paddingEnd="6dp"
|
||||
android:background="?selectableItemBackgroundBorderless"
|
||||
android:contentDescription="@string/conversation_activity__emoji_toggle_description" />
|
||||
|
||||
<Space
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="@dimen/conversation_compose_height" />
|
||||
|
||||
<org.thoughtcrime.securesms.components.ComposeText
|
||||
style="@style/ComposeEditText"
|
||||
android:id="@+id/mediasend_compose_text"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center_vertical"
|
||||
android:layout_weight="1"
|
||||
android:nextFocusForward="@+id/send_button"
|
||||
android:nextFocusRight="@+id/send_button"
|
||||
tools:hint="Send TextSecure message" >
|
||||
<requestFocus />
|
||||
</org.thoughtcrime.securesms.components.ComposeText>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<FrameLayout
|
||||
android:id="@+id/mediasend_send_button_bkg"
|
||||
android:layout_width="@dimen/conversation_compose_height"
|
||||
android:layout_height="@dimen/conversation_compose_height"
|
||||
android:layout_marginStart="12dp"
|
||||
android:layout_gravity="bottom"
|
||||
android:background="@drawable/circle_tintable"
|
||||
tools:backgroundTint="@color/core_blue">
|
||||
|
||||
<org.thoughtcrime.securesms.components.SendButton
|
||||
android:id="@+id/mediasend_send_button"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="match_parent"
|
||||
android:paddingTop="6dp"
|
||||
android:paddingEnd="6dp"
|
||||
android:paddingBottom="6dp"
|
||||
android:paddingStart="11dp"
|
||||
android:scaleType="fitCenter"
|
||||
android:contentDescription="@string/conversation_activity__send"
|
||||
android:src="?conversation_transport_sms_indicator"
|
||||
android:background="@drawable/circle_touch_highlight_background" />
|
||||
|
||||
</FrameLayout>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<include
|
||||
android:id="@+id/mediasend_count_button"
|
||||
layout="@layout/mediasend_count_button"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="11dp"
|
||||
android:layout_marginBottom="12dp"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:layout_gravity="bottom|end"
|
||||
android:visibility="gone"
|
||||
tools:visibility="visible" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/mediasend_characters_left"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:paddingBottom="12dp"
|
||||
android:visibility="gone"
|
||||
tools:visibility="visible"
|
||||
tools:text="160/160 (1)" />
|
||||
|
||||
<ViewStub
|
||||
android:id="@+id/mediasend_emoji_drawer_stub"
|
||||
android:layout="@layout/scribble_fragment_emojidrawer_stub"
|
||||
android:inflatedId="@+id/emoji_drawer"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"/>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</org.thoughtcrime.securesms.components.InputAwareLayout>
|
||||
|
||||
</FrameLayout>
|
36
res/layout/mediasend_count_button.xml
Normal file
36
res/layout/mediasend_count_button.xml
Normal file
|
@ -0,0 +1,36 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:padding="6dp"
|
||||
android:gravity="center"
|
||||
android:orientation="horizontal"
|
||||
android:background="@drawable/media_count_button_background"
|
||||
android:elevation="4dp">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/mediasend_count_button_text"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:minWidth="28dp"
|
||||
android:paddingStart="7dp"
|
||||
android:paddingEnd="7dp"
|
||||
android:paddingTop="2dp"
|
||||
android:paddingBottom="2dp"
|
||||
android:gravity="center"
|
||||
android:background="@drawable/media_count_number_background"
|
||||
android:textColor="@color/signal_primary"
|
||||
android:textSize="18dp"
|
||||
tools:text="3" />
|
||||
|
||||
|
||||
<ImageView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="20dp"
|
||||
android:layout_marginStart="2dp"
|
||||
android:src="@drawable/ic_arrow_right"
|
||||
android:tint="@color/core_white"/>
|
||||
|
||||
</LinearLayout>
|
|
@ -1,8 +1,6 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<FrameLayout
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:background="@color/core_black">
|
||||
|
@ -19,138 +17,4 @@
|
|||
android:animateLayoutChanges="true"
|
||||
android:layout_gravity="top"/>
|
||||
|
||||
<org.thoughtcrime.securesms.components.InputAwareLayout
|
||||
android:id="@+id/mediasend_hud"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/mediasend_caption_and_rail"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="bottom"
|
||||
android:orientation="vertical"
|
||||
android:background="@color/transparent_black_70">
|
||||
|
||||
<org.thoughtcrime.securesms.components.emoji.EmojiEditText
|
||||
android:id="@+id/mediasend_caption"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:paddingTop="6dp"
|
||||
android:paddingBottom="6dp"
|
||||
style="@style/Signal.Text.Body"
|
||||
android:maxLines="3"
|
||||
android:maxLength="240"
|
||||
android:hint="@string/MediaSendActivity_add_a_caption"
|
||||
android:autoText="true"
|
||||
android:inputType="textAutoCorrect|textCapSentences|textMultiLine"
|
||||
android:background="@null"/>
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/mediasend_media_rail"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="2dp"
|
||||
android:layout_marginStart="2dp"
|
||||
android:layout_marginEnd="2dp"
|
||||
tools:layout_height="64dp"/>
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/mediasend_compose_container"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="12dp"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:layout_marginBottom="12dp"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:paddingStart="8dp"
|
||||
android:paddingEnd="8dp"
|
||||
android:orientation="horizontal"
|
||||
android:background="@drawable/compose_background_camera">
|
||||
|
||||
<org.thoughtcrime.securesms.components.emoji.EmojiToggle
|
||||
android:id="@+id/mediasend_emoji_toggle"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="@dimen/conversation_compose_height"
|
||||
android:layout_gravity="bottom"
|
||||
android:paddingStart="4dp"
|
||||
android:paddingEnd="6dp"
|
||||
android:background="?selectableItemBackgroundBorderless"
|
||||
android:contentDescription="@string/conversation_activity__emoji_toggle_description" />
|
||||
|
||||
<Space
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="@dimen/conversation_compose_height" />
|
||||
|
||||
<org.thoughtcrime.securesms.components.ComposeText
|
||||
style="@style/ComposeEditText"
|
||||
android:id="@+id/mediasend_compose_text"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center_vertical"
|
||||
android:layout_weight="1"
|
||||
android:nextFocusForward="@+id/send_button"
|
||||
android:nextFocusRight="@+id/send_button"
|
||||
tools:hint="Send TextSecure message" >
|
||||
<requestFocus />
|
||||
</org.thoughtcrime.securesms.components.ComposeText>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<FrameLayout
|
||||
android:id="@+id/mediasend_send_button_bkg"
|
||||
android:layout_width="@dimen/conversation_compose_height"
|
||||
android:layout_height="@dimen/conversation_compose_height"
|
||||
android:layout_marginStart="12dp"
|
||||
android:layout_gravity="bottom"
|
||||
android:background="@drawable/circle_tintable"
|
||||
tools:backgroundTint="@color/core_blue">
|
||||
|
||||
<org.thoughtcrime.securesms.components.SendButton
|
||||
android:id="@+id/mediasend_send_button"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="match_parent"
|
||||
android:paddingTop="6dp"
|
||||
android:paddingEnd="6dp"
|
||||
android:paddingBottom="6dp"
|
||||
android:paddingStart="11dp"
|
||||
android:scaleType="fitCenter"
|
||||
android:contentDescription="@string/conversation_activity__send"
|
||||
android:src="?conversation_transport_sms_indicator"
|
||||
android:background="@drawable/circle_touch_highlight_background" />
|
||||
|
||||
</FrameLayout>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/mediasend_characters_left"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:paddingBottom="12dp"
|
||||
android:visibility="gone"
|
||||
tools:visibility="visible"
|
||||
tools:text="160/160 (1)" />
|
||||
|
||||
<ViewStub
|
||||
android:id="@+id/mediasend_emoji_drawer_stub"
|
||||
android:layout="@layout/scribble_fragment_emojidrawer_stub"
|
||||
android:inflatedId="@+id/emoji_drawer"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"/>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</org.thoughtcrime.securesms.components.InputAwareLayout>
|
||||
|
||||
</FrameLayout>
|
|
@ -5,9 +5,9 @@
|
|||
|
||||
<item
|
||||
android:title=""
|
||||
android:id="@+id/mediapicker_menu_add"
|
||||
android:id="@+id/mediapicker_menu_camera"
|
||||
android:visible="true"
|
||||
android:icon="@drawable/ic_create_album_outline_32"
|
||||
android:icon="@drawable/ic_camera_alt_white_24dp"
|
||||
app:showAsAction="always" />
|
||||
|
||||
</menu>
|
|
@ -46,6 +46,7 @@
|
|||
|
||||
<dimen name="media_picker_folder_width">175dp</dimen>
|
||||
<dimen name="media_picker_item_width">85dp</dimen>
|
||||
<dimen name="media_picker_rail_padding_affordance">130dp</dimen>
|
||||
|
||||
<dimen name="media_keyboard_provider_icon_padding">5dp</dimen>
|
||||
<dimen name="media_keyboard_provider_icon_margin">4dp</dimen>
|
||||
|
|
|
@ -24,11 +24,12 @@ public class MediaRailAdapter extends RecyclerView.Adapter<MediaRailAdapter.Medi
|
|||
private final GlideRequests glideRequests;
|
||||
private final List<Media> media;
|
||||
private final RailItemListener listener;
|
||||
private final boolean editable;
|
||||
private final StableIdGenerator<Media> stableIdGenerator;
|
||||
|
||||
private RailItemAddListener addListener;
|
||||
private int activePosition;
|
||||
private int activePosition;
|
||||
private boolean editable;
|
||||
private boolean interactive;
|
||||
|
||||
public MediaRailAdapter(@NonNull GlideRequests glideRequests, @NonNull RailItemListener listener, boolean editable) {
|
||||
this.glideRequests = glideRequests;
|
||||
|
@ -36,6 +37,7 @@ public class MediaRailAdapter extends RecyclerView.Adapter<MediaRailAdapter.Medi
|
|||
this.listener = listener;
|
||||
this.editable = editable;
|
||||
this.stableIdGenerator = new StableIdGenerator<>();
|
||||
this.interactive = true;
|
||||
|
||||
setHasStableIds(true);
|
||||
}
|
||||
|
@ -57,7 +59,7 @@ public class MediaRailAdapter extends RecyclerView.Adapter<MediaRailAdapter.Medi
|
|||
public void onBindViewHolder(@NonNull MediaRailViewHolder viewHolder, int i) {
|
||||
switch (getItemViewType(i)) {
|
||||
case TYPE_MEDIA:
|
||||
((MediaViewHolder) viewHolder).bind(media.get(i), i == activePosition, glideRequests, listener, i - activePosition, editable);
|
||||
((MediaViewHolder) viewHolder).bind(media.get(i), i == activePosition, glideRequests, listener, i - activePosition, editable, interactive);
|
||||
break;
|
||||
case TYPE_BUTTON:
|
||||
((ButtonViewHolder) viewHolder).bind(addListener);
|
||||
|
@ -121,6 +123,16 @@ public class MediaRailAdapter extends RecyclerView.Adapter<MediaRailAdapter.Medi
|
|||
notifyDataSetChanged();
|
||||
}
|
||||
|
||||
public void setEditable(boolean editable) {
|
||||
this.editable = editable;
|
||||
notifyDataSetChanged();
|
||||
}
|
||||
|
||||
public void setInteractive(boolean interactive) {
|
||||
this.interactive = interactive;
|
||||
notifyDataSetChanged();
|
||||
}
|
||||
|
||||
static abstract class MediaRailViewHolder extends RecyclerView.ViewHolder {
|
||||
public MediaRailViewHolder(@NonNull View itemView) {
|
||||
super(itemView);
|
||||
|
@ -145,16 +157,17 @@ public class MediaRailAdapter extends RecyclerView.Adapter<MediaRailAdapter.Medi
|
|||
}
|
||||
|
||||
void bind(@NonNull Media media, boolean isActive, @NonNull GlideRequests glideRequests,
|
||||
@NonNull RailItemListener railItemListener, int distanceFromActive, boolean editable)
|
||||
@NonNull RailItemListener railItemListener, int distanceFromActive, boolean editable,
|
||||
boolean interactive)
|
||||
{
|
||||
image.setImageResource(glideRequests, media.getUri());
|
||||
image.setOnClickListener(v -> railItemListener.onRailItemClicked(distanceFromActive));
|
||||
|
||||
outline.setVisibility(isActive ? View.VISIBLE : View.GONE);
|
||||
outline.setVisibility(isActive && interactive ? View.VISIBLE : View.GONE);
|
||||
|
||||
captionIndicator.setVisibility(media.getCaption().isPresent() ? View.VISIBLE : View.GONE);
|
||||
|
||||
if (editable && isActive) {
|
||||
if (editable && isActive && interactive) {
|
||||
deleteButton.setVisibility(View.VISIBLE);
|
||||
deleteButton.setOnClickListener(v -> railItemListener.onRailItemDeleteClicked(distanceFromActive));
|
||||
} else {
|
||||
|
|
|
@ -27,7 +27,10 @@ import android.view.animation.DecelerateInterpolator;
|
|||
import android.view.animation.RotateAnimation;
|
||||
import android.widget.Button;
|
||||
import android.widget.ImageButton;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.TextView;
|
||||
|
||||
import com.bumptech.glide.Glide;
|
||||
import com.bumptech.glide.load.MultiTransformation;
|
||||
import com.bumptech.glide.load.Transformation;
|
||||
import com.bumptech.glide.load.resource.bitmap.CenterCrop;
|
||||
|
@ -36,10 +39,13 @@ import com.bumptech.glide.request.transition.Transition;
|
|||
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
import org.thoughtcrime.securesms.mms.DecryptableStreamUriLoader;
|
||||
import org.thoughtcrime.securesms.mms.DecryptableStreamUriLoader.DecryptableUri;
|
||||
import org.thoughtcrime.securesms.mms.GlideApp;
|
||||
import org.thoughtcrime.securesms.util.ServiceUtil;
|
||||
import org.thoughtcrime.securesms.util.Stopwatch;
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||
import org.whispersystems.libsignal.util.guava.Optional;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
|
||||
|
@ -106,6 +112,9 @@ public class Camera1Fragment extends Fragment implements CameraFragment,
|
|||
|
||||
GestureDetector gestureDetector = new GestureDetector(flipGestureListener);
|
||||
cameraPreview.setOnTouchListener((v, event) -> gestureDetector.onTouchEvent(event));
|
||||
|
||||
viewModel.getMostRecentMediaItem(requireContext()).observe(this, this::presentRecentItemThumbnail);
|
||||
viewModel.getHudState().observe(this, this::presentHud);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -128,9 +137,6 @@ public class Camera1Fragment extends Fragment implements CameraFragment,
|
|||
});
|
||||
|
||||
orderEnforcer.run(Stage.CAMERA_PROPERTIES_AVAILABLE, this::updatePreviewScale);
|
||||
|
||||
requireActivity().getWindow().addFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN);
|
||||
requireActivity().getWindow().clearFlags(WindowManager.LayoutParams.FLAG_FORCE_NOT_FULLSCREEN);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -180,10 +186,46 @@ public class Camera1Fragment extends Fragment implements CameraFragment,
|
|||
controller.onCameraError();
|
||||
}
|
||||
|
||||
private void presentRecentItemThumbnail(Optional<Media> media) {
|
||||
if (media == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
ImageView thumbnail = controlsContainer.findViewById(R.id.camera_gallery_button);
|
||||
|
||||
if (media.isPresent()) {
|
||||
thumbnail.setVisibility(View.VISIBLE);
|
||||
Glide.with(this)
|
||||
.load(new DecryptableUri(media.get().getUri()))
|
||||
.centerCrop()
|
||||
.into(thumbnail);
|
||||
} else {
|
||||
thumbnail.setVisibility(View.GONE);
|
||||
thumbnail.setImageResource(0);
|
||||
}
|
||||
}
|
||||
|
||||
private void presentHud(@Nullable MediaSendViewModel.HudState state) {
|
||||
if (state == null) return;
|
||||
|
||||
View countButton = controlsContainer.findViewById(R.id.camera_count_button);
|
||||
TextView countButtonText = controlsContainer.findViewById(R.id.mediasend_count_button_text);
|
||||
|
||||
if (state.getButtonState() == MediaSendViewModel.ButtonState.COUNT) {
|
||||
countButton.setVisibility(View.VISIBLE);
|
||||
countButtonText.setText(String.valueOf(state.getSelectionCount()));
|
||||
} else {
|
||||
countButton.setVisibility(View.GONE);
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressLint("ClickableViewAccessibility")
|
||||
private void initControls() {
|
||||
flipButton = getView().findViewById(R.id.camera_flip_button);
|
||||
captureButton = getView().findViewById(R.id.camera_capture_button);
|
||||
flipButton = requireView().findViewById(R.id.camera_flip_button);
|
||||
captureButton = requireView().findViewById(R.id.camera_capture_button);
|
||||
|
||||
View galleryButton = requireView().findViewById(R.id.camera_gallery_button);
|
||||
View countButton = requireView().findViewById(R.id.camera_count_button);
|
||||
|
||||
captureButton.setOnTouchListener((v, event) -> {
|
||||
switch (event.getAction()) {
|
||||
|
@ -223,6 +265,11 @@ public class Camera1Fragment extends Fragment implements CameraFragment,
|
|||
flipButton.setVisibility(View.GONE);
|
||||
}
|
||||
});
|
||||
|
||||
galleryButton.setOnClickListener(v -> controller.onGalleryClicked());
|
||||
countButton.setOnClickListener(v -> controller.onContinueClicked());
|
||||
|
||||
viewModel.onCameraControlsInitialized();
|
||||
}
|
||||
|
||||
private void onCaptureClicked() {
|
||||
|
|
|
@ -21,6 +21,8 @@ public interface CameraFragment {
|
|||
interface Controller {
|
||||
void onCameraError();
|
||||
void onImageCaptured(@NonNull byte[] data, int width, int height);
|
||||
void onGalleryClicked();
|
||||
int getDisplayRotation();
|
||||
void onContinueClicked();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,6 +4,7 @@ import android.annotation.SuppressLint;
|
|||
import android.content.Context;
|
||||
import android.content.res.Configuration;
|
||||
import android.os.Bundle;
|
||||
import android.view.GestureDetector;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.MotionEvent;
|
||||
import android.view.View;
|
||||
|
@ -13,6 +14,8 @@ import android.view.animation.Animation;
|
|||
import android.view.animation.AnimationUtils;
|
||||
import android.view.animation.DecelerateInterpolator;
|
||||
import android.view.animation.RotateAnimation;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
@ -23,13 +26,17 @@ import androidx.camera.core.ImageProxy;
|
|||
import androidx.fragment.app.Fragment;
|
||||
import androidx.lifecycle.ViewModelProviders;
|
||||
|
||||
import com.bumptech.glide.Glide;
|
||||
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
import org.thoughtcrime.securesms.mediasend.camerax.CameraXUtil;
|
||||
import org.thoughtcrime.securesms.mediasend.camerax.CameraXView;
|
||||
import org.thoughtcrime.securesms.mms.DecryptableStreamUriLoader.DecryptableUri;
|
||||
import org.thoughtcrime.securesms.util.Stopwatch;
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||
import org.thoughtcrime.securesms.util.concurrent.SimpleTask;
|
||||
import org.whispersystems.libsignal.util.guava.Optional;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
|
@ -84,6 +91,9 @@ public class CameraXFragment extends Fragment implements CameraFragment {
|
|||
camera.setCameraLensFacing(CameraXUtil.toLensFacing(TextSecurePreferences.getDirectCaptureCameraId(requireContext())));
|
||||
|
||||
onOrientationChanged(getResources().getConfiguration().orientation);
|
||||
|
||||
viewModel.getMostRecentMediaItem(requireContext()).observe(this, this::presentRecentItemThumbnail);
|
||||
viewModel.getHudState().observe(this, this::presentHud);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -116,10 +126,45 @@ public class CameraXFragment extends Fragment implements CameraFragment {
|
|||
initControls();
|
||||
}
|
||||
|
||||
private void presentRecentItemThumbnail(Optional<Media> media) {
|
||||
if (media == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
ImageView thumbnail = controlsContainer.findViewById(R.id.camera_gallery_button);
|
||||
|
||||
if (media.isPresent()) {
|
||||
thumbnail.setVisibility(View.VISIBLE);
|
||||
Glide.with(this)
|
||||
.load(new DecryptableUri(media.get().getUri()))
|
||||
.centerCrop()
|
||||
.into(thumbnail);
|
||||
} else {
|
||||
thumbnail.setVisibility(View.GONE);
|
||||
thumbnail.setImageResource(0);
|
||||
}
|
||||
}
|
||||
|
||||
private void presentHud(@Nullable MediaSendViewModel.HudState state) {
|
||||
if (state == null) return;
|
||||
|
||||
View countButton = controlsContainer.findViewById(R.id.camera_count_button);
|
||||
TextView countButtonText = controlsContainer.findViewById(R.id.mediasend_count_button_text);
|
||||
|
||||
if (state.getButtonState() == MediaSendViewModel.ButtonState.COUNT) {
|
||||
countButton.setVisibility(View.VISIBLE);
|
||||
countButtonText.setText(String.valueOf(state.getSelectionCount()));
|
||||
} else {
|
||||
countButton.setVisibility(View.GONE);
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressLint({"ClickableViewAccessibility", "MissingPermission"})
|
||||
private void initControls() {
|
||||
View flipButton = requireView().findViewById(R.id.camera_flip_button);
|
||||
View captureButton = requireView().findViewById(R.id.camera_capture_button);
|
||||
View galleryButton = requireView().findViewById(R.id.camera_gallery_button);
|
||||
View countButton = requireView().findViewById(R.id.camera_count_button);
|
||||
|
||||
captureButton.setOnTouchListener((v, event) -> {
|
||||
switch (event.getAction()) {
|
||||
|
@ -154,9 +199,26 @@ public class CameraXFragment extends Fragment implements CameraFragment {
|
|||
animation.setInterpolator(new DecelerateInterpolator());
|
||||
flipButton.startAnimation(animation);
|
||||
});
|
||||
|
||||
GestureDetector gestureDetector = new GestureDetector(requireContext(), new GestureDetector.SimpleOnGestureListener() {
|
||||
@Override
|
||||
public boolean onDoubleTap(MotionEvent e) {
|
||||
flipButton.performClick();
|
||||
return true;
|
||||
}
|
||||
});
|
||||
|
||||
camera.setOnTouchListener((v, event) -> gestureDetector.onTouchEvent(event));
|
||||
|
||||
|
||||
} else {
|
||||
flipButton.setVisibility(View.GONE);
|
||||
}
|
||||
|
||||
galleryButton.setOnClickListener(v -> controller.onGalleryClicked());
|
||||
countButton.setOnClickListener(v -> controller.onContinueClicked());
|
||||
|
||||
viewModel.onCameraControlsInitialized();
|
||||
}
|
||||
|
||||
private void onCaptureClicked() {
|
||||
|
|
|
@ -13,6 +13,8 @@ import androidx.recyclerview.widget.GridLayoutManager;
|
|||
import androidx.recyclerview.widget.RecyclerView;
|
||||
import androidx.appcompat.widget.Toolbar;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.view.WindowManager;
|
||||
|
@ -51,6 +53,8 @@ public class MediaPickerFolderFragment extends Fragment implements MediaPickerFo
|
|||
@Override
|
||||
public void onCreate(@Nullable Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
setHasOptionsMenu(true);
|
||||
|
||||
recipientName = getArguments().getString(KEY_RECIPIENT_NAME);
|
||||
viewModel = ViewModelProviders.of(requireActivity(), new MediaSendViewModel.Factory(requireActivity().getApplication(), new MediaRepository())).get(MediaSendViewModel.class);
|
||||
}
|
||||
|
@ -92,14 +96,26 @@ public class MediaPickerFolderFragment extends Fragment implements MediaPickerFo
|
|||
@Override
|
||||
public void onResume() {
|
||||
super.onResume();
|
||||
|
||||
viewModel.onFolderPickerStarted();
|
||||
requireActivity().getWindow().addFlags(WindowManager.LayoutParams.FLAG_FORCE_NOT_FULLSCREEN);
|
||||
requireActivity().getWindow().clearFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onConfigurationChanged(Configuration newConfig) {
|
||||
public void onPrepareOptionsMenu(@NonNull Menu menu) {
|
||||
requireActivity().getMenuInflater().inflate(R.menu.mediapicker_default, menu);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onOptionsItemSelected(@NonNull MenuItem item) {
|
||||
switch (item.getItemId()) {
|
||||
case R.id.mediapicker_menu_camera:
|
||||
controller.onCameraSelected();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onConfigurationChanged(@NonNull Configuration newConfig) {
|
||||
super.onConfigurationChanged(newConfig);
|
||||
onScreenWidthChanged(getScreenWidth());
|
||||
}
|
||||
|
@ -131,5 +147,6 @@ public class MediaPickerFolderFragment extends Fragment implements MediaPickerFo
|
|||
|
||||
public interface Controller {
|
||||
void onFolderSelected(@NonNull MediaFolder folder);
|
||||
void onCameraSelected();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -104,41 +104,33 @@ public class MediaPickerItemFragment extends Fragment implements MediaPickerItem
|
|||
}
|
||||
|
||||
viewModel.getMediaInBucket(requireContext(), bucketId).observe(this, adapter::setMedia);
|
||||
|
||||
initMediaObserver(viewModel);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onResume() {
|
||||
super.onResume();
|
||||
|
||||
viewModel.onItemPickerStarted();
|
||||
requireActivity().getWindow().addFlags(WindowManager.LayoutParams.FLAG_FORCE_NOT_FULLSCREEN);
|
||||
requireActivity().getWindow().clearFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN);
|
||||
adapter.setForcedMultiSelect(true);
|
||||
viewModel.onMultiSelectStarted();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPrepareOptionsMenu(Menu menu) {
|
||||
public void onPrepareOptionsMenu(@NonNull Menu menu) {
|
||||
requireActivity().getMenuInflater().inflate(R.menu.mediapicker_default, menu);
|
||||
|
||||
if (viewModel.getCountButtonState().getValue() != null && viewModel.getCountButtonState().getValue().isVisible()) {
|
||||
menu.findItem(R.id.mediapicker_menu_add).setVisible(false);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onOptionsItemSelected(MenuItem item) {
|
||||
public boolean onOptionsItemSelected(@NonNull MenuItem item) {
|
||||
switch (item.getItemId()) {
|
||||
case R.id.mediapicker_menu_add:
|
||||
adapter.setForcedMultiSelect(true);
|
||||
viewModel.onMultiSelectStarted();
|
||||
case R.id.mediapicker_menu_camera:
|
||||
controller.onCameraSelected();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onConfigurationChanged(Configuration newConfig) {
|
||||
public void onConfigurationChanged(@NonNull Configuration newConfig) {
|
||||
super.onConfigurationChanged(newConfig);
|
||||
onScreenWidthChanged(getScreenWidth());
|
||||
}
|
||||
|
@ -172,12 +164,6 @@ public class MediaPickerItemFragment extends Fragment implements MediaPickerItem
|
|||
toolbar.setNavigationOnClickListener(v -> requireActivity().onBackPressed());
|
||||
}
|
||||
|
||||
private void initMediaObserver(@NonNull MediaSendViewModel viewModel) {
|
||||
viewModel.getCountButtonState().observe(this, media -> {
|
||||
requireActivity().invalidateOptionsMenu();
|
||||
});
|
||||
}
|
||||
|
||||
private void onScreenWidthChanged(int newWidth) {
|
||||
if (layoutManager != null) {
|
||||
layoutManager.setSpanCount(newWidth / getResources().getDimensionPixelSize(R.dimen.media_picker_item_width));
|
||||
|
@ -192,5 +178,6 @@ public class MediaPickerItemFragment extends Fragment implements MediaPickerItem
|
|||
|
||||
public interface Controller {
|
||||
void onMediaSelected(@NonNull Media media);
|
||||
void onCameraSelected();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,7 +4,6 @@ import android.annotation.TargetApi;
|
|||
import android.content.Context;
|
||||
import android.database.Cursor;
|
||||
import android.net.Uri;
|
||||
import android.os.AsyncTask;
|
||||
import android.os.Environment;
|
||||
import android.provider.MediaStore.Images;
|
||||
import android.provider.MediaStore.Video;
|
||||
|
@ -20,6 +19,7 @@ import org.thoughtcrime.securesms.R;
|
|||
import org.thoughtcrime.securesms.mms.PartAuthority;
|
||||
import org.thoughtcrime.securesms.util.MediaUtil;
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
import org.thoughtcrime.securesms.util.concurrent.SignalExecutors;
|
||||
import org.whispersystems.libsignal.util.guava.Optional;
|
||||
|
||||
import java.io.File;
|
||||
|
@ -40,14 +40,14 @@ class MediaRepository {
|
|||
* Retrieves a list of folders that contain media.
|
||||
*/
|
||||
void getFolders(@NonNull Context context, @NonNull Callback<List<MediaFolder>> callback) {
|
||||
AsyncTask.THREAD_POOL_EXECUTOR.execute(() -> callback.onComplete(getFolders(context)));
|
||||
SignalExecutors.BOUNDED.execute(() -> callback.onComplete(getFolders(context)));
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves a list of media items (images and videos) that are present int he specified bucket.
|
||||
*/
|
||||
void getMediaInBucket(@NonNull Context context, @NonNull String bucketId, @NonNull Callback<List<Media>> callback) {
|
||||
AsyncTask.THREAD_POOL_EXECUTOR.execute(() -> callback.onComplete(getMediaInBucket(context, bucketId)));
|
||||
SignalExecutors.BOUNDED.execute(() -> callback.onComplete(getMediaInBucket(context, bucketId)));
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -60,7 +60,11 @@ class MediaRepository {
|
|||
return;
|
||||
}
|
||||
|
||||
AsyncTask.THREAD_POOL_EXECUTOR.execute(() -> callback.onComplete(getPopulatedMedia(context, media)));
|
||||
SignalExecutors.BOUNDED.execute(() -> callback.onComplete(getPopulatedMedia(context, media)));
|
||||
}
|
||||
|
||||
void getMostRecentItem(@NonNull Context context, @NonNull Callback<Optional<Media>> callback) {
|
||||
SignalExecutors.BOUNDED.execute(() -> callback.onComplete(getMostRecentItem(context)));
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
|
@ -158,7 +162,7 @@ class MediaRepository {
|
|||
}
|
||||
|
||||
@WorkerThread
|
||||
private @NonNull List<Media> getMediaInBucket(@NonNull Context context, @NonNull String bucketId, @NonNull Uri contentUri, boolean hasOrienation) {
|
||||
private @NonNull List<Media> getMediaInBucket(@NonNull Context context, @NonNull String bucketId, @NonNull Uri contentUri, boolean hasOrientation) {
|
||||
List<Media> media = new LinkedList<>();
|
||||
String selection = Images.Media.BUCKET_ID + " = ? AND " + Images.Media.DATA + " NOT NULL";
|
||||
String[] selectionArgs = new String[] { bucketId };
|
||||
|
@ -166,7 +170,7 @@ class MediaRepository {
|
|||
|
||||
String[] projection;
|
||||
|
||||
if (hasOrienation) {
|
||||
if (hasOrientation) {
|
||||
projection = new String[]{Images.Media._ID, Images.Media.MIME_TYPE, Images.Media.DATE_TAKEN, Images.Media.ORIENTATION, Images.Media.WIDTH, Images.Media.HEIGHT, Images.Media.SIZE};
|
||||
} else {
|
||||
projection = new String[]{Images.Media._ID, Images.Media.MIME_TYPE, Images.Media.DATE_TAKEN, Images.Media.WIDTH, Images.Media.HEIGHT, Images.Media.SIZE};
|
||||
|
@ -182,7 +186,7 @@ class MediaRepository {
|
|||
Uri uri = Uri.withAppendedPath(contentUri, cursor.getString(cursor.getColumnIndexOrThrow(Images.Media._ID)));
|
||||
String mimetype = cursor.getString(cursor.getColumnIndexOrThrow(Images.Media.MIME_TYPE));
|
||||
long dateTaken = cursor.getLong(cursor.getColumnIndexOrThrow(Images.Media.DATE_TAKEN));
|
||||
int orientation = hasOrienation ? cursor.getInt(cursor.getColumnIndexOrThrow(Images.Media.ORIENTATION)) : 0;
|
||||
int orientation = hasOrientation ? cursor.getInt(cursor.getColumnIndexOrThrow(Images.Media.ORIENTATION)) : 0;
|
||||
int width = cursor.getInt(cursor.getColumnIndexOrThrow(getWidthColumn(orientation)));
|
||||
int height = cursor.getInt(cursor.getColumnIndexOrThrow(getHeightColumn(orientation)));
|
||||
long size = cursor.getLong(cursor.getColumnIndexOrThrow(Images.Media.SIZE));
|
||||
|
@ -211,6 +215,12 @@ class MediaRepository {
|
|||
}).toList();
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
private Optional<Media> getMostRecentItem(@NonNull Context context) {
|
||||
List<Media> media = getMediaInBucket(context, Media.ALL_MEDIA_BUCKET_ID, Images.Media.EXTERNAL_CONTENT_URI, true);
|
||||
return media.size() > 0 ? Optional.of(media.get(0)) : Optional.absent();
|
||||
}
|
||||
|
||||
@TargetApi(16)
|
||||
@SuppressWarnings("SuspiciousNameCombination")
|
||||
private String getWidthColumn(int orientation) {
|
||||
|
|
|
@ -1,45 +1,76 @@
|
|||
package org.thoughtcrime.securesms.mediasend;
|
||||
|
||||
import android.Manifest;
|
||||
|
||||
import androidx.fragment.app.FragmentTransaction;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
import androidx.appcompat.view.ContextThemeWrapper;
|
||||
import androidx.lifecycle.ViewModelProviders;
|
||||
|
||||
import android.Manifest;
|
||||
import android.annotation.SuppressLint;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.PorterDuff;
|
||||
import android.graphics.Rect;
|
||||
import android.net.Uri;
|
||||
import android.os.AsyncTask;
|
||||
import android.os.Bundle;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import androidx.fragment.app.FragmentManager;
|
||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import android.text.Editable;
|
||||
import android.text.TextWatcher;
|
||||
import android.view.KeyEvent;
|
||||
import android.view.View;
|
||||
import android.view.animation.AccelerateDecelerateInterpolator;
|
||||
import android.view.animation.AccelerateInterpolator;
|
||||
import android.view.animation.Animation;
|
||||
import android.view.animation.DecelerateInterpolator;
|
||||
import android.view.animation.OvershootInterpolator;
|
||||
import android.view.animation.ScaleAnimation;
|
||||
import android.view.ViewGroup;
|
||||
import android.view.ViewTreeObserver;
|
||||
import android.view.inputmethod.EditorInfo;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.TextView;
|
||||
import android.widget.Toast;
|
||||
|
||||
import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity;
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.TransportOption;
|
||||
import org.thoughtcrime.securesms.components.ComposeText;
|
||||
import org.thoughtcrime.securesms.components.InputAwareLayout;
|
||||
import org.thoughtcrime.securesms.components.SendButton;
|
||||
import org.thoughtcrime.securesms.components.emoji.EmojiEditText;
|
||||
import org.thoughtcrime.securesms.components.emoji.EmojiKeyboardProvider;
|
||||
import org.thoughtcrime.securesms.components.emoji.EmojiToggle;
|
||||
import org.thoughtcrime.securesms.components.emoji.MediaKeyboard;
|
||||
import org.thoughtcrime.securesms.contactshare.SimpleTextWatcher;
|
||||
import org.thoughtcrime.securesms.database.Address;
|
||||
import org.thoughtcrime.securesms.imageeditor.model.EditorModel;
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
import org.thoughtcrime.securesms.mediapreview.MediaRailAdapter;
|
||||
import org.thoughtcrime.securesms.mediasend.MediaSendViewModel.TimerState;
|
||||
import org.thoughtcrime.securesms.mms.GlideApp;
|
||||
import org.thoughtcrime.securesms.permissions.Permissions;
|
||||
import org.thoughtcrime.securesms.providers.BlobProvider;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.scribbles.ImageEditorFragment;
|
||||
import org.thoughtcrime.securesms.util.CharacterCalculator.CharacterState;
|
||||
import org.thoughtcrime.securesms.util.DynamicLanguage;
|
||||
import org.thoughtcrime.securesms.util.MediaUtil;
|
||||
import org.thoughtcrime.securesms.util.Stopwatch;
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
import org.thoughtcrime.securesms.util.concurrent.SignalExecutors;
|
||||
import org.thoughtcrime.securesms.util.concurrent.SimpleTask;
|
||||
import org.thoughtcrime.securesms.util.views.Stub;
|
||||
import org.whispersystems.libsignal.util.guava.Optional;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Encompasses the entire flow of sending media, starting from the selection process to the actual
|
||||
|
@ -50,15 +81,19 @@ import java.util.Locale;
|
|||
*/
|
||||
public class MediaSendActivity extends PassphraseRequiredActionBarActivity implements MediaPickerFolderFragment.Controller,
|
||||
MediaPickerItemFragment.Controller,
|
||||
MediaSendFragment.Controller,
|
||||
ImageEditorFragment.Controller,
|
||||
CameraFragment.Controller
|
||||
CameraFragment.Controller,
|
||||
ViewTreeObserver.OnGlobalLayoutListener,
|
||||
MediaRailAdapter.RailItemListener,
|
||||
InputAwareLayout.OnKeyboardShownListener,
|
||||
InputAwareLayout.OnKeyboardHiddenListener
|
||||
{
|
||||
private static final String TAG = MediaSendActivity.class.getSimpleName();
|
||||
|
||||
public static final String EXTRA_MEDIA = "media";
|
||||
public static final String EXTRA_MESSAGE = "message";
|
||||
public static final String EXTRA_TRANSPORT = "transport";
|
||||
public static final String EXTRA_MEDIA = "media";
|
||||
public static final String EXTRA_MESSAGE = "message";
|
||||
public static final String EXTRA_TRANSPORT = "transport";
|
||||
public static final String EXTRA_REVEAL_DURATION = "reveal_duration";
|
||||
|
||||
|
||||
private static final String KEY_ADDRESS = "address";
|
||||
|
@ -78,9 +113,25 @@ public class MediaSendActivity extends PassphraseRequiredActionBarActivity imple
|
|||
private TransportOption transport;
|
||||
private MediaSendViewModel viewModel;
|
||||
|
||||
private View countButton;
|
||||
private TextView countButtonText;
|
||||
private View cameraButton;
|
||||
private InputAwareLayout hud;
|
||||
private View captionAndRail;
|
||||
private SendButton sendButton;
|
||||
private ViewGroup sendButtonContainer;
|
||||
private ComposeText composeText;
|
||||
private ViewGroup composeRow;
|
||||
private ViewGroup composeContainer;
|
||||
private ViewGroup countButton;
|
||||
private TextView countButtonText;
|
||||
private EmojiEditText captionText;
|
||||
private EmojiToggle emojiToggle;
|
||||
private Stub<MediaKeyboard> emojiDrawer;
|
||||
private TextView charactersLeft;
|
||||
private RecyclerView mediaRail;
|
||||
private MediaRailAdapter mediaRailAdapter;
|
||||
|
||||
private int visibleHeight;
|
||||
|
||||
private final Rect visibleBounds = new Rect();
|
||||
|
||||
/**
|
||||
* Get an intent to launch the media send flow starting with the picker.
|
||||
|
@ -131,15 +182,27 @@ public class MediaSendActivity extends PassphraseRequiredActionBarActivity imple
|
|||
return;
|
||||
}
|
||||
|
||||
countButton = findViewById(R.id.mediasend_count_button);
|
||||
countButtonText = findViewById(R.id.mediasend_count_button_text);
|
||||
cameraButton = findViewById(R.id.mediasend_camera_button);
|
||||
hud = findViewById(R.id.mediasend_hud);
|
||||
captionAndRail = findViewById(R.id.mediasend_caption_and_rail);
|
||||
sendButton = findViewById(R.id.mediasend_send_button);
|
||||
sendButtonContainer = findViewById(R.id.mediasend_send_button_bkg);
|
||||
composeText = findViewById(R.id.mediasend_compose_text);
|
||||
composeRow = findViewById(R.id.mediasend_compose_row);
|
||||
composeContainer = findViewById(R.id.mediasend_compose_container);
|
||||
countButton = findViewById(R.id.mediasend_count_button);
|
||||
countButtonText = findViewById(R.id.mediasend_count_button_text);
|
||||
captionText = findViewById(R.id.mediasend_caption);
|
||||
emojiToggle = findViewById(R.id.mediasend_emoji_toggle);
|
||||
charactersLeft = findViewById(R.id.mediasend_characters_left);
|
||||
mediaRail = findViewById(R.id.mediasend_media_rail);
|
||||
emojiDrawer = new Stub<>(findViewById(R.id.mediasend_emoji_drawer_stub));
|
||||
|
||||
viewModel = ViewModelProviders.of(this, new MediaSendViewModel.Factory(getApplication(), new MediaRepository())).get(MediaSendViewModel.class);
|
||||
recipient = Recipient.from(this, Address.fromSerialized(getIntent().getStringExtra(KEY_ADDRESS)), true);
|
||||
transport = getIntent().getParcelableExtra(KEY_TRANSPORT);
|
||||
|
||||
viewModel.setTransport(transport);
|
||||
viewModel.setRecipient(recipient);
|
||||
viewModel.onBodyChanged(getIntent().getStringExtra(KEY_BODY));
|
||||
|
||||
List<Media> media = getIntent().getParcelableArrayListExtra(KEY_MEDIA);
|
||||
|
@ -165,19 +228,80 @@ public class MediaSendActivity extends PassphraseRequiredActionBarActivity imple
|
|||
.commit();
|
||||
}
|
||||
|
||||
initializeCountButtonObserver(transport, dynamicLanguage.getCurrentLocale());
|
||||
initializeCameraButtonObserver();
|
||||
initializeErrorObserver();
|
||||
sendButton.setOnClickListener(v -> {
|
||||
if (hud.isKeyboardOpen()) {
|
||||
hud.hideSoftkey(composeText, null);
|
||||
}
|
||||
|
||||
cameraButton.setOnClickListener(v -> {
|
||||
int maxSelection = viewModel.getMaxSelection();
|
||||
MediaSendFragment fragment = getMediaSendFragment();
|
||||
|
||||
if (viewModel.getSelectedMedia().getValue() != null && viewModel.getSelectedMedia().getValue().size() >= maxSelection) {
|
||||
Toast.makeText(this, getResources().getQuantityString(R.plurals.MediaSendActivity_cant_share_more_than_n_items, maxSelection, maxSelection), Toast.LENGTH_SHORT).show();
|
||||
if (fragment != null) {
|
||||
processMedia(fragment.getAllMedia(), fragment.getSavedState());
|
||||
} else {
|
||||
navigateToCamera();
|
||||
throw new AssertionError("No send fragment available!");
|
||||
}
|
||||
});
|
||||
|
||||
sendButton.addOnTransportChangedListener((newTransport, manuallySelected) -> {
|
||||
presentCharactersRemaining();
|
||||
composeText.setTransport(newTransport);
|
||||
sendButtonContainer.getBackground().setColorFilter(newTransport.getBackgroundColor(), PorterDuff.Mode.MULTIPLY);
|
||||
sendButtonContainer.getBackground().invalidateSelf();
|
||||
});
|
||||
|
||||
ComposeKeyPressedListener composeKeyPressedListener = new ComposeKeyPressedListener();
|
||||
|
||||
composeText.setOnKeyListener(composeKeyPressedListener);
|
||||
composeText.addTextChangedListener(composeKeyPressedListener);
|
||||
composeText.setOnClickListener(composeKeyPressedListener);
|
||||
composeText.setOnFocusChangeListener(composeKeyPressedListener);
|
||||
|
||||
captionText.clearFocus();
|
||||
composeText.requestFocus();
|
||||
|
||||
mediaRailAdapter = new MediaRailAdapter(GlideApp.with(this), this, true);
|
||||
mediaRail.setLayoutManager(new LinearLayoutManager(this, LinearLayoutManager.HORIZONTAL, false));
|
||||
mediaRail.setAdapter(mediaRailAdapter);
|
||||
|
||||
hud.getRootView().getViewTreeObserver().addOnGlobalLayoutListener(this);
|
||||
hud.addOnKeyboardShownListener(this);
|
||||
hud.addOnKeyboardHiddenListener(this);
|
||||
|
||||
captionText.addTextChangedListener(new SimpleTextWatcher() {
|
||||
@Override
|
||||
public void onTextChanged(String text) {
|
||||
viewModel.onCaptionChanged(text);
|
||||
}
|
||||
});
|
||||
|
||||
sendButton.setTransport(transport);
|
||||
sendButton.disableTransport(transport.getType() == TransportOption.Type.SMS ? TransportOption.Type.TEXTSECURE : TransportOption.Type.SMS);
|
||||
|
||||
countButton.setOnClickListener(v -> navigateToMediaSend(recipient, transport, dynamicLanguage.getCurrentLocale()));
|
||||
|
||||
composeText.append(viewModel.getBody());
|
||||
|
||||
if (recipient.isLocalNumber()) {
|
||||
composeText.setHint(getString(R.string.note_to_self), null);
|
||||
} else {
|
||||
String displayName = Optional.fromNullable(recipient.getName())
|
||||
.or(Optional.fromNullable(recipient.getProfileName())
|
||||
.or(recipient.getAddress().serialize()));
|
||||
composeText.setHint(getString(R.string.MediaSendActivity_message_to_s, displayName), null);
|
||||
}
|
||||
composeText.setOnEditorActionListener((v, actionId, event) -> {
|
||||
boolean isSend = actionId == EditorInfo.IME_ACTION_SEND;
|
||||
if (isSend) sendButton.performClick();
|
||||
return isSend;
|
||||
});
|
||||
|
||||
if (TextSecurePreferences.isSystemEmojiPreferred(this)) {
|
||||
emojiToggle.setVisibility(View.GONE);
|
||||
} else {
|
||||
emojiToggle.setOnClickListener(this::onEmojiToggleClicked);
|
||||
}
|
||||
|
||||
initViewModel();
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -188,13 +312,12 @@ public class MediaSendActivity extends PassphraseRequiredActionBarActivity imple
|
|||
|
||||
@Override
|
||||
public void onBackPressed() {
|
||||
MediaSendFragment sendFragment = (MediaSendFragment) getSupportFragmentManager().findFragmentByTag(TAG_SEND);
|
||||
if (sendFragment == null || !sendFragment.isVisible() || !sendFragment.handleBackPress()) {
|
||||
super.onBackPressed();
|
||||
MediaSendFragment sendFragment = (MediaSendFragment) getSupportFragmentManager().findFragmentByTag(TAG_SEND);
|
||||
|
||||
if (getIntent().getBooleanExtra(KEY_IS_CAMERA, false) && getSupportFragmentManager().getBackStackEntryCount() == 0) {
|
||||
viewModel.onImageCaptureUndo(this);
|
||||
}
|
||||
if (sendFragment == null || !sendFragment.isVisible() || !hud.isInputOpen()) {
|
||||
super.onBackPressed();
|
||||
} else {
|
||||
hud.hideCurrentInput(composeText);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -209,7 +332,6 @@ public class MediaSendActivity extends PassphraseRequiredActionBarActivity imple
|
|||
|
||||
MediaPickerItemFragment fragment = MediaPickerItemFragment.newInstance(folder.getBucketId(), folder.getTitle(), viewModel.getMaxSelection());
|
||||
getSupportFragmentManager().beginTransaction()
|
||||
.setCustomAnimations(R.anim.slide_from_right, R.anim.slide_to_left, R.anim.slide_from_left, R.anim.slide_to_right)
|
||||
.replace(R.id.mediasend_fragment_container, fragment, TAG_ITEM_PICKER)
|
||||
.addToBackStack(null)
|
||||
.commit();
|
||||
|
@ -218,48 +340,7 @@ public class MediaSendActivity extends PassphraseRequiredActionBarActivity imple
|
|||
@Override
|
||||
public void onMediaSelected(@NonNull Media media) {
|
||||
viewModel.onSingleMediaSelected(this, media);
|
||||
navigateToMediaSend(recipient, transport, dynamicLanguage.getCurrentLocale(), false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAddMediaClicked(@NonNull String bucketId) {
|
||||
// TODO: Get actual folder title somehow
|
||||
MediaPickerFolderFragment folderFragment = MediaPickerFolderFragment.newInstance(recipient);
|
||||
MediaPickerItemFragment itemFragment = MediaPickerItemFragment.newInstance(bucketId, "", viewModel.getMaxSelection());
|
||||
|
||||
getSupportFragmentManager().beginTransaction()
|
||||
.setCustomAnimations(R.anim.stationary, R.anim.slide_to_left, R.anim.slide_from_left, R.anim.slide_to_right)
|
||||
.replace(R.id.mediasend_fragment_container, folderFragment, TAG_FOLDER_PICKER)
|
||||
.addToBackStack(null)
|
||||
.commit();
|
||||
|
||||
getSupportFragmentManager().beginTransaction()
|
||||
.setCustomAnimations(R.anim.slide_from_right, R.anim.stationary, R.anim.slide_from_left, R.anim.slide_to_right)
|
||||
.replace(R.id.mediasend_fragment_container, itemFragment, TAG_ITEM_PICKER)
|
||||
.addToBackStack(null)
|
||||
.commit();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSendClicked(@NonNull List<Media> media, @NonNull String message, @NonNull TransportOption transport) {
|
||||
viewModel.onSendClicked();
|
||||
|
||||
ArrayList<Media> mediaList = new ArrayList<>(media);
|
||||
Intent intent = new Intent();
|
||||
|
||||
intent.putParcelableArrayListExtra(EXTRA_MEDIA, mediaList);
|
||||
intent.putExtra(EXTRA_MESSAGE, message);
|
||||
intent.putExtra(EXTRA_TRANSPORT, transport);
|
||||
setResult(RESULT_OK, intent);
|
||||
finish();
|
||||
|
||||
overridePendingTransition(R.anim.stationary, R.anim.camera_slide_to_bottom);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onNoMediaAvailable() {
|
||||
setResult(RESULT_CANCELED);
|
||||
finish();
|
||||
navigateToMediaSend(recipient, transport, dynamicLanguage.getCurrentLocale());
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -307,7 +388,7 @@ public class MediaSendActivity extends PassphraseRequiredActionBarActivity imple
|
|||
Log.i(TAG, "Camera capture stored: " + media.getUri().toString());
|
||||
|
||||
viewModel.onImageCaptured(media);
|
||||
navigateToMediaSend(recipient, transport, dynamicLanguage.getCurrentLocale(), true);
|
||||
navigateToMediaSend(recipient, transport, dynamicLanguage.getCurrentLocale());
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -316,37 +397,203 @@ public class MediaSendActivity extends PassphraseRequiredActionBarActivity imple
|
|||
return getWindowManager().getDefaultDisplay().getRotation();
|
||||
}
|
||||
|
||||
private void initializeCountButtonObserver(@NonNull TransportOption transport, @NonNull Locale locale) {
|
||||
viewModel.getCountButtonState().observe(this, buttonState -> {
|
||||
if (buttonState == null) return;
|
||||
@Override
|
||||
public void onContinueClicked() {
|
||||
navigateToMediaSend(recipient, transport, dynamicLanguage.getCurrentLocale());
|
||||
}
|
||||
|
||||
countButtonText.setText(String.valueOf(buttonState.getCount()));
|
||||
countButton.setEnabled(buttonState.isVisible());
|
||||
animateButtonVisibility(countButton, countButton.getVisibility(), buttonState.isVisible() ? View.VISIBLE : View.GONE);
|
||||
@Override
|
||||
public void onGalleryClicked() {
|
||||
MediaPickerFolderFragment folderFragment = MediaPickerFolderFragment.newInstance(recipient);
|
||||
|
||||
if (buttonState.getCount() > 0) {
|
||||
countButton.setOnClickListener(v -> navigateToMediaSend(recipient, transport, locale, false));
|
||||
if (buttonState.isVisible()) {
|
||||
animateButtonTextChange(countButton);
|
||||
}
|
||||
getSupportFragmentManager().beginTransaction()
|
||||
.replace(R.id.mediasend_fragment_container, folderFragment, TAG_FOLDER_PICKER)
|
||||
.setCustomAnimations(R.anim.slide_from_bottom, R.anim.stationary, R.anim.slide_to_bottom, R.anim.stationary)
|
||||
.addToBackStack(null)
|
||||
.commit();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRequestFullScreen(boolean fullScreen) {
|
||||
captionAndRail.setVisibility(fullScreen ? View.GONE : View.VISIBLE);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onGlobalLayout() {
|
||||
hud.getRootView().getWindowVisibleDisplayFrame(visibleBounds);
|
||||
|
||||
int currentVisibleHeight = visibleBounds.height();
|
||||
|
||||
if (currentVisibleHeight != visibleHeight) {
|
||||
hud.getLayoutParams().height = currentVisibleHeight;
|
||||
hud.layout(visibleBounds.left, visibleBounds.top, visibleBounds.right, visibleBounds.bottom);
|
||||
hud.requestLayout();
|
||||
|
||||
visibleHeight = currentVisibleHeight;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onKeyboardHidden() {
|
||||
viewModel.onKeyboardHidden(sendButton.getSelectedTransport().isSms());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onKeyboardShown() {
|
||||
viewModel.onKeyboardShown(composeText.hasFocus(), captionText.hasFocus(), sendButton.getSelectedTransport().isSms());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRailItemClicked(int distanceFromActive) {
|
||||
if (getMediaSendFragment() != null) {
|
||||
viewModel.onPageChanged(getMediaSendFragment().getCurrentImagePosition() + distanceFromActive);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRailItemDeleteClicked(int distanceFromActive) {
|
||||
if (getMediaSendFragment() != null) {
|
||||
viewModel.onMediaItemRemoved(this, getMediaSendFragment().getCurrentImagePosition() + distanceFromActive);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCameraSelected() {
|
||||
navigateToCamera();
|
||||
}
|
||||
|
||||
public void onAddMediaClicked(@NonNull String bucketId) {
|
||||
// TODO: Get actual folder title somehow
|
||||
MediaPickerFolderFragment folderFragment = MediaPickerFolderFragment.newInstance(recipient);
|
||||
MediaPickerItemFragment itemFragment = MediaPickerItemFragment.newInstance(bucketId, "", viewModel.getMaxSelection());
|
||||
|
||||
getSupportFragmentManager().beginTransaction()
|
||||
.replace(R.id.mediasend_fragment_container, folderFragment, TAG_FOLDER_PICKER)
|
||||
.addToBackStack(null)
|
||||
.commit();
|
||||
|
||||
getSupportFragmentManager().beginTransaction()
|
||||
.replace(R.id.mediasend_fragment_container, itemFragment, TAG_ITEM_PICKER)
|
||||
.addToBackStack(null)
|
||||
.commit();
|
||||
}
|
||||
|
||||
public void onSendClicked(@NonNull List<Media> media, @NonNull String message, @NonNull TransportOption transport) {
|
||||
viewModel.onSendClicked();
|
||||
|
||||
ArrayList<Media> mediaList = new ArrayList<>(media);
|
||||
|
||||
if (mediaList.size() > 0) {
|
||||
Intent intent = new Intent();
|
||||
|
||||
intent.putParcelableArrayListExtra(EXTRA_MEDIA, mediaList);
|
||||
intent.putExtra(EXTRA_MESSAGE, viewModel.getRevealDuration() == 0 ? message : "");
|
||||
intent.putExtra(EXTRA_TRANSPORT, transport);
|
||||
intent.putExtra(EXTRA_REVEAL_DURATION, viewModel.getRevealDuration());
|
||||
|
||||
setResult(RESULT_OK, intent);
|
||||
} else {
|
||||
setResult(RESULT_CANCELED);
|
||||
}
|
||||
|
||||
finish();
|
||||
|
||||
overridePendingTransition(R.anim.stationary, R.anim.camera_slide_to_bottom);
|
||||
}
|
||||
|
||||
public void onNoMediaAvailable() {
|
||||
setResult(RESULT_CANCELED);
|
||||
finish();
|
||||
}
|
||||
|
||||
|
||||
private void initViewModel() {
|
||||
viewModel.getHudState().observe(this, state -> {
|
||||
if (state == null) return;
|
||||
|
||||
hud.setVisibility(state.isHudVisible() ? View.VISIBLE : View.GONE);
|
||||
composeContainer.setVisibility(state.isComposeVisible() ? View.VISIBLE : (state.getTimerState() == TimerState.GONE ? View.GONE : View.INVISIBLE));
|
||||
captionText.setVisibility(state.isCaptionVisible() ? View.VISIBLE : View.GONE);
|
||||
|
||||
int captionBackground;
|
||||
|
||||
if (state.getRailState() == MediaSendViewModel.RailState.VIEWABLE) {
|
||||
captionBackground = R.color.core_grey_90;
|
||||
} else if (state.getTimerState() == TimerState.ENABLED) {
|
||||
captionBackground = 0;
|
||||
} else {
|
||||
countButton.setOnClickListener(null);
|
||||
captionBackground = R.color.transparent_black_70;
|
||||
}
|
||||
|
||||
captionAndRail.setBackgroundResource(captionBackground);
|
||||
|
||||
switch (state.getButtonState()) {
|
||||
case SEND:
|
||||
sendButtonContainer.setVisibility(View.VISIBLE);
|
||||
countButton.setVisibility(View.GONE);
|
||||
break;
|
||||
case COUNT:
|
||||
sendButtonContainer.setVisibility(View.GONE);
|
||||
countButton.setVisibility(View.VISIBLE);
|
||||
countButtonText.setText(String.valueOf(state.getSelectionCount()));
|
||||
break;
|
||||
case GONE:
|
||||
sendButtonContainer.setVisibility(View.GONE);
|
||||
countButton.setVisibility(View.GONE);
|
||||
break;
|
||||
}
|
||||
|
||||
switch (state.getRailState()) {
|
||||
case INTERACTIVE:
|
||||
mediaRail.setVisibility(View.VISIBLE);
|
||||
mediaRailAdapter.setEditable(true);
|
||||
mediaRailAdapter.setInteractive(true);
|
||||
break;
|
||||
case VIEWABLE:
|
||||
mediaRail.setVisibility(View.VISIBLE);
|
||||
mediaRailAdapter.setEditable(false);
|
||||
mediaRailAdapter.setInteractive(false);
|
||||
break;
|
||||
case GONE:
|
||||
mediaRail.setVisibility(View.GONE);
|
||||
break;
|
||||
}
|
||||
|
||||
if (composeContainer.getVisibility() == View.GONE && sendButtonContainer.getVisibility() == View.GONE) {
|
||||
composeRow.setVisibility(View.GONE);
|
||||
} else {
|
||||
composeRow.setVisibility(View.VISIBLE);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void initializeCameraButtonObserver() {
|
||||
viewModel.getCameraButtonVisibility().observe(this, visible -> {
|
||||
if (visible == null) return;
|
||||
animateButtonVisibility(cameraButton, cameraButton.getVisibility(), visible ? View.VISIBLE : View.GONE);
|
||||
viewModel.getSelectedMedia().observe(this, media -> {
|
||||
mediaRailAdapter.setMedia(media);
|
||||
});
|
||||
|
||||
viewModel.getPosition().observe(this, position -> {
|
||||
if (position == null || position < 0) return;
|
||||
|
||||
MediaSendFragment fragment = getMediaSendFragment();
|
||||
if (fragment != null && fragment.getAllMedia().size() > position) {
|
||||
captionText.setText(fragment.getAllMedia().get(position).getCaption().or(""));
|
||||
}
|
||||
|
||||
mediaRailAdapter.setActivePosition(position);
|
||||
mediaRail.smoothScrollToPosition(position);
|
||||
});
|
||||
|
||||
viewModel.getBucketId().observe(this, bucketId -> {
|
||||
if (bucketId == null) return;
|
||||
mediaRailAdapter.setAddButtonListener(() -> onAddMediaClicked(bucketId));
|
||||
});
|
||||
}
|
||||
|
||||
private void initializeErrorObserver() {
|
||||
viewModel.getError().observe(this, error -> {
|
||||
if (error == null) return;
|
||||
|
||||
switch (error) {
|
||||
case NO_ITEMS:
|
||||
onNoMediaAvailable();
|
||||
break;
|
||||
case ITEM_TOO_LARGE:
|
||||
Toast.makeText(this, R.string.MediaSendActivity_an_item_was_removed_because_it_exceeded_the_size_limit, Toast.LENGTH_LONG).show();
|
||||
break;
|
||||
|
@ -358,7 +605,7 @@ public class MediaSendActivity extends PassphraseRequiredActionBarActivity imple
|
|||
});
|
||||
}
|
||||
|
||||
private void navigateToMediaSend(@NonNull Recipient recipient, @NonNull TransportOption transport, @NonNull Locale locale, boolean fade) {
|
||||
private void navigateToMediaSend(@NonNull Recipient recipient, @NonNull TransportOption transport, @NonNull Locale locale) {
|
||||
MediaSendFragment fragment = MediaSendFragment.newInstance(recipient, transport, locale);
|
||||
String backstackTag = null;
|
||||
|
||||
|
@ -367,17 +614,11 @@ public class MediaSendActivity extends PassphraseRequiredActionBarActivity imple
|
|||
backstackTag = TAG_SEND;
|
||||
}
|
||||
|
||||
FragmentTransaction transaction = getSupportFragmentManager().beginTransaction();
|
||||
|
||||
if (fade) {
|
||||
transaction.setCustomAnimations(R.anim.fade_in, R.anim.fade_out, R.anim.fade_out, R.anim.fade_in);
|
||||
} else {
|
||||
transaction.setCustomAnimations(R.anim.slide_from_right, R.anim.slide_to_left, R.anim.slide_from_left, R.anim.slide_to_right);
|
||||
}
|
||||
|
||||
transaction.replace(R.id.mediasend_fragment_container, fragment, TAG_SEND)
|
||||
.addToBackStack(backstackTag)
|
||||
.commit();
|
||||
getSupportFragmentManager().beginTransaction()
|
||||
.replace(R.id.mediasend_fragment_container, fragment, TAG_SEND)
|
||||
.setCustomAnimations(R.anim.fade_in, R.anim.fade_out, R.anim.fade_in, R.anim.fade_out)
|
||||
.addToBackStack(backstackTag)
|
||||
.commit();
|
||||
}
|
||||
|
||||
private void navigateToCamera() {
|
||||
|
@ -389,7 +630,7 @@ public class MediaSendActivity extends PassphraseRequiredActionBarActivity imple
|
|||
.onAllGranted(() -> {
|
||||
Fragment fragment = getOrCreateCameraFragment();
|
||||
getSupportFragmentManager().beginTransaction()
|
||||
.setCustomAnimations(R.anim.slide_from_right, R.anim.slide_to_left, R.anim.slide_from_left, R.anim.slide_to_right)
|
||||
.setCustomAnimations(R.anim.fade_in, R.anim.fade_out, R.anim.fade_in, R.anim.fade_out)
|
||||
.replace(R.id.mediasend_fragment_container, fragment, TAG_CAMERA)
|
||||
.addToBackStack(null)
|
||||
.commit();
|
||||
|
@ -400,66 +641,182 @@ public class MediaSendActivity extends PassphraseRequiredActionBarActivity imple
|
|||
|
||||
private Fragment getOrCreateCameraFragment() {
|
||||
Fragment fragment = getSupportFragmentManager().findFragmentByTag(TAG_CAMERA);
|
||||
|
||||
return fragment != null ? fragment
|
||||
: CameraFragment.newInstance();
|
||||
return fragment != null ? fragment : CameraFragment.newInstance();
|
||||
}
|
||||
|
||||
private void animateButtonVisibility(@NonNull View button, int oldVisibility, int newVisibility) {
|
||||
if (oldVisibility == newVisibility) return;
|
||||
private EmojiEditText getActiveInputField() {
|
||||
if (captionText.hasFocus()) return captionText;
|
||||
else return composeText;
|
||||
}
|
||||
|
||||
if (button.getAnimation() != null) {
|
||||
button.clearAnimation();
|
||||
button.setVisibility(newVisibility);
|
||||
} else if (newVisibility == View.VISIBLE) {
|
||||
button.setVisibility(View.VISIBLE);
|
||||
|
||||
Animation animation = new ScaleAnimation(0, 1, 0, 1, Animation.RELATIVE_TO_SELF, 0.5f, Animation.RELATIVE_TO_SELF, 0.5f);
|
||||
animation.setDuration(250);
|
||||
animation.setInterpolator(new OvershootInterpolator());
|
||||
button.startAnimation(animation);
|
||||
private void presentCharactersRemaining() {
|
||||
String messageBody = composeText.getTextTrimmed();
|
||||
TransportOption transportOption = sendButton.getSelectedTransport();
|
||||
CharacterState characterState = transportOption.calculateCharacters(messageBody);
|
||||
|
||||
if (characterState.charactersRemaining <= 15 || characterState.messagesSpent > 1) {
|
||||
charactersLeft.setText(String.format(dynamicLanguage.getCurrentLocale(),
|
||||
"%d/%d (%d)",
|
||||
characterState.charactersRemaining,
|
||||
characterState.maxTotalMessageSize,
|
||||
characterState.messagesSpent));
|
||||
charactersLeft.setVisibility(View.VISIBLE);
|
||||
} else {
|
||||
Animation animation = new ScaleAnimation(1, 0, 1, 0, Animation.RELATIVE_TO_SELF, 0.5f, Animation.RELATIVE_TO_SELF, 0.5f);
|
||||
animation.setDuration(150);
|
||||
animation.setInterpolator(new AccelerateDecelerateInterpolator());
|
||||
animation.setAnimationListener(new SimpleAnimationListener() {
|
||||
charactersLeft.setVisibility(View.GONE);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private void onEmojiToggleClicked(View v) {
|
||||
if (!emojiDrawer.resolved()) {
|
||||
emojiDrawer.get().setProviders(0, new EmojiKeyboardProvider(this, new EmojiKeyboardProvider.EmojiEventListener() {
|
||||
@Override
|
||||
public void onAnimationEnd(Animation animation) {
|
||||
button.clearAnimation();
|
||||
button.setVisibility(View.GONE);
|
||||
public void onKeyEvent(KeyEvent keyEvent) {
|
||||
getActiveInputField().dispatchKeyEvent(keyEvent);
|
||||
}
|
||||
});
|
||||
|
||||
button.startAnimation(animation);
|
||||
@Override
|
||||
public void onEmojiSelected(String emoji) {
|
||||
getActiveInputField().insertEmoji(emoji);
|
||||
}
|
||||
}));
|
||||
emojiToggle.attach(emojiDrawer.get());
|
||||
}
|
||||
|
||||
if (hud.getCurrentInput() == emojiDrawer.get()) {
|
||||
hud.showSoftkey(composeText);
|
||||
} else {
|
||||
hud.hideSoftkey(composeText, () -> hud.post(() -> hud.show(composeText, emojiDrawer.get())));
|
||||
}
|
||||
}
|
||||
|
||||
private void animateButtonTextChange(@NonNull View button) {
|
||||
if (button.getAnimation() != null) {
|
||||
button.clearAnimation();
|
||||
}
|
||||
@SuppressLint("StaticFieldLeak")
|
||||
private void processMedia(@NonNull List<Media> mediaList, @NonNull Map<Uri, Object> savedState) {
|
||||
Map<Media, EditorModel> modelsToRender = new HashMap<>();
|
||||
|
||||
Animation grow = new ScaleAnimation(1f, 1.3f, 1f, 1.3f, Animation.RELATIVE_TO_SELF, 0.5f, Animation.RELATIVE_TO_SELF, 0.5f);
|
||||
grow.setDuration(125);
|
||||
grow.setInterpolator(new AccelerateInterpolator());
|
||||
grow.setAnimationListener(new SimpleAnimationListener() {
|
||||
@Override
|
||||
public void onAnimationEnd(Animation animation) {
|
||||
Animation shrink = new ScaleAnimation(1.3f, 1f, 1.3f, 1f, Animation.RELATIVE_TO_SELF, 0.5f, Animation.RELATIVE_TO_SELF, 0.5f);
|
||||
shrink.setDuration(125);
|
||||
shrink.setInterpolator(new DecelerateInterpolator());
|
||||
button.startAnimation(shrink);
|
||||
for (Media media : mediaList) {
|
||||
Object state = savedState.get(media.getUri());
|
||||
|
||||
if (state instanceof ImageEditorFragment.Data) {
|
||||
EditorModel model = ((ImageEditorFragment.Data) state).readModel();
|
||||
if (model != null && model.isChanged()) {
|
||||
modelsToRender.put(media, model);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
button.startAnimation(grow);
|
||||
new AsyncTask<Void, Void, List<Media>>() {
|
||||
|
||||
private Stopwatch renderTimer;
|
||||
private Runnable progressTimer;
|
||||
private AlertDialog dialog;
|
||||
|
||||
@Override
|
||||
protected void onPreExecute() {
|
||||
renderTimer = new Stopwatch("ProcessMedia");
|
||||
progressTimer = () -> {
|
||||
dialog = new AlertDialog.Builder(new ContextThemeWrapper(MediaSendActivity.this, R.style.TextSecure_MediaSendProgressDialog))
|
||||
.setView(R.layout.progress_dialog)
|
||||
.setCancelable(false)
|
||||
.create();
|
||||
dialog.show();
|
||||
dialog.getWindow().setLayout(getResources().getDimensionPixelSize(R.dimen.mediasend_progress_dialog_size),
|
||||
getResources().getDimensionPixelSize(R.dimen.mediasend_progress_dialog_size));
|
||||
};
|
||||
Util.runOnMainDelayed(progressTimer, 250);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected List<Media> doInBackground(Void... voids) {
|
||||
Context context = MediaSendActivity.this;
|
||||
List<Media> updatedMedia = new ArrayList<>(mediaList.size());
|
||||
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
|
||||
|
||||
for (Media media : mediaList) {
|
||||
EditorModel modelToRender = modelsToRender.get(media);
|
||||
if (modelToRender != null) {
|
||||
Bitmap bitmap = modelToRender.render(context);
|
||||
try {
|
||||
outputStream.reset();
|
||||
bitmap.compress(Bitmap.CompressFormat.JPEG, 80, outputStream);
|
||||
|
||||
Uri uri = BlobProvider.getInstance()
|
||||
.forData(outputStream.toByteArray())
|
||||
.withMimeType(MediaUtil.IMAGE_JPEG)
|
||||
.createForSingleSessionOnDisk(context, e -> Log.w(TAG, "Failed to write to disk.", e));
|
||||
|
||||
Media updated = new Media(uri, MediaUtil.IMAGE_JPEG, media.getDate(), bitmap.getWidth(), bitmap.getHeight(), outputStream.size(), media.getBucketId(), media.getCaption());
|
||||
|
||||
updatedMedia.add(updated);
|
||||
renderTimer.split("item");
|
||||
} catch (IOException e) {
|
||||
Log.w(TAG, "Failed to render image. Using base image.");
|
||||
updatedMedia.add(media);
|
||||
} finally {
|
||||
bitmap.recycle();
|
||||
}
|
||||
} else {
|
||||
updatedMedia.add(media);
|
||||
}
|
||||
}
|
||||
return updatedMedia;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPostExecute(List<Media> media) {
|
||||
onSendClicked(media, composeText.getTextTrimmed(), sendButton.getSelectedTransport());
|
||||
Util.cancelRunnableOnMain(progressTimer);
|
||||
if (dialog != null) {
|
||||
dialog.dismiss();
|
||||
}
|
||||
renderTimer.stop(TAG);
|
||||
}
|
||||
}.executeOnExecutor(SignalExecutors.BOUNDED);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRequestFullScreen(boolean fullScreen) {
|
||||
MediaSendFragment sendFragment = (MediaSendFragment) getSupportFragmentManager().findFragmentByTag(TAG_SEND);
|
||||
if (sendFragment != null && sendFragment.isVisible()) {
|
||||
sendFragment.onRequestFullScreen(fullScreen);
|
||||
private @Nullable MediaSendFragment getMediaSendFragment() {
|
||||
return (MediaSendFragment) getSupportFragmentManager().findFragmentByTag(TAG_SEND);
|
||||
}
|
||||
|
||||
private class ComposeKeyPressedListener implements View.OnKeyListener, View.OnClickListener, TextWatcher, View.OnFocusChangeListener {
|
||||
|
||||
int beforeLength;
|
||||
|
||||
@Override
|
||||
public boolean onKey(View v, int keyCode, KeyEvent event) {
|
||||
if (event.getAction() == KeyEvent.ACTION_DOWN) {
|
||||
if (keyCode == KeyEvent.KEYCODE_ENTER) {
|
||||
if (TextSecurePreferences.isEnterSendsEnabled(getApplicationContext())) {
|
||||
sendButton.dispatchKeyEvent(new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_ENTER));
|
||||
sendButton.dispatchKeyEvent(new KeyEvent(KeyEvent.ACTION_UP, KeyEvent.KEYCODE_ENTER));
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
hud.showSoftkey(composeText);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void beforeTextChanged(CharSequence s, int start, int count,int after) {
|
||||
beforeLength = composeText.getTextTrimmed().length();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void afterTextChanged(Editable s) {
|
||||
presentCharactersRemaining();
|
||||
viewModel.onBodyChanged(s);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onTextChanged(CharSequence s, int start, int before,int count) {}
|
||||
|
||||
@Override
|
||||
public void onFocusChange(View v, boolean hasFocus) {}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,80 +1,31 @@
|
|||
package org.thoughtcrime.securesms.mediasend;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.content.Context;
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.PorterDuff;
|
||||
import android.graphics.Rect;
|
||||
import androidx.lifecycle.ViewModelProviders;
|
||||
import android.net.Uri;
|
||||
import android.os.AsyncTask;
|
||||
import android.os.Bundle;
|
||||
import android.text.Editable;
|
||||
import android.text.TextWatcher;
|
||||
import android.view.KeyEvent;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import androidx.viewpager.widget.ViewPager;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.view.ViewTreeObserver;
|
||||
import android.view.WindowManager;
|
||||
import android.view.inputmethod.EditorInfo;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
import androidx.appcompat.view.ContextThemeWrapper;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import androidx.lifecycle.ViewModelProviders;
|
||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
import androidx.viewpager.widget.ViewPager;
|
||||
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.TransportOption;
|
||||
import org.thoughtcrime.securesms.components.ComposeText;
|
||||
import org.thoughtcrime.securesms.components.ControllableViewPager;
|
||||
import org.thoughtcrime.securesms.components.InputAwareLayout;
|
||||
import org.thoughtcrime.securesms.components.SendButton;
|
||||
import org.thoughtcrime.securesms.components.emoji.EmojiEditText;
|
||||
import org.thoughtcrime.securesms.components.emoji.EmojiKeyboardProvider;
|
||||
import org.thoughtcrime.securesms.components.emoji.EmojiToggle;
|
||||
import org.thoughtcrime.securesms.components.emoji.MediaKeyboard;
|
||||
import org.thoughtcrime.securesms.contactshare.SimpleTextWatcher;
|
||||
import org.thoughtcrime.securesms.imageeditor.model.EditorModel;
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
import org.thoughtcrime.securesms.mediapreview.MediaRailAdapter;
|
||||
import org.thoughtcrime.securesms.mms.GlideApp;
|
||||
import org.thoughtcrime.securesms.providers.BlobProvider;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.scribbles.ImageEditorFragment;
|
||||
import org.thoughtcrime.securesms.util.CharacterCalculator.CharacterState;
|
||||
import org.thoughtcrime.securesms.util.MediaUtil;
|
||||
import org.thoughtcrime.securesms.util.Stopwatch;
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||
import org.thoughtcrime.securesms.util.ThemeUtil;
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
import org.thoughtcrime.securesms.util.concurrent.ListenableFuture;
|
||||
import org.thoughtcrime.securesms.util.concurrent.SettableFuture;
|
||||
import org.thoughtcrime.securesms.util.views.Stub;
|
||||
import org.whispersystems.libsignal.util.guava.Optional;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
|
||||
/**
|
||||
* Allows the user to edit and caption a set of media items before choosing to send them.
|
||||
*/
|
||||
public class MediaSendFragment extends Fragment implements ViewTreeObserver.OnGlobalLayoutListener,
|
||||
MediaRailAdapter.RailItemListener,
|
||||
InputAwareLayout.OnKeyboardShownListener,
|
||||
InputAwareLayout.OnKeyboardHiddenListener
|
||||
{
|
||||
public class MediaSendFragment extends Fragment {
|
||||
|
||||
private static final String TAG = MediaSendFragment.class.getSimpleName();
|
||||
|
||||
|
@ -82,28 +33,12 @@ public class MediaSendFragment extends Fragment implements ViewTreeObserver.OnGl
|
|||
private static final String KEY_TRANSPORT = "transport";
|
||||
private static final String KEY_LOCALE = "locale";
|
||||
|
||||
private InputAwareLayout hud;
|
||||
private View captionAndRail;
|
||||
private SendButton sendButton;
|
||||
private ComposeText composeText;
|
||||
private ViewGroup composeContainer;
|
||||
private EmojiEditText captionText;
|
||||
private EmojiToggle emojiToggle;
|
||||
private Stub<MediaKeyboard> emojiDrawer;
|
||||
private ViewGroup playbackControlsContainer;
|
||||
private TextView charactersLeft;
|
||||
|
||||
private ViewGroup playbackControlsContainer;
|
||||
private ControllableViewPager fragmentPager;
|
||||
private MediaSendFragmentPagerAdapter fragmentPagerAdapter;
|
||||
private RecyclerView mediaRail;
|
||||
private MediaRailAdapter mediaRailAdapter;
|
||||
|
||||
private int visibleHeight;
|
||||
private MediaSendViewModel viewModel;
|
||||
private Controller controller;
|
||||
private Locale locale;
|
||||
|
||||
private final Rect visibleBounds = new Rect();
|
||||
|
||||
public static MediaSendFragment newInstance(@NonNull Recipient recipient, @NonNull TransportOption transport, @NonNull Locale locale) {
|
||||
Bundle args = new Bundle();
|
||||
|
@ -116,17 +51,6 @@ public class MediaSendFragment extends Fragment implements ViewTreeObserver.OnGl
|
|||
return fragment;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAttach(Context context) {
|
||||
super.onAttach(context);
|
||||
|
||||
if (!(requireActivity() instanceof Controller)) {
|
||||
throw new IllegalStateException("Parent activity must implement controller interface.");
|
||||
}
|
||||
|
||||
controller = (Controller) requireActivity();
|
||||
}
|
||||
|
||||
@Override
|
||||
public @Nullable View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
|
||||
return ThemeUtil.getThemedInflater(requireActivity(), inflater, R.style.TextSecure_DarkTheme)
|
||||
|
@ -137,52 +61,14 @@ public class MediaSendFragment extends Fragment implements ViewTreeObserver.OnGl
|
|||
public void onCreate(@Nullable Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
|
||||
locale = (Locale) getArguments().getSerializable(KEY_LOCALE);
|
||||
|
||||
initViewModel();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
|
||||
hud = view.findViewById(R.id.mediasend_hud);
|
||||
captionAndRail = view.findViewById(R.id.mediasend_caption_and_rail);
|
||||
sendButton = view.findViewById(R.id.mediasend_send_button);
|
||||
composeText = view.findViewById(R.id.mediasend_compose_text);
|
||||
composeContainer = view.findViewById(R.id.mediasend_compose_container);
|
||||
captionText = view.findViewById(R.id.mediasend_caption);
|
||||
emojiToggle = view.findViewById(R.id.mediasend_emoji_toggle);
|
||||
emojiDrawer = new Stub<>(view.findViewById(R.id.mediasend_emoji_drawer_stub));
|
||||
fragmentPager = view.findViewById(R.id.mediasend_pager);
|
||||
mediaRail = view.findViewById(R.id.mediasend_media_rail);
|
||||
playbackControlsContainer = view.findViewById(R.id.mediasend_playback_controls_container);
|
||||
charactersLeft = view.findViewById(R.id.mediasend_characters_left);
|
||||
|
||||
View sendButtonBkg = view.findViewById(R.id.mediasend_send_button_bkg);
|
||||
|
||||
sendButton.setOnClickListener(v -> {
|
||||
if (hud.isKeyboardOpen()) {
|
||||
hud.hideSoftkey(composeText, null);
|
||||
}
|
||||
|
||||
processMedia(fragmentPagerAdapter.getAllMedia(), fragmentPagerAdapter.getSavedState());
|
||||
});
|
||||
|
||||
sendButton.addOnTransportChangedListener((newTransport, manuallySelected) -> {
|
||||
presentCharactersRemaining();
|
||||
composeText.setTransport(newTransport);
|
||||
sendButtonBkg.getBackground().setColorFilter(newTransport.getBackgroundColor(), PorterDuff.Mode.MULTIPLY);
|
||||
sendButtonBkg.getBackground().invalidateSelf();
|
||||
});
|
||||
|
||||
ComposeKeyPressedListener composeKeyPressedListener = new ComposeKeyPressedListener();
|
||||
|
||||
composeText.setOnKeyListener(composeKeyPressedListener);
|
||||
composeText.addTextChangedListener(composeKeyPressedListener);
|
||||
composeText.setOnClickListener(composeKeyPressedListener);
|
||||
composeText.setOnFocusChangeListener(composeKeyPressedListener);
|
||||
|
||||
captionText.clearFocus();
|
||||
composeText.requestFocus();
|
||||
|
||||
fragmentPagerAdapter = new MediaSendFragmentPagerAdapter(getChildFragmentManager());
|
||||
fragmentPager.setAdapter(fragmentPagerAdapter);
|
||||
|
@ -190,45 +76,6 @@ public class MediaSendFragment extends Fragment implements ViewTreeObserver.OnGl
|
|||
FragmentPageChangeListener pageChangeListener = new FragmentPageChangeListener();
|
||||
fragmentPager.addOnPageChangeListener(pageChangeListener);
|
||||
fragmentPager.post(() -> pageChangeListener.onPageSelected(fragmentPager.getCurrentItem()));
|
||||
|
||||
mediaRailAdapter = new MediaRailAdapter(GlideApp.with(this), this, true);
|
||||
mediaRail.setLayoutManager(new LinearLayoutManager(requireContext(), LinearLayoutManager.HORIZONTAL, false));
|
||||
mediaRail.setAdapter(mediaRailAdapter);
|
||||
|
||||
hud.getRootView().getViewTreeObserver().addOnGlobalLayoutListener(this);
|
||||
hud.addOnKeyboardShownListener(this);
|
||||
hud.addOnKeyboardHiddenListener(this);
|
||||
|
||||
captionText.addTextChangedListener(new SimpleTextWatcher() {
|
||||
@Override
|
||||
public void onTextChanged(String text) {
|
||||
viewModel.onCaptionChanged(text);
|
||||
}
|
||||
});
|
||||
|
||||
TransportOption transportOption = getArguments().getParcelable(KEY_TRANSPORT);
|
||||
|
||||
sendButton.setTransport(transportOption);
|
||||
sendButton.disableTransport(transportOption.getType() == TransportOption.Type.SMS ? TransportOption.Type.TEXTSECURE : TransportOption.Type.SMS);
|
||||
|
||||
composeText.append(viewModel.getBody());
|
||||
|
||||
Recipient recipient = Recipient.from(requireContext(), getArguments().getParcelable(KEY_ADDRESS), false);
|
||||
String displayName = Optional.fromNullable(recipient.getName())
|
||||
.or(Optional.fromNullable(recipient.getProfileName())
|
||||
.or(recipient.getAddress().serialize()));
|
||||
composeText.setHint(getString(R.string.MediaSendActivity_message_to_s, displayName), null);
|
||||
composeText.setOnEditorActionListener((v, actionId, event) -> {
|
||||
boolean isSend = actionId == EditorInfo.IME_ACTION_SEND;
|
||||
if (isSend) sendButton.performClick();
|
||||
return isSend;
|
||||
});
|
||||
|
||||
if (TextSecurePreferences.isSystemEmojiPreferred(getContext())) {
|
||||
emojiToggle.setVisibility(View.GONE);
|
||||
} else {
|
||||
emojiToggle.setOnClickListener(this::onEmojiToggleClicked);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -237,9 +84,6 @@ public class MediaSendFragment extends Fragment implements ViewTreeObserver.OnGl
|
|||
|
||||
fragmentPagerAdapter.restoreState(viewModel.getDrawState());
|
||||
viewModel.onImageEditorStarted();
|
||||
|
||||
requireActivity().getWindow().addFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN);
|
||||
requireActivity().getWindow().clearFlags(WindowManager.LayoutParams.FLAG_FORCE_NOT_FULLSCREEN);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -254,82 +98,22 @@ public class MediaSendFragment extends Fragment implements ViewTreeObserver.OnGl
|
|||
viewModel.saveDrawState(fragmentPagerAdapter.getSavedState());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onGlobalLayout() {
|
||||
hud.getRootView().getWindowVisibleDisplayFrame(visibleBounds);
|
||||
|
||||
int currentVisibleHeight = visibleBounds.height();
|
||||
|
||||
if (currentVisibleHeight != visibleHeight) {
|
||||
hud.getLayoutParams().height = currentVisibleHeight;
|
||||
hud.layout(visibleBounds.left, visibleBounds.top, visibleBounds.right, visibleBounds.bottom);
|
||||
hud.requestLayout();
|
||||
|
||||
visibleHeight = currentVisibleHeight;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRailItemClicked(int distanceFromActive) {
|
||||
viewModel.onPageChanged(fragmentPager.getCurrentItem() + distanceFromActive);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRailItemDeleteClicked(int distanceFromActive) {
|
||||
viewModel.onMediaItemRemoved(requireContext(), fragmentPager.getCurrentItem() + distanceFromActive);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onKeyboardShown() {
|
||||
if (sendButton.getSelectedTransport().isSms()) {
|
||||
mediaRail.setVisibility(View.GONE);
|
||||
composeContainer.setVisibility(View.VISIBLE);
|
||||
captionText.setVisibility(View.GONE);
|
||||
} else {
|
||||
if (captionText.hasFocus()) {
|
||||
mediaRail.setVisibility(View.VISIBLE);
|
||||
composeContainer.setVisibility(View.GONE);
|
||||
captionText.setVisibility(View.VISIBLE);
|
||||
} else if (composeText.hasFocus()) {
|
||||
mediaRail.setVisibility(View.VISIBLE);
|
||||
composeContainer.setVisibility(View.VISIBLE);
|
||||
captionText.setVisibility(View.GONE);
|
||||
} else {
|
||||
mediaRail.setVisibility(View.GONE);
|
||||
composeContainer.setVisibility(View.VISIBLE);
|
||||
captionText.setVisibility(View.GONE);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onKeyboardHidden() {
|
||||
composeContainer.setVisibility(View.VISIBLE);
|
||||
|
||||
if (sendButton.getSelectedTransport().isSms()) {
|
||||
mediaRail.setVisibility(View.GONE);
|
||||
captionText.setVisibility(View.GONE);
|
||||
} else {
|
||||
mediaRail.setVisibility(View.VISIBLE);
|
||||
|
||||
if (!Util.isEmpty(viewModel.getSelectedMedia().getValue()) && viewModel.getSelectedMedia().getValue().size() > 1) {
|
||||
captionText.setVisibility(View.VISIBLE);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void onTouchEventsNeeded(boolean needed) {
|
||||
if (fragmentPager != null) {
|
||||
fragmentPager.setEnabled(!needed);
|
||||
}
|
||||
}
|
||||
|
||||
public boolean handleBackPress() {
|
||||
if (hud.isInputOpen()) {
|
||||
hud.hideCurrentInput(composeText);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
public List<Media> getAllMedia() {
|
||||
return fragmentPagerAdapter.getAllMedia();
|
||||
}
|
||||
|
||||
public @NonNull Map<Uri, Object> getSavedState() {
|
||||
return fragmentPagerAdapter.getSavedState();
|
||||
}
|
||||
|
||||
public int getCurrentImagePosition() {
|
||||
return fragmentPager.getCurrentItem();
|
||||
}
|
||||
|
||||
private void initViewModel() {
|
||||
|
@ -337,27 +121,16 @@ public class MediaSendFragment extends Fragment implements ViewTreeObserver.OnGl
|
|||
|
||||
viewModel.getSelectedMedia().observe(this, media -> {
|
||||
if (Util.isEmpty(media)) {
|
||||
controller.onNoMediaAvailable();
|
||||
return;
|
||||
}
|
||||
|
||||
fragmentPagerAdapter.setMedia(media);
|
||||
|
||||
mediaRail.setVisibility(sendButton.getSelectedTransport().isSms() ? View.GONE : View.VISIBLE);
|
||||
captionText.setVisibility((media.size() > 1 || media.get(0).getCaption().isPresent()) ? View.VISIBLE : View.GONE);
|
||||
mediaRailAdapter.setMedia(media);
|
||||
});
|
||||
|
||||
viewModel.getPosition().observe(this, position -> {
|
||||
if (position == null || position < 0) return;
|
||||
|
||||
fragmentPager.setCurrentItem(position, true);
|
||||
mediaRailAdapter.setActivePosition(position);
|
||||
mediaRail.smoothScrollToPosition(position);
|
||||
|
||||
if (fragmentPagerAdapter.getAllMedia().size() > position) {
|
||||
captionText.setText(fragmentPagerAdapter.getAllMedia().get(position).getCaption().or(""));
|
||||
}
|
||||
|
||||
View playbackControls = fragmentPagerAdapter.getPlaybackControls(position);
|
||||
|
||||
|
@ -370,146 +143,6 @@ public class MediaSendFragment extends Fragment implements ViewTreeObserver.OnGl
|
|||
playbackControlsContainer.removeAllViews();
|
||||
}
|
||||
});
|
||||
|
||||
viewModel.getBucketId().observe(this, bucketId -> {
|
||||
if (bucketId == null) return;
|
||||
|
||||
mediaRailAdapter.setAddButtonListener(() -> controller.onAddMediaClicked(bucketId));
|
||||
});
|
||||
}
|
||||
|
||||
private EmojiEditText getActiveInputField() {
|
||||
if (captionText.hasFocus()) return captionText;
|
||||
else return composeText;
|
||||
}
|
||||
|
||||
|
||||
private void presentCharactersRemaining() {
|
||||
String messageBody = composeText.getTextTrimmed();
|
||||
TransportOption transportOption = sendButton.getSelectedTransport();
|
||||
CharacterState characterState = transportOption.calculateCharacters(messageBody);
|
||||
|
||||
if (characterState.charactersRemaining <= 15 || characterState.messagesSpent > 1) {
|
||||
charactersLeft.setText(String.format(locale,
|
||||
"%d/%d (%d)",
|
||||
characterState.charactersRemaining,
|
||||
characterState.maxTotalMessageSize,
|
||||
characterState.messagesSpent));
|
||||
charactersLeft.setVisibility(View.VISIBLE);
|
||||
} else {
|
||||
charactersLeft.setVisibility(View.GONE);
|
||||
}
|
||||
}
|
||||
|
||||
private void onEmojiToggleClicked(View v) {
|
||||
if (!emojiDrawer.resolved()) {
|
||||
emojiDrawer.get().setProviders(0, new EmojiKeyboardProvider(requireContext(), new EmojiKeyboardProvider.EmojiEventListener() {
|
||||
@Override
|
||||
public void onKeyEvent(KeyEvent keyEvent) {
|
||||
getActiveInputField().dispatchKeyEvent(keyEvent);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onEmojiSelected(String emoji) {
|
||||
getActiveInputField().insertEmoji(emoji);
|
||||
}
|
||||
}));
|
||||
emojiToggle.attach(emojiDrawer.get());
|
||||
}
|
||||
|
||||
if (hud.getCurrentInput() == emojiDrawer.get()) {
|
||||
hud.showSoftkey(composeText);
|
||||
} else {
|
||||
hud.hideSoftkey(composeText, () -> hud.post(() -> hud.show(composeText, emojiDrawer.get())));
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressLint("StaticFieldLeak")
|
||||
private void processMedia(@NonNull List<Media> mediaList, @NonNull Map<Uri, Object> savedState) {
|
||||
Map<Media, EditorModel> modelsToRender = new HashMap<>();
|
||||
|
||||
for (Media media : mediaList) {
|
||||
Object state = savedState.get(media.getUri());
|
||||
|
||||
if (state instanceof ImageEditorFragment.Data) {
|
||||
EditorModel model = ((ImageEditorFragment.Data) state).readModel();
|
||||
if (model != null && model.isChanged()) {
|
||||
modelsToRender.put(media, model);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
new AsyncTask<Void, Void, List<Media>>() {
|
||||
|
||||
private Stopwatch renderTimer;
|
||||
private Runnable progressTimer;
|
||||
private AlertDialog dialog;
|
||||
|
||||
@Override
|
||||
protected void onPreExecute() {
|
||||
renderTimer = new Stopwatch("ProcessMedia");
|
||||
progressTimer = () -> {
|
||||
dialog = new AlertDialog.Builder(new ContextThemeWrapper(requireContext(), R.style.TextSecure_MediaSendProgressDialog))
|
||||
.setView(R.layout.progress_dialog)
|
||||
.setCancelable(false)
|
||||
.create();
|
||||
dialog.show();
|
||||
dialog.getWindow().setLayout(getResources().getDimensionPixelSize(R.dimen.mediasend_progress_dialog_size),
|
||||
getResources().getDimensionPixelSize(R.dimen.mediasend_progress_dialog_size));
|
||||
};
|
||||
Util.runOnMainDelayed(progressTimer, 250);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected List<Media> doInBackground(Void... voids) {
|
||||
Context context = requireContext();
|
||||
List<Media> updatedMedia = new ArrayList<>(mediaList.size());
|
||||
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
|
||||
|
||||
for (Media media : mediaList) {
|
||||
EditorModel modelToRender = modelsToRender.get(media);
|
||||
if (modelToRender != null) {
|
||||
Bitmap bitmap = modelToRender.render(context);
|
||||
try {
|
||||
outputStream.reset();
|
||||
bitmap.compress(Bitmap.CompressFormat.JPEG, 80, outputStream);
|
||||
|
||||
Uri uri = BlobProvider.getInstance()
|
||||
.forData(outputStream.toByteArray())
|
||||
.withMimeType(MediaUtil.IMAGE_JPEG)
|
||||
.createForSingleSessionOnDisk(context, e -> Log.w(TAG, "Failed to write to disk.", e));
|
||||
|
||||
Media updated = new Media(uri, MediaUtil.IMAGE_JPEG, media.getDate(), bitmap.getWidth(), bitmap.getHeight(), outputStream.size(), media.getBucketId(), media.getCaption());
|
||||
|
||||
updatedMedia.add(updated);
|
||||
renderTimer.split("item");
|
||||
} catch (IOException e) {
|
||||
Log.w(TAG, "Failed to render image. Using base image.");
|
||||
updatedMedia.add(media);
|
||||
} finally {
|
||||
bitmap.recycle();
|
||||
}
|
||||
} else {
|
||||
updatedMedia.add(media);
|
||||
}
|
||||
}
|
||||
return updatedMedia;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPostExecute(List<Media> media) {
|
||||
controller.onSendClicked(media, composeText.getTextTrimmed(), sendButton.getSelectedTransport());
|
||||
Util.cancelRunnableOnMain(progressTimer);
|
||||
if (dialog != null) {
|
||||
dialog.dismiss();
|
||||
}
|
||||
renderTimer.stop(TAG);
|
||||
}
|
||||
}.execute();
|
||||
}
|
||||
|
||||
public void onRequestFullScreen(boolean fullScreen) {
|
||||
captionAndRail.setVisibility(fullScreen ? View.GONE : View.VISIBLE);
|
||||
}
|
||||
|
||||
private class FragmentPageChangeListener extends ViewPager.SimpleOnPageChangeListener {
|
||||
|
@ -518,51 +151,4 @@ public class MediaSendFragment extends Fragment implements ViewTreeObserver.OnGl
|
|||
viewModel.onPageChanged(position);
|
||||
}
|
||||
}
|
||||
|
||||
private class ComposeKeyPressedListener implements View.OnKeyListener, View.OnClickListener, TextWatcher, View.OnFocusChangeListener {
|
||||
|
||||
int beforeLength;
|
||||
|
||||
@Override
|
||||
public boolean onKey(View v, int keyCode, KeyEvent event) {
|
||||
if (event.getAction() == KeyEvent.ACTION_DOWN) {
|
||||
if (keyCode == KeyEvent.KEYCODE_ENTER) {
|
||||
if (TextSecurePreferences.isEnterSendsEnabled(requireContext())) {
|
||||
sendButton.dispatchKeyEvent(new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_ENTER));
|
||||
sendButton.dispatchKeyEvent(new KeyEvent(KeyEvent.ACTION_UP, KeyEvent.KEYCODE_ENTER));
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
hud.showSoftkey(composeText);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void beforeTextChanged(CharSequence s, int start, int count,int after) {
|
||||
beforeLength = composeText.getTextTrimmed().length();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void afterTextChanged(Editable s) {
|
||||
presentCharactersRemaining();
|
||||
viewModel.onBodyChanged(s);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onTextChanged(CharSequence s, int start, int before,int count) {}
|
||||
|
||||
@Override
|
||||
public void onFocusChange(View v, boolean hasFocus) {}
|
||||
}
|
||||
|
||||
public interface Controller {
|
||||
void onAddMediaClicked(@NonNull String bucketId);
|
||||
void onSendClicked(@NonNull List<Media> media, @NonNull String body, @NonNull TransportOption transport);
|
||||
void onNoMediaAvailable();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -16,6 +16,7 @@ import org.thoughtcrime.securesms.TransportOption;
|
|||
import org.thoughtcrime.securesms.logging.Log;
|
||||
import org.thoughtcrime.securesms.mms.MediaConstraints;
|
||||
import org.thoughtcrime.securesms.providers.BlobProvider;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.util.MediaUtil;
|
||||
import org.thoughtcrime.securesms.util.SingleLiveEvent;
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
|
@ -41,52 +42,69 @@ class MediaSendViewModel extends ViewModel {
|
|||
private final MediaRepository repository;
|
||||
private final MutableLiveData<List<Media>> selectedMedia;
|
||||
private final MutableLiveData<List<Media>> bucketMedia;
|
||||
private final MutableLiveData<Optional<Media>> mostRecentMedia;
|
||||
private final MutableLiveData<Integer> position;
|
||||
private final MutableLiveData<String> bucketId;
|
||||
private final MutableLiveData<List<MediaFolder>> folders;
|
||||
private final MutableLiveData<CountButtonState> countButtonState;
|
||||
private final MutableLiveData<Boolean> cameraButtonVisibility;
|
||||
private final MutableLiveData<HudState> hudState;
|
||||
private final SingleLiveEvent<Error> error;
|
||||
private final Map<Uri, Object> savedDrawState;
|
||||
|
||||
private MediaConstraints mediaConstraints;
|
||||
private CharSequence body;
|
||||
private CountButtonState.Visibility countButtonVisibility;
|
||||
private boolean sentMedia;
|
||||
private Optional<Media> lastImageCapture;
|
||||
private int maxSelection;
|
||||
private MediaConstraints mediaConstraints;
|
||||
private CharSequence body;
|
||||
private boolean sentMedia;
|
||||
private int maxSelection;
|
||||
private Page page;
|
||||
private boolean isSms;
|
||||
private boolean isNoteToSelf;
|
||||
private Optional<Media> lastCameraCapture;
|
||||
|
||||
private boolean hudVisible;
|
||||
private boolean composeVisible;
|
||||
private boolean captionVisible;
|
||||
private ButtonState buttonState;
|
||||
private RailState railState;
|
||||
private TimerState timerState;
|
||||
|
||||
|
||||
private MediaSendViewModel(@NonNull Application application, @NonNull MediaRepository repository) {
|
||||
this.application = application;
|
||||
this.repository = repository;
|
||||
this.selectedMedia = new MutableLiveData<>();
|
||||
this.bucketMedia = new MutableLiveData<>();
|
||||
this.mostRecentMedia = new MutableLiveData<>();
|
||||
this.position = new MutableLiveData<>();
|
||||
this.bucketId = new MutableLiveData<>();
|
||||
this.folders = new MutableLiveData<>();
|
||||
this.countButtonState = new MutableLiveData<>();
|
||||
this.cameraButtonVisibility = new MutableLiveData<>();
|
||||
this.hudState = new MutableLiveData<>();
|
||||
this.error = new SingleLiveEvent<>();
|
||||
this.savedDrawState = new HashMap<>();
|
||||
this.countButtonVisibility = CountButtonState.Visibility.FORCED_OFF;
|
||||
this.lastImageCapture = Optional.absent();
|
||||
this.lastCameraCapture = Optional.absent();
|
||||
this.body = "";
|
||||
this.buttonState = ButtonState.GONE;
|
||||
this.railState = RailState.GONE;
|
||||
this.timerState = TimerState.GONE;
|
||||
this.page = Page.UNKNOWN;
|
||||
|
||||
position.setValue(-1);
|
||||
countButtonState.setValue(new CountButtonState(0, countButtonVisibility));
|
||||
cameraButtonVisibility.setValue(false);
|
||||
}
|
||||
|
||||
void setTransport(@NonNull TransportOption transport) {
|
||||
if (transport.isSms()) {
|
||||
isSms = true;
|
||||
maxSelection = MAX_SMS;
|
||||
mediaConstraints = MediaConstraints.getMmsMediaConstraints(transport.getSimSubscriptionId().or(-1));
|
||||
} else {
|
||||
isSms = false;
|
||||
maxSelection = MAX_PUSH;
|
||||
mediaConstraints = MediaConstraints.getPushMediaConstraints();
|
||||
}
|
||||
}
|
||||
|
||||
void setRecipient(@NonNull Recipient recipient) {
|
||||
isNoteToSelf = recipient.isLocalNumber();
|
||||
}
|
||||
|
||||
void onSelectedMediaChanged(@NonNull Context context, @NonNull List<Media> newMedia) {
|
||||
repository.getPopulatedMedia(context, newMedia, populatedMedia -> {
|
||||
Util.runOnMain(() -> {
|
||||
|
@ -113,11 +131,19 @@ class MediaSendViewModel extends ViewModel {
|
|||
bucketId.setValue(computedId);
|
||||
} else {
|
||||
bucketId.setValue(Media.ALL_MEDIA_BUCKET_ID);
|
||||
countButtonVisibility = CountButtonState.Visibility.CONDITIONAL;
|
||||
}
|
||||
|
||||
selectedMedia.setValue(filteredMedia);
|
||||
countButtonState.setValue(new CountButtonState(filteredMedia.size(), countButtonVisibility));
|
||||
if (page == Page.EDITOR && filteredMedia.isEmpty()) {
|
||||
error.postValue(Error.NO_ITEMS);
|
||||
} else if (filteredMedia.isEmpty()) {
|
||||
hudVisible = false;
|
||||
selectedMedia.setValue(filteredMedia);
|
||||
hudState.setValue(buildHudState());
|
||||
} else {
|
||||
hudVisible = true;
|
||||
selectedMedia.setValue(filteredMedia);
|
||||
hudState.setValue(buildHudState());
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
@ -134,41 +160,131 @@ class MediaSendViewModel extends ViewModel {
|
|||
bucketId.setValue(filteredMedia.get(0).getBucketId().or(Media.ALL_MEDIA_BUCKET_ID));
|
||||
}
|
||||
|
||||
countButtonVisibility = CountButtonState.Visibility.FORCED_OFF;
|
||||
|
||||
selectedMedia.setValue(filteredMedia);
|
||||
countButtonState.setValue(new CountButtonState(filteredMedia.size(), countButtonVisibility));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
void onMultiSelectStarted() {
|
||||
countButtonVisibility = CountButtonState.Visibility.FORCED_ON;
|
||||
countButtonState.setValue(new CountButtonState(getSelectedMediaOrDefault().size(), countButtonVisibility));
|
||||
hudVisible = true;
|
||||
composeVisible = false;
|
||||
captionVisible = false;
|
||||
buttonState = ButtonState.COUNT;
|
||||
railState = RailState.VIEWABLE;
|
||||
timerState = TimerState.GONE;
|
||||
|
||||
hudState.setValue(buildHudState());
|
||||
}
|
||||
|
||||
void onImageEditorStarted() {
|
||||
countButtonVisibility = CountButtonState.Visibility.FORCED_OFF;
|
||||
countButtonState.setValue(new CountButtonState(getSelectedMediaOrDefault().size(), countButtonVisibility));
|
||||
cameraButtonVisibility.setValue(false);
|
||||
page = Page.EDITOR;
|
||||
hudVisible = true;
|
||||
composeVisible = timerState != TimerState.ENABLED;
|
||||
captionVisible = getSelectedMediaOrDefault().size() > 1 || (getSelectedMediaOrDefault().size() > 0 && getSelectedMediaOrDefault().get(0).getCaption().isPresent());
|
||||
buttonState = ButtonState.SEND;
|
||||
railState = !isSms ? RailState.INTERACTIVE : RailState.GONE;
|
||||
|
||||
hudState.setValue(buildHudState());
|
||||
}
|
||||
|
||||
void onCameraStarted() {
|
||||
countButtonVisibility = CountButtonState.Visibility.CONDITIONAL;
|
||||
countButtonState.setValue(new CountButtonState(getSelectedMediaOrDefault().size(), countButtonVisibility));
|
||||
cameraButtonVisibility.setValue(false);
|
||||
Page previous = page;
|
||||
|
||||
page = Page.CAMERA;
|
||||
hudVisible = false;
|
||||
timerState = TimerState.GONE;
|
||||
buttonState = ButtonState.COUNT;
|
||||
|
||||
List<Media> selected = getSelectedMediaOrDefault();
|
||||
|
||||
if (previous == Page.EDITOR && lastCameraCapture.isPresent() && selected.contains(lastCameraCapture.get()) && selected.size() == 1) {
|
||||
selected.remove(lastCameraCapture.get());
|
||||
selectedMedia.setValue(selected);
|
||||
BlobProvider.getInstance().delete(application, lastCameraCapture.get().getUri());
|
||||
}
|
||||
|
||||
hudState.setValue(buildHudState());
|
||||
}
|
||||
|
||||
void onItemPickerStarted() {
|
||||
countButtonVisibility = CountButtonState.Visibility.CONDITIONAL;
|
||||
countButtonState.setValue(new CountButtonState(getSelectedMediaOrDefault().size(), countButtonVisibility));
|
||||
cameraButtonVisibility.setValue(true);
|
||||
page = Page.ITEM_PICKER;
|
||||
hudVisible = true;
|
||||
composeVisible = false;
|
||||
captionVisible = false;
|
||||
buttonState = ButtonState.COUNT;
|
||||
timerState = TimerState.GONE;
|
||||
railState = getSelectedMediaOrDefault().isEmpty() ? RailState.GONE : RailState.VIEWABLE;
|
||||
|
||||
lastCameraCapture = Optional.absent();
|
||||
|
||||
hudState.setValue(buildHudState());
|
||||
}
|
||||
|
||||
void onFolderPickerStarted() {
|
||||
countButtonVisibility = CountButtonState.Visibility.CONDITIONAL;
|
||||
countButtonState.setValue(new CountButtonState(getSelectedMediaOrDefault().size(), countButtonVisibility));
|
||||
cameraButtonVisibility.setValue(true);
|
||||
page = Page.FOLDER_PICKER;
|
||||
hudVisible = true;
|
||||
composeVisible = false;
|
||||
captionVisible = false;
|
||||
buttonState = ButtonState.COUNT;
|
||||
timerState = TimerState.GONE;
|
||||
railState = getSelectedMediaOrDefault().isEmpty() ? RailState.GONE : RailState.VIEWABLE;
|
||||
|
||||
lastCameraCapture = Optional.absent();
|
||||
|
||||
hudState.setValue(buildHudState());
|
||||
}
|
||||
|
||||
void onTimerButtonToggled() {
|
||||
hudVisible = true;
|
||||
timerState = (timerState == TimerState.ENABLED) ? TimerState.DISABLED : TimerState.ENABLED;
|
||||
composeVisible = (timerState != TimerState.ENABLED);
|
||||
|
||||
hudState.setValue(buildHudState());
|
||||
}
|
||||
|
||||
void onKeyboardHidden(boolean isSms) {
|
||||
if (page != Page.EDITOR) return;
|
||||
|
||||
composeVisible = (timerState != TimerState.ENABLED);
|
||||
buttonState = ButtonState.SEND;
|
||||
|
||||
if (isSms) {
|
||||
railState = RailState.GONE;
|
||||
captionVisible = false;
|
||||
} else {
|
||||
railState = RailState.INTERACTIVE;
|
||||
|
||||
if (getSelectedMediaOrDefault().size() > 1 || (getSelectedMediaOrDefault().size() > 0 && getSelectedMediaOrDefault().get(0).getCaption().isPresent())) {
|
||||
captionVisible = true;
|
||||
}
|
||||
}
|
||||
|
||||
hudState.setValue(buildHudState());
|
||||
}
|
||||
|
||||
void onKeyboardShown(boolean isComposeFocused, boolean isCaptionFocused, boolean isSms) {
|
||||
if (page != Page.EDITOR) return;
|
||||
|
||||
if (isSms) {
|
||||
railState = RailState.GONE;
|
||||
composeVisible = (timerState == TimerState.GONE);
|
||||
captionVisible = false;
|
||||
buttonState = ButtonState.SEND;
|
||||
} else {
|
||||
if (isCaptionFocused) {
|
||||
railState = RailState.INTERACTIVE;
|
||||
composeVisible = false;
|
||||
captionVisible = true;
|
||||
buttonState = ButtonState.GONE;
|
||||
} else if (isComposeFocused) {
|
||||
railState = RailState.INTERACTIVE;
|
||||
composeVisible = (timerState != TimerState.ENABLED);
|
||||
captionVisible = false;
|
||||
buttonState = ButtonState.SEND;
|
||||
}
|
||||
}
|
||||
|
||||
hudState.setValue(buildHudState());
|
||||
}
|
||||
|
||||
void onBodyChanged(@NonNull CharSequence body) {
|
||||
|
@ -201,10 +317,22 @@ class MediaSendViewModel extends ViewModel {
|
|||
BlobProvider.getInstance().delete(context, removed.getUri());
|
||||
}
|
||||
|
||||
selectedMedia.setValue(selectedMedia.getValue());
|
||||
if (page == Page.EDITOR && getSelectedMediaOrDefault().isEmpty()) {
|
||||
error.setValue(Error.NO_ITEMS);
|
||||
} else {
|
||||
selectedMedia.setValue(selectedMedia.getValue());
|
||||
}
|
||||
|
||||
if (getSelectedMediaOrDefault().size() > 0) {
|
||||
this.position.setValue(Math.min(position, getSelectedMediaOrDefault().size() - 1));
|
||||
}
|
||||
|
||||
hudState.setValue(buildHudState());
|
||||
}
|
||||
|
||||
void onImageCaptured(@NonNull Media media) {
|
||||
lastCameraCapture = Optional.of(media);
|
||||
|
||||
List<Media> selected = selectedMedia.getValue();
|
||||
|
||||
if (selected == null) {
|
||||
|
@ -216,40 +344,32 @@ class MediaSendViewModel extends ViewModel {
|
|||
return;
|
||||
}
|
||||
|
||||
lastImageCapture = Optional.of(media);
|
||||
|
||||
selected.add(media);
|
||||
selectedMedia.setValue(selected);
|
||||
position.setValue(selected.size() - 1);
|
||||
bucketId.setValue(Media.ALL_MEDIA_BUCKET_ID);
|
||||
|
||||
if (selected.size() == 1) {
|
||||
countButtonVisibility = CountButtonState.Visibility.FORCED_OFF;
|
||||
} else {
|
||||
countButtonVisibility = CountButtonState.Visibility.CONDITIONAL;
|
||||
}
|
||||
|
||||
countButtonState.setValue(new CountButtonState(selected.size(), countButtonVisibility));
|
||||
}
|
||||
|
||||
void onImageCaptureUndo(@NonNull Context context) {
|
||||
List<Media> selected = getSelectedMediaOrDefault();
|
||||
|
||||
if (lastImageCapture.isPresent() && selected.contains(lastImageCapture.get()) && selected.size() == 1) {
|
||||
selected.remove(lastImageCapture.get());
|
||||
if (lastCameraCapture.isPresent() && selected.contains(lastCameraCapture.get()) && selected.size() == 1) {
|
||||
selected.remove(lastCameraCapture.get());
|
||||
selectedMedia.setValue(selected);
|
||||
countButtonState.setValue(new CountButtonState(selected.size(), countButtonVisibility));
|
||||
BlobProvider.getInstance().delete(context, lastImageCapture.get().getUri());
|
||||
BlobProvider.getInstance().delete(context, lastCameraCapture.get().getUri());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
void onCaptionChanged(@NonNull String newCaption) {
|
||||
if (position.getValue() >= 0 && !Util.isEmpty(selectedMedia.getValue())) {
|
||||
selectedMedia.getValue().get(position.getValue()).setCaption(TextUtils.isEmpty(newCaption) ? null : newCaption);
|
||||
}
|
||||
}
|
||||
|
||||
void onCameraControlsInitialized() {
|
||||
repository.getMostRecentItem(application, mostRecentMedia::postValue);
|
||||
}
|
||||
|
||||
void saveDrawState(@NonNull Map<Uri, Object> state) {
|
||||
savedDrawState.clear();
|
||||
savedDrawState.putAll(state);
|
||||
|
@ -277,12 +397,8 @@ class MediaSendViewModel extends ViewModel {
|
|||
return folders;
|
||||
}
|
||||
|
||||
@NonNull LiveData<CountButtonState> getCountButtonState() {
|
||||
return countButtonState;
|
||||
}
|
||||
|
||||
@NonNull LiveData<Boolean> getCameraButtonVisibility() {
|
||||
return cameraButtonVisibility;
|
||||
@NonNull LiveData<Optional<Media>> getMostRecentMediaItem(@NonNull Context context) {
|
||||
return mostRecentMedia;
|
||||
}
|
||||
|
||||
@NonNull CharSequence getBody() {
|
||||
|
@ -301,10 +417,18 @@ class MediaSendViewModel extends ViewModel {
|
|||
return error;
|
||||
}
|
||||
|
||||
@NonNull LiveData<HudState> getHudState() {
|
||||
return hudState;
|
||||
}
|
||||
|
||||
int getMaxSelection() {
|
||||
return maxSelection;
|
||||
}
|
||||
|
||||
long getRevealDuration() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
private @NonNull List<Media> getSelectedMediaOrDefault() {
|
||||
return selectedMedia.getValue() == null ? Collections.emptyList()
|
||||
: selectedMedia.getValue();
|
||||
|
@ -322,44 +446,102 @@ class MediaSendViewModel extends ViewModel {
|
|||
|
||||
}
|
||||
|
||||
private HudState buildHudState() {
|
||||
List<Media> selectedMedia = getSelectedMediaOrDefault();
|
||||
int selectionCount = selectedMedia.size();
|
||||
ButtonState updatedButtonState = buttonState == ButtonState.COUNT && selectionCount == 0 ? ButtonState.GONE : buttonState;
|
||||
boolean updatdCaptionVisible = captionVisible && (selectedMedia.size() > 1 || (selectedMedia.size() > 0 && selectedMedia.get(0).getCaption().isPresent()));
|
||||
|
||||
return new HudState(hudVisible, composeVisible, updatdCaptionVisible, selectionCount, updatedButtonState, railState, timerState);
|
||||
}
|
||||
|
||||
private void clearPersistedMedia() {
|
||||
Stream.of(getSelectedMediaOrDefault())
|
||||
.map(Media::getUri)
|
||||
.filter(BlobProvider::isAuthority)
|
||||
.forEach(uri -> BlobProvider.getInstance().delete(application.getApplicationContext(), uri));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onCleared() {
|
||||
if (!sentMedia) {
|
||||
Stream.of(getSelectedMediaOrDefault())
|
||||
.map(Media::getUri)
|
||||
.filter(BlobProvider::isAuthority)
|
||||
.forEach(uri -> BlobProvider.getInstance().delete(application.getApplicationContext(), uri));
|
||||
clearPersistedMedia();
|
||||
}
|
||||
}
|
||||
|
||||
enum Error {
|
||||
ITEM_TOO_LARGE, TOO_MANY_ITEMS
|
||||
ITEM_TOO_LARGE, TOO_MANY_ITEMS, NO_ITEMS
|
||||
}
|
||||
|
||||
static class CountButtonState {
|
||||
private final int count;
|
||||
private final Visibility visibility;
|
||||
enum Page {
|
||||
CAMERA, ITEM_PICKER, FOLDER_PICKER, EDITOR, UNKNOWN
|
||||
}
|
||||
|
||||
private CountButtonState(int count, @NonNull Visibility visibility) {
|
||||
this.count = count;
|
||||
this.visibility = visibility;
|
||||
enum ButtonState {
|
||||
COUNT, SEND, GONE
|
||||
}
|
||||
|
||||
enum RailState {
|
||||
INTERACTIVE, VIEWABLE, GONE
|
||||
}
|
||||
|
||||
enum TimerState {
|
||||
ENABLED, DISABLED, GONE
|
||||
}
|
||||
|
||||
static class HudState {
|
||||
|
||||
private final boolean hudVisible;
|
||||
private final boolean composeVisible;
|
||||
private final boolean captionVisible;
|
||||
private final int selectionCount;
|
||||
private final ButtonState buttonState;
|
||||
private final RailState railState;
|
||||
private final TimerState timerState;
|
||||
|
||||
HudState(boolean hudVisible,
|
||||
boolean composeVisible,
|
||||
boolean captionVisible,
|
||||
int selectionCount,
|
||||
@NonNull ButtonState buttonState,
|
||||
@NonNull RailState railState,
|
||||
@NonNull TimerState timerState)
|
||||
{
|
||||
this.hudVisible = hudVisible;
|
||||
this.composeVisible = composeVisible;
|
||||
this.captionVisible = captionVisible;
|
||||
this.selectionCount = selectionCount;
|
||||
this.buttonState = buttonState;
|
||||
this.railState = railState;
|
||||
this.timerState = timerState;
|
||||
}
|
||||
|
||||
int getCount() {
|
||||
return count;
|
||||
public boolean isHudVisible() {
|
||||
return hudVisible;
|
||||
}
|
||||
|
||||
boolean isVisible() {
|
||||
switch (visibility) {
|
||||
case FORCED_ON: return true;
|
||||
case FORCED_OFF: return false;
|
||||
case CONDITIONAL: return count > 0;
|
||||
default: return false;
|
||||
}
|
||||
public boolean isComposeVisible() {
|
||||
return hudVisible && composeVisible;
|
||||
}
|
||||
|
||||
enum Visibility {
|
||||
CONDITIONAL, FORCED_ON, FORCED_OFF
|
||||
public boolean isCaptionVisible() {
|
||||
return hudVisible && captionVisible;
|
||||
}
|
||||
|
||||
public int getSelectionCount() {
|
||||
return selectionCount;
|
||||
}
|
||||
|
||||
public @NonNull ButtonState getButtonState() {
|
||||
return buttonState;
|
||||
}
|
||||
|
||||
public @NonNull RailState getRailState() {
|
||||
return hudVisible ? railState : RailState.GONE;
|
||||
}
|
||||
|
||||
public @NonNull TimerState getTimerState() {
|
||||
return hudVisible ? timerState : TimerState.GONE;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -20,7 +20,7 @@ public class SimpleTask {
|
|||
return;
|
||||
}
|
||||
|
||||
AsyncTask.THREAD_POOL_EXECUTOR.execute(() -> {
|
||||
SignalExecutors.BOUNDED.execute(() -> {
|
||||
final E result = backgroundTask.run();
|
||||
|
||||
if (isValid(lifecycle)) {
|
||||
|
@ -38,7 +38,7 @@ public class SimpleTask {
|
|||
* the main thread. Essentially {@link AsyncTask}, but lambda-compatible.
|
||||
*/
|
||||
public static <E> void run(@NonNull BackgroundTask<E> backgroundTask, @NonNull ForegroundTask<E> foregroundTask) {
|
||||
AsyncTask.THREAD_POOL_EXECUTOR.execute(() -> {
|
||||
SignalExecutors.BOUNDED.execute(() -> {
|
||||
final E result = backgroundTask.run();
|
||||
Util.runOnMain(() -> foregroundTask.run(result));
|
||||
});
|
||||
|
|
Loading…
Add table
Reference in a new issue