Implement new media send flow.
Update our media send flow to allow users to send multiple images/videos at once. This change includes: - New in-app media picker flow. - Ability to caption images and videos. - Image editing tools are made more prominent in the flow. - Some fixes to the image editing tools.
@ -278,6 +278,11 @@
|
||||
android:windowSoftInputMode="stateHidden"
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
|
||||
|
||||
<activity android:name=".mediasend.MediaSendActivity"
|
||||
android:theme="@style/TextSecure.LightNoActionBar"
|
||||
android:windowSoftInputMode="stateHidden"
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
|
||||
|
||||
<activity android:name=".PassphraseChangeActivity"
|
||||
android:label="@string/AndroidManifest__change_passphrase"
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
|
||||
|
BIN
res/drawable-hdpi/ic_add_caption.png
Normal file
After Width: | Height: | Size: 221 B |
BIN
res/drawable-hdpi/ic_add_photo.png
Normal file
After Width: | Height: | Size: 397 B |
BIN
res/drawable-hdpi/ic_x_circle.png
Normal file
After Width: | Height: | Size: 485 B |
BIN
res/drawable-mdpi/ic_add_caption.png
Normal file
After Width: | Height: | Size: 165 B |
BIN
res/drawable-mdpi/ic_add_photo.png
Normal file
After Width: | Height: | Size: 227 B |
BIN
res/drawable-mdpi/ic_x_circle.png
Normal file
After Width: | Height: | Size: 324 B |
BIN
res/drawable-xhdpi/ic_add_caption.png
Normal file
After Width: | Height: | Size: 198 B |
BIN
res/drawable-xhdpi/ic_add_photo.png
Normal file
After Width: | Height: | Size: 365 B |
BIN
res/drawable-xhdpi/ic_x_circle.png
Normal file
After Width: | Height: | Size: 664 B |
BIN
res/drawable-xxhdpi/ic_add_caption.png
Normal file
After Width: | Height: | Size: 231 B |
BIN
res/drawable-xxhdpi/ic_add_photo.png
Normal file
After Width: | Height: | Size: 521 B |
BIN
res/drawable-xxhdpi/ic_x_circle.png
Normal file
After Width: | Height: | Size: 956 B |
BIN
res/drawable-xxxhdpi/ic_add_caption.png
Normal file
After Width: | Height: | Size: 265 B |
BIN
res/drawable-xxxhdpi/ic_add_photo.png
Normal file
After Width: | Height: | Size: 688 B |
BIN
res/drawable-xxxhdpi/ic_x_circle.png
Normal file
After Width: | Height: | Size: 1.3 KiB |
@ -1,5 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="rectangle" >
|
||||
<solid android:color="@color/signal_primary"/>
|
||||
<corners android:radius="2dp"/>
|
||||
</shape>
|
10
res/drawable/mediapicker_item_border_dark.xml
Normal file
@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<shape
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:shape="rectangle">
|
||||
|
||||
<stroke
|
||||
android:width="1px"
|
||||
android:color="@color/transparent_white_30" />
|
||||
|
||||
</shape>
|
10
res/drawable/mediapicker_item_border_light.xml
Normal file
@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<shape
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:shape="rectangle">
|
||||
|
||||
<stroke
|
||||
android:width="1px"
|
||||
android:color="@color/transparent_black_30" />
|
||||
|
||||
</shape>
|
@ -1,12 +1,36 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
|
||||
<org.thoughtcrime.securesms.components.ThumbnailView
|
||||
<FrameLayout
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:id="@+id/rail_item_image"
|
||||
android:layout_width="48dp"
|
||||
android:layout_height="48sp"
|
||||
android:layout_margin="2dp"
|
||||
android:padding="2dp"
|
||||
android:background="@drawable/album_rail_item_background"
|
||||
app:thumbnail_radius="2dp"/>
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:animateLayoutChanges="true">
|
||||
|
||||
<org.thoughtcrime.securesms.components.ThumbnailView
|
||||
android:id="@+id/rail_item_image"
|
||||
android:layout_width="56dp"
|
||||
android:layout_height="56dp"
|
||||
android:layout_margin="2dp"
|
||||
android:padding="2dp"
|
||||
android:background="@drawable/media_rail_item_background"
|
||||
app:thumbnail_radius="0dp"/>
|
||||
|
||||
<ImageView
|
||||
android:layout_width="52dp"
|
||||
android:layout_height="52dp"
|
||||
android:layout_gravity="center"
|
||||
android:src="@drawable/mediapicker_item_border_dark"/>
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/rail_item_delete"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="right|end|top"
|
||||
android:src="@drawable/ic_x_circle"
|
||||
android:elevation="8dp"
|
||||
android:visibility="gone"
|
||||
tools:visibility="visible"/>
|
||||
|
||||
</FrameLayout>
|
||||
|
13
res/layout/mediapicker_activity.xml
Normal file
@ -0,0 +1,13 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:orientation="vertical"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<FrameLayout
|
||||
android:id="@+id/mediapicker_fragment_container"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent" />
|
||||
|
||||
</LinearLayout>
|
24
res/layout/mediapicker_folder_fragment.xml
Normal file
@ -0,0 +1,24 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="vertical"
|
||||
android:background="?android:windowBackground">
|
||||
|
||||
<android.support.v7.widget.Toolbar
|
||||
android:id="@+id/mediapicker_toolbar"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="?attr/actionBarSize"
|
||||
android:minHeight="?attr/actionBarSize"
|
||||
android:background="?attr/conversation_list_toolbar_background"
|
||||
android:theme="?attr/actionBarStyle" />
|
||||
|
||||
<android.support.v7.widget.RecyclerView
|
||||
android:id="@+id/mediapicker_folder_list"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_marginLeft="2dp"
|
||||
android:layout_marginTop="2dp" />
|
||||
|
||||
</LinearLayout>
|
62
res/layout/mediapicker_folder_item.xml
Normal file
@ -0,0 +1,62 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<FrameLayout
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginRight="2dp"
|
||||
android:layout_marginBottom="2dp">
|
||||
|
||||
<org.thoughtcrime.securesms.components.SquareImageView
|
||||
android:id="@+id/mediapicker_folder_item_thumbnail"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:adjustViewBounds="true"
|
||||
android:scaleType="centerCrop"/>
|
||||
|
||||
<ImageView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="50dp"
|
||||
android:layout_gravity="bottom"
|
||||
android:src="@drawable/image_shade"/>
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="bottom"
|
||||
android:orientation="horizontal"
|
||||
android:gravity="center_vertical"
|
||||
android:padding="6dp">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/mediapicker_folder_item_icon"
|
||||
android:layout_width="20dp"
|
||||
android:layout_height="20dp"
|
||||
android:layout_marginRight="6dp"
|
||||
android:layout_marginEnd="6dp"
|
||||
android:src="@drawable/ic_folder_white_48dp"/>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/mediapicker_folder_item_title"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:layout_marginRight="6dp"
|
||||
android:layout_marginEnd="6dp"
|
||||
android:maxLines="1"
|
||||
android:ellipsize="end"
|
||||
style="@style/Signal.Text.Preview"
|
||||
android:textColor="@color/core_white"
|
||||
tools:text="Daily Bugle"/>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/mediapicker_folder_item_count"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
style="@style/Signal.Text.Preview"
|
||||
android:textColor="@color/core_white"
|
||||
tools:text="122"/>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</FrameLayout>
|
24
res/layout/mediapicker_item_fragment.xml
Normal file
@ -0,0 +1,24 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="vertical"
|
||||
android:background="?android:windowBackground">
|
||||
|
||||
<android.support.v7.widget.Toolbar
|
||||
android:id="@+id/mediapicker_toolbar"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="?attr/actionBarSize"
|
||||
android:minHeight="?attr/actionBarSize"
|
||||
android:background="?attr/conversation_list_toolbar_background"
|
||||
android:theme="?attr/actionBarStyle" />
|
||||
|
||||
<android.support.v7.widget.RecyclerView
|
||||
android:id="@+id/mediapicker_item_list"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_marginLeft="2dp"
|
||||
android:layout_marginTop="2dp" />
|
||||
|
||||
</LinearLayout>
|
60
res/layout/mediapicker_media_item.xml
Normal file
@ -0,0 +1,60 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<FrameLayout
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginRight="2dp"
|
||||
android:layout_marginBottom="2dp">
|
||||
|
||||
<org.thoughtcrime.securesms.components.SquareImageView
|
||||
android:id="@+id/mediapicker_image_item_thumbnail"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:adjustViewBounds="true"
|
||||
android:scaleType="centerCrop"/>
|
||||
|
||||
<View
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:background="?mediapicker_image_outline" />
|
||||
|
||||
<FrameLayout
|
||||
android:id="@+id/mediapicker_play_overlay"
|
||||
android:layout_width="36dp"
|
||||
android:layout_height="36dp"
|
||||
android:background="@drawable/circle_white"
|
||||
android:layout_gravity="center"
|
||||
android:longClickable="false"
|
||||
android:visibility="gone"
|
||||
tools:visibility="visible">
|
||||
|
||||
<ImageView
|
||||
android:layout_width="15dp"
|
||||
android:layout_height="18dp"
|
||||
android:layout_gravity="center"
|
||||
android:layout_marginLeft="2dp"
|
||||
android:tint="@color/core_blue"
|
||||
android:scaleType="fitXY"
|
||||
app:srcCompat="@drawable/triangle_right" />
|
||||
|
||||
</FrameLayout>
|
||||
|
||||
|
||||
<FrameLayout
|
||||
android:id="@+id/mediapicker_selected"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:background="@color/transparent_black_70"
|
||||
android:visibility="gone">
|
||||
|
||||
<ImageView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
android:src="@drawable/ic_check_white_24dp" />
|
||||
|
||||
</FrameLayout>
|
||||
|
||||
</FrameLayout>
|
183
res/layout/mediasend_fragment.xml
Normal file
@ -0,0 +1,183 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<FrameLayout
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:background="@color/core_black">
|
||||
|
||||
<org.thoughtcrime.securesms.components.ControllableViewPager
|
||||
android:id="@+id/mediasend_pager"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent" />
|
||||
|
||||
<FrameLayout
|
||||
android:id="@+id/mediasend_playback_controls_container"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:animateLayoutChanges="true"
|
||||
android:layout_gravity="top"/>
|
||||
|
||||
<org.thoughtcrime.securesms.components.InputAwareLayout
|
||||
android:id="@+id/mediasend_hud"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<LinearLayout
|
||||
android: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_marginLeft="16dp"
|
||||
android:layout_marginRight="16dp"
|
||||
style="@style/Signal.Text.Body"
|
||||
android:paddingTop="11dp"
|
||||
android:paddingBottom="11dp"
|
||||
android:drawableLeft="@drawable/ic_add_caption"
|
||||
android:drawableStart="@drawable/ic_add_caption"
|
||||
android:drawablePadding="6dp"
|
||||
android:maxLines="3"
|
||||
android:maxLength="240"
|
||||
android:hint="@string/MediaSendActivity_add_a_caption"
|
||||
android:background="@null"/>
|
||||
|
||||
<android.support.v7.widget.RecyclerView
|
||||
android:id="@+id/mediasend_media_rail"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="8dp"
|
||||
android:layout_marginLeft="16dp"
|
||||
android:layout_marginRight="16dp"
|
||||
tools:layout_height="64dp"/>
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/mediasend_compose_container"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="6dp"
|
||||
android:layout_marginLeft="16dp"
|
||||
android:layout_marginRight="16dp"
|
||||
android:orientation="horizontal"
|
||||
android:paddingBottom="6dp">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/mediasend_add_button"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="bottom"
|
||||
android:paddingTop="8dp"
|
||||
android:paddingBottom="8dp"
|
||||
android:src="@drawable/ic_add_photo"
|
||||
android:background="?selectableItemBackgroundBorderless"
|
||||
android:visibility="gone"
|
||||
tools:visibility="visible"/>
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:layout_marginLeft="8dp"
|
||||
android:layout_marginStart="8dp"
|
||||
android:paddingLeft="10dp"
|
||||
android:paddingStart="10dp"
|
||||
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:paddingLeft="4dp"
|
||||
android:paddingStart="4dp"
|
||||
android:paddingRight="6dp"
|
||||
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_marginLeft="12dp"
|
||||
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:paddingRight="6dp"
|
||||
android:paddingBottom="6dp"
|
||||
android:paddingLeft="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_marginTop="6dp"
|
||||
android:layout_marginLeft="16dp"
|
||||
android:layout_marginRight="16dp"
|
||||
android:paddingBottom="6dp"
|
||||
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>
|
||||
|
||||
<!--<com.pnikosis.materialishprogress.ProgressWheel-->
|
||||
<!--android:id="@+id/mediasend_progress_wheel"-->
|
||||
<!--android:layout_width="70dp"-->
|
||||
<!--android:layout_height="70dp"-->
|
||||
<!--android:visibility="gone"-->
|
||||
<!--android:layout_gravity="center"-->
|
||||
<!--app:matProg_barColor="@color/white"-->
|
||||
<!--app:matProg_progressIndeterminate="true" />-->
|
||||
|
||||
</FrameLayout>
|
8
res/layout/mediasend_image_fragment.xml
Normal file
@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<ImageView
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:scaleType="fitCenter">
|
||||
|
||||
</ImageView>
|
7
res/layout/mediasend_video_fragment.xml
Normal file
@ -0,0 +1,7 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<org.thoughtcrime.securesms.video.VideoPlayer
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
</org.thoughtcrime.securesms.video.VideoPlayer>
|
18
res/layout/progress_dialog.xml
Normal file
@ -0,0 +1,18 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<FrameLayout
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<com.pnikosis.materialishprogress.ProgressWheel
|
||||
android:id="@+id/mediasend_progress_wheel"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_gravity="center"
|
||||
android:padding="8dp"
|
||||
android:indeterminate="true"
|
||||
app:matProg_progressIndeterminate="true"
|
||||
app:matProg_barColor="@color/core_grey_25"/>
|
||||
|
||||
</FrameLayout>
|
13
res/menu/mediapicker_default.xml
Normal file
@ -0,0 +1,13 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<menu
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||
|
||||
<item
|
||||
android:title=""
|
||||
android:id="@+id/mediapicker_menu_add"
|
||||
android:visible="true"
|
||||
android:icon="@drawable/ic_add_photo"
|
||||
app:showAsAction="always" />
|
||||
|
||||
</menu>
|
10
res/menu/mediapicker_multiselect.xml
Normal file
@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<menu xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||
|
||||
<item android:title=""
|
||||
android:id="@+id/mediapicker_menu_confirm"
|
||||
android:visible="true"
|
||||
android:icon="@drawable/ic_check_white_24dp"
|
||||
app:showAsAction="always" />
|
||||
|
||||
</menu>
|
@ -102,6 +102,8 @@
|
||||
|
||||
<attr name="device_link_item_card_background" format="reference|color" />
|
||||
|
||||
<attr name="mediapicker_image_outline" format="reference" />
|
||||
|
||||
<attr name="import_export_item_background_color" format="reference|color" />
|
||||
<attr name="import_export_item_background_shadow_color" format="reference|color" />
|
||||
<attr name="import_export_item_card_background" format="reference" />
|
||||
|
@ -35,6 +35,11 @@
|
||||
<dimen name="media_bubble_min_height">100dp</dimen>
|
||||
<dimen name="media_bubble_max_height">320dp</dimen>
|
||||
|
||||
<dimen name="media_picker_folder_width">175dp</dimen>
|
||||
<dimen name="media_picker_item_width">85dp</dimen>
|
||||
|
||||
<dimen name="mediasend_progress_dialog_size">120dp</dimen>
|
||||
|
||||
<dimen name="conversation_compose_height">40dp</dimen>
|
||||
<dimen name="conversation_individual_right_gutter">16dp</dimen>
|
||||
<dimen name="conversation_individual_left_gutter">16dp</dimen>
|
||||
|
@ -435,6 +435,15 @@
|
||||
<string name="NotificationMmsMessageRecord_downloading_mms_message">Downloading MMS message</string>
|
||||
<string name="NotificationMmsMessageRecord_error_downloading_mms_message">Error downloading MMS message, tap to retry</string>
|
||||
|
||||
<!-- MediaPickerActivity -->
|
||||
<string name="MediaPickerActivity_send_to">Send to %s</string>
|
||||
|
||||
<!-- MediaPickerItemFragment -->
|
||||
<string name="MediaPickerItemFragment_tap_to_select">Tap to select</string>
|
||||
|
||||
<!-- MediaSendActivity -->
|
||||
<string name="MediaSendActivity_add_a_caption">Add a caption...</string>
|
||||
|
||||
<!-- MessageRecord -->
|
||||
<string name="MessageRecord_message_encrypted_with_a_legacy_protocol_version_that_is_no_longer_supported">Received a message encrypted using an old version of Signal that is no longer supported. Please ask the sender to update to the most recent version and resend the message.</string>
|
||||
<string name="MessageRecord_left_group">You have left the group.</string>
|
||||
|
@ -217,6 +217,8 @@
|
||||
|
||||
<item name="device_link_item_card_background">@color/device_link_item_background_light</item>
|
||||
|
||||
<item name="mediapicker_image_outline">@drawable/mediapicker_item_border_light</item>
|
||||
|
||||
<item name="import_export_item_background_color">@color/import_export_item_background_light</item>
|
||||
<item name="import_export_item_background_shadow_color">@color/import_export_item_background_shadow_light</item>
|
||||
<item name="import_export_item_card_background">@drawable/clickable_card_light</item>
|
||||
@ -324,6 +326,8 @@
|
||||
|
||||
<item name="device_link_item_card_background">@color/device_link_item_background_dark</item>
|
||||
|
||||
<item name="mediapicker_image_outline">@drawable/mediapicker_item_border_dark</item>
|
||||
|
||||
<item name="import_export_item_background_color">@color/import_export_item_background_dark</item>
|
||||
<item name="import_export_item_background_shadow_color">@color/import_export_item_background_shadow_dark</item>
|
||||
<item name="import_export_item_card_background">@drawable/clickable_card_dark</item>
|
||||
@ -420,4 +424,8 @@
|
||||
<style name="TextSecure.ScribbleTheme" parent="TextSecure.DarkNoActionBar">
|
||||
</style>
|
||||
|
||||
<style name="TextSecure.MediaSendProgressDialog" parent="@android:style/Theme.Dialog">
|
||||
<item name="android:background">@color/core_grey_95</item>
|
||||
</style>
|
||||
|
||||
</resources>
|
||||
|
@ -22,7 +22,6 @@ import android.annotation.TargetApi;
|
||||
import android.content.ActivityNotFoundException;
|
||||
import android.content.BroadcastReceiver;
|
||||
import android.content.Context;
|
||||
import android.content.DialogInterface;
|
||||
import android.content.Intent;
|
||||
import android.content.IntentFilter;
|
||||
import android.content.res.Configuration;
|
||||
@ -126,6 +125,8 @@ import org.thoughtcrime.securesms.database.model.MessageRecord;
|
||||
import org.thoughtcrime.securesms.database.model.MmsMessageRecord;
|
||||
import org.thoughtcrime.securesms.events.ReminderUpdateEvent;
|
||||
import org.thoughtcrime.securesms.giph.ui.GiphyActivity;
|
||||
import org.thoughtcrime.securesms.mediasend.MediaSendActivity;
|
||||
import org.thoughtcrime.securesms.mediasend.Media;
|
||||
import org.thoughtcrime.securesms.jobs.MultiDeviceBlockedUpdateJob;
|
||||
import org.thoughtcrime.securesms.jobs.RetrieveProfileJob;
|
||||
import org.thoughtcrime.securesms.jobs.ServiceOutageDetectionJob;
|
||||
@ -133,6 +134,7 @@ import org.thoughtcrime.securesms.logging.Log;
|
||||
import org.thoughtcrime.securesms.mms.AttachmentManager;
|
||||
import org.thoughtcrime.securesms.mms.AttachmentManager.MediaType;
|
||||
import org.thoughtcrime.securesms.mms.AudioSlide;
|
||||
import org.thoughtcrime.securesms.mms.GifSlide;
|
||||
import org.thoughtcrime.securesms.mms.GlideApp;
|
||||
import org.thoughtcrime.securesms.mms.GlideRequests;
|
||||
import org.thoughtcrime.securesms.mms.ImageSlide;
|
||||
@ -146,6 +148,7 @@ import org.thoughtcrime.securesms.mms.QuoteId;
|
||||
import org.thoughtcrime.securesms.mms.QuoteModel;
|
||||
import org.thoughtcrime.securesms.mms.Slide;
|
||||
import org.thoughtcrime.securesms.mms.SlideDeck;
|
||||
import org.thoughtcrime.securesms.mms.VideoSlide;
|
||||
import org.thoughtcrime.securesms.notifications.MarkReadReceiver;
|
||||
import org.thoughtcrime.securesms.notifications.MessageNotifier;
|
||||
import org.thoughtcrime.securesms.notifications.NotificationChannels;
|
||||
@ -220,6 +223,7 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
|
||||
public static final String THREAD_ID_EXTRA = "thread_id";
|
||||
public static final String IS_ARCHIVED_EXTRA = "is_archived";
|
||||
public static final String TEXT_EXTRA = "draft_text";
|
||||
public static final String MEDIA_EXTRA = "media_list";
|
||||
public static final String DISTRIBUTION_TYPE_EXTRA = "distribution_type";
|
||||
public static final String TIMING_EXTRA = "timing";
|
||||
public static final String LAST_SEEN_EXTRA = "last_seen";
|
||||
@ -237,7 +241,7 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
|
||||
private static final int PICK_GIF = 10;
|
||||
private static final int SMS_DEFAULT = 11;
|
||||
private static final int PICK_CAMERA = 12;
|
||||
private static final int EDIT_IMAGE = 13;
|
||||
private static final int MEDIA_SENDER = 13;
|
||||
|
||||
private GlideRequests glideRequests;
|
||||
protected ComposeText composeText;
|
||||
@ -443,18 +447,6 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
|
||||
}
|
||||
|
||||
switch (reqCode) {
|
||||
case PICK_GALLERY:
|
||||
MediaType mediaType;
|
||||
|
||||
String mimeType = MediaUtil.getMimeType(this, data.getData());
|
||||
|
||||
if (MediaUtil.isGif(mimeType)) mediaType = MediaType.GIF;
|
||||
else if (MediaUtil.isVideo(mimeType)) mediaType = MediaType.VIDEO;
|
||||
else mediaType = MediaType.IMAGE;
|
||||
|
||||
setMedia(data.getData(), mediaType);
|
||||
|
||||
break;
|
||||
case PICK_DOCUMENT:
|
||||
setMedia(data.getData(), MediaType.DOCUMENT);
|
||||
break;
|
||||
@ -526,6 +518,38 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
|
||||
|
||||
sendMediaMessage(transport.isSms(), message, slideDeck, Collections.emptyList(), expiresIn, subscriptionId, initiating);
|
||||
break;
|
||||
|
||||
case MEDIA_SENDER:
|
||||
expiresIn = recipient.getExpireMessages() * 1000L;
|
||||
subscriptionId = sendButton.getSelectedTransport().getSimSubscriptionId().or(-1);
|
||||
initiating = threadId == -1;
|
||||
transport = data.getParcelableExtra(MediaSendActivity.EXTRA_TRANSPORT);
|
||||
message = data.getStringExtra(MediaSendActivity.EXTRA_MESSAGE);
|
||||
slideDeck = new SlideDeck();
|
||||
|
||||
if (transport == null) {
|
||||
throw new IllegalStateException("Received a null transport from the MediaSendActivity.");
|
||||
}
|
||||
|
||||
sendButton.setTransport(transport);
|
||||
|
||||
List<Media> mediaList = data.getParcelableArrayListExtra(MediaSendActivity.EXTRA_MEDIA);
|
||||
|
||||
for (Media mediaItem : mediaList) {
|
||||
if (MediaUtil.isVideoType(mediaItem.getMimeType())) {
|
||||
slideDeck.addSlide(new VideoSlide(this, mediaItem.getUri(), 0, mediaItem.getCaption().orNull()));
|
||||
} else if (MediaUtil.isGif(mediaItem.getMimeType())) {
|
||||
slideDeck.addSlide(new GifSlide(this, mediaItem.getUri(), 0, mediaItem.getWidth(), mediaItem.getHeight(), mediaItem.getCaption().orNull()));
|
||||
} else if (MediaUtil.isImageType(mediaItem.getMimeType())) {
|
||||
slideDeck.addSlide(new ImageSlide(this, mediaItem.getUri(), 0, mediaItem.getWidth(), mediaItem.getHeight(), mediaItem.getCaption().orNull()));
|
||||
} else {
|
||||
Log.w(TAG, "Asked to send an unexpected mimeType: '" + mediaItem.getMimeType() + "'. Skipping.");
|
||||
}
|
||||
}
|
||||
|
||||
sendMediaMessage(transport.isSms(), message, slideDeck, Collections.emptyList(), expiresIn, subscriptionId, initiating);
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@ -1094,14 +1118,22 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
|
||||
private ListenableFuture<Boolean> initializeDraft() {
|
||||
final SettableFuture<Boolean> result = new SettableFuture<>();
|
||||
|
||||
final String draftText = getIntent().getStringExtra(TEXT_EXTRA);
|
||||
final Uri draftMedia = getIntent().getData();
|
||||
final MediaType draftMediaType = MediaType.from(getIntent().getType());
|
||||
final String draftText = getIntent().getStringExtra(TEXT_EXTRA);
|
||||
final Uri draftMedia = getIntent().getData();
|
||||
final MediaType draftMediaType = MediaType.from(getIntent().getType());
|
||||
final List<Media> mediaList = getIntent().getParcelableArrayListExtra(MEDIA_EXTRA);
|
||||
|
||||
if (!Util.isEmpty(mediaList)) {
|
||||
Intent sendIntent = MediaSendActivity.getIntent(this, mediaList, recipient, draftText, sendButton.getSelectedTransport());
|
||||
startActivityForResult(sendIntent, MEDIA_SENDER);
|
||||
return new SettableFuture<>(false);
|
||||
}
|
||||
|
||||
if (draftText != null) {
|
||||
composeText.setText(draftText);
|
||||
result.set(true);
|
||||
}
|
||||
|
||||
if (draftMedia != null && draftMediaType != null) {
|
||||
return setMedia(draftMedia, draftMediaType);
|
||||
}
|
||||
@ -1517,7 +1549,7 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
|
||||
Log.i(TAG, "Selected: " + type);
|
||||
switch (type) {
|
||||
case AttachmentTypeSelector.ADD_GALLERY:
|
||||
AttachmentManager.selectGallery(this, PICK_GALLERY); break;
|
||||
AttachmentManager.selectGallery(this, MEDIA_SENDER, recipient, sendButton.getSelectedTransport()); break;
|
||||
case AttachmentTypeSelector.ADD_DOCUMENT:
|
||||
AttachmentManager.selectDocument(this, PICK_DOCUMENT); break;
|
||||
case AttachmentTypeSelector.ADD_SOUND:
|
||||
@ -1545,6 +1577,10 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
|
||||
if (MediaType.VCARD.equals(mediaType) && isSecureText) {
|
||||
openContactShareEditor(uri);
|
||||
return new SettableFuture<>(false);
|
||||
} else if (MediaType.IMAGE.equals(mediaType) || MediaType.GIF.equals(mediaType) || MediaType.VIDEO.equals(mediaType)) {
|
||||
Media media = new Media(uri, MediaUtil.getMimeType(this, uri), 0, width, height, Optional.absent(), Optional.absent());
|
||||
startActivityForResult(MediaSendActivity.getIntent(ConversationActivity.this, Collections.singletonList(media), recipient, composeText.getTextTrimmed(), sendButton.getSelectedTransport()), MEDIA_SENDER);
|
||||
return new SettableFuture<>(false);
|
||||
} else {
|
||||
return attachmentManager.setMedia(glideRequests, uri, mediaType, getCurrentMediaConstraints(), width, height);
|
||||
}
|
||||
@ -1858,9 +1894,7 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
|
||||
private ListenableFuture<Void> sendMediaMessage(final boolean forceSms, String body, SlideDeck slideDeck, List<Contact> contacts, final long expiresIn, final int subscriptionId, final boolean initiating) {
|
||||
if (!isDefaultSms && (!isSecureText || forceSms)) {
|
||||
showDefaultSmsPrompt();
|
||||
SettableFuture<Void> future = new SettableFuture<>();
|
||||
future.set(null);
|
||||
return future;
|
||||
return new SettableFuture<>(null);
|
||||
}
|
||||
|
||||
OutgoingMediaMessage outgoingMessageCandidate = new OutgoingMediaMessage(recipient, slideDeck, body, System.currentTimeMillis(), subscriptionId, expiresIn, distributionType, inputPanel.getQuote().orNull(), contacts);
|
||||
@ -2158,11 +2192,9 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onQuickAttachment(Uri uri) {
|
||||
Intent intent = new Intent();
|
||||
intent.setData(uri);
|
||||
|
||||
onActivityResult(PICK_GALLERY, RESULT_OK, intent);
|
||||
public void onQuickAttachment(Uri uri, String mimeType, String bucketId, long dateTaken, int width, int height) {
|
||||
Media media = new Media(uri, mimeType, dateTaken, width, height, Optional.fromNullable(bucketId), Optional.absent());
|
||||
startActivityForResult(MediaSendActivity.getIntent(ConversationActivity.this, Collections.singletonList(media), recipient, composeText.getTextTrimmed(), sendButton.getSelectedTransport()), MEDIA_SENDER);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -18,15 +18,14 @@ package org.thoughtcrime.securesms;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.app.Activity;
|
||||
import android.arch.lifecycle.Observer;
|
||||
import android.content.Context;
|
||||
import android.content.DialogInterface;
|
||||
import android.content.Intent;
|
||||
import android.database.Cursor;
|
||||
import android.net.Uri;
|
||||
import android.os.AsyncTask;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.os.Parcelable;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.v4.app.ActivityCompat;
|
||||
import android.support.v4.app.ActivityOptionsCompat;
|
||||
@ -37,17 +36,16 @@ import android.support.v7.app.AlertDialog;
|
||||
import android.support.v7.app.AppCompatActivity;
|
||||
import android.support.v7.view.ActionMode;
|
||||
import android.support.v7.widget.LinearLayoutManager;
|
||||
import android.support.v7.widget.LinearSmoothScroller;
|
||||
import android.support.v7.widget.RecyclerView;
|
||||
import android.support.v7.widget.RecyclerView.OnScrollListener;
|
||||
import android.text.ClipboardManager;
|
||||
import android.text.TextUtils;
|
||||
|
||||
import org.thoughtcrime.securesms.attachments.Attachment;
|
||||
import org.thoughtcrime.securesms.components.ConversationTypingView;
|
||||
import org.thoughtcrime.securesms.components.recyclerview.SmoothScrollingLinearLayoutManager;
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
|
||||
import android.util.DisplayMetrics;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuInflater;
|
||||
@ -74,6 +72,7 @@ import org.thoughtcrime.securesms.database.model.MediaMmsMessageRecord;
|
||||
import org.thoughtcrime.securesms.database.model.MessageRecord;
|
||||
import org.thoughtcrime.securesms.database.model.MmsMessageRecord;
|
||||
import org.thoughtcrime.securesms.jobs.DirectoryRefreshJob;
|
||||
import org.thoughtcrime.securesms.mediasend.Media;
|
||||
import org.thoughtcrime.securesms.mms.GlideApp;
|
||||
import org.thoughtcrime.securesms.mms.OutgoingMediaMessage;
|
||||
import org.thoughtcrime.securesms.mms.Slide;
|
||||
@ -83,12 +82,13 @@ import org.thoughtcrime.securesms.sms.MessageSender;
|
||||
import org.thoughtcrime.securesms.sms.OutgoingTextMessage;
|
||||
import org.thoughtcrime.securesms.util.CommunicationActions;
|
||||
import org.thoughtcrime.securesms.util.SaveAttachmentTask;
|
||||
import org.thoughtcrime.securesms.util.SaveAttachmentTask.Attachment;
|
||||
import org.thoughtcrime.securesms.util.StickyHeaderDecoration;
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||
import org.thoughtcrime.securesms.util.ViewUtil;
|
||||
import org.thoughtcrime.securesms.util.task.ProgressDialogAsyncTask;
|
||||
import org.whispersystems.libsignal.util.guava.Optional;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.Comparator;
|
||||
import java.util.LinkedList;
|
||||
@ -507,7 +507,32 @@ public class ConversationFragment extends Fragment
|
||||
composeIntent.putExtra(Intent.EXTRA_TEXT, message.getDisplayBody().toString());
|
||||
if (message.isMms()) {
|
||||
MmsMessageRecord mediaMessage = (MmsMessageRecord) message;
|
||||
if (mediaMessage.containsMediaSlide()) {
|
||||
boolean isAlbum = mediaMessage.containsMediaSlide() &&
|
||||
mediaMessage.getSlideDeck().getSlides().size() > 1 &&
|
||||
mediaMessage.getSlideDeck().getAudioSlide() == null &&
|
||||
mediaMessage.getSlideDeck().getDocumentSlide() == null;
|
||||
|
||||
if (isAlbum) {
|
||||
ArrayList<Media> mediaList = new ArrayList<>(mediaMessage.getSlideDeck().getSlides().size());
|
||||
|
||||
for (Attachment attachment : mediaMessage.getSlideDeck().asAttachments()) {
|
||||
Uri uri = attachment.getDataUri() != null ? attachment.getDataUri() : attachment.getThumbnailUri();
|
||||
|
||||
if (uri != null) {
|
||||
mediaList.add(new Media(uri,
|
||||
attachment.getContentType(),
|
||||
System.currentTimeMillis(),
|
||||
attachment.getWidth(),
|
||||
attachment.getHeight(),
|
||||
Optional.absent(),
|
||||
Optional.fromNullable(attachment.getCaption())));
|
||||
}
|
||||
}
|
||||
|
||||
if (!mediaList.isEmpty()) {
|
||||
composeIntent.putExtra(ConversationActivity.MEDIA_EXTRA, mediaList);
|
||||
}
|
||||
} else if (mediaMessage.containsMediaSlide()) {
|
||||
Slide slide = mediaMessage.getSlideDeck().getSlides().get(0);
|
||||
composeIntent.putExtra(Intent.EXTRA_STREAM, slide.getUri());
|
||||
composeIntent.setType(slide.getContentType());
|
||||
@ -537,7 +562,7 @@ public class ConversationFragment extends Fragment
|
||||
for (Slide slide : message.getSlideDeck().getSlides()) {
|
||||
if ((slide.hasImage() || slide.hasVideo() || slide.hasAudio() || slide.hasDocument()) && slide.getUri() != null) {
|
||||
SaveAttachmentTask saveTask = new SaveAttachmentTask(getActivity());
|
||||
saveTask.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, new Attachment(slide.getUri(), slide.getContentType(), message.getDateReceived(), slide.getFileName().orNull()));
|
||||
saveTask.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, new SaveAttachmentTask.Attachment(slide.getUri(), slide.getContentType(), message.getDateReceived(), slide.getFileName().orNull()));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
@ -60,7 +60,7 @@ import org.thoughtcrime.securesms.components.viewpager.ExtendedOnPageChangedList
|
||||
import org.thoughtcrime.securesms.database.Address;
|
||||
import org.thoughtcrime.securesms.database.MediaDatabase.MediaRecord;
|
||||
import org.thoughtcrime.securesms.database.loaders.PagingMediaLoader;
|
||||
import org.thoughtcrime.securesms.mediapreview.AlbumRailAdapter;
|
||||
import org.thoughtcrime.securesms.mediapreview.MediaRailAdapter;
|
||||
import org.thoughtcrime.securesms.mediapreview.MediaPreviewViewModel;
|
||||
import org.thoughtcrime.securesms.mms.GlideApp;
|
||||
import org.thoughtcrime.securesms.mms.GlideRequests;
|
||||
@ -82,7 +82,7 @@ import java.util.WeakHashMap;
|
||||
*/
|
||||
public class MediaPreviewActivity extends PassphraseRequiredActionBarActivity implements RecipientModifiedListener,
|
||||
LoaderManager.LoaderCallbacks<Pair<Cursor, Integer>>,
|
||||
AlbumRailAdapter.RailItemClickedListener
|
||||
MediaRailAdapter.RailItemListener
|
||||
{
|
||||
|
||||
private final static String TAG = MediaPreviewActivity.class.getSimpleName();
|
||||
@ -101,7 +101,7 @@ public class MediaPreviewActivity extends PassphraseRequiredActionBarActivity im
|
||||
private TextView caption;
|
||||
private View captionContainer;
|
||||
private RecyclerView albumRail;
|
||||
private AlbumRailAdapter albumRailAdapter;
|
||||
private MediaRailAdapter albumRailAdapter;
|
||||
private ViewGroup playbackControlsContainer;
|
||||
private Uri initialMediaUri;
|
||||
private String initialMediaType;
|
||||
@ -163,6 +163,11 @@ public class MediaPreviewActivity extends PassphraseRequiredActionBarActivity im
|
||||
mediaPager.setCurrentItem(mediaPager.getCurrentItem() + distanceFromActive);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRailItemDeleteClicked(int distanceFromActive) {
|
||||
throw new UnsupportedOperationException("Callback unsupported.");
|
||||
}
|
||||
|
||||
@SuppressWarnings("ConstantConditions")
|
||||
private void initializeActionBar() {
|
||||
MediaItem mediaItem = getCurrentMediaItem();
|
||||
@ -211,7 +216,7 @@ public class MediaPreviewActivity extends PassphraseRequiredActionBarActivity im
|
||||
mediaPager.addOnPageChangeListener(new ViewPagerListener());
|
||||
|
||||
albumRail = findViewById(R.id.media_preview_album_rail);
|
||||
albumRailAdapter = new AlbumRailAdapter(GlideApp.with(this), this);
|
||||
albumRailAdapter = new MediaRailAdapter(GlideApp.with(this), this, false);
|
||||
|
||||
albumRail.setLayoutManager(new LinearLayoutManager(this, LinearLayoutManager.HORIZONTAL, false));
|
||||
albumRail.setAdapter(albumRailAdapter);
|
||||
@ -254,7 +259,7 @@ public class MediaPreviewActivity extends PassphraseRequiredActionBarActivity im
|
||||
}
|
||||
|
||||
albumRail.setVisibility(previewData.getAlbumThumbnails().isEmpty() ? View.GONE : View.VISIBLE);
|
||||
albumRailAdapter.setRecords(previewData.getAlbumThumbnails(), previewData.getActivePosition());
|
||||
albumRailAdapter.setMedia(previewData.getAlbumThumbnails(), previewData.getActivePosition());
|
||||
albumRail.smoothScrollToPosition(previewData.getActivePosition());
|
||||
|
||||
captionContainer.setVisibility(previewData.getCaption() == null ? View.GONE : View.VISIBLE);
|
||||
@ -446,7 +451,7 @@ public class MediaPreviewActivity extends PassphraseRequiredActionBarActivity im
|
||||
mediaPager.setAdapter(adapter);
|
||||
adapter.setActive(true);
|
||||
|
||||
viewModel.setCursor(data.first, leftIsRecent);
|
||||
viewModel.setCursor(this, data.first, leftIsRecent);
|
||||
|
||||
if (restartItem < 0) mediaPager.setCurrentItem(data.second);
|
||||
else mediaPager.setCurrentItem(restartItem);
|
||||
|
@ -42,6 +42,7 @@ import org.thoughtcrime.securesms.contacts.ContactsCursorLoader.DisplayMode;
|
||||
import org.thoughtcrime.securesms.database.Address;
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
||||
import org.thoughtcrime.securesms.database.ThreadDatabase;
|
||||
import org.thoughtcrime.securesms.mediasend.Media;
|
||||
import org.thoughtcrime.securesms.mms.PartAuthority;
|
||||
import org.thoughtcrime.securesms.providers.PersistentBlobProvider;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
@ -56,6 +57,7 @@ import org.thoughtcrime.securesms.util.ViewUtil;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.util.ArrayList;
|
||||
|
||||
/**
|
||||
* An activity to quickly share content with contacts
|
||||
@ -254,9 +256,13 @@ public class ShareActivity extends PassphraseRequiredActionBarActivity
|
||||
}
|
||||
|
||||
private Intent getBaseShareIntent(final @NonNull Class<?> target) {
|
||||
final Intent intent = new Intent(this, target);
|
||||
final String textExtra = getIntent().getStringExtra(Intent.EXTRA_TEXT);
|
||||
final Intent intent = new Intent(this, target);
|
||||
final String textExtra = getIntent().getStringExtra(Intent.EXTRA_TEXT);
|
||||
final ArrayList<Media> mediaExtra = getIntent().getParcelableArrayListExtra(ConversationActivity.MEDIA_EXTRA);
|
||||
|
||||
intent.putExtra(ConversationActivity.TEXT_EXTRA, textExtra);
|
||||
intent.putExtra(ConversationActivity.MEDIA_EXTRA, mediaExtra);
|
||||
|
||||
if (resolvedExtra != null) intent.setDataAndType(resolvedExtra, mimeType);
|
||||
|
||||
return intent;
|
||||
|
@ -17,12 +17,9 @@ import org.thoughtcrime.securesms.logging.Log;
|
||||
import org.thoughtcrime.securesms.mms.DecryptableStreamUriLoader;
|
||||
import org.thoughtcrime.securesms.mms.GlideApp;
|
||||
import org.thoughtcrime.securesms.providers.MemoryBlobProvider;
|
||||
import org.thoughtcrime.securesms.providers.PersistentBlobProvider;
|
||||
import org.thoughtcrime.securesms.scribbles.ScribbleFragment;
|
||||
import org.thoughtcrime.securesms.util.DynamicLanguage;
|
||||
import org.thoughtcrime.securesms.util.MediaUtil;
|
||||
import org.thoughtcrime.securesms.util.concurrent.AssertedSuccessListener;
|
||||
import org.thoughtcrime.securesms.util.concurrent.LifecycleBoundTask;
|
||||
import org.thoughtcrime.securesms.util.concurrent.SettableFuture;
|
||||
import org.whispersystems.libsignal.util.guava.Optional;
|
||||
|
||||
@ -125,7 +122,7 @@ public class CameraActivity extends PassphraseRequiredActionBarActivity implemen
|
||||
result.addListener(new AssertedSuccessListener<Boolean>() {
|
||||
@Override
|
||||
public void onSuccess(Boolean result) {
|
||||
ScribbleFragment fragment = ScribbleFragment.newInstance(captureUri, dynamicLanguage.getCurrentLocale(), Optional.of(transport));
|
||||
ScribbleFragment fragment = ScribbleFragment.newInstance(captureUri, dynamicLanguage.getCurrentLocale(), Optional.of(transport), true);
|
||||
getSupportFragmentManager().beginTransaction()
|
||||
.setCustomAnimations(R.anim.fade_in, R.anim.fade_out, R.anim.fade_in, R.anim.fade_out)
|
||||
.replace(R.id.fragment_container, fragment, TAG_EDITOR)
|
||||
@ -163,4 +160,7 @@ public class CameraActivity extends PassphraseRequiredActionBarActivity implemen
|
||||
Toast.makeText(this, R.string.CameraActivity_image_save_failure, Toast.LENGTH_SHORT).show();
|
||||
finish();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onTouchEventsNeeded(boolean needed) { }
|
||||
}
|
||||
|
@ -257,10 +257,10 @@ public class AttachmentTypeSelector extends PopupWindow {
|
||||
|
||||
private class RecentPhotoSelectedListener implements RecentPhotoViewRail.OnItemClickedListener {
|
||||
@Override
|
||||
public void onItemClicked(Uri uri) {
|
||||
public void onItemClicked(Uri uri, String mimeType, String bucketId, long dateTaken, int width, int height) {
|
||||
animateWindowOutTranslate(getContentView());
|
||||
|
||||
if (listener != null) listener.onQuickAttachment(uri);
|
||||
if (listener != null) listener.onQuickAttachment(uri, mimeType, bucketId, dateTaken, width, height);
|
||||
}
|
||||
}
|
||||
|
||||
@ -289,8 +289,8 @@ public class AttachmentTypeSelector extends PopupWindow {
|
||||
}
|
||||
|
||||
public interface AttachmentClickedListener {
|
||||
public void onClick(int type);
|
||||
public void onQuickAttachment(Uri uri);
|
||||
void onClick(int type);
|
||||
void onQuickAttachment(Uri uri, String mimeType, String bucketId, long dateTaken, int width, int height);
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -7,10 +7,12 @@ import android.support.v4.view.ViewPager;
|
||||
import android.util.AttributeSet;
|
||||
import android.view.MotionEvent;
|
||||
|
||||
import org.thoughtcrime.securesms.components.viewpager.HackyViewPager;
|
||||
|
||||
/**
|
||||
* An implementation of {@link ViewPager} that disables swiping when the view is disabled.
|
||||
*/
|
||||
public class ControllableViewPager extends ViewPager {
|
||||
public class ControllableViewPager extends HackyViewPager {
|
||||
|
||||
public ControllableViewPager(@NonNull Context context) {
|
||||
super(context);
|
||||
|
@ -77,7 +77,7 @@ public class InputAwareLayout extends KeyboardAwareLinearLayout implements OnKey
|
||||
});
|
||||
}
|
||||
|
||||
protected void hideSoftkey(final EditText inputTarget, @Nullable Runnable runAfterClose) {
|
||||
public void hideSoftkey(final EditText inputTarget, @Nullable Runnable runAfterClose) {
|
||||
if (runAfterClose != null) postOnKeyboardClose(runAfterClose);
|
||||
|
||||
ServiceUtil.getInputMethodManager(inputTarget.getContext())
|
||||
|
@ -4,6 +4,7 @@ package org.thoughtcrime.securesms.components;
|
||||
import android.content.Context;
|
||||
import android.database.Cursor;
|
||||
import android.net.Uri;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.provider.MediaStore;
|
||||
import android.support.annotation.NonNull;
|
||||
@ -106,7 +107,10 @@ public class RecentPhotoViewRail extends FrameLayout implements LoaderManager.Lo
|
||||
long dateTaken = cursor.getLong(cursor.getColumnIndexOrThrow(MediaStore.Images.ImageColumns.DATE_TAKEN));
|
||||
long dateModified = cursor.getLong(cursor.getColumnIndexOrThrow(MediaStore.Images.ImageColumns.DATE_MODIFIED));
|
||||
String mimeType = cursor.getString(cursor.getColumnIndexOrThrow(MediaStore.Images.ImageColumns.MIME_TYPE));
|
||||
String bucketId = cursor.getString(cursor.getColumnIndexOrThrow(MediaStore.Images.ImageColumns.BUCKET_ID));
|
||||
int orientation = cursor.getInt(cursor.getColumnIndexOrThrow(MediaStore.Images.ImageColumns.ORIENTATION));
|
||||
int width = Build.VERSION.SDK_INT >= 16 ? cursor.getInt(cursor.getColumnIndexOrThrow(MediaStore.Images.ImageColumns.WIDTH)) : 0;
|
||||
int height = Build.VERSION.SDK_INT >= 16 ? cursor.getInt(cursor.getColumnIndexOrThrow(MediaStore.Images.ImageColumns.HEIGHT)) : 0;
|
||||
|
||||
final Uri uri = Uri.withAppendedPath(baseUri, Long.toString(id));
|
||||
|
||||
@ -119,7 +123,7 @@ public class RecentPhotoViewRail extends FrameLayout implements LoaderManager.Lo
|
||||
.into(viewHolder.imageView);
|
||||
|
||||
viewHolder.imageView.setOnClickListener(v -> {
|
||||
if (clickedListener != null) clickedListener.onItemClicked(uri);
|
||||
if (clickedListener != null) clickedListener.onItemClicked(uri, mimeType, bucketId, dateTaken, width, height);
|
||||
});
|
||||
|
||||
}
|
||||
@ -141,6 +145,6 @@ public class RecentPhotoViewRail extends FrameLayout implements LoaderManager.Lo
|
||||
}
|
||||
|
||||
public interface OnItemClickedListener {
|
||||
void onItemClicked(Uri uri);
|
||||
void onItemClicked(Uri uri, String mimeType, String bucketId, long dateTaken, int width, int height);
|
||||
}
|
||||
}
|
||||
|
@ -106,7 +106,7 @@ public class SendButton extends ImageButton
|
||||
|
||||
@Override
|
||||
public boolean onLongClick(View v) {
|
||||
if (transportOptions.getEnabledTransports().size() > 1) {
|
||||
if (isEnabled() && transportOptions.getEnabledTransports().size() > 1) {
|
||||
getTransportOptionsPopup().display(transportOptions.getEnabledTransports());
|
||||
return true;
|
||||
}
|
||||
|
@ -295,11 +295,18 @@ public class ThumbnailView extends FrameLayout {
|
||||
SettableFuture<Boolean> future = new SettableFuture<>();
|
||||
|
||||
if (transferControls.isPresent()) getTransferControls().setVisibility(View.GONE);
|
||||
glideRequests.load(new DecryptableUri(uri))
|
||||
.diskCacheStrategy(DiskCacheStrategy.NONE)
|
||||
.transforms(new CenterCrop(), new RoundedCorners(radius))
|
||||
.transition(withCrossFade())
|
||||
.into(new GlideDrawableListeningTarget(image, future));
|
||||
|
||||
GlideRequest request = glideRequests.load(new DecryptableUri(uri))
|
||||
.diskCacheStrategy(DiskCacheStrategy.NONE)
|
||||
.transition(withCrossFade());
|
||||
|
||||
if (radius > 0) {
|
||||
request = request.transforms(new CenterCrop(), new RoundedCorners(radius));
|
||||
} else {
|
||||
request = request.transforms(new CenterCrop());
|
||||
}
|
||||
|
||||
request.into(new GlideDrawableListeningTarget(image, future));
|
||||
|
||||
return future;
|
||||
}
|
||||
|
@ -27,7 +27,6 @@ import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.attachments.Attachment;
|
||||
import org.thoughtcrime.securesms.database.AttachmentDatabase;
|
||||
import org.thoughtcrime.securesms.events.PartProgressEvent;
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
import org.thoughtcrime.securesms.mms.Slide;
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
import org.thoughtcrime.securesms.util.ViewUtil;
|
||||
@ -117,11 +116,11 @@ public class TransferControlView extends FrameLayout {
|
||||
if (!isUpdateToExistingSet(slides)) {
|
||||
downloadProgress.clear();
|
||||
Stream.of(slides).forEach(s -> downloadProgress.put(s.asAttachment(), 0f));
|
||||
} else {
|
||||
for (Slide slide : slides) {
|
||||
if (slide.asAttachment().getTransferState() == AttachmentDatabase.TRANSFER_PROGRESS_DONE) {
|
||||
downloadProgress.put(slide.asAttachment(), 1f);
|
||||
}
|
||||
}
|
||||
|
||||
for (Slide slide : slides) {
|
||||
if (slide.asAttachment().getTransferState() == AttachmentDatabase.TRANSFER_PROGRESS_DONE) {
|
||||
downloadProgress.put(slide.asAttachment(), 1f);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -85,9 +85,7 @@ public class MediaDatabase extends Database {
|
||||
private final long date;
|
||||
private final boolean outgoing;
|
||||
|
||||
// TODO: Make private again
|
||||
public MediaRecord(DatabaseAttachment attachment, @Nullable Address address, long date, boolean outgoing) {
|
||||
// private MediaRecord(DatabaseAttachment attachment, @Nullable Address address, long date, boolean outgoing) {
|
||||
private MediaRecord(DatabaseAttachment attachment, @Nullable Address address, long date, boolean outgoing) {
|
||||
this.attachment = attachment;
|
||||
this.address = address;
|
||||
this.date = date;
|
||||
|
@ -5,6 +5,7 @@ import android.Manifest;
|
||||
import android.content.Context;
|
||||
import android.database.Cursor;
|
||||
import android.net.Uri;
|
||||
import android.os.Build;
|
||||
import android.provider.MediaStore;
|
||||
import android.support.v4.content.CursorLoader;
|
||||
|
||||
@ -19,7 +20,19 @@ public class RecentPhotosLoader extends CursorLoader {
|
||||
MediaStore.Images.ImageColumns.DATE_TAKEN,
|
||||
MediaStore.Images.ImageColumns.DATE_MODIFIED,
|
||||
MediaStore.Images.ImageColumns.ORIENTATION,
|
||||
MediaStore.Images.ImageColumns.MIME_TYPE
|
||||
MediaStore.Images.ImageColumns.MIME_TYPE,
|
||||
MediaStore.Images.ImageColumns.BUCKET_ID
|
||||
};
|
||||
|
||||
private static final String[] PROJECTION_16 = new String[] {
|
||||
MediaStore.Images.ImageColumns._ID,
|
||||
MediaStore.Images.ImageColumns.DATE_TAKEN,
|
||||
MediaStore.Images.ImageColumns.DATE_MODIFIED,
|
||||
MediaStore.Images.ImageColumns.ORIENTATION,
|
||||
MediaStore.Images.ImageColumns.MIME_TYPE,
|
||||
MediaStore.Images.ImageColumns.BUCKET_ID,
|
||||
MediaStore.Images.ImageColumns.WIDTH,
|
||||
MediaStore.Images.ImageColumns.HEIGHT
|
||||
};
|
||||
|
||||
private final Context context;
|
||||
@ -31,9 +44,11 @@ public class RecentPhotosLoader extends CursorLoader {
|
||||
|
||||
@Override
|
||||
public Cursor loadInBackground() {
|
||||
String[] projection = Build.VERSION.SDK_INT >= 16 ? PROJECTION_16 : PROJECTION;
|
||||
|
||||
if (Permissions.hasAll(context, Manifest.permission.WRITE_EXTERNAL_STORAGE)) {
|
||||
return context.getContentResolver().query(MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
|
||||
PROJECTION, null, null,
|
||||
projection, null, null,
|
||||
MediaStore.Images.ImageColumns.DATE_MODIFIED + " DESC");
|
||||
} else {
|
||||
return null;
|
||||
|
@ -29,9 +29,11 @@ import org.whispersystems.signalservice.api.push.exceptions.PushNetworkException
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.net.ConnectException;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import javax.inject.Inject;
|
||||
import javax.net.ssl.SSLException;
|
||||
|
||||
import androidx.work.Data;
|
||||
import androidx.work.WorkerParameters;
|
||||
@ -94,7 +96,9 @@ public class AttachmentUploadJob extends ContextJob implements InjectableType {
|
||||
|
||||
@Override
|
||||
protected boolean onShouldRetry(Exception exception) {
|
||||
return exception instanceof PushNetworkException;
|
||||
return exception instanceof PushNetworkException ||
|
||||
exception instanceof SSLException ||
|
||||
exception instanceof ConnectException;
|
||||
}
|
||||
|
||||
protected SignalServiceAttachment getAttachmentFor(Attachment attachment) {
|
||||
|
@ -1,100 +0,0 @@
|
||||
package org.thoughtcrime.securesms.mediapreview;
|
||||
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.v7.widget.RecyclerView;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.components.ThumbnailView;
|
||||
import org.thoughtcrime.securesms.database.MediaDatabase.MediaRecord;
|
||||
import org.thoughtcrime.securesms.mms.GlideRequests;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
public class AlbumRailAdapter extends RecyclerView.Adapter<AlbumRailAdapter.AlbumRailViewHolder> {
|
||||
|
||||
private final GlideRequests glideRequests;
|
||||
private final List<MediaRecord> records;
|
||||
private final RailItemClickedListener listener;
|
||||
|
||||
private int activePosition;
|
||||
|
||||
public AlbumRailAdapter(@NonNull GlideRequests glideRequests, @NonNull RailItemClickedListener listener) {
|
||||
this.glideRequests = glideRequests;
|
||||
this.records = new ArrayList<>();
|
||||
this.listener = listener;
|
||||
|
||||
setHasStableIds(true);
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public AlbumRailViewHolder onCreateViewHolder(@NonNull ViewGroup viewGroup, int i) {
|
||||
return new AlbumRailViewHolder(LayoutInflater.from(viewGroup.getContext()).inflate(R.layout.media_preview_album_rail_item, viewGroup, false));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBindViewHolder(@NonNull AlbumRailViewHolder albumRailViewHolder, int i) {
|
||||
albumRailViewHolder.bind(records.get(i), i == activePosition, glideRequests, listener, i - activePosition);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onViewRecycled(@NonNull AlbumRailViewHolder holder) {
|
||||
holder.recycle();
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getItemId(int position) {
|
||||
return records.get(position).getAttachment().getAttachmentId().getUniqueId();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getItemCount() {
|
||||
return records.size();
|
||||
}
|
||||
|
||||
public void setRecords(@NonNull List<MediaRecord> records, int activePosition) {
|
||||
this.activePosition = activePosition;
|
||||
|
||||
this.records.clear();
|
||||
this.records.addAll(records);
|
||||
|
||||
notifyDataSetChanged();
|
||||
}
|
||||
|
||||
static class AlbumRailViewHolder extends RecyclerView.ViewHolder {
|
||||
|
||||
private final ThumbnailView image;
|
||||
|
||||
AlbumRailViewHolder(@NonNull View itemView) {
|
||||
super(itemView);
|
||||
image = (ThumbnailView) itemView;
|
||||
}
|
||||
|
||||
void bind(@NonNull MediaRecord record, boolean isActive, @NonNull GlideRequests glideRequests,
|
||||
@NonNull RailItemClickedListener railItemClickedListener, int distanceFromActive)
|
||||
{
|
||||
if (record.getAttachment().getThumbnailUri() != null) {
|
||||
image.setImageResource(glideRequests, record.getAttachment().getThumbnailUri());
|
||||
} else if (record.getAttachment().getDataUri() != null) {
|
||||
image.setImageResource(glideRequests, record.getAttachment().getDataUri());
|
||||
} else {
|
||||
image.clear(glideRequests);
|
||||
}
|
||||
|
||||
image.setBackgroundResource(isActive ? R.drawable.album_rail_item_background : 0);
|
||||
image.setOnClickListener(v -> railItemClickedListener.onRailItemClicked(distanceFromActive));
|
||||
}
|
||||
|
||||
void recycle() {
|
||||
image.setOnClickListener(null);
|
||||
}
|
||||
}
|
||||
|
||||
public interface RailItemClickedListener {
|
||||
void onRailItemClicked(int distanceFromActive);
|
||||
}
|
||||
}
|
@ -5,10 +5,13 @@ import android.arch.lifecycle.MutableLiveData;
|
||||
import android.arch.lifecycle.ViewModel;
|
||||
import android.content.Context;
|
||||
import android.database.Cursor;
|
||||
import android.net.Uri;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.annotation.Nullable;
|
||||
|
||||
import org.thoughtcrime.securesms.database.MediaDatabase.MediaRecord;
|
||||
import org.thoughtcrime.securesms.mediasend.Media;
|
||||
import org.whispersystems.libsignal.util.guava.Optional;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedList;
|
||||
@ -22,9 +25,15 @@ public class MediaPreviewViewModel extends ViewModel {
|
||||
|
||||
private @Nullable Cursor cursor;
|
||||
|
||||
public void setCursor(@Nullable Cursor cursor, boolean leftIsRecent) {
|
||||
public void setCursor(@NonNull Context context, @Nullable Cursor cursor, boolean leftIsRecent) {
|
||||
boolean firstLoad = (this.cursor == null) && (cursor != null);
|
||||
|
||||
this.cursor = cursor;
|
||||
this.leftIsRecent = leftIsRecent;
|
||||
|
||||
if (firstLoad) {
|
||||
setActiveAlbumRailItem(context, 0);
|
||||
}
|
||||
}
|
||||
|
||||
public void setActiveAlbumRailItem(@NonNull Context context, int activePosition) {
|
||||
@ -37,15 +46,17 @@ public class MediaPreviewViewModel extends ViewModel {
|
||||
|
||||
cursor.moveToPosition(activePosition);
|
||||
|
||||
MediaRecord activeRecord = MediaRecord.from(context, cursor);
|
||||
LinkedList<MediaRecord> rail = new LinkedList<>();
|
||||
MediaRecord activeRecord = MediaRecord.from(context, cursor);
|
||||
LinkedList<Media> rail = new LinkedList<>();
|
||||
|
||||
rail.add(activeRecord);
|
||||
Media activeMedia = toMedia(activeRecord);
|
||||
if (activeMedia != null) rail.add(activeMedia);
|
||||
|
||||
while (cursor.moveToPrevious()) {
|
||||
MediaRecord record = MediaRecord.from(context, cursor);
|
||||
if (record.getAttachment().getMmsId() == activeRecord.getAttachment().getMmsId()) {
|
||||
rail.addFirst(record);
|
||||
Media media = toMedia(record);
|
||||
if (media != null) rail.addFirst(media);
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
@ -56,7 +67,8 @@ public class MediaPreviewViewModel extends ViewModel {
|
||||
while (cursor.moveToNext()) {
|
||||
MediaRecord record = MediaRecord.from(context, cursor);
|
||||
if (record.getAttachment().getMmsId() == activeRecord.getAttachment().getMmsId()) {
|
||||
rail.addLast(record);
|
||||
Media media = toMedia(record);
|
||||
if (media != null) rail.addLast(media);
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
@ -68,7 +80,7 @@ public class MediaPreviewViewModel extends ViewModel {
|
||||
|
||||
previewData.postValue(new PreviewData(rail.size() > 1 ? rail : Collections.emptyList(),
|
||||
activeRecord.getAttachment().getCaption(),
|
||||
rail.indexOf(activeRecord)));
|
||||
rail.indexOf(activeMedia)));
|
||||
}
|
||||
|
||||
private int getCursorPosition(int position) {
|
||||
@ -80,22 +92,39 @@ public class MediaPreviewViewModel extends ViewModel {
|
||||
else return cursor.getCount() - 1 - position;
|
||||
}
|
||||
|
||||
private @Nullable Media toMedia(@NonNull MediaRecord mediaRecord) {
|
||||
Uri uri = mediaRecord.getAttachment().getThumbnailUri() != null ? mediaRecord.getAttachment().getThumbnailUri()
|
||||
: mediaRecord.getAttachment().getDataUri();
|
||||
|
||||
if (uri == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return new Media(uri,
|
||||
mediaRecord.getContentType(),
|
||||
mediaRecord.getDate(),
|
||||
mediaRecord.getAttachment().getWidth(),
|
||||
mediaRecord.getAttachment().getHeight(),
|
||||
Optional.absent(),
|
||||
Optional.fromNullable(mediaRecord.getAttachment().getCaption()));
|
||||
}
|
||||
|
||||
public LiveData<PreviewData> getPreviewData() {
|
||||
return previewData;
|
||||
}
|
||||
|
||||
public static class PreviewData {
|
||||
private final List<MediaRecord> albumThumbnails;
|
||||
private final String caption;
|
||||
private final int activePosition;
|
||||
private final List<Media> albumThumbnails;
|
||||
private final String caption;
|
||||
private final int activePosition;
|
||||
|
||||
public PreviewData(@NonNull List<MediaRecord> albumThumbnails, @Nullable String caption, int activePosition) {
|
||||
public PreviewData(@NonNull List<Media> albumThumbnails, @Nullable String caption, int activePosition) {
|
||||
this.albumThumbnails = albumThumbnails;
|
||||
this.caption = caption;
|
||||
this.activePosition = activePosition;
|
||||
}
|
||||
|
||||
public @NonNull List<MediaRecord> getAlbumThumbnails() {
|
||||
public @NonNull List<Media> getAlbumThumbnails() {
|
||||
return albumThumbnails;
|
||||
}
|
||||
|
||||
|
@ -0,0 +1,108 @@
|
||||
package org.thoughtcrime.securesms.mediapreview;
|
||||
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.v7.widget.RecyclerView;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.components.ThumbnailView;
|
||||
import org.thoughtcrime.securesms.mediasend.Media;
|
||||
import org.thoughtcrime.securesms.mms.GlideRequests;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
public class MediaRailAdapter extends RecyclerView.Adapter<MediaRailAdapter.MediaRailViewHolder> {
|
||||
|
||||
private final GlideRequests glideRequests;
|
||||
private final List<Media> media;
|
||||
private final RailItemListener listener;
|
||||
private final boolean deleteEnabled;
|
||||
|
||||
private int activePosition;
|
||||
|
||||
public MediaRailAdapter(@NonNull GlideRequests glideRequests, @NonNull RailItemListener listener, boolean deleteEnabled) {
|
||||
this.glideRequests = glideRequests;
|
||||
this.media = new ArrayList<>();
|
||||
this.listener = listener;
|
||||
this.deleteEnabled = deleteEnabled;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public MediaRailViewHolder onCreateViewHolder(@NonNull ViewGroup viewGroup, int i) {
|
||||
return new MediaRailViewHolder(LayoutInflater.from(viewGroup.getContext()).inflate(R.layout.media_preview_album_rail_item, viewGroup, false));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBindViewHolder(@NonNull MediaRailViewHolder mediaRailViewHolder, int i) {
|
||||
mediaRailViewHolder.bind(media.get(i), i == activePosition, glideRequests, listener, i - activePosition, deleteEnabled);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onViewRecycled(@NonNull MediaRailViewHolder holder) {
|
||||
holder.recycle();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getItemCount() {
|
||||
return media.size();
|
||||
}
|
||||
|
||||
public void setMedia(@NonNull List<Media> media) {
|
||||
setMedia(media, activePosition);
|
||||
}
|
||||
|
||||
public void setMedia(@NonNull List<Media> records, int activePosition) {
|
||||
this.activePosition = activePosition;
|
||||
|
||||
this.media.clear();
|
||||
this.media.addAll(records);
|
||||
|
||||
notifyDataSetChanged();
|
||||
}
|
||||
|
||||
public void setActivePosition(int activePosition) {
|
||||
this.activePosition = activePosition;
|
||||
notifyDataSetChanged();
|
||||
}
|
||||
|
||||
static class MediaRailViewHolder extends RecyclerView.ViewHolder {
|
||||
|
||||
private final ThumbnailView image;
|
||||
private final View deleteButton;
|
||||
|
||||
MediaRailViewHolder(@NonNull View itemView) {
|
||||
super(itemView);
|
||||
image = itemView.findViewById(R.id.rail_item_image);
|
||||
deleteButton = itemView.findViewById(R.id.rail_item_delete);
|
||||
}
|
||||
|
||||
void bind(@NonNull Media media, boolean isActive, @NonNull GlideRequests glideRequests,
|
||||
@NonNull RailItemListener railItemListener, int distanceFromActive, boolean deleteEnabled)
|
||||
{
|
||||
image.setImageResource(glideRequests, media.getUri());
|
||||
image.setBackgroundResource(isActive ? R.drawable.media_rail_item_background : 0);
|
||||
image.setOnClickListener(v -> railItemListener.onRailItemClicked(distanceFromActive));
|
||||
|
||||
if (deleteEnabled && isActive) {
|
||||
deleteButton.setVisibility(View.VISIBLE);
|
||||
deleteButton.setOnClickListener(v -> railItemListener.onRailItemDeleteClicked(distanceFromActive));
|
||||
} else {
|
||||
deleteButton.setVisibility(View.GONE);
|
||||
}
|
||||
}
|
||||
|
||||
void recycle() {
|
||||
image.setOnClickListener(null);
|
||||
deleteButton.setOnClickListener(null);
|
||||
}
|
||||
}
|
||||
|
||||
public interface RailItemListener {
|
||||
void onRailItemClicked(int distanceFromActive);
|
||||
void onRailItemDeleteClicked(int distanceFromActive);
|
||||
}
|
||||
}
|
118
src/org/thoughtcrime/securesms/mediasend/Media.java
Normal file
@ -0,0 +1,118 @@
|
||||
package org.thoughtcrime.securesms.mediasend;
|
||||
|
||||
import android.net.Uri;
|
||||
import android.os.Parcel;
|
||||
import android.os.Parcelable;
|
||||
import android.support.annotation.NonNull;
|
||||
|
||||
import org.whispersystems.libsignal.util.guava.Optional;
|
||||
|
||||
/**
|
||||
* Represents a piece of media that the user has on their device.
|
||||
*/
|
||||
public class Media implements Parcelable {
|
||||
|
||||
private final Uri uri;
|
||||
private final String mimeType;
|
||||
private final long date;
|
||||
private final int width;
|
||||
private final int height;
|
||||
|
||||
private Optional<String> bucketId;
|
||||
private Optional<String> caption;
|
||||
|
||||
public Media(@NonNull Uri uri, @NonNull String mimeType, long date, int width, int height, Optional<String> bucketId, Optional<String> caption) {
|
||||
this.uri = uri;
|
||||
this.mimeType = mimeType;
|
||||
this.date = date;
|
||||
this.width = width;
|
||||
this.height = height;
|
||||
this.bucketId = bucketId;
|
||||
this.caption = caption;
|
||||
}
|
||||
|
||||
protected Media(Parcel in) {
|
||||
uri = in.readParcelable(Uri.class.getClassLoader());
|
||||
mimeType = in.readString();
|
||||
date = in.readLong();
|
||||
width = in.readInt();
|
||||
height = in.readInt();
|
||||
bucketId = Optional.fromNullable(in.readString());
|
||||
caption = Optional.fromNullable(in.readString());
|
||||
}
|
||||
|
||||
public Uri getUri() {
|
||||
return uri;
|
||||
}
|
||||
|
||||
public String getMimeType() {
|
||||
return mimeType;
|
||||
}
|
||||
|
||||
public long getDate() {
|
||||
return date;
|
||||
}
|
||||
|
||||
public int getWidth() {
|
||||
return width;
|
||||
}
|
||||
|
||||
public int getHeight() {
|
||||
return height;
|
||||
}
|
||||
|
||||
public Optional<String> getBucketId() {
|
||||
return bucketId;
|
||||
}
|
||||
|
||||
public Optional<String> getCaption() {
|
||||
return caption;
|
||||
}
|
||||
|
||||
public void setCaption(String caption) {
|
||||
this.caption = Optional.fromNullable(caption);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int describeContents() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void writeToParcel(Parcel dest, int flags) {
|
||||
dest.writeParcelable(uri, flags);
|
||||
dest.writeString(mimeType);
|
||||
dest.writeLong(date);
|
||||
dest.writeInt(width);
|
||||
dest.writeInt(height);
|
||||
dest.writeString(bucketId.orNull());
|
||||
dest.writeString(caption.orNull());
|
||||
}
|
||||
|
||||
public static final Creator<Media> CREATOR = new Creator<Media>() {
|
||||
@Override
|
||||
public Media createFromParcel(Parcel in) {
|
||||
return new Media(in);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Media[] newArray(int size) {
|
||||
return new Media[size];
|
||||
}
|
||||
};
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) return true;
|
||||
if (o == null || getClass() != o.getClass()) return false;
|
||||
|
||||
Media media = (Media) o;
|
||||
|
||||
return uri.equals(media.uri);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return uri.hashCode();
|
||||
}
|
||||
}
|
48
src/org/thoughtcrime/securesms/mediasend/MediaFolder.java
Normal file
@ -0,0 +1,48 @@
|
||||
package org.thoughtcrime.securesms.mediasend;
|
||||
|
||||
import android.net.Uri;
|
||||
import android.support.annotation.NonNull;
|
||||
|
||||
/**
|
||||
* Represents a folder that's shown in {@link MediaPickerFolderFragment}.
|
||||
*/
|
||||
public class MediaFolder {
|
||||
|
||||
private final Uri thumbnailUri;
|
||||
private final String title;
|
||||
private final int itemCount;
|
||||
private final String bucketId;
|
||||
private final FolderType folderType;
|
||||
|
||||
MediaFolder(@NonNull Uri thumbnailUri, @NonNull String title, int itemCount, @NonNull String bucketId, @NonNull FolderType folderType) {
|
||||
this.thumbnailUri = thumbnailUri;
|
||||
this.title = title;
|
||||
this.itemCount = itemCount;
|
||||
this.bucketId = bucketId;
|
||||
this.folderType = folderType;
|
||||
}
|
||||
|
||||
Uri getThumbnailUri() {
|
||||
return thumbnailUri;
|
||||
}
|
||||
|
||||
public String getTitle() {
|
||||
return title;
|
||||
}
|
||||
|
||||
int getItemCount() {
|
||||
return itemCount;
|
||||
}
|
||||
|
||||
public String getBucketId() {
|
||||
return bucketId;
|
||||
}
|
||||
|
||||
FolderType getFolderType() {
|
||||
return folderType;
|
||||
}
|
||||
|
||||
enum FolderType {
|
||||
NORMAL, CAMERA
|
||||
}
|
||||
}
|
@ -0,0 +1,96 @@
|
||||
package org.thoughtcrime.securesms.mediasend;
|
||||
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.v7.widget.RecyclerView;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.TextView;
|
||||
|
||||
import com.bumptech.glide.load.engine.DiskCacheStrategy;
|
||||
import com.bumptech.glide.load.resource.drawable.DrawableTransitionOptions;
|
||||
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.mms.GlideRequests;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
class MediaPickerFolderAdapter extends RecyclerView.Adapter<MediaPickerFolderAdapter.FolderViewHolder> {
|
||||
|
||||
private final GlideRequests glideRequests;
|
||||
private final EventListener eventListener;
|
||||
private final List<MediaFolder> folders;
|
||||
|
||||
MediaPickerFolderAdapter(@NonNull GlideRequests glideRequests, @NonNull EventListener eventListener) {
|
||||
this.glideRequests = glideRequests;
|
||||
this.eventListener = eventListener;
|
||||
this.folders = new ArrayList<>();
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public FolderViewHolder onCreateViewHolder(@NonNull ViewGroup viewGroup, int i) {
|
||||
return new FolderViewHolder(LayoutInflater.from(viewGroup.getContext()).inflate(R.layout.mediapicker_folder_item, viewGroup, false));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBindViewHolder(@NonNull FolderViewHolder folderViewHolder, int i) {
|
||||
folderViewHolder.bind(folders.get(i), glideRequests, eventListener);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onViewRecycled(@NonNull FolderViewHolder holder) {
|
||||
holder.recycle();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getItemCount() {
|
||||
return folders.size();
|
||||
}
|
||||
|
||||
void setFolders(@NonNull List<MediaFolder> folders) {
|
||||
this.folders.clear();
|
||||
this.folders.addAll(folders);
|
||||
notifyDataSetChanged();
|
||||
}
|
||||
|
||||
static class FolderViewHolder extends RecyclerView.ViewHolder {
|
||||
|
||||
private final ImageView thumbnail;
|
||||
private final ImageView icon;
|
||||
private final TextView title;
|
||||
private final TextView count;
|
||||
|
||||
FolderViewHolder(@NonNull View itemView) {
|
||||
super(itemView);
|
||||
|
||||
thumbnail = itemView.findViewById(R.id.mediapicker_folder_item_thumbnail);
|
||||
icon = itemView.findViewById(R.id.mediapicker_folder_item_icon);
|
||||
title = itemView.findViewById(R.id.mediapicker_folder_item_title);
|
||||
count = itemView.findViewById(R.id.mediapicker_folder_item_count);
|
||||
}
|
||||
|
||||
void bind(@NonNull MediaFolder folder, @NonNull GlideRequests glideRequests, @NonNull EventListener eventListener) {
|
||||
title.setText(folder.getTitle());
|
||||
count.setText(String.valueOf(folder.getItemCount()));
|
||||
icon.setImageResource(folder.getFolderType() == MediaFolder.FolderType.CAMERA ? R.drawable.ic_camera_alt_white_24dp : R.drawable.ic_folder_white_48dp);
|
||||
|
||||
glideRequests.load(folder.getThumbnailUri())
|
||||
.diskCacheStrategy(DiskCacheStrategy.NONE)
|
||||
.transition(DrawableTransitionOptions.withCrossFade())
|
||||
.into(thumbnail);
|
||||
|
||||
itemView.setOnClickListener(v -> eventListener.onFolderClicked(folder));
|
||||
}
|
||||
|
||||
void recycle() {
|
||||
itemView.setOnClickListener(null);
|
||||
}
|
||||
}
|
||||
|
||||
interface EventListener {
|
||||
void onFolderClicked(@NonNull MediaFolder mediaFolder);
|
||||
}
|
||||
}
|
@ -0,0 +1,125 @@
|
||||
package org.thoughtcrime.securesms.mediasend;
|
||||
|
||||
import android.arch.lifecycle.ViewModelProviders;
|
||||
import android.content.Context;
|
||||
import android.content.res.Configuration;
|
||||
import android.graphics.Point;
|
||||
import android.os.Bundle;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.support.v4.app.Fragment;
|
||||
import android.support.v7.app.AppCompatActivity;
|
||||
import android.support.v7.widget.GridLayoutManager;
|
||||
import android.support.v7.widget.RecyclerView;
|
||||
import android.support.v7.widget.Toolbar;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.mms.GlideApp;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.whispersystems.libsignal.util.guava.Optional;
|
||||
|
||||
/**
|
||||
* Allows the user to select a media folder to explore.
|
||||
*/
|
||||
public class MediaPickerFolderFragment extends Fragment implements MediaPickerFolderAdapter.EventListener {
|
||||
|
||||
private static final String KEY_RECIPIENT_NAME = "recipient_name";
|
||||
|
||||
private String recipientName;
|
||||
private MediaSendViewModel viewModel;
|
||||
private Controller controller;
|
||||
private GridLayoutManager layoutManager;
|
||||
|
||||
public static @NonNull MediaPickerFolderFragment newInstance(@NonNull Recipient recipient) {
|
||||
String name = Optional.fromNullable(recipient.getName())
|
||||
.or(Optional.fromNullable(recipient.getProfileName()))
|
||||
.or(recipient.toShortString());
|
||||
|
||||
Bundle args = new Bundle();
|
||||
args.putString(KEY_RECIPIENT_NAME, name);
|
||||
|
||||
MediaPickerFolderFragment fragment = new MediaPickerFolderFragment();
|
||||
fragment.setArguments(args);
|
||||
|
||||
return fragment;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreate(@Nullable Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
recipientName = getArguments().getString(KEY_RECIPIENT_NAME);
|
||||
viewModel = ViewModelProviders.of(requireActivity(), new MediaSendViewModel.Factory(new MediaRepository())).get(MediaSendViewModel.class);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAttach(Context context) {
|
||||
super.onAttach(context);
|
||||
|
||||
if (!(getActivity() instanceof Controller)) {
|
||||
throw new IllegalStateException("Parent activity must implement controller class.");
|
||||
}
|
||||
|
||||
controller = (Controller) getActivity();
|
||||
}
|
||||
|
||||
@Override
|
||||
public @Nullable View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
|
||||
return inflater.inflate(R.layout.mediapicker_folder_fragment, container, false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
|
||||
super.onViewCreated(view, savedInstanceState);
|
||||
|
||||
RecyclerView list = view.findViewById(R.id.mediapicker_folder_list);
|
||||
MediaPickerFolderAdapter adapter = new MediaPickerFolderAdapter(GlideApp.with(this), this);
|
||||
|
||||
layoutManager = new GridLayoutManager(requireContext(), 2);
|
||||
onScreenWidthChanged(getScreenWidth());
|
||||
|
||||
list.setLayoutManager(layoutManager);
|
||||
list.setAdapter(adapter);
|
||||
|
||||
viewModel.getFolders(requireContext()).observe(this, adapter::setFolders);
|
||||
|
||||
initToolbar(view.findViewById(R.id.mediapicker_toolbar));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onConfigurationChanged(Configuration newConfig) {
|
||||
super.onConfigurationChanged(newConfig);
|
||||
onScreenWidthChanged(getScreenWidth());
|
||||
}
|
||||
|
||||
private void initToolbar(Toolbar toolbar) {
|
||||
((AppCompatActivity) requireActivity()).setSupportActionBar(toolbar);
|
||||
((AppCompatActivity) requireActivity()).getSupportActionBar().setDisplayHomeAsUpEnabled(true);
|
||||
((AppCompatActivity) requireActivity()).getSupportActionBar().setTitle(getString(R.string.MediaPickerActivity_send_to, recipientName));
|
||||
|
||||
toolbar.setNavigationOnClickListener(v -> requireActivity().onBackPressed());
|
||||
}
|
||||
|
||||
private void onScreenWidthChanged(int newWidth) {
|
||||
if (layoutManager != null) {
|
||||
layoutManager.setSpanCount(newWidth / getResources().getDimensionPixelSize(R.dimen.media_picker_folder_width));
|
||||
}
|
||||
}
|
||||
|
||||
private int getScreenWidth() {
|
||||
Point size = new Point();
|
||||
requireActivity().getWindowManager().getDefaultDisplay().getSize(size);
|
||||
return size.x;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFolderClicked(@NonNull MediaFolder folder) {
|
||||
controller.onFolderSelected(folder);
|
||||
}
|
||||
|
||||
public interface Controller {
|
||||
void onFolderSelected(@NonNull MediaFolder folder);
|
||||
}
|
||||
}
|
@ -0,0 +1,147 @@
|
||||
package org.thoughtcrime.securesms.mediasend;
|
||||
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.v7.widget.RecyclerView;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.ImageView;
|
||||
|
||||
import com.bumptech.glide.load.engine.DiskCacheStrategy;
|
||||
import com.bumptech.glide.load.resource.drawable.DrawableTransitionOptions;
|
||||
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.mms.GlideRequests;
|
||||
import org.thoughtcrime.securesms.util.MediaUtil;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.TreeSet;
|
||||
|
||||
public class MediaPickerItemAdapter extends RecyclerView.Adapter<MediaPickerItemAdapter.ItemViewHolder> {
|
||||
|
||||
private final GlideRequests glideRequests;
|
||||
private final EventListener eventListener;
|
||||
private final List<Media> media;
|
||||
private final Set<Media> selected;
|
||||
private final int maxSelection;
|
||||
|
||||
private boolean forcedMultiSelect;
|
||||
|
||||
public MediaPickerItemAdapter(@NonNull GlideRequests glideRequests, @NonNull EventListener eventListener, int maxSelection) {
|
||||
this.glideRequests = glideRequests;
|
||||
this.eventListener = eventListener;
|
||||
this.media = new ArrayList<>();
|
||||
this.maxSelection = maxSelection;
|
||||
this.selected = new TreeSet<>((m1, m2) -> {
|
||||
if (m1.equals(m2)) return 0;
|
||||
else return Long.compare(m2.getDate(), m1.getDate());
|
||||
});
|
||||
|
||||
setHasStableIds(true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull ItemViewHolder onCreateViewHolder(@NonNull ViewGroup viewGroup, int i) {
|
||||
return new ItemViewHolder(LayoutInflater.from(viewGroup.getContext()).inflate(R.layout.mediapicker_media_item, viewGroup, false));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBindViewHolder(@NonNull ItemViewHolder holder, int i) {
|
||||
holder.bind(media.get(i), forcedMultiSelect, selected, maxSelection, glideRequests, eventListener);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onViewRecycled(@NonNull ItemViewHolder holder) {
|
||||
holder.recycle();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getItemCount() {
|
||||
return media.size();
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getItemId(int position) {
|
||||
return media.get(position).getDate();
|
||||
}
|
||||
|
||||
void setMedia(@NonNull List<Media> media) {
|
||||
this.media.clear();
|
||||
this.media.addAll(media);
|
||||
notifyDataSetChanged();
|
||||
}
|
||||
|
||||
void setSelected(@NonNull Collection<Media> selected) {
|
||||
this.selected.clear();
|
||||
this.selected.addAll(selected);
|
||||
notifyDataSetChanged();
|
||||
}
|
||||
|
||||
Set<Media> getSelected() {
|
||||
return selected;
|
||||
}
|
||||
|
||||
void setForcedMultiSelect(boolean forcedMultiSelect) {
|
||||
this.forcedMultiSelect = forcedMultiSelect;
|
||||
notifyDataSetChanged();
|
||||
}
|
||||
|
||||
static class ItemViewHolder extends RecyclerView.ViewHolder {
|
||||
|
||||
private final ImageView thumbnail;
|
||||
private final View playOverlay;
|
||||
private final View selectedOverlay;
|
||||
|
||||
ItemViewHolder(@NonNull View itemView) {
|
||||
super(itemView);
|
||||
thumbnail = itemView.findViewById(R.id.mediapicker_image_item_thumbnail);
|
||||
playOverlay = itemView.findViewById(R.id.mediapicker_play_overlay);
|
||||
selectedOverlay = itemView.findViewById(R.id.mediapicker_selected);
|
||||
}
|
||||
|
||||
void bind(@NonNull Media media, boolean multiSelect, Set<Media> selected, int maxSelection, @NonNull GlideRequests glideRequests, @NonNull EventListener eventListener) {
|
||||
glideRequests.load(media.getUri())
|
||||
.diskCacheStrategy(DiskCacheStrategy.NONE)
|
||||
.transition(DrawableTransitionOptions.withCrossFade())
|
||||
.into(thumbnail);
|
||||
|
||||
playOverlay.setVisibility(MediaUtil.isVideoType(media.getMimeType()) ? View.VISIBLE : View.GONE);
|
||||
selectedOverlay.setVisibility(selected.contains(media) ? View.VISIBLE : View.GONE);
|
||||
|
||||
if (selected.isEmpty() && !multiSelect) {
|
||||
itemView.setOnClickListener(v -> eventListener.onMediaChosen(media));
|
||||
if (maxSelection > 1) {
|
||||
itemView.setOnLongClickListener(v -> {
|
||||
selected.add(media);
|
||||
eventListener.onMediaSelectionChanged(new ArrayList<>(selected));
|
||||
return true;
|
||||
});
|
||||
}
|
||||
} else if (selected.contains(media)) {
|
||||
itemView.setOnClickListener(v -> {
|
||||
selected.remove(media);
|
||||
eventListener.onMediaSelectionChanged(new ArrayList<>(selected));
|
||||
});
|
||||
} else {
|
||||
itemView.setOnClickListener(v -> {
|
||||
if (selected.size() < maxSelection) {
|
||||
selected.add(media);
|
||||
eventListener.onMediaSelectionChanged(new ArrayList<>(selected));
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
void recycle() {
|
||||
itemView.setOnClickListener(null);
|
||||
}
|
||||
}
|
||||
|
||||
interface EventListener {
|
||||
void onMediaChosen(@NonNull Media media);
|
||||
void onMediaSelectionChanged(@NonNull List<Media> media);
|
||||
}
|
||||
}
|
@ -0,0 +1,241 @@
|
||||
package org.thoughtcrime.securesms.mediasend;
|
||||
|
||||
import android.arch.lifecycle.ViewModelProviders;
|
||||
import android.content.Context;
|
||||
import android.content.res.Configuration;
|
||||
import android.graphics.Point;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.support.v4.app.Fragment;
|
||||
import android.support.v7.app.AppCompatActivity;
|
||||
import android.support.v7.view.ActionMode;
|
||||
import android.support.v7.widget.GridLayoutManager;
|
||||
import android.support.v7.widget.RecyclerView;
|
||||
import android.support.v7.widget.Toolbar;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuInflater;
|
||||
import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.view.Window;
|
||||
import android.view.WindowManager;
|
||||
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.mms.GlideApp;
|
||||
import org.thoughtcrime.securesms.util.ThemeUtil;
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Allows the user to select a set of media items from a specified folder.
|
||||
*/
|
||||
public class MediaPickerItemFragment extends Fragment implements MediaPickerItemAdapter.EventListener {
|
||||
|
||||
private static final String KEY_BUCKET_ID = "bucket_id";
|
||||
private static final String KEY_FOLDER_TITLE = "folder_title";
|
||||
private static final String KEY_MAX_SELECTION = "max_selection";
|
||||
|
||||
private String bucketId;
|
||||
private String folderTitle;
|
||||
private int maxSelection;
|
||||
private MediaSendViewModel viewModel;
|
||||
private MediaPickerItemAdapter adapter;
|
||||
private Controller controller;
|
||||
private GridLayoutManager layoutManager;
|
||||
private ActionMode actionMode;
|
||||
private ActionMode.Callback actionModeCallback;
|
||||
|
||||
public static MediaPickerItemFragment newInstance(@NonNull String bucketId, @NonNull String folderTitle, int maxSelection) {
|
||||
Bundle args = new Bundle();
|
||||
args.putString(KEY_BUCKET_ID, bucketId);
|
||||
args.putString(KEY_FOLDER_TITLE, folderTitle);
|
||||
args.putInt(KEY_MAX_SELECTION, maxSelection);
|
||||
|
||||
MediaPickerItemFragment fragment = new MediaPickerItemFragment();
|
||||
fragment.setArguments(args);
|
||||
|
||||
return fragment;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreate(@Nullable Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
setHasOptionsMenu(true);
|
||||
|
||||
bucketId = getArguments().getString(KEY_BUCKET_ID);
|
||||
folderTitle = getArguments().getString(KEY_FOLDER_TITLE);
|
||||
maxSelection = getArguments().getInt(KEY_MAX_SELECTION);
|
||||
viewModel = ViewModelProviders.of(requireActivity(), new MediaSendViewModel.Factory(new MediaRepository())).get(MediaSendViewModel.class);
|
||||
actionModeCallback = new ActionModeCallback();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAttach(Context context) {
|
||||
super.onAttach(context);
|
||||
|
||||
if (!(getActivity() instanceof Controller)) {
|
||||
throw new IllegalStateException("Parent activity must implement controller class.");
|
||||
}
|
||||
|
||||
controller = (Controller) getActivity();
|
||||
}
|
||||
|
||||
@Override
|
||||
public @Nullable View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
|
||||
return inflater.inflate(R.layout.mediapicker_item_fragment, container, false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
|
||||
super.onViewCreated(view, savedInstanceState);
|
||||
|
||||
RecyclerView imageList = view.findViewById(R.id.mediapicker_item_list);
|
||||
|
||||
adapter = new MediaPickerItemAdapter(GlideApp.with(this), this, maxSelection);
|
||||
layoutManager = new GridLayoutManager(requireContext(), 4);
|
||||
|
||||
imageList.setLayoutManager(layoutManager);
|
||||
imageList.setAdapter(adapter);
|
||||
|
||||
initToolbar(view.findViewById(R.id.mediapicker_toolbar));
|
||||
onScreenWidthChanged(getScreenWidth());
|
||||
|
||||
if (!Util.isEmpty(viewModel.getSelectedMedia().getValue())) {
|
||||
adapter.setSelected(viewModel.getSelectedMedia().getValue());
|
||||
onMediaSelectionChanged(new ArrayList<>(viewModel.getSelectedMedia().getValue()));
|
||||
}
|
||||
|
||||
viewModel.getMediaInBucket(requireContext(), bucketId).observe(this, adapter::setMedia);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onResume() {
|
||||
super.onResume();
|
||||
|
||||
requireActivity().getWindow().addFlags(WindowManager.LayoutParams.FLAG_FORCE_NOT_FULLSCREEN);
|
||||
requireActivity().getWindow().clearFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
|
||||
inflater.inflate(R.menu.mediapicker_default, menu);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onOptionsItemSelected(MenuItem item) {
|
||||
if (item.getItemId() == R.id.mediapicker_menu_add) {
|
||||
adapter.setForcedMultiSelect(true);
|
||||
actionMode = ((AppCompatActivity) requireActivity()).startSupportActionMode(actionModeCallback);
|
||||
actionMode.setTitle(getResources().getString(R.string.MediaPickerItemFragment_tap_to_select));
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onConfigurationChanged(Configuration newConfig) {
|
||||
super.onConfigurationChanged(newConfig);
|
||||
onScreenWidthChanged(getScreenWidth());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onMediaChosen(@NonNull Media media) {
|
||||
controller.onMediaSelected(bucketId, Collections.singleton(media));
|
||||
viewModel.onSelectedMediaChanged(Collections.singletonList(media));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onMediaSelectionChanged(@NonNull List<Media> selected) {
|
||||
adapter.notifyDataSetChanged();
|
||||
|
||||
if (actionMode == null && !selected.isEmpty()) {
|
||||
actionMode = ((AppCompatActivity) requireActivity()).startSupportActionMode(actionModeCallback);
|
||||
actionMode.setTitle(String.valueOf(selected.size()));
|
||||
} else if (actionMode != null && selected.isEmpty()) {
|
||||
actionMode.finish();
|
||||
} else if (actionMode != null) {
|
||||
actionMode.setTitle(String.valueOf(selected.size()));
|
||||
}
|
||||
|
||||
viewModel.onSelectedMediaChanged(selected);
|
||||
}
|
||||
|
||||
private void initToolbar(Toolbar toolbar) {
|
||||
((AppCompatActivity) requireActivity()).setSupportActionBar(toolbar);
|
||||
((AppCompatActivity) requireActivity()).getSupportActionBar().setDisplayHomeAsUpEnabled(true);
|
||||
((AppCompatActivity) requireActivity()).getSupportActionBar().setTitle(folderTitle);
|
||||
|
||||
toolbar.setNavigationOnClickListener(v -> requireActivity().onBackPressed());
|
||||
}
|
||||
|
||||
private void onScreenWidthChanged(int newWidth) {
|
||||
if (layoutManager != null) {
|
||||
layoutManager.setSpanCount(newWidth / getResources().getDimensionPixelSize(R.dimen.media_picker_item_width));
|
||||
}
|
||||
}
|
||||
|
||||
private int getScreenWidth() {
|
||||
Point size = new Point();
|
||||
requireActivity().getWindowManager().getDefaultDisplay().getSize(size);
|
||||
return size.x;
|
||||
}
|
||||
|
||||
private class ActionModeCallback implements ActionMode.Callback {
|
||||
|
||||
private int statusBarColor;
|
||||
|
||||
@Override
|
||||
public boolean onCreateActionMode(ActionMode mode, Menu menu) {
|
||||
MenuInflater inflater = mode.getMenuInflater();
|
||||
inflater.inflate(R.menu.mediapicker_multiselect, menu);
|
||||
|
||||
if (Build.VERSION.SDK_INT >= 21) {
|
||||
Window window = requireActivity().getWindow();
|
||||
statusBarColor = window.getStatusBarColor();
|
||||
window.setStatusBarColor(getResources().getColor(R.color.action_mode_status_bar));
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onPrepareActionMode(ActionMode mode, Menu menu) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onActionItemClicked(ActionMode mode, MenuItem menuItem) {
|
||||
if (menuItem.getItemId() == R.id.mediapicker_menu_confirm) {
|
||||
List<Media> selected = new ArrayList<>(adapter.getSelected());
|
||||
actionMode.finish();
|
||||
viewModel.onSelectedMediaChanged(selected);
|
||||
controller.onMediaSelected(bucketId, selected);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroyActionMode(ActionMode mode) {
|
||||
actionMode = null;
|
||||
adapter.setSelected(Collections.emptySet());
|
||||
viewModel.onSelectedMediaChanged(Collections.emptyList());
|
||||
|
||||
if (Build.VERSION.SDK_INT >= 21) {
|
||||
requireActivity().getWindow().setStatusBarColor(statusBarColor);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public interface Controller {
|
||||
void onMediaSelected(@NonNull String bucketId, @NonNull Collection<Media> media);
|
||||
}
|
||||
}
|
190
src/org/thoughtcrime/securesms/mediasend/MediaRepository.java
Normal file
@ -0,0 +1,190 @@
|
||||
package org.thoughtcrime.securesms.mediasend;
|
||||
|
||||
import android.content.Context;
|
||||
import android.database.Cursor;
|
||||
import android.net.Uri;
|
||||
import android.os.AsyncTask;
|
||||
import android.os.Build;
|
||||
import android.os.Environment;
|
||||
import android.provider.MediaStore.Images;
|
||||
import android.provider.MediaStore.Video;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.annotation.WorkerThread;
|
||||
|
||||
import com.annimon.stream.Stream;
|
||||
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
import org.whispersystems.libsignal.util.Pair;
|
||||
import org.whispersystems.libsignal.util.guava.Optional;
|
||||
|
||||
import java.io.File;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Handles the retrieval of media present on the user's device.
|
||||
*/
|
||||
class MediaRepository {
|
||||
|
||||
/**
|
||||
* Retrieves a list of folders that contain media.
|
||||
*/
|
||||
void getFolders(@NonNull Context context, @NonNull Callback<List<MediaFolder>> callback) {
|
||||
AsyncTask.THREAD_POOL_EXECUTOR.execute(() -> callback.onComplete(getFolders(context)));
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves a list of media items (images and videos) that are present int he specified bucket.
|
||||
*/
|
||||
void getMediaInBucket(@NonNull Context context, @NonNull String bucketId, @NonNull Callback<List<Media>> callback) {
|
||||
AsyncTask.THREAD_POOL_EXECUTOR.execute(() -> callback.onComplete(getMediaInBucket(context, bucketId)));
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
private @NonNull List<MediaFolder> getFolders(@NonNull Context context) {
|
||||
Pair<String, Map<String, FolderData>> imageFolders = getFolders(context, Images.Media.EXTERNAL_CONTENT_URI);
|
||||
Pair<String, Map<String, FolderData>> videoFolders = getFolders(context, Video.Media.EXTERNAL_CONTENT_URI);
|
||||
Map<String, FolderData> folders = new HashMap<>(imageFolders.second());
|
||||
|
||||
for (Map.Entry<String, FolderData> entry : videoFolders.second().entrySet()) {
|
||||
if (folders.containsKey(entry.getKey())) {
|
||||
folders.get(entry.getKey()).incrementCount(entry.getValue().getCount());
|
||||
} else {
|
||||
folders.put(entry.getKey(), entry.getValue());
|
||||
}
|
||||
}
|
||||
|
||||
String cameraBucketId = imageFolders.first() != null ? imageFolders.first() : videoFolders.first();
|
||||
FolderData cameraFolder = cameraBucketId != null ? folders.remove(cameraBucketId) : null;
|
||||
List<MediaFolder> mediaFolders = Stream.of(folders.values()).map(folder -> new MediaFolder(folder.getThumbnail(),
|
||||
folder.getTitle(),
|
||||
folder.getCount(),
|
||||
folder.getBucketId(),
|
||||
MediaFolder.FolderType.NORMAL))
|
||||
.sorted((o1, o2) -> o1.getTitle().toLowerCase().compareTo(o2.getTitle().toLowerCase()))
|
||||
.toList();
|
||||
|
||||
if (cameraFolder != null) {
|
||||
mediaFolders.add(0, new MediaFolder(cameraFolder.getThumbnail(), cameraFolder.getTitle(), cameraFolder.getCount(), cameraFolder.getBucketId(), MediaFolder.FolderType.CAMERA));
|
||||
}
|
||||
|
||||
return mediaFolders;
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
private @NonNull Pair<String, Map<String, FolderData>> getFolders(@NonNull Context context, @NonNull Uri contentUri) {
|
||||
String cameraPath = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DCIM).getAbsolutePath() + File.separator + "Camera";
|
||||
String cameraBucketId = null;
|
||||
Map<String, FolderData> folders = new HashMap<>();
|
||||
|
||||
String[] projection = new String[] { Images.Media.DATA, Images.Media.BUCKET_ID, Images.Media.BUCKET_DISPLAY_NAME };
|
||||
String selection = Images.Media.DATA + " NOT NULL";
|
||||
String sortBy = Images.Media.BUCKET_DISPLAY_NAME + " COLLATE NOCASE ASC, " + Images.Media.DATE_TAKEN + " DESC";
|
||||
|
||||
try (Cursor cursor = context.getContentResolver().query(contentUri, projection, selection, null, sortBy)) {
|
||||
while (cursor != null && cursor.moveToNext()) {
|
||||
String path = cursor.getString(cursor.getColumnIndexOrThrow(projection[0]));
|
||||
Uri thumbnail = Uri.fromFile(new File(path));
|
||||
String bucketId = cursor.getString(cursor.getColumnIndexOrThrow(projection[1]));
|
||||
String title = cursor.getString(cursor.getColumnIndexOrThrow(projection[2]));
|
||||
FolderData folder = Util.getOrDefault(folders, bucketId, new FolderData(thumbnail, title, bucketId));
|
||||
|
||||
folder.incrementCount();
|
||||
folders.put(bucketId, folder);
|
||||
|
||||
if (cameraBucketId == null && path.startsWith(cameraPath)) {
|
||||
cameraBucketId = bucketId;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return new Pair<>(cameraBucketId, folders);
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
private @NonNull List<Media> getMediaInBucket(@NonNull Context context, @NonNull String bucketId) {
|
||||
List<Media> images = getMediaInBucket(context, bucketId, Images.Media.EXTERNAL_CONTENT_URI);
|
||||
List<Media> videos = getMediaInBucket(context, bucketId, Video.Media.EXTERNAL_CONTENT_URI);
|
||||
List<Media> media = new ArrayList<>(images.size() + videos.size());
|
||||
|
||||
media.addAll(images);
|
||||
media.addAll(videos);
|
||||
Collections.sort(media, (o1, o2) -> Long.compare(o2.getDate(), o1.getDate()));
|
||||
|
||||
return media;
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
private @NonNull List<Media> getMediaInBucket(@NonNull Context context, @NonNull String bucketId, @NonNull Uri contentUri) {
|
||||
List<Media> media = new LinkedList<>();
|
||||
String selection = Images.Media.BUCKET_ID + " = ? AND " + Images.Media.DATA + " NOT NULL";
|
||||
String sortBy = Images.Media.DATE_TAKEN + " DESC";
|
||||
String[] projection = Build.VERSION.SDK_INT >= 16 ? new String[] { Images.Media._ID, Images.Media.MIME_TYPE, Images.Media.DATE_TAKEN, Images.Media.WIDTH, Images.Media.HEIGHT }
|
||||
: new String[] { Images.Media._ID, Images.Media.MIME_TYPE, Images.Media.DATE_TAKEN };
|
||||
|
||||
try (Cursor cursor = context.getContentResolver().query(contentUri, projection, selection, new String[] { bucketId }, sortBy)) {
|
||||
while (cursor != null && cursor.moveToNext()) {
|
||||
Uri uri = Uri.withAppendedPath(contentUri, cursor.getString(cursor.getColumnIndexOrThrow(projection[0])));
|
||||
String mimetype = cursor.getString(cursor.getColumnIndexOrThrow(projection[1]));
|
||||
long dateTaken = cursor.getLong(cursor.getColumnIndexOrThrow(projection[2]));
|
||||
int width = 0;
|
||||
int height = 0;
|
||||
|
||||
if (Build.VERSION.SDK_INT >= 16) {
|
||||
width = cursor.getInt(cursor.getColumnIndexOrThrow(projection[3]));
|
||||
height = cursor.getInt(cursor.getColumnIndexOrThrow(projection[4]));
|
||||
}
|
||||
|
||||
media.add(new Media(uri, mimetype, dateTaken, width, height, Optional.of(bucketId), Optional.absent()));
|
||||
}
|
||||
}
|
||||
|
||||
return media;
|
||||
}
|
||||
|
||||
private static class FolderData {
|
||||
private final Uri thumbnail;
|
||||
private final String title;
|
||||
private final String bucketId;
|
||||
|
||||
private int count;
|
||||
|
||||
private FolderData(Uri thumbnail, String title, String bucketId) {
|
||||
this.thumbnail = thumbnail;
|
||||
this.title = title;
|
||||
this.bucketId = bucketId;
|
||||
}
|
||||
|
||||
Uri getThumbnail() {
|
||||
return thumbnail;
|
||||
}
|
||||
|
||||
String getTitle() {
|
||||
return title;
|
||||
}
|
||||
|
||||
String getBucketId() {
|
||||
return bucketId;
|
||||
}
|
||||
|
||||
int getCount() {
|
||||
return count;
|
||||
}
|
||||
|
||||
void incrementCount() {
|
||||
incrementCount(1);
|
||||
}
|
||||
|
||||
void incrementCount(int amount) {
|
||||
count += amount;
|
||||
}
|
||||
}
|
||||
|
||||
interface Callback<E> {
|
||||
void onComplete(@NonNull E result);
|
||||
}
|
||||
}
|
228
src/org/thoughtcrime/securesms/mediasend/MediaSendActivity.java
Normal file
@ -0,0 +1,228 @@
|
||||
package org.thoughtcrime.securesms.mediasend;
|
||||
|
||||
import android.arch.lifecycle.ViewModelProviders;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
import android.support.annotation.NonNull;
|
||||
|
||||
import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity;
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.TransportOption;
|
||||
import org.thoughtcrime.securesms.database.Address;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.scribbles.ScribbleFragment;
|
||||
import org.thoughtcrime.securesms.util.DynamicLanguage;
|
||||
import org.thoughtcrime.securesms.util.DynamicNoActionBarTheme;
|
||||
import org.thoughtcrime.securesms.util.DynamicTheme;
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
import org.whispersystems.libsignal.util.guava.Optional;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Encompasses the entire flow of sending media, starting from the selection process to the actual
|
||||
* captioning and editing of the content.
|
||||
*
|
||||
* This activity is intended to be launched via {@link #startActivityForResult(Intent, int)}.
|
||||
* It will return the {@link Media} that the user decided to send.
|
||||
*/
|
||||
public class MediaSendActivity extends PassphraseRequiredActionBarActivity implements MediaPickerFolderFragment.Controller,
|
||||
MediaPickerItemFragment.Controller,
|
||||
MediaSendFragment.Controller,
|
||||
ScribbleFragment.Controller
|
||||
{
|
||||
public static final String EXTRA_MEDIA = "media";
|
||||
public static final String EXTRA_MESSAGE = "message";
|
||||
public static final String EXTRA_TRANSPORT = "transport";
|
||||
|
||||
private static final int MAX_PUSH = 32;
|
||||
private static final int MAX_SMS = 1;
|
||||
|
||||
private static final String KEY_ADDRESS = "address";
|
||||
private static final String KEY_BODY = "body";
|
||||
private static final String KEY_MEDIA = "media";
|
||||
private static final String KEY_TRANSPORT = "transport";
|
||||
|
||||
private static final String TAG_FOLDER_PICKER = "folder_picker";
|
||||
private static final String TAG_ITEM_PICKER = "item_picker";
|
||||
private static final String TAG_SEND = "send";
|
||||
|
||||
private final DynamicTheme dynamicTheme = new DynamicNoActionBarTheme();
|
||||
private final DynamicLanguage dynamicLanguage = new DynamicLanguage();
|
||||
|
||||
private Recipient recipient;
|
||||
private TransportOption transport;
|
||||
private MediaSendViewModel viewModel;
|
||||
|
||||
/**
|
||||
* Get an intent to launch the media send flow starting with the picker.
|
||||
*/
|
||||
public static Intent getIntent(@NonNull Context context, @NonNull Recipient recipient, @NonNull TransportOption transport) {
|
||||
Intent intent = new Intent(context, MediaSendActivity.class);
|
||||
intent.putExtra(KEY_ADDRESS, recipient.getAddress().serialize());
|
||||
intent.putExtra(KEY_TRANSPORT, transport);
|
||||
return intent;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get an intent to launch the media send flow with a specific list of media. Will jump right to
|
||||
* the editor screen.
|
||||
*/
|
||||
public static Intent getIntent(@NonNull Context context,
|
||||
@NonNull List<Media> media,
|
||||
@NonNull Recipient recipient,
|
||||
@NonNull String body,
|
||||
@NonNull TransportOption transport)
|
||||
{
|
||||
Intent intent = getIntent(context, recipient, transport);
|
||||
intent.putParcelableArrayListExtra(KEY_MEDIA, new ArrayList<>(media));
|
||||
intent.putExtra(KEY_BODY, body);
|
||||
return intent;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPreCreate() {
|
||||
dynamicTheme.onCreate(this);
|
||||
dynamicLanguage.onCreate(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState, boolean ready) {
|
||||
setContentView(R.layout.mediapicker_activity);
|
||||
setResult(RESULT_CANCELED);
|
||||
|
||||
if (savedInstanceState != null) {
|
||||
return;
|
||||
}
|
||||
|
||||
viewModel = ViewModelProviders.of(this, new MediaSendViewModel.Factory(new MediaRepository())).get(MediaSendViewModel.class);
|
||||
recipient = Recipient.from(this, Address.fromSerialized(getIntent().getStringExtra(KEY_ADDRESS)), true);
|
||||
transport = getIntent().getParcelableExtra(KEY_TRANSPORT);
|
||||
|
||||
List<Media> media = getIntent().getParcelableArrayListExtra(KEY_MEDIA);
|
||||
String body = getIntent().getStringExtra(KEY_BODY);
|
||||
|
||||
if (!Util.isEmpty(media)) {
|
||||
navigateToMediaSend(media, body, transport);
|
||||
} else {
|
||||
navigateToFolderPicker(recipient);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onResume() {
|
||||
super.onResume();
|
||||
dynamicTheme.onResume(this);
|
||||
dynamicLanguage.onResume(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBackPressed() {
|
||||
MediaSendFragment sendFragment = (MediaSendFragment) getSupportFragmentManager().findFragmentByTag(TAG_SEND);
|
||||
if (sendFragment == null || !sendFragment.handleBackPress()) {
|
||||
super.onBackPressed();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFolderSelected(@NonNull MediaFolder folder) {
|
||||
viewModel.onFolderSelected(folder.getBucketId());
|
||||
|
||||
MediaPickerItemFragment fragment = MediaPickerItemFragment.newInstance(folder.getBucketId(),
|
||||
folder.getTitle(),
|
||||
transport.isSms() ? MAX_SMS : MAX_PUSH);
|
||||
|
||||
getSupportFragmentManager().beginTransaction()
|
||||
.setCustomAnimations(R.anim.fade_in, R.anim.fade_out, R.anim.fade_in, R.anim.fade_out)
|
||||
.replace(R.id.mediapicker_fragment_container, fragment, TAG_ITEM_PICKER)
|
||||
.addToBackStack(null)
|
||||
.commit();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onMediaSelected(@NonNull String bucketId, @NonNull Collection<Media> media) {
|
||||
MediaSendFragment fragment = MediaSendFragment.newInstance("", transport, dynamicLanguage.getCurrentLocale());
|
||||
getSupportFragmentManager().beginTransaction()
|
||||
.setCustomAnimations(R.anim.fade_in, R.anim.fade_out, R.anim.fade_in, R.anim.fade_out)
|
||||
.replace(R.id.mediapicker_fragment_container, fragment, TAG_SEND)
|
||||
.addToBackStack(null)
|
||||
.commit();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAddMediaClicked(@NonNull String bucketId) {
|
||||
MediaPickerFolderFragment folderFragment = MediaPickerFolderFragment.newInstance(recipient);
|
||||
MediaPickerItemFragment itemFragment = MediaPickerItemFragment.newInstance(bucketId,
|
||||
"",
|
||||
transport.isSms() ? MAX_SMS : MAX_PUSH);
|
||||
|
||||
getSupportFragmentManager().beginTransaction()
|
||||
.replace(R.id.mediapicker_fragment_container, folderFragment, TAG_FOLDER_PICKER)
|
||||
.addToBackStack(null)
|
||||
.commit();
|
||||
|
||||
getSupportFragmentManager().beginTransaction()
|
||||
.replace(R.id.mediapicker_fragment_container, itemFragment, TAG_ITEM_PICKER)
|
||||
.addToBackStack(null)
|
||||
.commit();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSendClicked(@NonNull List<Media> media, @NonNull String message, @NonNull TransportOption transport) {
|
||||
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
|
||||
public void onImageEditComplete(@NonNull Uri uri, int width, int height, long size, @NonNull Optional<String> message, @NonNull Optional<TransportOption> transport) {
|
||||
throw new UnsupportedOperationException("Callback unsupported.");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onImageEditFailure() {
|
||||
throw new UnsupportedOperationException("Callback unsupported.");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onTouchEventsNeeded(boolean needed) {
|
||||
MediaSendFragment fragment = (MediaSendFragment) getSupportFragmentManager().findFragmentByTag(TAG_SEND);
|
||||
if (fragment != null) {
|
||||
fragment.onTouchEventsNeeded(needed);
|
||||
}
|
||||
}
|
||||
|
||||
private void navigateToMediaSend(List<Media> media, String body, TransportOption transport) {
|
||||
viewModel.setInitialSelectedMedia(media);
|
||||
|
||||
MediaSendFragment sendFragment = MediaSendFragment.newInstance(body, transport, dynamicLanguage.getCurrentLocale());
|
||||
getSupportFragmentManager().beginTransaction()
|
||||
.replace(R.id.mediapicker_fragment_container, sendFragment, TAG_SEND)
|
||||
.commit();
|
||||
}
|
||||
|
||||
private void navigateToFolderPicker(@NonNull Recipient recipient) {
|
||||
MediaPickerFolderFragment folderFragment = MediaPickerFolderFragment.newInstance(recipient);
|
||||
|
||||
getSupportFragmentManager().beginTransaction()
|
||||
.replace(R.id.mediapicker_fragment_container, folderFragment, TAG_FOLDER_PICKER)
|
||||
.commit();
|
||||
}
|
||||
}
|
506
src/org/thoughtcrime/securesms/mediasend/MediaSendFragment.java
Normal file
@ -0,0 +1,506 @@
|
||||
package org.thoughtcrime.securesms.mediasend;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.app.ProgressDialog;
|
||||
import android.arch.lifecycle.ViewModelProviders;
|
||||
import android.content.Context;
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.PorterDuff;
|
||||
import android.graphics.Rect;
|
||||
import android.net.Uri;
|
||||
import android.os.AsyncTask;
|
||||
import android.os.Bundle;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.support.v4.app.Fragment;
|
||||
import android.support.v4.view.ViewPager;
|
||||
import android.support.v7.app.AlertDialog;
|
||||
import android.support.v7.view.ContextThemeWrapper;
|
||||
import android.support.v7.widget.LinearLayoutManager;
|
||||
import android.support.v7.widget.RecyclerView;
|
||||
import android.text.Editable;
|
||||
import android.text.TextWatcher;
|
||||
import android.view.KeyEvent;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.view.ViewTreeObserver;
|
||||
import android.view.WindowManager;
|
||||
import android.widget.TextView;
|
||||
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.TransportOption;
|
||||
import org.thoughtcrime.securesms.components.ComposeText;
|
||||
import org.thoughtcrime.securesms.components.ControllableViewPager;
|
||||
import org.thoughtcrime.securesms.components.InputAwareLayout;
|
||||
import org.thoughtcrime.securesms.components.SendButton;
|
||||
import org.thoughtcrime.securesms.components.emoji.EmojiDrawer;
|
||||
import org.thoughtcrime.securesms.components.emoji.EmojiEditText;
|
||||
import org.thoughtcrime.securesms.components.emoji.EmojiToggle;
|
||||
import org.thoughtcrime.securesms.contactshare.SimpleTextWatcher;
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
import org.thoughtcrime.securesms.mediapreview.MediaRailAdapter;
|
||||
import org.thoughtcrime.securesms.mms.GlideApp;
|
||||
import org.thoughtcrime.securesms.providers.PersistentBlobProvider;
|
||||
import org.thoughtcrime.securesms.scribbles.widget.ScribbleView;
|
||||
import org.thoughtcrime.securesms.util.CharacterCalculator.CharacterState;
|
||||
import org.thoughtcrime.securesms.util.MediaUtil;
|
||||
import org.thoughtcrime.securesms.util.Stopwatch;
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||
import org.thoughtcrime.securesms.util.ThemeUtil;
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
import org.thoughtcrime.securesms.util.concurrent.ListenableFuture;
|
||||
import org.thoughtcrime.securesms.util.views.Stub;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ExecutionException;
|
||||
|
||||
/**
|
||||
* Allows the user to edit and caption a set of media items before choosing to send them.
|
||||
*/
|
||||
public class MediaSendFragment extends Fragment implements ViewTreeObserver.OnGlobalLayoutListener,
|
||||
MediaRailAdapter.RailItemListener,
|
||||
InputAwareLayout.OnKeyboardShownListener,
|
||||
InputAwareLayout.OnKeyboardHiddenListener
|
||||
{
|
||||
|
||||
private static final String TAG = MediaSendFragment.class.getSimpleName();
|
||||
|
||||
private static final String KEY_BODY = "body";
|
||||
private static final String KEY_TRANSPORT = "transport";
|
||||
private static final String KEY_LOCALE = "locale";
|
||||
|
||||
private InputAwareLayout hud;
|
||||
private SendButton sendButton;
|
||||
private View addButton;
|
||||
private ComposeText composeText;
|
||||
private ViewGroup composeContainer;
|
||||
private EmojiEditText captionText;
|
||||
private EmojiToggle emojiToggle;
|
||||
private Stub<EmojiDrawer> emojiDrawer;
|
||||
private ViewGroup playbackControlsContainer;
|
||||
private TextView charactersLeft;
|
||||
|
||||
private ControllableViewPager fragmentPager;
|
||||
private MediaSendFragmentPagerAdapter fragmentPagerAdapter;
|
||||
private RecyclerView mediaRail;
|
||||
private MediaRailAdapter mediaRailAdapter;
|
||||
|
||||
private int visibleHeight;
|
||||
private MediaSendViewModel viewModel;
|
||||
private Controller controller;
|
||||
private Locale locale;
|
||||
|
||||
private final Rect visibleBounds = new Rect();
|
||||
|
||||
public static MediaSendFragment newInstance(@NonNull String body, @NonNull TransportOption transport, @NonNull Locale locale) {
|
||||
Bundle args = new Bundle();
|
||||
args.putString(KEY_BODY, body);
|
||||
args.putParcelable(KEY_TRANSPORT, transport);
|
||||
args.putSerializable(KEY_LOCALE, locale);
|
||||
|
||||
MediaSendFragment fragment = new MediaSendFragment();
|
||||
fragment.setArguments(args);
|
||||
return fragment;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAttach(Context context) {
|
||||
super.onAttach(context);
|
||||
|
||||
if (!(requireActivity() instanceof Controller)) {
|
||||
throw new IllegalStateException("Parent activity must implement controller interface.");
|
||||
}
|
||||
|
||||
controller = (Controller) requireActivity();
|
||||
}
|
||||
|
||||
@Override
|
||||
public @Nullable View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
|
||||
return ThemeUtil.getThemedInflater(requireActivity(), inflater, R.style.TextSecure_DarkTheme)
|
||||
.inflate(R.layout.mediasend_fragment, container, false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreate(@Nullable Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
|
||||
locale = (Locale) getArguments().getSerializable(KEY_LOCALE);
|
||||
|
||||
initViewModel();
|
||||
|
||||
requireActivity().getWindow().addFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN);
|
||||
requireActivity().getWindow().clearFlags(WindowManager.LayoutParams.FLAG_FORCE_NOT_FULLSCREEN);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
|
||||
hud = view.findViewById(R.id.mediasend_hud);
|
||||
sendButton = view.findViewById(R.id.mediasend_send_button);
|
||||
composeText = view.findViewById(R.id.mediasend_compose_text);
|
||||
composeContainer = view.findViewById(R.id.mediasend_compose_container);
|
||||
captionText = view.findViewById(R.id.mediasend_caption);
|
||||
emojiToggle = view.findViewById(R.id.mediasend_emoji_toggle);
|
||||
emojiDrawer = new Stub<>(view.findViewById(R.id.mediasend_emoji_drawer_stub));
|
||||
fragmentPager = view.findViewById(R.id.mediasend_pager);
|
||||
mediaRail = view.findViewById(R.id.mediasend_media_rail);
|
||||
addButton = view.findViewById(R.id.mediasend_add_button);
|
||||
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();
|
||||
|
||||
emojiToggle.setOnClickListener(this::onEmojiToggleClicked);
|
||||
|
||||
fragmentPagerAdapter = new MediaSendFragmentPagerAdapter(requireActivity().getSupportFragmentManager(), locale);
|
||||
fragmentPager.setAdapter(fragmentPagerAdapter);
|
||||
|
||||
FragmentPageChangeListener pageChangeListener = new FragmentPageChangeListener();
|
||||
fragmentPager.addOnPageChangeListener(pageChangeListener);
|
||||
fragmentPager.post(() -> pageChangeListener.onPageSelected(fragmentPager.getCurrentItem()));
|
||||
|
||||
mediaRailAdapter = new MediaRailAdapter(GlideApp.with(this), this, true);
|
||||
mediaRail.setLayoutManager(new LinearLayoutManager(requireContext(), LinearLayoutManager.HORIZONTAL, false));
|
||||
mediaRail.setAdapter(mediaRailAdapter);
|
||||
|
||||
hud.getRootView().getViewTreeObserver().addOnGlobalLayoutListener(this);
|
||||
hud.addOnKeyboardShownListener(this);
|
||||
hud.addOnKeyboardHiddenListener(this);
|
||||
|
||||
captionText.addTextChangedListener(new SimpleTextWatcher() {
|
||||
@Override
|
||||
public void onTextChanged(String text) {
|
||||
viewModel.onCaptionChanged(text);
|
||||
}
|
||||
});
|
||||
|
||||
TransportOption transportOption = getArguments().getParcelable(KEY_TRANSPORT);
|
||||
|
||||
sendButton.setTransport(transportOption);
|
||||
sendButton.disableTransport(transportOption.getType() == TransportOption.Type.SMS ? TransportOption.Type.TEXTSECURE : TransportOption.Type.SMS);
|
||||
|
||||
composeText.append(getArguments().getString(KEY_BODY));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStart() {
|
||||
super.onStart();
|
||||
fragmentPagerAdapter.restoreState(viewModel.getDrawState());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStop() {
|
||||
super.onStop();
|
||||
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(fragmentPager.getCurrentItem() + distanceFromActive);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onKeyboardShown() {
|
||||
if (composeText.hasFocus()) {
|
||||
composeContainer.setVisibility(View.VISIBLE);
|
||||
captionText.setVisibility(View.GONE);
|
||||
} else if (captionText.hasFocus()) {
|
||||
mediaRail.setVisibility(View.GONE);
|
||||
composeContainer.setVisibility(View.GONE);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onKeyboardHidden() {
|
||||
composeContainer.setVisibility(View.VISIBLE);
|
||||
|
||||
if (!Util.isEmpty(viewModel.getSelectedMedia().getValue()) && viewModel.getSelectedMedia().getValue().size() > 1) {
|
||||
mediaRail.setVisibility(View.VISIBLE);
|
||||
captionText.setVisibility(View.VISIBLE);
|
||||
}
|
||||
}
|
||||
|
||||
public void onTouchEventsNeeded(boolean needed) {
|
||||
fragmentPager.setEnabled(!needed);
|
||||
}
|
||||
|
||||
public boolean handleBackPress() {
|
||||
if (hud.isInputOpen()) {
|
||||
hud.hideCurrentInput(composeText);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private void initViewModel() {
|
||||
viewModel = ViewModelProviders.of(requireActivity(), new MediaSendViewModel.Factory(new MediaRepository())).get(MediaSendViewModel.class);
|
||||
|
||||
viewModel.getSelectedMedia().observe(this, media -> {
|
||||
if (Util.isEmpty(media)) {
|
||||
controller.onNoMediaAvailable();
|
||||
return;
|
||||
}
|
||||
|
||||
fragmentPagerAdapter.setMedia(media);
|
||||
|
||||
mediaRail.setVisibility(media.size() > 1 ? View.VISIBLE : View.GONE);
|
||||
captionText.setVisibility((media.size() > 1 || media.get(0).getCaption().isPresent()) ? View.VISIBLE : View.GONE);
|
||||
mediaRailAdapter.setMedia(media);
|
||||
});
|
||||
|
||||
viewModel.getPosition().observe(this, position -> {
|
||||
if (position == null || position < 0) return;
|
||||
|
||||
fragmentPager.setCurrentItem(position, true);
|
||||
mediaRailAdapter.setActivePosition(position);
|
||||
mediaRail.smoothScrollToPosition(position);
|
||||
|
||||
if (!fragmentPagerAdapter.getAllMedia().isEmpty()) {
|
||||
captionText.setText(fragmentPagerAdapter.getAllMedia().get(position).getCaption().or(""));
|
||||
}
|
||||
|
||||
View playbackControls = fragmentPagerAdapter.getPlaybackControls(position);
|
||||
|
||||
if (playbackControls != null) {
|
||||
ViewGroup.LayoutParams params = new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT);
|
||||
playbackControls.setLayoutParams(params);
|
||||
playbackControlsContainer.removeAllViews();
|
||||
playbackControlsContainer.addView(playbackControls);
|
||||
} else {
|
||||
playbackControlsContainer.removeAllViews();
|
||||
}
|
||||
});
|
||||
|
||||
viewModel.getBucketId().observe(this, bucketId -> {
|
||||
if (bucketId == null || !bucketId.isPresent() || sendButton.getSelectedTransport().isSms()) {
|
||||
addButton.setVisibility(View.GONE);
|
||||
} else {
|
||||
addButton.setVisibility(View.VISIBLE);
|
||||
addButton.setOnClickListener(v -> controller.onAddMediaClicked(bucketId.get()));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
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.maxMessageSize,
|
||||
characterState.messagesSpent));
|
||||
charactersLeft.setVisibility(View.VISIBLE);
|
||||
} else {
|
||||
charactersLeft.setVisibility(View.GONE);
|
||||
}
|
||||
}
|
||||
|
||||
private void onEmojiToggleClicked(View v) {
|
||||
if (!emojiDrawer.resolved()) {
|
||||
emojiToggle.attach(emojiDrawer.get());
|
||||
emojiDrawer.get().setEmojiEventListener(new EmojiDrawer.EmojiEventListener() {
|
||||
@Override
|
||||
public void onKeyEvent(KeyEvent keyEvent) {
|
||||
getActiveInputField().dispatchKeyEvent(keyEvent);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onEmojiSelected(String emoji) {
|
||||
getActiveInputField().insertEmoji(emoji);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
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, ListenableFuture<Bitmap>> futures = new HashMap<>();
|
||||
|
||||
for (Media media : mediaList) {
|
||||
Object state = savedState.get(media.getUri());
|
||||
|
||||
if (state instanceof ScribbleView.SavedState && !((ScribbleView.SavedState) state).isEmpty()) {
|
||||
futures.put(media, ScribbleView.renderImage(requireContext(), media.getUri(), (ScribbleView.SavedState) state, GlideApp.with(this)));
|
||||
}
|
||||
}
|
||||
|
||||
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());
|
||||
|
||||
for (Media media : mediaList) {
|
||||
if (futures.containsKey(media)) {
|
||||
try {
|
||||
Bitmap bitmap = futures.get(media).get();
|
||||
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
||||
bitmap.compress(Bitmap.CompressFormat.JPEG, 80, baos);
|
||||
|
||||
Uri uri = PersistentBlobProvider.getInstance(context).create(context, baos.toByteArray(), MediaUtil.IMAGE_JPEG, null);
|
||||
Media updated = new Media(uri, MediaUtil.IMAGE_JPEG, media.getDate(), bitmap.getWidth(), bitmap.getHeight(), media.getBucketId(), media.getCaption());
|
||||
|
||||
updatedMedia.add(updated);
|
||||
renderTimer.split("item");
|
||||
|
||||
} catch (InterruptedException | ExecutionException e) {
|
||||
Log.w(TAG, "Failed to render image. Using base image.");
|
||||
updatedMedia.add(media);
|
||||
}
|
||||
} 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();
|
||||
}
|
||||
|
||||
private class FragmentPageChangeListener extends ViewPager.SimpleOnPageChangeListener {
|
||||
@Override
|
||||
public void onPageSelected(int 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();
|
||||
}
|
||||
|
||||
@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();
|
||||
}
|
||||
}
|
@ -0,0 +1,117 @@
|
||||
package org.thoughtcrime.securesms.mediasend;
|
||||
|
||||
import android.net.Uri;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.support.v4.app.Fragment;
|
||||
import android.support.v4.app.FragmentManager;
|
||||
import android.support.v4.app.FragmentStatePagerAdapter;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
|
||||
import org.thoughtcrime.securesms.scribbles.ScribbleFragment;
|
||||
import org.thoughtcrime.securesms.util.MediaUtil;
|
||||
import org.whispersystems.libsignal.util.guava.Optional;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
|
||||
class MediaSendFragmentPagerAdapter extends FragmentStatePagerAdapter {
|
||||
|
||||
private final Locale locale;
|
||||
private final List<Media> media;
|
||||
private final Map<Integer, MediaSendPageFragment> fragments;
|
||||
private final Map<Uri, Object> savedState;
|
||||
|
||||
MediaSendFragmentPagerAdapter(@NonNull FragmentManager fm, @NonNull Locale locale) {
|
||||
super(fm);
|
||||
this.locale = locale;
|
||||
this.media = new ArrayList<>();
|
||||
this.fragments = new HashMap<>();
|
||||
this.savedState = new HashMap<>();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Fragment getItem(int i) {
|
||||
Media mediaItem = media.get(i);
|
||||
|
||||
if (MediaUtil.isGif(mediaItem.getMimeType())) {
|
||||
return MediaSendGifFragment.newInstance(mediaItem.getUri());
|
||||
} else if (MediaUtil.isImageType(mediaItem.getMimeType())) {
|
||||
return ScribbleFragment.newInstance(mediaItem.getUri(), locale, Optional.absent(), true);
|
||||
} else if (MediaUtil.isVideoType(mediaItem.getMimeType())) {
|
||||
return MediaSendVideoFragment.newInstance(mediaItem.getUri());
|
||||
} else {
|
||||
throw new UnsupportedOperationException("Can only render images and videos. Found mimetype: '" + mediaItem.getMimeType() + "'");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getItemPosition(@NonNull Object object) {
|
||||
return POSITION_NONE;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public Object instantiateItem(@NonNull ViewGroup container, int position) {
|
||||
MediaSendPageFragment fragment = (MediaSendPageFragment) super.instantiateItem(container, position);
|
||||
fragments.put(position, fragment);
|
||||
|
||||
Object state = savedState.get(fragment.getUri());
|
||||
if (state != null) {
|
||||
fragment.restoreState(state);
|
||||
}
|
||||
|
||||
return fragment;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void destroyItem(@NonNull ViewGroup container, int position, @NonNull Object object) {
|
||||
MediaSendPageFragment fragment = (MediaSendPageFragment) object;
|
||||
|
||||
Object state = fragment.saveState();
|
||||
if (state != null) {
|
||||
savedState.put(fragment.getUri(), state);
|
||||
}
|
||||
|
||||
super.destroyItem(container, position, object);
|
||||
fragments.remove(position);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getCount() {
|
||||
return media.size();
|
||||
}
|
||||
|
||||
List<Media> getAllMedia() {
|
||||
return media;
|
||||
}
|
||||
|
||||
void setMedia(@NonNull List<Media> media) {
|
||||
this.media.clear();
|
||||
this.media.addAll(media);
|
||||
notifyDataSetChanged();
|
||||
}
|
||||
|
||||
Map<Uri, Object> getSavedState() {
|
||||
for (MediaSendPageFragment fragment : fragments.values()) {
|
||||
Object state = fragment.saveState();
|
||||
if (state != null) {
|
||||
savedState.put(fragment.getUri(), state);
|
||||
}
|
||||
}
|
||||
return new HashMap<>(savedState);
|
||||
}
|
||||
|
||||
void restoreState(@NonNull Map<Uri, Object> state) {
|
||||
savedState.clear();
|
||||
savedState.putAll(state);
|
||||
}
|
||||
|
||||
@Nullable View getPlaybackControls(int position) {
|
||||
return fragments.containsKey(position) ? fragments.get(position).getPlaybackControls() : null;
|
||||
}
|
||||
}
|
@ -0,0 +1,68 @@
|
||||
package org.thoughtcrime.securesms.mediasend;
|
||||
|
||||
import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.support.v4.app.Fragment;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.ImageView;
|
||||
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.mms.DecryptableStreamUriLoader;
|
||||
import org.thoughtcrime.securesms.mms.GlideApp;
|
||||
|
||||
public class MediaSendGifFragment extends Fragment implements MediaSendPageFragment {
|
||||
|
||||
private static final String KEY_URI = "uri";
|
||||
|
||||
private Uri uri;
|
||||
|
||||
public static MediaSendGifFragment newInstance(@NonNull Uri uri) {
|
||||
Bundle args = new Bundle();
|
||||
args.putParcelable(KEY_URI, uri);
|
||||
|
||||
MediaSendGifFragment fragment = new MediaSendGifFragment();
|
||||
fragment.setArguments(args);
|
||||
fragment.setUri(uri);
|
||||
return fragment;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @Nullable View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
|
||||
return inflater.inflate(R.layout.mediasend_image_fragment, container, false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
|
||||
super.onViewCreated(view, savedInstanceState);
|
||||
|
||||
uri = getArguments().getParcelable(KEY_URI);
|
||||
GlideApp.with(this).load(new DecryptableStreamUriLoader.DecryptableUri(uri)).into((ImageView) view);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setUri(@NonNull Uri uri) {
|
||||
this.uri = uri;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull Uri getUri() {
|
||||
return uri;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @Nullable View getPlaybackControls() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @Nullable Object saveState() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void restoreState(@NonNull Object state) { }
|
||||
}
|
@ -0,0 +1,22 @@
|
||||
package org.thoughtcrime.securesms.mediasend;
|
||||
|
||||
import android.net.Uri;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.view.View;
|
||||
|
||||
/**
|
||||
* A page that sits in the {@link MediaSendFragmentPagerAdapter}.
|
||||
*/
|
||||
public interface MediaSendPageFragment {
|
||||
|
||||
@NonNull Uri getUri();
|
||||
|
||||
void setUri(@NonNull Uri uri);
|
||||
|
||||
@Nullable View getPlaybackControls();
|
||||
|
||||
@Nullable Object saveState();
|
||||
|
||||
void restoreState(@NonNull Object state);
|
||||
}
|
@ -0,0 +1,88 @@
|
||||
package org.thoughtcrime.securesms.mediasend;
|
||||
|
||||
import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.support.v4.app.Fragment;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
import org.thoughtcrime.securesms.mms.VideoSlide;
|
||||
import org.thoughtcrime.securesms.video.VideoPlayer;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
public class MediaSendVideoFragment extends Fragment implements MediaSendPageFragment {
|
||||
|
||||
private static final String TAG = MediaSendVideoFragment.class.getSimpleName();
|
||||
|
||||
private static final String KEY_URI = "uri";
|
||||
|
||||
private Uri uri;
|
||||
|
||||
public static MediaSendVideoFragment newInstance(@NonNull Uri uri) {
|
||||
Bundle args = new Bundle();
|
||||
args.putParcelable(KEY_URI, uri);
|
||||
|
||||
MediaSendVideoFragment fragment = new MediaSendVideoFragment();
|
||||
fragment.setArguments(args);
|
||||
fragment.setUri(uri);
|
||||
return fragment;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @Nullable View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
|
||||
return inflater.inflate(R.layout.mediasend_video_fragment, container, false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
|
||||
super.onViewCreated(view, savedInstanceState);
|
||||
|
||||
uri = getArguments().getParcelable(KEY_URI);
|
||||
VideoSlide slide = new VideoSlide(requireContext(), uri, 0);
|
||||
try {
|
||||
((VideoPlayer) view).setWindow(requireActivity().getWindow());
|
||||
((VideoPlayer) view).setVideoSource(slide, false);
|
||||
} catch (IOException e) {
|
||||
Log.w(TAG, "Failed to play video.", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroyView() {
|
||||
super.onDestroyView();
|
||||
|
||||
if (getView() != null) {
|
||||
((VideoPlayer) getView()).cleanup();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setUri(@NonNull Uri uri) {
|
||||
this.uri = uri;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull Uri getUri() {
|
||||
return uri;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @Nullable View getPlaybackControls() {
|
||||
VideoPlayer player = (VideoPlayer) getView();
|
||||
return player != null ? player.getControlView() : null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @Nullable Object saveState() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void restoreState(@NonNull Object state) { }
|
||||
}
|
136
src/org/thoughtcrime/securesms/mediasend/MediaSendViewModel.java
Normal file
@ -0,0 +1,136 @@
|
||||
package org.thoughtcrime.securesms.mediasend;
|
||||
|
||||
import android.arch.lifecycle.LiveData;
|
||||
import android.arch.lifecycle.MutableLiveData;
|
||||
import android.arch.lifecycle.ViewModel;
|
||||
import android.arch.lifecycle.ViewModelProvider;
|
||||
import android.content.Context;
|
||||
import android.net.Uri;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.text.TextUtils;
|
||||
|
||||
import com.annimon.stream.Stream;
|
||||
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
import org.whispersystems.libsignal.util.guava.Optional;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Manages the observable datasets available in {@link MediaSendActivity}.
|
||||
*/
|
||||
class MediaSendViewModel extends ViewModel {
|
||||
|
||||
private final MediaRepository repository;
|
||||
private final MutableLiveData<List<Media>> selectedMedia;
|
||||
private final MutableLiveData<List<Media>> bucketMedia;
|
||||
private final MutableLiveData<Integer> position;
|
||||
private final MutableLiveData<Optional<String>> bucketId;
|
||||
private final MutableLiveData<List<MediaFolder>> folders;
|
||||
private final Map<Uri, Object> savedDrawState;
|
||||
|
||||
private MediaSendViewModel(@NonNull MediaRepository repository) {
|
||||
this.repository = repository;
|
||||
this.selectedMedia = new MutableLiveData<>();
|
||||
this.bucketMedia = new MutableLiveData<>();
|
||||
this.position = new MutableLiveData<>();
|
||||
this.bucketId = new MutableLiveData<>();
|
||||
this.folders = new MutableLiveData<>();
|
||||
this.savedDrawState = new HashMap<>();
|
||||
|
||||
position.setValue(-1);
|
||||
}
|
||||
|
||||
void setInitialSelectedMedia(@NonNull List<Media> newMedia) {
|
||||
boolean allBucketsPopulated = Stream.of(newMedia).reduce(true, (populated, m) -> populated && m.getBucketId().isPresent());
|
||||
|
||||
selectedMedia.setValue(newMedia);
|
||||
bucketId.setValue(allBucketsPopulated ? computeBucketId(newMedia) : Optional.absent());
|
||||
}
|
||||
|
||||
void onSelectedMediaChanged(@NonNull List<Media> newMedia) {
|
||||
selectedMedia.setValue(newMedia);
|
||||
position.setValue(newMedia.isEmpty() ? -1 : 0);
|
||||
}
|
||||
|
||||
void onFolderSelected(@NonNull String bucketId) {
|
||||
this.bucketId.setValue(Optional.of(bucketId));
|
||||
bucketMedia.setValue(Collections.emptyList());
|
||||
}
|
||||
|
||||
void onPageChanged(int position) {
|
||||
this.position.setValue(position);
|
||||
}
|
||||
|
||||
void onMediaItemRemoved(int position) {
|
||||
selectedMedia.getValue().remove(position);
|
||||
selectedMedia.setValue(selectedMedia.getValue());
|
||||
}
|
||||
|
||||
void onCaptionChanged(@NonNull String newCaption) {
|
||||
if (position.getValue() >= 0 && !Util.isEmpty(selectedMedia.getValue())) {
|
||||
selectedMedia.getValue().get(position.getValue()).setCaption(TextUtils.isEmpty(newCaption) ? null : newCaption);
|
||||
}
|
||||
}
|
||||
|
||||
void saveDrawState(@NonNull Map<Uri, Object> state) {
|
||||
savedDrawState.clear();
|
||||
savedDrawState.putAll(state);
|
||||
}
|
||||
|
||||
@NonNull Map<Uri, Object> getDrawState() {
|
||||
return savedDrawState;
|
||||
}
|
||||
|
||||
LiveData<List<Media>> getSelectedMedia() {
|
||||
return selectedMedia;
|
||||
}
|
||||
|
||||
LiveData<List<Media>> getMediaInBucket(@NonNull Context context, @NonNull String bucketId) {
|
||||
repository.getMediaInBucket(context, bucketId, bucketMedia::postValue);
|
||||
return bucketMedia;
|
||||
}
|
||||
|
||||
@NonNull LiveData<List<MediaFolder>> getFolders(@NonNull Context context) {
|
||||
repository.getFolders(context, folders::postValue);
|
||||
return folders;
|
||||
}
|
||||
|
||||
LiveData<Integer> getPosition() {
|
||||
return position;
|
||||
}
|
||||
|
||||
LiveData<Optional<String>> getBucketId() {
|
||||
return bucketId;
|
||||
}
|
||||
|
||||
private Optional<String> computeBucketId(@NonNull List<Media> media) {
|
||||
if (media.isEmpty() || !media.get(0).getBucketId().isPresent()) return Optional.absent();
|
||||
|
||||
String candidate = media.get(0).getBucketId().get();
|
||||
for (int i = 1; i < media.size(); i++) {
|
||||
if (!Util.equals(candidate, media.get(i).getBucketId().orNull())) {
|
||||
return Optional.absent();
|
||||
}
|
||||
}
|
||||
|
||||
return Optional.of(candidate);
|
||||
}
|
||||
|
||||
static class Factory extends ViewModelProvider.NewInstanceFactory {
|
||||
|
||||
private final MediaRepository repository;
|
||||
|
||||
Factory(@NonNull MediaRepository repository) {
|
||||
this.repository = repository;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull <T extends ViewModel> T create(@NonNull Class<T> modelClass) {
|
||||
return modelClass.cast(new MediaSendViewModel(repository));
|
||||
}
|
||||
}
|
||||
}
|
@ -35,6 +35,8 @@ import android.support.annotation.NonNull;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.text.TextUtils;
|
||||
|
||||
import org.thoughtcrime.securesms.TransportOption;
|
||||
import org.thoughtcrime.securesms.mediasend.MediaSendActivity;
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
import android.util.Pair;
|
||||
import android.view.View;
|
||||
@ -56,6 +58,7 @@ import org.thoughtcrime.securesms.components.location.SignalPlace;
|
||||
import org.thoughtcrime.securesms.giph.ui.GiphyActivity;
|
||||
import org.thoughtcrime.securesms.permissions.Permissions;
|
||||
import org.thoughtcrime.securesms.providers.PersistentBlobProvider;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.scribbles.ScribbleActivity;
|
||||
import org.thoughtcrime.securesms.util.BitmapUtil;
|
||||
import org.thoughtcrime.securesms.util.MediaUtil;
|
||||
@ -370,12 +373,13 @@ public class AttachmentManager {
|
||||
.execute();
|
||||
}
|
||||
|
||||
public static void selectGallery(Activity activity, int requestCode) {
|
||||
public static void selectGallery(Activity activity, int requestCode, @NonNull Recipient recipient, @NonNull TransportOption transport) {
|
||||
Permissions.with(activity)
|
||||
.request(Manifest.permission.WRITE_EXTERNAL_STORAGE)
|
||||
.ifNecessary()
|
||||
.withPermanentDenialDialog(activity.getString(R.string.AttachmentManager_signal_requires_the_external_storage_permission_in_order_to_attach_photos_videos_or_audio))
|
||||
.onAllGranted(() -> selectMediaType(activity, "image/*", new String[] {"image/*", "video/*"}, requestCode))
|
||||
.onAllGranted(() -> activity.startActivityForResult(MediaSendActivity.getIntent(activity, recipient, transport), requestCode))
|
||||
.execute();
|
||||
}
|
||||
|
||||
|
@ -34,7 +34,7 @@ import org.thoughtcrime.securesms.util.ResUtil;
|
||||
public class AudioSlide extends Slide {
|
||||
|
||||
public AudioSlide(Context context, Uri uri, long dataSize, boolean voiceNote) {
|
||||
super(context, constructAttachmentFromUri(context, uri, MediaUtil.AUDIO_UNSPECIFIED, dataSize, 0, 0, false, null, voiceNote, false));
|
||||
super(context, constructAttachmentFromUri(context, uri, MediaUtil.AUDIO_UNSPECIFIED, dataSize, 0, 0, false, null, null, voiceNote, false));
|
||||
}
|
||||
|
||||
public AudioSlide(Context context, Uri uri, long dataSize, String contentType, boolean voiceNote) {
|
||||
|
@ -19,7 +19,7 @@ public class DocumentSlide extends Slide {
|
||||
@NonNull String contentType, long size,
|
||||
@Nullable String fileName)
|
||||
{
|
||||
super(context, constructAttachmentFromUri(context, uri, contentType, size, 0, 0, true, StorageUtil.getCleanFileName(fileName), false, false));
|
||||
super(context, constructAttachmentFromUri(context, uri, contentType, size, 0, 0, true, StorageUtil.getCleanFileName(fileName), null, false, false));
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -13,8 +13,13 @@ public class GifSlide extends ImageSlide {
|
||||
super(context, attachment);
|
||||
}
|
||||
|
||||
|
||||
public GifSlide(Context context, Uri uri, long size, int width, int height) {
|
||||
super(context, constructAttachmentFromUri(context, uri, MediaUtil.IMAGE_GIF, size, width, height, true, null, false, false));
|
||||
this(context, uri, size, width, height, null);
|
||||
}
|
||||
|
||||
public GifSlide(Context context, Uri uri, long size, int width, int height, @Nullable String caption) {
|
||||
super(context, constructAttachmentFromUri(context, uri, MediaUtil.IMAGE_GIF, size, width, height, true, null, caption, false, false));
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -37,7 +37,11 @@ public class ImageSlide extends Slide {
|
||||
}
|
||||
|
||||
public ImageSlide(Context context, Uri uri, long size, int width, int height) {
|
||||
super(context, constructAttachmentFromUri(context, uri, MediaUtil.IMAGE_JPEG, size, width, height, true, null, false, false));
|
||||
this(context, uri, size, width, height, null);
|
||||
}
|
||||
|
||||
public ImageSlide(Context context, Uri uri, long size, int width, int height, @Nullable String caption) {
|
||||
super(context, constructAttachmentFromUri(context, uri, MediaUtil.IMAGE_JPEG, size, width, height, true, null, caption, false, false));
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -135,12 +135,13 @@ public abstract class Slide {
|
||||
protected static Attachment constructAttachmentFromUri(@NonNull Context context,
|
||||
@NonNull Uri uri,
|
||||
@NonNull String defaultMime,
|
||||
long size,
|
||||
int width,
|
||||
int height,
|
||||
boolean hasThumbnail,
|
||||
@Nullable String fileName,
|
||||
boolean voiceNote,
|
||||
long size,
|
||||
int width,
|
||||
int height,
|
||||
boolean hasThumbnail,
|
||||
@Nullable String fileName,
|
||||
@Nullable String caption,
|
||||
boolean voiceNote,
|
||||
boolean quote)
|
||||
{
|
||||
try {
|
||||
@ -157,7 +158,7 @@ public abstract class Slide {
|
||||
fastPreflightId,
|
||||
voiceNote,
|
||||
quote,
|
||||
null);
|
||||
caption);
|
||||
} catch (NoSuchAlgorithmException e) {
|
||||
throw new AssertionError(e);
|
||||
}
|
||||
|
@ -21,6 +21,7 @@ import android.content.res.Resources.Theme;
|
||||
import android.net.Uri;
|
||||
import android.support.annotation.DrawableRes;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.annotation.Nullable;
|
||||
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.attachments.Attachment;
|
||||
@ -30,7 +31,11 @@ import org.thoughtcrime.securesms.util.ResUtil;
|
||||
public class VideoSlide extends Slide {
|
||||
|
||||
public VideoSlide(Context context, Uri uri, long dataSize) {
|
||||
super(context, constructAttachmentFromUri(context, uri, MediaUtil.VIDEO_UNSPECIFIED, dataSize, 0, 0, MediaUtil.hasVideoThumbnail(uri), null, false, false));
|
||||
this(context, uri, dataSize, null);
|
||||
}
|
||||
|
||||
public VideoSlide(Context context, Uri uri, long dataSize, @Nullable String caption) {
|
||||
super(context, constructAttachmentFromUri(context, uri, MediaUtil.VIDEO_UNSPECIFIED, dataSize, 0, 0, MediaUtil.hasVideoThumbnail(uri), null, caption, false, false));
|
||||
}
|
||||
|
||||
public VideoSlide(Context context, Attachment attachment) {
|
||||
|
@ -13,8 +13,6 @@ import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity;
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.TransportOption;
|
||||
import org.thoughtcrime.securesms.util.DynamicLanguage;
|
||||
import org.thoughtcrime.securesms.util.DynamicNoActionBarTheme;
|
||||
import org.thoughtcrime.securesms.util.DynamicTheme;
|
||||
import org.whispersystems.libsignal.util.guava.Optional;
|
||||
|
||||
@TargetApi(Build.VERSION_CODES.JELLY_BEAN)
|
||||
@ -36,7 +34,7 @@ public class ScribbleActivity extends PassphraseRequiredActionBarActivity implem
|
||||
setContentView(R.layout.scribble_activity);
|
||||
|
||||
if (savedInstanceState == null) {
|
||||
ScribbleFragment fragment = ScribbleFragment.newInstance(getIntent().getData(), dynamicLanguage.getCurrentLocale(), Optional.absent());
|
||||
ScribbleFragment fragment = ScribbleFragment.newInstance(getIntent().getData(), dynamicLanguage.getCurrentLocale(), Optional.absent(), false);
|
||||
getSupportFragmentManager().beginTransaction().add(R.id.fragment_container, fragment).commit();
|
||||
}
|
||||
|
||||
@ -66,4 +64,7 @@ public class ScribbleActivity extends PassphraseRequiredActionBarActivity implem
|
||||
Toast.makeText(ScribbleActivity.this, R.string.ScribbleActivity_save_failure, Toast.LENGTH_SHORT).show();
|
||||
finish();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onTouchEventsNeeded(boolean needed) { }
|
||||
}
|
||||
|
@ -20,6 +20,7 @@ import android.view.WindowManager;
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.TransportOption;
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
import org.thoughtcrime.securesms.mediasend.MediaSendPageFragment;
|
||||
import org.thoughtcrime.securesms.mms.GlideApp;
|
||||
import org.thoughtcrime.securesms.mms.GlideRequests;
|
||||
import org.thoughtcrime.securesms.providers.PersistentBlobProvider;
|
||||
@ -46,13 +47,17 @@ import java.util.concurrent.ExecutionException;
|
||||
import static android.app.Activity.RESULT_OK;
|
||||
|
||||
@TargetApi(Build.VERSION_CODES.JELLY_BEAN)
|
||||
public class ScribbleFragment extends Fragment implements ScribbleHud.EventListener, VerticalSlideColorPicker.OnColorChangeListener {
|
||||
public class ScribbleFragment extends Fragment implements ScribbleHud.EventListener,
|
||||
VerticalSlideColorPicker.OnColorChangeListener,
|
||||
MediaSendPageFragment
|
||||
{
|
||||
|
||||
private static final String TAG = ScribbleFragment.class.getSimpleName();
|
||||
|
||||
private static final String KEY_IMAGE_URI = "image_uri";
|
||||
private static final String KEY_LOCALE = "locale";
|
||||
private static final String KEY_TRANSPORT = "compose_mode";
|
||||
private static final String KEY_HIDE_SAVE = "hide_save";
|
||||
|
||||
public static final int SELECT_STICKER_REQUEST_CODE = 123;
|
||||
|
||||
@ -60,15 +65,20 @@ public class ScribbleFragment extends Fragment implements ScribbleHud.EventListe
|
||||
private ScribbleHud scribbleHud;
|
||||
private ScribbleView scribbleView;
|
||||
private GlideRequests glideRequests;
|
||||
private Uri imageUri;
|
||||
|
||||
public static ScribbleFragment newInstance(@NonNull Uri imageUri, @NonNull Locale locale, Optional<TransportOption> transport) {
|
||||
private ScribbleView.SavedState savedState;
|
||||
|
||||
public static ScribbleFragment newInstance(@NonNull Uri imageUri, @NonNull Locale locale, Optional<TransportOption> transport, boolean hideSave) {
|
||||
Bundle args = new Bundle();
|
||||
args.putParcelable(KEY_IMAGE_URI, imageUri);
|
||||
args.putSerializable(KEY_LOCALE, locale);
|
||||
args.putParcelable(KEY_TRANSPORT, transport.orNull());
|
||||
args.putBoolean(KEY_HIDE_SAVE, hideSave);
|
||||
|
||||
ScribbleFragment fragment = new ScribbleFragment();
|
||||
fragment.setArguments(args);
|
||||
fragment.setUri(imageUri);
|
||||
return fragment;
|
||||
}
|
||||
|
||||
@ -79,6 +89,7 @@ public class ScribbleFragment extends Fragment implements ScribbleHud.EventListe
|
||||
throw new IllegalStateException("Parent activity must implement Controller interface.");
|
||||
}
|
||||
controller = (Controller) getActivity();
|
||||
imageUri = getArguments().getParcelable(KEY_IMAGE_URI);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@ -97,12 +108,46 @@ public class ScribbleFragment extends Fragment implements ScribbleHud.EventListe
|
||||
|
||||
scribbleHud.setEventListener(this);
|
||||
scribbleHud.setTransport(Optional.fromNullable(getArguments().getParcelable(KEY_TRANSPORT)));
|
||||
scribbleHud.hideSaveButton(getArguments().getBoolean(KEY_HIDE_SAVE));
|
||||
scribbleHud.setFullscreen((getActivity().getWindow().getAttributes().flags & WindowManager.LayoutParams.FLAG_FULLSCREEN) > 0);
|
||||
|
||||
scribbleView.setMotionViewCallback(motionViewCallback);
|
||||
scribbleView.setDrawingChangedListener(() -> scribbleHud.setColorPalette(scribbleView.getUniqueColors()));
|
||||
scribbleView.setDrawingMode(false);
|
||||
scribbleView.setImage(glideRequests, getArguments().getParcelable(KEY_IMAGE_URI));
|
||||
scribbleView.setImage(glideRequests, imageUri);
|
||||
|
||||
if (savedState != null) {
|
||||
scribbleView.restoreState(savedState);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setUri(@NonNull Uri uri) {
|
||||
this.imageUri = uri;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull Uri getUri() {
|
||||
return imageUri;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @Nullable View getPlaybackControls() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @Nullable Object saveState() {
|
||||
return scribbleView.saveState();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void restoreState(@NonNull Object state) {
|
||||
if (state instanceof ScribbleView.SavedState) {
|
||||
savedState = (ScribbleView.SavedState) state;
|
||||
} else {
|
||||
Log.w(TAG, "Received a bad saved state. Received class: " + state.getClass().getName());
|
||||
}
|
||||
}
|
||||
|
||||
public boolean isEmojiKeyboardVisible() {
|
||||
@ -204,27 +249,32 @@ public class ScribbleFragment extends Fragment implements ScribbleHud.EventListe
|
||||
public void onModeStarted(@NonNull ScribbleHud.Mode mode) {
|
||||
switch (mode) {
|
||||
case DRAW:
|
||||
controller.onTouchEventsNeeded(true);
|
||||
scribbleView.setDrawingMode(true);
|
||||
scribbleView.setDrawingBrushWidth(ScribbleView.DEFAULT_BRUSH_WIDTH);
|
||||
break;
|
||||
|
||||
case HIGHLIGHT:
|
||||
controller.onTouchEventsNeeded(true);
|
||||
scribbleView.setDrawingMode(true);
|
||||
scribbleView.setDrawingBrushWidth(ScribbleView.DEFAULT_BRUSH_WIDTH * 3);
|
||||
break;
|
||||
|
||||
case TEXT:
|
||||
controller.onTouchEventsNeeded(true);
|
||||
scribbleView.setDrawingMode(false);
|
||||
addTextSticker();
|
||||
break;
|
||||
|
||||
case STICKER:
|
||||
controller.onTouchEventsNeeded(true);
|
||||
scribbleView.setDrawingMode(false);
|
||||
Intent intent = new Intent(getContext(), StickerSelectActivity.class);
|
||||
startActivityForResult(intent, SELECT_STICKER_REQUEST_CODE);
|
||||
break;
|
||||
|
||||
case NONE:
|
||||
controller.onTouchEventsNeeded(false);
|
||||
scribbleView.clearSelection();
|
||||
scribbleView.setDrawingMode(false);
|
||||
break;
|
||||
@ -283,13 +333,16 @@ public class ScribbleFragment extends Fragment implements ScribbleHud.EventListe
|
||||
public void onEntitySelected(@Nullable MotionEntity entity) {
|
||||
if (entity == null) {
|
||||
scribbleHud.enterMode(ScribbleHud.Mode.NONE);
|
||||
controller.onTouchEventsNeeded(false);
|
||||
} else if (entity instanceof TextEntity) {
|
||||
int textColor = ((TextEntity) entity).getLayer().getFont().getColor();
|
||||
|
||||
scribbleHud.enterMode(ScribbleHud.Mode.TEXT);
|
||||
scribbleHud.setActiveColor(textColor);
|
||||
controller.onTouchEventsNeeded(true);
|
||||
} else {
|
||||
scribbleHud.enterMode(ScribbleHud.Mode.STICKER);
|
||||
controller.onTouchEventsNeeded(true);
|
||||
}
|
||||
}
|
||||
|
||||
@ -302,5 +355,6 @@ public class ScribbleFragment extends Fragment implements ScribbleHud.EventListe
|
||||
public interface Controller {
|
||||
void onImageEditComplete(@NonNull Uri uri, int width, int height, long size, @NonNull Optional<String> message, @NonNull Optional<TransportOption> transport);
|
||||
void onImageEditFailure();
|
||||
void onTouchEventsNeeded(boolean needed);
|
||||
}
|
||||
}
|
||||
|
@ -201,6 +201,12 @@ public class ScribbleHud extends InputAwareLayout implements ViewTreeObserver.On
|
||||
}
|
||||
}
|
||||
|
||||
public void hideSaveButton(boolean hide) {
|
||||
if (hide) {
|
||||
saveButton.setVisibility(GONE);
|
||||
}
|
||||
}
|
||||
|
||||
public void dismissEmojiKeyboard() {
|
||||
hideCurrentInput(composeText);
|
||||
}
|
||||
|
@ -58,9 +58,11 @@ public class CanvasView extends View {
|
||||
QUBIC_BEZIER;
|
||||
}
|
||||
|
||||
private int canvasWidth = 1;
|
||||
private int canvasHeight = 1;
|
||||
private Bitmap bitmap = null;
|
||||
private int initialWidth = 0;
|
||||
private int initialHeight = 0;
|
||||
private int canvasWidth = 1;
|
||||
private int canvasHeight = 1;
|
||||
private Bitmap bitmap = null;
|
||||
|
||||
private List<Path> pathLists = new ArrayList<Path>();
|
||||
private List<Paint> paintLists = new ArrayList<Paint>();
|
||||
@ -177,8 +179,8 @@ public class CanvasView extends View {
|
||||
Path path = new Path();
|
||||
|
||||
// Save for ACTION_MOVE
|
||||
this.startX = event.getX();
|
||||
this.startY = event.getY();
|
||||
this.startX = scaleX(event.getX());
|
||||
this.startY = scaleY(event.getY());
|
||||
|
||||
path.moveTo(this.startX, this.startY);
|
||||
|
||||
@ -279,7 +281,7 @@ public class CanvasView extends View {
|
||||
switch (this.drawer) {
|
||||
case PEN :
|
||||
for (int i = 0; i < event.getHistorySize(); i++) {
|
||||
path.lineTo(event.getHistoricalX(i), event.getHistoricalY(i));
|
||||
path.lineTo(scaleX(event.getHistoricalX(i)), scaleY(event.getHistoricalY(i)));
|
||||
}
|
||||
break;
|
||||
case LINE :
|
||||
@ -316,7 +318,7 @@ public class CanvasView extends View {
|
||||
Path path = this.getCurrentPath();
|
||||
|
||||
path.reset();
|
||||
path.moveTo(this.startX, this.startY);
|
||||
path.moveTo(scaleX(this.startX), scaleY(this.startY));
|
||||
path.quadTo(this.controlX, this.controlY, x, y);
|
||||
}
|
||||
|
||||
@ -344,6 +346,25 @@ public class CanvasView extends View {
|
||||
}
|
||||
}
|
||||
|
||||
public SavedState saveState() {
|
||||
return new SavedState(pathLists, paintLists, historyPointer, initialWidth, initialHeight, canvasWidth, canvasHeight);
|
||||
}
|
||||
|
||||
public void restoreState(@NonNull SavedState state) {
|
||||
this.pathLists.clear();
|
||||
this.pathLists.addAll(state.getPaths());
|
||||
|
||||
this.paintLists.clear();
|
||||
this.paintLists.addAll(state.getPaints());
|
||||
|
||||
this.historyPointer = state.getHistoryPointer();
|
||||
|
||||
this.initialWidth = state.getInitialWidth();
|
||||
this.initialHeight = state.getInitialHeight();
|
||||
|
||||
postInvalidate();
|
||||
}
|
||||
|
||||
public void setActive(boolean active) {
|
||||
this.active = active;
|
||||
}
|
||||
@ -357,19 +378,8 @@ public class CanvasView extends View {
|
||||
protected void onDraw(Canvas canvas) {
|
||||
super.onDraw(canvas);
|
||||
|
||||
// Before "drawPath"
|
||||
canvas.drawColor(this.baseColor);
|
||||
|
||||
if (this.bitmap != null) {
|
||||
canvas.drawBitmap(this.bitmap, 0F, 0F, new Paint());
|
||||
}
|
||||
|
||||
for (int i = 0; i < this.historyPointer; i++) {
|
||||
Path path = this.pathLists.get(i);
|
||||
Paint paint = this.paintLists.get(i);
|
||||
|
||||
canvas.drawPath(path, paint);
|
||||
}
|
||||
render(canvas);
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -377,18 +387,41 @@ public class CanvasView extends View {
|
||||
super.onSizeChanged(w, h, oldw, oldh);
|
||||
this.canvasWidth = w;
|
||||
this.canvasHeight = h;
|
||||
|
||||
if (initialWidth == 0) {
|
||||
initialWidth = canvasWidth;
|
||||
}
|
||||
|
||||
if (initialHeight == 0) {
|
||||
initialHeight = canvasHeight;
|
||||
}
|
||||
}
|
||||
|
||||
public void render(Canvas canvas) {
|
||||
float scaleX = 1.0F * canvas.getWidth() / canvasWidth;
|
||||
float scaleY = 1.0F * canvas.getHeight() / canvasHeight;
|
||||
render(canvas, initialWidth, initialHeight, canvasWidth, canvasHeight, pathLists, paintLists, historyPointer);
|
||||
}
|
||||
|
||||
public static void render(Canvas canvas, int initialWidth, int initialHeight, int canvasWidth, int canvasHeight, List<Path> pathLists, List<Paint> paintLists, int historyPointer) {
|
||||
float scaleX = 1f;
|
||||
float scaleY = 1f;
|
||||
|
||||
if (initialWidth > 0) {
|
||||
scaleX *= (float) canvasWidth / initialWidth;
|
||||
}
|
||||
|
||||
if (initialHeight > 0) {
|
||||
scaleY *= (float) canvasHeight / initialHeight;
|
||||
}
|
||||
|
||||
scaleX *= (float) canvas.getWidth() / canvasWidth;
|
||||
scaleY *= (float) canvas.getHeight() / canvasHeight;
|
||||
|
||||
Matrix matrix = new Matrix();
|
||||
matrix.setScale(scaleX, scaleY);
|
||||
|
||||
for (int i = 0; i < this.historyPointer; i++) {
|
||||
Path path = this.pathLists.get(i);
|
||||
Paint paint = this.paintLists.get(i);
|
||||
for (int i = 0; i < historyPointer; i++) {
|
||||
Path path = pathLists.get(i);
|
||||
Paint paint = paintLists.get(i);
|
||||
|
||||
Path scaledPath = new Path();
|
||||
path.transform(matrix, scaledPath);
|
||||
@ -785,4 +818,64 @@ public class CanvasView extends View {
|
||||
|
||||
return colors;
|
||||
}
|
||||
|
||||
private float scaleX(float x) {
|
||||
return ((float) initialWidth / canvasWidth) * x;
|
||||
}
|
||||
|
||||
private float scaleY(float y) {
|
||||
return ((float) initialWidth / canvasWidth) * y;
|
||||
}
|
||||
|
||||
static class SavedState {
|
||||
private final List<Path> paths;
|
||||
private final List<Paint> paints;
|
||||
private final int historyPointer;
|
||||
private final int initialWidth;
|
||||
private final int initialHeight;
|
||||
private final int canvasWidth;
|
||||
private final int canvasHeight;
|
||||
|
||||
SavedState(List<Path> paths, List<Paint> paints, int historyPointer, int initialWidth, int initialHeight, int canvasWidth, int canvasHeight) {
|
||||
this.paths = new ArrayList<>(paths);
|
||||
this.paints = new ArrayList<>(paints);
|
||||
this.historyPointer = historyPointer;
|
||||
this.initialWidth = initialWidth;
|
||||
this.initialHeight = initialHeight;
|
||||
this.canvasWidth = canvasWidth;
|
||||
this.canvasHeight = canvasHeight;
|
||||
}
|
||||
|
||||
List<Path> getPaths() {
|
||||
return paths;
|
||||
}
|
||||
|
||||
List<Paint> getPaints() {
|
||||
return paints;
|
||||
}
|
||||
|
||||
int getHistoryPointer() {
|
||||
return historyPointer;
|
||||
}
|
||||
|
||||
int getInitialWidth() {
|
||||
return initialWidth;
|
||||
}
|
||||
|
||||
int getInitialHeight() {
|
||||
return initialHeight;
|
||||
}
|
||||
|
||||
int getCanvasWidth() {
|
||||
return canvasWidth;
|
||||
}
|
||||
|
||||
int getCanvasHeight() {
|
||||
return canvasHeight;
|
||||
}
|
||||
|
||||
boolean isEmpty() {
|
||||
return paths.size() <= 1;
|
||||
}
|
||||
}
|
||||
}
|
@ -49,6 +49,8 @@ import android.view.inputmethod.InputMethodManager;
|
||||
import android.widget.EditText;
|
||||
import android.widget.FrameLayout;
|
||||
|
||||
import com.annimon.stream.Stream;
|
||||
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
import org.thoughtcrime.securesms.scribbles.multitouch.MoveGestureDetector;
|
||||
@ -146,6 +148,17 @@ public class MotionView extends FrameLayout implements TextWatcher {
|
||||
updateUI();
|
||||
}
|
||||
|
||||
public SavedState saveState() {
|
||||
return new SavedState(entities);
|
||||
}
|
||||
|
||||
public void restoreState(@NonNull SavedState savedState) {
|
||||
this.entities.clear();
|
||||
this.entities.addAll(savedState.getEntities());
|
||||
|
||||
postInvalidate();
|
||||
}
|
||||
|
||||
public void startEditing(TextEntity entity) {
|
||||
editText.setFocusableInTouchMode(true);
|
||||
editText.setFocusable(true);
|
||||
@ -224,7 +237,7 @@ public class MotionView extends FrameLayout implements TextWatcher {
|
||||
@Override
|
||||
protected void onDraw(Canvas canvas) {
|
||||
super.onDraw(canvas);
|
||||
drawAllEntities(canvas);
|
||||
render(canvas, entities);
|
||||
}
|
||||
|
||||
public void render(Canvas canvas) {
|
||||
@ -232,11 +245,7 @@ public class MotionView extends FrameLayout implements TextWatcher {
|
||||
draw(canvas);
|
||||
}
|
||||
|
||||
/**
|
||||
* draws all entities on the canvas
|
||||
* @param canvas Canvas where to draw all entities
|
||||
*/
|
||||
private void drawAllEntities(Canvas canvas) {
|
||||
public static void render(Canvas canvas, List<MotionEntity> entities) {
|
||||
for (int i = 0; i < entities.size(); i++) {
|
||||
entities.get(i).draw(canvas, null);
|
||||
}
|
||||
@ -254,7 +263,7 @@ public class MotionView extends FrameLayout implements TextWatcher {
|
||||
// which doesn't have transparent pixels, the background will be black
|
||||
bmp.eraseColor(Color.WHITE);
|
||||
Canvas canvas = new Canvas(bmp);
|
||||
drawAllEntities(canvas);
|
||||
render(canvas, entities);
|
||||
|
||||
return bmp;
|
||||
}
|
||||
@ -494,4 +503,21 @@ public class MotionView extends FrameLayout implements TextWatcher {
|
||||
}
|
||||
}
|
||||
|
||||
static class SavedState {
|
||||
|
||||
private final List<MotionEntity> entities;
|
||||
|
||||
SavedState(List<MotionEntity> entities) {
|
||||
this.entities = new ArrayList<>(entities);
|
||||
Stream.of(entities).forEach(e -> e.setIsSelected(false));
|
||||
}
|
||||
|
||||
List<MotionEntity> getEntities() {
|
||||
return entities;
|
||||
}
|
||||
|
||||
boolean isEmpty() {
|
||||
return entities.isEmpty();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -29,7 +29,6 @@ import android.support.annotation.NonNull;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.util.AttributeSet;
|
||||
import android.view.MotionEvent;
|
||||
import android.view.View;
|
||||
import android.widget.FrameLayout;
|
||||
import android.widget.ImageView;
|
||||
|
||||
@ -93,8 +92,15 @@ public class ScribbleView extends FrameLayout {
|
||||
}
|
||||
|
||||
public @NonNull ListenableFuture<Bitmap> getRenderedImage(@NonNull GlideRequests glideRequests) {
|
||||
return renderImage(getContext(), imageUri, saveState(), glideRequests);
|
||||
}
|
||||
|
||||
public static @NonNull ListenableFuture<Bitmap> renderImage(@NonNull Context context,
|
||||
@Nullable Uri imageUri,
|
||||
@NonNull SavedState savedState,
|
||||
@NonNull GlideRequests glideRequests)
|
||||
{
|
||||
final SettableFuture<Bitmap> future = new SettableFuture<>();
|
||||
final Context context = getContext();
|
||||
final boolean isLowMemory = Util.isLowMemory(context);
|
||||
|
||||
if (imageUri == null) {
|
||||
@ -119,8 +125,15 @@ public class ScribbleView extends FrameLayout {
|
||||
@Override
|
||||
public void onResourceReady(@NonNull Bitmap bitmap, @Nullable Transition<? super Bitmap> transition) {
|
||||
Canvas canvas = new Canvas(bitmap);
|
||||
motionView.render(canvas);
|
||||
canvasView.render(canvas);
|
||||
MotionView.render(canvas, savedState.getMotionState().getEntities());
|
||||
CanvasView.render(canvas,
|
||||
savedState.getCanvasState().getInitialWidth(),
|
||||
savedState.getCanvasState().getInitialHeight(),
|
||||
savedState.getCanvasState().getCanvasWidth(),
|
||||
savedState.getCanvasState().getCanvasHeight(),
|
||||
savedState.getCanvasState().getPaths(),
|
||||
savedState.getCanvasState().getPaints(),
|
||||
savedState.getCanvasState().getHistoryPointer());
|
||||
future.set(bitmap);
|
||||
}
|
||||
|
||||
@ -128,11 +141,20 @@ public class ScribbleView extends FrameLayout {
|
||||
public void onLoadFailed(@Nullable Drawable errorDrawable) {
|
||||
future.setException(new Throwable("Failed to load image."));
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
return future;
|
||||
}
|
||||
|
||||
public SavedState saveState() {
|
||||
return new SavedState(canvasView.saveState(), motionView.saveState());
|
||||
}
|
||||
|
||||
public void restoreState(@NonNull SavedState state) {
|
||||
canvasView.restoreState(state.getCanvasState());
|
||||
motionView.restoreState(state.getMotionState());
|
||||
}
|
||||
|
||||
private void initialize(@NonNull Context context) {
|
||||
inflate(context, R.layout.scribble_view, this);
|
||||
|
||||
@ -221,4 +243,26 @@ public class ScribbleView extends FrameLayout {
|
||||
public interface DrawingChangedListener {
|
||||
void onDrawingChanged();
|
||||
}
|
||||
|
||||
public static class SavedState {
|
||||
private final CanvasView.SavedState canvasState;
|
||||
private final MotionView.SavedState motionState;
|
||||
|
||||
SavedState(@NonNull CanvasView.SavedState canvasState, @NonNull MotionView.SavedState motionState) {
|
||||
this.canvasState = canvasState;
|
||||
this.motionState = motionState;
|
||||
}
|
||||
|
||||
CanvasView.SavedState getCanvasState() {
|
||||
return canvasState;
|
||||
}
|
||||
|
||||
MotionView.SavedState getMotionState() {
|
||||
return motionState;
|
||||
}
|
||||
|
||||
public boolean isEmpty() {
|
||||
return canvasState.isEmpty() && motionState.isEmpty();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -239,6 +239,8 @@ public class MediaUtil {
|
||||
|
||||
if ("com.android.providers.media.documents".equals(uri.getAuthority())) {
|
||||
return uri.getLastPathSegment().contains("video");
|
||||
} else if (uri.toString().startsWith(MediaStore.Video.Media.EXTERNAL_CONTENT_URI.toString())) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
@ -248,6 +250,13 @@ public class MediaUtil {
|
||||
if ("com.android.providers.media.documents".equals(uri.getAuthority())) {
|
||||
long videoId = Long.parseLong(uri.getLastPathSegment().split(":")[1]);
|
||||
|
||||
return MediaStore.Video.Thumbnails.getThumbnail(context.getContentResolver(),
|
||||
videoId,
|
||||
MediaStore.Images.Thumbnails.MINI_KIND,
|
||||
null);
|
||||
} else if (uri.toString().startsWith(MediaStore.Video.Media.EXTERNAL_CONTENT_URI.toString())) {
|
||||
long videoId = Long.parseLong(uri.getLastPathSegment());
|
||||
|
||||
return MediaStore.Video.Thumbnails.getThumbnail(context.getContentResolver(),
|
||||
videoId,
|
||||
MediaStore.Images.Thumbnails.MINI_KIND,
|
||||
|
@ -5,7 +5,11 @@ import android.content.res.Resources;
|
||||
import android.graphics.Color;
|
||||
import android.support.annotation.AttrRes;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.annotation.StyleRes;
|
||||
import android.support.v7.view.ContextThemeWrapper;
|
||||
import android.util.TypedValue;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
|
||||
import org.thoughtcrime.securesms.R;
|
||||
|
||||
@ -25,6 +29,11 @@ public class ThemeUtil {
|
||||
return Color.RED;
|
||||
}
|
||||
|
||||
public static LayoutInflater getThemedInflater(@NonNull Context context, @NonNull LayoutInflater inflater, @StyleRes int theme) {
|
||||
Context contextThemeWrapper = new ContextThemeWrapper(context, theme);
|
||||
return inflater.cloneInContext(contextThemeWrapper);
|
||||
}
|
||||
|
||||
private static String getAttribute(Context context, int attribute, String defaultValue) {
|
||||
TypedValue outValue = new TypedValue();
|
||||
|
||||
|
@ -133,6 +133,10 @@ public class Util {
|
||||
return value == null || value.getText() == null || TextUtils.isEmpty(value.getTextTrimmed());
|
||||
}
|
||||
|
||||
public static boolean isEmpty(Collection collection) {
|
||||
return collection == null || collection.isEmpty();
|
||||
}
|
||||
|
||||
public static <K, V> V getOrDefault(@NonNull Map<K, V> map, K key, V defaultValue) {
|
||||
return map.containsKey(key) ? map.get(key) : defaultValue;
|
||||
}
|
||||
|