diff --git a/app/src/flipper/java/org/thoughtcrime/securesms/database/FlipperSqlCipherAdapter.java b/app/src/flipper/java/org/thoughtcrime/securesms/database/FlipperSqlCipherAdapter.java index 786c616289..c660f644a8 100644 --- a/app/src/flipper/java/org/thoughtcrime/securesms/database/FlipperSqlCipherAdapter.java +++ b/app/src/flipper/java/org/thoughtcrime/securesms/database/FlipperSqlCipherAdapter.java @@ -15,7 +15,9 @@ import net.sqlcipher.database.SQLiteDatabase; import net.sqlcipher.database.SQLiteStatement; import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper; +import org.thoughtcrime.securesms.logging.Log; +import java.lang.reflect.Field; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; @@ -29,13 +31,23 @@ import java.util.Map; */ public class FlipperSqlCipherAdapter extends DatabaseDriver { + private static final String TAG = Log.tag(FlipperSqlCipherAdapter.class); + public FlipperSqlCipherAdapter(Context context) { super(context); } @Override public List getDatabases() { - return Collections.singletonList(new Descriptor(DatabaseFactory.getRawDatabase(getContext()))); + try { + Field databaseHelperField = DatabaseFactory.class.getDeclaredField("databaseHelper"); + databaseHelperField.setAccessible(true); + SQLCipherOpenHelper sqlCipherOpenHelper = (SQLCipherOpenHelper) databaseHelperField.get(DatabaseFactory.getInstance(getContext())); + return Collections.singletonList(new Descriptor(sqlCipherOpenHelper)); + } catch (Exception e) { + Log.i(TAG, "Unable to use reflection to access raw database.", e); + } + return Collections.emptyList(); } @Override diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 7a069a466f..a939785036 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -640,6 +640,8 @@ + + (v -> new SingleSelectSetting.ViewHolder(v, selectionChangedListener), R.layout.single_select_item)); + } + + public void configureCustomizableSingleSelect(@NonNull CustomizableSingleSelectSetting.CustomizableSingleSelectionListener selectionListener) { + registerFactory(CustomizableSingleSelectSetting.Item.class, + new LayoutFactory<>(v -> new CustomizableSingleSelectSetting.ViewHolder(v, selectionListener), R.layout.customizable_single_select_item)); + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/BaseSettingsFragment.java b/app/src/main/java/org/thoughtcrime/securesms/components/settings/BaseSettingsFragment.java new file mode 100644 index 0000000000..02a7fd9f55 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/BaseSettingsFragment.java @@ -0,0 +1,93 @@ +package org.thoughtcrime.securesms.components.settings; + +import android.os.Bundle; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.fragment.app.Fragment; +import androidx.fragment.app.FragmentActivity; +import androidx.recyclerview.widget.LinearLayoutManager; +import androidx.recyclerview.widget.RecyclerView; + +import org.thoughtcrime.securesms.R; +import org.thoughtcrime.securesms.util.MappingModelList; + +import java.io.Serializable; +import java.util.Objects; + +/** + * A simple settings screen that takes its configuration via {@link Configuration}. + */ +public class BaseSettingsFragment extends Fragment { + + private static final String CONFIGURATION_ARGUMENT = "current_selection"; + + private RecyclerView recycler; + + public static @NonNull BaseSettingsFragment create(@NonNull Configuration configuration) { + BaseSettingsFragment fragment = new BaseSettingsFragment(); + + Bundle arguments = new Bundle(); + arguments.putSerializable(CONFIGURATION_ARGUMENT, configuration); + fragment.setArguments(arguments); + + return fragment; + } + + @Override + public @Nullable View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { + View view = inflater.inflate(R.layout.base_settings_fragment, container, false); + + recycler = view.findViewById(R.id.base_settings_list); + recycler.setItemAnimator(null); + + return view; + } + + @Override + public void onActivityCreated(@Nullable Bundle savedInstanceState) { + super.onActivityCreated(savedInstanceState); + + BaseSettingsAdapter adapter = new BaseSettingsAdapter(); + + recycler.setLayoutManager(new LinearLayoutManager(requireContext())); + recycler.setAdapter(adapter); + + Configuration configuration = (Configuration) Objects.requireNonNull(requireArguments().getSerializable(CONFIGURATION_ARGUMENT)); + configuration.configure(requireActivity(), adapter); + configuration.setArguments(getArguments()); + configuration.configureAdapter(adapter); + + adapter.submitList(configuration.getSettings()); + } + + /** + * A configuration for a settings screen. Utilizes serializable to hide + * reflection of instantiating from a fragment argument. + */ + public static abstract class Configuration implements Serializable { + protected transient FragmentActivity activity; + protected transient BaseSettingsAdapter adapter; + + public void configure(@NonNull FragmentActivity activity, @NonNull BaseSettingsAdapter adapter) { + this.activity = activity; + this.adapter = adapter; + } + + /** + * Retrieve any runtime information from the fragment's arguments. + */ + public void setArguments(@Nullable Bundle arguments) {} + + protected void updateSettingsList() { + adapter.submitList(getSettings()); + } + + public abstract void configureAdapter(@NonNull BaseSettingsAdapter adapter); + + public abstract @NonNull MappingModelList getSettings(); + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/CustomizableSingleSelectSetting.java b/app/src/main/java/org/thoughtcrime/securesms/components/settings/CustomizableSingleSelectSetting.java new file mode 100644 index 0000000000..70c227edcf --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/CustomizableSingleSelectSetting.java @@ -0,0 +1,92 @@ +package org.thoughtcrime.securesms.components.settings; + +import android.view.View; +import android.widget.RadioButton; +import android.widget.TextView; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.constraintlayout.widget.Group; + +import org.thoughtcrime.securesms.R; +import org.thoughtcrime.securesms.util.MappingModel; +import org.thoughtcrime.securesms.util.MappingViewHolder; + +import java.util.Objects; + +/** + * Adds ability to customize a value for a single select (radio) setting. + */ +public class CustomizableSingleSelectSetting { + + public interface CustomizableSingleSelectionListener extends SingleSelectSetting.SingleSelectSelectionChangedListener { + void onCustomizeClicked(@NonNull Item item); + } + + public static class ViewHolder extends MappingViewHolder { + private final TextView summaryText; + private final View customize; + private final RadioButton radio; + private final SingleSelectSetting.ViewHolder delegate; + private final Group customizeGroup; + private final CustomizableSingleSelectionListener selectionListener; + + public ViewHolder(@NonNull View itemView, @NonNull CustomizableSingleSelectionListener selectionListener) { + super(itemView); + this.selectionListener = selectionListener; + + radio = findViewById(R.id.customizable_single_select_radio); + summaryText = findViewById(R.id.customizable_single_select_summary); + customize = findViewById(R.id.customizable_single_select_customize); + customizeGroup = findViewById(R.id.customizable_single_select_customize_group); + + delegate = new SingleSelectSetting.ViewHolder(itemView, selectionListener) { + @Override + protected void setChecked(boolean checked) { + radio.setChecked(checked); + } + }; + } + + @Override + public void bind(@NonNull Item model) { + delegate.bind(model.singleSelectItem); + customizeGroup.setVisibility(radio.isChecked() ? View.VISIBLE : View.GONE); + customize.setOnClickListener(v -> selectionListener.onCustomizeClicked(model)); + if (model.getCustomValue() != null) { + summaryText.setText(model.getSummaryText()); + } + } + } + + public static class Item implements MappingModel { + private SingleSelectSetting.Item singleSelectItem; + private Object customValue; + private String summaryText; + + public Item(@NonNull T item, @Nullable String text, boolean isSelected, @Nullable Object customValue, @Nullable String summaryText) { + this.customValue = customValue; + this.summaryText = summaryText; + + singleSelectItem = new SingleSelectSetting.Item(item, text, isSelected); + } + + public @Nullable Object getCustomValue() { + return customValue; + } + + public @Nullable String getSummaryText() { + return summaryText; + } + + @Override + public boolean areItemsTheSame(@NonNull Item newItem) { + return singleSelectItem.areItemsTheSame(newItem.singleSelectItem); + } + + @Override + public boolean areContentsTheSame(@NonNull Item newItem) { + return singleSelectItem.areContentsTheSame(newItem.singleSelectItem) && Objects.equals(customValue, newItem.customValue) && Objects.equals(summaryText, newItem.summaryText); + } + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/SingleSelectSetting.java b/app/src/main/java/org/thoughtcrime/securesms/components/settings/SingleSelectSetting.java new file mode 100644 index 0000000000..7300bcd8bc --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/SingleSelectSetting.java @@ -0,0 +1,76 @@ +package org.thoughtcrime.securesms.components.settings; + +import android.view.View; +import android.widget.CheckedTextView; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import org.thoughtcrime.securesms.R; +import org.thoughtcrime.securesms.util.MappingModel; +import org.thoughtcrime.securesms.util.MappingViewHolder; + +import java.util.Objects; + +/** + * Single select (radio) setting option + */ +public class SingleSelectSetting { + + public interface SingleSelectSelectionChangedListener { + void onSelectionChanged(@NonNull Object selection); + } + + public static class ViewHolder extends MappingViewHolder { + + protected final CheckedTextView text; + protected final SingleSelectSelectionChangedListener selectionChangedListener; + + public ViewHolder(@NonNull View itemView, @NonNull SingleSelectSelectionChangedListener selectionChangedListener) { + super(itemView); + this.selectionChangedListener = selectionChangedListener; + this.text = findViewById(R.id.single_select_item_text); + } + + @Override + public void bind(@NonNull Item model) { + text.setText(model.text); + setChecked(model.isSelected); + itemView.setOnClickListener(v -> selectionChangedListener.onSelectionChanged(model.item)); + } + + protected void setChecked(boolean checked) { + text.setChecked(checked); + } + } + + public static class Item implements MappingModel { + private final String text; + private final Object item; + private final boolean isSelected; + + public Item(@NonNull T item, @Nullable String text, boolean isSelected) { + this.item = item; + this.text = text != null ? text : item.toString(); + this.isSelected = isSelected; + } + + public @NonNull String getText() { + return text; + } + + public @NonNull Object getItem() { + return item; + } + + @Override + public boolean areItemsTheSame(@NonNull Item newItem) { + return item.equals(newItem.item); + } + + @Override + public boolean areContentsTheSame(@NonNull Item newItem) { + return Objects.equals(text, newItem.text) && isSelected == newItem.isSelected; + } + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/AttachmentDatabase.java b/app/src/main/java/org/thoughtcrime/securesms/database/AttachmentDatabase.java index 17d7edae4f..3a1362052d 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/AttachmentDatabase.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/AttachmentDatabase.java @@ -66,6 +66,7 @@ import org.thoughtcrime.securesms.util.JsonUtils; import org.thoughtcrime.securesms.util.MediaMetadataRetrieverUtil; import org.thoughtcrime.securesms.util.MediaUtil; import org.thoughtcrime.securesms.util.MediaUtil.ThumbnailData; +import org.thoughtcrime.securesms.util.SetUtil; import org.thoughtcrime.securesms.util.StorageUtil; import org.thoughtcrime.securesms.util.Util; import org.thoughtcrime.securesms.video.EncryptedMediaDataSource; @@ -83,10 +84,12 @@ import java.security.NoSuchAlgorithmException; import java.util.Collection; import java.util.Collections; import java.util.HashMap; +import java.util.HashSet; import java.util.LinkedList; import java.util.List; import java.util.Locale; import java.util.Map; +import java.util.Set; import java.util.concurrent.Callable; import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutorService; @@ -481,6 +484,39 @@ public class AttachmentDatabase extends Database { } } + public void trimAllAbandonedAttachments() { + SQLiteDatabase db = databaseHelper.getWritableDatabase(); + String selectAllMmsIds = "SELECT " + MmsDatabase.ID + " FROM " + MmsDatabase.TABLE_NAME; + String selectDataInUse = "SELECT DISTINCT " + DATA + " FROM " + TABLE_NAME + " WHERE " + QUOTE + " = 0 AND " + MMS_ID + " IN (" + selectAllMmsIds + ")"; + String where = MMS_ID + " NOT IN (" + selectAllMmsIds + ") AND " + DATA + " NOT IN (" + selectDataInUse + ")"; + + db.delete(TABLE_NAME, where, null); + } + + public void deleteAbandonedAttachmentFiles() { + Set filesOnDisk = new HashSet<>(); + Set filesInDb = new HashSet<>(); + + File attachmentDirectory = context.getDir(DIRECTORY, Context.MODE_PRIVATE); + for (File file : attachmentDirectory.listFiles()) { + filesOnDisk.add(file.getAbsolutePath()); + } + + try (Cursor cursor = databaseHelper.getReadableDatabase().query(true, TABLE_NAME, new String[] { DATA, THUMBNAIL }, null, null, null, null, null, null)) { + while (cursor != null && cursor.moveToNext()) { + filesInDb.add(CursorUtil.requireString(cursor, DATA)); + filesInDb.add(CursorUtil.requireString(cursor, THUMBNAIL)); + } + } + + Set onDiskButNotInDatabase = SetUtil.difference(filesOnDisk, filesInDb); + + for (String filePath : onDiskButNotInDatabase) { + //noinspection ResultOfMethodCallIgnored + new File(filePath).delete(); + } + } + @SuppressWarnings("ResultOfMethodCallIgnored") void deleteAllAttachments() { SQLiteDatabase database = databaseHelper.getWritableDatabase(); diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/GroupReceiptDatabase.java b/app/src/main/java/org/thoughtcrime/securesms/database/GroupReceiptDatabase.java index 959a71fbcd..4fb767f784 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/GroupReceiptDatabase.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/GroupReceiptDatabase.java @@ -115,6 +115,11 @@ public class GroupReceiptDatabase extends Database { db.delete(TABLE_NAME, MMS_ID + " = ?", new String[] {String.valueOf(mmsId)}); } + void deleteAbandonedRows() { + SQLiteDatabase db = databaseHelper.getWritableDatabase(); + db.delete(TABLE_NAME, MMS_ID + " NOT IN (SELECT " + MmsDatabase.ID + " FROM " + MmsDatabase.TABLE_NAME + ")", null); + } + void deleteAllRows() { SQLiteDatabase db = databaseHelper.getWritableDatabase(); db.delete(TABLE_NAME, null, null); diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/MessageDatabase.java b/app/src/main/java/org/thoughtcrime/securesms/database/MessageDatabase.java index d6674d7c23..d6dfceb630 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/MessageDatabase.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/MessageDatabase.java @@ -21,9 +21,9 @@ import org.thoughtcrime.securesms.database.documents.IdentityKeyMismatchList; import org.thoughtcrime.securesms.database.documents.NetworkFailure; import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper; import org.thoughtcrime.securesms.database.model.MessageRecord; +import org.thoughtcrime.securesms.database.model.ReactionRecord; import org.thoughtcrime.securesms.database.model.SmsMessageRecord; import org.thoughtcrime.securesms.database.model.databaseprotos.ReactionList; -import org.thoughtcrime.securesms.database.model.ReactionRecord; import org.thoughtcrime.securesms.insights.InsightsConstants; import org.thoughtcrime.securesms.logging.Log; import org.thoughtcrime.securesms.mms.IncomingMediaMessage; @@ -143,6 +143,7 @@ public abstract class MessageDatabase extends Database implements MmsSmsColumns abstract void deleteMessagesInThreadBeforeDate(long threadId, long date); abstract void deleteThreads(@NonNull Set threadIds); abstract void deleteAllThreads(); + abstract void deleteAbandonedMessages(); public abstract SQLiteDatabase beginTransaction(); public abstract void endTransaction(SQLiteDatabase database); diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/MmsDatabase.java b/app/src/main/java/org/thoughtcrime/securesms/database/MmsDatabase.java index 9877498bec..6274398d2c 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/MmsDatabase.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/MmsDatabase.java @@ -19,7 +19,6 @@ package org.thoughtcrime.securesms.database; import android.content.ContentValues; import android.content.Context; import android.database.Cursor; -import android.net.Uri; import android.text.TextUtils; import androidx.annotation.NonNull; @@ -41,7 +40,6 @@ import org.thoughtcrime.securesms.attachments.AttachmentId; import org.thoughtcrime.securesms.attachments.DatabaseAttachment; import org.thoughtcrime.securesms.attachments.MmsNotificationAttachment; import org.thoughtcrime.securesms.contactshare.Contact; -import org.thoughtcrime.securesms.database.documents.Document; import org.thoughtcrime.securesms.database.documents.IdentityKeyMismatch; import org.thoughtcrime.securesms.database.documents.IdentityKeyMismatchList; import org.thoughtcrime.securesms.database.documents.NetworkFailure; @@ -79,11 +77,9 @@ import org.thoughtcrime.securesms.util.JsonUtils; import org.thoughtcrime.securesms.util.SqlUtil; import org.thoughtcrime.securesms.util.TextSecurePreferences; import org.thoughtcrime.securesms.util.Util; -import org.whispersystems.libsignal.IdentityKey; import org.whispersystems.libsignal.util.Pair; import org.whispersystems.libsignal.util.guava.Optional; -import java.io.Closeable; import java.io.IOException; import java.security.SecureRandom; import java.util.Collection; @@ -1588,29 +1584,18 @@ public class MmsDatabase extends MessageDatabase { @Override void deleteMessagesInThreadBeforeDate(long threadId, long date) { - Cursor cursor = null; + SQLiteDatabase db = databaseHelper.getWritableDatabase(); + String where = THREAD_ID + " = ? AND " + DATE_RECEIVED + " < " + date; - try { - SQLiteDatabase db = databaseHelper.getReadableDatabase(); - String where = THREAD_ID + " = ? AND (CASE (" + MESSAGE_BOX + " & " + Types.BASE_TYPE_MASK + ") "; + db.delete(TABLE_NAME, where, SqlUtil.buildArgs(threadId)); + } - for (long outgoingType : Types.OUTGOING_MESSAGE_TYPES) { - where += " WHEN " + outgoingType + " THEN " + DATE_SENT + " < " + date; - } + @Override + void deleteAbandonedMessages() { + SQLiteDatabase db = databaseHelper.getWritableDatabase(); + String where = THREAD_ID + " NOT IN (SELECT _id FROM " + ThreadDatabase.TABLE_NAME + ")"; - where += (" ELSE " + DATE_RECEIVED + " < " + date + " END)"); - - cursor = db.query(TABLE_NAME, new String[] {ID}, where, new String[] {threadId+""}, null, null, null); - - while (cursor != null && cursor.moveToNext()) { - Log.i(TAG, "Trimming: " + cursor.getLong(0)); - deleteMessage(cursor.getLong(0)); - } - - } finally { - if (cursor != null) - cursor.close(); - } + db.delete(TABLE_NAME, where, null); } @Override diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/MmsSmsDatabase.java b/app/src/main/java/org/thoughtcrime/securesms/database/MmsSmsDatabase.java index d712de6272..03ef88f96b 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/MmsSmsDatabase.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/MmsSmsDatabase.java @@ -272,6 +272,18 @@ public class MmsSmsDatabase extends Database { return count; } + public int getMessageCountBeforeDate(long date) { + String selection = MmsSmsColumns.NORMALIZED_DATE_RECEIVED + " < " + date; + + try (Cursor cursor = queryTables(new String[] { "COUNT(*)" }, selection, null, null)) { + if (cursor != null && cursor.moveToFirst()) { + return cursor.getInt(0); + } + } + + return 0; + } + public int getSecureMessageCountForInsights() { int count = DatabaseFactory.getSmsDatabase(context).getSecureMessageCountForInsights(); count += DatabaseFactory.getMmsDatabase(context).getSecureMessageCountForInsights(); @@ -362,6 +374,29 @@ public class MmsSmsDatabase extends Database { return -1; } + public long getTimestampForFirstMessageAfterDate(long date) { + String order = MmsSmsColumns.NORMALIZED_DATE_RECEIVED + " ASC"; + String selection = MmsSmsColumns.NORMALIZED_DATE_RECEIVED + " > " + date; + + try (Cursor cursor = queryTables(new String[] { MmsSmsColumns.NORMALIZED_DATE_RECEIVED }, selection, order, "1")) { + if (cursor != null && cursor.moveToFirst()) { + return cursor.getLong(0); + } + } + + return 0; + } + + public void deleteMessagesInThreadBeforeDate(long threadId, long trimBeforeDate) { + DatabaseFactory.getSmsDatabase(context).deleteMessagesInThreadBeforeDate(threadId, trimBeforeDate); + DatabaseFactory.getMmsDatabase(context).deleteMessagesInThreadBeforeDate(threadId, trimBeforeDate); + } + + public void deleteAbandonedMessages() { + DatabaseFactory.getSmsDatabase(context).deleteAbandonedMessages(); + DatabaseFactory.getMmsDatabase(context).deleteAbandonedMessages(); + } + private Cursor queryTables(String[] projection, String selection, String order, String limit) { String[] mmsProjection = {MmsDatabase.DATE_SENT + " AS " + MmsSmsColumns.NORMALIZED_DATE_SENT, MmsDatabase.DATE_RECEIVED + " AS " + MmsSmsColumns.NORMALIZED_DATE_RECEIVED, diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/SmsDatabase.java b/app/src/main/java/org/thoughtcrime/securesms/database/SmsDatabase.java index 07c82ff5ed..5fcc14e442 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/SmsDatabase.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/SmsDatabase.java @@ -968,16 +968,18 @@ public class SmsDatabase extends MessageDatabase { @Override void deleteMessagesInThreadBeforeDate(long threadId, long date) { - SQLiteDatabase db = databaseHelper.getWritableDatabase(); - String where = THREAD_ID + " = ? AND (CASE " + TYPE; + SQLiteDatabase db = databaseHelper.getWritableDatabase(); + String where = THREAD_ID + " = ? AND " + DATE_RECEIVED + " < " + date; - for (long outgoingType : Types.OUTGOING_MESSAGE_TYPES) { - where += " WHEN " + outgoingType + " THEN " + DATE_SENT + " < " + date; - } + db.delete(TABLE_NAME, where, SqlUtil.buildArgs(threadId)); + } - where += (" ELSE " + DATE_RECEIVED + " < " + date + " END)"); + @Override + void deleteAbandonedMessages() { + SQLiteDatabase db = databaseHelper.getWritableDatabase(); + String where = THREAD_ID + " NOT IN (SELECT _id FROM " + ThreadDatabase.TABLE_NAME + ")"; - db.delete(TABLE_NAME, where, new String[] {threadId + ""}); + db.delete(TABLE_NAME, where, null); } @Override diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/ThreadDatabase.java b/app/src/main/java/org/thoughtcrime/securesms/database/ThreadDatabase.java index c9ec79660c..6f4eb704e3 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/ThreadDatabase.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/ThreadDatabase.java @@ -34,8 +34,8 @@ import net.sqlcipher.database.SQLiteDatabase; import org.jsoup.helper.StringUtil; import org.signal.storageservice.protos.groups.local.DecryptedGroup; import org.signal.storageservice.protos.groups.local.DecryptedMember; -import org.thoughtcrime.securesms.database.RecipientDatabase.RecipientSettings; import org.thoughtcrime.securesms.database.MessageDatabase.MarkedMessageInfo; +import org.thoughtcrime.securesms.database.RecipientDatabase.RecipientSettings; import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper; import org.thoughtcrime.securesms.database.model.MediaMmsMessageRecord; import org.thoughtcrime.securesms.database.model.MessageRecord; @@ -76,6 +76,9 @@ public class ThreadDatabase extends Database { private static final String TAG = ThreadDatabase.class.getSimpleName(); + public static final long NO_TRIM_BEFORE_DATE_SET = 0; + public static final int NO_TRIM_MESSAGE_COUNT_SET = Integer.MAX_VALUE; + public static final String TABLE_NAME = "thread"; public static final String ID = "_id"; public static final String DATE = "date"; @@ -258,53 +261,88 @@ public class ThreadDatabase extends Database { notifyConversationListListeners(); } - public void trimAllThreads(int length, ProgressListener listener) { - Cursor cursor = null; - int threadCount = 0; - int complete = 0; + public void trimAllThreads(int length, long trimBeforeDate) { + if (length == NO_TRIM_MESSAGE_COUNT_SET && trimBeforeDate == NO_TRIM_BEFORE_DATE_SET) { + return; + } + + SQLiteDatabase db = databaseHelper.getWritableDatabase(); + AttachmentDatabase attachmentDatabase = DatabaseFactory.getAttachmentDatabase(context); + GroupReceiptDatabase groupReceiptDatabase = DatabaseFactory.getGroupReceiptDatabase(context); + MmsSmsDatabase mmsSmsDatabase = DatabaseFactory.getMmsSmsDatabase(context); + + try (Cursor cursor = databaseHelper.getReadableDatabase().query(TABLE_NAME, new String[] { ID }, null, null, null, null, null)) { + while (cursor != null && cursor.moveToNext()) { + trimThreadInternal(CursorUtil.requireLong(cursor, ID), length, trimBeforeDate); + } + } + + db.beginTransaction(); try { - cursor = this.getConversationList(); - - if (cursor != null) - threadCount = cursor.getCount(); - - while (cursor != null && cursor.moveToNext()) { - long threadId = cursor.getLong(cursor.getColumnIndexOrThrow(ID)); - trimThread(threadId, length); - - listener.onProgress(++complete, threadCount); - } + mmsSmsDatabase.deleteAbandonedMessages(); + attachmentDatabase.trimAllAbandonedAttachments(); + groupReceiptDatabase.deleteAbandonedRows(); + db.setTransactionSuccessful(); } finally { - if (cursor != null) - cursor.close(); + db.endTransaction(); } + + attachmentDatabase.deleteAbandonedAttachmentFiles(); + + notifyAttachmentListeners(); + notifyStickerListeners(); + notifyStickerPackListeners(); } - public void trimThread(long threadId, int length) { - Log.i(TAG, "Trimming thread: " + threadId + " to: " + length); - Cursor cursor = null; + public void trimThread(long threadId, int length, long trimBeforeDate) { + SQLiteDatabase db = databaseHelper.getWritableDatabase(); + AttachmentDatabase attachmentDatabase = DatabaseFactory.getAttachmentDatabase(context); + GroupReceiptDatabase groupReceiptDatabase = DatabaseFactory.getGroupReceiptDatabase(context); + MmsSmsDatabase mmsSmsDatabase = DatabaseFactory.getMmsSmsDatabase(context); + + db.beginTransaction(); try { - cursor = DatabaseFactory.getMmsSmsDatabase(context).getConversation(threadId); - - if (cursor != null && length > 0 && cursor.getCount() > length) { - Log.w(TAG, "Cursor count is greater than length!"); - cursor.moveToPosition(length - 1); - - long lastTweetDate = cursor.getLong(cursor.getColumnIndexOrThrow(MmsSmsColumns.NORMALIZED_DATE_RECEIVED)); - - Log.i(TAG, "Cut off tweet date: " + lastTweetDate); - - DatabaseFactory.getSmsDatabase(context).deleteMessagesInThreadBeforeDate(threadId, lastTweetDate); - DatabaseFactory.getMmsDatabase(context).deleteMessagesInThreadBeforeDate(threadId, lastTweetDate); - - update(threadId, false); - notifyConversationListeners(threadId); - } + trimThreadInternal(threadId, length, trimBeforeDate); + mmsSmsDatabase.deleteAbandonedMessages(); + attachmentDatabase.trimAllAbandonedAttachments(); + groupReceiptDatabase.deleteAbandonedRows(); + db.setTransactionSuccessful(); } finally { - if (cursor != null) - cursor.close(); + db.endTransaction(); + } + + attachmentDatabase.deleteAbandonedAttachmentFiles(); + + notifyAttachmentListeners(); + notifyStickerListeners(); + notifyStickerPackListeners(); + } + + private void trimThreadInternal(long threadId, int length, long trimBeforeDate) { + if (length == NO_TRIM_MESSAGE_COUNT_SET && trimBeforeDate == NO_TRIM_BEFORE_DATE_SET) { + return; + } + + long trimDate = trimBeforeDate; + + if (length != NO_TRIM_MESSAGE_COUNT_SET) { + try (Cursor cursor = DatabaseFactory.getMmsSmsDatabase(context).getConversation(threadId)) { + if (cursor != null && length > 0 && cursor.getCount() > length) { + cursor.moveToPosition(length - 1); + trimDate = Math.max(trimDate, cursor.getLong(cursor.getColumnIndexOrThrow(MmsSmsColumns.NORMALIZED_DATE_RECEIVED))); + } + } + } + + if (trimDate != NO_TRIM_BEFORE_DATE_SET) { + Log.i(TAG, "Trimming thread: " + threadId + " before: " + trimBeforeDate); + + DatabaseFactory.getMmsSmsDatabase(context).deleteMessagesInThreadBeforeDate(threadId, trimBeforeDate); + + update(threadId, false); + notifyConversationListeners(threadId); } } @@ -1153,10 +1191,6 @@ public class ThreadDatabase extends Database { return query; } - public interface ProgressListener { - void onProgress(int complete, int total); - } - public Reader readerFor(Cursor cursor) { return new Reader(cursor); } diff --git a/app/src/main/java/org/thoughtcrime/securesms/dependencies/ApplicationDependencies.java b/app/src/main/java/org/thoughtcrime/securesms/dependencies/ApplicationDependencies.java index 33ae68d030..1f76dbd21f 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/dependencies/ApplicationDependencies.java +++ b/app/src/main/java/org/thoughtcrime/securesms/dependencies/ApplicationDependencies.java @@ -18,6 +18,7 @@ import org.thoughtcrime.securesms.notifications.MessageNotifier; import org.thoughtcrime.securesms.push.SignalServiceNetworkAccess; import org.thoughtcrime.securesms.recipients.LiveRecipientCache; import org.thoughtcrime.securesms.messages.IncomingMessageObserver; +import org.thoughtcrime.securesms.service.TrimThreadsByDateManager; import org.thoughtcrime.securesms.util.EarlyMessageCache; import org.thoughtcrime.securesms.util.FeatureFlags; import org.thoughtcrime.securesms.util.FrameRateTracker; @@ -60,6 +61,7 @@ public class ApplicationDependencies { private static GroupsV2Operations groupsV2Operations; private static EarlyMessageCache earlyMessageCache; private static MessageNotifier messageNotifier; + private static TrimThreadsByDateManager trimThreadsByDateManager; @MainThread public static synchronized void init(@NonNull Application application, @NonNull Provider provider) { @@ -67,9 +69,10 @@ public class ApplicationDependencies { throw new IllegalStateException("Already initialized!"); } - ApplicationDependencies.application = application; - ApplicationDependencies.provider = provider; - ApplicationDependencies.messageNotifier = provider.provideMessageNotifier(); + ApplicationDependencies.application = application; + ApplicationDependencies.provider = provider; + ApplicationDependencies.messageNotifier = provider.provideMessageNotifier(); + ApplicationDependencies.trimThreadsByDateManager = provider.provideTrimThreadsByDateManager(); } public static @NonNull Application getApplication() { @@ -257,6 +260,11 @@ public class ApplicationDependencies { return incomingMessageObserver; } + public static synchronized @NonNull TrimThreadsByDateManager getTrimThreadsByDateManager() { + assertInitialization(); + return trimThreadsByDateManager; + } + private static void assertInitialization() { if (application == null || provider == null) { throw new UninitializedException(); @@ -279,6 +287,7 @@ public class ApplicationDependencies { @NonNull EarlyMessageCache provideEarlyMessageCache(); @NonNull MessageNotifier provideMessageNotifier(); @NonNull IncomingMessageObserver provideIncomingMessageObserver(); + @NonNull TrimThreadsByDateManager provideTrimThreadsByDateManager(); } private static class UninitializedException extends IllegalStateException { diff --git a/app/src/main/java/org/thoughtcrime/securesms/dependencies/ApplicationDependencyProvider.java b/app/src/main/java/org/thoughtcrime/securesms/dependencies/ApplicationDependencyProvider.java index f5436f7a19..74349fe1b9 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/dependencies/ApplicationDependencyProvider.java +++ b/app/src/main/java/org/thoughtcrime/securesms/dependencies/ApplicationDependencyProvider.java @@ -36,6 +36,7 @@ import org.thoughtcrime.securesms.push.SecurityEventListener; import org.thoughtcrime.securesms.push.SignalServiceNetworkAccess; import org.thoughtcrime.securesms.recipients.LiveRecipientCache; import org.thoughtcrime.securesms.messages.IncomingMessageObserver; +import org.thoughtcrime.securesms.service.TrimThreadsByDateManager; import org.thoughtcrime.securesms.util.AlarmSleepTimer; import org.thoughtcrime.securesms.util.EarlyMessageCache; import org.thoughtcrime.securesms.util.FeatureFlags; @@ -178,6 +179,11 @@ public class ApplicationDependencyProvider implements ApplicationDependencies.Pr return new IncomingMessageObserver(context); } + @Override + public @NonNull TrimThreadsByDateManager provideTrimThreadsByDateManager() { + return new TrimThreadsByDateManager(context); + } + private static class DynamicCredentialsProvider implements CredentialsProvider { private final Context context; diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/JobManagerFactories.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/JobManagerFactories.java index e070e7ccc9..668b3578ba 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/JobManagerFactories.java +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/JobManagerFactories.java @@ -39,6 +39,7 @@ import org.thoughtcrime.securesms.migrations.StickerAdditionMigrationJob; import org.thoughtcrime.securesms.migrations.StickerLaunchMigrationJob; import org.thoughtcrime.securesms.migrations.StorageCapabilityMigrationJob; import org.thoughtcrime.securesms.migrations.StorageServiceMigrationJob; +import org.thoughtcrime.securesms.migrations.TrimByLengthSettingsMigrationJob; import org.thoughtcrime.securesms.migrations.UuidMigrationJob; import java.util.Arrays; @@ -139,6 +140,7 @@ public final class JobManagerFactories { put(StickerAdditionMigrationJob.KEY, new StickerAdditionMigrationJob.Factory()); put(StorageCapabilityMigrationJob.KEY, new StorageCapabilityMigrationJob.Factory()); put(StorageServiceMigrationJob.KEY, new StorageServiceMigrationJob.Factory()); + put(TrimByLengthSettingsMigrationJob.KEY, new TrimByLengthSettingsMigrationJob.Factory()); put(UuidMigrationJob.KEY, new UuidMigrationJob.Factory()); // Dead jobs diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/TrimThreadJob.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/TrimThreadJob.java index 8b7ff0f7f5..72cc5fa4ad 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/TrimThreadJob.java +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/TrimThreadJob.java @@ -19,10 +19,12 @@ package org.thoughtcrime.securesms.jobs; import androidx.annotation.NonNull; import org.thoughtcrime.securesms.database.DatabaseFactory; +import org.thoughtcrime.securesms.database.ThreadDatabase; import org.thoughtcrime.securesms.jobmanager.Data; import org.thoughtcrime.securesms.jobmanager.Job; +import org.thoughtcrime.securesms.keyvalue.KeepMessagesDuration; +import org.thoughtcrime.securesms.keyvalue.SignalStore; import org.thoughtcrime.securesms.logging.Log; -import org.thoughtcrime.securesms.util.TextSecurePreferences; public class TrimThreadJob extends BaseJob { @@ -55,13 +57,15 @@ public class TrimThreadJob extends BaseJob { @Override public void onRun() { - boolean trimmingEnabled = TextSecurePreferences.isThreadLengthTrimmingEnabled(context); - int threadLengthLimit = TextSecurePreferences.getThreadTrimLength(context); + KeepMessagesDuration keepMessagesDuration = SignalStore.settings().getKeepMessagesDuration(); - if (!trimmingEnabled) - return; + int trimLength = SignalStore.settings().isTrimByLengthEnabled() ? SignalStore.settings().getThreadTrimLength() + : ThreadDatabase.NO_TRIM_MESSAGE_COUNT_SET; - DatabaseFactory.getThreadDatabase(context).trimThread(threadId, threadLengthLimit); + long trimBeforeDate = keepMessagesDuration != KeepMessagesDuration.FOREVER ? System.currentTimeMillis() - keepMessagesDuration.getDuration() + : ThreadDatabase.NO_TRIM_BEFORE_DATE_SET; + + DatabaseFactory.getThreadDatabase(context).trimThread(threadId, trimLength, trimBeforeDate); } @Override diff --git a/app/src/main/java/org/thoughtcrime/securesms/keyvalue/KeepMessagesDuration.java b/app/src/main/java/org/thoughtcrime/securesms/keyvalue/KeepMessagesDuration.java new file mode 100644 index 0000000000..6577c5dad1 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/keyvalue/KeepMessagesDuration.java @@ -0,0 +1,41 @@ +package org.thoughtcrime.securesms.keyvalue; + +import androidx.annotation.NonNull; +import androidx.annotation.StringRes; + +import org.thoughtcrime.securesms.R; + +import java.util.concurrent.TimeUnit; + +public enum KeepMessagesDuration { + FOREVER(0, R.string.preferences_storage__forever, Long.MAX_VALUE), + ONE_YEAR(1, R.string.preferences_storage__one_year, TimeUnit.DAYS.toMillis(365)), + SIX_MONTHS(2, R.string.preferences_storage__six_months, TimeUnit.DAYS.toMillis(183)), + THIRTY_DAYS(3, R.string.preferences_storage__thirty_days, TimeUnit.DAYS.toMillis(30)); + + private final int id; + private final int stringResource; + private final long duration; + + KeepMessagesDuration(int id, @StringRes int stringResource, long duration) { + this.id = id; + this.stringResource = stringResource; + this.duration = duration; + } + + public int getId() { + return id; + } + + public @StringRes int getStringResource() { + return stringResource; + } + + public long getDuration() { + return duration; + } + + static @NonNull KeepMessagesDuration fromId(int id) { + return values()[id]; + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/keyvalue/SettingsValues.java b/app/src/main/java/org/thoughtcrime/securesms/keyvalue/SettingsValues.java index f7cd610414..ed14f118d6 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/keyvalue/SettingsValues.java +++ b/app/src/main/java/org/thoughtcrime/securesms/keyvalue/SettingsValues.java @@ -4,7 +4,11 @@ import androidx.annotation.NonNull; public final class SettingsValues extends SignalStoreValues { - public static final String LINK_PREVIEWS = "settings.link_previews"; + public static final String LINK_PREVIEWS = "settings.link_previews"; + public static final String KEEP_MESSAGES_DURATION = "settings.keep_messages_duration"; + + public static final String THREAD_TRIM_LENGTH = "pref_trim_length"; + public static final String THREAD_TRIM_ENABLED = "pref_trim_threads"; SettingsValues(@NonNull KeyValueStore store) { super(store); @@ -24,4 +28,29 @@ public final class SettingsValues extends SignalStoreValues { public void setLinkPreviewsEnabled(boolean enabled) { putBoolean(LINK_PREVIEWS, enabled); } + + public @NonNull KeepMessagesDuration getKeepMessagesDuration() { + return KeepMessagesDuration.fromId(getInteger(KEEP_MESSAGES_DURATION, 0)); + } + + public void setKeepMessagesForDuration(@NonNull KeepMessagesDuration duration) { + putInteger(KEEP_MESSAGES_DURATION, duration.getId()); + } + + public boolean isTrimByLengthEnabled() { + return getBoolean(THREAD_TRIM_ENABLED, false); + } + + public void setThreadTrimByLengthEnabled(boolean enabled) { + putBoolean(THREAD_TRIM_ENABLED, enabled); + } + + public int getThreadTrimLength() { + return getInteger(THREAD_TRIM_LENGTH, 500); + } + + public void setThreadTrimLength(int length) { + putInteger(THREAD_TRIM_LENGTH, length); + } + } diff --git a/app/src/main/java/org/thoughtcrime/securesms/migrations/ApplicationMigrations.java b/app/src/main/java/org/thoughtcrime/securesms/migrations/ApplicationMigrations.java index 685881fc2b..47df39184c 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/migrations/ApplicationMigrations.java +++ b/app/src/main/java/org/thoughtcrime/securesms/migrations/ApplicationMigrations.java @@ -39,7 +39,7 @@ public class ApplicationMigrations { private static final int LEGACY_CANONICAL_VERSION = 455; - public static final int CURRENT_VERSION = 17; + public static final int CURRENT_VERSION = 18; private static final class Version { static final int LEGACY = 1; @@ -59,6 +59,7 @@ public class ApplicationMigrations { static final int PIN_REMINDER = 15; static final int VERSIONED_PROFILE = 16; static final int PIN_OPT_OUT = 17; + static final int TRIM_SETTINGS = 18; } /** @@ -241,6 +242,10 @@ public class ApplicationMigrations { jobs.put(Version.PIN_OPT_OUT, new PinOptOutMigration()); } + if (lastSeenVersion < Version.TRIM_SETTINGS) { + jobs.put(Version.TRIM_SETTINGS, new TrimByLengthSettingsMigrationJob()); + } + return jobs; } diff --git a/app/src/main/java/org/thoughtcrime/securesms/migrations/TrimByLengthSettingsMigrationJob.java b/app/src/main/java/org/thoughtcrime/securesms/migrations/TrimByLengthSettingsMigrationJob.java new file mode 100644 index 0000000000..1153ce87a4 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/migrations/TrimByLengthSettingsMigrationJob.java @@ -0,0 +1,67 @@ +package org.thoughtcrime.securesms.migrations; + +import android.content.SharedPreferences; +import android.preference.PreferenceManager; + +import androidx.annotation.NonNull; + +import org.thoughtcrime.securesms.dependencies.ApplicationDependencies; +import org.thoughtcrime.securesms.jobmanager.Data; +import org.thoughtcrime.securesms.jobmanager.Job; +import org.thoughtcrime.securesms.keyvalue.SignalStore; +import org.thoughtcrime.securesms.logging.Log; + +import static org.thoughtcrime.securesms.keyvalue.SettingsValues.THREAD_TRIM_ENABLED; +import static org.thoughtcrime.securesms.keyvalue.SettingsValues.THREAD_TRIM_LENGTH; + +public class TrimByLengthSettingsMigrationJob extends MigrationJob { + + private static final String TAG = Log.tag(TrimByLengthSettingsMigrationJob.class); + + public static final String KEY = "TrimByLengthSettingsMigrationJob"; + + TrimByLengthSettingsMigrationJob() { + this(new Parameters.Builder().build()); + } + + private TrimByLengthSettingsMigrationJob(@NonNull Parameters parameters) { + super(parameters); + } + + @Override + boolean isUiBlocking() { + return false; + } + + @Override + void performMigration() throws Exception { + SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(ApplicationDependencies.getApplication()); + if (preferences.contains(THREAD_TRIM_ENABLED)) { + SignalStore.settings().setThreadTrimByLengthEnabled(preferences.getBoolean(THREAD_TRIM_ENABLED, false)); + //noinspection ConstantConditions + SignalStore.settings().setThreadTrimLength(Integer.parseInt(preferences.getString(THREAD_TRIM_LENGTH, "500"))); + + preferences.edit() + .remove(THREAD_TRIM_ENABLED) + .remove(THREAD_TRIM_LENGTH) + .apply(); + } + } + + @Override + boolean shouldRetry(@NonNull Exception e) { + return false; + } + + @Override + public @NonNull String getFactoryKey() { + return KEY; + } + + public static class Factory implements Job.Factory { + @Override + public @NonNull TrimByLengthSettingsMigrationJob create(@NonNull Parameters parameters, @NonNull Data data) { + return new TrimByLengthSettingsMigrationJob(parameters); + } + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/preferences/StoragePreferenceFragment.java b/app/src/main/java/org/thoughtcrime/securesms/preferences/StoragePreferenceFragment.java index da5410e98e..4ceb228819 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/preferences/StoragePreferenceFragment.java +++ b/app/src/main/java/org/thoughtcrime/securesms/preferences/StoragePreferenceFragment.java @@ -1,35 +1,69 @@ package org.thoughtcrime.securesms.preferences; +import android.annotation.SuppressLint; import android.os.Bundle; +import android.text.Editable; +import android.text.TextUtils; +import android.text.TextWatcher; +import android.view.LayoutInflater; +import android.view.View; +import android.widget.EditText; +import android.widget.TextView; 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 com.annimon.stream.Stream; + import org.thoughtcrime.securesms.ApplicationPreferencesActivity; import org.thoughtcrime.securesms.R; -import org.thoughtcrime.securesms.logging.Log; +import org.thoughtcrime.securesms.components.settings.BaseSettingsAdapter; +import org.thoughtcrime.securesms.components.settings.BaseSettingsFragment; +import org.thoughtcrime.securesms.components.settings.CustomizableSingleSelectSetting; +import org.thoughtcrime.securesms.components.settings.SingleSelectSetting; +import org.thoughtcrime.securesms.database.DatabaseFactory; +import org.thoughtcrime.securesms.database.ThreadDatabase; +import org.thoughtcrime.securesms.dependencies.ApplicationDependencies; +import org.thoughtcrime.securesms.keyvalue.KeepMessagesDuration; +import org.thoughtcrime.securesms.keyvalue.SettingsValues; +import org.thoughtcrime.securesms.keyvalue.SignalStore; 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; +import org.thoughtcrime.securesms.util.MappingModelList; +import org.thoughtcrime.securesms.util.StringUtil; +import org.thoughtcrime.securesms.util.concurrent.SignalExecutors; + +import java.text.NumberFormat; public class StoragePreferenceFragment extends ListSummaryPreferenceFragment { - private static final String TAG = Log.tag(StoragePreferenceFragment.class); + private Preference keepMessages; + private Preference trimLength; @Override - public void onCreate(Bundle paramBundle) { + public void onCreate(@Nullable Bundle paramBundle) { super.onCreate(paramBundle); - findPreference(TextSecurePreferences.THREAD_TRIM_NOW) - .setOnPreferenceClickListener(new TrimNowClickListener()); - findPreference(TextSecurePreferences.THREAD_TRIM_LENGTH) - .setOnPreferenceChangeListener(new TrimLengthValidationListener()); + findPreference("pref_storage_clear_message_history") + .setOnPreferenceClickListener(new ClearMessageHistoryClickListener()); + + trimLength = findPreference(SettingsValues.THREAD_TRIM_LENGTH); + trimLength.setOnPreferenceClickListener(p -> { + getApplicationPreferencesActivity().requireSupportActionBar().setTitle(R.string.preferences__conversation_length_limit); + getApplicationPreferencesActivity().pushFragment(BaseSettingsFragment.create(new ConversationLengthLimitConfiguration())); + return true; + }); + + keepMessages = findPreference(SettingsValues.KEEP_MESSAGES_DURATION); + keepMessages.setOnPreferenceClickListener(p -> { + getApplicationPreferencesActivity().requireSupportActionBar().setTitle(R.string.preferences__keep_messages); + getApplicationPreferencesActivity().pushFragment(BaseSettingsFragment.create(new KeepMessagesConfiguration())); + return true; + }); StoragePreferenceCategory storageCategory = (StoragePreferenceCategory) findPreference("pref_storage_category"); FragmentActivity activity = requireActivity(); @@ -41,19 +75,24 @@ public class StoragePreferenceFragment extends ListSummaryPreferenceFragment { } @Override - public void onCreatePreferences(@Nullable Bundle savedInstanceState, String rootKey) { + public void onCreatePreferences(@Nullable Bundle savedInstanceState, @Nullable String rootKey) { addPreferencesFromResource(R.xml.preferences_storage); } @Override public void onResume() { super.onResume(); - ((ApplicationPreferencesActivity)getActivity()).getSupportActionBar().setTitle(R.string.preferences__storage); + ((ApplicationPreferencesActivity) requireActivity()).requireSupportActionBar().setTitle(R.string.preferences__storage); FragmentActivity activity = requireActivity(); ApplicationPreferencesViewModel viewModel = ApplicationPreferencesViewModel.getApplicationPreferencesViewModel(activity); viewModel.refreshStorageBreakdown(activity.getApplicationContext()); + + keepMessages.setSummary(SignalStore.settings().getKeepMessagesDuration().getStringResource()); + + trimLength.setSummary(SignalStore.settings().isTrimByLengthEnabled() ? getString(R.string.preferences_storage__s_messages, NumberFormat.getInstance().format(SignalStore.settings().getThreadTrimLength())) + : getString(R.string.preferences_storage__none)); } @Override @@ -61,49 +100,197 @@ public class StoragePreferenceFragment extends ListSummaryPreferenceFragment { Permissions.onRequestPermissionsResult(this, requestCode, permissions, grantResults); } - private class TrimNowClickListener implements Preference.OnPreferenceClickListener { + private @NonNull ApplicationPreferencesActivity getApplicationPreferencesActivity() { + return (ApplicationPreferencesActivity) requireActivity(); + } + + private class ClearMessageHistoryClickListener implements Preference.OnPreferenceClickListener { @Override - public boolean onPreferenceClick(Preference preference) { - final int threadLengthLimit = TextSecurePreferences.getThreadTrimLength(getActivity()); + public boolean onPreferenceClick(@NonNull Preference preference) { 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)) + .setTitle(R.string.preferences_storage__clear_message_history) + .setMessage(R.string.preferences_storage__this_will_delete_all_message_history_and_media_from_your_device) + .setPositiveButton(R.string.delete, (d, w) -> showAreYouReallySure()) .setNegativeButton(android.R.string.cancel, null) .show(); return true; } + + private void showAreYouReallySure() { + new AlertDialog.Builder(requireActivity()) + .setTitle(R.string.preferences_storage__are_you_sure_you_want_to_delete_all_message_history) + .setMessage(R.string.preferences_storage__all_message_history_will_be_permanently_removed_this_action_cannot_be_undone) + .setPositiveButton(R.string.preferences_storage__delete_all_now, (d, w) -> SignalExecutors.BOUNDED.execute(() -> DatabaseFactory.getThreadDatabase(ApplicationDependencies.getApplication()).deleteAllConversations())) + .setNegativeButton(android.R.string.cancel, null) + .show(); + } } - private class TrimLengthValidationListener implements Preference.OnPreferenceChangeListener { + public static class KeepMessagesConfiguration extends BaseSettingsFragment.Configuration implements SingleSelectSetting.SingleSelectSelectionChangedListener { - TrimLengthValidationListener() { - EditTextPreference preference = (EditTextPreference)findPreference(TextSecurePreferences.THREAD_TRIM_LENGTH); - onPreferenceChange(preference, preference.getText()); + @Override + public void configureAdapter(@NonNull BaseSettingsAdapter adapter) { + adapter.configureSingleSelect(this); } @Override - public boolean onPreferenceChange(Preference preference, Object newValue) { - if (newValue == null || ((String)newValue).trim().length() == 0) { - return false; + public @NonNull MappingModelList getSettings() { + KeepMessagesDuration currentDuration = SignalStore.settings().getKeepMessagesDuration(); + return Stream.of(KeepMessagesDuration.values()) + .map(duration -> new SingleSelectSetting.Item(duration, activity.getString(duration.getStringResource()), duration.equals(currentDuration))) + .collect(MappingModelList.toMappingModelList()); + } + + @Override + public void onSelectionChanged(@NonNull Object selection) { + KeepMessagesDuration currentDuration = SignalStore.settings().getKeepMessagesDuration(); + KeepMessagesDuration newDuration = (KeepMessagesDuration) selection; + + if (newDuration.ordinal() > currentDuration.ordinal()) { + new AlertDialog.Builder(activity) + .setTitle(R.string.preferences_storage__delete_older_messages) + .setMessage(activity.getString(R.string.preferences_storage__this_will_permanently_delete_all_message_history_and_media, activity.getString(newDuration.getStringResource()))) + .setPositiveButton(R.string.delete, (d, w) -> updateTrimByTime(newDuration)) + .setNegativeButton(android.R.string.cancel, null) + .show(); + } else { + updateTrimByTime(newDuration); + } + } + + private void updateTrimByTime(@NonNull KeepMessagesDuration newDuration) { + SignalStore.settings().setKeepMessagesForDuration(newDuration); + updateSettingsList(); + ApplicationDependencies.getTrimThreadsByDateManager().scheduleIfNecessary(); + } + } + + public static class ConversationLengthLimitConfiguration extends BaseSettingsFragment.Configuration implements CustomizableSingleSelectSetting.CustomizableSingleSelectionListener { + + private static final int CUSTOM_LENGTH = -1; + + @Override + public void configureAdapter(@NonNull BaseSettingsAdapter adapter) { + adapter.configureSingleSelect(this); + adapter.configureCustomizableSingleSelect(this); + } + + @Override + public @NonNull MappingModelList getSettings() { + int trimLength = SignalStore.settings().isTrimByLengthEnabled() ? SignalStore.settings().getThreadTrimLength() : 0; + int[] options = activity.getResources().getIntArray(R.array.conversation_length_limit); + boolean hasSelection = false; + MappingModelList settings = new MappingModelList(); + + for (int option : options) { + boolean isSelected = option == trimLength; + String text = option == 0 ? activity.getString(R.string.preferences_storage__none) + : activity.getString(R.string.preferences_storage__s_messages, NumberFormat.getInstance().format(option)); + + settings.add(new SingleSelectSetting.Item(option, text, isSelected)); + + hasSelection = hasSelection || isSelected; } - int value; - try { - value = Integer.parseInt((String)newValue); - } catch (NumberFormatException nfe) { - Log.w(TAG, nfe); - return false; + int currentValue = SignalStore.settings().getThreadTrimLength(); + settings.add(new CustomizableSingleSelectSetting.Item(CUSTOM_LENGTH, + activity.getString(R.string.preferences_storage__custom), + !hasSelection, + currentValue, + activity.getString(R.string.preferences_storage__s_messages, NumberFormat.getInstance().format(currentValue)))); + return settings; + } + + @SuppressLint("InflateParams") + @Override + public void onCustomizeClicked(@Nullable CustomizableSingleSelectSetting.Item item) { + boolean trimLengthEnabled = SignalStore.settings().isTrimByLengthEnabled(); + int trimLength = trimLengthEnabled ? SignalStore.settings().getThreadTrimLength() : 0; + + View view = LayoutInflater.from(activity).inflate(R.layout.customizable_setting_edit_text, null, false); + EditText editText = view.findViewById(R.id.customizable_setting_edit_text); + if (trimLength > 0) { + editText.setText(String.valueOf(trimLength)); } - if (value < 1) { - return false; - } + AlertDialog dialog = new AlertDialog.Builder(activity) + .setTitle(R.string.preferences_Storage__custom_conversation_length_limit) + .setView(view) + .setPositiveButton(android.R.string.ok, (d, w) -> onSelectionChanged(Integer.parseInt(editText.getText().toString()))) + .setNegativeButton(android.R.string.cancel, (d, w) -> updateSettingsList()) + .create(); - preference.setSummary(getResources().getQuantityString(R.plurals.ApplicationPreferencesActivity_messages_per_conversation, value, value)); - return true; + dialog.setOnShowListener(d -> { + dialog.getButton(AlertDialog.BUTTON_POSITIVE).setEnabled(!TextUtils.isEmpty(editText.getText())); + editText.requestFocus(); + editText.addTextChangedListener(new TextWatcher() { + @Override + public void afterTextChanged(@NonNull Editable sequence) { + CharSequence trimmed = StringUtil.trimSequence(sequence); + if (TextUtils.isEmpty(trimmed)) { + sequence.replace(0, sequence.length(), ""); + } else { + try { + Integer.parseInt(trimmed.toString()); + dialog.getButton(AlertDialog.BUTTON_POSITIVE).setEnabled(true); + return; + } catch (NumberFormatException e) { + String onlyDigits = trimmed.toString().replaceAll("[^\\d]", ""); + if (!onlyDigits.equals(trimmed.toString())) { + sequence.replace(0, sequence.length(), onlyDigits); + } + } + } + dialog.getButton(AlertDialog.BUTTON_POSITIVE).setEnabled(false); + } + + @Override + public void beforeTextChanged(@NonNull CharSequence sequence, int start, int count, int after) {} + + @Override + public void onTextChanged(@NonNull CharSequence sequence, int start, int before, int count) {} + }); + }); + + dialog.show(); + } + + @Override + public void onSelectionChanged(@NonNull Object selection) { + boolean trimLengthEnabled = SignalStore.settings().isTrimByLengthEnabled(); + int trimLength = trimLengthEnabled ? SignalStore.settings().getThreadTrimLength() : 0; + int newTrimLength = (Integer) selection; + + if (newTrimLength > 0 && (!trimLengthEnabled || newTrimLength < trimLength)) { + new AlertDialog.Builder(activity) + .setTitle(R.string.preferences_storage__delete_older_messages) + .setMessage(activity.getString(R.string.preferences_storage__this_will_permanently_trim_all_conversations_to_the_d_most_recent_messages, NumberFormat.getInstance().format(newTrimLength))) + .setPositiveButton(R.string.delete, (d, w) -> updateTrimByLength(newTrimLength)) + .setNegativeButton(android.R.string.cancel, null) + .show(); + } else if (newTrimLength == CUSTOM_LENGTH) { + onCustomizeClicked(null); + } else { + updateTrimByLength(newTrimLength); + } + } + + private void updateTrimByLength(int length) { + boolean restrictingChange = !SignalStore.settings().isTrimByLengthEnabled() || length < SignalStore.settings().getThreadTrimLength(); + + SignalStore.settings().setThreadTrimByLengthEnabled(length > 0); + SignalStore.settings().setThreadTrimLength(length); + updateSettingsList(); + + if (SignalStore.settings().isTrimByLengthEnabled() && restrictingChange) { + KeepMessagesDuration keepMessagesDuration = SignalStore.settings().getKeepMessagesDuration(); + + long trimBeforeDate = keepMessagesDuration != KeepMessagesDuration.FOREVER ? System.currentTimeMillis() - keepMessagesDuration.getDuration() + : ThreadDatabase.NO_TRIM_BEFORE_DATE_SET; + + SignalExecutors.BOUNDED.execute(() -> DatabaseFactory.getThreadDatabase(ApplicationDependencies.getApplication()).trimAllThreads(length, trimBeforeDate)); + } } } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/service/TrimThreadsByDateManager.java b/app/src/main/java/org/thoughtcrime/securesms/service/TrimThreadsByDateManager.java new file mode 100644 index 0000000000..8953625091 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/service/TrimThreadsByDateManager.java @@ -0,0 +1,100 @@ +package org.thoughtcrime.securesms.service; + +import android.app.Application; +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import org.thoughtcrime.securesms.database.DatabaseFactory; +import org.thoughtcrime.securesms.database.MmsSmsDatabase; +import org.thoughtcrime.securesms.database.ThreadDatabase; +import org.thoughtcrime.securesms.dependencies.ApplicationDependencies; +import org.thoughtcrime.securesms.keyvalue.KeepMessagesDuration; +import org.thoughtcrime.securesms.keyvalue.SignalStore; +import org.thoughtcrime.securesms.logging.Log; + +public class TrimThreadsByDateManager extends TimedEventManager { + + private static final String TAG = Log.tag(TrimThreadsByDateManager.class); + + private final ThreadDatabase threadDatabase; + private final MmsSmsDatabase mmsSmsDatabase; + + public TrimThreadsByDateManager(@NonNull Application application) { + super(application, "TrimThreadsByDateManager"); + + threadDatabase = DatabaseFactory.getThreadDatabase(application); + mmsSmsDatabase = DatabaseFactory.getMmsSmsDatabase(application); + + scheduleIfNecessary(); + } + + @Override + protected @Nullable TrimEvent getNextClosestEvent() { + KeepMessagesDuration keepMessagesDuration = SignalStore.settings().getKeepMessagesDuration(); + if (keepMessagesDuration == KeepMessagesDuration.FOREVER) { + return null; + } + + long trimBeforeDate = System.currentTimeMillis() - keepMessagesDuration.getDuration(); + + if (mmsSmsDatabase.getMessageCountBeforeDate(trimBeforeDate) > 0) { + Log.i(TAG, "Messages exist before date, trim immediately"); + return new TrimEvent(0); + } + + long timestamp = mmsSmsDatabase.getTimestampForFirstMessageAfterDate(trimBeforeDate); + + if (timestamp == 0) { + return null; + } + + return new TrimEvent(Math.max(0, keepMessagesDuration.getDuration() - (System.currentTimeMillis() - timestamp))); + } + + @Override + protected void executeEvent(@NonNull TrimEvent event) { + KeepMessagesDuration keepMessagesDuration = SignalStore.settings().getKeepMessagesDuration(); + + int trimLength = SignalStore.settings().isTrimByLengthEnabled() ? SignalStore.settings().getThreadTrimLength() + : ThreadDatabase.NO_TRIM_MESSAGE_COUNT_SET; + + long trimBeforeDate = keepMessagesDuration != KeepMessagesDuration.FOREVER ? System.currentTimeMillis() - keepMessagesDuration.getDuration() + : ThreadDatabase.NO_TRIM_BEFORE_DATE_SET; + + Log.i(TAG, "Trimming all threads with length: " + trimLength + " before: " + trimBeforeDate); + threadDatabase.trimAllThreads(trimLength, trimBeforeDate); + } + + @Override + protected long getDelayForEvent(@NonNull TrimEvent event) { + return event.delay; + } + + @Override + protected void scheduleAlarm(@NonNull Application application, long delay) { + setAlarm(application, delay, TrimThreadsByDateAlarm.class); + } + + public static class TrimThreadsByDateAlarm extends BroadcastReceiver { + + private static final String TAG = Log.tag(TrimThreadsByDateAlarm.class); + + @Override + public void onReceive(@NonNull Context context, @NonNull Intent intent) { + Log.d(TAG, "onReceive()"); + ApplicationDependencies.getTrimThreadsByDateManager().scheduleIfNecessary(); + } + } + + public static class TrimEvent { + final long delay; + + public TrimEvent(long delay) { + this.delay = delay; + } + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/util/MappingModelList.java b/app/src/main/java/org/thoughtcrime/securesms/util/MappingModelList.java new file mode 100644 index 0000000000..0413eefbeb --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/util/MappingModelList.java @@ -0,0 +1,32 @@ +package org.thoughtcrime.securesms.util; + +import androidx.annotation.NonNull; + +import com.annimon.stream.Collector; +import com.annimon.stream.function.BiConsumer; +import com.annimon.stream.function.Function; +import com.annimon.stream.function.Supplier; + +import java.util.ArrayList; + +public class MappingModelList extends ArrayList> { + + public static @NonNull Collector, MappingModelList, MappingModelList> toMappingModelList() { + return new Collector, MappingModelList, MappingModelList>() { + @Override + public @NonNull Supplier supplier() { + return MappingModelList::new; + } + + @Override + public @NonNull BiConsumer> accumulator() { + return MappingModelList::add; + } + + @Override + public @NonNull Function finisher() { + return mappingModels -> mappingModels; + } + }; + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/util/TextSecurePreferences.java b/app/src/main/java/org/thoughtcrime/securesms/util/TextSecurePreferences.java index 8036cdfdfe..43d672b857 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/util/TextSecurePreferences.java +++ b/app/src/main/java/org/thoughtcrime/securesms/util/TextSecurePreferences.java @@ -51,8 +51,6 @@ public class TextSecurePreferences { public static final String MMSC_USERNAME_PREF = "pref_apn_mmsc_username"; private static final String MMSC_CUSTOM_PASSWORD_PREF = "pref_apn_mmsc_custom_password"; public static final String MMSC_PASSWORD_PREF = "pref_apn_mmsc_password"; - public static final String THREAD_TRIM_LENGTH = "pref_trim_length"; - public static final String THREAD_TRIM_NOW = "pref_trim_now"; public static final String ENABLE_MANUAL_MMS_PREF = "pref_enable_manual_mms"; private static final String LAST_VERSION_CODE_PREF = "last_version_code"; @@ -74,7 +72,6 @@ public class TextSecurePreferences { private static final String SMS_DELIVERY_REPORT_PREF = "pref_delivery_report_sms"; public static final String MMS_USER_AGENT = "pref_mms_user_agent"; private static final String MMS_CUSTOM_USER_AGENT = "pref_custom_mms_user_agent"; - private static final String THREAD_TRIM_ENABLED = "pref_trim_threads"; private static final String LOCAL_NUMBER_PREF = "pref_local_number"; private static final String LOCAL_UUID_PREF = "pref_local_uuid"; private static final String LOCAL_USERNAME_PREF = "pref_local_username"; @@ -1014,14 +1011,6 @@ public class TextSecurePreferences { setStringPreference(context, LED_BLINK_PREF_CUSTOM, pattern); } - public static boolean isThreadLengthTrimmingEnabled(Context context) { - return getBooleanPreference(context, THREAD_TRIM_ENABLED, false); - } - - public static int getThreadTrimLength(Context context) { - return Integer.parseInt(getStringPreference(context, THREAD_TRIM_LENGTH, "500")); - } - public static boolean isSystemEmojiPreferred(Context context) { return getBooleanPreference(context, SYSTEM_EMOJI_PREF, false); } diff --git a/app/src/main/java/org/thoughtcrime/securesms/util/Trimmer.java b/app/src/main/java/org/thoughtcrime/securesms/util/Trimmer.java deleted file mode 100644 index eac7575f09..0000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/util/Trimmer.java +++ /dev/null @@ -1,65 +0,0 @@ -package org.thoughtcrime.securesms.util; - -import android.app.ProgressDialog; -import android.content.Context; -import android.os.AsyncTask; -import android.widget.Toast; - -import org.thoughtcrime.securesms.R; -import org.thoughtcrime.securesms.database.DatabaseFactory; -import org.thoughtcrime.securesms.database.ThreadDatabase; - -public class Trimmer { - - public static void trimAllThreads(Context context, int threadLengthLimit) { - new TrimmingProgressTask(context).executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, threadLengthLimit); - } - - private static class TrimmingProgressTask extends AsyncTask implements ThreadDatabase.ProgressListener { - private ProgressDialog progressDialog; - private Context context; - - public TrimmingProgressTask(Context context) { - this.context = context; - } - - @Override - protected void onPreExecute() { - progressDialog = new ProgressDialog(context); - progressDialog.setProgressStyle(ProgressDialog.STYLE_HORIZONTAL); - progressDialog.setCancelable(false); - progressDialog.setIndeterminate(false); - progressDialog.setTitle(R.string.trimmer__deleting); - progressDialog.setMessage(context.getString(R.string.trimmer__deleting_old_messages)); - progressDialog.setMax(100); - progressDialog.show(); - } - - @Override - protected Void doInBackground(Integer... params) { - DatabaseFactory.getThreadDatabase(context).trimAllThreads(params[0], this); - return null; - } - - @Override - protected void onProgressUpdate(Integer... progress) { - double count = progress[1]; - double index = progress[0]; - - progressDialog.setProgress((int)Math.round((index / count) * 100.0)); - } - - @Override - protected void onPostExecute(Void result) { - progressDialog.dismiss(); - Toast.makeText(context, - R.string.trimmer__old_messages_successfully_deleted, - Toast.LENGTH_LONG).show(); - } - - @Override - public void onProgress(int complete, int total) { - this.publishProgress(complete, total); - } - } -} diff --git a/app/src/main/res/drawable/ic_settings_outline_24.xml b/app/src/main/res/drawable/ic_settings_outline_24.xml new file mode 100644 index 0000000000..dad0eb5d45 --- /dev/null +++ b/app/src/main/res/drawable/ic_settings_outline_24.xml @@ -0,0 +1,12 @@ + + + + diff --git a/app/src/main/res/layout/base_settings_fragment.xml b/app/src/main/res/layout/base_settings_fragment.xml new file mode 100644 index 0000000000..adaa315d9a --- /dev/null +++ b/app/src/main/res/layout/base_settings_fragment.xml @@ -0,0 +1,9 @@ + + diff --git a/app/src/main/res/layout/customizable_setting_edit_text.xml b/app/src/main/res/layout/customizable_setting_edit_text.xml new file mode 100644 index 0000000000..049b4eb78a --- /dev/null +++ b/app/src/main/res/layout/customizable_setting_edit_text.xml @@ -0,0 +1,14 @@ + + + + + + diff --git a/app/src/main/res/layout/customizable_single_select_item.xml b/app/src/main/res/layout/customizable_single_select_item.xml new file mode 100644 index 0000000000..91b5dadc99 --- /dev/null +++ b/app/src/main/res/layout/customizable_single_select_item.xml @@ -0,0 +1,77 @@ + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/single_select_item.xml b/app/src/main/res/layout/single_select_item.xml new file mode 100644 index 0000000000..af7baac1a7 --- /dev/null +++ b/app/src/main/res/layout/single_select_item.xml @@ -0,0 +1,16 @@ + + diff --git a/app/src/main/res/values/arrays.xml b/app/src/main/res/values/arrays.xml index 5bde206530..a56c1b8153 100644 --- a/app/src/main/res/values/arrays.xml +++ b/app/src/main/res/values/arrays.xml @@ -294,6 +294,14 @@ 604800 + + 0 + 5000 + 1000 + 500 + 100 + + #ffffff #ff0000 diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index b9dcfb507e..c3291deb78 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -2059,13 +2059,11 @@ MMSC Password SMS delivery reports Request a delivery report for each SMS message you send - Automatically delete older messages once a conversation exceeds a specified length - Delete old messages Chats and media Storage Conversation length limit - Trim all conversations now - Scan through all conversations and enforce conversation length limits + Keep messages + Clear message history Linked devices Light Dark @@ -2095,13 +2093,29 @@ When using Wi-Fi When roaming Media auto-download - Message trimming + Message history Storage usage Photos Videos Files Audio Review storage + Delete older messages? + Clear message history? + This will permanently delete all messsage history and media from your device that are older than %1$s. + This will permanently trim all conversations to the %1$s most recent messages. + This will permanently delete all messsage history and media from your device. + Are you sure you want to delete all message history? + All message history will be permanently removed. This action cannot be undone. + Delete all now + Forever + 1 year + 6 months + 30 days + None + %1$s messages + Custom + Custom conversation length limit Use system emoji Disable Signal\'s built-in emoji support Relay all calls through the Signal server to avoid revealing your IP address to your contact. Enabling will reduce call quality. @@ -2132,6 +2146,8 @@ Receive notifications when you’re mentioned in muted chats Setup a username + Customize option + Internal Preferences Groups V2 diff --git a/app/src/main/res/xml/preferences_storage.xml b/app/src/main/res/xml/preferences_storage.xml index 903d69d610..24b2333552 100644 --- a/app/src/main/res/xml/preferences_storage.xml +++ b/app/src/main/res/xml/preferences_storage.xml @@ -1,5 +1,6 @@ - + @@ -9,26 +10,22 @@ - - - - + android:title="@string/preferences_chats__message_history"> + android:key="settings.keep_messages_duration" + android:title="@string/preferences__keep_messages" + tools:summary="@string/preferences_storage__forever" /> + + + +