Add trim conversations by time option.

This commit is contained in:
Cody Henthorne 2020-09-03 17:52:44 -04:00
parent 6a14dc69c0
commit bcd27355f9
36 changed files with 1183 additions and 233 deletions

View file

@ -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<FlipperSqlCipherAdapter.Descriptor> {
private static final String TAG = Log.tag(FlipperSqlCipherAdapter.class);
public FlipperSqlCipherAdapter(Context context) {
super(context);
}
@Override
public List<Descriptor> 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

View file

@ -640,6 +640,8 @@
<receiver android:name=".revealable.ViewOnceMessageManager$ViewOnceAlarm" />
<receiver android:name=".service.TrimThreadsByDateManager$TrimThreadsByDateAlarm" />
<provider android:name=".providers.PartProvider"
android:grantUriPermissions="true"
android:exported="false"

View file

@ -142,6 +142,14 @@ public class ApplicationPreferencesActivity extends PassphraseRequiredActivity
}
}
public void pushFragment(@NonNull Fragment fragment) {
getSupportFragmentManager().beginTransaction()
.setCustomAnimations(R.anim.slide_from_end, R.anim.slide_to_start, R.anim.slide_from_start, R.anim.slide_to_end)
.replace(android.R.id.content, fragment)
.addToBackStack(null)
.commit();
}
public static class ApplicationPreferenceFragment extends CorrectedPreferenceFragment {
@Override
@ -292,14 +300,7 @@ public class ApplicationPreferencesActivity extends PassphraseRequiredActivity
Bundle args = new Bundle();
fragment.setArguments(args);
FragmentManager fragmentManager = getActivity().getSupportFragmentManager();
FragmentTransaction fragmentTransaction = fragmentManager.beginTransaction();
fragmentTransaction.setCustomAnimations(R.anim.slide_from_end, R.anim.slide_to_start, R.anim.slide_from_start, R.anim.slide_to_end);
fragmentTransaction.replace(android.R.id.content, fragment);
fragmentTransaction.addToBackStack(null);
fragmentTransaction.commit();
((ApplicationPreferencesActivity) requireActivity()).pushFragment(fragment);
}
return true;

View file

@ -91,7 +91,7 @@ public abstract class BaseActivity extends AppCompatActivity {
Log.d(TAG, "[" + Log.tag(getClass()) + "] " + event);
}
protected final @NonNull ActionBar requireSupportActionBar() {
public final @NonNull ActionBar requireSupportActionBar() {
return Objects.requireNonNull(getSupportActionBar());
}
}

View file

@ -0,0 +1,21 @@
package org.thoughtcrime.securesms.components.settings;
import androidx.annotation.NonNull;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.util.MappingAdapter;
/**
* Reusable adapter for generic settings list.
*/
public class BaseSettingsAdapter extends MappingAdapter {
public void configureSingleSelect(@NonNull SingleSelectSetting.SingleSelectSelectionChangedListener selectionChangedListener) {
registerFactory(SingleSelectSetting.Item.class,
new LayoutFactory<>(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));
}
}

View file

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

View file

@ -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<Item> {
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<Item> {
private SingleSelectSetting.Item singleSelectItem;
private Object customValue;
private String summaryText;
public <T> 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);
}
}
}

View file

@ -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<Item> {
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<Item> {
private final String text;
private final Object item;
private final boolean isSelected;
public <T> 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;
}
}
}

View file

@ -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<String> filesOnDisk = new HashSet<>();
Set<String> 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<String> onDiskButNotInDatabase = SetUtil.difference(filesOnDisk, filesInDb);
for (String filePath : onDiskButNotInDatabase) {
//noinspection ResultOfMethodCallIgnored
new File(filePath).delete();
}
}
@SuppressWarnings("ResultOfMethodCallIgnored")
void deleteAllAttachments() {
SQLiteDatabase database = databaseHelper.getWritableDatabase();

View file

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

View file

@ -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<Long> threadIds);
abstract void deleteAllThreads();
abstract void deleteAbandonedMessages();
public abstract SQLiteDatabase beginTransaction();
public abstract void endTransaction(SQLiteDatabase database);

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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<TrimByLengthSettingsMigrationJob> {
@Override
public @NonNull TrimByLengthSettingsMigrationJob create(@NonNull Parameters parameters, @NonNull Data data) {
return new TrimByLengthSettingsMigrationJob(parameters);
}
}
}

View file

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

View file

@ -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<TrimThreadsByDateManager.TrimEvent> {
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;
}
}
}

View file

@ -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<MappingModel<?>> {
public static @NonNull Collector<MappingModel<?>, MappingModelList, MappingModelList> toMappingModelList() {
return new Collector<MappingModel<?>, MappingModelList, MappingModelList>() {
@Override
public @NonNull Supplier<MappingModelList> supplier() {
return MappingModelList::new;
}
@Override
public @NonNull BiConsumer<MappingModelList, MappingModel<?>> accumulator() {
return MappingModelList::add;
}
@Override
public @NonNull Function<MappingModelList, MappingModelList> finisher() {
return mappingModels -> mappingModels;
}
};
}
}

View file

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

View file

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

View file

@ -0,0 +1,12 @@
<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="#FF000000"
android:pathData="M12,8.5A3.5,3.5 0,1 1,8.5 12,3.5 3.5,0 0,1 12,8.5M12,7a5,5 0,1 0,5 5,5 5,0 0,0 -5,-5Z"/>
<path
android:fillColor="#FF000000"
android:pathData="M12,2.51a9.83,9.83 0,0 1,1.21 0.08l0.21,2.49 0.91,0.33a5.72,5.72 0,0 1,0.68 0.28l0.88,0.42L17.8,4.49A9.23,9.23 0,0 1,19.51 6.2L17.89,8.11l0.42,0.88a5.72,5.72 0,0 1,0.28 0.68l0.33,0.91 2.49,0.21a8.91,8.91 0,0 1,0 2.42l-2.49,0.21 -0.33,0.91a5.72,5.72 0,0 1,-0.28 0.68l-0.42,0.88 1.62,1.91a9.23,9.23 0,0 1,-1.71 1.71l-1.91,-1.62 -0.88,0.42a5.72,5.72 0,0 1,-0.68 0.28l-0.91,0.33 -0.21,2.49a9.19,9.19 0,0 1,-2.42 0l-0.21,-2.49 -0.91,-0.33A5.72,5.72 0,0 1,9 18.31l-0.88,-0.42L6.2,19.51A9.23,9.23 0,0 1,4.49 17.8l1.62,-1.91L5.69,15a5.72,5.72 0,0 1,-0.28 -0.68l-0.33,-0.91 -2.49,-0.21a8.91,8.91 0,0 1,0 -2.42l2.49,-0.21 0.33,-0.91A5.72,5.72 0,0 1,5.69 9l0.42,-0.88L4.49,6.2A9.23,9.23 0,0 1,6.2 4.49L8.11,6.11 9,5.69a5.72,5.72 0,0 1,0.68 -0.28l0.91,-0.33 0.21,-2.49A9.83,9.83 0,0 1,12 2.51h0M12,1a10.93,10.93 0,0 0,-1.88 0.16,1 1,0 0,0 -0.79,0.9L9.17,4a7.64,7.64 0,0 0,-0.83 0.35L6.87,3.09a1,1 0,0 0,-0.66 -0.24A1,1 0,0 0,5.67 3,11 11,0 0,0 3.05,5.62a1,1 0,0 0,0 1.25L4.34,8.34A7.64,7.64 0,0 0,4 9.17l-1.92,0.16a1,1 0,0 0,-0.9 0.79,11 11,0 0,0 0,3.76 1,1 0,0 0,0.9 0.79L4,14.83a7.64,7.64 0,0 0,0.35 0.83L3.09,17.13A1,1 0,0 0,3 18.33,11 11,0 0,0 5.62,21a1,1 0,0 0,0.61 0.19,1 1,0 0,0 0.64,-0.23l1.47,-1.25a7.64,7.64 0,0 0,0.83 0.35l0.16,1.92a1,1 0,0 0,0.79 0.9A11.83,11.83 0,0 0,12 23a10.93,10.93 0,0 0,1.88 -0.16,1 1,0 0,0 0.79,-0.9L14.83,20a7.64,7.64 0,0 0,0.83 -0.35l1.47,1.25a1,1 0,0 0,0.66 0.24,1 1,0 0,0 0.54,-0.16A11,11 0,0 0,21 18.38a1,1 0,0 0,0 -1.25l-1.25,-1.47a7.64,7.64 0,0 0,0.35 -0.83l1.92,-0.16a1,1 0,0 0,0.9 -0.79,11 11,0 0,0 0,-3.76 1,1 0,0 0,-0.9 -0.79L20,9.17a7.64,7.64 0,0 0,-0.35 -0.83l1.25,-1.47A1,1 0,0 0,21 5.67a11,11 0,0 0,-2.61 -2.62,1 1,0 0,0 -0.61,-0.19 1,1 0,0 0,-0.64 0.23L15.66,4.34A7.64,7.64 0,0 0,14.83 4l-0.16,-1.92a1,1 0,0 0,-0.79 -0.9A11.83,11.83 0,0 0,12 1Z"/>
</vector>

View file

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.recyclerview.widget.RecyclerView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/base_settings_list"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:scrollIndicators="top|bottom"
tools:ignore="UnusedAttribute"
tools:listitem="@layout/single_select_item" />

View file

@ -0,0 +1,14 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingStart="20dp"
android:paddingEnd="20dp">
<EditText
android:id="@+id/customizable_setting_edit_text"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="number"/>
</FrameLayout>

View file

@ -0,0 +1,77 @@
<?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"
android:background="?selectableItemBackground"
android:minHeight="?attr/listPreferredItemHeightSmall"
android:orientation="horizontal"
android:paddingStart="20dp"
android:paddingTop="8dp"
android:paddingEnd="?attr/dialogPreferredPadding"
android:paddingBottom="8dp">
<RadioButton
android:id="@+id/customizable_single_select_radio"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:paddingEnd="20dp"
android:clickable="false"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:ignore="RtlSymmetry" />
<CheckedTextView
android:id="@+id/single_select_item_text"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:gravity="center_vertical"
android:textAppearance="?android:attr/textAppearanceMedium"
android:textColor="?attr/textColorAlertDialogListItem"
app:layout_constraintBottom_toTopOf="@id/customizable_single_select_summary"
app:layout_constraintStart_toEndOf="@id/customizable_single_select_radio"
app:layout_constraintTop_toTopOf="parent"
tools:text="Pick me!" />
<TextView
android:id="@+id/customizable_single_select_summary"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textColor="@color/core_grey_60"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="@id/single_select_item_text"
app:layout_constraintTop_toBottomOf="@id/single_select_item_text"
tools:text="Test" />
<ImageView
android:id="@+id/customizable_single_select_customize"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="?selectableItemBackground"
android:contentDescription="@string/configurable_single_select__customize_option"
android:padding="10dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:srcCompat="@drawable/ic_settings_outline_24" />
<View
android:id="@+id/customizable_single_select_divider"
android:layout_width="1dp"
android:layout_height="0dp"
android:layout_marginEnd="10dp"
android:background="@color/core_grey_20"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@id/customizable_single_select_customize"
app:layout_constraintTop_toTopOf="parent" />
<androidx.constraintlayout.widget.Group
android:id="@+id/customizable_single_select_customize_group"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:visibility="gone"
app:constraint_referenced_ids="customizable_single_select_summary,customizable_single_select_customize,customizable_single_select_divider" />
</androidx.constraintlayout.widget.ConstraintLayout>

View file

@ -0,0 +1,16 @@
<?xml version="1.0" encoding="utf-8"?>
<CheckedTextView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/single_select_item_text"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?selectableItemBackground"
android:drawableStart="?android:attr/listChoiceIndicatorSingle"
android:drawablePadding="20dp"
android:gravity="center_vertical"
android:minHeight="?attr/listPreferredItemHeightSmall"
android:paddingStart="20dp"
android:paddingEnd="?attr/dialogPreferredPadding"
tools:text="Pick me!"
android:textAppearance="?android:attr/textAppearanceMedium"
android:textColor="?attr/textColorAlertDialogListItem" />

View file

@ -294,6 +294,14 @@
<item>604800</item>
</integer-array>
<integer-array name="conversation_length_limit">
<item>0</item>
<item>5000</item>
<item>1000</item>
<item>500</item>
<item>100</item>
</integer-array>
<array name="scribble_colors">
<item>#ffffff</item>
<item>#ff0000</item>

View file

@ -2059,13 +2059,11 @@
<string name="preferences__mmsc_password">MMSC Password</string>
<string name="preferences__sms_delivery_reports">SMS delivery reports</string>
<string name="preferences__request_a_delivery_report_for_each_sms_message_you_send">Request a delivery report for each SMS message you send</string>
<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>
<string name="preferences__keep_messages">Keep messages</string>
<string name="preferences__clear_message_history">Clear message history</string>
<string name="preferences__linked_devices">Linked devices</string>
<string name="preferences__light_theme">Light</string>
<string name="preferences__dark_theme">Dark</string>
@ -2095,13 +2093,29 @@
<string name="preferences_chats__when_using_wifi">When using Wi-Fi</string>
<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_chats__message_history">Message history</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_storage__delete_older_messages">Delete older messages?</string>
<string name="preferences_storage__clear_message_history">Clear message history?</string>
<string name="preferences_storage__this_will_permanently_delete_all_message_history_and_media">This will permanently delete all messsage history and media from your device that are older than %1$s.</string>
<string name="preferences_storage__this_will_permanently_trim_all_conversations_to_the_d_most_recent_messages">This will permanently trim all conversations to the %1$s most recent messages.</string>
<string name="preferences_storage__this_will_delete_all_message_history_and_media_from_your_device">This will permanently delete all messsage history and media from your device.</string>
<string name="preferences_storage__are_you_sure_you_want_to_delete_all_message_history">Are you sure you want to delete all message history?</string>
<string name="preferences_storage__all_message_history_will_be_permanently_removed_this_action_cannot_be_undone">All message history will be permanently removed. This action cannot be undone.</string>
<string name="preferences_storage__delete_all_now">Delete all now</string>
<string name="preferences_storage__forever">Forever</string>
<string name="preferences_storage__one_year">1 year</string>
<string name="preferences_storage__six_months">6 months</string>
<string name="preferences_storage__thirty_days">30 days</string>
<string name="preferences_storage__none">None</string>
<string name="preferences_storage__s_messages">%1$s messages</string>
<string name="preferences_storage__custom">Custom</string>
<string name="preferences_Storage__custom_conversation_length_limit">Custom conversation length limit</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>
@ -2132,6 +2146,8 @@
<string name="preferences_notifications__receive_notifications_when_youre_mentioned_in_muted_chats">Receive notifications when youre mentioned in muted chats</string>
<string name="preferences_setup_a_username">Setup a username</string>
<string name="configurable_single_select__customize_option">Customize option</string>
<!-- Internal only preferences -->
<string name="preferences__internal_preferences" translatable="false">Internal Preferences</string>
<string name="preferences__internal_preferences_groups_v2" translatable="false">Groups V2</string>

View file

@ -1,5 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android">
<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<PreferenceCategory android:title="@string/preferences_storage__storage_usage" />
@ -9,26 +10,22 @@
<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" />
android:title="@string/preferences_chats__message_history">
<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" />
android:key="settings.keep_messages_duration"
android:title="@string/preferences__keep_messages"
tools:summary="@string/preferences_storage__forever" />
<Preference
android:inputType="number"
android:key="pref_trim_length"
android:title="@string/preferences__conversation_length_limit"
tools:summary="None" />
<Preference
android:key="pref_storage_clear_message_history"
android:title="@string/preferences__clear_message_history" />
</PreferenceCategory>