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>
|
<item>
|
||||||
<shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="rectangle" >
|
<shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="rectangle" >
|
||||||
<corners android:radius="5dp" />
|
<corners android:radius="5dp" />
|
||||||
<solid android:color="@color/transparent_black_70"/>
|
<solid android:color="@color/transparent_white_40"/>
|
||||||
</shape>
|
</shape>
|
||||||
</item>
|
</item>
|
||||||
</ripple>
|
</ripple>
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="rectangle" >
|
<shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="rectangle" >
|
||||||
<corners android:radius="5dp" />
|
<corners android:radius="5dp" />
|
||||||
<solid android:color="@color/transparent_black_70"/>
|
<solid android:color="@color/transparent_white_40"/>
|
||||||
</shape>
|
</shape>
|
|
@ -1,32 +1,60 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<RelativeLayout
|
<androidx.constraintlayout.widget.ConstraintLayout
|
||||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
xmlns:tools="http://schemas.android.com/tools"
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
android:layout_width="wrap_content"
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
android:layout_height="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_marginEnd="16dp"
|
android:layout_height="match_parent">
|
||||||
android:layout_gravity="end">
|
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
android:id="@+id/camera_capture_button"
|
android:id="@+id/camera_capture_button"
|
||||||
android:layout_width="80dp"
|
android:layout_width="80dp"
|
||||||
android:layout_height="80dp"
|
android:layout_height="80dp"
|
||||||
android:layout_centerHorizontal="true"
|
android:layout_marginEnd="24dp"
|
||||||
android:layout_centerVertical="true"
|
android:background="@drawable/ic_camera_shutter"
|
||||||
android:background="@drawable/ic_camera_shutter" />
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"/>
|
||||||
|
|
||||||
<ImageButton
|
<ImageButton
|
||||||
android:id="@+id/camera_flip_button"
|
android:id="@+id/camera_flip_button"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_above="@+id/camera_capture_button"
|
android:layout_marginStart="16dp"
|
||||||
android:layout_marginBottom="40dp"
|
android:layout_marginTop="14dp"
|
||||||
android:layout_centerHorizontal="true"
|
|
||||||
android:src="@drawable/ic_switch_camera_32"
|
android:src="@drawable/ic_switch_camera_32"
|
||||||
android:scaleType="fitCenter"
|
android:scaleType="fitCenter"
|
||||||
android:background="?selectableItemBackgroundBorderless"
|
android:background="?selectableItemBackgroundBorderless"
|
||||||
android:visibility="gone"
|
android:visibility="gone"
|
||||||
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
tools:visibility="visible" />
|
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"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<RelativeLayout
|
<androidx.constraintlayout.widget.ConstraintLayout
|
||||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
xmlns:tools="http://schemas.android.com/tools"
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="match_parent">
|
||||||
android:layout_marginBottom="16dp"
|
|
||||||
android:layout_gravity="bottom">
|
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
android:id="@+id/camera_capture_button"
|
android:id="@+id/camera_capture_button"
|
||||||
android:layout_width="80dp"
|
android:layout_width="80dp"
|
||||||
android:layout_height="80dp"
|
android:layout_height="80dp"
|
||||||
android:layout_centerHorizontal="true"
|
android:layout_marginBottom="24dp"
|
||||||
android:layout_centerVertical="true"
|
android:background="@drawable/ic_camera_shutter"
|
||||||
android:background="@drawable/ic_camera_shutter" />
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
|
app:layout_constraintStart_toStartOf="parent" />
|
||||||
|
|
||||||
<ImageButton
|
<ImageButton
|
||||||
android:id="@+id/camera_flip_button"
|
android:id="@+id/camera_flip_button"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_toStartOf="@+id/camera_capture_button"
|
android:layout_marginEnd="16dp"
|
||||||
android:layout_marginEnd="40dp"
|
android:layout_marginTop="14dp"
|
||||||
android:layout_centerVertical="true"
|
|
||||||
android:src="@drawable/ic_switch_camera_32"
|
android:src="@drawable/ic_switch_camera_32"
|
||||||
android:scaleType="fitCenter"
|
android:scaleType="fitCenter"
|
||||||
android:background="?selectableItemBackgroundBorderless"
|
android:background="?selectableItemBackgroundBorderless"
|
||||||
android:visibility="gone"
|
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" />
|
tools:visibility="visible" />
|
||||||
|
|
||||||
</RelativeLayout>
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
|
|
||||||
|
|
|
@ -19,6 +19,8 @@
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
android:layout_marginStart="2dp"
|
android:layout_marginStart="2dp"
|
||||||
android:layout_marginTop="2dp" />
|
android:layout_marginTop="2dp"
|
||||||
|
android:paddingBottom="@dimen/media_picker_rail_padding_affordance"
|
||||||
|
android:clipToPadding="false" />
|
||||||
|
|
||||||
</LinearLayout>
|
</LinearLayout>
|
|
@ -19,6 +19,8 @@
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
android:layout_marginStart="2dp"
|
android:layout_marginStart="2dp"
|
||||||
android:layout_marginTop="2dp" />
|
android:layout_marginTop="2dp"
|
||||||
|
android:paddingBottom="@dimen/media_picker_rail_padding_affordance"
|
||||||
|
android:clipToPadding="false"/>
|
||||||
|
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
|
|
@ -2,6 +2,7 @@
|
||||||
<FrameLayout
|
<FrameLayout
|
||||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
xmlns:tools="http://schemas.android.com/tools"
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
android:orientation="vertical"
|
android:orientation="vertical"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent">
|
android:layout_height="match_parent">
|
||||||
|
@ -11,59 +12,152 @@
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent" />
|
android:layout_height="match_parent" />
|
||||||
|
|
||||||
<LinearLayout
|
<org.thoughtcrime.securesms.components.InputAwareLayout
|
||||||
android:id="@+id/mediasend_count_button"
|
android:id="@+id/mediasend_hud"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="match_parent"
|
||||||
android:layout_marginBottom="32dp"
|
android:animateLayoutChanges="true">
|
||||||
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">
|
|
||||||
|
|
||||||
<TextView
|
<LinearLayout
|
||||||
android:id="@+id/mediasend_count_button_text"
|
android:id="@+id/mediasend_caption_and_rail"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:minWidth="28dp"
|
android:layout_gravity="bottom"
|
||||||
android:paddingStart="7dp"
|
android:orientation="vertical"
|
||||||
android:paddingEnd="7dp"
|
android:background="@color/transparent_black_70">
|
||||||
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" />
|
|
||||||
|
|
||||||
|
<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
|
<androidx.recyclerview.widget.RecyclerView
|
||||||
android:layout_width="wrap_content"
|
android:id="@+id/mediasend_media_rail"
|
||||||
android:layout_height="20dp"
|
android:layout_width="match_parent"
|
||||||
android:layout_marginStart="2dp"
|
android:layout_height="wrap_content"
|
||||||
android:src="@drawable/ic_arrow_right"
|
android:layout_marginTop="2dp"
|
||||||
android:tint="@color/core_white"/>
|
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
|
<LinearLayout
|
||||||
android:id="@+id/mediasend_camera_button"
|
android:id="@+id/mediasend_compose_container"
|
||||||
android:layout_width="44dp"
|
android:layout_width="0dp"
|
||||||
android:layout_height="44dp"
|
android:layout_height="wrap_content"
|
||||||
android:layout_marginBottom="32dp"
|
android:layout_weight="1"
|
||||||
android:layout_marginStart="32dp"
|
android:paddingStart="8dp"
|
||||||
android:layout_gravity="bottom|start"
|
android:paddingEnd="8dp"
|
||||||
android:padding="12dp"
|
android:orientation="horizontal"
|
||||||
android:src="@drawable/ic_camera_filled_24"
|
android:background="@drawable/compose_background_camera">
|
||||||
android:tint="@color/core_grey_60"
|
|
||||||
android:background="@drawable/media_camera_button_background"
|
<org.thoughtcrime.securesms.components.emoji.EmojiToggle
|
||||||
android:elevation="4dp"
|
android:id="@+id/mediasend_emoji_toggle"
|
||||||
android:visibility="gone"
|
android:layout_width="wrap_content"
|
||||||
tools:visibility="visible"/>
|
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>
|
</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"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<FrameLayout
|
<FrameLayout
|
||||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
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_width="match_parent"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
android:background="@color/core_black">
|
android:background="@color/core_black">
|
||||||
|
@ -19,138 +17,4 @@
|
||||||
android:animateLayoutChanges="true"
|
android:animateLayoutChanges="true"
|
||||||
android:layout_gravity="top"/>
|
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>
|
</FrameLayout>
|
|
@ -5,9 +5,9 @@
|
||||||
|
|
||||||
<item
|
<item
|
||||||
android:title=""
|
android:title=""
|
||||||
android:id="@+id/mediapicker_menu_add"
|
android:id="@+id/mediapicker_menu_camera"
|
||||||
android:visible="true"
|
android:visible="true"
|
||||||
android:icon="@drawable/ic_create_album_outline_32"
|
android:icon="@drawable/ic_camera_alt_white_24dp"
|
||||||
app:showAsAction="always" />
|
app:showAsAction="always" />
|
||||||
|
|
||||||
</menu>
|
</menu>
|
|
@ -46,6 +46,7 @@
|
||||||
|
|
||||||
<dimen name="media_picker_folder_width">175dp</dimen>
|
<dimen name="media_picker_folder_width">175dp</dimen>
|
||||||
<dimen name="media_picker_item_width">85dp</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_padding">5dp</dimen>
|
||||||
<dimen name="media_keyboard_provider_icon_margin">4dp</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 GlideRequests glideRequests;
|
||||||
private final List<Media> media;
|
private final List<Media> media;
|
||||||
private final RailItemListener listener;
|
private final RailItemListener listener;
|
||||||
private final boolean editable;
|
|
||||||
private final StableIdGenerator<Media> stableIdGenerator;
|
private final StableIdGenerator<Media> stableIdGenerator;
|
||||||
|
|
||||||
private RailItemAddListener addListener;
|
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) {
|
public MediaRailAdapter(@NonNull GlideRequests glideRequests, @NonNull RailItemListener listener, boolean editable) {
|
||||||
this.glideRequests = glideRequests;
|
this.glideRequests = glideRequests;
|
||||||
|
@ -36,6 +37,7 @@ public class MediaRailAdapter extends RecyclerView.Adapter<MediaRailAdapter.Medi
|
||||||
this.listener = listener;
|
this.listener = listener;
|
||||||
this.editable = editable;
|
this.editable = editable;
|
||||||
this.stableIdGenerator = new StableIdGenerator<>();
|
this.stableIdGenerator = new StableIdGenerator<>();
|
||||||
|
this.interactive = true;
|
||||||
|
|
||||||
setHasStableIds(true);
|
setHasStableIds(true);
|
||||||
}
|
}
|
||||||
|
@ -57,7 +59,7 @@ public class MediaRailAdapter extends RecyclerView.Adapter<MediaRailAdapter.Medi
|
||||||
public void onBindViewHolder(@NonNull MediaRailViewHolder viewHolder, int i) {
|
public void onBindViewHolder(@NonNull MediaRailViewHolder viewHolder, int i) {
|
||||||
switch (getItemViewType(i)) {
|
switch (getItemViewType(i)) {
|
||||||
case TYPE_MEDIA:
|
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;
|
break;
|
||||||
case TYPE_BUTTON:
|
case TYPE_BUTTON:
|
||||||
((ButtonViewHolder) viewHolder).bind(addListener);
|
((ButtonViewHolder) viewHolder).bind(addListener);
|
||||||
|
@ -121,6 +123,16 @@ public class MediaRailAdapter extends RecyclerView.Adapter<MediaRailAdapter.Medi
|
||||||
notifyDataSetChanged();
|
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 {
|
static abstract class MediaRailViewHolder extends RecyclerView.ViewHolder {
|
||||||
public MediaRailViewHolder(@NonNull View itemView) {
|
public MediaRailViewHolder(@NonNull View itemView) {
|
||||||
super(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,
|
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.setImageResource(glideRequests, media.getUri());
|
||||||
image.setOnClickListener(v -> railItemListener.onRailItemClicked(distanceFromActive));
|
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);
|
captionIndicator.setVisibility(media.getCaption().isPresent() ? View.VISIBLE : View.GONE);
|
||||||
|
|
||||||
if (editable && isActive) {
|
if (editable && isActive && interactive) {
|
||||||
deleteButton.setVisibility(View.VISIBLE);
|
deleteButton.setVisibility(View.VISIBLE);
|
||||||
deleteButton.setOnClickListener(v -> railItemListener.onRailItemDeleteClicked(distanceFromActive));
|
deleteButton.setOnClickListener(v -> railItemListener.onRailItemDeleteClicked(distanceFromActive));
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -27,7 +27,10 @@ import android.view.animation.DecelerateInterpolator;
|
||||||
import android.view.animation.RotateAnimation;
|
import android.view.animation.RotateAnimation;
|
||||||
import android.widget.Button;
|
import android.widget.Button;
|
||||||
import android.widget.ImageButton;
|
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.MultiTransformation;
|
||||||
import com.bumptech.glide.load.Transformation;
|
import com.bumptech.glide.load.Transformation;
|
||||||
import com.bumptech.glide.load.resource.bitmap.CenterCrop;
|
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.R;
|
||||||
import org.thoughtcrime.securesms.logging.Log;
|
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.mms.GlideApp;
|
||||||
import org.thoughtcrime.securesms.util.ServiceUtil;
|
import org.thoughtcrime.securesms.util.ServiceUtil;
|
||||||
import org.thoughtcrime.securesms.util.Stopwatch;
|
import org.thoughtcrime.securesms.util.Stopwatch;
|
||||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||||
|
import org.whispersystems.libsignal.util.guava.Optional;
|
||||||
|
|
||||||
import java.io.ByteArrayOutputStream;
|
import java.io.ByteArrayOutputStream;
|
||||||
|
|
||||||
|
@ -106,6 +112,9 @@ public class Camera1Fragment extends Fragment implements CameraFragment,
|
||||||
|
|
||||||
GestureDetector gestureDetector = new GestureDetector(flipGestureListener);
|
GestureDetector gestureDetector = new GestureDetector(flipGestureListener);
|
||||||
cameraPreview.setOnTouchListener((v, event) -> gestureDetector.onTouchEvent(event));
|
cameraPreview.setOnTouchListener((v, event) -> gestureDetector.onTouchEvent(event));
|
||||||
|
|
||||||
|
viewModel.getMostRecentMediaItem(requireContext()).observe(this, this::presentRecentItemThumbnail);
|
||||||
|
viewModel.getHudState().observe(this, this::presentHud);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -128,9 +137,6 @@ public class Camera1Fragment extends Fragment implements CameraFragment,
|
||||||
});
|
});
|
||||||
|
|
||||||
orderEnforcer.run(Stage.CAMERA_PROPERTIES_AVAILABLE, this::updatePreviewScale);
|
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
|
@Override
|
||||||
|
@ -180,10 +186,46 @@ public class Camera1Fragment extends Fragment implements CameraFragment,
|
||||||
controller.onCameraError();
|
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")
|
@SuppressLint("ClickableViewAccessibility")
|
||||||
private void initControls() {
|
private void initControls() {
|
||||||
flipButton = getView().findViewById(R.id.camera_flip_button);
|
flipButton = requireView().findViewById(R.id.camera_flip_button);
|
||||||
captureButton = getView().findViewById(R.id.camera_capture_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) -> {
|
captureButton.setOnTouchListener((v, event) -> {
|
||||||
switch (event.getAction()) {
|
switch (event.getAction()) {
|
||||||
|
@ -223,6 +265,11 @@ public class Camera1Fragment extends Fragment implements CameraFragment,
|
||||||
flipButton.setVisibility(View.GONE);
|
flipButton.setVisibility(View.GONE);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
galleryButton.setOnClickListener(v -> controller.onGalleryClicked());
|
||||||
|
countButton.setOnClickListener(v -> controller.onContinueClicked());
|
||||||
|
|
||||||
|
viewModel.onCameraControlsInitialized();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void onCaptureClicked() {
|
private void onCaptureClicked() {
|
||||||
|
|
|
@ -21,6 +21,8 @@ public interface CameraFragment {
|
||||||
interface Controller {
|
interface Controller {
|
||||||
void onCameraError();
|
void onCameraError();
|
||||||
void onImageCaptured(@NonNull byte[] data, int width, int height);
|
void onImageCaptured(@NonNull byte[] data, int width, int height);
|
||||||
|
void onGalleryClicked();
|
||||||
int getDisplayRotation();
|
int getDisplayRotation();
|
||||||
|
void onContinueClicked();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,6 +4,7 @@ import android.annotation.SuppressLint;
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.content.res.Configuration;
|
import android.content.res.Configuration;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
|
import android.view.GestureDetector;
|
||||||
import android.view.LayoutInflater;
|
import android.view.LayoutInflater;
|
||||||
import android.view.MotionEvent;
|
import android.view.MotionEvent;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
|
@ -13,6 +14,8 @@ import android.view.animation.Animation;
|
||||||
import android.view.animation.AnimationUtils;
|
import android.view.animation.AnimationUtils;
|
||||||
import android.view.animation.DecelerateInterpolator;
|
import android.view.animation.DecelerateInterpolator;
|
||||||
import android.view.animation.RotateAnimation;
|
import android.view.animation.RotateAnimation;
|
||||||
|
import android.widget.ImageView;
|
||||||
|
import android.widget.TextView;
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
|
@ -23,13 +26,17 @@ import androidx.camera.core.ImageProxy;
|
||||||
import androidx.fragment.app.Fragment;
|
import androidx.fragment.app.Fragment;
|
||||||
import androidx.lifecycle.ViewModelProviders;
|
import androidx.lifecycle.ViewModelProviders;
|
||||||
|
|
||||||
|
import com.bumptech.glide.Glide;
|
||||||
|
|
||||||
import org.thoughtcrime.securesms.R;
|
import org.thoughtcrime.securesms.R;
|
||||||
import org.thoughtcrime.securesms.logging.Log;
|
import org.thoughtcrime.securesms.logging.Log;
|
||||||
import org.thoughtcrime.securesms.mediasend.camerax.CameraXUtil;
|
import org.thoughtcrime.securesms.mediasend.camerax.CameraXUtil;
|
||||||
import org.thoughtcrime.securesms.mediasend.camerax.CameraXView;
|
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.Stopwatch;
|
||||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||||
import org.thoughtcrime.securesms.util.concurrent.SimpleTask;
|
import org.thoughtcrime.securesms.util.concurrent.SimpleTask;
|
||||||
|
import org.whispersystems.libsignal.util.guava.Optional;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
|
||||||
|
@ -84,6 +91,9 @@ public class CameraXFragment extends Fragment implements CameraFragment {
|
||||||
camera.setCameraLensFacing(CameraXUtil.toLensFacing(TextSecurePreferences.getDirectCaptureCameraId(requireContext())));
|
camera.setCameraLensFacing(CameraXUtil.toLensFacing(TextSecurePreferences.getDirectCaptureCameraId(requireContext())));
|
||||||
|
|
||||||
onOrientationChanged(getResources().getConfiguration().orientation);
|
onOrientationChanged(getResources().getConfiguration().orientation);
|
||||||
|
|
||||||
|
viewModel.getMostRecentMediaItem(requireContext()).observe(this, this::presentRecentItemThumbnail);
|
||||||
|
viewModel.getHudState().observe(this, this::presentHud);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -116,10 +126,45 @@ public class CameraXFragment extends Fragment implements CameraFragment {
|
||||||
initControls();
|
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"})
|
@SuppressLint({"ClickableViewAccessibility", "MissingPermission"})
|
||||||
private void initControls() {
|
private void initControls() {
|
||||||
View flipButton = requireView().findViewById(R.id.camera_flip_button);
|
View flipButton = requireView().findViewById(R.id.camera_flip_button);
|
||||||
View captureButton = requireView().findViewById(R.id.camera_capture_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) -> {
|
captureButton.setOnTouchListener((v, event) -> {
|
||||||
switch (event.getAction()) {
|
switch (event.getAction()) {
|
||||||
|
@ -154,9 +199,26 @@ public class CameraXFragment extends Fragment implements CameraFragment {
|
||||||
animation.setInterpolator(new DecelerateInterpolator());
|
animation.setInterpolator(new DecelerateInterpolator());
|
||||||
flipButton.startAnimation(animation);
|
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 {
|
} else {
|
||||||
flipButton.setVisibility(View.GONE);
|
flipButton.setVisibility(View.GONE);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
galleryButton.setOnClickListener(v -> controller.onGalleryClicked());
|
||||||
|
countButton.setOnClickListener(v -> controller.onContinueClicked());
|
||||||
|
|
||||||
|
viewModel.onCameraControlsInitialized();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void onCaptureClicked() {
|
private void onCaptureClicked() {
|
||||||
|
|
|
@ -13,6 +13,8 @@ import androidx.recyclerview.widget.GridLayoutManager;
|
||||||
import androidx.recyclerview.widget.RecyclerView;
|
import androidx.recyclerview.widget.RecyclerView;
|
||||||
import androidx.appcompat.widget.Toolbar;
|
import androidx.appcompat.widget.Toolbar;
|
||||||
import android.view.LayoutInflater;
|
import android.view.LayoutInflater;
|
||||||
|
import android.view.Menu;
|
||||||
|
import android.view.MenuItem;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
import android.view.ViewGroup;
|
import android.view.ViewGroup;
|
||||||
import android.view.WindowManager;
|
import android.view.WindowManager;
|
||||||
|
@ -51,6 +53,8 @@ public class MediaPickerFolderFragment extends Fragment implements MediaPickerFo
|
||||||
@Override
|
@Override
|
||||||
public void onCreate(@Nullable Bundle savedInstanceState) {
|
public void onCreate(@Nullable Bundle savedInstanceState) {
|
||||||
super.onCreate(savedInstanceState);
|
super.onCreate(savedInstanceState);
|
||||||
|
setHasOptionsMenu(true);
|
||||||
|
|
||||||
recipientName = getArguments().getString(KEY_RECIPIENT_NAME);
|
recipientName = getArguments().getString(KEY_RECIPIENT_NAME);
|
||||||
viewModel = ViewModelProviders.of(requireActivity(), new MediaSendViewModel.Factory(requireActivity().getApplication(), new MediaRepository())).get(MediaSendViewModel.class);
|
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
|
@Override
|
||||||
public void onResume() {
|
public void onResume() {
|
||||||
super.onResume();
|
super.onResume();
|
||||||
|
|
||||||
viewModel.onFolderPickerStarted();
|
viewModel.onFolderPickerStarted();
|
||||||
requireActivity().getWindow().addFlags(WindowManager.LayoutParams.FLAG_FORCE_NOT_FULLSCREEN);
|
|
||||||
requireActivity().getWindow().clearFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@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);
|
super.onConfigurationChanged(newConfig);
|
||||||
onScreenWidthChanged(getScreenWidth());
|
onScreenWidthChanged(getScreenWidth());
|
||||||
}
|
}
|
||||||
|
@ -131,5 +147,6 @@ public class MediaPickerFolderFragment extends Fragment implements MediaPickerFo
|
||||||
|
|
||||||
public interface Controller {
|
public interface Controller {
|
||||||
void onFolderSelected(@NonNull MediaFolder folder);
|
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);
|
viewModel.getMediaInBucket(requireContext(), bucketId).observe(this, adapter::setMedia);
|
||||||
|
|
||||||
initMediaObserver(viewModel);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onResume() {
|
public void onResume() {
|
||||||
super.onResume();
|
super.onResume();
|
||||||
|
|
||||||
viewModel.onItemPickerStarted();
|
viewModel.onItemPickerStarted();
|
||||||
requireActivity().getWindow().addFlags(WindowManager.LayoutParams.FLAG_FORCE_NOT_FULLSCREEN);
|
adapter.setForcedMultiSelect(true);
|
||||||
requireActivity().getWindow().clearFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN);
|
viewModel.onMultiSelectStarted();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onPrepareOptionsMenu(Menu menu) {
|
public void onPrepareOptionsMenu(@NonNull Menu menu) {
|
||||||
requireActivity().getMenuInflater().inflate(R.menu.mediapicker_default, 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
|
@Override
|
||||||
public boolean onOptionsItemSelected(MenuItem item) {
|
public boolean onOptionsItemSelected(@NonNull MenuItem item) {
|
||||||
switch (item.getItemId()) {
|
switch (item.getItemId()) {
|
||||||
case R.id.mediapicker_menu_add:
|
case R.id.mediapicker_menu_camera:
|
||||||
adapter.setForcedMultiSelect(true);
|
controller.onCameraSelected();
|
||||||
viewModel.onMultiSelectStarted();
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onConfigurationChanged(Configuration newConfig) {
|
public void onConfigurationChanged(@NonNull Configuration newConfig) {
|
||||||
super.onConfigurationChanged(newConfig);
|
super.onConfigurationChanged(newConfig);
|
||||||
onScreenWidthChanged(getScreenWidth());
|
onScreenWidthChanged(getScreenWidth());
|
||||||
}
|
}
|
||||||
|
@ -172,12 +164,6 @@ public class MediaPickerItemFragment extends Fragment implements MediaPickerItem
|
||||||
toolbar.setNavigationOnClickListener(v -> requireActivity().onBackPressed());
|
toolbar.setNavigationOnClickListener(v -> requireActivity().onBackPressed());
|
||||||
}
|
}
|
||||||
|
|
||||||
private void initMediaObserver(@NonNull MediaSendViewModel viewModel) {
|
|
||||||
viewModel.getCountButtonState().observe(this, media -> {
|
|
||||||
requireActivity().invalidateOptionsMenu();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private void onScreenWidthChanged(int newWidth) {
|
private void onScreenWidthChanged(int newWidth) {
|
||||||
if (layoutManager != null) {
|
if (layoutManager != null) {
|
||||||
layoutManager.setSpanCount(newWidth / getResources().getDimensionPixelSize(R.dimen.media_picker_item_width));
|
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 {
|
public interface Controller {
|
||||||
void onMediaSelected(@NonNull Media media);
|
void onMediaSelected(@NonNull Media media);
|
||||||
|
void onCameraSelected();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,7 +4,6 @@ import android.annotation.TargetApi;
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.database.Cursor;
|
import android.database.Cursor;
|
||||||
import android.net.Uri;
|
import android.net.Uri;
|
||||||
import android.os.AsyncTask;
|
|
||||||
import android.os.Environment;
|
import android.os.Environment;
|
||||||
import android.provider.MediaStore.Images;
|
import android.provider.MediaStore.Images;
|
||||||
import android.provider.MediaStore.Video;
|
import android.provider.MediaStore.Video;
|
||||||
|
@ -20,6 +19,7 @@ import org.thoughtcrime.securesms.R;
|
||||||
import org.thoughtcrime.securesms.mms.PartAuthority;
|
import org.thoughtcrime.securesms.mms.PartAuthority;
|
||||||
import org.thoughtcrime.securesms.util.MediaUtil;
|
import org.thoughtcrime.securesms.util.MediaUtil;
|
||||||
import org.thoughtcrime.securesms.util.Util;
|
import org.thoughtcrime.securesms.util.Util;
|
||||||
|
import org.thoughtcrime.securesms.util.concurrent.SignalExecutors;
|
||||||
import org.whispersystems.libsignal.util.guava.Optional;
|
import org.whispersystems.libsignal.util.guava.Optional;
|
||||||
|
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
|
@ -40,14 +40,14 @@ class MediaRepository {
|
||||||
* Retrieves a list of folders that contain media.
|
* Retrieves a list of folders that contain media.
|
||||||
*/
|
*/
|
||||||
void getFolders(@NonNull Context context, @NonNull Callback<List<MediaFolder>> callback) {
|
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.
|
* 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) {
|
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;
|
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
|
@WorkerThread
|
||||||
|
@ -158,7 +162,7 @@ class MediaRepository {
|
||||||
}
|
}
|
||||||
|
|
||||||
@WorkerThread
|
@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<>();
|
List<Media> media = new LinkedList<>();
|
||||||
String selection = Images.Media.BUCKET_ID + " = ? AND " + Images.Media.DATA + " NOT NULL";
|
String selection = Images.Media.BUCKET_ID + " = ? AND " + Images.Media.DATA + " NOT NULL";
|
||||||
String[] selectionArgs = new String[] { bucketId };
|
String[] selectionArgs = new String[] { bucketId };
|
||||||
|
@ -166,7 +170,7 @@ class MediaRepository {
|
||||||
|
|
||||||
String[] projection;
|
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};
|
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 {
|
} else {
|
||||||
projection = new String[]{Images.Media._ID, Images.Media.MIME_TYPE, Images.Media.DATE_TAKEN, Images.Media.WIDTH, Images.Media.HEIGHT, Images.Media.SIZE};
|
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)));
|
Uri uri = Uri.withAppendedPath(contentUri, cursor.getString(cursor.getColumnIndexOrThrow(Images.Media._ID)));
|
||||||
String mimetype = cursor.getString(cursor.getColumnIndexOrThrow(Images.Media.MIME_TYPE));
|
String mimetype = cursor.getString(cursor.getColumnIndexOrThrow(Images.Media.MIME_TYPE));
|
||||||
long dateTaken = cursor.getLong(cursor.getColumnIndexOrThrow(Images.Media.DATE_TAKEN));
|
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 width = cursor.getInt(cursor.getColumnIndexOrThrow(getWidthColumn(orientation)));
|
||||||
int height = cursor.getInt(cursor.getColumnIndexOrThrow(getHeightColumn(orientation)));
|
int height = cursor.getInt(cursor.getColumnIndexOrThrow(getHeightColumn(orientation)));
|
||||||
long size = cursor.getLong(cursor.getColumnIndexOrThrow(Images.Media.SIZE));
|
long size = cursor.getLong(cursor.getColumnIndexOrThrow(Images.Media.SIZE));
|
||||||
|
@ -211,6 +215,12 @@ class MediaRepository {
|
||||||
}).toList();
|
}).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)
|
@TargetApi(16)
|
||||||
@SuppressWarnings("SuspiciousNameCombination")
|
@SuppressWarnings("SuspiciousNameCombination")
|
||||||
private String getWidthColumn(int orientation) {
|
private String getWidthColumn(int orientation) {
|
||||||
|
|
|
@ -1,45 +1,76 @@
|
||||||
package org.thoughtcrime.securesms.mediasend;
|
package org.thoughtcrime.securesms.mediasend;
|
||||||
|
|
||||||
import android.Manifest;
|
import androidx.annotation.Nullable;
|
||||||
|
import androidx.appcompat.app.AlertDialog;
|
||||||
import androidx.fragment.app.FragmentTransaction;
|
import androidx.appcompat.view.ContextThemeWrapper;
|
||||||
import androidx.lifecycle.ViewModelProviders;
|
import androidx.lifecycle.ViewModelProviders;
|
||||||
|
|
||||||
|
import android.Manifest;
|
||||||
|
import android.annotation.SuppressLint;
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
|
import android.graphics.Bitmap;
|
||||||
|
import android.graphics.PorterDuff;
|
||||||
|
import android.graphics.Rect;
|
||||||
import android.net.Uri;
|
import android.net.Uri;
|
||||||
|
import android.os.AsyncTask;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
import androidx.fragment.app.Fragment;
|
import androidx.fragment.app.Fragment;
|
||||||
import androidx.fragment.app.FragmentManager;
|
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.View;
|
||||||
import android.view.animation.AccelerateDecelerateInterpolator;
|
import android.view.ViewGroup;
|
||||||
import android.view.animation.AccelerateInterpolator;
|
import android.view.ViewTreeObserver;
|
||||||
import android.view.animation.Animation;
|
import android.view.inputmethod.EditorInfo;
|
||||||
import android.view.animation.DecelerateInterpolator;
|
import android.widget.ImageView;
|
||||||
import android.view.animation.OvershootInterpolator;
|
|
||||||
import android.view.animation.ScaleAnimation;
|
|
||||||
import android.widget.TextView;
|
import android.widget.TextView;
|
||||||
import android.widget.Toast;
|
import android.widget.Toast;
|
||||||
|
|
||||||
import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity;
|
import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity;
|
||||||
import org.thoughtcrime.securesms.R;
|
import org.thoughtcrime.securesms.R;
|
||||||
import org.thoughtcrime.securesms.TransportOption;
|
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.database.Address;
|
||||||
|
import org.thoughtcrime.securesms.imageeditor.model.EditorModel;
|
||||||
import org.thoughtcrime.securesms.logging.Log;
|
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.permissions.Permissions;
|
||||||
import org.thoughtcrime.securesms.providers.BlobProvider;
|
import org.thoughtcrime.securesms.providers.BlobProvider;
|
||||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||||
import org.thoughtcrime.securesms.scribbles.ImageEditorFragment;
|
import org.thoughtcrime.securesms.scribbles.ImageEditorFragment;
|
||||||
|
import org.thoughtcrime.securesms.util.CharacterCalculator.CharacterState;
|
||||||
import org.thoughtcrime.securesms.util.DynamicLanguage;
|
import org.thoughtcrime.securesms.util.DynamicLanguage;
|
||||||
import org.thoughtcrime.securesms.util.MediaUtil;
|
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.Util;
|
||||||
|
import org.thoughtcrime.securesms.util.concurrent.SignalExecutors;
|
||||||
import org.thoughtcrime.securesms.util.concurrent.SimpleTask;
|
import org.thoughtcrime.securesms.util.concurrent.SimpleTask;
|
||||||
|
import org.thoughtcrime.securesms.util.views.Stub;
|
||||||
import org.whispersystems.libsignal.util.guava.Optional;
|
import org.whispersystems.libsignal.util.guava.Optional;
|
||||||
|
|
||||||
|
import java.io.ByteArrayOutputStream;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
|
import java.util.HashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Locale;
|
import java.util.Locale;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Encompasses the entire flow of sending media, starting from the selection process to the actual
|
* 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,
|
public class MediaSendActivity extends PassphraseRequiredActionBarActivity implements MediaPickerFolderFragment.Controller,
|
||||||
MediaPickerItemFragment.Controller,
|
MediaPickerItemFragment.Controller,
|
||||||
MediaSendFragment.Controller,
|
|
||||||
ImageEditorFragment.Controller,
|
ImageEditorFragment.Controller,
|
||||||
CameraFragment.Controller
|
CameraFragment.Controller,
|
||||||
|
ViewTreeObserver.OnGlobalLayoutListener,
|
||||||
|
MediaRailAdapter.RailItemListener,
|
||||||
|
InputAwareLayout.OnKeyboardShownListener,
|
||||||
|
InputAwareLayout.OnKeyboardHiddenListener
|
||||||
{
|
{
|
||||||
private static final String TAG = MediaSendActivity.class.getSimpleName();
|
private static final String TAG = MediaSendActivity.class.getSimpleName();
|
||||||
|
|
||||||
public static final String EXTRA_MEDIA = "media";
|
public static final String EXTRA_MEDIA = "media";
|
||||||
public static final String EXTRA_MESSAGE = "message";
|
public static final String EXTRA_MESSAGE = "message";
|
||||||
public static final String EXTRA_TRANSPORT = "transport";
|
public static final String EXTRA_TRANSPORT = "transport";
|
||||||
|
public static final String EXTRA_REVEAL_DURATION = "reveal_duration";
|
||||||
|
|
||||||
|
|
||||||
private static final String KEY_ADDRESS = "address";
|
private static final String KEY_ADDRESS = "address";
|
||||||
|
@ -78,9 +113,25 @@ public class MediaSendActivity extends PassphraseRequiredActionBarActivity imple
|
||||||
private TransportOption transport;
|
private TransportOption transport;
|
||||||
private MediaSendViewModel viewModel;
|
private MediaSendViewModel viewModel;
|
||||||
|
|
||||||
private View countButton;
|
private InputAwareLayout hud;
|
||||||
private TextView countButtonText;
|
private View captionAndRail;
|
||||||
private View cameraButton;
|
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.
|
* Get an intent to launch the media send flow starting with the picker.
|
||||||
|
@ -131,15 +182,27 @@ public class MediaSendActivity extends PassphraseRequiredActionBarActivity imple
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
countButton = findViewById(R.id.mediasend_count_button);
|
hud = findViewById(R.id.mediasend_hud);
|
||||||
countButtonText = findViewById(R.id.mediasend_count_button_text);
|
captionAndRail = findViewById(R.id.mediasend_caption_and_rail);
|
||||||
cameraButton = findViewById(R.id.mediasend_camera_button);
|
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);
|
viewModel = ViewModelProviders.of(this, new MediaSendViewModel.Factory(getApplication(), new MediaRepository())).get(MediaSendViewModel.class);
|
||||||
recipient = Recipient.from(this, Address.fromSerialized(getIntent().getStringExtra(KEY_ADDRESS)), true);
|
recipient = Recipient.from(this, Address.fromSerialized(getIntent().getStringExtra(KEY_ADDRESS)), true);
|
||||||
transport = getIntent().getParcelableExtra(KEY_TRANSPORT);
|
transport = getIntent().getParcelableExtra(KEY_TRANSPORT);
|
||||||
|
|
||||||
viewModel.setTransport(transport);
|
viewModel.setTransport(transport);
|
||||||
|
viewModel.setRecipient(recipient);
|
||||||
viewModel.onBodyChanged(getIntent().getStringExtra(KEY_BODY));
|
viewModel.onBodyChanged(getIntent().getStringExtra(KEY_BODY));
|
||||||
|
|
||||||
List<Media> media = getIntent().getParcelableArrayListExtra(KEY_MEDIA);
|
List<Media> media = getIntent().getParcelableArrayListExtra(KEY_MEDIA);
|
||||||
|
@ -165,19 +228,80 @@ public class MediaSendActivity extends PassphraseRequiredActionBarActivity imple
|
||||||
.commit();
|
.commit();
|
||||||
}
|
}
|
||||||
|
|
||||||
initializeCountButtonObserver(transport, dynamicLanguage.getCurrentLocale());
|
sendButton.setOnClickListener(v -> {
|
||||||
initializeCameraButtonObserver();
|
if (hud.isKeyboardOpen()) {
|
||||||
initializeErrorObserver();
|
hud.hideSoftkey(composeText, null);
|
||||||
|
}
|
||||||
|
|
||||||
cameraButton.setOnClickListener(v -> {
|
MediaSendFragment fragment = getMediaSendFragment();
|
||||||
int maxSelection = viewModel.getMaxSelection();
|
|
||||||
|
|
||||||
if (viewModel.getSelectedMedia().getValue() != null && viewModel.getSelectedMedia().getValue().size() >= maxSelection) {
|
if (fragment != null) {
|
||||||
Toast.makeText(this, getResources().getQuantityString(R.plurals.MediaSendActivity_cant_share_more_than_n_items, maxSelection, maxSelection), Toast.LENGTH_SHORT).show();
|
processMedia(fragment.getAllMedia(), fragment.getSavedState());
|
||||||
} else {
|
} 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
|
@Override
|
||||||
|
@ -188,13 +312,12 @@ public class MediaSendActivity extends PassphraseRequiredActionBarActivity imple
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onBackPressed() {
|
public void onBackPressed() {
|
||||||
MediaSendFragment sendFragment = (MediaSendFragment) getSupportFragmentManager().findFragmentByTag(TAG_SEND);
|
MediaSendFragment sendFragment = (MediaSendFragment) getSupportFragmentManager().findFragmentByTag(TAG_SEND);
|
||||||
if (sendFragment == null || !sendFragment.isVisible() || !sendFragment.handleBackPress()) {
|
|
||||||
super.onBackPressed();
|
|
||||||
|
|
||||||
if (getIntent().getBooleanExtra(KEY_IS_CAMERA, false) && getSupportFragmentManager().getBackStackEntryCount() == 0) {
|
if (sendFragment == null || !sendFragment.isVisible() || !hud.isInputOpen()) {
|
||||||
viewModel.onImageCaptureUndo(this);
|
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());
|
MediaPickerItemFragment fragment = MediaPickerItemFragment.newInstance(folder.getBucketId(), folder.getTitle(), viewModel.getMaxSelection());
|
||||||
getSupportFragmentManager().beginTransaction()
|
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)
|
.replace(R.id.mediasend_fragment_container, fragment, TAG_ITEM_PICKER)
|
||||||
.addToBackStack(null)
|
.addToBackStack(null)
|
||||||
.commit();
|
.commit();
|
||||||
|
@ -218,48 +340,7 @@ public class MediaSendActivity extends PassphraseRequiredActionBarActivity imple
|
||||||
@Override
|
@Override
|
||||||
public void onMediaSelected(@NonNull Media media) {
|
public void onMediaSelected(@NonNull Media media) {
|
||||||
viewModel.onSingleMediaSelected(this, media);
|
viewModel.onSingleMediaSelected(this, media);
|
||||||
navigateToMediaSend(recipient, transport, dynamicLanguage.getCurrentLocale(), false);
|
navigateToMediaSend(recipient, transport, dynamicLanguage.getCurrentLocale());
|
||||||
}
|
|
||||||
|
|
||||||
@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();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -307,7 +388,7 @@ public class MediaSendActivity extends PassphraseRequiredActionBarActivity imple
|
||||||
Log.i(TAG, "Camera capture stored: " + media.getUri().toString());
|
Log.i(TAG, "Camera capture stored: " + media.getUri().toString());
|
||||||
|
|
||||||
viewModel.onImageCaptured(media);
|
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();
|
return getWindowManager().getDefaultDisplay().getRotation();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void initializeCountButtonObserver(@NonNull TransportOption transport, @NonNull Locale locale) {
|
@Override
|
||||||
viewModel.getCountButtonState().observe(this, buttonState -> {
|
public void onContinueClicked() {
|
||||||
if (buttonState == null) return;
|
navigateToMediaSend(recipient, transport, dynamicLanguage.getCurrentLocale());
|
||||||
|
}
|
||||||
|
|
||||||
countButtonText.setText(String.valueOf(buttonState.getCount()));
|
@Override
|
||||||
countButton.setEnabled(buttonState.isVisible());
|
public void onGalleryClicked() {
|
||||||
animateButtonVisibility(countButton, countButton.getVisibility(), buttonState.isVisible() ? View.VISIBLE : View.GONE);
|
MediaPickerFolderFragment folderFragment = MediaPickerFolderFragment.newInstance(recipient);
|
||||||
|
|
||||||
if (buttonState.getCount() > 0) {
|
getSupportFragmentManager().beginTransaction()
|
||||||
countButton.setOnClickListener(v -> navigateToMediaSend(recipient, transport, locale, false));
|
.replace(R.id.mediasend_fragment_container, folderFragment, TAG_FOLDER_PICKER)
|
||||||
if (buttonState.isVisible()) {
|
.setCustomAnimations(R.anim.slide_from_bottom, R.anim.stationary, R.anim.slide_to_bottom, R.anim.stationary)
|
||||||
animateButtonTextChange(countButton);
|
.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 {
|
} 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.getSelectedMedia().observe(this, media -> {
|
||||||
viewModel.getCameraButtonVisibility().observe(this, visible -> {
|
mediaRailAdapter.setMedia(media);
|
||||||
if (visible == null) return;
|
});
|
||||||
animateButtonVisibility(cameraButton, cameraButton.getVisibility(), visible ? View.VISIBLE : View.GONE);
|
|
||||||
|
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 -> {
|
viewModel.getError().observe(this, error -> {
|
||||||
if (error == null) return;
|
if (error == null) return;
|
||||||
|
|
||||||
switch (error) {
|
switch (error) {
|
||||||
|
case NO_ITEMS:
|
||||||
|
onNoMediaAvailable();
|
||||||
|
break;
|
||||||
case ITEM_TOO_LARGE:
|
case ITEM_TOO_LARGE:
|
||||||
Toast.makeText(this, R.string.MediaSendActivity_an_item_was_removed_because_it_exceeded_the_size_limit, Toast.LENGTH_LONG).show();
|
Toast.makeText(this, R.string.MediaSendActivity_an_item_was_removed_because_it_exceeded_the_size_limit, Toast.LENGTH_LONG).show();
|
||||||
break;
|
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);
|
MediaSendFragment fragment = MediaSendFragment.newInstance(recipient, transport, locale);
|
||||||
String backstackTag = null;
|
String backstackTag = null;
|
||||||
|
|
||||||
|
@ -367,17 +614,11 @@ public class MediaSendActivity extends PassphraseRequiredActionBarActivity imple
|
||||||
backstackTag = TAG_SEND;
|
backstackTag = TAG_SEND;
|
||||||
}
|
}
|
||||||
|
|
||||||
FragmentTransaction transaction = getSupportFragmentManager().beginTransaction();
|
getSupportFragmentManager().beginTransaction()
|
||||||
|
.replace(R.id.mediasend_fragment_container, fragment, TAG_SEND)
|
||||||
if (fade) {
|
.setCustomAnimations(R.anim.fade_in, R.anim.fade_out, R.anim.fade_in, R.anim.fade_out)
|
||||||
transaction.setCustomAnimations(R.anim.fade_in, R.anim.fade_out, R.anim.fade_out, R.anim.fade_in);
|
.addToBackStack(backstackTag)
|
||||||
} else {
|
.commit();
|
||||||
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();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void navigateToCamera() {
|
private void navigateToCamera() {
|
||||||
|
@ -389,7 +630,7 @@ public class MediaSendActivity extends PassphraseRequiredActionBarActivity imple
|
||||||
.onAllGranted(() -> {
|
.onAllGranted(() -> {
|
||||||
Fragment fragment = getOrCreateCameraFragment();
|
Fragment fragment = getOrCreateCameraFragment();
|
||||||
getSupportFragmentManager().beginTransaction()
|
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)
|
.replace(R.id.mediasend_fragment_container, fragment, TAG_CAMERA)
|
||||||
.addToBackStack(null)
|
.addToBackStack(null)
|
||||||
.commit();
|
.commit();
|
||||||
|
@ -400,66 +641,182 @@ public class MediaSendActivity extends PassphraseRequiredActionBarActivity imple
|
||||||
|
|
||||||
private Fragment getOrCreateCameraFragment() {
|
private Fragment getOrCreateCameraFragment() {
|
||||||
Fragment fragment = getSupportFragmentManager().findFragmentByTag(TAG_CAMERA);
|
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) {
|
private EmojiEditText getActiveInputField() {
|
||||||
if (oldVisibility == newVisibility) return;
|
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);
|
private void presentCharactersRemaining() {
|
||||||
animation.setDuration(250);
|
String messageBody = composeText.getTextTrimmed();
|
||||||
animation.setInterpolator(new OvershootInterpolator());
|
TransportOption transportOption = sendButton.getSelectedTransport();
|
||||||
button.startAnimation(animation);
|
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 {
|
} else {
|
||||||
Animation animation = new ScaleAnimation(1, 0, 1, 0, Animation.RELATIVE_TO_SELF, 0.5f, Animation.RELATIVE_TO_SELF, 0.5f);
|
charactersLeft.setVisibility(View.GONE);
|
||||||
animation.setDuration(150);
|
}
|
||||||
animation.setInterpolator(new AccelerateDecelerateInterpolator());
|
}
|
||||||
animation.setAnimationListener(new SimpleAnimationListener() {
|
|
||||||
|
|
||||||
|
private void onEmojiToggleClicked(View v) {
|
||||||
|
if (!emojiDrawer.resolved()) {
|
||||||
|
emojiDrawer.get().setProviders(0, new EmojiKeyboardProvider(this, new EmojiKeyboardProvider.EmojiEventListener() {
|
||||||
@Override
|
@Override
|
||||||
public void onAnimationEnd(Animation animation) {
|
public void onKeyEvent(KeyEvent keyEvent) {
|
||||||
button.clearAnimation();
|
getActiveInputField().dispatchKeyEvent(keyEvent);
|
||||||
button.setVisibility(View.GONE);
|
|
||||||
}
|
}
|
||||||
});
|
|
||||||
|
|
||||||
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) {
|
@SuppressLint("StaticFieldLeak")
|
||||||
if (button.getAnimation() != null) {
|
private void processMedia(@NonNull List<Media> mediaList, @NonNull Map<Uri, Object> savedState) {
|
||||||
button.clearAnimation();
|
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);
|
for (Media media : mediaList) {
|
||||||
grow.setDuration(125);
|
Object state = savedState.get(media.getUri());
|
||||||
grow.setInterpolator(new AccelerateInterpolator());
|
|
||||||
grow.setAnimationListener(new SimpleAnimationListener() {
|
if (state instanceof ImageEditorFragment.Data) {
|
||||||
@Override
|
EditorModel model = ((ImageEditorFragment.Data) state).readModel();
|
||||||
public void onAnimationEnd(Animation animation) {
|
if (model != null && model.isChanged()) {
|
||||||
Animation shrink = new ScaleAnimation(1.3f, 1f, 1.3f, 1f, Animation.RELATIVE_TO_SELF, 0.5f, Animation.RELATIVE_TO_SELF, 0.5f);
|
modelsToRender.put(media, model);
|
||||||
shrink.setDuration(125);
|
}
|
||||||
shrink.setInterpolator(new DecelerateInterpolator());
|
|
||||||
button.startAnimation(shrink);
|
|
||||||
}
|
}
|
||||||
});
|
}
|
||||||
|
|
||||||
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
|
private @Nullable MediaSendFragment getMediaSendFragment() {
|
||||||
public void onRequestFullScreen(boolean fullScreen) {
|
return (MediaSendFragment) getSupportFragmentManager().findFragmentByTag(TAG_SEND);
|
||||||
MediaSendFragment sendFragment = (MediaSendFragment) getSupportFragmentManager().findFragmentByTag(TAG_SEND);
|
}
|
||||||
if (sendFragment != null && sendFragment.isVisible()) {
|
|
||||||
sendFragment.onRequestFullScreen(fullScreen);
|
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;
|
package org.thoughtcrime.securesms.mediasend;
|
||||||
|
|
||||||
import android.annotation.SuppressLint;
|
import androidx.lifecycle.ViewModelProviders;
|
||||||
import android.content.Context;
|
|
||||||
import android.graphics.Bitmap;
|
|
||||||
import android.graphics.PorterDuff;
|
|
||||||
import android.graphics.Rect;
|
|
||||||
import android.net.Uri;
|
import android.net.Uri;
|
||||||
import android.os.AsyncTask;
|
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.text.Editable;
|
import androidx.annotation.NonNull;
|
||||||
import android.text.TextWatcher;
|
import androidx.annotation.Nullable;
|
||||||
import android.view.KeyEvent;
|
import androidx.fragment.app.Fragment;
|
||||||
|
import androidx.viewpager.widget.ViewPager;
|
||||||
import android.view.LayoutInflater;
|
import android.view.LayoutInflater;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
import android.view.ViewGroup;
|
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.R;
|
||||||
import org.thoughtcrime.securesms.TransportOption;
|
import org.thoughtcrime.securesms.TransportOption;
|
||||||
import org.thoughtcrime.securesms.components.ComposeText;
|
|
||||||
import org.thoughtcrime.securesms.components.ControllableViewPager;
|
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.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.ThemeUtil;
|
||||||
import org.thoughtcrime.securesms.util.Util;
|
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.List;
|
||||||
import java.util.Locale;
|
import java.util.Locale;
|
||||||
import java.util.Map;
|
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.
|
* 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,
|
public class MediaSendFragment extends Fragment {
|
||||||
MediaRailAdapter.RailItemListener,
|
|
||||||
InputAwareLayout.OnKeyboardShownListener,
|
|
||||||
InputAwareLayout.OnKeyboardHiddenListener
|
|
||||||
{
|
|
||||||
|
|
||||||
private static final String TAG = MediaSendFragment.class.getSimpleName();
|
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_TRANSPORT = "transport";
|
||||||
private static final String KEY_LOCALE = "locale";
|
private static final String KEY_LOCALE = "locale";
|
||||||
|
|
||||||
private InputAwareLayout hud;
|
private ViewGroup playbackControlsContainer;
|
||||||
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 ControllableViewPager fragmentPager;
|
private ControllableViewPager fragmentPager;
|
||||||
private MediaSendFragmentPagerAdapter fragmentPagerAdapter;
|
private MediaSendFragmentPagerAdapter fragmentPagerAdapter;
|
||||||
private RecyclerView mediaRail;
|
|
||||||
private MediaRailAdapter mediaRailAdapter;
|
|
||||||
|
|
||||||
private int visibleHeight;
|
|
||||||
private MediaSendViewModel viewModel;
|
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) {
|
public static MediaSendFragment newInstance(@NonNull Recipient recipient, @NonNull TransportOption transport, @NonNull Locale locale) {
|
||||||
Bundle args = new Bundle();
|
Bundle args = new Bundle();
|
||||||
|
@ -116,17 +51,6 @@ public class MediaSendFragment extends Fragment implements ViewTreeObserver.OnGl
|
||||||
return fragment;
|
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
|
@Override
|
||||||
public @Nullable View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
|
public @Nullable View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
|
||||||
return ThemeUtil.getThemedInflater(requireActivity(), inflater, R.style.TextSecure_DarkTheme)
|
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) {
|
public void onCreate(@Nullable Bundle savedInstanceState) {
|
||||||
super.onCreate(savedInstanceState);
|
super.onCreate(savedInstanceState);
|
||||||
|
|
||||||
locale = (Locale) getArguments().getSerializable(KEY_LOCALE);
|
|
||||||
|
|
||||||
initViewModel();
|
initViewModel();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
|
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);
|
fragmentPager = view.findViewById(R.id.mediasend_pager);
|
||||||
mediaRail = view.findViewById(R.id.mediasend_media_rail);
|
|
||||||
playbackControlsContainer = view.findViewById(R.id.mediasend_playback_controls_container);
|
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());
|
fragmentPagerAdapter = new MediaSendFragmentPagerAdapter(getChildFragmentManager());
|
||||||
fragmentPager.setAdapter(fragmentPagerAdapter);
|
fragmentPager.setAdapter(fragmentPagerAdapter);
|
||||||
|
@ -190,45 +76,6 @@ public class MediaSendFragment extends Fragment implements ViewTreeObserver.OnGl
|
||||||
FragmentPageChangeListener pageChangeListener = new FragmentPageChangeListener();
|
FragmentPageChangeListener pageChangeListener = new FragmentPageChangeListener();
|
||||||
fragmentPager.addOnPageChangeListener(pageChangeListener);
|
fragmentPager.addOnPageChangeListener(pageChangeListener);
|
||||||
fragmentPager.post(() -> pageChangeListener.onPageSelected(fragmentPager.getCurrentItem()));
|
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
|
@Override
|
||||||
|
@ -237,9 +84,6 @@ public class MediaSendFragment extends Fragment implements ViewTreeObserver.OnGl
|
||||||
|
|
||||||
fragmentPagerAdapter.restoreState(viewModel.getDrawState());
|
fragmentPagerAdapter.restoreState(viewModel.getDrawState());
|
||||||
viewModel.onImageEditorStarted();
|
viewModel.onImageEditorStarted();
|
||||||
|
|
||||||
requireActivity().getWindow().addFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN);
|
|
||||||
requireActivity().getWindow().clearFlags(WindowManager.LayoutParams.FLAG_FORCE_NOT_FULLSCREEN);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -254,82 +98,22 @@ public class MediaSendFragment extends Fragment implements ViewTreeObserver.OnGl
|
||||||
viewModel.saveDrawState(fragmentPagerAdapter.getSavedState());
|
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) {
|
public void onTouchEventsNeeded(boolean needed) {
|
||||||
if (fragmentPager != null) {
|
if (fragmentPager != null) {
|
||||||
fragmentPager.setEnabled(!needed);
|
fragmentPager.setEnabled(!needed);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean handleBackPress() {
|
public List<Media> getAllMedia() {
|
||||||
if (hud.isInputOpen()) {
|
return fragmentPagerAdapter.getAllMedia();
|
||||||
hud.hideCurrentInput(composeText);
|
}
|
||||||
return true;
|
|
||||||
}
|
public @NonNull Map<Uri, Object> getSavedState() {
|
||||||
return false;
|
return fragmentPagerAdapter.getSavedState();
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getCurrentImagePosition() {
|
||||||
|
return fragmentPager.getCurrentItem();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void initViewModel() {
|
private void initViewModel() {
|
||||||
|
@ -337,27 +121,16 @@ public class MediaSendFragment extends Fragment implements ViewTreeObserver.OnGl
|
||||||
|
|
||||||
viewModel.getSelectedMedia().observe(this, media -> {
|
viewModel.getSelectedMedia().observe(this, media -> {
|
||||||
if (Util.isEmpty(media)) {
|
if (Util.isEmpty(media)) {
|
||||||
controller.onNoMediaAvailable();
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
fragmentPagerAdapter.setMedia(media);
|
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 -> {
|
viewModel.getPosition().observe(this, position -> {
|
||||||
if (position == null || position < 0) return;
|
if (position == null || position < 0) return;
|
||||||
|
|
||||||
fragmentPager.setCurrentItem(position, true);
|
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);
|
View playbackControls = fragmentPagerAdapter.getPlaybackControls(position);
|
||||||
|
|
||||||
|
@ -370,146 +143,6 @@ public class MediaSendFragment extends Fragment implements ViewTreeObserver.OnGl
|
||||||
playbackControlsContainer.removeAllViews();
|
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 {
|
private class FragmentPageChangeListener extends ViewPager.SimpleOnPageChangeListener {
|
||||||
|
@ -518,51 +151,4 @@ public class MediaSendFragment extends Fragment implements ViewTreeObserver.OnGl
|
||||||
viewModel.onPageChanged(position);
|
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.logging.Log;
|
||||||
import org.thoughtcrime.securesms.mms.MediaConstraints;
|
import org.thoughtcrime.securesms.mms.MediaConstraints;
|
||||||
import org.thoughtcrime.securesms.providers.BlobProvider;
|
import org.thoughtcrime.securesms.providers.BlobProvider;
|
||||||
|
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||||
import org.thoughtcrime.securesms.util.MediaUtil;
|
import org.thoughtcrime.securesms.util.MediaUtil;
|
||||||
import org.thoughtcrime.securesms.util.SingleLiveEvent;
|
import org.thoughtcrime.securesms.util.SingleLiveEvent;
|
||||||
import org.thoughtcrime.securesms.util.Util;
|
import org.thoughtcrime.securesms.util.Util;
|
||||||
|
@ -41,52 +42,69 @@ class MediaSendViewModel extends ViewModel {
|
||||||
private final MediaRepository repository;
|
private final MediaRepository repository;
|
||||||
private final MutableLiveData<List<Media>> selectedMedia;
|
private final MutableLiveData<List<Media>> selectedMedia;
|
||||||
private final MutableLiveData<List<Media>> bucketMedia;
|
private final MutableLiveData<List<Media>> bucketMedia;
|
||||||
|
private final MutableLiveData<Optional<Media>> mostRecentMedia;
|
||||||
private final MutableLiveData<Integer> position;
|
private final MutableLiveData<Integer> position;
|
||||||
private final MutableLiveData<String> bucketId;
|
private final MutableLiveData<String> bucketId;
|
||||||
private final MutableLiveData<List<MediaFolder>> folders;
|
private final MutableLiveData<List<MediaFolder>> folders;
|
||||||
private final MutableLiveData<CountButtonState> countButtonState;
|
private final MutableLiveData<HudState> hudState;
|
||||||
private final MutableLiveData<Boolean> cameraButtonVisibility;
|
|
||||||
private final SingleLiveEvent<Error> error;
|
private final SingleLiveEvent<Error> error;
|
||||||
private final Map<Uri, Object> savedDrawState;
|
private final Map<Uri, Object> savedDrawState;
|
||||||
|
|
||||||
private MediaConstraints mediaConstraints;
|
private MediaConstraints mediaConstraints;
|
||||||
private CharSequence body;
|
private CharSequence body;
|
||||||
private CountButtonState.Visibility countButtonVisibility;
|
private boolean sentMedia;
|
||||||
private boolean sentMedia;
|
private int maxSelection;
|
||||||
private Optional<Media> lastImageCapture;
|
private Page page;
|
||||||
private int maxSelection;
|
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) {
|
private MediaSendViewModel(@NonNull Application application, @NonNull MediaRepository repository) {
|
||||||
this.application = application;
|
this.application = application;
|
||||||
this.repository = repository;
|
this.repository = repository;
|
||||||
this.selectedMedia = new MutableLiveData<>();
|
this.selectedMedia = new MutableLiveData<>();
|
||||||
this.bucketMedia = new MutableLiveData<>();
|
this.bucketMedia = new MutableLiveData<>();
|
||||||
|
this.mostRecentMedia = new MutableLiveData<>();
|
||||||
this.position = new MutableLiveData<>();
|
this.position = new MutableLiveData<>();
|
||||||
this.bucketId = new MutableLiveData<>();
|
this.bucketId = new MutableLiveData<>();
|
||||||
this.folders = new MutableLiveData<>();
|
this.folders = new MutableLiveData<>();
|
||||||
this.countButtonState = new MutableLiveData<>();
|
this.hudState = new MutableLiveData<>();
|
||||||
this.cameraButtonVisibility = new MutableLiveData<>();
|
|
||||||
this.error = new SingleLiveEvent<>();
|
this.error = new SingleLiveEvent<>();
|
||||||
this.savedDrawState = new HashMap<>();
|
this.savedDrawState = new HashMap<>();
|
||||||
this.countButtonVisibility = CountButtonState.Visibility.FORCED_OFF;
|
this.lastCameraCapture = Optional.absent();
|
||||||
this.lastImageCapture = Optional.absent();
|
|
||||||
this.body = "";
|
this.body = "";
|
||||||
|
this.buttonState = ButtonState.GONE;
|
||||||
|
this.railState = RailState.GONE;
|
||||||
|
this.timerState = TimerState.GONE;
|
||||||
|
this.page = Page.UNKNOWN;
|
||||||
|
|
||||||
position.setValue(-1);
|
position.setValue(-1);
|
||||||
countButtonState.setValue(new CountButtonState(0, countButtonVisibility));
|
|
||||||
cameraButtonVisibility.setValue(false);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void setTransport(@NonNull TransportOption transport) {
|
void setTransport(@NonNull TransportOption transport) {
|
||||||
if (transport.isSms()) {
|
if (transport.isSms()) {
|
||||||
|
isSms = true;
|
||||||
maxSelection = MAX_SMS;
|
maxSelection = MAX_SMS;
|
||||||
mediaConstraints = MediaConstraints.getMmsMediaConstraints(transport.getSimSubscriptionId().or(-1));
|
mediaConstraints = MediaConstraints.getMmsMediaConstraints(transport.getSimSubscriptionId().or(-1));
|
||||||
} else {
|
} else {
|
||||||
|
isSms = false;
|
||||||
maxSelection = MAX_PUSH;
|
maxSelection = MAX_PUSH;
|
||||||
mediaConstraints = MediaConstraints.getPushMediaConstraints();
|
mediaConstraints = MediaConstraints.getPushMediaConstraints();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void setRecipient(@NonNull Recipient recipient) {
|
||||||
|
isNoteToSelf = recipient.isLocalNumber();
|
||||||
|
}
|
||||||
|
|
||||||
void onSelectedMediaChanged(@NonNull Context context, @NonNull List<Media> newMedia) {
|
void onSelectedMediaChanged(@NonNull Context context, @NonNull List<Media> newMedia) {
|
||||||
repository.getPopulatedMedia(context, newMedia, populatedMedia -> {
|
repository.getPopulatedMedia(context, newMedia, populatedMedia -> {
|
||||||
Util.runOnMain(() -> {
|
Util.runOnMain(() -> {
|
||||||
|
@ -113,11 +131,19 @@ class MediaSendViewModel extends ViewModel {
|
||||||
bucketId.setValue(computedId);
|
bucketId.setValue(computedId);
|
||||||
} else {
|
} else {
|
||||||
bucketId.setValue(Media.ALL_MEDIA_BUCKET_ID);
|
bucketId.setValue(Media.ALL_MEDIA_BUCKET_ID);
|
||||||
countButtonVisibility = CountButtonState.Visibility.CONDITIONAL;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
selectedMedia.setValue(filteredMedia);
|
if (page == Page.EDITOR && filteredMedia.isEmpty()) {
|
||||||
countButtonState.setValue(new CountButtonState(filteredMedia.size(), countButtonVisibility));
|
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));
|
bucketId.setValue(filteredMedia.get(0).getBucketId().or(Media.ALL_MEDIA_BUCKET_ID));
|
||||||
}
|
}
|
||||||
|
|
||||||
countButtonVisibility = CountButtonState.Visibility.FORCED_OFF;
|
|
||||||
|
|
||||||
selectedMedia.setValue(filteredMedia);
|
selectedMedia.setValue(filteredMedia);
|
||||||
countButtonState.setValue(new CountButtonState(filteredMedia.size(), countButtonVisibility));
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
void onMultiSelectStarted() {
|
void onMultiSelectStarted() {
|
||||||
countButtonVisibility = CountButtonState.Visibility.FORCED_ON;
|
hudVisible = true;
|
||||||
countButtonState.setValue(new CountButtonState(getSelectedMediaOrDefault().size(), countButtonVisibility));
|
composeVisible = false;
|
||||||
|
captionVisible = false;
|
||||||
|
buttonState = ButtonState.COUNT;
|
||||||
|
railState = RailState.VIEWABLE;
|
||||||
|
timerState = TimerState.GONE;
|
||||||
|
|
||||||
|
hudState.setValue(buildHudState());
|
||||||
}
|
}
|
||||||
|
|
||||||
void onImageEditorStarted() {
|
void onImageEditorStarted() {
|
||||||
countButtonVisibility = CountButtonState.Visibility.FORCED_OFF;
|
page = Page.EDITOR;
|
||||||
countButtonState.setValue(new CountButtonState(getSelectedMediaOrDefault().size(), countButtonVisibility));
|
hudVisible = true;
|
||||||
cameraButtonVisibility.setValue(false);
|
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() {
|
void onCameraStarted() {
|
||||||
countButtonVisibility = CountButtonState.Visibility.CONDITIONAL;
|
Page previous = page;
|
||||||
countButtonState.setValue(new CountButtonState(getSelectedMediaOrDefault().size(), countButtonVisibility));
|
|
||||||
cameraButtonVisibility.setValue(false);
|
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() {
|
void onItemPickerStarted() {
|
||||||
countButtonVisibility = CountButtonState.Visibility.CONDITIONAL;
|
page = Page.ITEM_PICKER;
|
||||||
countButtonState.setValue(new CountButtonState(getSelectedMediaOrDefault().size(), countButtonVisibility));
|
hudVisible = true;
|
||||||
cameraButtonVisibility.setValue(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() {
|
void onFolderPickerStarted() {
|
||||||
countButtonVisibility = CountButtonState.Visibility.CONDITIONAL;
|
page = Page.FOLDER_PICKER;
|
||||||
countButtonState.setValue(new CountButtonState(getSelectedMediaOrDefault().size(), countButtonVisibility));
|
hudVisible = true;
|
||||||
cameraButtonVisibility.setValue(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) {
|
void onBodyChanged(@NonNull CharSequence body) {
|
||||||
|
@ -201,10 +317,22 @@ class MediaSendViewModel extends ViewModel {
|
||||||
BlobProvider.getInstance().delete(context, removed.getUri());
|
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) {
|
void onImageCaptured(@NonNull Media media) {
|
||||||
|
lastCameraCapture = Optional.of(media);
|
||||||
|
|
||||||
List<Media> selected = selectedMedia.getValue();
|
List<Media> selected = selectedMedia.getValue();
|
||||||
|
|
||||||
if (selected == null) {
|
if (selected == null) {
|
||||||
|
@ -216,40 +344,32 @@ class MediaSendViewModel extends ViewModel {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
lastImageCapture = Optional.of(media);
|
|
||||||
|
|
||||||
selected.add(media);
|
selected.add(media);
|
||||||
selectedMedia.setValue(selected);
|
selectedMedia.setValue(selected);
|
||||||
position.setValue(selected.size() - 1);
|
position.setValue(selected.size() - 1);
|
||||||
bucketId.setValue(Media.ALL_MEDIA_BUCKET_ID);
|
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) {
|
void onImageCaptureUndo(@NonNull Context context) {
|
||||||
List<Media> selected = getSelectedMediaOrDefault();
|
List<Media> selected = getSelectedMediaOrDefault();
|
||||||
|
|
||||||
if (lastImageCapture.isPresent() && selected.contains(lastImageCapture.get()) && selected.size() == 1) {
|
if (lastCameraCapture.isPresent() && selected.contains(lastCameraCapture.get()) && selected.size() == 1) {
|
||||||
selected.remove(lastImageCapture.get());
|
selected.remove(lastCameraCapture.get());
|
||||||
selectedMedia.setValue(selected);
|
selectedMedia.setValue(selected);
|
||||||
countButtonState.setValue(new CountButtonState(selected.size(), countButtonVisibility));
|
BlobProvider.getInstance().delete(context, lastCameraCapture.get().getUri());
|
||||||
BlobProvider.getInstance().delete(context, lastImageCapture.get().getUri());
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
void onCaptionChanged(@NonNull String newCaption) {
|
void onCaptionChanged(@NonNull String newCaption) {
|
||||||
if (position.getValue() >= 0 && !Util.isEmpty(selectedMedia.getValue())) {
|
if (position.getValue() >= 0 && !Util.isEmpty(selectedMedia.getValue())) {
|
||||||
selectedMedia.getValue().get(position.getValue()).setCaption(TextUtils.isEmpty(newCaption) ? null : newCaption);
|
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) {
|
void saveDrawState(@NonNull Map<Uri, Object> state) {
|
||||||
savedDrawState.clear();
|
savedDrawState.clear();
|
||||||
savedDrawState.putAll(state);
|
savedDrawState.putAll(state);
|
||||||
|
@ -277,12 +397,8 @@ class MediaSendViewModel extends ViewModel {
|
||||||
return folders;
|
return folders;
|
||||||
}
|
}
|
||||||
|
|
||||||
@NonNull LiveData<CountButtonState> getCountButtonState() {
|
@NonNull LiveData<Optional<Media>> getMostRecentMediaItem(@NonNull Context context) {
|
||||||
return countButtonState;
|
return mostRecentMedia;
|
||||||
}
|
|
||||||
|
|
||||||
@NonNull LiveData<Boolean> getCameraButtonVisibility() {
|
|
||||||
return cameraButtonVisibility;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@NonNull CharSequence getBody() {
|
@NonNull CharSequence getBody() {
|
||||||
|
@ -301,10 +417,18 @@ class MediaSendViewModel extends ViewModel {
|
||||||
return error;
|
return error;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@NonNull LiveData<HudState> getHudState() {
|
||||||
|
return hudState;
|
||||||
|
}
|
||||||
|
|
||||||
int getMaxSelection() {
|
int getMaxSelection() {
|
||||||
return maxSelection;
|
return maxSelection;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
long getRevealDuration() {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
private @NonNull List<Media> getSelectedMediaOrDefault() {
|
private @NonNull List<Media> getSelectedMediaOrDefault() {
|
||||||
return selectedMedia.getValue() == null ? Collections.emptyList()
|
return selectedMedia.getValue() == null ? Collections.emptyList()
|
||||||
: selectedMedia.getValue();
|
: 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
|
@Override
|
||||||
protected void onCleared() {
|
protected void onCleared() {
|
||||||
if (!sentMedia) {
|
if (!sentMedia) {
|
||||||
Stream.of(getSelectedMediaOrDefault())
|
clearPersistedMedia();
|
||||||
.map(Media::getUri)
|
|
||||||
.filter(BlobProvider::isAuthority)
|
|
||||||
.forEach(uri -> BlobProvider.getInstance().delete(application.getApplicationContext(), uri));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
enum Error {
|
enum Error {
|
||||||
ITEM_TOO_LARGE, TOO_MANY_ITEMS
|
ITEM_TOO_LARGE, TOO_MANY_ITEMS, NO_ITEMS
|
||||||
}
|
}
|
||||||
|
|
||||||
static class CountButtonState {
|
enum Page {
|
||||||
private final int count;
|
CAMERA, ITEM_PICKER, FOLDER_PICKER, EDITOR, UNKNOWN
|
||||||
private final Visibility visibility;
|
}
|
||||||
|
|
||||||
private CountButtonState(int count, @NonNull Visibility visibility) {
|
enum ButtonState {
|
||||||
this.count = count;
|
COUNT, SEND, GONE
|
||||||
this.visibility = visibility;
|
}
|
||||||
|
|
||||||
|
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() {
|
public boolean isHudVisible() {
|
||||||
return count;
|
return hudVisible;
|
||||||
}
|
}
|
||||||
|
|
||||||
boolean isVisible() {
|
public boolean isComposeVisible() {
|
||||||
switch (visibility) {
|
return hudVisible && composeVisible;
|
||||||
case FORCED_ON: return true;
|
|
||||||
case FORCED_OFF: return false;
|
|
||||||
case CONDITIONAL: return count > 0;
|
|
||||||
default: return false;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
enum Visibility {
|
public boolean isCaptionVisible() {
|
||||||
CONDITIONAL, FORCED_ON, FORCED_OFF
|
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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
AsyncTask.THREAD_POOL_EXECUTOR.execute(() -> {
|
SignalExecutors.BOUNDED.execute(() -> {
|
||||||
final E result = backgroundTask.run();
|
final E result = backgroundTask.run();
|
||||||
|
|
||||||
if (isValid(lifecycle)) {
|
if (isValid(lifecycle)) {
|
||||||
|
@ -38,7 +38,7 @@ public class SimpleTask {
|
||||||
* the main thread. Essentially {@link AsyncTask}, but lambda-compatible.
|
* the main thread. Essentially {@link AsyncTask}, but lambda-compatible.
|
||||||
*/
|
*/
|
||||||
public static <E> void run(@NonNull BackgroundTask<E> backgroundTask, @NonNull ForegroundTask<E> foregroundTask) {
|
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();
|
final E result = backgroundTask.run();
|
||||||
Util.runOnMain(() -> foregroundTask.run(result));
|
Util.runOnMain(() -> foregroundTask.run(result));
|
||||||
});
|
});
|
||||||
|
|
Loading…
Add table
Reference in a new issue