Add storage management features.

This commit is contained in:
Alan Evans 2019-12-03 11:05:03 -05:00 committed by Greyson Parrelli
parent bceb69b284
commit 52447f5e97
78 changed files with 3343 additions and 1622 deletions

View file

@ -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"

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View file

@ -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>

View 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>

View 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>

View file

@ -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>

View 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>

View 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>

View 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>

View 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>

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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>

View 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>

View 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>

View 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>

View file

@ -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" />

View file

@ -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>

View file

@ -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>

View file

@ -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">

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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"/>

View file

@ -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"

View 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>

View file

@ -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);

View file

@ -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);
}
}
}

View file

@ -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);
}
}

View file

@ -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();
}
}
}

View file

@ -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;
}
}

View file

@ -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

View file

@ -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);
}
}

View file

@ -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;
}

View file

@ -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()) {

View file

@ -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() {

View file

@ -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());

View file

@ -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) {

View file

@ -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;
}
}
}

View file

@ -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);

View file

@ -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);
}
}
}
}

View file

@ -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));
}
}
}

View file

@ -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
}
}

View file

@ -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);

View file

@ -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);
}
}

View file

@ -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();
}
}
}

View 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();
}
}

View file

@ -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);
}
}

View file

@ -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();
}
}
}

View file

@ -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
}
}

View file

@ -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);
}
}

View file

@ -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;

View file

@ -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);

View file

@ -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;

View file

@ -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);
});
}
}

View file

@ -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;
}

View file

@ -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;
}
}
}

View file

@ -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;
}
}
}

View file

@ -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);
}
}
}

View 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));
}
}

View file

@ -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)));
}

View file

@ -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
}
}