Add storage management features.
This commit is contained in:
parent
bceb69b284
commit
52447f5e97
78 changed files with 3343 additions and 1622 deletions
|
@ -334,10 +334,9 @@
|
|||
android:launchMode="singleTask"
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
|
||||
|
||||
<activity android:name=".MediaOverviewActivity"
|
||||
<activity android:name=".mediaoverview.MediaOverviewActivity"
|
||||
android:theme="@style/TextSecure.LightNoActionBar"
|
||||
android:windowSoftInputMode="stateHidden"
|
||||
android:launchMode="singleTask"
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
|
||||
|
||||
<activity android:name=".DummyActivity"
|
||||
|
|
9
res/drawable/ic_archive_outline_24dp.xml
Normal file
9
res/drawable/ic_archive_outline_24dp.xml
Normal file
|
@ -0,0 +1,9 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="?icon_tint"
|
||||
android:pathData="M14,13L10,13a1,1 0,0 1,-1 -1L9,12a1,1 0,0 1,1 -1h4a1,1 0,0 1,1 1h0A1,1 0,0 1,14 13ZM22.5,4.5v3A1.5,1.5 0,0 1,21 9L21,19.5A1.5,1.5 0,0 1,19.5 21L4.5,21A1.5,1.5 0,0 1,3 19.5L3,9A1.5,1.5 0,0 1,1.5 7.5v-3A1.5,1.5 0,0 1,3 3L21,3A1.5,1.5 0,0 1,22.5 4.5ZM19.5,9L4.5,9L4.5,19.5h15ZM21,4.5L3,4.5v3L21,7.5Z"/>
|
||||
</vector>
|
9
res/drawable/ic_archive_solid_24dp.xml
Normal file
9
res/drawable/ic_archive_solid_24dp.xml
Normal file
|
@ -0,0 +1,9 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="?icon_tint"
|
||||
android:pathData="M3,8.5v11A1.5,1.5 0,0 0,4.5 21h15A1.5,1.5 0,0 0,21 19.5L21,8.5ZM14,13L10,13a1,1 0,0 1,0 -2h4a1,1 0,0 1,0 2ZM21.75,7L2.25,7a0.75,0.75 0,0 1,-0.75 -0.75L1.5,3.75A0.75,0.75 0,0 1,2.25 3h19.5a0.75,0.75 0,0 1,0.75 0.75v2.5A0.75,0.75 0,0 1,21.75 7Z"/>
|
||||
</vector>
|
9
res/drawable/ic_arrow_down_14.xml
Normal file
9
res/drawable/ic_arrow_down_14.xml
Normal file
|
@ -0,0 +1,9 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="14dp"
|
||||
android:height="14dp"
|
||||
android:viewportWidth="14"
|
||||
android:viewportHeight="14">
|
||||
<path
|
||||
android:pathData="M12,7.0018L11.0377,6.0395L8.4348,8.6434L7.7065,9.6637V2H6.3461V9.6637L5.6713,8.7196L2.9559,6.0368L2,7.0045L7.029,11.9737L12,7.0018Z"
|
||||
android:fillColor="#000000"/>
|
||||
</vector>
|
12
res/drawable/ic_check_circle_solid_20.xml
Normal file
12
res/drawable/ic_check_circle_solid_20.xml
Normal file
|
@ -0,0 +1,12 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="20dp"
|
||||
android:height="20dp"
|
||||
android:viewportWidth="20"
|
||||
android:viewportHeight="20">
|
||||
<path
|
||||
android:fillColor="#2090EA"
|
||||
android:pathData="M10,10m-9,0a9,9 0,1 1,18 0a9,9 0,1 1,-18 0" />
|
||||
<path
|
||||
android:fillColor="#ffffff"
|
||||
android:pathData="M9.0001,11.9393L14.4697,6.4697L15.5304,7.5303L9.0001,14.0607L5.4697,10.5303L6.5304,9.4697L9.0001,11.9393Z" />
|
||||
</vector>
|
36
res/drawable/ic_grid_outline_20.xml
Normal file
36
res/drawable/ic_grid_outline_20.xml
Normal file
|
@ -0,0 +1,36 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="20dp"
|
||||
android:height="20dp"
|
||||
android:viewportWidth="20"
|
||||
android:viewportHeight="20">
|
||||
<path
|
||||
android:pathData="M2,4.89L3.94,4.89A0.75,0.75 0,0 1,4.69 5.64L4.69,7.58A0.75,0.75 0,0 1,3.94 8.33L2,8.33A0.75,0.75 0,0 1,1.25 7.58L1.25,5.64A0.75,0.75 0,0 1,2 4.89z"
|
||||
android:strokeWidth="1.5"
|
||||
android:fillColor="#00000000"
|
||||
android:strokeColor="#000"/>
|
||||
<path
|
||||
android:pathData="M9.03,4.89L10.97,4.89A0.75,0.75 0,0 1,11.72 5.64L11.72,7.58A0.75,0.75 0,0 1,10.97 8.33L9.03,8.33A0.75,0.75 0,0 1,8.28 7.58L8.28,5.64A0.75,0.75 0,0 1,9.03 4.89z"
|
||||
android:strokeWidth="1.5"
|
||||
android:fillColor="#00000000"
|
||||
android:strokeColor="#000"/>
|
||||
<path
|
||||
android:pathData="M16.06,4.89L18,4.89A0.75,0.75 0,0 1,18.75 5.64L18.75,7.58A0.75,0.75 0,0 1,18 8.33L16.06,8.33A0.75,0.75 0,0 1,15.31 7.58L15.31,5.64A0.75,0.75 0,0 1,16.06 4.89z"
|
||||
android:strokeWidth="1.5"
|
||||
android:fillColor="#00000000"
|
||||
android:strokeColor="#000"/>
|
||||
<path
|
||||
android:pathData="M2,11.67L3.94,11.67A0.75,0.75 0,0 1,4.69 12.42L4.69,14.36A0.75,0.75 0,0 1,3.94 15.11L2,15.11A0.75,0.75 0,0 1,1.25 14.36L1.25,12.42A0.75,0.75 0,0 1,2 11.67z"
|
||||
android:strokeWidth="1.5"
|
||||
android:fillColor="#00000000"
|
||||
android:strokeColor="#000"/>
|
||||
<path
|
||||
android:pathData="M9.03,11.67L10.97,11.67A0.75,0.75 0,0 1,11.72 12.42L11.72,14.36A0.75,0.75 0,0 1,10.97 15.11L9.03,15.11A0.75,0.75 0,0 1,8.28 14.36L8.28,12.42A0.75,0.75 0,0 1,9.03 11.67z"
|
||||
android:strokeWidth="1.5"
|
||||
android:fillColor="#00000000"
|
||||
android:strokeColor="#000"/>
|
||||
<path
|
||||
android:pathData="M16.06,11.67L18,11.67A0.75,0.75 0,0 1,18.75 12.42L18.75,14.36A0.75,0.75 0,0 1,18 15.11L16.06,15.11A0.75,0.75 0,0 1,15.31 14.36L15.31,12.42A0.75,0.75 0,0 1,16.06 11.67z"
|
||||
android:strokeWidth="1.5"
|
||||
android:fillColor="#00000000"
|
||||
android:strokeColor="#000"/>
|
||||
</vector>
|
24
res/drawable/ic_grid_solid_20.xml
Normal file
24
res/drawable/ic_grid_solid_20.xml
Normal file
|
@ -0,0 +1,24 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="20dp"
|
||||
android:height="20dp"
|
||||
android:viewportWidth="20"
|
||||
android:viewportHeight="20">
|
||||
<path
|
||||
android:fillColor="#FF000000"
|
||||
android:pathData="M2,4.39L3.95,4.39A1.25,1.25 0,0 1,5.2 5.64L5.2,7.58A1.25,1.25 0,0 1,3.95 8.83L2,8.83A1.25,1.25 0,0 1,0.75 7.58L0.75,5.64A1.25,1.25 0,0 1,2 4.39z"/>
|
||||
<path
|
||||
android:fillColor="#FF000000"
|
||||
android:pathData="M9.03,4.39L10.98,4.39A1.25,1.25 0,0 1,12.23 5.64L12.23,7.58A1.25,1.25 0,0 1,10.98 8.83L9.03,8.83A1.25,1.25 0,0 1,7.78 7.58L7.78,5.64A1.25,1.25 0,0 1,9.03 4.39z"/>
|
||||
<path
|
||||
android:fillColor="#FF000000"
|
||||
android:pathData="M16.05,4.39L18,4.39A1.25,1.25 0,0 1,19.25 5.64L19.25,7.58A1.25,1.25 0,0 1,18 8.83L16.05,8.83A1.25,1.25 0,0 1,14.8 7.58L14.8,5.64A1.25,1.25 0,0 1,16.05 4.39z"/>
|
||||
<path
|
||||
android:fillColor="#FF000000"
|
||||
android:pathData="M2,11.17L3.95,11.17A1.25,1.25 0,0 1,5.2 12.42L5.2,14.36A1.25,1.25 0,0 1,3.95 15.61L2,15.61A1.25,1.25 0,0 1,0.75 14.36L0.75,12.42A1.25,1.25 0,0 1,2 11.17z"/>
|
||||
<path
|
||||
android:fillColor="#FF000000"
|
||||
android:pathData="M9.03,11.17L10.98,11.17A1.25,1.25 0,0 1,12.23 12.42L12.23,14.36A1.25,1.25 0,0 1,10.98 15.61L9.03,15.61A1.25,1.25 0,0 1,7.78 14.36L7.78,12.42A1.25,1.25 0,0 1,9.03 11.17z"/>
|
||||
<path
|
||||
android:fillColor="#FF000000"
|
||||
android:pathData="M16.05,11.17L18,11.17A1.25,1.25 0,0 1,19.25 12.42L19.25,14.36A1.25,1.25 0,0 1,18 15.61L16.05,15.61A1.25,1.25 0,0 1,14.8 14.36L14.8,12.42A1.25,1.25 0,0 1,16.05 11.17z"/>
|
||||
</vector>
|
36
res/drawable/ic_list_outline_20.xml
Normal file
36
res/drawable/ic_list_outline_20.xml
Normal file
|
@ -0,0 +1,36 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="20dp"
|
||||
android:height="20dp"
|
||||
android:viewportWidth="20"
|
||||
android:viewportHeight="20">
|
||||
<path
|
||||
android:pathData="M3.5,8.5L5,8.5A0.75,0.75 0,0 1,5.75 9.25L5.75,10.75A0.75,0.75 0,0 1,5 11.5L3.5,11.5A0.75,0.75 0,0 1,2.75 10.75L2.75,9.25A0.75,0.75 0,0 1,3.5 8.5z"
|
||||
android:strokeWidth="1.5"
|
||||
android:fillColor="#00000000"
|
||||
android:strokeColor="#5E5E5E"/>
|
||||
<path
|
||||
android:pathData="M3.5,14.25L5,14.25A0.75,0.75 0,0 1,5.75 15L5.75,16.5A0.75,0.75 0,0 1,5 17.25L3.5,17.25A0.75,0.75 0,0 1,2.75 16.5L2.75,15A0.75,0.75 0,0 1,3.5 14.25z"
|
||||
android:strokeWidth="1.5"
|
||||
android:fillColor="#00000000"
|
||||
android:strokeColor="#5E5E5E"/>
|
||||
<path
|
||||
android:pathData="M3.5,2.75L5,2.75A0.75,0.75 0,0 1,5.75 3.5L5.75,5A0.75,0.75 0,0 1,5 5.75L3.5,5.75A0.75,0.75 0,0 1,2.75 5L2.75,3.5A0.75,0.75 0,0 1,3.5 2.75z"
|
||||
android:strokeWidth="1.5"
|
||||
android:fillColor="#00000000"
|
||||
android:strokeColor="#5E5E5E"/>
|
||||
<path
|
||||
android:pathData="M8,4.25H18"
|
||||
android:strokeWidth="1.5"
|
||||
android:fillColor="#00000000"
|
||||
android:strokeColor="#5E5E5E"/>
|
||||
<path
|
||||
android:pathData="M8,10H18"
|
||||
android:strokeWidth="1.5"
|
||||
android:fillColor="#00000000"
|
||||
android:strokeColor="#5E5E5E"/>
|
||||
<path
|
||||
android:pathData="M8,15.75H18"
|
||||
android:strokeWidth="1.5"
|
||||
android:fillColor="#00000000"
|
||||
android:strokeColor="#5E5E5E"/>
|
||||
</vector>
|
30
res/drawable/ic_list_solid_20.xml
Normal file
30
res/drawable/ic_list_solid_20.xml
Normal file
|
@ -0,0 +1,30 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="20dp"
|
||||
android:height="20dp"
|
||||
android:viewportWidth="20"
|
||||
android:viewportHeight="20">
|
||||
<path
|
||||
android:pathData="M3.25,7.75L5.25,7.75A1.25,1.25 0,0 1,6.5 9L6.5,11A1.25,1.25 0,0 1,5.25 12.25L3.25,12.25A1.25,1.25 0,0 1,2 11L2,9A1.25,1.25 0,0 1,3.25 7.75z"
|
||||
android:fillColor="#DEDEDE"/>
|
||||
<path
|
||||
android:pathData="M3.25,13.5L5.25,13.5A1.25,1.25 0,0 1,6.5 14.75L6.5,16.75A1.25,1.25 0,0 1,5.25 18L3.25,18A1.25,1.25 0,0 1,2 16.75L2,14.75A1.25,1.25 0,0 1,3.25 13.5z"
|
||||
android:fillColor="#DEDEDE"/>
|
||||
<path
|
||||
android:pathData="M3.25,2L5.25,2A1.25,1.25 0,0 1,6.5 3.25L6.5,5.25A1.25,1.25 0,0 1,5.25 6.5L3.25,6.5A1.25,1.25 0,0 1,2 5.25L2,3.25A1.25,1.25 0,0 1,3.25 2z"
|
||||
android:fillColor="#DEDEDE"/>
|
||||
<path
|
||||
android:pathData="M8,4.25H18"
|
||||
android:strokeWidth="1.5"
|
||||
android:fillColor="#00000000"
|
||||
android:strokeColor="#DEDEDE"/>
|
||||
<path
|
||||
android:pathData="M8,10H18"
|
||||
android:strokeWidth="1.5"
|
||||
android:fillColor="#00000000"
|
||||
android:strokeColor="#DEDEDE"/>
|
||||
<path
|
||||
android:pathData="M8,15.75H18"
|
||||
android:strokeWidth="1.5"
|
||||
android:fillColor="#00000000"
|
||||
android:strokeColor="#DEDEDE"/>
|
||||
</vector>
|
13
res/drawable/media_overview_size_pill_background.xml
Normal file
13
res/drawable/media_overview_size_pill_background.xml
Normal file
|
@ -0,0 +1,13 @@
|
|||
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:dither="true"
|
||||
android:shape="rectangle">
|
||||
|
||||
<corners android:radius="10dp" />
|
||||
|
||||
<solid android:color="#99000000" />
|
||||
|
||||
<size
|
||||
android:width="50dp"
|
||||
android:height="20dp" />
|
||||
|
||||
</shape>
|
11
res/drawable/storage_legend_audio.xml
Normal file
11
res/drawable/storage_legend_audio.xml
Normal file
|
@ -0,0 +1,11 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:shape="oval">
|
||||
|
||||
<size
|
||||
android:width="@dimen/storage_legend_circle_size"
|
||||
android:height="@dimen/storage_legend_circle_size" />
|
||||
|
||||
<solid android:color="@color/storage_color_audio" />
|
||||
|
||||
</shape>
|
11
res/drawable/storage_legend_files.xml
Normal file
11
res/drawable/storage_legend_files.xml
Normal file
|
@ -0,0 +1,11 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:shape="oval">
|
||||
|
||||
<size
|
||||
android:width="@dimen/storage_legend_circle_size"
|
||||
android:height="@dimen/storage_legend_circle_size" />
|
||||
|
||||
<solid android:color="@color/storage_color_files" />
|
||||
|
||||
</shape>
|
11
res/drawable/storage_legend_photos.xml
Normal file
11
res/drawable/storage_legend_photos.xml
Normal file
|
@ -0,0 +1,11 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:shape="oval">
|
||||
|
||||
<size
|
||||
android:width="@dimen/storage_legend_circle_size"
|
||||
android:height="@dimen/storage_legend_circle_size" />
|
||||
|
||||
<solid android:color="@color/storage_color_photos" />
|
||||
|
||||
</shape>
|
11
res/drawable/storage_legend_videos.xml
Normal file
11
res/drawable/storage_legend_videos.xml
Normal file
|
@ -0,0 +1,11 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:shape="oval">
|
||||
|
||||
<size
|
||||
android:width="@dimen/storage_legend_circle_size"
|
||||
android:height="@dimen/storage_legend_circle_size" />
|
||||
|
||||
<solid android:color="@color/storage_color_videos" />
|
||||
|
||||
</shape>
|
|
@ -1,87 +1,43 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<merge 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"
|
||||
tools:context="org.thoughtcrime.securesms.components.AudioView">
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
tools:context="org.thoughtcrime.securesms.components.AudioView">
|
||||
|
||||
<LinearLayout android:id="@+id/audio_widget_container"
|
||||
android:orientation="vertical"
|
||||
android:layout_width="match_parent"
|
||||
tools:background="#ff00ff"
|
||||
android:layout_height="wrap_content">
|
||||
<LinearLayout
|
||||
android:id="@+id/audio_widget_container"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
tools:background="#ff00ff">
|
||||
|
||||
<LinearLayout android:layout_width="fill_parent"
|
||||
android:layout_height="fill_parent"
|
||||
android:orientation="horizontal">
|
||||
<LinearLayout
|
||||
android:layout_width="fill_parent"
|
||||
android:layout_height="fill_parent"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<org.thoughtcrime.securesms.components.AnimatingToggle
|
||||
android:id="@+id/control_toggle"
|
||||
android:layout_width="48dp"
|
||||
android:layout_height="48dp"
|
||||
android:layout_gravity="center"
|
||||
android:gravity="center">
|
||||
<include layout="@layout/audio_view_circle" />
|
||||
|
||||
<FrameLayout
|
||||
android:id="@+id/progress_and_play"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<com.pnikosis.materialishprogress.ProgressWheel
|
||||
android:id="@+id/download_progress"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_gravity="center"
|
||||
app:matProg_barColor="@color/white"
|
||||
app:matProg_linearProgress="true"
|
||||
app:matProg_spinSpeed="0.333" />
|
||||
|
||||
<com.airbnb.lottie.LottieAnimationView
|
||||
android:id="@+id/play"
|
||||
android:layout_width="48dp"
|
||||
android:layout_height="48dp"
|
||||
android:layout_gravity="center"
|
||||
android:background="@drawable/circle_touch_highlight_background"
|
||||
android:contentDescription="@string/audio_view__play_pause_accessibility_description"
|
||||
android:gravity="center_vertical"
|
||||
android:padding="12dp"
|
||||
android:visibility="gone"
|
||||
app:lottie_rawRes="@raw/lottie_play_pause"
|
||||
tools:visibility="visible" />
|
||||
|
||||
</FrameLayout>
|
||||
|
||||
<ImageView android:id="@+id/download"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center_vertical"
|
||||
android:clickable="true"
|
||||
android:visibility="gone"
|
||||
android:background="@drawable/circle_touch_highlight_background"
|
||||
android:src="@drawable/ic_download_circle_fill_white_48dp"
|
||||
android:contentDescription="@string/audio_view__download_accessibility_description"/>
|
||||
|
||||
</org.thoughtcrime.securesms.components.AnimatingToggle>
|
||||
|
||||
<SeekBar android:id="@+id/seek"
|
||||
android:layout_width="fill_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center_vertical"/>
|
||||
<SeekBar
|
||||
android:id="@+id/seek"
|
||||
android:layout_width="fill_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center_vertical" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<TextView android:id="@+id/timestamp"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="76dip"
|
||||
android:textAppearance="?android:attr/textAppearanceSmall"
|
||||
android:textColor="?conversation_item_sent_text_secondary_color"
|
||||
android:textSize="@dimen/conversation_item_date_text_size"
|
||||
android:fontFamily="sans-serif-light"
|
||||
android:autoLink="none"
|
||||
android:visibility="gone"
|
||||
tools:text="00:15"
|
||||
tools:visibility="visible"
|
||||
/>
|
||||
<TextView
|
||||
android:id="@+id/timestamp"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="76dip"
|
||||
android:autoLink="none"
|
||||
android:fontFamily="sans-serif-light"
|
||||
android:textAppearance="?android:attr/textAppearanceSmall"
|
||||
android:textColor="?conversation_item_sent_text_secondary_color"
|
||||
android:textSize="@dimen/conversation_item_date_text_size"
|
||||
android:visibility="gone"
|
||||
tools:text="00:15"
|
||||
tools:visibility="visible" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
|
|
53
res/layout/audio_view_circle.xml
Normal file
53
res/layout/audio_view_circle.xml
Normal file
|
@ -0,0 +1,53 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<org.thoughtcrime.securesms.components.AnimatingToggle xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:id="@+id/control_toggle"
|
||||
android:layout_width="48dp"
|
||||
android:layout_height="48dp"
|
||||
android:layout_gravity="center"
|
||||
android:gravity="center"
|
||||
tools:showIn="@layout/audio_view">
|
||||
|
||||
<FrameLayout
|
||||
android:id="@+id/progress_and_play"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<com.pnikosis.materialishprogress.ProgressWheel
|
||||
android:id="@+id/circle_progress"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_gravity="center"
|
||||
app:matProg_barColor="@color/white"
|
||||
app:matProg_linearProgress="true"
|
||||
app:matProg_spinSpeed="0.333" />
|
||||
|
||||
<com.airbnb.lottie.LottieAnimationView
|
||||
android:id="@+id/play"
|
||||
android:layout_width="48dp"
|
||||
android:layout_height="48dp"
|
||||
android:layout_gravity="center"
|
||||
android:background="@drawable/circle_touch_highlight_background"
|
||||
android:contentDescription="@string/audio_view__play_pause_accessibility_description"
|
||||
android:gravity="center_vertical"
|
||||
android:padding="12dp"
|
||||
android:visibility="gone"
|
||||
app:lottie_rawRes="@raw/lottie_play_pause"
|
||||
tools:visibility="visible" />
|
||||
|
||||
</FrameLayout>
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/download"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center_vertical"
|
||||
android:background="@drawable/circle_touch_highlight_background"
|
||||
android:clickable="true"
|
||||
android:contentDescription="@string/audio_view__download_accessibility_description"
|
||||
android:focusable="true"
|
||||
android:src="@drawable/ic_download_circle_fill_white_48dp"
|
||||
android:visibility="gone" />
|
||||
|
||||
</org.thoughtcrime.securesms.components.AnimatingToggle>
|
24
res/layout/audio_view_small.xml
Normal file
24
res/layout/audio_view_small.xml
Normal file
|
@ -0,0 +1,24 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<merge xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
tools:context="org.thoughtcrime.securesms.components.AudioView">
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/audio_widget_container"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
tools:background="#ff00ff">
|
||||
|
||||
<include layout="@layout/audio_view_circle" />
|
||||
|
||||
<SeekBar
|
||||
android:id="@+id/seek"
|
||||
android:layout_width="fill_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center_vertical"
|
||||
android:visibility="gone" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</merge>
|
|
@ -1,39 +1,121 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.coordinatorlayout.widget.CoordinatorLayout
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_width="match_parent"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<com.google.android.material.appbar.AppBarLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content">
|
||||
android:id="@+id/appBarLayout"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent">
|
||||
|
||||
<androidx.appcompat.widget.Toolbar
|
||||
android:id="@+id/toolbar"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="?android:attr/actionBarSize"
|
||||
android:background="?attr/media_overview_toolbar_background"
|
||||
android:titleTextColor="?attr/media_overview_toolbar_foreground"
|
||||
app:layout_scrollFlags="scroll|enterAlways"/>
|
||||
android:id="@+id/toolbar"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="?android:attr/actionBarSize"
|
||||
android:background="?attr/media_overview_toolbar_background"
|
||||
android:titleTextColor="?attr/media_overview_toolbar_foreground"
|
||||
app:layout_scrollFlags="scroll|enterAlways" />
|
||||
|
||||
<org.thoughtcrime.securesms.components.ControllableTabLayout
|
||||
android:id="@+id/tab_layout"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="top"
|
||||
android:background="?attr/media_overview_toolbar_background"
|
||||
app:tabBackground="?attr/media_overview_toolbar_background"
|
||||
app:tabIndicatorColor="@color/textsecure_primary"
|
||||
app:tabSelectedTextColor="@color/textsecure_primary"/>
|
||||
android:id="@+id/tab_layout"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="top"
|
||||
android:background="?attr/media_overview_toolbar_background"
|
||||
app:tabBackground="?attr/media_overview_toolbar_background"
|
||||
app:tabIndicatorColor="@color/textsecure_primary"
|
||||
app:tabSelectedTextColor="@color/textsecure_primary" />
|
||||
|
||||
</com.google.android.material.appbar.AppBarLayout>
|
||||
|
||||
<org.thoughtcrime.securesms.components.ControllableViewPager
|
||||
android:id="@+id/pager"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
app:layout_behavior="@string/appbar_scrolling_view_behavior"/>
|
||||
<View
|
||||
android:id="@+id/sorting"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="32dp"
|
||||
android:background="?attr/media_overview_toolbar_secondary_background"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/appBarLayout" />
|
||||
|
||||
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
||||
<TextView
|
||||
android:id="@+id/sort_order"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="0dp"
|
||||
android:layout_marginStart="16dp"
|
||||
android:gravity="center_vertical"
|
||||
android:textColor="?attr/media_overview_toolbar_foreground"
|
||||
app:layout_constraintBottom_toBottomOf="@+id/sorting"
|
||||
app:layout_constraintStart_toStartOf="@+id/sorting"
|
||||
app:layout_constraintTop_toBottomOf="@+id/appBarLayout"
|
||||
tools:text="@string/MediaOverviewActivity_Storage_used" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/sort_order_arrow"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="0dp"
|
||||
android:importantForAccessibility="no"
|
||||
android:padding="4dp"
|
||||
android:tint="?attr/media_overview_toolbar_foreground"
|
||||
app:layout_constraintBottom_toBottomOf="@+id/sort_order"
|
||||
app:layout_constraintStart_toEndOf="@+id/sort_order"
|
||||
app:layout_constraintTop_toTopOf="@+id/sort_order"
|
||||
app:srcCompat="@drawable/ic_arrow_down_14" />
|
||||
|
||||
<org.thoughtcrime.securesms.components.AnimatingToggle
|
||||
android:id="@+id/grid_list_toggle"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="0dp"
|
||||
android:layout_gravity="center"
|
||||
android:gravity="center"
|
||||
android:paddingStart="16dp"
|
||||
android:paddingEnd="16dp"
|
||||
app:layout_constraintBottom_toBottomOf="@+id/sorting"
|
||||
app:layout_constraintEnd_toEndOf="@+id/sorting"
|
||||
app:layout_constraintTop_toBottomOf="@+id/appBarLayout">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/view_grid"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center_vertical"
|
||||
android:background="@drawable/circle_touch_highlight_background"
|
||||
android:clickable="true"
|
||||
android:contentDescription="@string/MediaOverviewActivity_Grid_view_description"
|
||||
android:focusable="true"
|
||||
android:gravity="center_vertical"
|
||||
android:tint="?attr/media_overview_toolbar_foreground"
|
||||
android:visibility="visible"
|
||||
app:srcCompat="?attr/media_overview_grid_view_icon" />
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/view_detail"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center_vertical"
|
||||
android:background="@drawable/circle_touch_highlight_background"
|
||||
android:clickable="true"
|
||||
android:contentDescription="@string/MediaOverviewActivity_List_view_description"
|
||||
android:focusable="true"
|
||||
android:gravity="center_vertical"
|
||||
android:tint="?attr/media_overview_toolbar_foreground"
|
||||
android:visibility="gone"
|
||||
app:srcCompat="?attr/media_overview_list_view_icon" />
|
||||
|
||||
</org.thoughtcrime.securesms.components.AnimatingToggle>
|
||||
|
||||
<org.thoughtcrime.securesms.components.ControllableViewPager
|
||||
android:id="@+id/pager"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
app:layout_behavior="@string/appbar_scrolling_view_behavior"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/sorting" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
|
29
res/layout/media_overview_detail_item_audio.xml
Normal file
29
res/layout/media_overview_detail_item_audio.xml
Normal file
|
@ -0,0 +1,29 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.constraintlayout.widget.ConstraintLayout 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="wrap_content">
|
||||
|
||||
<FrameLayout
|
||||
android:id="@+id/image_container"
|
||||
android:layout_width="48dp"
|
||||
android:layout_height="48dp"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_marginTop="8dp"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent">
|
||||
|
||||
<org.thoughtcrime.securesms.components.AudioView
|
||||
android:id="@+id/audio"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
app:autoRewind="true"
|
||||
app:small="true" />
|
||||
|
||||
<include layout="@layout/media_overview_selected_overlay" />
|
||||
|
||||
</FrameLayout>
|
||||
|
||||
<include layout="@layout/media_overview_detail_text" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
56
res/layout/media_overview_detail_item_document.xml
Normal file
56
res/layout/media_overview_detail_item_document.xml
Normal file
|
@ -0,0 +1,56 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<FrameLayout
|
||||
android:id="@+id/image_container"
|
||||
android:layout_width="48dp"
|
||||
android:layout_height="48dp"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_marginTop="8dp"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent">
|
||||
|
||||
<FrameLayout
|
||||
android:id="@+id/document_icon_container"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
android:clickable="false"
|
||||
android:focusable="false"
|
||||
android:gravity="center">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/icon"
|
||||
android:layout_width="38dp"
|
||||
android:layout_height="50dp"
|
||||
android:importantForAccessibility="no"
|
||||
android:src="?attachment_document_icon_large" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/document_extension"
|
||||
style="@style/Signal.Text.Caption"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
android:clickable="false"
|
||||
android:gravity="center"
|
||||
android:scaleType="centerInside"
|
||||
android:textAlignment="center"
|
||||
android:textColor="@color/core_black"
|
||||
android:textSize="10sp"
|
||||
android:visibility="visible"
|
||||
tools:text="PDF"
|
||||
tools:visibility="visible" />
|
||||
</FrameLayout>
|
||||
|
||||
<include layout="@layout/media_overview_selected_overlay" />
|
||||
|
||||
</FrameLayout>
|
||||
|
||||
<include layout="@layout/media_overview_detail_text" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
28
res/layout/media_overview_detail_item_media.xml
Normal file
28
res/layout/media_overview_detail_item_media.xml
Normal file
|
@ -0,0 +1,28 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.constraintlayout.widget.ConstraintLayout 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="wrap_content">
|
||||
|
||||
<FrameLayout
|
||||
android:id="@+id/image_container"
|
||||
android:layout_width="48dp"
|
||||
android:layout_height="48dp"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_marginTop="8dp"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent">
|
||||
|
||||
<org.thoughtcrime.securesms.components.ThumbnailView
|
||||
android:id="@+id/image"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:contentDescription="@string/media_preview_activity__media_content_description" />
|
||||
|
||||
<include layout="@layout/media_overview_selected_overlay" />
|
||||
|
||||
</FrameLayout>
|
||||
|
||||
<include layout="@layout/media_overview_detail_text" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
46
res/layout/media_overview_detail_text.xml
Normal file
46
res/layout/media_overview_detail_text.xml
Normal file
|
@ -0,0 +1,46 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toEndOf="@+id/image_container"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
tools:showIn="@layout/media_overview_detail_item_media">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/line1"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:ellipsize="end"
|
||||
android:gravity="center"
|
||||
android:maxLines="1"
|
||||
android:paddingStart="6dp"
|
||||
android:paddingEnd="6dp"
|
||||
android:textSize="14sp"
|
||||
android:visibility="gone"
|
||||
app:layout_constraintStart_toEndOf="@+id/image_container"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
tools:text="Sent voice note, 02:35"
|
||||
tools:visibility="visible" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/line2"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:ellipsize="end"
|
||||
android:gravity="center"
|
||||
android:maxLines="1"
|
||||
android:paddingStart="6dp"
|
||||
android:paddingEnd="6dp"
|
||||
android:textSize="14sp"
|
||||
android:visibility="gone"
|
||||
app:layout_constraintStart_toEndOf="@+id/image_container"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
tools:text="2.7 MB, 11.06.19 at 5:25 AM"
|
||||
tools:visibility="visible" />
|
||||
|
||||
</LinearLayout>
|
|
@ -1,32 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:orientation="horizontal"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:padding="16dp">
|
||||
|
||||
<org.thoughtcrime.securesms.components.DocumentView
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:id="@+id/document_view"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center_vertical"
|
||||
android:layout_weight="1"
|
||||
android:visibility="visible"
|
||||
app:doc_titleColor="?media_overview_document_primary"
|
||||
app:doc_captionColor="?media_overview_document_secondary"
|
||||
tools:visibility="visible"/>
|
||||
|
||||
<TextView android:id="@+id/date"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center_vertical"
|
||||
android:textSize="12sp"
|
||||
android:textColor="?media_overview_document_secondary"
|
||||
android:paddingTop="20dp"
|
||||
tools:text="Jun 1"/>
|
||||
|
||||
</LinearLayout>
|
|
@ -1,22 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<RelativeLayout 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"
|
||||
android:background="?attr/media_overview_toolbar_background">
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/recycler_view"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:scrollbars="vertical" />
|
||||
|
||||
<TextView android:id="@+id/no_documents"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:textSize="24sp"
|
||||
android:gravity="center"
|
||||
android:visibility="gone"
|
||||
android:text="@string/media_overview_documents_fragment__no_documents_found" />
|
||||
|
||||
</RelativeLayout>
|
|
@ -1,22 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<RelativeLayout 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"
|
||||
android:background="?attr/media_overview_toolbar_background">
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/media_grid"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:scrollbars="vertical" />
|
||||
|
||||
<TextView android:id="@+id/no_images"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:textSize="24sp"
|
||||
android:gravity="center"
|
||||
android:visibility="gone"
|
||||
android:text="@string/media_overview_activity__no_media" />
|
||||
|
||||
</RelativeLayout>
|
|
@ -1,28 +1,50 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<org.thoughtcrime.securesms.components.SquareFrameLayout
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:padding="2dp">
|
||||
<org.thoughtcrime.securesms.components.SquareFrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<org.thoughtcrime.securesms.components.ThumbnailView
|
||||
android:id="@+id/image"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:contentDescription="@string/media_preview_activity__media_content_description" />
|
||||
android:id="@+id/image"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:contentDescription="@string/media_preview_activity__media_content_description"
|
||||
android:padding="2dp" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/image_file_size"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="end|bottom"
|
||||
android:layout_margin="4dp"
|
||||
android:background="@drawable/media_overview_size_pill_background"
|
||||
android:gravity="center"
|
||||
android:paddingStart="6dp"
|
||||
android:paddingEnd="6dp"
|
||||
android:textColor="@color/white"
|
||||
android:textSize="12sp"
|
||||
android:visibility="gone"
|
||||
tools:text="1.3 MB"
|
||||
tools:visibility="visible" />
|
||||
|
||||
<FrameLayout
|
||||
android:id="@+id/selected_indicator"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:background="@color/MediaOverview_Media_selected_overlay"
|
||||
android:visibility="gone">
|
||||
android:visibility="gone"
|
||||
tools:showIn="@layout/media_overview_gallery_item"
|
||||
tools:visibility="visible">
|
||||
|
||||
<ImageView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:src="@drawable/check"
|
||||
android:layout_gravity="center"/>
|
||||
android:layout_marginTop="8dp"
|
||||
android:layout_marginStart="8dp"
|
||||
android:contentDescription="@string/MediaOverviewActivity_Selected_description"
|
||||
app:srcCompat="@drawable/ic_check_circle_solid_20" />
|
||||
|
||||
</FrameLayout>
|
||||
|
||||
</org.thoughtcrime.securesms.components.SquareFrameLayout>
|
||||
|
|
|
@ -1,18 +0,0 @@
|
|||
<?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:background="?attr/media_overview_toolbar_background"
|
||||
android:padding="16dp">
|
||||
|
||||
<TextView android:id="@+id/text"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="start|center_vertical"
|
||||
android:textColor="?attr/media_overview_header_foreground"
|
||||
android:textSize="14sp"
|
||||
android:textStyle="bold"
|
||||
android:textAllCaps="true"
|
||||
tools:text="March 1, 2015" />
|
||||
</FrameLayout>
|
22
res/layout/media_overview_page_fragment.xml
Normal file
22
res/layout/media_overview_page_fragment.xml
Normal file
|
@ -0,0 +1,22 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:background="?attr/media_overview_toolbar_background">
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/media_grid"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:scrollbars="vertical" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/no_images"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:gravity="center"
|
||||
android:text="@string/media_overview_activity__no_media"
|
||||
android:textSize="24sp"
|
||||
android:visibility="gone" />
|
||||
|
||||
</RelativeLayout>
|
20
res/layout/media_overview_selected_overlay.xml
Normal file
20
res/layout/media_overview_selected_overlay.xml
Normal file
|
@ -0,0 +1,20 @@
|
|||
<?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"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:id="@+id/selected_indicator"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:background="?attr/media_overview_toolbar_background"
|
||||
android:visibility="gone"
|
||||
tools:showIn="@layout/media_overview_detail_item_document"
|
||||
tools:visibility="visible">
|
||||
|
||||
<ImageView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
android:contentDescription="@string/MediaOverviewActivity_Selected_description"
|
||||
app:srcCompat="@drawable/ic_check_circle_solid_20" />
|
||||
|
||||
</FrameLayout>
|
98
res/layout/preference_storage_category.xml
Normal file
98
res/layout/preference_storage_category.xml
Normal file
|
@ -0,0 +1,98 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/total_size"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:textSize="28sp"
|
||||
app:layout_constraintEnd_toEndOf="@+id/storageGraphView"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
tools:text="1.2GB" />
|
||||
|
||||
<org.thoughtcrime.securesms.preferences.widgets.StorageGraphView
|
||||
android:id="@+id/storageGraphView"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="28dp"
|
||||
android:layout_marginStart="32dp"
|
||||
android:layout_marginTop="16dp"
|
||||
android:layout_marginEnd="32dp"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintHorizontal_bias="0.5"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/total_size" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/legend_photos"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="8dp"
|
||||
android:drawableStart="@drawable/storage_legend_photos"
|
||||
android:drawablePadding="4dp"
|
||||
android:text="@string/preferences_storage__photos"
|
||||
android:textSize="12sp"
|
||||
app:layout_constraintEnd_toStartOf="@+id/legend_videos"
|
||||
app:layout_constraintHorizontal_bias="0.5"
|
||||
app:layout_constraintStart_toStartOf="@+id/storageGraphView"
|
||||
app:layout_constraintTop_toBottomOf="@+id/storageGraphView" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/legend_videos"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:drawableStart="@drawable/storage_legend_videos"
|
||||
android:drawablePadding="4dp"
|
||||
android:text="@string/preferences_storage__videos"
|
||||
android:textSize="12sp"
|
||||
app:layout_constraintEnd_toStartOf="@+id/legend_audio"
|
||||
app:layout_constraintHorizontal_bias="0.5"
|
||||
app:layout_constraintStart_toEndOf="@+id/legend_photos"
|
||||
app:layout_constraintTop_toTopOf="@+id/legend_photos" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/legend_audio"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:drawableStart="@drawable/storage_legend_files"
|
||||
android:drawablePadding="4dp"
|
||||
android:text="@string/preferences_storage__files"
|
||||
android:textSize="12sp"
|
||||
app:layout_constraintEnd_toStartOf="@+id/legend_files"
|
||||
app:layout_constraintHorizontal_bias="0.5"
|
||||
app:layout_constraintStart_toEndOf="@+id/legend_videos"
|
||||
app:layout_constraintTop_toTopOf="@+id/legend_videos" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/legend_files"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:drawableStart="@drawable/storage_legend_audio"
|
||||
android:drawablePadding="4dp"
|
||||
android:text="@string/preferences_storage__audio"
|
||||
android:textSize="12sp"
|
||||
app:layout_constraintEnd_toEndOf="@+id/storageGraphView"
|
||||
app:layout_constraintHorizontal_bias="0.5"
|
||||
app:layout_constraintStart_toEndOf="@+id/legend_audio"
|
||||
app:layout_constraintTop_toTopOf="@+id/legend_audio" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/free_up_space"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="16dp"
|
||||
android:layout_marginBottom="16dp"
|
||||
android:text="@string/preferences_storage__review_storage"
|
||||
android:textAllCaps="true"
|
||||
android:textColor="@color/signal_primary_dark"
|
||||
android:textSize="16sp"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintHorizontal_bias="0.5"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/legend_photos" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
|
@ -30,7 +30,9 @@
|
|||
android:layout_height="wrap_content"
|
||||
android:padding="6dp"
|
||||
android:src="@drawable/ic_caption_28"
|
||||
android:visibility="gone" />
|
||||
android:visibility="gone"
|
||||
android:contentDescription="@string/ThumbnailView_Has_a_caption_description"
|
||||
tools:visibility="visible" />
|
||||
|
||||
<FrameLayout
|
||||
android:id="@+id/play_overlay"
|
||||
|
@ -43,12 +45,13 @@
|
|||
tools:visibility="visible">
|
||||
|
||||
<ImageView
|
||||
android:layout_width="24dp"
|
||||
android:layout_height="24dp"
|
||||
android:layout_gravity="center"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_margin="12dp"
|
||||
android:contentDescription="@string/ThumbnailView_Play_video_description"
|
||||
android:paddingLeft="5dp"
|
||||
android:tint="@color/core_blue"
|
||||
android:scaleType="fitXY"
|
||||
android:tint="@color/core_blue"
|
||||
app:srcCompat="@drawable/triangle_right"
|
||||
tools:ignore="RtlHardcoded,RtlSymmetry" />
|
||||
|
||||
|
|
|
@ -4,17 +4,17 @@
|
|||
<item android:id="@+id/media_preview__forward"
|
||||
android:title="@string/media_preview__forward_title"
|
||||
android:icon="?menu_forward_icon"
|
||||
app:showAsAction="always"/>
|
||||
app:showAsAction="never"/>
|
||||
<item android:id="@+id/save"
|
||||
android:title="@string/media_preview__save_title"
|
||||
android:icon="@drawable/ic_download_filled_white_24"
|
||||
app:showAsAction="always"/>
|
||||
app:showAsAction="never"/>
|
||||
<item android:id="@+id/media_preview__overview"
|
||||
android:title="@string/media_preview__all_media_title"
|
||||
android:icon="@drawable/ic_photo_library_white_24dp"
|
||||
app:showAsAction="ifRoom"/>
|
||||
app:showAsAction="never"/>
|
||||
<item android:id="@+id/delete"
|
||||
android:title="@string/delete"
|
||||
android:icon="?menu_trash_icon"
|
||||
app:showAsAction="ifRoom"/>
|
||||
app:showAsAction="never"/>
|
||||
</menu>
|
||||
|
|
|
@ -324,4 +324,10 @@
|
|||
<item>2</item>
|
||||
</string-array>
|
||||
|
||||
<string-array name="MediaOverviewActivity_Sort_by">
|
||||
<item>@string/MediaOverviewActivity_Newest</item>
|
||||
<item>@string/MediaOverviewActivity_Oldest</item>
|
||||
<item>@string/MediaOverviewActivity_Storage_used</item>
|
||||
</string-array>
|
||||
|
||||
</resources>
|
||||
|
|
|
@ -185,6 +185,7 @@
|
|||
<attr name="privacy_icon" format="reference" />
|
||||
<attr name="appearance_icon" format="reference" />
|
||||
<attr name="chats_and_media_icon" format="reference" />
|
||||
<attr name="data_and_storage_icon" format="reference" />
|
||||
<attr name="linked_devices_icon" format="reference" />
|
||||
<attr name="advanced_icon" format="reference" />
|
||||
<attr name="safety_number_icon" format="reference" />
|
||||
|
@ -212,9 +213,12 @@
|
|||
|
||||
<attr name="media_overview_toolbar_background" format="color"/>
|
||||
<attr name="media_overview_toolbar_foreground" format="color"/>
|
||||
<attr name="media_overview_toolbar_secondary_background" format="color"/>
|
||||
<attr name="media_overview_header_foreground" format="color"/>
|
||||
<attr name="media_overview_document_primary" format="color"/>
|
||||
<attr name="media_overview_document_secondary" format="color"/>
|
||||
<attr name="media_overview_grid_view_icon" format="reference"/>
|
||||
<attr name="media_overview_list_view_icon" format="reference"/>
|
||||
|
||||
<attr name="shared_contact_details_header_background" format="color"/>
|
||||
<attr name="shared_contact_details_titlebar" format="color"/>
|
||||
|
@ -275,6 +279,8 @@
|
|||
<attr name="widgetBackground" format="color"/>
|
||||
<attr name="foregroundTintColor" format="color" />
|
||||
<attr name="backgroundTintColor" format="color" />
|
||||
<attr name="small" format="boolean" />
|
||||
<attr name="autoRewind" format="boolean" />
|
||||
</declare-styleable>
|
||||
|
||||
<declare-styleable name="CircleColorImageView">
|
||||
|
|
|
@ -46,7 +46,7 @@
|
|||
<color name="transparent">#00FFFFFF</color>
|
||||
<color name="transparent_black">#00000000</color>
|
||||
|
||||
<color name="MediaOverview_Media_selected_overlay">#88000000</color>
|
||||
<color name="MediaOverview_Media_selected_overlay">#99000000</color>
|
||||
|
||||
<color name="logsubmit_confirmation_background">#44ff2d00</color>
|
||||
|
||||
|
@ -55,4 +55,10 @@
|
|||
<color name="media_preview_bar_background">#90010101</color>
|
||||
<color name="media_preview_bar_shade_center">#88000000</color>
|
||||
|
||||
<color name="storage_color_empty">@color/core_grey_50</color>
|
||||
<color name="storage_color_photos">#ff5951C8</color>
|
||||
<color name="storage_color_videos">#ff9BCFBD</color>
|
||||
<color name="storage_color_audio">#ff2090EA</color>
|
||||
<color name="storage_color_files">#ffA23474</color>
|
||||
|
||||
</resources>
|
||||
|
|
|
@ -148,4 +148,6 @@
|
|||
<dimen name="conversation_reaction_scrub_vertical_translation">20dp</dimen>
|
||||
<dimen name="conversation_reaction_scrub_horizontal_margin">16dp</dimen>
|
||||
|
||||
<dimen name="storage_legend_circle_size">8dp</dimen>
|
||||
|
||||
</resources>
|
||||
|
|
|
@ -88,6 +88,9 @@
|
|||
<string name="BucketedThreadMedia_Yesterday">Yesterday</string>
|
||||
<string name="BucketedThreadMedia_This_week">This week</string>
|
||||
<string name="BucketedThreadMedia_This_month">This month</string>
|
||||
<string name="BucketedThreadMedia_Large">Large</string>
|
||||
<string name="BucketedThreadMedia_Medium">Medium</string>
|
||||
<string name="BucketedThreadMedia_Small">Small</string>
|
||||
|
||||
<!-- CallScreen -->
|
||||
<string name="CallScreen_Incoming_call">Incoming call</string>
|
||||
|
@ -351,6 +354,10 @@
|
|||
|
||||
<!-- DocumentView -->
|
||||
<string name="DocumentView_unknown_file">Unknown file</string>
|
||||
<string name="DocumentView_unnamed_file">Unnamed file</string>
|
||||
<string name="DocumentView_audio_file">Audio file</string>
|
||||
<string name="DocumentView_image_file">Image file</string>
|
||||
<string name="DocumentView_video_file">Video file</string>
|
||||
|
||||
<!-- DozeReminder -->
|
||||
<string name="DozeReminder_optimize_for_missing_play_services">Optimize for missing Play Services</string>
|
||||
|
@ -480,18 +487,28 @@
|
|||
<!-- MediaOverviewActivity -->
|
||||
<string name="MediaOverviewActivity_Media">Media</string>
|
||||
<plurals name="MediaOverviewActivity_Media_delete_confirm_title">
|
||||
<item quantity="one">Delete selected message?</item>
|
||||
<item quantity="other">Delete selected messages?</item>
|
||||
<item quantity="one">Delete selected item?</item>
|
||||
<item quantity="other">Delete selected items?</item>
|
||||
</plurals>
|
||||
<plurals name="MediaOverviewActivity_Media_delete_confirm_message">
|
||||
<item quantity="one">This will permanently delete the selected message.</item>
|
||||
<item quantity="other">This will permanently delete all %1$d selected messages.</item>
|
||||
<item quantity="one">This will permanently delete the selected file. Any message text associated with this item will also be deleted.</item>
|
||||
<item quantity="other">This will permanently delete all %1$d selected files. Any message text associated with these items will also be deleted.</item>
|
||||
</plurals>
|
||||
<string name="MediaOverviewActivity_Media_delete_progress_title">Deleting</string>
|
||||
<string name="MediaOverviewActivity_Media_delete_progress_message">Deleting messages…</string>
|
||||
<string name="MediaOverviewActivity_Documents">Documents</string>
|
||||
<string name="MediaOverviewActivity_Files">Files</string>
|
||||
<string name="MediaOverviewActivity_Select_all">Select all</string>
|
||||
<string name="MediaOverviewActivity_collecting_attachments">Collecting attachments…</string>
|
||||
<string name="MediaOverviewActivity_Audio">Audio</string>
|
||||
<string name="MediaOverviewActivity_All">All</string>
|
||||
<string name="MediaOverviewActivity_Sort_by">Sort by</string>
|
||||
<string name="MediaOverviewActivity_Newest">Newest</string>
|
||||
<string name="MediaOverviewActivity_Oldest">Oldest</string>
|
||||
<string name="MediaOverviewActivity_Storage_used">Storage used</string>
|
||||
<string name="MediaOverviewActivity_All_storage_use">All storage use</string>
|
||||
<string name="MediaOverviewActivity_Grid_view_description">Grid view</string>
|
||||
<string name="MediaOverviewActivity_List_view_description">List view</string>
|
||||
<string name="MediaOverviewActivity_Selected_description">Selected</string>
|
||||
|
||||
<!--- NotificationBarManager -->
|
||||
<string name="NotificationBarManager_signal_call_in_progress">Signal call in progress</string>
|
||||
|
@ -872,6 +889,7 @@
|
|||
<string name="MediaPreviewActivity_unable_to_write_to_external_storage_without_permission">Unable to save to external storage without permissions</string>
|
||||
<string name="MediaPreviewActivity_media_delete_confirmation_title">Delete message?</string>
|
||||
<string name="MediaPreviewActivity_media_delete_confirmation_message">This will permanently delete this message.</string>
|
||||
<string name="MediaPreviewActivity_s_to_s">%1$s to %2$s</string>
|
||||
|
||||
<!-- MessageNotifier -->
|
||||
<string name="MessageNotifier_d_new_messages_in_d_conversations">%1$d new messages in %2$d conversations</string>
|
||||
|
@ -930,6 +948,10 @@
|
|||
<string name="SingleRecipientNotificationBuilder_signal">Signal</string>
|
||||
<string name="SingleRecipientNotificationBuilder_new_message">New message</string>
|
||||
|
||||
<!-- ThumbnailView -->
|
||||
<string name="ThumbnailView_Play_video_description">Play video</string>
|
||||
<string name="ThumbnailView_Has_a_caption_description">Has a caption</string>
|
||||
|
||||
<!-- TransferControlView -->
|
||||
<plurals name="TransferControlView_n_items">
|
||||
<item quantity="one">%d Item</item>
|
||||
|
@ -1393,6 +1415,7 @@
|
|||
<string name="preferences__automatically_delete_older_messages_once_a_conversation_exceeds_a_specified_length">Automatically delete older messages once a conversation exceeds a specified length</string>
|
||||
<string name="preferences__delete_old_messages">Delete old messages</string>
|
||||
<string name="preferences__chats">Chats and media</string>
|
||||
<string name="preferences__storage">Storage</string>
|
||||
<string name="preferences__conversation_length_limit">Conversation length limit</string>
|
||||
<string name="preferences__trim_all_conversations_now">Trim all conversations now</string>
|
||||
<string name="preferences__scan_through_all_conversations_and_enforce_conversation_length_limits">Scan through all conversations and enforce conversation length limits</string>
|
||||
|
@ -1421,6 +1444,12 @@
|
|||
<string name="preferences_chats__when_roaming">When roaming</string>
|
||||
<string name="preferences_chats__media_auto_download">Media auto-download</string>
|
||||
<string name="preferences_chats__message_trimming">Message trimming</string>
|
||||
<string name="preferences_storage__storage_usage">Storage usage</string>
|
||||
<string name="preferences_storage__photos">Photos</string>
|
||||
<string name="preferences_storage__videos">Videos</string>
|
||||
<string name="preferences_storage__files">Files</string>
|
||||
<string name="preferences_storage__audio">Audio</string>
|
||||
<string name="preferences_storage__review_storage">Review storage</string>
|
||||
<string name="preferences_advanced__use_system_emoji">Use system emoji</string>
|
||||
<string name="preferences_advanced__disable_signal_built_in_emoji_support">Disable Signal\'s built-in emoji support</string>
|
||||
<string name="preferences_advanced__relay_all_calls_through_the_signal_server_to_avoid_revealing_your_ip_address">Relay all calls through the Signal server to avoid revealing your IP address to your contact. Enabling will reduce call quality.</string>
|
||||
|
@ -1576,9 +1605,6 @@
|
|||
<string name="media_preview__forward_title">Forward</string>
|
||||
<string name="media_preview__all_media_title">All media</string>
|
||||
|
||||
<!-- media_overview -->
|
||||
<string name="media_overview_documents_fragment__no_documents_found">No documents</string>
|
||||
|
||||
<!-- media_preview_activity -->
|
||||
<string name="media_preview_activity__media_content_description">Media preview</string>
|
||||
|
||||
|
|
|
@ -23,10 +23,13 @@
|
|||
<item name="contact_selection_header_text">@color/textsecure_primary_dark</item>
|
||||
|
||||
<item name="media_overview_toolbar_background">@color/white</item>
|
||||
<item name="media_overview_toolbar_secondary_background">@color/core_grey_02</item>
|
||||
<item name="media_overview_toolbar_foreground">@color/core_grey_70</item>
|
||||
<item name="media_overview_header_foreground">@color/core_grey_50</item>
|
||||
<item name="media_overview_document_primary">@color/core_grey_90</item>
|
||||
<item name="media_overview_document_secondary">@color/core_grey_60</item>
|
||||
<item name="media_overview_grid_view_icon">@drawable/ic_grid_outline_20</item>
|
||||
<item name="media_overview_list_view_icon">@drawable/ic_list_outline_20</item>
|
||||
</style>
|
||||
|
||||
<style name="TextSecure.DarkNoActionBar" parent="@style/TextSecure.BaseDarkNoActionBar">
|
||||
|
@ -59,10 +62,13 @@
|
|||
<item name="contact_selection_header_text">#66eeeeee</item>
|
||||
|
||||
<item name="media_overview_toolbar_background">@color/black</item>
|
||||
<item name="media_overview_toolbar_secondary_background">@color/core_grey_80</item>
|
||||
<item name="media_overview_toolbar_foreground">@color/white</item>
|
||||
<item name="media_overview_header_foreground">@color/core_grey_10</item>
|
||||
<item name="media_overview_document_primary">@color/core_grey_05</item>
|
||||
<item name="media_overview_document_secondary">@color/core_grey_25</item>
|
||||
<item name="media_overview_grid_view_icon">@drawable/ic_grid_solid_20</item>
|
||||
<item name="media_overview_list_view_icon">@drawable/ic_list_solid_20</item>
|
||||
</style>
|
||||
|
||||
<style name="Signal.Light.NoActionBar.Invite" parent="Base.Signal.Light.NoActionBar.Invite">
|
||||
|
@ -332,6 +338,7 @@
|
|||
<item name="privacy_icon">@drawable/ic_lock_outline_24</item>
|
||||
<item name="appearance_icon">@drawable/ic_appearance_outline_24</item>
|
||||
<item name="chats_and_media_icon">@drawable/ic_photo_outline_24</item>
|
||||
<item name="data_and_storage_icon">@drawable/ic_archive_outline_24dp</item>
|
||||
<item name="linked_devices_icon">@drawable/ic_linked_devices_24</item>
|
||||
<item name="advanced_icon">@drawable/ic_advanced_24</item>
|
||||
<item name="safety_number_icon">@drawable/ic_safety_number_outline_24</item>
|
||||
|
@ -572,6 +579,7 @@
|
|||
<item name="privacy_icon">@drawable/ic_lock_solid_24</item>
|
||||
<item name="appearance_icon">@drawable/ic_appearance_solid_24</item>
|
||||
<item name="chats_and_media_icon">@drawable/ic_photo_solid_24</item>
|
||||
<item name="data_and_storage_icon">@drawable/ic_archive_solid_24dp</item>
|
||||
<item name="linked_devices_icon">@drawable/ic_linked_devices_24</item>
|
||||
<item name="advanced_icon">@drawable/ic_advanced_24</item>
|
||||
<item name="safety_number_icon">@drawable/ic_safety_number_solid_24</item>
|
||||
|
|
|
@ -25,6 +25,10 @@
|
|||
android:title="@string/preferences__chats"
|
||||
android:icon="?attr/chats_and_media_icon"/>
|
||||
|
||||
<Preference android:key="preference_category_storage"
|
||||
android:title="@string/preferences__storage"
|
||||
android:icon="?attr/data_and_storage_icon"/>
|
||||
|
||||
<Preference android:key="preference_category_devices"
|
||||
android:title="@string/preferences__linked_devices"
|
||||
android:icon="?attr/linked_devices_icon"/>
|
||||
|
|
|
@ -61,27 +61,6 @@
|
|||
|
||||
<PreferenceCategory android:layout="@layout/preference_divider"/>
|
||||
|
||||
<PreferenceCategory android:key="message_trimming" android:title="@string/preferences_chats__message_trimming">
|
||||
<org.thoughtcrime.securesms.components.SwitchPreferenceCompat
|
||||
android:defaultValue="false"
|
||||
android:key="pref_trim_threads"
|
||||
android:summary="@string/preferences__automatically_delete_older_messages_once_a_conversation_exceeds_a_specified_length"
|
||||
android:title="@string/preferences__delete_old_messages" />
|
||||
|
||||
<EditTextPreference android:defaultValue="500"
|
||||
android:key="pref_trim_length"
|
||||
android:title="@string/preferences__conversation_length_limit"
|
||||
android:inputType="number"
|
||||
android:dependency="pref_trim_threads" />
|
||||
|
||||
<Preference android:key="pref_trim_now"
|
||||
android:title="@string/preferences__trim_all_conversations_now"
|
||||
android:summary="@string/preferences__scan_through_all_conversations_and_enforce_conversation_length_limits"
|
||||
android:dependency="pref_trim_threads" />
|
||||
</PreferenceCategory>
|
||||
|
||||
<PreferenceCategory android:layout="@layout/preference_divider"/>
|
||||
|
||||
<PreferenceCategory android:key="backup_category" android:title="@string/preferences_chats__backups">
|
||||
<org.thoughtcrime.securesms.components.SwitchPreferenceCompat
|
||||
android:defaultValue="false"
|
||||
|
|
35
res/xml/preferences_storage.xml
Normal file
35
res/xml/preferences_storage.xml
Normal file
|
@ -0,0 +1,35 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
|
||||
<PreferenceCategory android:title="@string/preferences_storage__storage_usage" />
|
||||
|
||||
<org.thoughtcrime.securesms.preferences.widgets.StoragePreferenceCategory android:key="pref_storage_category" />
|
||||
|
||||
<PreferenceCategory android:layout="@layout/preference_divider" />
|
||||
|
||||
<PreferenceCategory
|
||||
android:key="storage_limits"
|
||||
android:title="@string/preferences_chats__message_trimming">
|
||||
|
||||
<org.thoughtcrime.securesms.components.SwitchPreferenceCompat
|
||||
android:defaultValue="false"
|
||||
android:key="pref_trim_threads"
|
||||
android:summary="@string/preferences__automatically_delete_older_messages_once_a_conversation_exceeds_a_specified_length"
|
||||
android:title="@string/preferences__delete_old_messages" />
|
||||
|
||||
<EditTextPreference
|
||||
android:defaultValue="500"
|
||||
android:dependency="pref_trim_threads"
|
||||
android:inputType="number"
|
||||
android:key="pref_trim_length"
|
||||
android:title="@string/preferences__conversation_length_limit" />
|
||||
|
||||
<Preference
|
||||
android:dependency="pref_trim_threads"
|
||||
android:key="pref_trim_now"
|
||||
android:summary="@string/preferences__scan_through_all_conversations_and_enforce_conversation_length_limits"
|
||||
android:title="@string/preferences__trim_all_conversations_now" />
|
||||
|
||||
</PreferenceCategory>
|
||||
|
||||
</PreferenceScreen>
|
|
@ -36,6 +36,7 @@ import org.thoughtcrime.securesms.preferences.ChatsPreferenceFragment;
|
|||
import org.thoughtcrime.securesms.preferences.CorrectedPreferenceFragment;
|
||||
import org.thoughtcrime.securesms.preferences.NotificationsPreferenceFragment;
|
||||
import org.thoughtcrime.securesms.preferences.SmsMmsPreferenceFragment;
|
||||
import org.thoughtcrime.securesms.preferences.StoragePreferenceFragment;
|
||||
import org.thoughtcrime.securesms.preferences.widgets.ProfilePreference;
|
||||
import org.thoughtcrime.securesms.service.KeyCachingService;
|
||||
import org.thoughtcrime.securesms.usernames.ProfileEditActivityV2;
|
||||
|
@ -64,6 +65,7 @@ public class ApplicationPreferencesActivity extends PassphraseRequiredActionBarA
|
|||
private static final String PREFERENCE_CATEGORY_APP_PROTECTION = "preference_category_app_protection";
|
||||
private static final String PREFERENCE_CATEGORY_APPEARANCE = "preference_category_appearance";
|
||||
private static final String PREFERENCE_CATEGORY_CHATS = "preference_category_chats";
|
||||
private static final String PREFERENCE_CATEGORY_STORAGE = "preference_category_storage";
|
||||
private static final String PREFERENCE_CATEGORY_DEVICES = "preference_category_devices";
|
||||
private static final String PREFERENCE_CATEGORY_ADVANCED = "preference_category_advanced";
|
||||
|
||||
|
@ -149,6 +151,8 @@ public class ApplicationPreferencesActivity extends PassphraseRequiredActionBarA
|
|||
.setOnPreferenceClickListener(new CategoryClickListener(PREFERENCE_CATEGORY_APPEARANCE));
|
||||
this.findPreference(PREFERENCE_CATEGORY_CHATS)
|
||||
.setOnPreferenceClickListener(new CategoryClickListener(PREFERENCE_CATEGORY_CHATS));
|
||||
this.findPreference(PREFERENCE_CATEGORY_STORAGE)
|
||||
.setOnPreferenceClickListener(new CategoryClickListener(PREFERENCE_CATEGORY_STORAGE));
|
||||
this.findPreference(PREFERENCE_CATEGORY_DEVICES)
|
||||
.setOnPreferenceClickListener(new CategoryClickListener(PREFERENCE_CATEGORY_DEVICES));
|
||||
this.findPreference(PREFERENCE_CATEGORY_ADVANCED)
|
||||
|
@ -227,6 +231,9 @@ public class ApplicationPreferencesActivity extends PassphraseRequiredActionBarA
|
|||
case PREFERENCE_CATEGORY_CHATS:
|
||||
fragment = new ChatsPreferenceFragment();
|
||||
break;
|
||||
case PREFERENCE_CATEGORY_STORAGE:
|
||||
fragment = new StoragePreferenceFragment();
|
||||
break;
|
||||
case PREFERENCE_CATEGORY_DEVICES:
|
||||
Intent intent = new Intent(getActivity(), DeviceActivity.class);
|
||||
startActivity(intent);
|
||||
|
|
|
@ -1,129 +0,0 @@
|
|||
package org.thoughtcrime.securesms;
|
||||
|
||||
|
||||
import android.content.ActivityNotFoundException;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.database.Cursor;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.TextView;
|
||||
import android.widget.Toast;
|
||||
|
||||
import org.thoughtcrime.securesms.MediaDocumentsAdapter.HeaderViewHolder;
|
||||
import org.thoughtcrime.securesms.MediaDocumentsAdapter.ViewHolder;
|
||||
import org.thoughtcrime.securesms.components.DocumentView;
|
||||
import org.thoughtcrime.securesms.database.CursorRecyclerViewAdapter;
|
||||
import org.thoughtcrime.securesms.database.MediaDatabase;
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
import org.thoughtcrime.securesms.mms.DocumentSlide;
|
||||
import org.thoughtcrime.securesms.mms.PartAuthority;
|
||||
import org.thoughtcrime.securesms.mms.Slide;
|
||||
import org.thoughtcrime.securesms.util.DateUtils;
|
||||
import org.thoughtcrime.securesms.util.MediaUtil;
|
||||
import org.thoughtcrime.securesms.util.StickyHeaderDecoration;
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
|
||||
import java.util.Calendar;
|
||||
import java.util.Date;
|
||||
import java.util.Locale;
|
||||
|
||||
import static com.codewaves.stickyheadergrid.StickyHeaderGridLayoutManager.TAG;
|
||||
|
||||
public class MediaDocumentsAdapter extends CursorRecyclerViewAdapter<ViewHolder> implements StickyHeaderDecoration.StickyHeaderAdapter<HeaderViewHolder> {
|
||||
|
||||
private final Calendar calendar;
|
||||
private final Locale locale;
|
||||
|
||||
MediaDocumentsAdapter(Context context, Cursor cursor, Locale locale) {
|
||||
super(context, cursor);
|
||||
|
||||
this.calendar = Calendar.getInstance();
|
||||
this.locale = locale;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ViewHolder onCreateItemViewHolder(ViewGroup parent, int viewType) {
|
||||
return new ViewHolder(LayoutInflater.from(getContext()).inflate(R.layout.media_overview_document_item, parent, false));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBindItemViewHolder(ViewHolder viewHolder, @NonNull Cursor cursor) {
|
||||
MediaDatabase.MediaRecord mediaRecord = MediaDatabase.MediaRecord.from(getContext(), cursor);
|
||||
Slide slide = MediaUtil.getSlideForAttachment(getContext(), mediaRecord.getAttachment());
|
||||
|
||||
if (slide != null && slide.hasDocument()) {
|
||||
viewHolder.documentView.setDocument((DocumentSlide)slide, false);
|
||||
viewHolder.date.setText(DateUtils.getRelativeDate(getContext(), locale, mediaRecord.getDate()));
|
||||
viewHolder.documentView.setVisibility(View.VISIBLE);
|
||||
viewHolder.date.setVisibility(View.VISIBLE);
|
||||
viewHolder.documentView.setOnClickListener(view -> {
|
||||
Intent intent = new Intent(Intent.ACTION_VIEW);
|
||||
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
|
||||
intent.setDataAndType(PartAuthority.getAttachmentPublicUri(slide.getUri()), slide.getContentType());
|
||||
try {
|
||||
getContext().startActivity(intent);
|
||||
} catch (ActivityNotFoundException anfe) {
|
||||
Log.w(TAG, "No activity existed to view the media.");
|
||||
Toast.makeText(getContext(), R.string.ConversationItem_unable_to_open_media, Toast.LENGTH_LONG).show();
|
||||
}
|
||||
});
|
||||
} else {
|
||||
viewHolder.documentView.setVisibility(View.GONE);
|
||||
viewHolder.date.setVisibility(View.GONE);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getHeaderId(int position) {
|
||||
if (!isActiveCursor()) return -1;
|
||||
if (isHeaderPosition(position)) return -1;
|
||||
if (isFooterPosition(position)) return -1;
|
||||
if (position >= getItemCount()) return -1;
|
||||
if (position < 0) return -1;
|
||||
|
||||
Cursor cursor = getCursorAtPositionOrThrow(position);
|
||||
MediaDatabase.MediaRecord mediaRecord = MediaDatabase.MediaRecord.from(getContext(), cursor);
|
||||
|
||||
calendar.setTime(new Date(mediaRecord.getDate()));
|
||||
return Util.hashCode(calendar.get(Calendar.YEAR), calendar.get(Calendar.DAY_OF_YEAR));
|
||||
}
|
||||
|
||||
@Override
|
||||
public HeaderViewHolder onCreateHeaderViewHolder(ViewGroup parent, int position) {
|
||||
return new HeaderViewHolder(LayoutInflater.from(getContext()).inflate(R.layout.media_overview_document_item_header, parent, false));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBindHeaderViewHolder(HeaderViewHolder viewHolder, int position) {
|
||||
Cursor cursor = getCursorAtPositionOrThrow(position);
|
||||
MediaDatabase.MediaRecord mediaRecord = MediaDatabase.MediaRecord.from(getContext(), cursor);
|
||||
viewHolder.textView.setText(DateUtils.getRelativeDate(getContext(), locale, mediaRecord.getDate()));
|
||||
}
|
||||
|
||||
public static class ViewHolder extends RecyclerView.ViewHolder {
|
||||
|
||||
private final DocumentView documentView;
|
||||
private final TextView date;
|
||||
|
||||
public ViewHolder(View itemView) {
|
||||
super(itemView);
|
||||
this.documentView = itemView.findViewById(R.id.document_view);
|
||||
this.date = itemView.findViewById(R.id.date);
|
||||
}
|
||||
}
|
||||
|
||||
static class HeaderViewHolder extends RecyclerView.ViewHolder {
|
||||
|
||||
private final TextView textView;
|
||||
|
||||
HeaderViewHolder(View itemView) {
|
||||
super(itemView);
|
||||
this.textView = itemView.findViewById(R.id.text);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -1,170 +0,0 @@
|
|||
/*
|
||||
* Copyright (C) 2015 Open Whisper Systems
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.thoughtcrime.securesms;
|
||||
|
||||
import android.content.Context;
|
||||
import androidx.annotation.NonNull;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.TextView;
|
||||
|
||||
import com.codewaves.stickyheadergrid.StickyHeaderGridAdapter;
|
||||
|
||||
import org.thoughtcrime.securesms.components.ThumbnailView;
|
||||
import org.thoughtcrime.securesms.database.MediaDatabase.MediaRecord;
|
||||
import org.thoughtcrime.securesms.database.loaders.BucketedThreadMediaLoader.BucketedThreadMedia;
|
||||
import org.thoughtcrime.securesms.mms.GlideRequests;
|
||||
import org.thoughtcrime.securesms.mms.Slide;
|
||||
import org.thoughtcrime.securesms.util.MediaUtil;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.HashSet;
|
||||
import java.util.Locale;
|
||||
import java.util.Set;
|
||||
|
||||
class MediaGalleryAdapter extends StickyHeaderGridAdapter {
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
private static final String TAG = MediaGalleryAdapter.class.getSimpleName();
|
||||
|
||||
private final Context context;
|
||||
private final GlideRequests glideRequests;
|
||||
private final Locale locale;
|
||||
private final ItemClickListener itemClickListener;
|
||||
private final Set<MediaRecord> selected;
|
||||
|
||||
private BucketedThreadMedia media;
|
||||
|
||||
private static class ViewHolder extends StickyHeaderGridAdapter.ItemViewHolder {
|
||||
ThumbnailView imageView;
|
||||
View selectedIndicator;
|
||||
|
||||
ViewHolder(View v) {
|
||||
super(v);
|
||||
imageView = v.findViewById(R.id.image);
|
||||
selectedIndicator = v.findViewById(R.id.selected_indicator);
|
||||
}
|
||||
}
|
||||
|
||||
private static class HeaderHolder extends StickyHeaderGridAdapter.HeaderViewHolder {
|
||||
TextView textView;
|
||||
|
||||
HeaderHolder(View itemView) {
|
||||
super(itemView);
|
||||
textView = itemView.findViewById(R.id.text);
|
||||
}
|
||||
}
|
||||
|
||||
MediaGalleryAdapter(@NonNull Context context,
|
||||
@NonNull GlideRequests glideRequests,
|
||||
BucketedThreadMedia media,
|
||||
Locale locale,
|
||||
ItemClickListener clickListener)
|
||||
{
|
||||
this.context = context;
|
||||
this.glideRequests = glideRequests;
|
||||
this.locale = locale;
|
||||
this.media = media;
|
||||
this.itemClickListener = clickListener;
|
||||
this.selected = new HashSet<>();
|
||||
}
|
||||
|
||||
public void setMedia(BucketedThreadMedia media) {
|
||||
this.media = media;
|
||||
}
|
||||
|
||||
@Override
|
||||
public StickyHeaderGridAdapter.HeaderViewHolder onCreateHeaderViewHolder(ViewGroup parent, int headerType) {
|
||||
return new HeaderHolder(LayoutInflater.from(context).inflate(R.layout.media_overview_gallery_item_header, parent, false));
|
||||
}
|
||||
|
||||
@Override
|
||||
public ItemViewHolder onCreateItemViewHolder(ViewGroup parent, int itemType) {
|
||||
return new ViewHolder(LayoutInflater.from(context).inflate(R.layout.media_overview_gallery_item, parent, false));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBindHeaderViewHolder(StickyHeaderGridAdapter.HeaderViewHolder viewHolder, int section) {
|
||||
((HeaderHolder)viewHolder).textView.setText(media.getName(section, locale));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBindItemViewHolder(ItemViewHolder viewHolder, int section, int offset) {
|
||||
MediaRecord mediaRecord = media.get(section, offset);
|
||||
ThumbnailView thumbnailView = ((ViewHolder)viewHolder).imageView;
|
||||
View selectedIndicator = ((ViewHolder)viewHolder).selectedIndicator;
|
||||
Slide slide = MediaUtil.getSlideForAttachment(context, mediaRecord.getAttachment());
|
||||
|
||||
if (slide != null) {
|
||||
thumbnailView.setImageResource(glideRequests, slide, false, false);
|
||||
}
|
||||
|
||||
thumbnailView.setOnClickListener(view -> itemClickListener.onMediaClicked(mediaRecord));
|
||||
thumbnailView.setOnLongClickListener(view -> {
|
||||
itemClickListener.onMediaLongClicked(mediaRecord);
|
||||
return true;
|
||||
});
|
||||
|
||||
selectedIndicator.setVisibility(selected.contains(mediaRecord) ? View.VISIBLE : View.GONE);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getSectionCount() {
|
||||
return media.getSectionCount();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getSectionItemCount(int section) {
|
||||
return media.getSectionItemCount(section);
|
||||
}
|
||||
|
||||
public void toggleSelection(@NonNull MediaRecord mediaRecord) {
|
||||
if (!selected.remove(mediaRecord)) {
|
||||
selected.add(mediaRecord);
|
||||
}
|
||||
notifyDataSetChanged();
|
||||
}
|
||||
|
||||
public int getSelectedMediaCount() {
|
||||
return selected.size();
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public Collection<MediaRecord> getSelectedMedia() {
|
||||
return new HashSet<>(selected);
|
||||
}
|
||||
|
||||
public void clearSelection() {
|
||||
selected.clear();
|
||||
notifyDataSetChanged();
|
||||
}
|
||||
|
||||
void selectAllMedia() {
|
||||
for (int section = 0; section < media.getSectionCount(); section++) {
|
||||
for (int item = 0; item < media.getSectionItemCount(section); item++) {
|
||||
selected.add(media.get(section, item));
|
||||
}
|
||||
}
|
||||
this.notifyDataSetChanged();
|
||||
}
|
||||
|
||||
interface ItemClickListener {
|
||||
void onMediaClicked(@NonNull MediaRecord mediaRecord);
|
||||
void onMediaLongClicked(MediaRecord mediaRecord);
|
||||
}
|
||||
}
|
|
@ -1,515 +0,0 @@
|
|||
/*
|
||||
* Copyright (C) 2015 Open Whisper Systems
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.thoughtcrime.securesms;
|
||||
|
||||
import android.Manifest;
|
||||
import android.annotation.SuppressLint;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.res.Configuration;
|
||||
import android.content.res.Resources;
|
||||
import android.database.Cursor;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import androidx.annotation.NonNull;
|
||||
import com.google.android.material.tabs.TabLayout;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import androidx.fragment.app.FragmentManager;
|
||||
import androidx.fragment.app.FragmentStatePagerAdapter;
|
||||
import androidx.loader.app.LoaderManager;
|
||||
import androidx.loader.content.Loader;
|
||||
import androidx.viewpager.widget.ViewPager;
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
import androidx.appcompat.view.ActionMode;
|
||||
import androidx.recyclerview.widget.DividerItemDecoration;
|
||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
import androidx.appcompat.widget.Toolbar;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.view.Window;
|
||||
import android.widget.TextView;
|
||||
import android.widget.Toast;
|
||||
|
||||
import com.codewaves.stickyheadergrid.StickyHeaderGridLayoutManager;
|
||||
|
||||
import org.thoughtcrime.securesms.database.CursorRecyclerViewAdapter;
|
||||
import org.thoughtcrime.securesms.database.MediaDatabase;
|
||||
import org.thoughtcrime.securesms.database.loaders.BucketedThreadMediaLoader;
|
||||
import org.thoughtcrime.securesms.database.loaders.BucketedThreadMediaLoader.BucketedThreadMedia;
|
||||
import org.thoughtcrime.securesms.database.loaders.ThreadMediaLoader;
|
||||
import org.thoughtcrime.securesms.mms.GlideApp;
|
||||
import org.thoughtcrime.securesms.permissions.Permissions;
|
||||
import org.thoughtcrime.securesms.recipients.LiveRecipient;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||
import org.thoughtcrime.securesms.util.AttachmentUtil;
|
||||
import org.thoughtcrime.securesms.util.DynamicLanguage;
|
||||
import org.thoughtcrime.securesms.util.DynamicNoActionBarTheme;
|
||||
import org.thoughtcrime.securesms.util.DynamicTheme;
|
||||
import org.thoughtcrime.securesms.util.SaveAttachmentTask;
|
||||
import org.thoughtcrime.securesms.util.StickyHeaderDecoration;
|
||||
import org.thoughtcrime.securesms.util.ViewUtil;
|
||||
import org.thoughtcrime.securesms.util.task.ProgressDialogAsyncTask;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
|
||||
/**
|
||||
* Activity for displaying media attachments in-app
|
||||
*/
|
||||
public class MediaOverviewActivity extends PassphraseRequiredActionBarActivity {
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
private final static String TAG = MediaOverviewActivity.class.getSimpleName();
|
||||
|
||||
public static final String RECIPIENT_EXTRA = "recipient_id";
|
||||
|
||||
private final DynamicTheme dynamicTheme = new DynamicNoActionBarTheme();
|
||||
private final DynamicLanguage dynamicLanguage = new DynamicLanguage();
|
||||
|
||||
private Toolbar toolbar;
|
||||
private TabLayout tabLayout;
|
||||
private ViewPager viewPager;
|
||||
private LiveRecipient recipient;
|
||||
|
||||
@Override
|
||||
protected void onPreCreate() {
|
||||
dynamicTheme.onCreate(this);
|
||||
dynamicLanguage.onCreate(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle bundle, boolean ready) {
|
||||
setContentView(R.layout.media_overview_activity);
|
||||
|
||||
initializeResources();
|
||||
initializeToolbar();
|
||||
|
||||
this.tabLayout.setupWithViewPager(viewPager);
|
||||
this.viewPager.setAdapter(new MediaOverviewPagerAdapter(getSupportFragmentManager()));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onResume() {
|
||||
super.onResume();
|
||||
dynamicTheme.onResume(this);
|
||||
dynamicLanguage.onResume(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onOptionsItemSelected(MenuItem item) {
|
||||
super.onOptionsItemSelected(item);
|
||||
|
||||
switch (item.getItemId()) {
|
||||
case android.R.id.home: finish(); return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private void initializeResources() {
|
||||
RecipientId recipientId = getIntent().getParcelableExtra(RECIPIENT_EXTRA);
|
||||
|
||||
this.viewPager = ViewUtil.findById(this, R.id.pager);
|
||||
this.toolbar = ViewUtil.findById(this, R.id.toolbar);
|
||||
this.tabLayout = ViewUtil.findById(this, R.id.tab_layout);
|
||||
this.recipient = Recipient.live(recipientId);
|
||||
}
|
||||
|
||||
private void initializeToolbar() {
|
||||
setSupportActionBar(this.toolbar);
|
||||
getSupportActionBar().setTitle(recipient.get().toShortString(this));
|
||||
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
|
||||
this.recipient.observe(this, recipient -> getSupportActionBar().setTitle(recipient.toShortString(this)));
|
||||
}
|
||||
|
||||
public void onEnterMultiSelect() {
|
||||
tabLayout.setEnabled(false);
|
||||
viewPager.setEnabled(false);
|
||||
}
|
||||
|
||||
public void onExitMultiSelect() {
|
||||
tabLayout.setEnabled(true);
|
||||
viewPager.setEnabled(true);
|
||||
}
|
||||
|
||||
private class MediaOverviewPagerAdapter extends FragmentStatePagerAdapter {
|
||||
|
||||
MediaOverviewPagerAdapter(FragmentManager fragmentManager) {
|
||||
super(fragmentManager);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Fragment getItem(int position) {
|
||||
Fragment fragment;
|
||||
|
||||
if (position == 0) fragment = new MediaOverviewGalleryFragment();
|
||||
else if (position == 1) fragment = new MediaOverviewDocumentsFragment();
|
||||
else throw new AssertionError();
|
||||
|
||||
Bundle args = new Bundle();
|
||||
args.putParcelable(MediaOverviewGalleryFragment.RECIPIENT_EXTRA, recipient.getId());
|
||||
args.putSerializable(MediaOverviewGalleryFragment.LOCALE_EXTRA, dynamicLanguage.getCurrentLocale());
|
||||
|
||||
fragment.setArguments(args);
|
||||
|
||||
return fragment;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getCount() {
|
||||
return 2;
|
||||
}
|
||||
|
||||
@Override
|
||||
public CharSequence getPageTitle(int position) {
|
||||
if (position == 0) return getString(R.string.MediaOverviewActivity_Media);
|
||||
else if (position == 1) return getString(R.string.MediaOverviewActivity_Documents);
|
||||
else throw new AssertionError();
|
||||
}
|
||||
}
|
||||
|
||||
public static abstract class MediaOverviewFragment<T> extends Fragment implements LoaderManager.LoaderCallbacks<T> {
|
||||
|
||||
public static final String RECIPIENT_EXTRA = "recipient_id";
|
||||
public static final String LOCALE_EXTRA = "locale_extra";
|
||||
|
||||
protected TextView noMedia;
|
||||
protected Recipient recipient;
|
||||
protected RecyclerView recyclerView;
|
||||
protected Locale locale;
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle bundle) {
|
||||
super.onCreate(bundle);
|
||||
|
||||
RecipientId recipientId = getArguments().getParcelable(RECIPIENT_EXTRA);
|
||||
Locale locale = (Locale)getArguments().getSerializable(LOCALE_EXTRA);
|
||||
|
||||
if (recipientId == null) throw new AssertionError();
|
||||
if (locale == null) throw new AssertionError();
|
||||
|
||||
this.recipient = Recipient.live(recipientId).get();
|
||||
this.locale = locale;
|
||||
|
||||
getLoaderManager().initLoader(0, null, this);
|
||||
}
|
||||
}
|
||||
|
||||
public static class MediaOverviewGalleryFragment
|
||||
extends MediaOverviewFragment<BucketedThreadMedia>
|
||||
implements MediaGalleryAdapter.ItemClickListener
|
||||
{
|
||||
|
||||
private StickyHeaderGridLayoutManager gridManager;
|
||||
private ActionMode actionMode;
|
||||
private ActionModeCallback actionModeCallback = new ActionModeCallback();
|
||||
|
||||
@Override
|
||||
public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
|
||||
View view = inflater.inflate(R.layout.media_overview_gallery_fragment, container, false);
|
||||
|
||||
this.recyclerView = ViewUtil.findById(view, R.id.media_grid);
|
||||
this.noMedia = ViewUtil.findById(view, R.id.no_images);
|
||||
this.gridManager = new StickyHeaderGridLayoutManager(getResources().getInteger(R.integer.media_overview_cols));
|
||||
|
||||
this.recyclerView.setAdapter(new MediaGalleryAdapter(getContext(),
|
||||
GlideApp.with(this),
|
||||
new BucketedThreadMedia(getContext()),
|
||||
locale,
|
||||
this));
|
||||
this.recyclerView.setLayoutManager(gridManager);
|
||||
this.recyclerView.setHasFixedSize(true);
|
||||
|
||||
return view;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onConfigurationChanged(Configuration newConfig) {
|
||||
super.onConfigurationChanged(newConfig);
|
||||
if (gridManager != null) {
|
||||
this.gridManager = new StickyHeaderGridLayoutManager(getResources().getInteger(R.integer.media_overview_cols));
|
||||
this.recyclerView.setLayoutManager(gridManager);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull Loader<BucketedThreadMedia> onCreateLoader(int i, Bundle bundle) {
|
||||
return new BucketedThreadMediaLoader(getContext(), recipient.getId());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLoadFinished(@NonNull Loader<BucketedThreadMedia> loader, BucketedThreadMedia bucketedThreadMedia) {
|
||||
((MediaGalleryAdapter) recyclerView.getAdapter()).setMedia(bucketedThreadMedia);
|
||||
((MediaGalleryAdapter) recyclerView.getAdapter()).notifyAllSectionsDataSetChanged();
|
||||
|
||||
noMedia.setVisibility(recyclerView.getAdapter().getItemCount() > 0 ? View.GONE : View.VISIBLE);
|
||||
getActivity().invalidateOptionsMenu();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLoaderReset(@NonNull Loader<BucketedThreadMedia> cursorLoader) {
|
||||
((MediaGalleryAdapter) recyclerView.getAdapter()).setMedia(new BucketedThreadMedia(getContext()));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onMediaClicked(@NonNull MediaDatabase.MediaRecord mediaRecord) {
|
||||
if (actionMode != null) {
|
||||
handleMediaMultiSelectClick(mediaRecord);
|
||||
} else {
|
||||
handleMediaPreviewClick(mediaRecord);
|
||||
}
|
||||
}
|
||||
|
||||
private void handleMediaMultiSelectClick(@NonNull MediaDatabase.MediaRecord mediaRecord) {
|
||||
MediaGalleryAdapter adapter = getListAdapter();
|
||||
|
||||
adapter.toggleSelection(mediaRecord);
|
||||
if (adapter.getSelectedMediaCount() == 0) {
|
||||
actionMode.finish();
|
||||
} else {
|
||||
actionMode.setTitle(String.valueOf(adapter.getSelectedMediaCount()));
|
||||
}
|
||||
}
|
||||
|
||||
private void handleMediaPreviewClick(@NonNull MediaDatabase.MediaRecord mediaRecord) {
|
||||
if (mediaRecord.getAttachment().getDataUri() == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
Context context = getContext();
|
||||
if (context == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
Intent intent = new Intent(context, MediaPreviewActivity.class);
|
||||
intent.putExtra(MediaPreviewActivity.DATE_EXTRA, mediaRecord.getDate());
|
||||
intent.putExtra(MediaPreviewActivity.SIZE_EXTRA, mediaRecord.getAttachment().getSize());
|
||||
intent.putExtra(MediaPreviewActivity.RECIPIENT_EXTRA, recipient.getId());
|
||||
intent.putExtra(MediaPreviewActivity.OUTGOING_EXTRA, mediaRecord.isOutgoing());
|
||||
intent.putExtra(MediaPreviewActivity.LEFT_IS_RECENT_EXTRA, true);
|
||||
|
||||
intent.setDataAndType(mediaRecord.getAttachment().getDataUri(), mediaRecord.getContentType());
|
||||
context.startActivity(intent);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onMediaLongClicked(MediaDatabase.MediaRecord mediaRecord) {
|
||||
if (actionMode == null) {
|
||||
((MediaGalleryAdapter) recyclerView.getAdapter()).toggleSelection(mediaRecord);
|
||||
recyclerView.getAdapter().notifyDataSetChanged();
|
||||
|
||||
enterMultiSelect();
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings("CodeBlock2Expr")
|
||||
@SuppressLint({"InlinedApi","StaticFieldLeak"})
|
||||
private void handleSaveMedia(@NonNull Collection<MediaDatabase.MediaRecord> mediaRecords) {
|
||||
final Context context = getContext();
|
||||
SaveAttachmentTask.showWarningDialog(context, (dialogInterface, which) -> {
|
||||
Permissions.with(this)
|
||||
.request(android.Manifest.permission.WRITE_EXTERNAL_STORAGE, Manifest.permission.READ_EXTERNAL_STORAGE)
|
||||
.ifNecessary()
|
||||
.withPermanentDenialDialog(getString(R.string.MediaPreviewActivity_signal_needs_the_storage_permission_in_order_to_write_to_external_storage_but_it_has_been_permanently_denied))
|
||||
.onAnyDenied(() -> Toast.makeText(getContext(), R.string.MediaPreviewActivity_unable_to_write_to_external_storage_without_permission, Toast.LENGTH_LONG).show())
|
||||
.onAllGranted(() -> {
|
||||
new ProgressDialogAsyncTask<Void, Void, List<SaveAttachmentTask.Attachment>>(context,
|
||||
R.string.MediaOverviewActivity_collecting_attachments,
|
||||
R.string.please_wait) {
|
||||
@Override
|
||||
protected List<SaveAttachmentTask.Attachment> doInBackground(Void... params) {
|
||||
List<SaveAttachmentTask.Attachment> attachments = new LinkedList<>();
|
||||
|
||||
for (MediaDatabase.MediaRecord mediaRecord : mediaRecords) {
|
||||
if (mediaRecord.getAttachment().getDataUri() != null) {
|
||||
attachments.add(new SaveAttachmentTask.Attachment(mediaRecord.getAttachment().getDataUri(),
|
||||
mediaRecord.getContentType(),
|
||||
mediaRecord.getDate(),
|
||||
mediaRecord.getAttachment().getFileName()));
|
||||
}
|
||||
}
|
||||
|
||||
return attachments;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPostExecute(List<SaveAttachmentTask.Attachment> attachments) {
|
||||
super.onPostExecute(attachments);
|
||||
SaveAttachmentTask saveTask = new SaveAttachmentTask(context,
|
||||
attachments.size());
|
||||
saveTask.executeOnExecutor(THREAD_POOL_EXECUTOR,
|
||||
attachments.toArray(new SaveAttachmentTask.Attachment[attachments.size()]));
|
||||
actionMode.finish();
|
||||
}
|
||||
}.execute();
|
||||
})
|
||||
.execute();
|
||||
}, mediaRecords.size());
|
||||
}
|
||||
|
||||
@SuppressLint("StaticFieldLeak")
|
||||
private void handleDeleteMedia(@NonNull Collection<MediaDatabase.MediaRecord> mediaRecords) {
|
||||
int recordCount = mediaRecords.size();
|
||||
Resources res = getContext().getResources();
|
||||
String confirmTitle = res.getQuantityString(R.plurals.MediaOverviewActivity_Media_delete_confirm_title,
|
||||
recordCount,
|
||||
recordCount);
|
||||
String confirmMessage = res.getQuantityString(R.plurals.MediaOverviewActivity_Media_delete_confirm_message,
|
||||
recordCount,
|
||||
recordCount);
|
||||
|
||||
AlertDialog.Builder builder = new AlertDialog.Builder(getContext());
|
||||
builder.setIconAttribute(R.attr.dialog_alert_icon);
|
||||
builder.setTitle(confirmTitle);
|
||||
builder.setMessage(confirmMessage);
|
||||
builder.setCancelable(true);
|
||||
|
||||
builder.setPositiveButton(R.string.delete, (dialogInterface, i) -> {
|
||||
new ProgressDialogAsyncTask<MediaDatabase.MediaRecord, Void, Void>(getContext(),
|
||||
R.string.MediaOverviewActivity_Media_delete_progress_title,
|
||||
R.string.MediaOverviewActivity_Media_delete_progress_message)
|
||||
{
|
||||
@Override
|
||||
protected Void doInBackground(MediaDatabase.MediaRecord... records) {
|
||||
if (records == null || records.length == 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
for (MediaDatabase.MediaRecord record : records) {
|
||||
AttachmentUtil.deleteAttachment(getContext(), record.getAttachment());
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
}.execute(mediaRecords.toArray(new MediaDatabase.MediaRecord[mediaRecords.size()]));
|
||||
});
|
||||
builder.setNegativeButton(android.R.string.cancel, null);
|
||||
builder.show();
|
||||
}
|
||||
|
||||
private void handleSelectAllMedia() {
|
||||
getListAdapter().selectAllMedia();
|
||||
actionMode.setTitle(String.valueOf(getListAdapter().getSelectedMediaCount()));
|
||||
}
|
||||
|
||||
private MediaGalleryAdapter getListAdapter() {
|
||||
return (MediaGalleryAdapter) recyclerView.getAdapter();
|
||||
}
|
||||
|
||||
private void enterMultiSelect() {
|
||||
actionMode = ((AppCompatActivity) getActivity()).startSupportActionMode(actionModeCallback);
|
||||
((MediaOverviewActivity) getActivity()).onEnterMultiSelect();
|
||||
}
|
||||
|
||||
private class ActionModeCallback implements ActionMode.Callback {
|
||||
|
||||
private int originalStatusBarColor;
|
||||
|
||||
@Override
|
||||
public boolean onCreateActionMode(ActionMode mode, Menu menu) {
|
||||
mode.getMenuInflater().inflate(R.menu.media_overview_context, menu);
|
||||
mode.setTitle("1");
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
||||
Window window = getActivity().getWindow();
|
||||
originalStatusBarColor = 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) {
|
||||
switch (menuItem.getItemId()) {
|
||||
case R.id.save:
|
||||
handleSaveMedia(getListAdapter().getSelectedMedia());
|
||||
return true;
|
||||
case R.id.delete:
|
||||
handleDeleteMedia(getListAdapter().getSelectedMedia());
|
||||
actionMode.finish();
|
||||
return true;
|
||||
case R.id.select_all:
|
||||
handleSelectAllMedia();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroyActionMode(ActionMode mode) {
|
||||
actionMode = null;
|
||||
getListAdapter().clearSelection();
|
||||
((MediaOverviewActivity) getActivity()).onExitMultiSelect();
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
||||
getActivity().getWindow().setStatusBarColor(originalStatusBarColor);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static class MediaOverviewDocumentsFragment extends MediaOverviewFragment<Cursor> {
|
||||
|
||||
@Override
|
||||
public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
|
||||
View view = inflater.inflate(R.layout.media_overview_documents_fragment, container, false);
|
||||
MediaDocumentsAdapter adapter = new MediaDocumentsAdapter(getContext(), null, locale);
|
||||
|
||||
this.recyclerView = ViewUtil.findById(view, R.id.recycler_view);
|
||||
this.noMedia = ViewUtil.findById(view, R.id.no_documents);
|
||||
|
||||
this.recyclerView.setAdapter(adapter);
|
||||
this.recyclerView.setLayoutManager(new LinearLayoutManager(getContext(), RecyclerView.VERTICAL, false));
|
||||
this.recyclerView.addItemDecoration(new StickyHeaderDecoration(adapter, false, true));
|
||||
this.recyclerView.addItemDecoration(new DividerItemDecoration(getContext(), DividerItemDecoration.VERTICAL));
|
||||
|
||||
return view;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull Loader<Cursor> onCreateLoader(int id, Bundle args) {
|
||||
return new ThreadMediaLoader(requireContext(), recipient.getId(), false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLoadFinished(@NonNull Loader<Cursor> loader, Cursor data) {
|
||||
((CursorRecyclerViewAdapter)this.recyclerView.getAdapter()).changeCursor(data);
|
||||
requireActivity().invalidateOptionsMenu();
|
||||
|
||||
this.noMedia.setVisibility(data.getCount() > 0 ? View.GONE : View.VISIBLE);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLoaderReset(@NonNull Loader<Cursor> loader) {
|
||||
((CursorRecyclerViewAdapter)this.recyclerView.getAdapter()).changeCursor(null);
|
||||
getActivity().invalidateOptionsMenu();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -52,10 +52,12 @@ import androidx.viewpager.widget.ViewPager;
|
|||
import org.thoughtcrime.securesms.animation.DepthPageTransformer;
|
||||
import org.thoughtcrime.securesms.attachments.DatabaseAttachment;
|
||||
import org.thoughtcrime.securesms.components.viewpager.ExtendedOnPageChangedListener;
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
||||
import org.thoughtcrime.securesms.database.MediaDatabase;
|
||||
import org.thoughtcrime.securesms.database.MediaDatabase.MediaRecord;
|
||||
import org.thoughtcrime.securesms.database.loaders.PagingMediaLoader;
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
import org.thoughtcrime.securesms.mediaoverview.MediaOverviewActivity;
|
||||
import org.thoughtcrime.securesms.mediapreview.MediaPreviewFragment;
|
||||
import org.thoughtcrime.securesms.mediapreview.MediaPreviewViewModel;
|
||||
import org.thoughtcrime.securesms.mediapreview.MediaRailAdapter;
|
||||
|
@ -67,7 +69,6 @@ import org.thoughtcrime.securesms.util.AttachmentUtil;
|
|||
import org.thoughtcrime.securesms.util.DateUtils;
|
||||
import org.thoughtcrime.securesms.util.SaveAttachmentTask;
|
||||
import org.thoughtcrime.securesms.util.SaveAttachmentTask.Attachment;
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Locale;
|
||||
|
@ -84,12 +85,16 @@ public final class MediaPreviewActivity extends PassphraseRequiredActionBarActiv
|
|||
|
||||
private final static String TAG = MediaPreviewActivity.class.getSimpleName();
|
||||
|
||||
public static final String RECIPIENT_EXTRA = "recipient_id";
|
||||
private static final int NOT_IN_A_THREAD = -2;
|
||||
|
||||
public static final String THREAD_ID_EXTRA = "thread_id";
|
||||
public static final String DATE_EXTRA = "date";
|
||||
public static final String SIZE_EXTRA = "size";
|
||||
public static final String CAPTION_EXTRA = "caption";
|
||||
public static final String OUTGOING_EXTRA = "outgoing";
|
||||
public static final String LEFT_IS_RECENT_EXTRA = "left_is_recent";
|
||||
public static final String HIDE_ALL_MEDIA_EXTRA = "came_from_all_media";
|
||||
public static final String SHOW_THREAD_EXTRA = "show_thread";
|
||||
public static final String SORTING_EXTRA = "sorting";
|
||||
|
||||
private ViewPager mediaPager;
|
||||
private View detailsContainer;
|
||||
|
@ -102,12 +107,15 @@ public final class MediaPreviewActivity extends PassphraseRequiredActionBarActiv
|
|||
private String initialMediaType;
|
||||
private long initialMediaSize;
|
||||
private String initialCaption;
|
||||
private Recipient conversationRecipient;
|
||||
private boolean leftIsRecent;
|
||||
private MediaPreviewViewModel viewModel;
|
||||
private ViewPagerListener viewPagerListener;
|
||||
|
||||
private int restartItem = -1;
|
||||
private int restartItem = -1;
|
||||
private long threadId = NOT_IN_A_THREAD;
|
||||
private boolean cameFromAllMedia;
|
||||
private boolean showThread;
|
||||
private MediaDatabase.Sorting sorting;
|
||||
|
||||
@SuppressWarnings("ConstantConditions")
|
||||
@Override
|
||||
|
@ -151,19 +159,45 @@ public final class MediaPreviewActivity extends PassphraseRequiredActionBarActiv
|
|||
MediaItem mediaItem = getCurrentMediaItem();
|
||||
|
||||
if (mediaItem != null) {
|
||||
CharSequence relativeTimeSpan;
|
||||
getSupportActionBar().setTitle(getTitleText(mediaItem));
|
||||
getSupportActionBar().setSubtitle(getSubTitleText(mediaItem));
|
||||
}
|
||||
}
|
||||
|
||||
if (mediaItem.date > 0) {
|
||||
relativeTimeSpan = DateUtils.getExtendedRelativeTimeSpanString(this, Locale.getDefault(), mediaItem.date);
|
||||
} else {
|
||||
relativeTimeSpan = getString(R.string.MediaPreviewActivity_draft);
|
||||
private @NonNull String getTitleText(@NonNull MediaItem mediaItem) {
|
||||
String from;
|
||||
if (mediaItem.outgoing) from = getString(R.string.MediaPreviewActivity_you);
|
||||
else if (mediaItem.recipient != null) from = mediaItem.recipient.toShortString(this);
|
||||
else from = "";
|
||||
|
||||
if (showThread) {
|
||||
String to = null;
|
||||
Recipient threadRecipient = mediaItem.threadRecipient;
|
||||
|
||||
if (threadRecipient != null) {
|
||||
if (mediaItem.outgoing || threadRecipient.isGroup()) {
|
||||
if (threadRecipient.isLocalNumber()) {
|
||||
from = getString(R.string.note_to_self);
|
||||
} else {
|
||||
to = threadRecipient.toShortString(this);
|
||||
}
|
||||
} else {
|
||||
to = getString(R.string.MediaPreviewActivity_you);
|
||||
}
|
||||
}
|
||||
|
||||
if (mediaItem.outgoing) getSupportActionBar().setTitle(getString(R.string.MediaPreviewActivity_you));
|
||||
else if (mediaItem.recipient != null) getSupportActionBar().setTitle(mediaItem.recipient.toShortString(this));
|
||||
else getSupportActionBar().setTitle("");
|
||||
return to != null ? getString(R.string.MediaPreviewActivity_s_to_s, from, to)
|
||||
: from;
|
||||
} else {
|
||||
return from;
|
||||
}
|
||||
}
|
||||
|
||||
getSupportActionBar().setSubtitle(relativeTimeSpan);
|
||||
private @NonNull String getSubTitleText(@NonNull MediaItem mediaItem) {
|
||||
if (mediaItem.date > 0) {
|
||||
return DateUtils.getExtendedRelativeTimeSpanString(this, Locale.getDefault(), mediaItem.date);
|
||||
} else {
|
||||
return getString(R.string.MediaPreviewActivity_draft);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -216,20 +250,19 @@ public final class MediaPreviewActivity extends PassphraseRequiredActionBarActiv
|
|||
}
|
||||
|
||||
private void initializeResources() {
|
||||
RecipientId recipientId = getIntent().getParcelableExtra(RECIPIENT_EXTRA);
|
||||
Intent intent = getIntent();
|
||||
|
||||
initialMediaUri = getIntent().getData();
|
||||
initialMediaType = getIntent().getType();
|
||||
initialMediaSize = getIntent().getLongExtra(SIZE_EXTRA, 0);
|
||||
initialCaption = getIntent().getStringExtra(CAPTION_EXTRA);
|
||||
leftIsRecent = getIntent().getBooleanExtra(LEFT_IS_RECENT_EXTRA, false);
|
||||
threadId = intent.getLongExtra(THREAD_ID_EXTRA, NOT_IN_A_THREAD);
|
||||
cameFromAllMedia = intent.getBooleanExtra(HIDE_ALL_MEDIA_EXTRA, false);
|
||||
showThread = intent.getBooleanExtra(SHOW_THREAD_EXTRA, false);
|
||||
sorting = MediaDatabase.Sorting.values()[intent.getIntExtra(SORTING_EXTRA, 0)];
|
||||
|
||||
initialMediaUri = intent.getData();
|
||||
initialMediaType = intent.getType();
|
||||
initialMediaSize = intent.getLongExtra(SIZE_EXTRA, 0);
|
||||
initialCaption = intent.getStringExtra(CAPTION_EXTRA);
|
||||
leftIsRecent = intent.getBooleanExtra(LEFT_IS_RECENT_EXTRA, false);
|
||||
restartItem = -1;
|
||||
|
||||
if (recipientId != null) {
|
||||
conversationRecipient = Recipient.live(recipientId).get();
|
||||
} else {
|
||||
conversationRecipient = null;
|
||||
}
|
||||
}
|
||||
|
||||
private void initializeObservers() {
|
||||
|
@ -279,7 +312,7 @@ public final class MediaPreviewActivity extends PassphraseRequiredActionBarActiv
|
|||
|
||||
Log.i(TAG, "Loading Part URI: " + initialMediaUri);
|
||||
|
||||
if (conversationRecipient != null) {
|
||||
if (isMediaInDb()) {
|
||||
LoaderManager.getInstance(this).restartLoader(0, null, this);
|
||||
} else {
|
||||
mediaPager.setAdapter(new SingleItemPagerAdapter(getSupportFragmentManager(), initialMediaUri, initialMediaType, initialMediaSize));
|
||||
|
@ -302,9 +335,7 @@ public final class MediaPreviewActivity extends PassphraseRequiredActionBarActiv
|
|||
}
|
||||
|
||||
private void showOverview() {
|
||||
Intent intent = new Intent(this, MediaOverviewActivity.class);
|
||||
intent.putExtra(MediaOverviewActivity.RECIPIENT_EXTRA, conversationRecipient.getId());
|
||||
startActivity(intent);
|
||||
startActivity(MediaOverviewActivity.forThread(this, threadId));
|
||||
}
|
||||
|
||||
private void forward() {
|
||||
|
@ -382,6 +413,10 @@ public final class MediaPreviewActivity extends PassphraseRequiredActionBarActiv
|
|||
menu.findItem(R.id.delete).setVisible(false);
|
||||
}
|
||||
|
||||
if (cameFromAllMedia) {
|
||||
menu.findItem(R.id.media_preview__overview).setVisible(false);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
|
@ -401,7 +436,7 @@ public final class MediaPreviewActivity extends PassphraseRequiredActionBarActiv
|
|||
}
|
||||
|
||||
private boolean isMediaInDb() {
|
||||
return conversationRecipient != null;
|
||||
return threadId != NOT_IN_A_THREAD;
|
||||
}
|
||||
|
||||
private @Nullable MediaItem getCurrentMediaItem() {
|
||||
|
@ -420,7 +455,7 @@ public final class MediaPreviewActivity extends PassphraseRequiredActionBarActiv
|
|||
|
||||
@Override
|
||||
public @NonNull Loader<Pair<Cursor, Integer>> onCreateLoader(int id, Bundle args) {
|
||||
return new PagingMediaLoader(this, conversationRecipient, initialMediaUri, leftIsRecent);
|
||||
return new PagingMediaLoader(this, threadId, initialMediaUri, leftIsRecent, sorting);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -550,7 +585,7 @@ public final class MediaPreviewActivity extends PassphraseRequiredActionBarActiv
|
|||
|
||||
@Override
|
||||
public MediaItem getMediaItemFor(int position) {
|
||||
return new MediaItem(null, null, uri, mediaType, -1, true);
|
||||
return new MediaItem(null, null, null, uri, mediaType, -1, true);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -684,12 +719,16 @@ public final class MediaPreviewActivity extends PassphraseRequiredActionBarActiv
|
|||
|
||||
public MediaItem getMediaItemFor(int position) {
|
||||
cursor.moveToPosition(getCursorPosition(position));
|
||||
MediaRecord mediaRecord = MediaRecord.from(context, cursor);
|
||||
RecipientId recipientId = mediaRecord.getRecipientId();
|
||||
|
||||
MediaRecord mediaRecord = MediaRecord.from(context, cursor);
|
||||
RecipientId recipientId = mediaRecord.getRecipientId();
|
||||
RecipientId threadRecipientId = DatabaseFactory.getThreadDatabase(context)
|
||||
.getRecipientIdForThreadId(mediaRecord.getThreadId());
|
||||
|
||||
if (mediaRecord.getAttachment().getDataUri() == null) throw new AssertionError();
|
||||
|
||||
return new MediaItem(Recipient.live(recipientId).get(),
|
||||
threadRecipientId != null ? Recipient.live(threadRecipientId).get() : null,
|
||||
mediaRecord.getAttachment(),
|
||||
mediaRecord.getAttachment().getDataUri(),
|
||||
mediaRecord.getContentType(),
|
||||
|
@ -723,6 +762,7 @@ public final class MediaPreviewActivity extends PassphraseRequiredActionBarActiv
|
|||
|
||||
private static class MediaItem {
|
||||
private final @Nullable Recipient recipient;
|
||||
private final @Nullable Recipient threadRecipient;
|
||||
private final @Nullable DatabaseAttachment attachment;
|
||||
private final @NonNull Uri uri;
|
||||
private final @NonNull String type;
|
||||
|
@ -730,18 +770,20 @@ public final class MediaPreviewActivity extends PassphraseRequiredActionBarActiv
|
|||
private final boolean outgoing;
|
||||
|
||||
private MediaItem(@Nullable Recipient recipient,
|
||||
@Nullable Recipient threadRecipient,
|
||||
@Nullable DatabaseAttachment attachment,
|
||||
@NonNull Uri uri,
|
||||
@NonNull String type,
|
||||
long date,
|
||||
boolean outgoing)
|
||||
{
|
||||
this.recipient = recipient;
|
||||
this.attachment = attachment;
|
||||
this.uri = uri;
|
||||
this.type = type;
|
||||
this.date = date;
|
||||
this.outgoing = outgoing;
|
||||
this.recipient = recipient;
|
||||
this.threadRecipient = threadRecipient;
|
||||
this.attachment = attachment;
|
||||
this.uri = uri;
|
||||
this.type = type;
|
||||
this.date = date;
|
||||
this.outgoing = outgoing;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -13,30 +13,6 @@ import android.os.AsyncTask;
|
|||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.provider.Settings;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import com.google.android.material.appbar.CollapsingToolbarLayout;
|
||||
|
||||
import androidx.fragment.app.Fragment;
|
||||
import androidx.loader.app.LoaderManager;
|
||||
import androidx.loader.content.Loader;
|
||||
import androidx.core.view.ViewCompat;
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
import androidx.preference.CheckBoxPreference;
|
||||
import androidx.preference.ListPreference;
|
||||
import androidx.preference.Preference;
|
||||
import androidx.preference.PreferenceCategory;
|
||||
import androidx.appcompat.widget.Toolbar;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import org.thoughtcrime.securesms.components.SwitchPreferenceCompat;
|
||||
import org.thoughtcrime.securesms.contacts.avatars.ContactPhoto;
|
||||
import org.thoughtcrime.securesms.contacts.avatars.FallbackContactPhoto;
|
||||
import org.thoughtcrime.securesms.contacts.avatars.ProfileContactPhoto;
|
||||
import org.thoughtcrime.securesms.contacts.avatars.ResourceContactPhoto;
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
|
||||
import android.telephony.PhoneNumberUtils;
|
||||
import android.util.Pair;
|
||||
import android.view.LayoutInflater;
|
||||
|
@ -47,19 +23,43 @@ import android.view.WindowManager;
|
|||
import android.widget.ImageView;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
import androidx.appcompat.widget.Toolbar;
|
||||
import androidx.core.view.ViewCompat;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import androidx.loader.app.LoaderManager;
|
||||
import androidx.loader.content.Loader;
|
||||
import androidx.preference.CheckBoxPreference;
|
||||
import androidx.preference.ListPreference;
|
||||
import androidx.preference.Preference;
|
||||
import androidx.preference.PreferenceCategory;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import com.bumptech.glide.load.engine.DiskCacheStrategy;
|
||||
import com.google.android.material.appbar.CollapsingToolbarLayout;
|
||||
|
||||
import org.thoughtcrime.securesms.color.MaterialColor;
|
||||
import org.thoughtcrime.securesms.color.MaterialColors;
|
||||
import org.thoughtcrime.securesms.components.SwitchPreferenceCompat;
|
||||
import org.thoughtcrime.securesms.components.ThreadPhotoRailView;
|
||||
import org.thoughtcrime.securesms.contacts.avatars.ContactPhoto;
|
||||
import org.thoughtcrime.securesms.contacts.avatars.FallbackContactPhoto;
|
||||
import org.thoughtcrime.securesms.contacts.avatars.ProfileContactPhoto;
|
||||
import org.thoughtcrime.securesms.contacts.avatars.ResourceContactPhoto;
|
||||
import org.thoughtcrime.securesms.crypto.IdentityKeyParcelable;
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
||||
import org.thoughtcrime.securesms.database.IdentityDatabase;
|
||||
import org.thoughtcrime.securesms.database.IdentityDatabase.IdentityRecord;
|
||||
import org.thoughtcrime.securesms.database.MediaDatabase;
|
||||
import org.thoughtcrime.securesms.database.RecipientDatabase;
|
||||
import org.thoughtcrime.securesms.database.RecipientDatabase.VibrateState;
|
||||
import org.thoughtcrime.securesms.database.loaders.ThreadMediaLoader;
|
||||
import org.thoughtcrime.securesms.database.loaders.RecipientMediaLoader;
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
||||
import org.thoughtcrime.securesms.jobs.MultiDeviceContactUpdateJob;
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
import org.thoughtcrime.securesms.mediaoverview.MediaOverviewActivity;
|
||||
import org.thoughtcrime.securesms.mms.GlideApp;
|
||||
import org.thoughtcrime.securesms.mms.GlideRequests;
|
||||
import org.thoughtcrime.securesms.notifications.NotificationChannels;
|
||||
|
@ -71,7 +71,6 @@ import org.thoughtcrime.securesms.recipients.LiveRecipient;
|
|||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientUtil;
|
||||
import org.thoughtcrime.securesms.sms.MessageSender;
|
||||
import org.thoughtcrime.securesms.util.CommunicationActions;
|
||||
import org.thoughtcrime.securesms.util.Dialogs;
|
||||
import org.thoughtcrime.securesms.util.DynamicDarkToolbarTheme;
|
||||
|
@ -84,6 +83,7 @@ import org.thoughtcrime.securesms.util.ThemeUtil;
|
|||
import org.thoughtcrime.securesms.util.ViewUtil;
|
||||
import org.thoughtcrime.securesms.util.concurrent.ListenableFuture;
|
||||
import org.thoughtcrime.securesms.util.concurrent.SignalExecutors;
|
||||
import org.thoughtcrime.securesms.util.concurrent.SimpleTask;
|
||||
import org.whispersystems.libsignal.util.guava.Optional;
|
||||
|
||||
import java.util.concurrent.ExecutionException;
|
||||
|
@ -181,8 +181,7 @@ public class RecipientPreferenceActivity extends PassphraseRequiredActionBarActi
|
|||
|
||||
this.threadPhotoRailView.setListener(mediaRecord -> {
|
||||
Intent intent = new Intent(RecipientPreferenceActivity.this, MediaPreviewActivity.class);
|
||||
intent.putExtra(MediaPreviewActivity.RECIPIENT_EXTRA, recipientId);
|
||||
intent.putExtra(MediaPreviewActivity.OUTGOING_EXTRA, mediaRecord.isOutgoing());
|
||||
intent.putExtra(MediaPreviewActivity.THREAD_ID_EXTRA, mediaRecord.getThreadId());
|
||||
intent.putExtra(MediaPreviewActivity.DATE_EXTRA, mediaRecord.getDate());
|
||||
intent.putExtra(MediaPreviewActivity.SIZE_EXTRA, mediaRecord.getAttachment().getSize());
|
||||
intent.putExtra(MediaPreviewActivity.CAPTION_EXTRA, mediaRecord.getAttachment().getCaption());
|
||||
|
@ -191,11 +190,15 @@ public class RecipientPreferenceActivity extends PassphraseRequiredActionBarActi
|
|||
startActivity(intent);
|
||||
});
|
||||
|
||||
this.threadPhotoRailLabel.setOnClickListener(v -> {
|
||||
Intent intent = new Intent(this, MediaOverviewActivity.class);
|
||||
intent.putExtra(MediaOverviewActivity.RECIPIENT_EXTRA, recipientId);
|
||||
startActivity(intent);
|
||||
});
|
||||
SimpleTask.run(
|
||||
() -> DatabaseFactory.getThreadDatabase(this).getThreadIdFor(recipientId),
|
||||
(threadId) -> {
|
||||
if (threadId == null) {
|
||||
throw new AssertionError();
|
||||
}
|
||||
this.threadPhotoRailLabel.setOnClickListener(v -> startActivity(MediaOverviewActivity.forThread(this, threadId)));
|
||||
}
|
||||
);
|
||||
|
||||
Toolbar toolbar = ViewUtil.findById(this, R.id.toolbar);
|
||||
setSupportActionBar(toolbar);
|
||||
|
@ -230,7 +233,7 @@ public class RecipientPreferenceActivity extends PassphraseRequiredActionBarActi
|
|||
|
||||
@Override
|
||||
public @NonNull Loader<Cursor> onCreateLoader(int id, Bundle args) {
|
||||
return new ThreadMediaLoader(this, recipientId, true);
|
||||
return new RecipientMediaLoader(this, recipientId, RecipientMediaLoader.MediaType.GALLERY, MediaDatabase.Sorting.Newest);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
|
@ -51,9 +51,12 @@ public final class AudioView extends FrameLayout implements AudioSlidePlayer.Lis
|
|||
@NonNull private final View progressAndPlay;
|
||||
@NonNull private final LottieAnimationView playPauseButton;
|
||||
@NonNull private final ImageView downloadButton;
|
||||
@NonNull private final ProgressWheel downloadProgress;
|
||||
@NonNull private final ProgressWheel circleProgress;
|
||||
@NonNull private final SeekBar seekBar;
|
||||
@NonNull private final TextView timestamp;
|
||||
private final boolean smallView;
|
||||
private final boolean autoRewind;
|
||||
|
||||
@Nullable private final TextView timestamp;
|
||||
|
||||
@Nullable private SlideClickListener downloadListener;
|
||||
@Nullable private AudioSlidePlayer audioSlidePlayer;
|
||||
|
@ -71,27 +74,35 @@ public final class AudioView extends FrameLayout implements AudioSlidePlayer.Lis
|
|||
|
||||
public AudioView(Context context, AttributeSet attrs, int defStyleAttr) {
|
||||
super(context, attrs, defStyleAttr);
|
||||
inflate(context, R.layout.audio_view, this);
|
||||
TypedArray typedArray = null;
|
||||
try {
|
||||
typedArray = context.getTheme().obtainStyledAttributes(attrs, R.styleable.AudioView, 0, 0);
|
||||
|
||||
this.container = findViewById(R.id.audio_widget_container);
|
||||
this.controlToggle = findViewById(R.id.control_toggle);
|
||||
this.playPauseButton = findViewById(R.id.play);
|
||||
this.progressAndPlay = findViewById(R.id.progress_and_play);
|
||||
this.downloadButton = findViewById(R.id.download);
|
||||
this.downloadProgress = findViewById(R.id.download_progress);
|
||||
this.seekBar = findViewById(R.id.seek);
|
||||
this.timestamp = findViewById(R.id.timestamp);
|
||||
smallView = typedArray.getBoolean(R.styleable.AudioView_small, false);
|
||||
autoRewind = typedArray.getBoolean(R.styleable.AudioView_autoRewind, false);
|
||||
|
||||
lottieDirection = REVERSE;
|
||||
this.playPauseButton.setOnClickListener(new PlayPauseClickedListener());
|
||||
this.seekBar.setOnSeekBarChangeListener(new SeekBarModifiedListener());
|
||||
inflate(context, smallView ? R.layout.audio_view_small : R.layout.audio_view, this);
|
||||
|
||||
this.container = findViewById(R.id.audio_widget_container);
|
||||
this.controlToggle = findViewById(R.id.control_toggle);
|
||||
this.playPauseButton = findViewById(R.id.play);
|
||||
this.progressAndPlay = findViewById(R.id.progress_and_play);
|
||||
this.downloadButton = findViewById(R.id.download);
|
||||
this.circleProgress = findViewById(R.id.circle_progress);
|
||||
this.seekBar = findViewById(R.id.seek);
|
||||
this.timestamp = findViewById(R.id.timestamp);
|
||||
|
||||
lottieDirection = REVERSE;
|
||||
this.playPauseButton.setOnClickListener(new PlayPauseClickedListener());
|
||||
this.seekBar.setOnSeekBarChangeListener(new SeekBarModifiedListener());
|
||||
|
||||
if (attrs != null) {
|
||||
TypedArray typedArray = context.getTheme().obtainStyledAttributes(attrs, R.styleable.AudioView, 0, 0);
|
||||
setTint(typedArray.getColor(R.styleable.AudioView_foregroundTintColor, Color.WHITE),
|
||||
typedArray.getColor(R.styleable.AudioView_backgroundTintColor, Color.WHITE));
|
||||
container.setBackgroundColor(typedArray.getColor(R.styleable.AudioView_widgetBackground, Color.TRANSPARENT));
|
||||
typedArray.recycle();
|
||||
} finally {
|
||||
if (typedArray != null) {
|
||||
typedArray.recycle();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -115,14 +126,14 @@ public final class AudioView extends FrameLayout implements AudioSlidePlayer.Lis
|
|||
controlToggle.displayQuick(downloadButton);
|
||||
seekBar.setEnabled(false);
|
||||
downloadButton.setOnClickListener(new DownloadClickedListener(audio));
|
||||
if (downloadProgress.isSpinning()) downloadProgress.stopSpinning();
|
||||
if (circleProgress.isSpinning()) circleProgress.stopSpinning();
|
||||
} else if (showControls && audio.getTransferState() == AttachmentDatabase.TRANSFER_PROGRESS_STARTED) {
|
||||
controlToggle.displayQuick(progressAndPlay);
|
||||
seekBar.setEnabled(false);
|
||||
downloadProgress.spin();
|
||||
circleProgress.spin();
|
||||
} else {
|
||||
seekBar.setEnabled(true);
|
||||
if (downloadProgress.isSpinning()) downloadProgress.stopSpinning();
|
||||
if (circleProgress.isSpinning()) circleProgress.stopSpinning();
|
||||
showPlayButton();
|
||||
lottieDirection = REVERSE;
|
||||
playPauseButton.cancelAnimation();
|
||||
|
@ -153,9 +164,9 @@ public final class AudioView extends FrameLayout implements AudioSlidePlayer.Lis
|
|||
isPlaying = false;
|
||||
togglePauseToPlay();
|
||||
|
||||
if (seekBar.getProgress() + 5 >= seekBar.getMax()) {
|
||||
if (autoRewind || seekBar.getProgress() + 5 >= seekBar.getMax()) {
|
||||
backwardsCounter = 4;
|
||||
onProgress(0.0, 0);
|
||||
rewind();
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -187,28 +198,40 @@ public final class AudioView extends FrameLayout implements AudioSlidePlayer.Lis
|
|||
|
||||
@Override
|
||||
public void onProgress(double progress, long millis) {
|
||||
int seekProgress = (int)Math.floor(progress * this.seekBar.getMax());
|
||||
int seekProgress = (int) Math.floor(progress * seekBar.getMax());
|
||||
|
||||
if (seekProgress > seekBar.getProgress() || backwardsCounter > 3) {
|
||||
backwardsCounter = 0;
|
||||
this.seekBar.setProgress(seekProgress);
|
||||
this.timestamp.setText(String.format(Locale.getDefault(), "%02d:%02d",
|
||||
TimeUnit.MILLISECONDS.toMinutes(millis),
|
||||
TimeUnit.MILLISECONDS.toSeconds(millis)));
|
||||
seekBar.setProgress(seekProgress);
|
||||
updateProgress((float) progress, millis);
|
||||
} else {
|
||||
backwardsCounter++;
|
||||
}
|
||||
}
|
||||
|
||||
private void updateProgress(float progress, long millis) {
|
||||
if (timestamp != null) {
|
||||
timestamp.setText(String.format(Locale.getDefault(), "%02d:%02d",
|
||||
TimeUnit.MILLISECONDS.toMinutes(millis),
|
||||
TimeUnit.MILLISECONDS.toSeconds(millis)));
|
||||
}
|
||||
|
||||
if (smallView) {
|
||||
circleProgress.setInstantProgress(seekBar.getProgress() == 0 ? 1 : progress);
|
||||
}
|
||||
}
|
||||
|
||||
public void setTint(int foregroundTint, int backgroundTint) {
|
||||
this.playPauseButton.addValueCallback(new KeyPath("**"),
|
||||
LottieProperty.COLOR_FILTER,
|
||||
new LottieValueCallback<>(new SimpleColorFilter(foregroundTint)));
|
||||
|
||||
this.downloadButton.setColorFilter(foregroundTint, PorterDuff.Mode.SRC_IN);
|
||||
this.downloadProgress.setBarColor(foregroundTint);
|
||||
this.circleProgress.setBarColor(foregroundTint);
|
||||
|
||||
this.timestamp.setTextColor(foregroundTint);
|
||||
if (this.timestamp != null) {
|
||||
this.timestamp.setTextColor(foregroundTint);
|
||||
}
|
||||
this.seekBar.getProgressDrawable().setColorFilter(foregroundTint, PorterDuff.Mode.SRC_IN);
|
||||
this.seekBar.getThumb().setColorFilter(foregroundTint, PorterDuff.Mode.SRC_IN);
|
||||
}
|
||||
|
@ -247,12 +270,22 @@ public final class AudioView extends FrameLayout implements AudioSlidePlayer.Lis
|
|||
}
|
||||
|
||||
private void showPlayButton() {
|
||||
downloadProgress.setInstantProgress(1);
|
||||
downloadProgress.setVisibility(VISIBLE);
|
||||
if (!smallView || seekBar.getProgress() == 0) {
|
||||
circleProgress.setInstantProgress(1);
|
||||
}
|
||||
circleProgress.setVisibility(VISIBLE);
|
||||
playPauseButton.setVisibility(VISIBLE);
|
||||
controlToggle.displayQuick(progressAndPlay);
|
||||
}
|
||||
|
||||
public void stopPlaybackAndReset() {
|
||||
if (this.audioSlidePlayer != null && isPlaying) {
|
||||
this.audioSlidePlayer.stop();
|
||||
togglePauseToPlay();
|
||||
}
|
||||
rewind();
|
||||
}
|
||||
|
||||
private class PlayPauseClickedListener implements View.OnClickListener {
|
||||
|
||||
@Override
|
||||
|
@ -272,11 +305,19 @@ public final class AudioView extends FrameLayout implements AudioSlidePlayer.Lis
|
|||
if (audioSlidePlayer != null) {
|
||||
togglePauseToPlay();
|
||||
audioSlidePlayer.stop();
|
||||
if (autoRewind) {
|
||||
rewind();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void rewind() {
|
||||
seekBar.setProgress(0);
|
||||
updateProgress(0, 0);
|
||||
}
|
||||
|
||||
private class DownloadClickedListener implements View.OnClickListener {
|
||||
private final @NonNull AudioSlide slide;
|
||||
|
||||
|
@ -327,7 +368,7 @@ public final class AudioView extends FrameLayout implements AudioSlidePlayer.Lis
|
|||
@Subscribe(sticky = true, threadMode = ThreadMode.MAIN)
|
||||
public void onEventAsync(final PartProgressEvent event) {
|
||||
if (audioSlidePlayer != null && event.attachment.equals(audioSlidePlayer.getAudioSlide().asAttachment())) {
|
||||
downloadProgress.setInstantProgress(((float) event.progress) / event.total);
|
||||
circleProgress.setInstantProgress(((float) event.progress) / event.total);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -5,9 +5,6 @@ import android.content.Context;
|
|||
import android.content.res.TypedArray;
|
||||
import android.graphics.Color;
|
||||
import android.graphics.PorterDuff;
|
||||
import androidx.annotation.AttrRes;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import android.util.AttributeSet;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
|
@ -15,6 +12,10 @@ import android.widget.FrameLayout;
|
|||
import android.widget.ImageView;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.annotation.AttrRes;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import com.pnikosis.materialishprogress.ProgressWheel;
|
||||
|
||||
import org.greenrobot.eventbus.Subscribe;
|
||||
|
@ -22,10 +23,9 @@ import org.greenrobot.eventbus.ThreadMode;
|
|||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.database.AttachmentDatabase;
|
||||
import org.thoughtcrime.securesms.events.PartProgressEvent;
|
||||
import org.thoughtcrime.securesms.mms.DocumentSlide;
|
||||
import org.thoughtcrime.securesms.mms.Slide;
|
||||
import org.thoughtcrime.securesms.mms.SlideClickListener;
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
import org.whispersystems.libsignal.util.guava.Optional;
|
||||
|
||||
public class DocumentView extends FrameLayout {
|
||||
|
||||
|
@ -42,7 +42,7 @@ public class DocumentView extends FrameLayout {
|
|||
|
||||
private @Nullable SlideClickListener downloadListener;
|
||||
private @Nullable SlideClickListener viewListener;
|
||||
private @Nullable DocumentSlide documentSlide;
|
||||
private @Nullable Slide documentSlide;
|
||||
|
||||
public DocumentView(@NonNull Context context) {
|
||||
this(context, null);
|
||||
|
@ -87,7 +87,7 @@ public class DocumentView extends FrameLayout {
|
|||
this.viewListener = listener;
|
||||
}
|
||||
|
||||
public void setDocument(final @NonNull DocumentSlide documentSlide,
|
||||
public void setDocument(final @NonNull Slide documentSlide,
|
||||
final boolean showControls)
|
||||
{
|
||||
if (showControls && documentSlide.isPendingDownload()) {
|
||||
|
@ -104,9 +104,11 @@ public class DocumentView extends FrameLayout {
|
|||
|
||||
this.documentSlide = documentSlide;
|
||||
|
||||
this.fileName.setText(documentSlide.getFileName().or(getContext().getString(R.string.DocumentView_unknown_file)));
|
||||
this.fileName.setText(documentSlide.getFileName()
|
||||
.or(documentSlide.getCaption())
|
||||
.or(getContext().getString(R.string.DocumentView_unnamed_file)));
|
||||
this.fileSize.setText(Util.getPrettyFileSize(documentSlide.getFileSize()));
|
||||
this.document.setText(getFileType(documentSlide.getFileName()));
|
||||
this.document.setText(documentSlide.getFileType(getContext()).or("").toLowerCase());
|
||||
this.setOnClickListener(new OpenClickedListener(documentSlide));
|
||||
}
|
||||
|
||||
|
@ -128,24 +130,6 @@ public class DocumentView extends FrameLayout {
|
|||
this.downloadButton.setEnabled(enabled);
|
||||
}
|
||||
|
||||
private @NonNull String getFileType(Optional<String> fileName) {
|
||||
if (!fileName.isPresent()) return "";
|
||||
|
||||
String[] parts = fileName.get().split("\\.");
|
||||
|
||||
if (parts.length < 2) {
|
||||
return "";
|
||||
}
|
||||
|
||||
String suffix = parts[parts.length - 1];
|
||||
|
||||
if (suffix.length() <= 3) {
|
||||
return suffix;
|
||||
}
|
||||
|
||||
return "";
|
||||
}
|
||||
|
||||
@Subscribe(sticky = true, threadMode = ThreadMode.MAIN)
|
||||
public void onEventAsync(final PartProgressEvent event) {
|
||||
if (documentSlide != null && event.attachment.equals(documentSlide.asAttachment())) {
|
||||
|
@ -154,9 +138,9 @@ public class DocumentView extends FrameLayout {
|
|||
}
|
||||
|
||||
private class DownloadClickedListener implements View.OnClickListener {
|
||||
private final @NonNull DocumentSlide slide;
|
||||
private final @NonNull Slide slide;
|
||||
|
||||
private DownloadClickedListener(@NonNull DocumentSlide slide) {
|
||||
private DownloadClickedListener(@NonNull Slide slide) {
|
||||
this.slide = slide;
|
||||
}
|
||||
|
||||
|
@ -167,9 +151,9 @@ public class DocumentView extends FrameLayout {
|
|||
}
|
||||
|
||||
private class OpenClickedListener implements View.OnClickListener {
|
||||
private final @NonNull DocumentSlide slide;
|
||||
private final @NonNull Slide slide;
|
||||
|
||||
private OpenClickedListener(@NonNull DocumentSlide slide) {
|
||||
private OpenClickedListener(@NonNull Slide slide) {
|
||||
this.slide = slide;
|
||||
}
|
||||
|
||||
|
|
|
@ -4,17 +4,15 @@ import android.content.Context;
|
|||
import android.content.res.TypedArray;
|
||||
import android.graphics.Bitmap;
|
||||
import android.net.Uri;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.UiThread;
|
||||
import android.util.AttributeSet;
|
||||
|
||||
import org.thoughtcrime.securesms.blurhash.BlurHash;
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.FrameLayout;
|
||||
import android.widget.ImageView;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.UiThread;
|
||||
|
||||
import com.bumptech.glide.RequestBuilder;
|
||||
import com.bumptech.glide.load.engine.DiskCacheStrategy;
|
||||
import com.bumptech.glide.load.resource.bitmap.BitmapTransformation;
|
||||
|
@ -23,7 +21,9 @@ import com.bumptech.glide.load.resource.bitmap.RoundedCorners;
|
|||
import com.bumptech.glide.request.RequestOptions;
|
||||
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.blurhash.BlurHash;
|
||||
import org.thoughtcrime.securesms.database.AttachmentDatabase;
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
import org.thoughtcrime.securesms.mms.DecryptableStreamUriLoader.DecryptableUri;
|
||||
import org.thoughtcrime.securesms.mms.GlideRequest;
|
||||
import org.thoughtcrime.securesms.mms.GlideRequests;
|
||||
|
@ -118,6 +118,26 @@ public class ThumbnailView extends FrameLayout {
|
|||
MeasureSpec.makeMeasureSpec(finalHeight, MeasureSpec.EXACTLY));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
|
||||
super.onSizeChanged(w, h, oldw, oldh);
|
||||
|
||||
float playOverlayScale = 1;
|
||||
float captionIconScale = 1;
|
||||
int playOverlayWidth = playOverlay.getLayoutParams().width;
|
||||
|
||||
if (playOverlayWidth * 2 > getWidth()) {
|
||||
playOverlayScale /= 2;
|
||||
captionIconScale = 0;
|
||||
}
|
||||
|
||||
playOverlay.setScaleX(playOverlayScale);
|
||||
playOverlay.setScaleY(playOverlayScale);
|
||||
|
||||
captionIcon.setScaleX(captionIconScale);
|
||||
captionIcon.setScaleY(captionIconScale);
|
||||
}
|
||||
|
||||
@SuppressWarnings("SuspiciousNameCombination")
|
||||
private void fillTargetDimensions(int[] targetDimens, int[] dimens, int[] bounds) {
|
||||
int dimensFilledCount = getNonZeroCount(dimens);
|
||||
|
@ -291,7 +311,7 @@ public class ThumbnailView extends FrameLayout {
|
|||
SettableFuture<Boolean> result = new SettableFuture<>();
|
||||
boolean resultHandled = false;
|
||||
|
||||
if (slide.hasPlaceholder() && !Objects.equals(slide.getPlaceholderBlur(), previousBlurhash)) {
|
||||
if (slide.hasPlaceholder() && (previousBlurhash == null || !Objects.equals(slide.getPlaceholderBlur(), previousBlurhash))) {
|
||||
buildPlaceholderGlideRequest(glideRequests, slide).into(new GlideBitmapListeningTarget(blurhash, result));
|
||||
resultHandled = true;
|
||||
} else if (!slide.hasPlaceholder()) {
|
||||
|
|
|
@ -85,7 +85,6 @@ import org.thoughtcrime.securesms.ExpirationDialog;
|
|||
import org.thoughtcrime.securesms.GroupCreateActivity;
|
||||
import org.thoughtcrime.securesms.GroupMembersDialog;
|
||||
import org.thoughtcrime.securesms.MainActivity;
|
||||
import org.thoughtcrime.securesms.MediaOverviewActivity;
|
||||
import org.thoughtcrime.securesms.MuteDialog;
|
||||
import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity;
|
||||
import org.thoughtcrime.securesms.PromptMmsActivity;
|
||||
|
@ -128,6 +127,7 @@ import org.thoughtcrime.securesms.contactshare.Contact;
|
|||
import org.thoughtcrime.securesms.contactshare.ContactShareEditActivity;
|
||||
import org.thoughtcrime.securesms.contactshare.ContactUtil;
|
||||
import org.thoughtcrime.securesms.contactshare.SimpleTextWatcher;
|
||||
import org.thoughtcrime.securesms.conversationlist.model.MessageResult;
|
||||
import org.thoughtcrime.securesms.crypto.IdentityKeyParcelable;
|
||||
import org.thoughtcrime.securesms.crypto.SecurityEvent;
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
||||
|
@ -161,6 +161,7 @@ import org.thoughtcrime.securesms.linkpreview.LinkPreviewRepository;
|
|||
import org.thoughtcrime.securesms.linkpreview.LinkPreviewViewModel;
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
import org.thoughtcrime.securesms.maps.PlacePickerActivity;
|
||||
import org.thoughtcrime.securesms.mediaoverview.MediaOverviewActivity;
|
||||
import org.thoughtcrime.securesms.mediasend.Media;
|
||||
import org.thoughtcrime.securesms.mediasend.MediaSendActivity;
|
||||
import org.thoughtcrime.securesms.messagerequests.MessageRequestFragment;
|
||||
|
@ -198,7 +199,6 @@ import org.thoughtcrime.securesms.recipients.RecipientFormattingException;
|
|||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientUtil;
|
||||
import org.thoughtcrime.securesms.registration.RegistrationNavigationActivity;
|
||||
import org.thoughtcrime.securesms.conversationlist.model.MessageResult;
|
||||
import org.thoughtcrime.securesms.service.KeyCachingService;
|
||||
import org.thoughtcrime.securesms.sms.MessageSender;
|
||||
import org.thoughtcrime.securesms.sms.OutgoingEncryptedMessage;
|
||||
|
@ -996,9 +996,7 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
|
|||
}
|
||||
|
||||
private void handleViewMedia() {
|
||||
Intent intent = new Intent(this, MediaOverviewActivity.class);
|
||||
intent.putExtra(MediaOverviewActivity.RECIPIENT_EXTRA, recipient.getId());
|
||||
startActivity(intent);
|
||||
startActivity(MediaOverviewActivity.forThread(this, threadId));
|
||||
}
|
||||
|
||||
private void handleAddShortcut() {
|
||||
|
|
|
@ -40,7 +40,6 @@ import android.text.style.URLSpan;
|
|||
import android.text.util.Linkify;
|
||||
import android.util.AttributeSet;
|
||||
import android.util.TypedValue;
|
||||
import android.view.MotionEvent;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.LinearLayout;
|
||||
|
@ -51,7 +50,6 @@ import androidx.annotation.DimenRes;
|
|||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
import androidx.core.view.ViewCompat;
|
||||
|
||||
import com.annimon.stream.Stream;
|
||||
|
||||
|
@ -70,10 +68,6 @@ import org.thoughtcrime.securesms.components.DocumentView;
|
|||
import org.thoughtcrime.securesms.components.LinkPreviewView;
|
||||
import org.thoughtcrime.securesms.components.Outliner;
|
||||
import org.thoughtcrime.securesms.components.QuoteView;
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
||||
import org.thoughtcrime.securesms.recipients.LiveRecipient;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientForeverObserver;
|
||||
import org.thoughtcrime.securesms.revealable.ViewOnceMessageView;
|
||||
import org.thoughtcrime.securesms.components.SharedContactView;
|
||||
import org.thoughtcrime.securesms.components.StickerView;
|
||||
import org.thoughtcrime.securesms.components.emoji.EmojiTextView;
|
||||
|
@ -88,6 +82,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.database.model.Quote;
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
||||
import org.thoughtcrime.securesms.jobs.AttachmentDownloadJob;
|
||||
import org.thoughtcrime.securesms.jobs.MmsDownloadJob;
|
||||
import org.thoughtcrime.securesms.jobs.MmsSendJob;
|
||||
|
@ -102,7 +97,10 @@ import org.thoughtcrime.securesms.mms.Slide;
|
|||
import org.thoughtcrime.securesms.mms.SlideClickListener;
|
||||
import org.thoughtcrime.securesms.mms.SlidesClickedListener;
|
||||
import org.thoughtcrime.securesms.mms.TextSlide;
|
||||
import org.thoughtcrime.securesms.recipients.LiveRecipient;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientForeverObserver;
|
||||
import org.thoughtcrime.securesms.revealable.ViewOnceMessageView;
|
||||
import org.thoughtcrime.securesms.revealable.ViewOnceUtil;
|
||||
import org.thoughtcrime.securesms.stickers.StickerUrl;
|
||||
import org.thoughtcrime.securesms.util.DateUtils;
|
||||
|
@ -1291,8 +1289,7 @@ public class ConversationItem extends LinearLayout implements BindableConversati
|
|||
Intent intent = new Intent(context, MediaPreviewActivity.class);
|
||||
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
|
||||
intent.setDataAndType(slide.getUri(), slide.getContentType());
|
||||
intent.putExtra(MediaPreviewActivity.RECIPIENT_EXTRA, conversationRecipient.getId());
|
||||
intent.putExtra(MediaPreviewActivity.OUTGOING_EXTRA, messageRecord.isOutgoing());
|
||||
intent.putExtra(MediaPreviewActivity.THREAD_ID_EXTRA, messageRecord.getThreadId());
|
||||
intent.putExtra(MediaPreviewActivity.DATE_EXTRA, messageRecord.getTimestamp());
|
||||
intent.putExtra(MediaPreviewActivity.SIZE_EXTRA, slide.asAttachment().getSize());
|
||||
intent.putExtra(MediaPreviewActivity.CAPTION_EXTRA, slide.getCaption().orNull());
|
||||
|
|
|
@ -19,20 +19,21 @@ package org.thoughtcrime.securesms.database;
|
|||
import android.content.Context;
|
||||
import android.database.Cursor;
|
||||
import android.database.DataSetObserver;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.VisibleForTesting;
|
||||
import androidx.recyclerview.widget.DiffUtil;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
import androidx.recyclerview.widget.RecyclerView.ViewHolder;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
|
||||
/**
|
||||
* RecyclerView.Adapter that manages a Cursor, comparable to the CursorAdapter usable in ListView/GridView.
|
||||
*/
|
||||
public abstract class CursorRecyclerViewAdapter<VH extends RecyclerView.ViewHolder> extends RecyclerView.Adapter<RecyclerView.ViewHolder> {
|
||||
private final Context context;
|
||||
private final @NonNull Context context;
|
||||
private final DataSetObserver observer = new AdapterDataSetObserver();
|
||||
|
||||
@VisibleForTesting static final int HEADER_TYPE = Integer.MIN_VALUE;
|
||||
|
@ -40,7 +41,7 @@ public abstract class CursorRecyclerViewAdapter<VH extends RecyclerView.ViewHold
|
|||
@VisibleForTesting static final long HEADER_ID = Long.MIN_VALUE;
|
||||
@VisibleForTesting static final long FOOTER_ID = Long.MIN_VALUE + 1;
|
||||
|
||||
private Cursor cursor;
|
||||
private @Nullable Cursor cursor;
|
||||
private boolean valid;
|
||||
private @Nullable View header;
|
||||
private @Nullable View footer;
|
||||
|
@ -51,7 +52,7 @@ public abstract class CursorRecyclerViewAdapter<VH extends RecyclerView.ViewHold
|
|||
}
|
||||
}
|
||||
|
||||
protected CursorRecyclerViewAdapter(Context context, Cursor cursor) {
|
||||
protected CursorRecyclerViewAdapter(@NonNull Context context, @Nullable Cursor cursor) {
|
||||
this.context = context;
|
||||
this.cursor = cursor;
|
||||
if (cursor != null) {
|
||||
|
|
|
@ -3,19 +3,22 @@ package org.thoughtcrime.securesms.database;
|
|||
import android.content.Context;
|
||||
import android.database.ContentObserver;
|
||||
import android.database.Cursor;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import net.sqlcipher.database.SQLiteDatabase;
|
||||
|
||||
import org.thoughtcrime.securesms.attachments.DatabaseAttachment;
|
||||
import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||
import org.thoughtcrime.securesms.util.MediaUtil;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public class MediaDatabase extends Database {
|
||||
|
||||
public static final int ALL_THREADS = -1;
|
||||
|
||||
private static final String BASE_MEDIA_QUERY = "SELECT " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.ROW_ID + " AS " + AttachmentDatabase.ROW_ID + ", "
|
||||
+ AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.CONTENT_TYPE + ", "
|
||||
+ AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.THUMBNAIL_ASPECT_RATIO + ", "
|
||||
|
@ -44,19 +47,28 @@ public class MediaDatabase extends Database {
|
|||
+ MmsDatabase.TABLE_NAME + "." + MmsDatabase.MESSAGE_BOX + ", "
|
||||
+ MmsDatabase.TABLE_NAME + "." + MmsDatabase.DATE_SENT + ", "
|
||||
+ MmsDatabase.TABLE_NAME + "." + MmsDatabase.DATE_RECEIVED + ", "
|
||||
+ MmsDatabase.TABLE_NAME + "." + MmsDatabase.THREAD_ID + ", "
|
||||
+ MmsDatabase.TABLE_NAME + "." + MmsDatabase.RECIPIENT_ID + " "
|
||||
+ "FROM " + AttachmentDatabase.TABLE_NAME + " LEFT JOIN " + MmsDatabase.TABLE_NAME
|
||||
+ " ON " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.MMS_ID + " = " + MmsDatabase.TABLE_NAME + "." + MmsDatabase.ID + " "
|
||||
+ "WHERE " + AttachmentDatabase.MMS_ID + " IN (SELECT " + MmsSmsColumns.ID
|
||||
+ " FROM " + MmsDatabase.TABLE_NAME
|
||||
+ " WHERE " + MmsDatabase.THREAD_ID + " = ?) AND (%s) AND "
|
||||
+ " WHERE " + MmsDatabase.THREAD_ID + " __EQUALITY__ ?) AND (%s) AND "
|
||||
+ MmsDatabase.VIEW_ONCE + " = 0 AND "
|
||||
+ AttachmentDatabase.DATA + " IS NOT NULL AND "
|
||||
+ AttachmentDatabase.QUOTE + " = 0 AND "
|
||||
+ AttachmentDatabase.STICKER_PACK_ID + " IS NULL "
|
||||
+ "ORDER BY " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.ROW_ID + " DESC";
|
||||
+ AttachmentDatabase.STICKER_PACK_ID + " IS NULL ";
|
||||
|
||||
private static final String UNIQUE_MEDIA_QUERY = "SELECT "
|
||||
+ "MAX(" + AttachmentDatabase.SIZE + ") as " + AttachmentDatabase.SIZE + ", "
|
||||
+ AttachmentDatabase.CONTENT_TYPE + " "
|
||||
+ "FROM " + AttachmentDatabase.TABLE_NAME + " "
|
||||
+ "WHERE " + AttachmentDatabase.STICKER_PACK_ID + " IS NULL "
|
||||
+ "GROUP BY " + AttachmentDatabase.DATA;
|
||||
|
||||
private static final String GALLERY_MEDIA_QUERY = String.format(BASE_MEDIA_QUERY, AttachmentDatabase.CONTENT_TYPE + " LIKE 'image/%' OR " + AttachmentDatabase.CONTENT_TYPE + " LIKE 'video/%'");
|
||||
private static final String AUDIO_MEDIA_QUERY = String.format(BASE_MEDIA_QUERY, AttachmentDatabase.CONTENT_TYPE + " LIKE 'audio/%'");
|
||||
private static final String ALL_MEDIA_QUERY = String.format(BASE_MEDIA_QUERY, AttachmentDatabase.CONTENT_TYPE + " NOT LIKE 'text/x-signal-plain'");
|
||||
private static final String DOCUMENT_MEDIA_QUERY = String.format(BASE_MEDIA_QUERY, AttachmentDatabase.CONTENT_TYPE + " NOT LIKE 'image/%' AND " +
|
||||
AttachmentDatabase.CONTENT_TYPE + " NOT LIKE 'video/%' AND " +
|
||||
AttachmentDatabase.CONTENT_TYPE + " NOT LIKE 'audio/%' AND " +
|
||||
|
@ -66,13 +78,46 @@ public class MediaDatabase extends Database {
|
|||
super(context, databaseHelper);
|
||||
}
|
||||
|
||||
public Cursor getGalleryMediaForThread(long threadId) {
|
||||
public @NonNull Cursor getGalleryMediaForThread(long threadId, @NonNull Sorting sorting) {
|
||||
SQLiteDatabase database = databaseHelper.getReadableDatabase();
|
||||
Cursor cursor = database.rawQuery(GALLERY_MEDIA_QUERY, new String[]{threadId+""});
|
||||
String query = sorting.applyToQuery(applyEqualityOperator(threadId, GALLERY_MEDIA_QUERY));
|
||||
String[] args = {threadId + ""};
|
||||
Cursor cursor = database.rawQuery(query, args);
|
||||
setNotifyConverationListeners(cursor, threadId);
|
||||
return cursor;
|
||||
}
|
||||
|
||||
public @NonNull Cursor getDocumentMediaForThread(long threadId, @NonNull Sorting sorting) {
|
||||
SQLiteDatabase database = databaseHelper.getReadableDatabase();
|
||||
String query = sorting.applyToQuery(applyEqualityOperator(threadId, DOCUMENT_MEDIA_QUERY));
|
||||
String[] args = {threadId + ""};
|
||||
Cursor cursor = database.rawQuery(query, args);
|
||||
setNotifyConverationListeners(cursor, threadId);
|
||||
return cursor;
|
||||
}
|
||||
|
||||
public @NonNull Cursor getAudioMediaForThread(long threadId, @NonNull Sorting sorting) {
|
||||
SQLiteDatabase database = databaseHelper.getReadableDatabase();
|
||||
String query = sorting.applyToQuery(applyEqualityOperator(threadId, AUDIO_MEDIA_QUERY));
|
||||
String[] args = {threadId + ""};
|
||||
Cursor cursor = database.rawQuery(query, args);
|
||||
setNotifyConverationListeners(cursor, threadId);
|
||||
return cursor;
|
||||
}
|
||||
|
||||
public @NonNull Cursor getAllMediaForThread(long threadId, @NonNull Sorting sorting) {
|
||||
SQLiteDatabase database = databaseHelper.getReadableDatabase();
|
||||
String query = sorting.applyToQuery(applyEqualityOperator(threadId, ALL_MEDIA_QUERY));
|
||||
String[] args = {threadId + ""};
|
||||
Cursor cursor = database.rawQuery(query, args);
|
||||
setNotifyConverationListeners(cursor, threadId);
|
||||
return cursor;
|
||||
}
|
||||
|
||||
private static String applyEqualityOperator(long threadId, String query) {
|
||||
return query.replace("__EQUALITY__", threadId == ALL_THREADS ? "!=" : "=");
|
||||
}
|
||||
|
||||
public void subscribeToMediaChanges(@NonNull ContentObserver observer) {
|
||||
registerAttachmentListeners(observer);
|
||||
}
|
||||
|
@ -81,23 +126,55 @@ public class MediaDatabase extends Database {
|
|||
context.getContentResolver().unregisterContentObserver(observer);
|
||||
}
|
||||
|
||||
public Cursor getDocumentMediaForThread(long threadId) {
|
||||
SQLiteDatabase database = databaseHelper.getReadableDatabase();
|
||||
Cursor cursor = database.rawQuery(DOCUMENT_MEDIA_QUERY, new String[]{threadId+""});
|
||||
setNotifyConverationListeners(cursor, threadId);
|
||||
return cursor;
|
||||
public StorageBreakdown getStorageBreakdown() {
|
||||
StorageBreakdown storageBreakdown = new StorageBreakdown();
|
||||
SQLiteDatabase database = databaseHelper.getReadableDatabase();
|
||||
|
||||
try (Cursor cursor = database.rawQuery(UNIQUE_MEDIA_QUERY, new String[0])) {
|
||||
int sizeColumn = cursor.getColumnIndexOrThrow(AttachmentDatabase.SIZE);
|
||||
int contentTypeColumn = cursor.getColumnIndexOrThrow(AttachmentDatabase.CONTENT_TYPE);
|
||||
|
||||
while (cursor.moveToNext()) {
|
||||
int size = cursor.getInt(sizeColumn);
|
||||
String type = cursor.getString(contentTypeColumn);
|
||||
|
||||
switch (MediaUtil.getSlideTypeFromContentType(type)) {
|
||||
case GIF:
|
||||
case IMAGE:
|
||||
case MMS:
|
||||
storageBreakdown.photoSize += size;
|
||||
break;
|
||||
case VIDEO:
|
||||
storageBreakdown.videoSize += size;
|
||||
break;
|
||||
case AUDIO:
|
||||
storageBreakdown.audioSize += size;
|
||||
break;
|
||||
case LONG_TEXT:
|
||||
case DOCUMENT:
|
||||
storageBreakdown.documentSize += size;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return storageBreakdown;
|
||||
}
|
||||
|
||||
public static class MediaRecord {
|
||||
|
||||
private final DatabaseAttachment attachment;
|
||||
private final RecipientId recipientId;
|
||||
private final long threadId;
|
||||
private final long date;
|
||||
private final boolean outgoing;
|
||||
|
||||
private MediaRecord(DatabaseAttachment attachment, @NonNull RecipientId recipientId, long date, boolean outgoing) {
|
||||
private MediaRecord(DatabaseAttachment attachment, @NonNull RecipientId recipientId, long threadId, long date, boolean outgoing) {
|
||||
this.attachment = attachment;
|
||||
this.recipientId = recipientId;
|
||||
this.threadId = threadId;
|
||||
this.date = date;
|
||||
this.outgoing = outgoing;
|
||||
}
|
||||
|
@ -106,6 +183,7 @@ public class MediaDatabase extends Database {
|
|||
AttachmentDatabase attachmentDatabase = DatabaseFactory.getAttachmentDatabase(context);
|
||||
List<DatabaseAttachment> attachments = attachmentDatabase.getAttachment(cursor);
|
||||
RecipientId recipientId = RecipientId.from(cursor.getLong(cursor.getColumnIndexOrThrow(MmsDatabase.RECIPIENT_ID)));
|
||||
long threadId = cursor.getLong(cursor.getColumnIndexOrThrow(MmsDatabase.THREAD_ID));
|
||||
boolean outgoing = MessagingDatabase.Types.isOutgoingMessageType(cursor.getLong(cursor.getColumnIndexOrThrow(MmsDatabase.MESSAGE_BOX)));
|
||||
|
||||
long date;
|
||||
|
@ -116,7 +194,7 @@ public class MediaDatabase extends Database {
|
|||
date = cursor.getLong(cursor.getColumnIndexOrThrow(MmsDatabase.DATE_RECEIVED));
|
||||
}
|
||||
|
||||
return new MediaRecord(attachments != null && attachments.size() > 0 ? attachments.get(0) : null, recipientId, date, outgoing);
|
||||
return new MediaRecord(attachments != null && attachments.size() > 0 ? attachments.get(0) : null, recipientId, threadId, date, outgoing);
|
||||
}
|
||||
|
||||
public DatabaseAttachment getAttachment() {
|
||||
|
@ -131,6 +209,10 @@ public class MediaDatabase extends Database {
|
|||
return recipientId;
|
||||
}
|
||||
|
||||
public long getThreadId() {
|
||||
return threadId;
|
||||
}
|
||||
|
||||
public long getDate() {
|
||||
return date;
|
||||
}
|
||||
|
@ -138,8 +220,48 @@ public class MediaDatabase extends Database {
|
|||
public boolean isOutgoing() {
|
||||
return outgoing;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public enum Sorting {
|
||||
Newest (AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.ROW_ID + " DESC"),
|
||||
Oldest (AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.ROW_ID + " ASC" ),
|
||||
Largest(AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.SIZE + " DESC");
|
||||
|
||||
private final String postFix;
|
||||
|
||||
Sorting(@NonNull String order) {
|
||||
postFix = " ORDER BY " + order;
|
||||
}
|
||||
|
||||
private String applyToQuery(@NonNull String query) {
|
||||
return query + postFix;
|
||||
}
|
||||
|
||||
public boolean isRelatedToFileSize() {
|
||||
return this == Largest;
|
||||
}
|
||||
}
|
||||
|
||||
public final static class StorageBreakdown {
|
||||
private long photoSize;
|
||||
private long videoSize;
|
||||
private long audioSize;
|
||||
private long documentSize;
|
||||
|
||||
public long getPhotoSize() {
|
||||
return photoSize;
|
||||
}
|
||||
|
||||
public long getVideoSize() {
|
||||
return videoSize;
|
||||
}
|
||||
|
||||
public long getAudioSize() {
|
||||
return audioSize;
|
||||
}
|
||||
|
||||
public long getDocumentSize() {
|
||||
return documentSize;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -532,49 +532,52 @@ public class ThreadDatabase extends Database {
|
|||
}
|
||||
}
|
||||
|
||||
public long getThreadIdFor(Recipient recipient) {
|
||||
public long getThreadIdFor(@NonNull Recipient recipient) {
|
||||
return getThreadIdFor(recipient, DistributionTypes.DEFAULT);
|
||||
}
|
||||
|
||||
public long getThreadIdFor(Recipient recipient, int distributionType) {
|
||||
SQLiteDatabase db = databaseHelper.getReadableDatabase();
|
||||
String where = RECIPIENT_ID + " = ?";
|
||||
String[] recipientsArg = new String[]{recipient.getId().serialize()};
|
||||
Cursor cursor = null;
|
||||
|
||||
try {
|
||||
cursor = db.query(TABLE_NAME, new String[]{ID}, where, recipientsArg, null, null, null);
|
||||
|
||||
if (cursor != null && cursor.moveToFirst()) {
|
||||
return cursor.getLong(cursor.getColumnIndexOrThrow(ID));
|
||||
} else {
|
||||
return createThreadForRecipient(recipient.getId(), recipient.isGroup(), distributionType);
|
||||
}
|
||||
} finally {
|
||||
if (cursor != null)
|
||||
cursor.close();
|
||||
public long getThreadIdFor(@NonNull Recipient recipient, int distributionType) {
|
||||
Long threadId = getThreadIdFor(recipient.getId());
|
||||
if (threadId != null) {
|
||||
return threadId;
|
||||
} else {
|
||||
return createThreadForRecipient(recipient.getId(), recipient.isGroup(), distributionType);
|
||||
}
|
||||
}
|
||||
|
||||
public @Nullable Recipient getRecipientForThreadId(long threadId) {
|
||||
SQLiteDatabase db = databaseHelper.getReadableDatabase();
|
||||
Cursor cursor = null;
|
||||
public Long getThreadIdFor(@NonNull RecipientId recipientId) {
|
||||
SQLiteDatabase db = databaseHelper.getReadableDatabase();
|
||||
String where = RECIPIENT_ID + " = ?";
|
||||
String[] recipientsArg = new String[]{recipientId.serialize()};
|
||||
|
||||
try {
|
||||
cursor = db.query(TABLE_NAME, null, ID + " = ?", new String[] {threadId+""}, null, null, null);
|
||||
try (Cursor cursor = db.query(TABLE_NAME, new String[]{ ID }, where, recipientsArg, null, null, null)) {
|
||||
if (cursor != null && cursor.moveToFirst()) {
|
||||
return cursor.getLong(cursor.getColumnIndexOrThrow(ID));
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public @Nullable RecipientId getRecipientIdForThreadId(long threadId) {
|
||||
SQLiteDatabase db = databaseHelper.getReadableDatabase();
|
||||
|
||||
try (Cursor cursor = db.query(TABLE_NAME, null, ID + " = ?", new String[]{ threadId + "" }, null, null, null)) {
|
||||
|
||||
if (cursor != null && cursor.moveToFirst()) {
|
||||
RecipientId id = RecipientId.from(cursor.getLong(cursor.getColumnIndexOrThrow(RECIPIENT_ID)));
|
||||
return Recipient.resolved(id);
|
||||
return RecipientId.from(cursor.getLong(cursor.getColumnIndexOrThrow(RECIPIENT_ID)));
|
||||
}
|
||||
} finally {
|
||||
if (cursor != null)
|
||||
cursor.close();
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public @Nullable Recipient getRecipientForThreadId(long threadId) {
|
||||
RecipientId id = getRecipientIdForThreadId(threadId);
|
||||
if (id == null) return null;
|
||||
return Recipient.resolved(id);
|
||||
}
|
||||
|
||||
public void setHasSent(long threadId, boolean hasSent) {
|
||||
ContentValues contentValues = new ContentValues(1);
|
||||
contentValues.put(HAS_SENT, hasSent ? 1 : 0);
|
||||
|
|
|
@ -1,228 +0,0 @@
|
|||
package org.thoughtcrime.securesms.database.loaders;
|
||||
|
||||
|
||||
import android.content.Context;
|
||||
import android.database.ContentObserver;
|
||||
import android.database.Cursor;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.loader.content.AsyncTaskLoader;
|
||||
|
||||
import com.annimon.stream.Stream;
|
||||
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
||||
import org.thoughtcrime.securesms.database.MediaDatabase;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Calendar;
|
||||
import java.util.Collections;
|
||||
import java.util.Date;
|
||||
import java.util.HashMap;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
|
||||
public class BucketedThreadMediaLoader extends AsyncTaskLoader<BucketedThreadMediaLoader.BucketedThreadMedia> {
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
private static final String TAG = BucketedThreadMediaLoader.class.getSimpleName();
|
||||
|
||||
private final RecipientId recipientId;
|
||||
private final ContentObserver observer;
|
||||
|
||||
public BucketedThreadMediaLoader(@NonNull Context context, @NonNull RecipientId recipientId) {
|
||||
super(context);
|
||||
this.recipientId = recipientId;
|
||||
this.observer = new ForceLoadContentObserver();
|
||||
|
||||
onContentChanged();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onStartLoading() {
|
||||
if (takeContentChanged()) {
|
||||
forceLoad();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onStopLoading() {
|
||||
cancelLoad();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onAbandon() {
|
||||
DatabaseFactory.getMediaDatabase(getContext()).unsubscribeToMediaChanges(observer);
|
||||
}
|
||||
|
||||
@Override
|
||||
public BucketedThreadMedia loadInBackground() {
|
||||
BucketedThreadMedia result = new BucketedThreadMedia(getContext());
|
||||
long threadId = DatabaseFactory.getThreadDatabase(getContext()).getThreadIdFor(Recipient.resolved(recipientId));
|
||||
|
||||
DatabaseFactory.getMediaDatabase(getContext()).subscribeToMediaChanges(observer);
|
||||
try (Cursor cursor = DatabaseFactory.getMediaDatabase(getContext()).getGalleryMediaForThread(threadId)) {
|
||||
while (cursor != null && cursor.moveToNext()) {
|
||||
result.add(MediaDatabase.MediaRecord.from(getContext(), cursor));
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
public static class BucketedThreadMedia {
|
||||
|
||||
private final TimeBucket TODAY;
|
||||
private final TimeBucket YESTERDAY;
|
||||
private final TimeBucket THIS_WEEK;
|
||||
private final TimeBucket THIS_MONTH;
|
||||
private final MonthBuckets OLDER;
|
||||
|
||||
private final TimeBucket[] TIME_SECTIONS;
|
||||
|
||||
public BucketedThreadMedia(@NonNull Context context) {
|
||||
this.TODAY = new TimeBucket(context.getString(R.string.BucketedThreadMedia_Today), TimeBucket.addToCalendar(Calendar.DAY_OF_YEAR, -1), TimeBucket.addToCalendar(Calendar.DAY_OF_YEAR, 1000));
|
||||
this.YESTERDAY = new TimeBucket(context.getString(R.string.BucketedThreadMedia_Yesterday), TimeBucket.addToCalendar(Calendar.DAY_OF_YEAR, -2), TimeBucket.addToCalendar(Calendar.DAY_OF_YEAR, -1));
|
||||
this.THIS_WEEK = new TimeBucket(context.getString(R.string.BucketedThreadMedia_This_week), TimeBucket.addToCalendar(Calendar.DAY_OF_YEAR, -7), TimeBucket.addToCalendar(Calendar.DAY_OF_YEAR, -2));
|
||||
this.THIS_MONTH = new TimeBucket(context.getString(R.string.BucketedThreadMedia_This_month), TimeBucket.addToCalendar(Calendar.DAY_OF_YEAR, -30), TimeBucket.addToCalendar(Calendar.DAY_OF_YEAR, -7));
|
||||
this.TIME_SECTIONS = new TimeBucket[]{TODAY, YESTERDAY, THIS_WEEK, THIS_MONTH};
|
||||
this.OLDER = new MonthBuckets();
|
||||
}
|
||||
|
||||
|
||||
public void add(MediaDatabase.MediaRecord mediaRecord) {
|
||||
for (TimeBucket timeSection : TIME_SECTIONS) {
|
||||
if (timeSection.inRange(mediaRecord.getDate())) {
|
||||
timeSection.add(mediaRecord);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
OLDER.add(mediaRecord);
|
||||
}
|
||||
|
||||
public int getSectionCount() {
|
||||
return (int)Stream.of(TIME_SECTIONS)
|
||||
.filter(timeBucket -> !timeBucket.isEmpty())
|
||||
.count() +
|
||||
OLDER.getSectionCount();
|
||||
}
|
||||
|
||||
public int getSectionItemCount(int section) {
|
||||
List<TimeBucket> activeTimeBuckets = Stream.of(TIME_SECTIONS).filter(timeBucket -> !timeBucket.isEmpty()).toList();
|
||||
|
||||
if (section < activeTimeBuckets.size()) return activeTimeBuckets.get(section).getItemCount();
|
||||
else return OLDER.getSectionItemCount(section - activeTimeBuckets.size());
|
||||
}
|
||||
|
||||
public MediaDatabase.MediaRecord get(int section, int item) {
|
||||
List<TimeBucket> activeTimeBuckets = Stream.of(TIME_SECTIONS).filter(timeBucket -> !timeBucket.isEmpty()).toList();
|
||||
|
||||
if (section < activeTimeBuckets.size()) return activeTimeBuckets.get(section).getItem(item);
|
||||
else return OLDER.getItem(section - activeTimeBuckets.size(), item);
|
||||
}
|
||||
|
||||
public String getName(int section, Locale locale) {
|
||||
List<TimeBucket> activeTimeBuckets = Stream.of(TIME_SECTIONS).filter(timeBucket -> !timeBucket.isEmpty()).toList();
|
||||
|
||||
if (section < activeTimeBuckets.size()) return activeTimeBuckets.get(section).getName();
|
||||
else return OLDER.getName(section - activeTimeBuckets.size(), locale);
|
||||
}
|
||||
|
||||
private static class TimeBucket {
|
||||
|
||||
private final List<MediaDatabase.MediaRecord> records = new LinkedList<>();
|
||||
|
||||
private final long startTime;
|
||||
private final long endtime;
|
||||
private final String name;
|
||||
|
||||
TimeBucket(String name, long startTime, long endtime) {
|
||||
this.name = name;
|
||||
this.startTime = startTime;
|
||||
this.endtime = endtime;
|
||||
}
|
||||
|
||||
void add(MediaDatabase.MediaRecord record) {
|
||||
this.records.add(record);
|
||||
}
|
||||
|
||||
boolean inRange(long timestamp) {
|
||||
return timestamp > startTime && timestamp <= endtime;
|
||||
}
|
||||
|
||||
boolean isEmpty() {
|
||||
return records.isEmpty();
|
||||
}
|
||||
|
||||
int getItemCount() {
|
||||
return records.size();
|
||||
}
|
||||
|
||||
MediaDatabase.MediaRecord getItem(int position) {
|
||||
return records.get(position);
|
||||
}
|
||||
|
||||
String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
static long addToCalendar(int field, int amount) {
|
||||
Calendar calendar = Calendar.getInstance();
|
||||
calendar.add(field, amount);
|
||||
return calendar.getTimeInMillis();
|
||||
}
|
||||
}
|
||||
|
||||
private static class MonthBuckets {
|
||||
|
||||
private final Map<Date, List<MediaDatabase.MediaRecord>> months = new HashMap<>();
|
||||
|
||||
void add(MediaDatabase.MediaRecord record) {
|
||||
Calendar calendar = Calendar.getInstance();
|
||||
calendar.setTimeInMillis(record.getDate());
|
||||
|
||||
int year = calendar.get(Calendar.YEAR) - 1900;
|
||||
int month = calendar.get(Calendar.MONTH);
|
||||
Date date = new Date(year, month, 1);
|
||||
|
||||
if (months.containsKey(date)) {
|
||||
months.get(date).add(record);
|
||||
} else {
|
||||
List<MediaDatabase.MediaRecord> list = new LinkedList<>();
|
||||
list.add(record);
|
||||
months.put(date, list);
|
||||
}
|
||||
}
|
||||
|
||||
int getSectionCount() {
|
||||
return months.size();
|
||||
}
|
||||
|
||||
int getSectionItemCount(int section) {
|
||||
return months.get(getSection(section)).size();
|
||||
}
|
||||
|
||||
MediaDatabase.MediaRecord getItem(int section, int position) {
|
||||
return months.get(getSection(section)).get(position);
|
||||
}
|
||||
|
||||
Date getSection(int section) {
|
||||
ArrayList<Date> keys = new ArrayList<>(months.keySet());
|
||||
Collections.sort(keys, Collections.reverseOrder());
|
||||
|
||||
return keys.get(section);
|
||||
}
|
||||
|
||||
String getName(int section, Locale locale) {
|
||||
Date sectionDate = getSection(section);
|
||||
|
||||
return new SimpleDateFormat("MMMM, yyyy", locale).format(sectionDate);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,313 @@
|
|||
package org.thoughtcrime.securesms.database.loaders;
|
||||
|
||||
import android.content.Context;
|
||||
import android.database.ContentObserver;
|
||||
import android.database.Cursor;
|
||||
import android.util.SparseArray;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.loader.content.AsyncTaskLoader;
|
||||
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
||||
import org.thoughtcrime.securesms.database.MediaDatabase;
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
import org.thoughtcrime.securesms.util.CalendarDateOnly;
|
||||
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.Calendar;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
|
||||
public final class GroupedThreadMediaLoader extends AsyncTaskLoader<GroupedThreadMediaLoader.GroupedThreadMedia> {
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
private static final String TAG = Log.tag(GroupedThreadMediaLoader.class);
|
||||
|
||||
private final ContentObserver observer;
|
||||
private final MediaLoader.MediaType mediaType;
|
||||
private final MediaDatabase.Sorting sorting;
|
||||
private final long threadId;
|
||||
|
||||
public GroupedThreadMediaLoader(@NonNull Context context,
|
||||
long threadId,
|
||||
@NonNull MediaLoader.MediaType mediaType,
|
||||
@NonNull MediaDatabase.Sorting sorting)
|
||||
{
|
||||
super(context);
|
||||
this.threadId = threadId;
|
||||
this.mediaType = mediaType;
|
||||
this.sorting = sorting;
|
||||
this.observer = new ForceLoadContentObserver();
|
||||
|
||||
onContentChanged();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onStartLoading() {
|
||||
if (takeContentChanged()) {
|
||||
forceLoad();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onStopLoading() {
|
||||
cancelLoad();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onAbandon() {
|
||||
DatabaseFactory.getMediaDatabase(getContext()).unsubscribeToMediaChanges(observer);
|
||||
}
|
||||
|
||||
@Override
|
||||
public GroupedThreadMedia loadInBackground() {
|
||||
Context context = getContext();
|
||||
GroupingMethod groupingMethod = sorting.isRelatedToFileSize()
|
||||
? new RoughSizeGroupingMethod(context)
|
||||
: new DateGroupingMethod(context, CalendarDateOnly.getInstance());
|
||||
|
||||
PopulatedGroupedThreadMedia mediaGrouping = new PopulatedGroupedThreadMedia(groupingMethod);
|
||||
|
||||
DatabaseFactory.getMediaDatabase(context).subscribeToMediaChanges(observer);
|
||||
try (Cursor cursor = ThreadMediaLoader.createThreadMediaCursor(context, threadId, mediaType, sorting)) {
|
||||
while (cursor != null && cursor.moveToNext()) {
|
||||
mediaGrouping.add(MediaDatabase.MediaRecord.from(context, cursor));
|
||||
}
|
||||
}
|
||||
|
||||
if (sorting == MediaDatabase.Sorting.Oldest || sorting == MediaDatabase.Sorting.Largest) {
|
||||
return new ReversedGroupedThreadMedia(mediaGrouping);
|
||||
} else {
|
||||
return mediaGrouping;
|
||||
}
|
||||
}
|
||||
|
||||
public interface GroupingMethod {
|
||||
|
||||
int groupForRecord(@NonNull MediaDatabase.MediaRecord mediaRecord);
|
||||
|
||||
@NonNull String groupName(int groupNo);
|
||||
}
|
||||
|
||||
public static class DateGroupingMethod implements GroupingMethod {
|
||||
|
||||
private final Context context;
|
||||
private final long yesterdayStart;
|
||||
private final long todayStart;
|
||||
private final long thisWeekStart;
|
||||
private final long thisMonthStart;
|
||||
|
||||
private static final int TODAY = Integer.MIN_VALUE;
|
||||
private static final int YESTERDAY = Integer.MIN_VALUE + 1;
|
||||
private static final int THIS_WEEK = Integer.MIN_VALUE + 2;
|
||||
private static final int THIS_MONTH = Integer.MIN_VALUE + 3;
|
||||
|
||||
DateGroupingMethod(@NonNull Context context, @NonNull Calendar today) {
|
||||
this.context = context;
|
||||
todayStart = today.getTimeInMillis();
|
||||
yesterdayStart = getTimeInMillis(today, Calendar.DAY_OF_YEAR, -1);
|
||||
thisWeekStart = getTimeInMillis(today, Calendar.DAY_OF_YEAR, -6);
|
||||
thisMonthStart = getTimeInMillis(today, Calendar.DAY_OF_YEAR, -30);
|
||||
}
|
||||
|
||||
private static long getTimeInMillis(@NonNull Calendar now, int field, int offset) {
|
||||
Calendar copy = (Calendar) now.clone();
|
||||
copy.add(field, offset);
|
||||
return copy.getTimeInMillis();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int groupForRecord(@NonNull MediaDatabase.MediaRecord mediaRecord) {
|
||||
long date = mediaRecord.getDate();
|
||||
|
||||
if (date > todayStart) return TODAY;
|
||||
if (date > yesterdayStart) return YESTERDAY;
|
||||
if (date > thisWeekStart) return THIS_WEEK;
|
||||
if (date > thisMonthStart) return THIS_MONTH;
|
||||
|
||||
Calendar calendar = Calendar.getInstance();
|
||||
calendar.setTimeInMillis(date);
|
||||
|
||||
int year = calendar.get(Calendar.YEAR);
|
||||
int month = calendar.get(Calendar.MONTH);
|
||||
|
||||
return -(year * 12 + month);
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull String groupName(int groupNo) {
|
||||
switch (groupNo) {
|
||||
case TODAY:
|
||||
return context.getString(R.string.BucketedThreadMedia_Today);
|
||||
case YESTERDAY:
|
||||
return context.getString(R.string.BucketedThreadMedia_Yesterday);
|
||||
case THIS_WEEK:
|
||||
return context.getString(R.string.BucketedThreadMedia_This_week);
|
||||
case THIS_MONTH:
|
||||
return context.getString(R.string.BucketedThreadMedia_This_month);
|
||||
default:
|
||||
int yearAndMonth = -groupNo;
|
||||
int month = yearAndMonth % 12;
|
||||
int year = yearAndMonth / 12;
|
||||
|
||||
Calendar calendar = Calendar.getInstance();
|
||||
calendar.set(Calendar.YEAR, year);
|
||||
calendar.set(Calendar.MONTH, month);
|
||||
|
||||
return new SimpleDateFormat("MMMM, yyyy", Locale.getDefault()).format(calendar.getTime());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static class RoughSizeGroupingMethod implements GroupingMethod {
|
||||
|
||||
private final String largeDescription;
|
||||
private final String mediumDescription;
|
||||
private final String smallDescription;
|
||||
|
||||
private static final int MB = 1024 * 1024;
|
||||
private static final int SMALL = 0;
|
||||
private static final int MEDIUM = 1;
|
||||
private static final int LARGE = 2;
|
||||
|
||||
RoughSizeGroupingMethod(@NonNull Context context) {
|
||||
smallDescription = context.getString(R.string.BucketedThreadMedia_Small);
|
||||
mediumDescription = context.getString(R.string.BucketedThreadMedia_Medium);
|
||||
largeDescription = context.getString(R.string.BucketedThreadMedia_Large);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int groupForRecord(@NonNull MediaDatabase.MediaRecord mediaRecord) {
|
||||
long size = mediaRecord.getAttachment().getSize();
|
||||
|
||||
if (size < MB) return SMALL;
|
||||
if (size < 20 * MB) return MEDIUM;
|
||||
|
||||
return LARGE;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull String groupName(int groupNo) {
|
||||
switch (groupNo) {
|
||||
case SMALL : return smallDescription;
|
||||
case MEDIUM: return mediumDescription;
|
||||
case LARGE : return largeDescription;
|
||||
default: throw new AssertionError();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static abstract class GroupedThreadMedia {
|
||||
|
||||
public abstract int getSectionCount();
|
||||
|
||||
public abstract int getSectionItemCount(int section);
|
||||
|
||||
public abstract @NonNull MediaDatabase.MediaRecord get(int section, int item);
|
||||
|
||||
public abstract @NonNull String getName(int section);
|
||||
|
||||
}
|
||||
|
||||
public static class EmptyGroupedThreadMedia extends GroupedThreadMedia {
|
||||
|
||||
@Override
|
||||
public int getSectionCount() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getSectionItemCount(int section) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull MediaDatabase.MediaRecord get(int section, int item) {
|
||||
throw new AssertionError();
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull String getName(int section) {
|
||||
throw new AssertionError();
|
||||
}
|
||||
}
|
||||
|
||||
public static class ReversedGroupedThreadMedia extends GroupedThreadMedia {
|
||||
|
||||
private final GroupedThreadMedia decorated;
|
||||
|
||||
ReversedGroupedThreadMedia(@NonNull GroupedThreadMedia decorated) {
|
||||
this.decorated = decorated;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getSectionCount() {
|
||||
return decorated.getSectionCount();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getSectionItemCount(int section) {
|
||||
return decorated.getSectionItemCount(getReversedSection(section));
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull MediaDatabase.MediaRecord get(int section, int item) {
|
||||
return decorated.get(getReversedSection(section), item);
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull String getName(int section) {
|
||||
return decorated.getName(getReversedSection(section));
|
||||
}
|
||||
|
||||
private int getReversedSection(int section) {
|
||||
return decorated.getSectionCount() - 1 - section;
|
||||
}
|
||||
}
|
||||
|
||||
private static class PopulatedGroupedThreadMedia extends GroupedThreadMedia {
|
||||
|
||||
@NonNull
|
||||
private final GroupingMethod groupingMethod;
|
||||
|
||||
private final SparseArray<List<MediaDatabase.MediaRecord>> records = new SparseArray<>();
|
||||
|
||||
private PopulatedGroupedThreadMedia(@NonNull GroupingMethod groupingMethod) {
|
||||
this.groupingMethod = groupingMethod;
|
||||
}
|
||||
|
||||
private void add(@NonNull MediaDatabase.MediaRecord mediaRecord) {
|
||||
int groupNo = groupingMethod.groupForRecord(mediaRecord);
|
||||
|
||||
List<MediaDatabase.MediaRecord> mediaRecords = records.get(groupNo);
|
||||
if (mediaRecords == null) {
|
||||
mediaRecords = new LinkedList<>();
|
||||
records.put(groupNo, mediaRecords);
|
||||
}
|
||||
|
||||
mediaRecords.add(mediaRecord);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getSectionCount() {
|
||||
return records.size();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getSectionItemCount(int section) {
|
||||
return records.get(records.keyAt(section)).size();
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull MediaDatabase.MediaRecord get(int section, int item) {
|
||||
return records.get(records.keyAt(section)).get(item);
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull String getName(int section) {
|
||||
return groupingMethod.groupName(records.keyAt(section));
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,19 @@
|
|||
package org.thoughtcrime.securesms.database.loaders;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
import org.thoughtcrime.securesms.util.AbstractCursorLoader;
|
||||
|
||||
public abstract class MediaLoader extends AbstractCursorLoader {
|
||||
|
||||
MediaLoader(Context context) {
|
||||
super(context);
|
||||
}
|
||||
|
||||
public enum MediaType {
|
||||
GALLERY,
|
||||
DOCUMENT,
|
||||
AUDIO,
|
||||
ALL
|
||||
}
|
||||
}
|
|
@ -1,9 +1,9 @@
|
|||
package org.thoughtcrime.securesms.database.loaders;
|
||||
|
||||
|
||||
import android.content.Context;
|
||||
import android.database.Cursor;
|
||||
import android.net.Uri;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.core.util.Pair;
|
||||
|
@ -11,33 +11,33 @@ import androidx.core.util.Pair;
|
|||
import org.thoughtcrime.securesms.attachments.AttachmentId;
|
||||
import org.thoughtcrime.securesms.database.AttachmentDatabase;
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
||||
import org.thoughtcrime.securesms.database.MediaDatabase.Sorting;
|
||||
import org.thoughtcrime.securesms.mms.PartAuthority;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.util.AsyncLoader;
|
||||
|
||||
public class PagingMediaLoader extends AsyncLoader<Pair<Cursor, Integer>> {
|
||||
public final class PagingMediaLoader extends AsyncLoader<Pair<Cursor, Integer>> {
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
private static final String TAG = PagingMediaLoader.class.getSimpleName();
|
||||
|
||||
private final Recipient recipient;
|
||||
private final Uri uri;
|
||||
private final boolean leftIsRecent;
|
||||
private final Uri uri;
|
||||
private final boolean leftIsRecent;
|
||||
private final Sorting sorting;
|
||||
private final long threadId;
|
||||
|
||||
public PagingMediaLoader(@NonNull Context context, @NonNull Recipient recipient, @NonNull Uri uri, boolean leftIsRecent) {
|
||||
public PagingMediaLoader(@NonNull Context context, long threadId, @NonNull Uri uri, boolean leftIsRecent, @NonNull Sorting sorting) {
|
||||
super(context);
|
||||
this.recipient = recipient;
|
||||
this.threadId = threadId;
|
||||
this.uri = uri;
|
||||
this.leftIsRecent = leftIsRecent;
|
||||
this.sorting = sorting;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public Pair<Cursor, Integer> loadInBackground() {
|
||||
long threadId = DatabaseFactory.getThreadDatabase(getContext()).getThreadIdFor(recipient);
|
||||
Cursor cursor = DatabaseFactory.getMediaDatabase(getContext()).getGalleryMediaForThread(threadId);
|
||||
public @Nullable Pair<Cursor, Integer> loadInBackground() {
|
||||
Cursor cursor = DatabaseFactory.getMediaDatabase(getContext()).getGalleryMediaForThread(threadId, sorting);
|
||||
|
||||
while (cursor != null && cursor.moveToNext()) {
|
||||
while (cursor.moveToNext()) {
|
||||
AttachmentId attachmentId = new AttachmentId(cursor.getLong(cursor.getColumnIndexOrThrow(AttachmentDatabase.ROW_ID)), cursor.getLong(cursor.getColumnIndexOrThrow(AttachmentDatabase.UNIQUE_ID)));
|
||||
Uri attachmentUri = PartAuthority.getAttachmentDataUri(attachmentId);
|
||||
|
||||
|
|
|
@ -0,0 +1,44 @@
|
|||
package org.thoughtcrime.securesms.database.loaders;
|
||||
|
||||
import android.content.Context;
|
||||
import android.database.Cursor;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
||||
import org.thoughtcrime.securesms.database.MediaDatabase;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||
|
||||
/**
|
||||
* It is more efficient to use the {@link ThreadMediaLoader} if you know the thread id already.
|
||||
*/
|
||||
public final class RecipientMediaLoader extends MediaLoader {
|
||||
|
||||
@Nullable private final RecipientId recipientId;
|
||||
@NonNull private final MediaType mediaType;
|
||||
@NonNull private final MediaDatabase.Sorting sorting;
|
||||
|
||||
public RecipientMediaLoader(@NonNull Context context,
|
||||
@Nullable RecipientId recipientId,
|
||||
@NonNull MediaType mediaType,
|
||||
@NonNull MediaDatabase.Sorting sorting)
|
||||
{
|
||||
super(context);
|
||||
this.recipientId = recipientId;
|
||||
this.mediaType = mediaType;
|
||||
this.sorting = sorting;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Cursor getCursor() {
|
||||
if (recipientId == null || recipientId.isUnknown()) return null;
|
||||
|
||||
long threadId = DatabaseFactory.getThreadDatabase(getContext())
|
||||
.getThreadIdFor(Recipient.resolved(recipientId));
|
||||
|
||||
return ThreadMediaLoader.createThreadMediaCursor(context, threadId, mediaType, sorting);
|
||||
}
|
||||
|
||||
}
|
|
@ -1,38 +1,48 @@
|
|||
package org.thoughtcrime.securesms.database.loaders;
|
||||
|
||||
|
||||
import android.content.Context;
|
||||
import android.database.Cursor;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||
import org.thoughtcrime.securesms.util.AbstractCursorLoader;
|
||||
import org.thoughtcrime.securesms.database.MediaDatabase;
|
||||
|
||||
public class ThreadMediaLoader extends AbstractCursorLoader {
|
||||
public final class ThreadMediaLoader extends MediaLoader {
|
||||
|
||||
private final RecipientId recipientId;
|
||||
private final boolean gallery;
|
||||
private final long threadId;
|
||||
@NonNull private final MediaType mediaType;
|
||||
@NonNull private final MediaDatabase.Sorting sorting;
|
||||
|
||||
public ThreadMediaLoader(@NonNull Context context, @NonNull RecipientId recipientId, boolean gallery) {
|
||||
public ThreadMediaLoader(@NonNull Context context,
|
||||
long threadId,
|
||||
@NonNull MediaType mediaType,
|
||||
@NonNull MediaDatabase.Sorting sorting)
|
||||
{
|
||||
super(context);
|
||||
this.recipientId = recipientId;
|
||||
this.gallery = gallery;
|
||||
this.threadId = threadId;
|
||||
this.mediaType = mediaType;
|
||||
this.sorting = sorting;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Cursor getCursor() {
|
||||
if (recipientId.isUnknown()) return null;
|
||||
|
||||
long threadId = DatabaseFactory.getThreadDatabase(getContext()).getThreadIdFor(Recipient.resolved(recipientId));
|
||||
|
||||
if (gallery) return DatabaseFactory.getMediaDatabase(getContext()).getGalleryMediaForThread(threadId);
|
||||
else return DatabaseFactory.getMediaDatabase(getContext()).getDocumentMediaForThread(threadId);
|
||||
return createThreadMediaCursor(context, threadId, mediaType, sorting);
|
||||
}
|
||||
|
||||
public RecipientId getRecipientId() {
|
||||
return recipientId;
|
||||
static Cursor createThreadMediaCursor(@NonNull Context context,
|
||||
long threadId,
|
||||
@NonNull MediaType mediaType,
|
||||
@NonNull MediaDatabase.Sorting sorting) {
|
||||
MediaDatabase mediaDatabase = DatabaseFactory.getMediaDatabase(context);
|
||||
|
||||
switch (mediaType) {
|
||||
case GALLERY : return mediaDatabase.getGalleryMediaForThread(threadId, sorting);
|
||||
case DOCUMENT: return mediaDatabase.getDocumentMediaForThread(threadId, sorting);
|
||||
case AUDIO : return mediaDatabase.getAudioMediaForThread(threadId, sorting);
|
||||
case ALL : return mediaDatabase.getAllMediaForThread(threadId, sorting);
|
||||
default : throw new AssertionError();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
114
src/org/thoughtcrime/securesms/mediaoverview/MediaActions.java
Normal file
114
src/org/thoughtcrime/securesms/mediaoverview/MediaActions.java
Normal file
|
@ -0,0 +1,114 @@
|
|||
package org.thoughtcrime.securesms.mediaoverview;
|
||||
|
||||
import android.Manifest;
|
||||
import android.content.Context;
|
||||
import android.content.res.Resources;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
import androidx.fragment.app.Fragment;
|
||||
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.database.MediaDatabase;
|
||||
import org.thoughtcrime.securesms.permissions.Permissions;
|
||||
import org.thoughtcrime.securesms.util.AttachmentUtil;
|
||||
import org.thoughtcrime.securesms.util.SaveAttachmentTask;
|
||||
import org.thoughtcrime.securesms.util.task.ProgressDialogAsyncTask;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
|
||||
final class MediaActions {
|
||||
|
||||
private MediaActions() {
|
||||
}
|
||||
|
||||
static void handleSaveMedia(@NonNull Fragment fragment,
|
||||
@NonNull Collection<MediaDatabase.MediaRecord> mediaRecords,
|
||||
@Nullable Runnable postExecute)
|
||||
{
|
||||
Context context = fragment.requireContext();
|
||||
|
||||
SaveAttachmentTask.showWarningDialog(context, (dialogInterface, which) -> Permissions.with(fragment)
|
||||
.request(Manifest.permission.WRITE_EXTERNAL_STORAGE, Manifest.permission.READ_EXTERNAL_STORAGE)
|
||||
.ifNecessary()
|
||||
.withPermanentDenialDialog(fragment.getString(R.string.MediaPreviewActivity_signal_needs_the_storage_permission_in_order_to_write_to_external_storage_but_it_has_been_permanently_denied))
|
||||
.onAnyDenied(() -> Toast.makeText(context, R.string.MediaPreviewActivity_unable_to_write_to_external_storage_without_permission, Toast.LENGTH_LONG).show())
|
||||
.onAllGranted(() ->
|
||||
new ProgressDialogAsyncTask<Void, Void, List<SaveAttachmentTask.Attachment>>(context,
|
||||
R.string.MediaOverviewActivity_collecting_attachments,
|
||||
R.string.please_wait)
|
||||
{
|
||||
@Override
|
||||
protected List<SaveAttachmentTask.Attachment> doInBackground(Void... params) {
|
||||
List<SaveAttachmentTask.Attachment> attachments = new LinkedList<>();
|
||||
|
||||
for (MediaDatabase.MediaRecord mediaRecord : mediaRecords) {
|
||||
if (mediaRecord.getAttachment().getDataUri() != null) {
|
||||
attachments.add(new SaveAttachmentTask.Attachment(mediaRecord.getAttachment().getDataUri(),
|
||||
mediaRecord.getContentType(),
|
||||
mediaRecord.getDate(),
|
||||
mediaRecord.getAttachment().getFileName()));
|
||||
}
|
||||
}
|
||||
|
||||
return attachments;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPostExecute(List<SaveAttachmentTask.Attachment> attachments) {
|
||||
super.onPostExecute(attachments);
|
||||
SaveAttachmentTask saveTask = new SaveAttachmentTask(context, attachments.size());
|
||||
saveTask.executeOnExecutor(THREAD_POOL_EXECUTOR,
|
||||
attachments.toArray(new SaveAttachmentTask.Attachment[0]));
|
||||
|
||||
if (postExecute != null) postExecute.run();
|
||||
}
|
||||
}.execute()
|
||||
).execute(), mediaRecords.size());
|
||||
}
|
||||
|
||||
static void handleDeleteMedia(@NonNull Context context,
|
||||
@NonNull Collection<MediaDatabase.MediaRecord> mediaRecords)
|
||||
{
|
||||
int recordCount = mediaRecords.size();
|
||||
Resources res = context.getResources();
|
||||
String confirmTitle = res.getQuantityString(R.plurals.MediaOverviewActivity_Media_delete_confirm_title,
|
||||
recordCount,
|
||||
recordCount);
|
||||
String confirmMessage = res.getQuantityString(R.plurals.MediaOverviewActivity_Media_delete_confirm_message,
|
||||
recordCount,
|
||||
recordCount);
|
||||
|
||||
AlertDialog.Builder builder = new AlertDialog.Builder(context)
|
||||
.setIconAttribute(R.attr.dialog_alert_icon)
|
||||
.setTitle(confirmTitle)
|
||||
.setMessage(confirmMessage)
|
||||
.setCancelable(true);
|
||||
|
||||
builder.setPositiveButton(R.string.delete, (dialogInterface, i) ->
|
||||
new ProgressDialogAsyncTask<MediaDatabase.MediaRecord, Void, Void>(context,
|
||||
R.string.MediaOverviewActivity_Media_delete_progress_title,
|
||||
R.string.MediaOverviewActivity_Media_delete_progress_message)
|
||||
{
|
||||
@Override
|
||||
protected Void doInBackground(MediaDatabase.MediaRecord... records) {
|
||||
if (records == null || records.length == 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
for (MediaDatabase.MediaRecord record : records) {
|
||||
AttachmentUtil.deleteAttachment(context, record.getAttachment());
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
}.execute(mediaRecords.toArray(new MediaDatabase.MediaRecord[0]))
|
||||
);
|
||||
builder.setNegativeButton(android.R.string.cancel, null);
|
||||
builder.show();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,396 @@
|
|||
/*
|
||||
* Copyright (C) 2015 Open Whisper Systems
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.thoughtcrime.securesms.mediaoverview;
|
||||
|
||||
import android.content.Context;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import com.codewaves.stickyheadergrid.StickyHeaderGridAdapter;
|
||||
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.attachments.AttachmentId;
|
||||
import org.thoughtcrime.securesms.components.AudioView;
|
||||
import org.thoughtcrime.securesms.components.ThumbnailView;
|
||||
import org.thoughtcrime.securesms.database.MediaDatabase;
|
||||
import org.thoughtcrime.securesms.database.MediaDatabase.MediaRecord;
|
||||
import org.thoughtcrime.securesms.database.loaders.GroupedThreadMediaLoader.GroupedThreadMedia;
|
||||
import org.thoughtcrime.securesms.mms.AudioSlide;
|
||||
import org.thoughtcrime.securesms.mms.GlideRequests;
|
||||
import org.thoughtcrime.securesms.mms.Slide;
|
||||
import org.thoughtcrime.securesms.util.DateUtils;
|
||||
import org.thoughtcrime.securesms.util.MediaUtil;
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
|
||||
final class MediaGalleryAllAdapter extends StickyHeaderGridAdapter {
|
||||
|
||||
private final Context context;
|
||||
private final GlideRequests glideRequests;
|
||||
private final ItemClickListener itemClickListener;
|
||||
private final Map<AttachmentId, MediaRecord> selected = new HashMap<>();
|
||||
|
||||
private GroupedThreadMedia media;
|
||||
private boolean showFileSizes;
|
||||
private boolean detailView;
|
||||
|
||||
private static final int AUDIO_DETAIL = 1;
|
||||
private static final int GALLERY = 2;
|
||||
private static final int GALLERY_DETAIL = 3;
|
||||
private static final int DOCUMENT_DETAIL = 4;
|
||||
|
||||
public void pause(RecyclerView.ViewHolder holder) {
|
||||
if (holder instanceof AudioDetailViewHolder) {
|
||||
((AudioDetailViewHolder) holder).pause();
|
||||
}
|
||||
}
|
||||
|
||||
private static class HeaderHolder extends HeaderViewHolder {
|
||||
TextView textView;
|
||||
|
||||
HeaderHolder(View itemView) {
|
||||
super(itemView);
|
||||
textView = itemView.findViewById(R.id.text);
|
||||
}
|
||||
}
|
||||
|
||||
MediaGalleryAllAdapter(@NonNull Context context,
|
||||
@NonNull GlideRequests glideRequests,
|
||||
GroupedThreadMedia media,
|
||||
ItemClickListener clickListener,
|
||||
boolean showFileSizes)
|
||||
{
|
||||
this.context = context;
|
||||
this.glideRequests = glideRequests;
|
||||
this.media = media;
|
||||
this.itemClickListener = clickListener;
|
||||
this.showFileSizes = showFileSizes;
|
||||
}
|
||||
|
||||
public void setMedia(GroupedThreadMedia media) {
|
||||
this.media = media;
|
||||
}
|
||||
|
||||
@Override
|
||||
public HeaderViewHolder onCreateHeaderViewHolder(ViewGroup parent, int headerType) {
|
||||
return new HeaderHolder(LayoutInflater.from(context).inflate(R.layout.media_overview_item_header, parent, false));
|
||||
}
|
||||
|
||||
@Override
|
||||
public ItemViewHolder onCreateItemViewHolder(ViewGroup parent, int itemType) {
|
||||
switch (itemType) {
|
||||
case GALLERY:
|
||||
return new GalleryViewHolder(LayoutInflater.from(context).inflate(R.layout.media_overview_gallery_item, parent, false));
|
||||
case GALLERY_DETAIL:
|
||||
return new GalleryDetailViewHolder(LayoutInflater.from(context).inflate(R.layout.media_overview_detail_item_media, parent, false));
|
||||
case AUDIO_DETAIL:
|
||||
return new AudioDetailViewHolder(LayoutInflater.from(context).inflate(R.layout.media_overview_detail_item_audio, parent, false));
|
||||
default:
|
||||
return new DocumentDetailViewHolder(LayoutInflater.from(context).inflate(R.layout.media_overview_detail_item_document, parent, false));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getSectionItemViewType(int section, int offset) {
|
||||
MediaDatabase.MediaRecord mediaRecord = media.get(section, offset);
|
||||
Slide slide = MediaUtil.getSlideForAttachment(context, mediaRecord.getAttachment());
|
||||
|
||||
if (slide.hasAudio()) return AUDIO_DETAIL;
|
||||
if (slide.hasImage() || slide.hasVideo()) return detailView ? GALLERY_DETAIL : GALLERY;
|
||||
if (slide.hasDocument()) return DOCUMENT_DETAIL;
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBindHeaderViewHolder(HeaderViewHolder viewHolder, int section) {
|
||||
((HeaderHolder)viewHolder).textView.setText(media.getName(section));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBindItemViewHolder(ItemViewHolder viewHolder, int section, int offset) {
|
||||
MediaDatabase.MediaRecord mediaRecord = media.get(section, offset);
|
||||
Slide slide = MediaUtil.getSlideForAttachment(context, mediaRecord.getAttachment());
|
||||
|
||||
((SelectableViewHolder)viewHolder).bind(context, mediaRecord, slide);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onViewDetachedFromWindow(@NonNull ViewHolder holder) {
|
||||
super.onViewDetachedFromWindow(holder);
|
||||
if (holder instanceof SelectableViewHolder) {
|
||||
((SelectableViewHolder) holder).detached();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getSectionCount() {
|
||||
return media.getSectionCount();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getSectionItemCount(int section) {
|
||||
return media.getSectionItemCount(section);
|
||||
}
|
||||
|
||||
public void toggleSelection(@NonNull MediaRecord mediaRecord) {
|
||||
AttachmentId attachmentId = mediaRecord.getAttachment().getAttachmentId();
|
||||
MediaDatabase.MediaRecord removed = selected.remove(attachmentId);
|
||||
if (removed == null) {
|
||||
selected.put(attachmentId, mediaRecord);
|
||||
}
|
||||
|
||||
notifyDataSetChanged();
|
||||
}
|
||||
|
||||
public int getSelectedMediaCount() {
|
||||
return selected.size();
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public Collection<MediaRecord> getSelectedMedia() {
|
||||
return new HashSet<>(selected.values());
|
||||
}
|
||||
|
||||
public void clearSelection() {
|
||||
selected.clear();
|
||||
notifyDataSetChanged();
|
||||
}
|
||||
|
||||
void selectAllMedia() {
|
||||
for (int section = 0; section < media.getSectionCount(); section++) {
|
||||
for (int item = 0; item < media.getSectionItemCount(section); item++) {
|
||||
MediaRecord mediaRecord = media.get(section, item);
|
||||
selected.put(mediaRecord.getAttachment().getAttachmentId(), mediaRecord);
|
||||
}
|
||||
}
|
||||
this.notifyDataSetChanged();
|
||||
}
|
||||
|
||||
void setShowFileSizes(boolean showFileSizes) {
|
||||
this.showFileSizes = showFileSizes;
|
||||
}
|
||||
|
||||
void setDetailView(boolean detailView) {
|
||||
this.detailView = detailView;
|
||||
}
|
||||
|
||||
class SelectableViewHolder extends ItemViewHolder {
|
||||
|
||||
private final View selectedIndicator;
|
||||
private MediaDatabase.MediaRecord mediaRecord;
|
||||
|
||||
SelectableViewHolder(@NonNull View itemView) {
|
||||
super(itemView);
|
||||
this.selectedIndicator = itemView.findViewById(R.id.selected_indicator);
|
||||
}
|
||||
|
||||
public void bind(@NonNull Context context, @NonNull MediaDatabase.MediaRecord mediaRecord, @NonNull Slide slide) {
|
||||
this.mediaRecord = mediaRecord;
|
||||
updateSelectedView();
|
||||
}
|
||||
|
||||
private void updateSelectedView() {
|
||||
if (selectedIndicator != null) {
|
||||
selectedIndicator.setVisibility(selected.containsKey(mediaRecord.getAttachment().getAttachmentId()) ? View.VISIBLE : View.GONE);
|
||||
}
|
||||
}
|
||||
|
||||
boolean onLongClick() {
|
||||
itemClickListener.onMediaLongClicked(mediaRecord);
|
||||
updateSelectedView();
|
||||
return true;
|
||||
}
|
||||
|
||||
void detached() {
|
||||
}
|
||||
}
|
||||
|
||||
private class GalleryViewHolder extends SelectableViewHolder {
|
||||
|
||||
private final ThumbnailView thumbnailView;
|
||||
private final TextView imageFileSize;
|
||||
|
||||
GalleryViewHolder(@NonNull View itemView) {
|
||||
super(itemView);
|
||||
this.thumbnailView = itemView.findViewById(R.id.image);
|
||||
this.imageFileSize = itemView.findViewById(R.id.image_file_size);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void bind(@NonNull Context context, @NonNull MediaDatabase.MediaRecord mediaRecord, @NonNull Slide slide) {
|
||||
super.bind(context, mediaRecord, slide);
|
||||
|
||||
if (showFileSizes | detailView) {
|
||||
imageFileSize.setText(Util.getPrettyFileSize(slide.getFileSize()));
|
||||
imageFileSize.setVisibility(View.VISIBLE);
|
||||
} else {
|
||||
imageFileSize.setVisibility(View.GONE);
|
||||
}
|
||||
|
||||
thumbnailView.setImageResource(glideRequests, slide, false, false);
|
||||
|
||||
thumbnailView.setOnClickListener(view -> itemClickListener.onMediaClicked(mediaRecord));
|
||||
thumbnailView.setOnLongClickListener(view -> onLongClick());
|
||||
}
|
||||
|
||||
@Override
|
||||
void detached() {
|
||||
thumbnailView.clear(glideRequests);
|
||||
}
|
||||
}
|
||||
|
||||
private class DetailViewHolder extends SelectableViewHolder {
|
||||
|
||||
protected final View itemView;
|
||||
private final TextView line1;
|
||||
private final TextView line2;
|
||||
|
||||
DetailViewHolder(@NonNull View itemView) {
|
||||
super(itemView);
|
||||
this.line1 = itemView.findViewById(R.id.line1);
|
||||
this.line2 = itemView.findViewById(R.id.line2);
|
||||
this.itemView = itemView;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void bind(@NonNull Context context, @NonNull MediaDatabase.MediaRecord mediaRecord, @NonNull Slide slide) {
|
||||
super.bind(context, mediaRecord, slide);
|
||||
|
||||
line1.setText(getLine1(context, slide));
|
||||
line2.setText(getLine2(mediaRecord, slide));
|
||||
line1.setVisibility(View.VISIBLE);
|
||||
line2.setVisibility(View.VISIBLE);
|
||||
itemView.setOnClickListener(view -> itemClickListener.onMediaClicked(mediaRecord));
|
||||
itemView.setOnLongClickListener(view -> onLongClick());
|
||||
}
|
||||
|
||||
private String getLine1(@NonNull Context context, @NonNull Slide slide) {
|
||||
return slide.getFileName()
|
||||
.or(slide.getCaption())
|
||||
.or(() -> describeUnnamedFile(context, slide));
|
||||
}
|
||||
|
||||
private String getLine2(@NonNull MediaDatabase.MediaRecord mediaRecord, @NonNull Slide slide) {
|
||||
String date = DateUtils.formatDate(Locale.getDefault(), mediaRecord.getDate());
|
||||
return Util.getPrettyFileSize(slide.getFileSize()) + " " + date;
|
||||
}
|
||||
|
||||
protected String describeUnnamedFile(@NonNull Context context, @NonNull Slide slide) {
|
||||
return context.getString(R.string.DocumentView_unnamed_file);
|
||||
}
|
||||
}
|
||||
|
||||
private class DocumentDetailViewHolder extends DetailViewHolder {
|
||||
|
||||
private final TextView documentType;
|
||||
|
||||
DocumentDetailViewHolder(@NonNull View itemView) {
|
||||
super(itemView);
|
||||
this.documentType = itemView.findViewById(R.id.document_extension);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void bind(@NonNull Context context, @NonNull MediaDatabase.MediaRecord mediaRecord, @NonNull Slide slide) {
|
||||
super.bind(context, mediaRecord, slide);
|
||||
|
||||
documentType.setText(slide.getFileType(context).or("").toLowerCase());
|
||||
}
|
||||
}
|
||||
|
||||
private class AudioDetailViewHolder extends DetailViewHolder {
|
||||
|
||||
private final AudioView audioView;
|
||||
|
||||
AudioDetailViewHolder(@NonNull View itemView) {
|
||||
super(itemView);
|
||||
this.audioView = itemView.findViewById(R.id.audio);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void bind(@NonNull Context context, @NonNull MediaDatabase.MediaRecord mediaRecord, @NonNull Slide slide) {
|
||||
super.bind(context, mediaRecord, slide);
|
||||
|
||||
if (!slide.hasAudio()) {
|
||||
throw new AssertionError();
|
||||
}
|
||||
|
||||
audioView.setAudio((AudioSlide) slide, true);
|
||||
audioView.setOnClickListener(view -> itemClickListener.onMediaClicked(mediaRecord));
|
||||
itemView.setOnClickListener(view -> itemClickListener.onMediaClicked(mediaRecord));
|
||||
}
|
||||
|
||||
@Override
|
||||
void detached() {
|
||||
audioView.stopPlaybackAndReset();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String describeUnnamedFile(@NonNull Context context, @NonNull Slide slide) {
|
||||
return context.getString(R.string.DocumentView_audio_file);
|
||||
}
|
||||
|
||||
public void pause() {
|
||||
audioView.stopPlaybackAndReset();
|
||||
}
|
||||
}
|
||||
|
||||
private class GalleryDetailViewHolder extends DetailViewHolder {
|
||||
|
||||
private final ThumbnailView thumbnailView;
|
||||
|
||||
GalleryDetailViewHolder(@NonNull View itemView) {
|
||||
super(itemView);
|
||||
this.thumbnailView = itemView.findViewById(R.id.image);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void bind(@NonNull Context context, @NonNull MediaDatabase.MediaRecord mediaRecord, @NonNull Slide slide) {
|
||||
super.bind(context, mediaRecord, slide);
|
||||
|
||||
thumbnailView.setImageResource(glideRequests, slide, false, false);
|
||||
thumbnailView.setOnClickListener(view -> itemClickListener.onMediaClicked(mediaRecord));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String describeUnnamedFile(@NonNull Context context, @NonNull Slide slide) {
|
||||
if (slide.hasVideo()) return context.getString(R.string.DocumentView_video_file);
|
||||
if (slide.hasImage()) return context.getString(R.string.DocumentView_image_file);
|
||||
return super.describeUnnamedFile(context, slide);
|
||||
}
|
||||
|
||||
@Override
|
||||
void detached() {
|
||||
thumbnailView.clear(glideRequests);
|
||||
}
|
||||
}
|
||||
|
||||
interface ItemClickListener {
|
||||
void onMediaClicked(@NonNull MediaDatabase.MediaRecord mediaRecord);
|
||||
void onMediaLongClicked(MediaDatabase.MediaRecord mediaRecord);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,264 @@
|
|||
/*
|
||||
* Copyright (C) 2015 Open Whisper Systems
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.thoughtcrime.securesms.mediaoverview;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.os.Bundle;
|
||||
import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.StringRes;
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
import androidx.appcompat.widget.Toolbar;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import androidx.fragment.app.FragmentManager;
|
||||
import androidx.fragment.app.FragmentStatePagerAdapter;
|
||||
import androidx.viewpager.widget.ViewPager;
|
||||
|
||||
import com.google.android.material.tabs.TabLayout;
|
||||
|
||||
import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity;
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.components.AnimatingToggle;
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
||||
import org.thoughtcrime.securesms.database.MediaDatabase;
|
||||
import org.thoughtcrime.securesms.database.MediaDatabase.Sorting;
|
||||
import org.thoughtcrime.securesms.database.loaders.MediaLoader;
|
||||
import org.thoughtcrime.securesms.util.DynamicNoActionBarTheme;
|
||||
import org.thoughtcrime.securesms.util.DynamicTheme;
|
||||
import org.thoughtcrime.securesms.util.concurrent.SimpleTask;
|
||||
import org.whispersystems.libsignal.util.Pair;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Activity for displaying media attachments in-app
|
||||
*/
|
||||
public final class MediaOverviewActivity extends PassphraseRequiredActionBarActivity {
|
||||
|
||||
private static final String THREAD_ID_EXTRA = "thread_id";
|
||||
|
||||
private final DynamicTheme dynamicTheme = new DynamicNoActionBarTheme();
|
||||
|
||||
private Toolbar toolbar;
|
||||
private TabLayout tabLayout;
|
||||
private ViewPager viewPager;
|
||||
private TextView sortOrder;
|
||||
private View sortOrderArrow;
|
||||
private Sorting currentSorting;
|
||||
private Boolean currentDetailLayout;
|
||||
private MediaOverviewViewModel model;
|
||||
private AnimatingToggle displayToggle;
|
||||
private View viewGrid;
|
||||
private View viewDetail;
|
||||
private long threadId;
|
||||
|
||||
public static Intent forThread(@NonNull Context context, long threadId) {
|
||||
Intent intent = new Intent(context, MediaOverviewActivity.class);
|
||||
intent.putExtra(MediaOverviewActivity.THREAD_ID_EXTRA, threadId);
|
||||
return intent;
|
||||
}
|
||||
|
||||
public static Intent forAll(@NonNull Context context) {
|
||||
return forThread(context, MediaDatabase.ALL_THREADS);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPreCreate() {
|
||||
dynamicTheme.onCreate(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle bundle, boolean ready) {
|
||||
setContentView(R.layout.media_overview_activity);
|
||||
|
||||
initializeResources();
|
||||
initializeToolbar();
|
||||
|
||||
boolean allThreads = threadId == MediaDatabase.ALL_THREADS;
|
||||
|
||||
tabLayout.setupWithViewPager(viewPager);
|
||||
viewPager.setAdapter(new MediaOverviewPagerAdapter(getSupportFragmentManager()));
|
||||
|
||||
model = MediaOverviewViewModel.getMediaOverviewViewModel(this);
|
||||
model.setSortOrder(allThreads ? Sorting.Largest : Sorting.Newest);
|
||||
model.setDetailLayout(allThreads);
|
||||
model.getSortOrder().observe(this, this::setSorting);
|
||||
model.getDetailLayout().observe(this, this::setDetailLayout);
|
||||
|
||||
sortOrder.setOnClickListener(this::showSortOrderDialog);
|
||||
sortOrderArrow.setOnClickListener(this::showSortOrderDialog);
|
||||
|
||||
displayToggle.setOnClickListener(v -> setDetailLayout(!currentDetailLayout));
|
||||
|
||||
viewPager.addOnPageChangeListener(new ViewPager.SimpleOnPageChangeListener() {
|
||||
@Override
|
||||
public void onPageSelected(int position) {
|
||||
boolean gridToggleEnabled = allowGridSelectionOnPage(position);
|
||||
displayToggle.animate()
|
||||
.alpha(gridToggleEnabled ? 1 : 0)
|
||||
.start();
|
||||
displayToggle.setEnabled(gridToggleEnabled);
|
||||
}
|
||||
});
|
||||
|
||||
viewPager.setCurrentItem(allThreads ? 3 : 0);
|
||||
}
|
||||
|
||||
private static boolean allowGridSelectionOnPage(int page) {
|
||||
return page == 0;
|
||||
}
|
||||
|
||||
private void setSorting(@NonNull Sorting sorting) {
|
||||
if (currentSorting == sorting) return;
|
||||
|
||||
sortOrder.setText(sortingToString(sorting));
|
||||
currentSorting = sorting;
|
||||
model.setSortOrder(sorting);
|
||||
}
|
||||
|
||||
private static @StringRes int sortingToString(@NonNull Sorting sorting) {
|
||||
switch (sorting) {
|
||||
case Oldest : return R.string.MediaOverviewActivity_Oldest;
|
||||
case Newest : return R.string.MediaOverviewActivity_Newest;
|
||||
case Largest : return R.string.MediaOverviewActivity_Storage_used;
|
||||
default : throw new AssertionError();
|
||||
}
|
||||
}
|
||||
|
||||
private void setDetailLayout(@NonNull Boolean detailLayout) {
|
||||
if (currentDetailLayout == detailLayout) return;
|
||||
|
||||
currentDetailLayout = detailLayout;
|
||||
model.setDetailLayout(detailLayout);
|
||||
displayToggle.display(detailLayout ? viewGrid : viewDetail);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onResume() {
|
||||
super.onResume();
|
||||
dynamicTheme.onResume(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onOptionsItemSelected(MenuItem item) {
|
||||
super.onOptionsItemSelected(item);
|
||||
|
||||
if (item.getItemId() == android.R.id.home) {
|
||||
finish();
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private void initializeResources() {
|
||||
Intent intent = getIntent();
|
||||
long threadId = intent.getLongExtra(THREAD_ID_EXTRA, Long.MIN_VALUE);
|
||||
|
||||
if (threadId == Long.MIN_VALUE) throw new AssertionError();
|
||||
|
||||
this.viewPager = findViewById(R.id.pager);
|
||||
this.toolbar = findViewById(R.id.toolbar);
|
||||
this.tabLayout = findViewById(R.id.tab_layout);
|
||||
this.sortOrder = findViewById(R.id.sort_order);
|
||||
this.sortOrderArrow = findViewById(R.id.sort_order_arrow);
|
||||
this.displayToggle = findViewById(R.id.grid_list_toggle);
|
||||
this.viewDetail = findViewById(R.id.view_detail);
|
||||
this.viewGrid = findViewById(R.id.view_grid);
|
||||
this.threadId = threadId;
|
||||
}
|
||||
|
||||
private void initializeToolbar() {
|
||||
setSupportActionBar(toolbar);
|
||||
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
|
||||
|
||||
if (threadId == MediaDatabase.ALL_THREADS) {
|
||||
getSupportActionBar().setTitle(R.string.MediaOverviewActivity_All_storage_use);
|
||||
} else {
|
||||
SimpleTask.run(() -> DatabaseFactory.getThreadDatabase(this).getRecipientForThreadId(threadId),
|
||||
(recipient) -> {
|
||||
if (recipient != null) {
|
||||
getSupportActionBar().setTitle(recipient.toShortString(this));
|
||||
recipient.live().observe(this, r -> getSupportActionBar().setTitle(r.toShortString(this)));
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
public void onEnterMultiSelect() {
|
||||
tabLayout.setEnabled(false);
|
||||
viewPager.setEnabled(false);
|
||||
}
|
||||
|
||||
public void onExitMultiSelect() {
|
||||
tabLayout.setEnabled(true);
|
||||
viewPager.setEnabled(true);
|
||||
}
|
||||
|
||||
private void showSortOrderDialog(View v) {
|
||||
new AlertDialog.Builder(MediaOverviewActivity.this)
|
||||
.setTitle(R.string.MediaOverviewActivity_Sort_by)
|
||||
.setSingleChoiceItems(R.array.MediaOverviewActivity_Sort_by,
|
||||
currentSorting.ordinal(),
|
||||
(dialog, item) -> {
|
||||
setSorting(Sorting.values()[item]);
|
||||
dialog.dismiss();
|
||||
})
|
||||
.create()
|
||||
.show();
|
||||
}
|
||||
|
||||
private class MediaOverviewPagerAdapter extends FragmentStatePagerAdapter {
|
||||
|
||||
private final List<Pair<MediaLoader.MediaType, CharSequence>> pages;
|
||||
|
||||
MediaOverviewPagerAdapter(FragmentManager fragmentManager) {
|
||||
super(fragmentManager, BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT);
|
||||
|
||||
pages = new ArrayList<>(4);
|
||||
pages.add(new Pair<>(MediaLoader.MediaType.GALLERY, getString(R.string.MediaOverviewActivity_Media)));
|
||||
pages.add(new Pair<>(MediaLoader.MediaType.DOCUMENT, getString(R.string.MediaOverviewActivity_Files)));
|
||||
pages.add(new Pair<>(MediaLoader.MediaType.AUDIO, getString(R.string.MediaOverviewActivity_Audio)));
|
||||
pages.add(new Pair<>(MediaLoader.MediaType.ALL, getString(R.string.MediaOverviewActivity_All)));
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull Fragment getItem(int position) {
|
||||
MediaOverviewPageFragment.GridMode gridMode = allowGridSelectionOnPage(position)
|
||||
? MediaOverviewPageFragment.GridMode.FOLLOW_MODEL
|
||||
: MediaOverviewPageFragment.GridMode.FIXED_DETAIL;
|
||||
|
||||
return MediaOverviewPageFragment.newInstance(threadId, pages.get(position).first(), gridMode);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getCount() {
|
||||
return pages.size();
|
||||
}
|
||||
|
||||
@Override
|
||||
public CharSequence getPageTitle(int position) {
|
||||
return pages.get(position).second();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,329 @@
|
|||
package org.thoughtcrime.securesms.mediaoverview;
|
||||
|
||||
import android.content.ActivityNotFoundException;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.res.Configuration;
|
||||
import android.net.Uri;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.view.Window;
|
||||
import android.widget.TextView;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
import androidx.appcompat.view.ActionMode;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import androidx.fragment.app.FragmentActivity;
|
||||
import androidx.loader.app.LoaderManager;
|
||||
import androidx.loader.content.Loader;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import com.codewaves.stickyheadergrid.StickyHeaderGridLayoutManager;
|
||||
|
||||
import org.thoughtcrime.securesms.MediaPreviewActivity;
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.attachments.DatabaseAttachment;
|
||||
import org.thoughtcrime.securesms.database.MediaDatabase;
|
||||
import org.thoughtcrime.securesms.database.loaders.GroupedThreadMediaLoader;
|
||||
import org.thoughtcrime.securesms.database.loaders.MediaLoader;
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
import org.thoughtcrime.securesms.mms.GlideApp;
|
||||
import org.thoughtcrime.securesms.mms.PartAuthority;
|
||||
import org.thoughtcrime.securesms.util.MediaUtil;
|
||||
|
||||
public final class MediaOverviewPageFragment extends Fragment
|
||||
implements MediaGalleryAllAdapter.ItemClickListener,
|
||||
LoaderManager.LoaderCallbacks<GroupedThreadMediaLoader.GroupedThreadMedia>
|
||||
{
|
||||
|
||||
private static final String TAG = Log.tag(MediaOverviewPageFragment.class);
|
||||
|
||||
private static final String THREAD_ID_EXTRA = "thread_id";
|
||||
private static final String MEDIA_TYPE_EXTRA = "media_type";
|
||||
private static final String GRID_MODE = "grid_mode";
|
||||
|
||||
private final ActionModeCallback actionModeCallback = new ActionModeCallback();
|
||||
private MediaDatabase.Sorting sorting = MediaDatabase.Sorting.Newest;
|
||||
private MediaLoader.MediaType mediaType = MediaLoader.MediaType.GALLERY;
|
||||
private long threadId;
|
||||
private TextView noMedia;
|
||||
private RecyclerView recyclerView;
|
||||
private StickyHeaderGridLayoutManager gridManager;
|
||||
private ActionMode actionMode;
|
||||
private boolean detail;
|
||||
private MediaGalleryAllAdapter adapter;
|
||||
private GridMode gridMode;
|
||||
|
||||
public static @NonNull Fragment newInstance(long threadId,
|
||||
@NonNull MediaLoader.MediaType mediaType,
|
||||
@NonNull GridMode gridMode)
|
||||
{
|
||||
MediaOverviewPageFragment mediaOverviewAllFragment = new MediaOverviewPageFragment();
|
||||
Bundle args = new Bundle();
|
||||
args.putLong(THREAD_ID_EXTRA, threadId);
|
||||
args.putInt(MEDIA_TYPE_EXTRA, mediaType.ordinal());
|
||||
args.putInt(GRID_MODE, gridMode.ordinal());
|
||||
mediaOverviewAllFragment.setArguments(args);
|
||||
|
||||
return mediaOverviewAllFragment;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle bundle) {
|
||||
super.onCreate(bundle);
|
||||
|
||||
Bundle arguments = requireArguments();
|
||||
|
||||
threadId = arguments.getLong(THREAD_ID_EXTRA, Long.MIN_VALUE);
|
||||
mediaType = MediaLoader.MediaType.values()[arguments.getInt(MEDIA_TYPE_EXTRA)];
|
||||
gridMode = GridMode.values()[arguments.getInt(GRID_MODE)];
|
||||
|
||||
if (threadId == Long.MIN_VALUE) throw new AssertionError();
|
||||
|
||||
LoaderManager.getInstance(this).initLoader(0, null, this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
|
||||
Context context = requireContext();
|
||||
View view = inflater.inflate(R.layout.media_overview_page_fragment, container, false);
|
||||
|
||||
this.recyclerView = view.findViewById(R.id.media_grid);
|
||||
this.noMedia = view.findViewById(R.id.no_images);
|
||||
this.gridManager = new StickyHeaderGridLayoutManager(getResources().getInteger(R.integer.media_overview_cols));
|
||||
|
||||
this.adapter = new MediaGalleryAllAdapter(context,
|
||||
GlideApp.with(this),
|
||||
new GroupedThreadMediaLoader.EmptyGroupedThreadMedia(),
|
||||
this,
|
||||
sorting.isRelatedToFileSize());
|
||||
this.recyclerView.setAdapter(adapter);
|
||||
this.recyclerView.setLayoutManager(gridManager);
|
||||
this.recyclerView.setHasFixedSize(true);
|
||||
|
||||
MediaOverviewViewModel viewModel = MediaOverviewViewModel.getMediaOverviewViewModel(requireActivity());
|
||||
|
||||
viewModel.getSortOrder()
|
||||
.observe(this, sorting -> {
|
||||
if (sorting != null) {
|
||||
this.sorting = sorting;
|
||||
adapter.setShowFileSizes(sorting.isRelatedToFileSize());
|
||||
LoaderManager.getInstance(this).restartLoader(0, null, this);
|
||||
}
|
||||
});
|
||||
|
||||
if (gridMode == GridMode.FOLLOW_MODEL) {
|
||||
viewModel.getDetailLayout()
|
||||
.observe(this, this::setDetailView);
|
||||
} else {
|
||||
setDetailView(gridMode == GridMode.FIXED_DETAIL);
|
||||
}
|
||||
|
||||
return view;
|
||||
}
|
||||
|
||||
private void setDetailView(boolean detail) {
|
||||
this.detail = detail;
|
||||
adapter.setDetailView(detail);
|
||||
refreshLayoutManager();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onConfigurationChanged(@NonNull Configuration newConfig) {
|
||||
super.onConfigurationChanged(newConfig);
|
||||
if (gridManager != null) {
|
||||
refreshLayoutManager();
|
||||
}
|
||||
}
|
||||
|
||||
private void refreshLayoutManager() {
|
||||
this.gridManager = new StickyHeaderGridLayoutManager(detail ? 1 : getResources().getInteger(R.integer.media_overview_cols));
|
||||
this.recyclerView.setLayoutManager(gridManager);
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull Loader<GroupedThreadMediaLoader.GroupedThreadMedia> onCreateLoader(int i, Bundle bundle) {
|
||||
return new GroupedThreadMediaLoader(requireContext(), threadId, mediaType, sorting);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLoadFinished(@NonNull Loader<GroupedThreadMediaLoader.GroupedThreadMedia> loader, GroupedThreadMediaLoader.GroupedThreadMedia groupedThreadMedia) {
|
||||
((MediaGalleryAllAdapter) recyclerView.getAdapter()).setMedia(groupedThreadMedia);
|
||||
((MediaGalleryAllAdapter) recyclerView.getAdapter()).notifyAllSectionsDataSetChanged();
|
||||
|
||||
noMedia.setVisibility(recyclerView.getAdapter().getItemCount() > 0 ? View.GONE : View.VISIBLE);
|
||||
getActivity().invalidateOptionsMenu();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLoaderReset(@NonNull Loader<GroupedThreadMediaLoader.GroupedThreadMedia> cursorLoader) {
|
||||
((MediaGalleryAllAdapter) recyclerView.getAdapter()).setMedia(new GroupedThreadMediaLoader.EmptyGroupedThreadMedia());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onMediaClicked(@NonNull MediaDatabase.MediaRecord mediaRecord) {
|
||||
if (actionMode != null) {
|
||||
handleMediaMultiSelectClick(mediaRecord);
|
||||
} else {
|
||||
handleMediaPreviewClick(mediaRecord);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPause() {
|
||||
super.onPause();
|
||||
int childCount = recyclerView.getChildCount();
|
||||
for (int i = 0; i < childCount; i++) {
|
||||
adapter.pause(recyclerView.getChildViewHolder(recyclerView.getChildAt(i)));
|
||||
}
|
||||
}
|
||||
|
||||
private void handleMediaMultiSelectClick(@NonNull MediaDatabase.MediaRecord mediaRecord) {
|
||||
MediaGalleryAllAdapter adapter = getListAdapter();
|
||||
|
||||
adapter.toggleSelection(mediaRecord);
|
||||
if (adapter.getSelectedMediaCount() == 0) {
|
||||
actionMode.finish();
|
||||
} else {
|
||||
actionMode.setTitle(String.valueOf(adapter.getSelectedMediaCount()));
|
||||
}
|
||||
}
|
||||
|
||||
private void handleMediaPreviewClick(@NonNull MediaDatabase.MediaRecord mediaRecord) {
|
||||
if (mediaRecord.getAttachment().getDataUri() == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
Context context = getContext();
|
||||
if (context == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
DatabaseAttachment attachment = mediaRecord.getAttachment();
|
||||
|
||||
if (MediaUtil.isVideo(attachment) || MediaUtil.isImage(attachment)) {
|
||||
Intent intent = new Intent(context, MediaPreviewActivity.class);
|
||||
intent.putExtra(MediaPreviewActivity.DATE_EXTRA, mediaRecord.getDate());
|
||||
intent.putExtra(MediaPreviewActivity.SIZE_EXTRA, mediaRecord.getAttachment().getSize());
|
||||
intent.putExtra(MediaPreviewActivity.THREAD_ID_EXTRA, threadId);
|
||||
intent.putExtra(MediaPreviewActivity.LEFT_IS_RECENT_EXTRA, true);
|
||||
intent.putExtra(MediaPreviewActivity.HIDE_ALL_MEDIA_EXTRA, true);
|
||||
intent.putExtra(MediaPreviewActivity.SHOW_THREAD_EXTRA, threadId == MediaDatabase.ALL_THREADS);
|
||||
intent.putExtra(MediaPreviewActivity.SORTING_EXTRA, sorting.ordinal());
|
||||
|
||||
intent.setDataAndType(mediaRecord.getAttachment().getDataUri(), mediaRecord.getContentType());
|
||||
context.startActivity(intent);
|
||||
} else {
|
||||
if (!MediaUtil.isAudio(attachment)) {
|
||||
showFileExternally(context, mediaRecord);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void showFileExternally(@NonNull Context context, @NonNull MediaDatabase.MediaRecord mediaRecord) {
|
||||
Uri uri = mediaRecord.getAttachment().getDataUri();
|
||||
|
||||
Intent intent = new Intent(Intent.ACTION_VIEW);
|
||||
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
|
||||
intent.setDataAndType(PartAuthority.getAttachmentPublicUri(uri), mediaRecord.getContentType());
|
||||
try {
|
||||
context.startActivity(intent);
|
||||
} catch (ActivityNotFoundException e) {
|
||||
Log.w(TAG, "No activity existed to view the media.");
|
||||
Toast.makeText(context, R.string.ConversationItem_unable_to_open_media, Toast.LENGTH_LONG).show();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onMediaLongClicked(MediaDatabase.MediaRecord mediaRecord) {
|
||||
if (actionMode == null) {
|
||||
((MediaGalleryAllAdapter) recyclerView.getAdapter()).toggleSelection(mediaRecord);
|
||||
recyclerView.getAdapter().notifyDataSetChanged();
|
||||
|
||||
enterMultiSelect();
|
||||
}
|
||||
}
|
||||
|
||||
private void handleSelectAllMedia() {
|
||||
getListAdapter().selectAllMedia();
|
||||
actionMode.setTitle(String.valueOf(getListAdapter().getSelectedMediaCount()));
|
||||
}
|
||||
|
||||
private MediaGalleryAllAdapter getListAdapter() {
|
||||
return (MediaGalleryAllAdapter) recyclerView.getAdapter();
|
||||
}
|
||||
|
||||
private void enterMultiSelect() {
|
||||
FragmentActivity activity = requireActivity();
|
||||
actionMode = ((AppCompatActivity) activity).startSupportActionMode(actionModeCallback);
|
||||
((MediaOverviewActivity) activity).onEnterMultiSelect();
|
||||
}
|
||||
|
||||
private class ActionModeCallback implements ActionMode.Callback {
|
||||
|
||||
private int originalStatusBarColor;
|
||||
|
||||
@Override
|
||||
public boolean onCreateActionMode(ActionMode mode, Menu menu) {
|
||||
mode.getMenuInflater().inflate(R.menu.media_overview_context, menu);
|
||||
mode.setTitle("1");
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
||||
Window window = requireActivity().getWindow();
|
||||
originalStatusBarColor = 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) {
|
||||
switch (menuItem.getItemId()) {
|
||||
case R.id.save:
|
||||
MediaActions.handleSaveMedia(MediaOverviewPageFragment.this,
|
||||
getListAdapter().getSelectedMedia(),
|
||||
() -> actionMode.finish());
|
||||
return true;
|
||||
case R.id.delete:
|
||||
MediaActions.handleDeleteMedia(requireContext(), getListAdapter().getSelectedMedia());
|
||||
actionMode.finish();
|
||||
return true;
|
||||
case R.id.select_all:
|
||||
handleSelectAllMedia();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroyActionMode(ActionMode mode) {
|
||||
actionMode = null;
|
||||
getListAdapter().clearSelection();
|
||||
|
||||
FragmentActivity activity = requireActivity();
|
||||
|
||||
((MediaOverviewActivity) activity).onExitMultiSelect();
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
||||
activity.getWindow().setStatusBarColor(originalStatusBarColor);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public enum GridMode {
|
||||
FIXED_DETAIL,
|
||||
FOLLOW_MODEL
|
||||
}
|
||||
}
|
|
@ -0,0 +1,45 @@
|
|||
package org.thoughtcrime.securesms.mediaoverview;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.fragment.app.FragmentActivity;
|
||||
import androidx.lifecycle.LiveData;
|
||||
import androidx.lifecycle.MutableLiveData;
|
||||
import androidx.lifecycle.SavedStateHandle;
|
||||
import androidx.lifecycle.SavedStateViewModelFactory;
|
||||
import androidx.lifecycle.ViewModel;
|
||||
import androidx.lifecycle.ViewModelProviders;
|
||||
|
||||
import org.thoughtcrime.securesms.database.MediaDatabase.Sorting;
|
||||
|
||||
public class MediaOverviewViewModel extends ViewModel {
|
||||
|
||||
private final MutableLiveData<Sorting> sortOrder;
|
||||
private final MutableLiveData<Boolean> detailLayout;
|
||||
|
||||
public MediaOverviewViewModel(@NonNull SavedStateHandle savedStateHandle) {
|
||||
sortOrder = savedStateHandle.getLiveData("SORT_ORDER", Sorting.Newest);
|
||||
detailLayout = savedStateHandle.getLiveData("DETAIL_LAYOUT", false);
|
||||
}
|
||||
|
||||
public LiveData<Sorting> getSortOrder() {
|
||||
return sortOrder;
|
||||
}
|
||||
|
||||
public LiveData<Boolean> getDetailLayout() {
|
||||
return detailLayout;
|
||||
}
|
||||
|
||||
public void setSortOrder(@NonNull Sorting sortOrder) {
|
||||
this.sortOrder.setValue(sortOrder);
|
||||
}
|
||||
|
||||
public void setDetailLayout(boolean detailLayout) {
|
||||
this.detailLayout.setValue(detailLayout);
|
||||
}
|
||||
|
||||
static MediaOverviewViewModel getMediaOverviewViewModel(@NonNull FragmentActivity activity) {
|
||||
SavedStateViewModelFactory savedStateViewModelFactory = new SavedStateViewModelFactory(activity.getApplication(), activity);
|
||||
|
||||
return ViewModelProviders.of(activity, savedStateViewModelFactory).get(MediaOverviewViewModel.class);
|
||||
}
|
||||
}
|
|
@ -7,7 +7,6 @@ import android.view.ViewGroup;
|
|||
import androidx.annotation.NonNull;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import org.thoughtcrime.securesms.MediaDocumentsAdapter;
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.components.FromTextView;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
|
@ -51,7 +50,7 @@ class CameraContactSelectionAdapter extends RecyclerView.Adapter<CameraContactSe
|
|||
notifyDataSetChanged();
|
||||
}
|
||||
|
||||
static class RecipientViewHolder extends MediaDocumentsAdapter.ViewHolder {
|
||||
static class RecipientViewHolder extends RecyclerView.ViewHolder {
|
||||
|
||||
private final FromTextView name;
|
||||
|
||||
|
|
|
@ -43,7 +43,6 @@ import org.thoughtcrime.securesms.MediaPreviewActivity;
|
|||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.TransportOption;
|
||||
import org.thoughtcrime.securesms.attachments.Attachment;
|
||||
import org.thoughtcrime.securesms.color.MaterialColor;
|
||||
import org.thoughtcrime.securesms.blurhash.BlurHash;
|
||||
import org.thoughtcrime.securesms.components.AudioView;
|
||||
import org.thoughtcrime.securesms.components.DocumentView;
|
||||
|
@ -497,7 +496,6 @@ public class AttachmentManager {
|
|||
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
|
||||
intent.putExtra(MediaPreviewActivity.SIZE_EXTRA, slide.asAttachment().getSize());
|
||||
intent.putExtra(MediaPreviewActivity.CAPTION_EXTRA, slide.getCaption().orNull());
|
||||
intent.putExtra(MediaPreviewActivity.OUTGOING_EXTRA, true);
|
||||
intent.setDataAndType(slide.getUri(), slide.getContentType());
|
||||
|
||||
context.startActivity(intent);
|
||||
|
|
|
@ -19,6 +19,7 @@ package org.thoughtcrime.securesms.mms;
|
|||
import android.content.Context;
|
||||
import android.content.res.Resources.Theme;
|
||||
import android.net.Uri;
|
||||
|
||||
import androidx.annotation.DrawableRes;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
@ -172,6 +173,34 @@ public abstract class Slide {
|
|||
null);
|
||||
}
|
||||
|
||||
public @NonNull Optional<String> getFileType(@NonNull Context context) {
|
||||
Optional<String> fileName = getFileName();
|
||||
|
||||
if (fileName.isPresent()) {
|
||||
return Optional.of(getFileType(fileName));
|
||||
}
|
||||
|
||||
return Optional.fromNullable(MediaUtil.getExtension(context, getUri()));
|
||||
}
|
||||
|
||||
private static @NonNull String getFileType(Optional<String> fileName) {
|
||||
if (!fileName.isPresent()) return "";
|
||||
|
||||
String[] parts = fileName.get().split("\\.");
|
||||
|
||||
if (parts.length < 2) {
|
||||
return "";
|
||||
}
|
||||
|
||||
String suffix = parts[parts.length - 1];
|
||||
|
||||
if (suffix.length() <= 3) {
|
||||
return suffix;
|
||||
}
|
||||
|
||||
return "";
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object other) {
|
||||
if (other == null) return false;
|
||||
|
|
|
@ -0,0 +1,48 @@
|
|||
package org.thoughtcrime.securesms.preferences;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.core.content.ContextCompat;
|
||||
import androidx.fragment.app.FragmentActivity;
|
||||
import androidx.lifecycle.LiveData;
|
||||
import androidx.lifecycle.MutableLiveData;
|
||||
import androidx.lifecycle.ViewModel;
|
||||
import androidx.lifecycle.ViewModelProviders;
|
||||
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
||||
import org.thoughtcrime.securesms.database.MediaDatabase;
|
||||
import org.thoughtcrime.securesms.preferences.widgets.StorageGraphView;
|
||||
import org.thoughtcrime.securesms.util.concurrent.SignalExecutors;
|
||||
|
||||
import java.util.Arrays;
|
||||
|
||||
public class ApplicationPreferencesViewModel extends ViewModel {
|
||||
|
||||
private final MutableLiveData<StorageGraphView.StorageBreakdown> storageBreakdown = new MutableLiveData<>();
|
||||
|
||||
LiveData<StorageGraphView.StorageBreakdown> getStorageBreakdown() {
|
||||
return storageBreakdown;
|
||||
}
|
||||
|
||||
static ApplicationPreferencesViewModel getApplicationPreferencesViewModel(@NonNull FragmentActivity activity) {
|
||||
return ViewModelProviders.of(activity).get(ApplicationPreferencesViewModel.class);
|
||||
}
|
||||
|
||||
void refreshStorageBreakdown(@NonNull Context context) {
|
||||
SignalExecutors.BOUNDED.execute(() -> {
|
||||
MediaDatabase.StorageBreakdown breakdown = DatabaseFactory.getMediaDatabase(context)
|
||||
.getStorageBreakdown();
|
||||
|
||||
StorageGraphView.StorageBreakdown latestStorageBreakdown = new StorageGraphView.StorageBreakdown(Arrays.asList(
|
||||
new StorageGraphView.Entry(ContextCompat.getColor(context, R.color.storage_color_photos), breakdown.getPhotoSize()),
|
||||
new StorageGraphView.Entry(ContextCompat.getColor(context, R.color.storage_color_videos), breakdown.getVideoSize()),
|
||||
new StorageGraphView.Entry(ContextCompat.getColor(context, R.color.storage_color_files), breakdown.getDocumentSize()),
|
||||
new StorageGraphView.Entry(ContextCompat.getColor(context, R.color.storage_color_audio), breakdown.getAudioSize())
|
||||
));
|
||||
|
||||
storageBreakdown.postValue(latestStorageBreakdown);
|
||||
});
|
||||
}
|
||||
}
|
|
@ -3,20 +3,17 @@ package org.thoughtcrime.securesms.preferences;
|
|||
import android.Manifest;
|
||||
import android.annotation.SuppressLint;
|
||||
import android.content.Context;
|
||||
import android.content.DialogInterface;
|
||||
import android.os.Bundle;
|
||||
import android.text.TextUtils;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
import androidx.preference.EditTextPreference;
|
||||
import androidx.preference.ListPreference;
|
||||
import androidx.preference.Preference;
|
||||
import android.text.TextUtils;
|
||||
|
||||
import org.greenrobot.eventbus.EventBus;
|
||||
import org.greenrobot.eventbus.Subscribe;
|
||||
import org.greenrobot.eventbus.ThreadMode;
|
||||
import org.thoughtcrime.securesms.ApplicationContext;
|
||||
import org.thoughtcrime.securesms.ApplicationPreferencesActivity;
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.backup.BackupDialog;
|
||||
|
@ -29,7 +26,6 @@ import org.thoughtcrime.securesms.permissions.Permissions;
|
|||
import org.thoughtcrime.securesms.preferences.widgets.ProgressPreference;
|
||||
import org.thoughtcrime.securesms.util.BackupUtil;
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||
import org.thoughtcrime.securesms.util.Trimmer;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
@ -52,11 +48,6 @@ public class ChatsPreferenceFragment extends ListSummaryPreferenceFragment {
|
|||
findPreference(TextSecurePreferences.MESSAGE_BODY_TEXT_SIZE_PREF)
|
||||
.setOnPreferenceChangeListener(new ListSummaryListener());
|
||||
|
||||
findPreference(TextSecurePreferences.THREAD_TRIM_NOW)
|
||||
.setOnPreferenceClickListener(new TrimNowClickListener());
|
||||
findPreference(TextSecurePreferences.THREAD_TRIM_LENGTH)
|
||||
.setOnPreferenceChangeListener(new TrimLengthValidationListener());
|
||||
|
||||
findPreference(TextSecurePreferences.BACKUP_ENABLED)
|
||||
.setOnPreferenceClickListener(new BackupClickListener());
|
||||
findPreference(TextSecurePreferences.BACKUP_NOW)
|
||||
|
@ -171,29 +162,6 @@ public class ChatsPreferenceFragment extends ListSummaryPreferenceFragment {
|
|||
}
|
||||
}
|
||||
|
||||
private class TrimNowClickListener implements Preference.OnPreferenceClickListener {
|
||||
@Override
|
||||
public boolean onPreferenceClick(Preference preference) {
|
||||
final int threadLengthLimit = TextSecurePreferences.getThreadTrimLength(getActivity());
|
||||
AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());
|
||||
builder.setTitle(R.string.ApplicationPreferencesActivity_delete_all_old_messages_now);
|
||||
builder.setMessage(getResources().getQuantityString(R.plurals.ApplicationPreferencesActivity_this_will_immediately_trim_all_conversations_to_the_d_most_recent_messages,
|
||||
threadLengthLimit, threadLengthLimit));
|
||||
builder.setPositiveButton(R.string.ApplicationPreferencesActivity_delete,
|
||||
new DialogInterface.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(DialogInterface dialog, int which) {
|
||||
Trimmer.trimAllThreads(getActivity(), threadLengthLimit);
|
||||
}
|
||||
});
|
||||
|
||||
builder.setNegativeButton(android.R.string.cancel, null);
|
||||
builder.show();
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
private class MediaDownloadChangeListener implements Preference.OnPreferenceChangeListener {
|
||||
@SuppressWarnings("unchecked")
|
||||
@Override public boolean onPreferenceChange(Preference preference, Object newValue) {
|
||||
|
@ -203,36 +171,6 @@ public class ChatsPreferenceFragment extends ListSummaryPreferenceFragment {
|
|||
}
|
||||
}
|
||||
|
||||
private class TrimLengthValidationListener implements Preference.OnPreferenceChangeListener {
|
||||
|
||||
public TrimLengthValidationListener() {
|
||||
EditTextPreference preference = (EditTextPreference)findPreference(TextSecurePreferences.THREAD_TRIM_LENGTH);
|
||||
onPreferenceChange(preference, preference.getText());
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onPreferenceChange(Preference preference, Object newValue) {
|
||||
if (newValue == null || ((String)newValue).trim().length() == 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
int value;
|
||||
try {
|
||||
value = Integer.parseInt((String)newValue);
|
||||
} catch (NumberFormatException nfe) {
|
||||
Log.w(TAG, nfe);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (value < 1) {
|
||||
return false;
|
||||
}
|
||||
|
||||
preference.setSummary(getResources().getQuantityString(R.plurals.ApplicationPreferencesActivity_messages_per_conversation, value, value));
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
public static CharSequence getSummary(Context context) {
|
||||
return null;
|
||||
}
|
||||
|
|
|
@ -0,0 +1,109 @@
|
|||
package org.thoughtcrime.securesms.preferences;
|
||||
|
||||
import android.os.Bundle;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
import androidx.fragment.app.FragmentActivity;
|
||||
import androidx.preference.EditTextPreference;
|
||||
import androidx.preference.Preference;
|
||||
|
||||
import org.thoughtcrime.securesms.ApplicationPreferencesActivity;
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
import org.thoughtcrime.securesms.mediaoverview.MediaOverviewActivity;
|
||||
import org.thoughtcrime.securesms.permissions.Permissions;
|
||||
import org.thoughtcrime.securesms.preferences.widgets.StoragePreferenceCategory;
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||
import org.thoughtcrime.securesms.util.Trimmer;
|
||||
|
||||
public class StoragePreferenceFragment extends ListSummaryPreferenceFragment {
|
||||
|
||||
private static final String TAG = Log.tag(StoragePreferenceFragment.class);
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle paramBundle) {
|
||||
super.onCreate(paramBundle);
|
||||
|
||||
findPreference(TextSecurePreferences.THREAD_TRIM_NOW)
|
||||
.setOnPreferenceClickListener(new TrimNowClickListener());
|
||||
findPreference(TextSecurePreferences.THREAD_TRIM_LENGTH)
|
||||
.setOnPreferenceChangeListener(new TrimLengthValidationListener());
|
||||
|
||||
StoragePreferenceCategory storageCategory = (StoragePreferenceCategory) findPreference("pref_storage_category");
|
||||
FragmentActivity activity = requireActivity();
|
||||
ApplicationPreferencesViewModel viewModel = ApplicationPreferencesViewModel.getApplicationPreferencesViewModel(activity);
|
||||
|
||||
storageCategory.setOnFreeUpSpace(() -> activity.startActivity(MediaOverviewActivity.forAll(activity)));
|
||||
|
||||
viewModel.getStorageBreakdown().observe(activity, storageCategory::setStorage);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreatePreferences(@Nullable Bundle savedInstanceState, String rootKey) {
|
||||
addPreferencesFromResource(R.xml.preferences_storage);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onResume() {
|
||||
super.onResume();
|
||||
((ApplicationPreferencesActivity)getActivity()).getSupportActionBar().setTitle(R.string.preferences__storage);
|
||||
|
||||
FragmentActivity activity = requireActivity();
|
||||
ApplicationPreferencesViewModel viewModel = ApplicationPreferencesViewModel.getApplicationPreferencesViewModel(activity);
|
||||
|
||||
viewModel.refreshStorageBreakdown(activity.getApplicationContext());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
|
||||
Permissions.onRequestPermissionsResult(this, requestCode, permissions, grantResults);
|
||||
}
|
||||
|
||||
private class TrimNowClickListener implements Preference.OnPreferenceClickListener {
|
||||
@Override
|
||||
public boolean onPreferenceClick(Preference preference) {
|
||||
final int threadLengthLimit = TextSecurePreferences.getThreadTrimLength(getActivity());
|
||||
new AlertDialog.Builder(requireActivity())
|
||||
.setTitle(R.string.ApplicationPreferencesActivity_delete_all_old_messages_now)
|
||||
.setMessage(getResources().getQuantityString(R.plurals.ApplicationPreferencesActivity_this_will_immediately_trim_all_conversations_to_the_d_most_recent_messages,
|
||||
threadLengthLimit, threadLengthLimit))
|
||||
.setPositiveButton(R.string.ApplicationPreferencesActivity_delete, (dialog, which) -> Trimmer.trimAllThreads(getActivity(), threadLengthLimit))
|
||||
.setNegativeButton(android.R.string.cancel, null)
|
||||
.show();
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
private class TrimLengthValidationListener implements Preference.OnPreferenceChangeListener {
|
||||
|
||||
TrimLengthValidationListener() {
|
||||
EditTextPreference preference = (EditTextPreference)findPreference(TextSecurePreferences.THREAD_TRIM_LENGTH);
|
||||
onPreferenceChange(preference, preference.getText());
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onPreferenceChange(Preference preference, Object newValue) {
|
||||
if (newValue == null || ((String)newValue).trim().length() == 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
int value;
|
||||
try {
|
||||
value = Integer.parseInt((String)newValue);
|
||||
} catch (NumberFormatException nfe) {
|
||||
Log.w(TAG, nfe);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (value < 1) {
|
||||
return false;
|
||||
}
|
||||
|
||||
preference.setSummary(getResources().getQuantityString(R.plurals.ApplicationPreferencesActivity_messages_per_conversation, value, value));
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,125 @@
|
|||
package org.thoughtcrime.securesms.preferences.widgets;
|
||||
|
||||
import android.content.Context;
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.Paint;
|
||||
import android.graphics.Path;
|
||||
import android.graphics.RectF;
|
||||
import android.util.AttributeSet;
|
||||
import android.view.View;
|
||||
|
||||
import androidx.annotation.ColorInt;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.core.content.ContextCompat;
|
||||
|
||||
import org.thoughtcrime.securesms.R;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
public final class StorageGraphView extends View {
|
||||
|
||||
private final RectF rect = new RectF();
|
||||
private final Path path = new Path();
|
||||
private final Paint paint = new Paint();
|
||||
@NonNull private StorageBreakdown storageBreakdown;
|
||||
private StorageBreakdown emptyBreakdown;
|
||||
|
||||
public StorageGraphView(Context context) {
|
||||
super(context);
|
||||
initialize();
|
||||
}
|
||||
|
||||
public StorageGraphView(Context context, @Nullable AttributeSet attrs) {
|
||||
super(context, attrs);
|
||||
initialize();
|
||||
}
|
||||
|
||||
public StorageGraphView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
|
||||
super(context, attrs, defStyleAttr);
|
||||
initialize();
|
||||
}
|
||||
|
||||
private void initialize() {
|
||||
setWillNotDraw(false);
|
||||
paint.setStyle(Paint.Style.FILL);
|
||||
|
||||
Entry emptyEntry = new Entry(ContextCompat.getColor(getContext(), R.color.storage_color_empty), 1);
|
||||
|
||||
emptyBreakdown = new StorageBreakdown(Collections.singletonList(emptyEntry));
|
||||
|
||||
setStorageBreakdown(emptyBreakdown);
|
||||
}
|
||||
|
||||
public void setStorageBreakdown(@NonNull StorageBreakdown storageBreakdown) {
|
||||
if (storageBreakdown.totalSize == 0) {
|
||||
storageBreakdown = emptyBreakdown;
|
||||
}
|
||||
this.storageBreakdown = storageBreakdown;
|
||||
invalidate();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
|
||||
super.onSizeChanged(w, h, oldw, oldh);
|
||||
int radius = getHeight() / 2;
|
||||
rect.set(0, 0, w, h);
|
||||
path.reset();
|
||||
path.addRoundRect(rect, radius, radius, Path.Direction.CW);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDraw(Canvas canvas) {
|
||||
if (storageBreakdown.totalSize == 0) return;
|
||||
|
||||
canvas.clipPath(path);
|
||||
|
||||
int startX = 0;
|
||||
int entryCount = storageBreakdown.entries.size();
|
||||
int width = getWidth();
|
||||
|
||||
for (int i = 0; i < entryCount; i++) {
|
||||
Entry entry = storageBreakdown.entries.get(i);
|
||||
int endX = i < entryCount - 1 ? startX + (int) (width * entry.size / storageBreakdown.totalSize)
|
||||
: width;
|
||||
rect.left = startX;
|
||||
rect.right = endX;
|
||||
paint.setColor(entry.color);
|
||||
canvas.drawRect(rect, paint);
|
||||
startX = endX;
|
||||
}
|
||||
}
|
||||
|
||||
public static class StorageBreakdown {
|
||||
|
||||
private final List<Entry> entries;
|
||||
private final long totalSize;
|
||||
|
||||
public StorageBreakdown(@NonNull List<Entry> entries) {
|
||||
this.entries = new ArrayList<>(entries);
|
||||
|
||||
long total = 0;
|
||||
for (Entry entry : entries) {
|
||||
total += entry.size;
|
||||
}
|
||||
this.totalSize = total;
|
||||
}
|
||||
|
||||
long getTotalSize() {
|
||||
return totalSize;
|
||||
}
|
||||
}
|
||||
|
||||
public static class Entry {
|
||||
|
||||
@ColorInt private final int color;
|
||||
private final long size;
|
||||
|
||||
public Entry(@ColorInt int color, long size) {
|
||||
this.color = color;
|
||||
this.size = size;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,73 @@
|
|||
package org.thoughtcrime.securesms.preferences.widgets;
|
||||
|
||||
import android.content.Context;
|
||||
import android.util.AttributeSet;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.preference.PreferenceCategory;
|
||||
import androidx.preference.PreferenceViewHolder;
|
||||
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
|
||||
public final class StoragePreferenceCategory extends PreferenceCategory {
|
||||
|
||||
private Runnable onFreeUpSpace;
|
||||
private TextView totalSize;
|
||||
private StorageGraphView storageGraphView;
|
||||
private StorageGraphView.StorageBreakdown storage;
|
||||
|
||||
public StoragePreferenceCategory(Context context, AttributeSet attrs, int defStyleAttr) {
|
||||
super(context, attrs, defStyleAttr);
|
||||
initialize();
|
||||
}
|
||||
|
||||
public StoragePreferenceCategory(Context context, AttributeSet attrs) {
|
||||
super(context, attrs);
|
||||
initialize();
|
||||
}
|
||||
|
||||
public StoragePreferenceCategory(Context context) {
|
||||
super(context);
|
||||
initialize();
|
||||
}
|
||||
|
||||
private void initialize() {
|
||||
setLayoutResource(R.layout.preference_storage_category);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBindViewHolder(PreferenceViewHolder view) {
|
||||
super.onBindViewHolder(view);
|
||||
|
||||
totalSize = (TextView) view.findViewById(R.id.total_size);
|
||||
storageGraphView = (StorageGraphView) view.findViewById(R.id.storageGraphView);
|
||||
|
||||
view.findViewById(R.id.free_up_space)
|
||||
.setOnClickListener(v -> {
|
||||
if (onFreeUpSpace != null) {
|
||||
onFreeUpSpace.run();
|
||||
}
|
||||
});
|
||||
|
||||
totalSize.setText(Util.getPrettyFileSize(0));
|
||||
|
||||
if (storage != null) {
|
||||
setStorage(storage);
|
||||
}
|
||||
}
|
||||
|
||||
public void setOnFreeUpSpace(Runnable onFreeUpSpace) {
|
||||
this.onFreeUpSpace = onFreeUpSpace;
|
||||
}
|
||||
|
||||
public void setStorage(StorageGraphView.StorageBreakdown storage) {
|
||||
this.storage = storage;
|
||||
if (totalSize != null) {
|
||||
totalSize.setText(Util.getPrettyFileSize(storage.getTotalSize()));
|
||||
}
|
||||
if (storageGraphView != null) {
|
||||
storageGraphView.setStorageBreakdown(storage);
|
||||
}
|
||||
}
|
||||
}
|
23
src/org/thoughtcrime/securesms/util/CalendarDateOnly.java
Normal file
23
src/org/thoughtcrime/securesms/util/CalendarDateOnly.java
Normal file
|
@ -0,0 +1,23 @@
|
|||
package org.thoughtcrime.securesms.util;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import java.util.Calendar;
|
||||
|
||||
public final class CalendarDateOnly {
|
||||
|
||||
public static Calendar getInstance() {
|
||||
Calendar calendar = Calendar.getInstance();
|
||||
|
||||
removeTime(calendar);
|
||||
|
||||
return calendar;
|
||||
}
|
||||
|
||||
public static void removeTime(@NonNull Calendar calendar) {
|
||||
calendar.set(Calendar.HOUR_OF_DAY, calendar.getActualMinimum(Calendar.HOUR_OF_DAY));
|
||||
calendar.set(Calendar.MINUTE, calendar.getActualMinimum(Calendar.MINUTE));
|
||||
calendar.set(Calendar.SECOND, calendar.getActualMinimum(Calendar.SECOND));
|
||||
calendar.set(Calendar.MILLISECOND, calendar.getActualMinimum(Calendar.MILLISECOND));
|
||||
}
|
||||
}
|
|
@ -17,9 +17,9 @@
|
|||
package org.thoughtcrime.securesms.util;
|
||||
|
||||
import android.content.Context;
|
||||
import android.text.format.DateFormat;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import android.text.format.DateFormat;
|
||||
|
||||
import org.thoughtcrime.securesms.R;
|
||||
|
||||
|
@ -128,10 +128,14 @@ public class DateUtils extends android.text.format.DateUtils {
|
|||
} else if (isYesterday(timestamp)) {
|
||||
return context.getString(R.string.DateUtils_yesterday);
|
||||
} else {
|
||||
return getFormattedDateTime(timestamp, "EEE, MMM d, yyyy", locale);
|
||||
return formatDate(locale, timestamp);
|
||||
}
|
||||
}
|
||||
|
||||
public static String formatDate(@NonNull Locale locale, long timestamp) {
|
||||
return getFormattedDateTime(timestamp, "EEE, MMM d, yyyy", locale);
|
||||
}
|
||||
|
||||
public static boolean isSameDay(long t1, long t2) {
|
||||
return DATE_FORMAT.format(new Date(t1)).equals(DATE_FORMAT.format(new Date(t2)));
|
||||
}
|
||||
|
|
|
@ -9,13 +9,14 @@ import android.media.ThumbnailUtils;
|
|||
import android.net.Uri;
|
||||
import android.os.Build;
|
||||
import android.provider.MediaStore;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.WorkerThread;
|
||||
import android.text.TextUtils;
|
||||
import android.util.Pair;
|
||||
import android.webkit.MimeTypeMap;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.WorkerThread;
|
||||
|
||||
import com.bumptech.glide.load.engine.DiskCacheStrategy;
|
||||
import com.bumptech.glide.load.resource.gif.GifDrawable;
|
||||
|
||||
|
@ -56,31 +57,42 @@ public class MediaUtil {
|
|||
public static final String VCARD = "text/x-vcard";
|
||||
public static final String LONG_TEXT = "text/x-signal-plain";
|
||||
|
||||
|
||||
public static Slide getSlideForAttachment(Context context, Attachment attachment) {
|
||||
Slide slide = null;
|
||||
if (attachment.isSticker()) {
|
||||
slide = new StickerSlide(context, attachment);
|
||||
} else if (isGif(attachment.getContentType())) {
|
||||
slide = new GifSlide(context, attachment);
|
||||
} else if (isImageType(attachment.getContentType())) {
|
||||
slide = new ImageSlide(context, attachment);
|
||||
} else if (isVideoType(attachment.getContentType())) {
|
||||
slide = new VideoSlide(context, attachment);
|
||||
} else if (isAudioType(attachment.getContentType())) {
|
||||
slide = new AudioSlide(context, attachment);
|
||||
} else if (isMms(attachment.getContentType())) {
|
||||
slide = new MmsSlide(context, attachment);
|
||||
} else if (isLongTextType(attachment.getContentType())) {
|
||||
slide = new TextSlide(context, attachment);
|
||||
} else if (attachment.getContentType() != null) {
|
||||
slide = new DocumentSlide(context, attachment);
|
||||
public static SlideType getSlideTypeFromContentType(@NonNull String contentType) {
|
||||
if (isGif(contentType)) {
|
||||
return SlideType.GIF;
|
||||
} else if (isImageType(contentType)) {
|
||||
return SlideType.IMAGE;
|
||||
} else if (isVideoType(contentType)) {
|
||||
return SlideType.VIDEO;
|
||||
} else if (isAudioType(contentType)) {
|
||||
return SlideType.AUDIO;
|
||||
} else if (isMms(contentType)) {
|
||||
return SlideType.MMS;
|
||||
} else if (isLongTextType(contentType)) {
|
||||
return SlideType.LONG_TEXT;
|
||||
} else {
|
||||
return SlideType.DOCUMENT;
|
||||
}
|
||||
|
||||
return slide;
|
||||
}
|
||||
|
||||
public static @Nullable String getMimeType(Context context, Uri uri) {
|
||||
public static @NonNull Slide getSlideForAttachment(Context context, Attachment attachment) {
|
||||
if (attachment.isSticker()) {
|
||||
return new StickerSlide(context, attachment);
|
||||
}
|
||||
|
||||
switch (getSlideTypeFromContentType(attachment.getContentType())) {
|
||||
case GIF : return new GifSlide(context, attachment);
|
||||
case IMAGE : return new ImageSlide(context, attachment);
|
||||
case VIDEO : return new VideoSlide(context, attachment);
|
||||
case AUDIO : return new AudioSlide(context, attachment);
|
||||
case MMS : return new MmsSlide(context, attachment);
|
||||
case LONG_TEXT : return new TextSlide(context, attachment);
|
||||
case DOCUMENT : return new DocumentSlide(context, attachment);
|
||||
default : throw new AssertionError();
|
||||
}
|
||||
}
|
||||
|
||||
public static @Nullable String getMimeType(@NonNull Context context, @Nullable Uri uri) {
|
||||
if (uri == null) return null;
|
||||
|
||||
if (PartAuthority.isLocalUri(uri)) {
|
||||
|
@ -96,6 +108,11 @@ public class MediaUtil {
|
|||
return getCorrectedMimeType(type);
|
||||
}
|
||||
|
||||
public static @Nullable String getExtension(@NonNull Context context, @Nullable Uri uri) {
|
||||
return MimeTypeMap.getSingleton()
|
||||
.getExtensionFromMimeType(getMimeType(context, uri));
|
||||
}
|
||||
|
||||
public static @Nullable String getCorrectedMimeType(@Nullable String mimeType) {
|
||||
if (mimeType == null) return null;
|
||||
|
||||
|
@ -348,4 +365,14 @@ public class MediaUtil {
|
|||
return ContentResolver.SCHEME_CONTENT.equals(scheme) ||
|
||||
ContentResolver.SCHEME_FILE.equals(scheme);
|
||||
}
|
||||
|
||||
public enum SlideType {
|
||||
GIF,
|
||||
IMAGE,
|
||||
VIDEO,
|
||||
AUDIO,
|
||||
MMS,
|
||||
LONG_TEXT,
|
||||
DOCUMENT
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Add table
Reference in a new issue