diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 557b420..4dac443 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -146,6 +146,14 @@
+
+
+
+
0) {
+ values.put(ConversationColumns.ARCHIVE_STATUS, 0);
+ }
+ updateConversationRowIfExists(dbWrapper, conversationId, values);
+ }
+
static void addSnippetTextAndPreviewToContentValues(final MessageData message,
final boolean showDraft, final ContentValues values) {
values.put(ConversationColumns.SHOW_DRAFT, showDraft ? 1 : 0);
diff --git a/app/src/main/java/com/android/messaging/datamodel/BugleNotifications.java b/app/src/main/java/com/android/messaging/datamodel/BugleNotifications.java
index 4ef4305..4632562 100644
--- a/app/src/main/java/com/android/messaging/datamodel/BugleNotifications.java
+++ b/app/src/main/java/com/android/messaging/datamodel/BugleNotifications.java
@@ -539,7 +539,10 @@ public class BugleNotifications {
private static void createMessageNotification(final boolean silent,
final String conversationId) {
final NotificationState state = MessageNotificationState.getNotificationState();
- final boolean softSound = DataModel.get().isNewMessageObservable(conversationId);
+ final boolean muted = MutedConversationStore.isMuted(
+ Factory.get().getApplicationContext(), conversationId);
+ final boolean softSound = !muted
+ && DataModel.get().isNewMessageObservable(conversationId);
if (state == null) {
cancel(PendingIntentConstants.SMS_NOTIFICATION_ID);
if (softSound && !TextUtils.isEmpty(conversationId)) {
diff --git a/app/src/main/java/com/android/messaging/datamodel/DataModel.java b/app/src/main/java/com/android/messaging/datamodel/DataModel.java
index 16cb145..564feb7 100644
--- a/app/src/main/java/com/android/messaging/datamodel/DataModel.java
+++ b/app/src/main/java/com/android/messaging/datamodel/DataModel.java
@@ -68,8 +68,14 @@ public abstract class DataModel {
get().getActionService().scheduleAction(action, code, delayMs);
}
+ public ConversationListData createConversationListData(final Context context,
+ final ConversationListDataListener listener, final boolean archivedMode) {
+ return createConversationListData(context, listener, archivedMode, false);
+ }
+
public abstract ConversationListData createConversationListData(final Context context,
- final ConversationListDataListener listener, final boolean archivedMode);
+ final ConversationListDataListener listener, final boolean archivedMode,
+ final boolean trashMode);
public abstract ConversationData createConversationData(final Context context,
final ConversationDataListener listener, final String conversationId);
diff --git a/app/src/main/java/com/android/messaging/datamodel/DataModelImpl.java b/app/src/main/java/com/android/messaging/datamodel/DataModelImpl.java
index 564aeb1..66c87b5 100644
--- a/app/src/main/java/com/android/messaging/datamodel/DataModelImpl.java
+++ b/app/src/main/java/com/android/messaging/datamodel/DataModelImpl.java
@@ -26,6 +26,7 @@ import com.android.messaging.datamodel.action.ActionService;
import com.android.messaging.datamodel.action.BackgroundWorker;
import com.android.messaging.datamodel.action.FixupMessageStatusOnStartupAction;
import com.android.messaging.datamodel.action.ProcessPendingMessagesAction;
+import com.android.messaging.datamodel.action.PurgeExpiredTrashAction;
import com.android.messaging.datamodel.data.BlockedParticipantsData;
import com.android.messaging.datamodel.data.BlockedParticipantsData.BlockedParticipantsDataListener;
import com.android.messaging.datamodel.data.ConversationSearchData;
@@ -81,8 +82,9 @@ public class DataModelImpl extends DataModel {
@Override
public ConversationListData createConversationListData(final Context context,
- final ConversationListDataListener listener, final boolean archivedMode) {
- return new ConversationListData(context, listener, archivedMode);
+ final ConversationListDataListener listener, final boolean archivedMode,
+ final boolean trashMode) {
+ return new ConversationListData(context, listener, archivedMode, trashMode);
}
@Override
@@ -211,6 +213,7 @@ public class DataModelImpl extends DataModel {
createConnectivityUtilForEachActiveSubscription();
FixupMessageStatusOnStartupAction.fixupMessageStatus();
+ PurgeExpiredTrashAction.purgeExpired();
ProcessPendingMessagesAction.processFirstPendingMessage();
SyncManager.immediateSync();
diff --git a/app/src/main/java/com/android/messaging/datamodel/DatabaseHelper.java b/app/src/main/java/com/android/messaging/datamodel/DatabaseHelper.java
index 3eb6a26..ed3803d 100644
--- a/app/src/main/java/com/android/messaging/datamodel/DatabaseHelper.java
+++ b/app/src/main/java/com/android/messaging/datamodel/DatabaseHelper.java
@@ -101,6 +101,9 @@ public class DatabaseHelper extends SQLiteOpenHelper {
/* If this conversation is archived */
public static final String ARCHIVE_STATUS = "archive_status";
+ /* When the conversation was moved to Trash; 0 if not in Trash */
+ public static final String TRASH_TIMESTAMP = "trash_timestamp";
+
/* Timestamp for sorting purposes */
public static final String SORT_TIMESTAMP = "sort_timestamp";
@@ -159,6 +162,7 @@ public class DatabaseHelper extends SQLiteOpenHelper {
+ ConversationColumns.DRAFT_PREVIEW_URI + " TEXT, "
+ ConversationColumns.DRAFT_PREVIEW_CONTENT_TYPE + " TEXT, "
+ ConversationColumns.ARCHIVE_STATUS + " INT DEFAULT(0), "
+ + ConversationColumns.TRASH_TIMESTAMP + " INT DEFAULT(0), "
+ ConversationColumns.SORT_TIMESTAMP + " INT DEFAULT(0), "
+ ConversationColumns.LAST_READ_TIMESTAMP + " INT DEFAULT(0), "
+ ConversationColumns.ICON + " TEXT, "
@@ -183,6 +187,11 @@ public class DatabaseHelper extends SQLiteOpenHelper {
+ " ON " + CONVERSATIONS_TABLE
+ "(" + ConversationColumns.ARCHIVE_STATUS + ")";
+ private static final String CONVERSATIONS_TABLE_TRASH_TIMESTAMP_INDEX_SQL =
+ "CREATE INDEX index_" + CONVERSATIONS_TABLE + "_" + ConversationColumns.TRASH_TIMESTAMP
+ + " ON " + CONVERSATIONS_TABLE
+ + "(" + ConversationColumns.TRASH_TIMESTAMP + ")";
+
private static final String CONVERSATIONS_TABLE_SORT_TIMESTAMP_INDEX_SQL =
"CREATE INDEX index_" + CONVERSATIONS_TABLE + "_" + ConversationColumns.SORT_TIMESTAMP
+ " ON " + CONVERSATIONS_TABLE
@@ -525,6 +534,7 @@ public class DatabaseHelper extends SQLiteOpenHelper {
private static final String[] CREATE_INDEX_SQLS = new String[] {
CONVERSATIONS_TABLE_SMS_THREAD_ID_INDEX_SQL,
CONVERSATIONS_TABLE_ARCHIVE_STATUS_INDEX_SQL,
+ CONVERSATIONS_TABLE_TRASH_TIMESTAMP_INDEX_SQL,
CONVERSATIONS_TABLE_SORT_TIMESTAMP_INDEX_SQL,
MESSAGES_TABLE_SORT_INDEX_SQL,
MESSAGES_TABLE_STATUS_SEEN_INDEX_SQL,
diff --git a/app/src/main/java/com/android/messaging/datamodel/DatabaseUpgradeHelper.java b/app/src/main/java/com/android/messaging/datamodel/DatabaseUpgradeHelper.java
index 96aba86..9400a7f 100644
--- a/app/src/main/java/com/android/messaging/datamodel/DatabaseUpgradeHelper.java
+++ b/app/src/main/java/com/android/messaging/datamodel/DatabaseUpgradeHelper.java
@@ -48,6 +48,9 @@ public class DatabaseUpgradeHelper {
if (currentVersion < 2) {
currentVersion = upgradeToVersion2(db);
}
+ if (currentVersion < 3) {
+ currentVersion = upgradeToVersion3(db);
+ }
// Rebuild all the views
final Context context = Factory.get().getApplicationContext();
DatabaseHelper.dropAllViews(db);
@@ -63,6 +66,17 @@ public class DatabaseUpgradeHelper {
return 2;
}
+ private int upgradeToVersion3(final SQLiteDatabase db) {
+ db.execSQL("ALTER TABLE " + DatabaseHelper.CONVERSATIONS_TABLE + " ADD COLUMN " +
+ DatabaseHelper.ConversationColumns.TRASH_TIMESTAMP + " INT DEFAULT(0)");
+ db.execSQL("CREATE INDEX IF NOT EXISTS index_" + DatabaseHelper.CONVERSATIONS_TABLE
+ + "_" + DatabaseHelper.ConversationColumns.TRASH_TIMESTAMP
+ + " ON " + DatabaseHelper.CONVERSATIONS_TABLE
+ + "(" + DatabaseHelper.ConversationColumns.TRASH_TIMESTAMP + ")");
+ LogUtil.i(TAG, "Upgraded database to version 3");
+ return 3;
+ }
+
/**
* Checks db version correctness at the end of each milestone release. If target database
* version lies beyond the version range that the current release may handle, we snap the
diff --git a/app/src/main/java/com/android/messaging/datamodel/MessageNotificationState.java b/app/src/main/java/com/android/messaging/datamodel/MessageNotificationState.java
index 23a8baa..5079031 100644
--- a/app/src/main/java/com/android/messaging/datamodel/MessageNotificationState.java
+++ b/app/src/main/java/com/android/messaging/datamodel/MessageNotificationState.java
@@ -51,6 +51,7 @@ import com.android.messaging.datamodel.data.MessagePartData;
import com.android.messaging.datamodel.data.ParticipantData;
import com.android.messaging.sms.MmsUtils;
import com.android.messaging.ui.UIIntents;
+import com.android.messaging.ui.home.MutedConversationStore;
import com.android.messaging.util.Assert;
import com.android.messaging.util.AvatarUriUtil;
import com.android.messaging.util.BugleGservicesKeys;
@@ -802,6 +803,9 @@ public abstract class MessageNotificationState extends NotificationState {
final String messageText = convMessageData.getText();
final String convId = convMessageData.getConversationId();
+ if (MutedConversationStore.isMuted(context, convId)) {
+ continue;
+ }
final String messageId = convMessageData.getMessageId();
CharSequence text = messageText;
@@ -818,6 +822,9 @@ public abstract class MessageNotificationState extends NotificationState {
if (currConvInfo == null) {
final ConversationListItemData convData =
ConversationListItemData.getExistingConversation(db, convId);
+ if (convData == null || convData.getIsTrashed()) {
+ continue;
+ }
final int subId = BugleDatabaseOperations.getSelfSubscriptionId(db,
convData.getSelfId());
groupConversationName = convData.getName();
diff --git a/app/src/main/java/com/android/messaging/datamodel/action/InsertNewMessageAction.java b/app/src/main/java/com/android/messaging/datamodel/action/InsertNewMessageAction.java
index 754b58c..ddc5801 100644
--- a/app/src/main/java/com/android/messaging/datamodel/action/InsertNewMessageAction.java
+++ b/app/src/main/java/com/android/messaging/datamodel/action/InsertNewMessageAction.java
@@ -132,6 +132,13 @@ public class InsertNewMessageAction extends Action implements Parcelable {
final DatabaseWrapper db = DataModel.get().getDatabase();
final String conversationId = message.getConversationId();
+ final ConversationListItemData conversation =
+ ConversationListItemData.getExistingConversation(db, conversationId);
+ if (conversation != null && conversation.getIsTrashed()) {
+ LogUtil.w(TAG, "InsertNewMessageAction: conversation is in trash");
+ return null;
+ }
+
final ParticipantData self = getSelf(db, conversationId, message);
if (self == null) {
return null;
@@ -190,6 +197,7 @@ public class InsertNewMessageAction extends Action implements Parcelable {
messageToSend, BugleDatabaseOperations.UPDATE_MODE_CLEAR_DRAFT);
}
MessagingContentProvider.notifyConversationListChanged();
+ MessagingContentProvider.notifyConversationMetadataChanged(conversationId);
ProcessPendingMessagesAction.scheduleProcessPendingMessagesAction(false, this);
return message;
diff --git a/app/src/main/java/com/android/messaging/datamodel/action/PurgeExpiredTrashAction.java b/app/src/main/java/com/android/messaging/datamodel/action/PurgeExpiredTrashAction.java
new file mode 100644
index 0000000..97b3405
--- /dev/null
+++ b/app/src/main/java/com/android/messaging/datamodel/action/PurgeExpiredTrashAction.java
@@ -0,0 +1,77 @@
+package com.android.messaging.datamodel.action;
+
+import android.database.Cursor;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import androidx.annotation.NonNull;
+
+import com.android.messaging.datamodel.DataModel;
+import com.android.messaging.datamodel.DatabaseHelper;
+import com.android.messaging.datamodel.DatabaseHelper.ConversationColumns;
+import com.android.messaging.datamodel.DatabaseWrapper;
+import com.android.messaging.util.LogUtil;
+
+import java.util.ArrayList;
+
+/** Permanently deletes conversations that have been in Trash longer than 30 days. */
+public class PurgeExpiredTrashAction extends Action {
+
+ private static final String TAG = LogUtil.BUGLE_DATAMODEL_TAG;
+
+ public static void purgeExpired() {
+ new PurgeExpiredTrashAction().start();
+ }
+
+ private PurgeExpiredTrashAction() {
+ }
+
+ @Override
+ protected Object executeAction() {
+ final long cutoff = System.currentTimeMillis()
+ - UpdateConversationTrashStatusAction.RETENTION_MS;
+ final DatabaseWrapper db = DataModel.get().getDatabase();
+ final ArrayList ids = new ArrayList<>();
+ final ArrayList timestamps = new ArrayList<>();
+ try (Cursor cursor = db.query(DatabaseHelper.CONVERSATIONS_TABLE,
+ new String[] { ConversationColumns._ID, ConversationColumns.SORT_TIMESTAMP },
+ ConversationColumns.TRASH_TIMESTAMP + " > 0 AND "
+ + ConversationColumns.TRASH_TIMESTAMP + " < ?",
+ new String[] { Long.toString(cutoff) },
+ null, null, null)) {
+ while (cursor != null && cursor.moveToNext()) {
+ ids.add(cursor.getString(0));
+ timestamps.add(cursor.getLong(1));
+ }
+ }
+ for (int i = 0; i < ids.size(); i++) {
+ DeleteConversationAction.deleteConversation(ids.get(i), timestamps.get(i));
+ }
+ if (!ids.isEmpty()) {
+ LogUtil.i(TAG, "Purged " + ids.size() + " expired trash conversations");
+ }
+ return null;
+ }
+
+ private PurgeExpiredTrashAction(final Parcel in) {
+ super(in);
+ }
+
+ public static final Parcelable.Creator CREATOR
+ = new Parcelable.Creator<>() {
+ @Override
+ public PurgeExpiredTrashAction createFromParcel(final Parcel in) {
+ return new PurgeExpiredTrashAction(in);
+ }
+
+ @Override
+ public PurgeExpiredTrashAction[] newArray(final int size) {
+ return new PurgeExpiredTrashAction[size];
+ }
+ };
+
+ @Override
+ public void writeToParcel(@NonNull final Parcel parcel, final int flags) {
+ writeActionToParcel(parcel, flags);
+ }
+}
diff --git a/app/src/main/java/com/android/messaging/datamodel/action/ResendMessageAction.java b/app/src/main/java/com/android/messaging/datamodel/action/ResendMessageAction.java
index 147fdd7..95ae4f6 100644
--- a/app/src/main/java/com/android/messaging/datamodel/action/ResendMessageAction.java
+++ b/app/src/main/java/com/android/messaging/datamodel/action/ResendMessageAction.java
@@ -28,6 +28,7 @@ import com.android.messaging.datamodel.DataModel;
import com.android.messaging.datamodel.DatabaseHelper.MessageColumns;
import com.android.messaging.datamodel.DatabaseWrapper;
import com.android.messaging.datamodel.MessagingContentProvider;
+import com.android.messaging.datamodel.data.ConversationListItemData;
import com.android.messaging.datamodel.data.MessageData;
import com.android.messaging.util.LogUtil;
@@ -70,6 +71,12 @@ public class ResendMessageAction extends Action implements Parcelable {
final MessageData message = BugleDatabaseOperations.readMessage(db, messageId);
// Check message can be resent
if (message != null && message.canResendMessage()) {
+ final ConversationListItemData conversation =
+ ConversationListItemData.getExistingConversation(db, message.getConversationId());
+ if (conversation != null && conversation.getIsTrashed()) {
+ LogUtil.w(TAG, "ResendMessageAction: conversation is in trash");
+ return null;
+ }
final boolean isMms = message.getIsMms();
long timestamp = System.currentTimeMillis();
if (isMms) {
diff --git a/app/src/main/java/com/android/messaging/datamodel/action/UpdateConversationTrashStatusAction.java b/app/src/main/java/com/android/messaging/datamodel/action/UpdateConversationTrashStatusAction.java
new file mode 100644
index 0000000..de633d1
--- /dev/null
+++ b/app/src/main/java/com/android/messaging/datamodel/action/UpdateConversationTrashStatusAction.java
@@ -0,0 +1,86 @@
+package com.android.messaging.datamodel.action;
+
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.text.TextUtils;
+
+import androidx.annotation.NonNull;
+
+import com.android.messaging.Factory;
+import com.android.messaging.datamodel.BugleDatabaseOperations;
+import com.android.messaging.datamodel.DataModel;
+import com.android.messaging.datamodel.DatabaseWrapper;
+import com.android.messaging.datamodel.MessagingContentProvider;
+import com.android.messaging.ui.home.PinnedConversationStore;
+import com.android.messaging.util.Assert;
+
+public class UpdateConversationTrashStatusAction extends Action {
+
+ public static final long RETENTION_MS = 30L * 24 * 60 * 60 * 1000;
+
+ public static void moveToTrash(final String conversationId) {
+ final UpdateConversationTrashStatusAction action =
+ new UpdateConversationTrashStatusAction(conversationId, true);
+ action.start();
+ }
+
+ public static void restoreFromTrash(final String conversationId) {
+ final UpdateConversationTrashStatusAction action =
+ new UpdateConversationTrashStatusAction(conversationId, false);
+ action.start();
+ }
+
+ private static final String KEY_CONVERSATION_ID = "conversation_id";
+ private static final String KEY_TO_TRASH = "to_trash";
+
+ private UpdateConversationTrashStatusAction(final String conversationId,
+ final boolean toTrash) {
+ Assert.isTrue(!TextUtils.isEmpty(conversationId));
+ actionParameters.putString(KEY_CONVERSATION_ID, conversationId);
+ actionParameters.putBoolean(KEY_TO_TRASH, toTrash);
+ }
+
+ @Override
+ protected Object executeAction() {
+ final String conversationId = actionParameters.getString(KEY_CONVERSATION_ID);
+ final boolean toTrash = actionParameters.getBoolean(KEY_TO_TRASH);
+ final DatabaseWrapper db = DataModel.get().getDatabase();
+ db.beginTransaction();
+ try {
+ BugleDatabaseOperations.updateConversationTrashTimestampInTransaction(
+ db, conversationId, toTrash ? System.currentTimeMillis() : 0L);
+ db.setTransactionSuccessful();
+ } finally {
+ db.endTransaction();
+ }
+ if (toTrash) {
+ PinnedConversationStore.unpin(Factory.get().getApplicationContext(), conversationId);
+ UpdateMessageNotificationAction.updateMessageNotification();
+ }
+ MessagingContentProvider.notifyConversationListChanged();
+ MessagingContentProvider.notifyConversationMetadataChanged(conversationId);
+ return null;
+ }
+
+ private UpdateConversationTrashStatusAction(final Parcel in) {
+ super(in);
+ }
+
+ public static final Parcelable.Creator CREATOR
+ = new Parcelable.Creator<>() {
+ @Override
+ public UpdateConversationTrashStatusAction createFromParcel(final Parcel in) {
+ return new UpdateConversationTrashStatusAction(in);
+ }
+
+ @Override
+ public UpdateConversationTrashStatusAction[] newArray(final int size) {
+ return new UpdateConversationTrashStatusAction[size];
+ }
+ };
+
+ @Override
+ public void writeToParcel(@NonNull final Parcel parcel, final int flags) {
+ writeActionToParcel(parcel, flags);
+ }
+}
diff --git a/app/src/main/java/com/android/messaging/datamodel/data/ConversationData.java b/app/src/main/java/com/android/messaging/datamodel/data/ConversationData.java
index 95c60a1..19ad161 100644
--- a/app/src/main/java/com/android/messaging/datamodel/data/ConversationData.java
+++ b/app/src/main/java/com/android/messaging/datamodel/data/ConversationData.java
@@ -42,6 +42,7 @@ import com.android.messaging.datamodel.action.InsertNewMessageAction;
import com.android.messaging.datamodel.action.RedownloadMmsAction;
import com.android.messaging.datamodel.action.ResendMessageAction;
import com.android.messaging.datamodel.action.UpdateConversationArchiveStatusAction;
+import com.android.messaging.datamodel.action.UpdateConversationTrashStatusAction;
import com.android.messaging.datamodel.binding.BindableData;
import com.android.messaging.datamodel.binding.Binding;
import com.android.messaging.datamodel.binding.BindingBase;
@@ -460,7 +461,11 @@ public class ConversationData extends BindableData {
}
public boolean getIsArchived() {
- return mConversationMetadata.getIsArchived();
+ return mConversationMetadata != null && mConversationMetadata.getIsArchived();
+ }
+
+ public boolean getIsTrashed() {
+ return mConversationMetadata != null && mConversationMetadata.getIsTrashed();
}
public String getIcon() {
@@ -569,6 +574,12 @@ public class ConversationData extends BindableData {
final MessageData message) {
Assert.isTrue(TextUtils.equals(mConversationId, message.getConversationId()));
Assert.isTrue(binding.getData() == this);
+ if (getIsTrashed()) {
+ return;
+ }
+ if (getIsArchived()) {
+ unarchiveConversation(binding);
+ }
if (message.getSelfId() == null) {
InsertNewMessageAction.insertNewMessage(message);
@@ -612,6 +623,12 @@ public class ConversationData extends BindableData {
public void resendMessage(final BindingBase binding, final String messageId) {
Assert.isTrue(binding.getData() == this);
Assert.notNull(messageId);
+ if (getIsTrashed()) {
+ return;
+ }
+ if (getIsArchived()) {
+ unarchiveConversation(binding);
+ }
ResendMessageAction.resendMessage(messageId);
}
@@ -642,6 +659,16 @@ public class ConversationData extends BindableData {
UpdateConversationArchiveStatusAction.unarchiveConversation(mConversationId);
}
+ public void moveConversationToTrash(final BindingBase binding) {
+ Assert.isTrue(binding.getData() == this);
+ UpdateConversationTrashStatusAction.moveToTrash(mConversationId);
+ }
+
+ public void restoreConversationFromTrash(final BindingBase binding) {
+ Assert.isTrue(binding.getData() == this);
+ UpdateConversationTrashStatusAction.restoreFromTrash(mConversationId);
+ }
+
public ConversationParticipantsData getParticipants() {
return mParticipantData;
}
diff --git a/app/src/main/java/com/android/messaging/datamodel/data/ConversationListData.java b/app/src/main/java/com/android/messaging/datamodel/data/ConversationListData.java
index c33ebf8..d499dd3 100644
--- a/app/src/main/java/com/android/messaging/datamodel/data/ConversationListData.java
+++ b/app/src/main/java/com/android/messaging/datamodel/data/ConversationListData.java
@@ -52,6 +52,12 @@ public class ConversationListData extends BindableData
"(" + ConversationListViewColumns.ARCHIVE_STATUS + " = 1)";
public static final String WHERE_NOT_ARCHIVED =
"(" + ConversationListViewColumns.ARCHIVE_STATUS + " = 0)";
+ public static final String WHERE_NOT_TRASHED =
+ "(" + ConversationListViewColumns.TRASH_TIMESTAMP + " = 0)";
+ public static final String WHERE_TRASHED =
+ "(" + ConversationListViewColumns.TRASH_TIMESTAMP + " > 0)";
+ public static final String WHERE_INBOX = WHERE_NOT_ARCHIVED + " AND " + WHERE_NOT_TRASHED;
+ public static final String WHERE_ARCHIVED_VISIBLE = WHERE_ARCHIVED + " AND " + WHERE_NOT_TRASHED;
public interface ConversationListDataListener {
void onConversationListCursorUpdated(ConversationListData data, Cursor cursor);
@@ -61,13 +67,20 @@ public class ConversationListData extends BindableData
private ConversationListDataListener mListener;
private final Context mContext;
private final boolean mArchivedMode;
+ private final boolean mTrashMode;
private LoaderManager mLoaderManager;
public ConversationListData(final Context context, final ConversationListDataListener listener,
final boolean archivedMode) {
+ this(context, listener, archivedMode, false);
+ }
+
+ public ConversationListData(final Context context, final ConversationListDataListener listener,
+ final boolean archivedMode, final boolean trashMode) {
mListener = listener;
mContext = context;
- mArchivedMode = archivedMode;
+ mTrashMode = trashMode;
+ mArchivedMode = archivedMode && !trashMode;
}
private static final int CONVERSATION_LIST_LOADER = 1;
@@ -101,7 +114,7 @@ public class ConversationListData extends BindableData
loader = new BoundCursorLoader(bindingId, mContext,
MessagingContentProvider.CONVERSATIONS_URI,
ConversationListItemData.PROJECTION,
- mArchivedMode ? WHERE_ARCHIVED : WHERE_NOT_ARCHIVED,
+ listSelection(),
null, // selection args
SORT_ORDER);
break;
@@ -212,4 +225,18 @@ public class ConversationListData extends BindableData
public HashSet getBlockedParticipants() {
return mBlockedParticipants;
}
+
+ public boolean isTrashMode() {
+ return mTrashMode;
+ }
+
+ private String listSelection() {
+ if (mTrashMode) {
+ return WHERE_TRASHED;
+ }
+ if (mArchivedMode) {
+ return WHERE_ARCHIVED_VISIBLE;
+ }
+ return WHERE_INBOX;
+ }
}
diff --git a/app/src/main/java/com/android/messaging/datamodel/data/ConversationListItemData.java b/app/src/main/java/com/android/messaging/datamodel/data/ConversationListItemData.java
index 73cfbd2..c7ae40b 100644
--- a/app/src/main/java/com/android/messaging/datamodel/data/ConversationListItemData.java
+++ b/app/src/main/java/com/android/messaging/datamodel/data/ConversationListItemData.java
@@ -28,6 +28,7 @@ import com.android.messaging.datamodel.DatabaseHelper.MessageColumns;
import com.android.messaging.datamodel.DatabaseHelper.ParticipantColumns;
import com.android.messaging.datamodel.DatabaseWrapper;
import com.android.messaging.datamodel.action.DeleteConversationAction;
+import com.android.messaging.datamodel.action.UpdateConversationTrashStatusAction;
import com.android.messaging.util.Assert;
import com.android.messaging.util.ContactUtil;
import com.android.messaging.util.Dates;
@@ -61,6 +62,7 @@ public class ConversationListItemData {
private String mDraftPreviewContentType;
private String mDraftSnippetText;
private boolean mIsArchived;
+ private long mTrashTimestamp;
private String mSubject;
private String mDraftSubject;
private String mSnippetSenderFirstName;
@@ -110,6 +112,8 @@ public class ConversationListItemData {
}
mIsArchived = cursor.getInt(INDEX_ARCHIVE_STATUS) == 1;
+ final int trashIndex = cursor.getColumnIndex(ConversationListViewColumns.TRASH_TIMESTAMP);
+ mTrashTimestamp = trashIndex >= 0 ? cursor.getLong(trashIndex) : 0;
mSubject = cursor.getString(INDEX_SUBJECT_TEXT);
mSnippetSenderFirstName = cursor.getString(INDEX_SNIPPET_SENDER_FIRST_NAME);
mSnippetSenderDisplayDestination =
@@ -240,6 +244,14 @@ public class ConversationListItemData {
return mIsArchived;
}
+ public boolean getIsTrashed() {
+ return mTrashTimestamp > 0;
+ }
+
+ public long getTrashTimestamp() {
+ return mTrashTimestamp;
+ }
+
public String getSubject() {
return mSubject;
}
@@ -256,7 +268,11 @@ public class ConversationListItemData {
}
public void deleteConversation() {
- DeleteConversationAction.deleteConversation(mConversationId, mTimestamp);
+ if (getIsTrashed()) {
+ DeleteConversationAction.deleteConversation(mConversationId, mTimestamp);
+ } else {
+ UpdateConversationTrashStatusAction.moveToTrash(mConversationId);
+ }
}
/**
@@ -281,6 +297,8 @@ public class ConversationListItemData {
+ " as " + ConversationListViewColumns.CURRENT_SELF_ID + ", "
+ DatabaseHelper.CONVERSATIONS_TABLE + '.' + ConversationColumns.ARCHIVE_STATUS
+ " as " + ConversationListViewColumns.ARCHIVE_STATUS + ", "
+ + DatabaseHelper.CONVERSATIONS_TABLE + '.' + ConversationColumns.TRASH_TIMESTAMP
+ + " as " + ConversationListViewColumns.TRASH_TIMESTAMP + ", "
+ DatabaseHelper.MESSAGES_TABLE + '.' + MessageColumns.READ
+ " as " + ConversationListViewColumns.READ + ", "
+ DatabaseHelper.CONVERSATIONS_TABLE + '.' + ConversationColumns.ICON
@@ -354,6 +372,7 @@ public class ConversationListItemData {
public static final String _ID = ConversationColumns._ID;
static final String NAME = ConversationColumns.NAME;
static final String ARCHIVE_STATUS = ConversationColumns.ARCHIVE_STATUS;
+ static final String TRASH_TIMESTAMP = ConversationColumns.TRASH_TIMESTAMP;
static final String READ = MessageColumns.READ;
static final String SORT_TIMESTAMP = ConversationColumns.SORT_TIMESTAMP;
static final String PREVIEW_URI = ConversationColumns.PREVIEW_URI;
@@ -412,6 +431,7 @@ public class ConversationListItemData {
ConversationListViewColumns.SNIPPET_SENDER_FIRST_NAME,
ConversationListViewColumns.SNIPPET_SENDER_DISPLAY_DESTINATION,
ConversationListViewColumns.IS_ENTERPRISE,
+ ConversationListViewColumns.TRASH_TIMESTAMP,
};
private static final int INDEX_ID = 0;
@@ -441,6 +461,7 @@ public class ConversationListItemData {
private static final int INDEX_SNIPPET_SENDER_FIRST_NAME = 24;
private static final int INDEX_SNIPPET_SENDER_DISPLAY_DESTINATION = 25;
private static final int INDEX_IS_ENTERPRISE = 26;
+ private static final int INDEX_TRASH_TIMESTAMP = 27;
private static final String DIVIDER_TEXT = ", ";
diff --git a/app/src/main/java/com/android/messaging/datamodel/data/ParticipantListItemData.java b/app/src/main/java/com/android/messaging/datamodel/data/ParticipantListItemData.java
index f6c9b5f..9a94607 100644
--- a/app/src/main/java/com/android/messaging/datamodel/data/ParticipantListItemData.java
+++ b/app/src/main/java/com/android/messaging/datamodel/data/ParticipantListItemData.java
@@ -87,6 +87,11 @@ public class ParticipantListItemData extends PersonItemData {
return mNormalizedDestination;
}
+ public void unblockQuietly(final Context context) {
+ UpdateDestinationBlockedAction.updateDestinationBlocked(
+ mNormalizedDestination, false, null, (action, success, block, destination) -> { });
+ }
+
public void unblock(final Context context) {
UpdateDestinationBlockedAction.updateDestinationBlocked(
mNormalizedDestination, false, null,
diff --git a/app/src/main/java/com/android/messaging/ui/BugleActionBarActivity.java b/app/src/main/java/com/android/messaging/ui/BugleActionBarActivity.java
index 1afe232..75fd06f 100644
--- a/app/src/main/java/com/android/messaging/ui/BugleActionBarActivity.java
+++ b/app/src/main/java/com/android/messaging/ui/BugleActionBarActivity.java
@@ -346,6 +346,9 @@ public class BugleActionBarActivity extends AppCompatActivity implements ImeUtil
}
public void updateActionBar(final ActionBar actionBar) {
+ if (actionBar == null) {
+ return;
+ }
if (!BugleActionBarActivity.this.shouldShowContextualActionBar(mCallback)) {
actionBar.hide();
return;
diff --git a/app/src/main/java/com/android/messaging/ui/UIIntents.java b/app/src/main/java/com/android/messaging/ui/UIIntents.java
index c270efc..4c079d4 100644
--- a/app/src/main/java/com/android/messaging/ui/UIIntents.java
+++ b/app/src/main/java/com/android/messaging/ui/UIIntents.java
@@ -30,6 +30,7 @@ import androidx.activity.result.ActivityResultLauncher;
import androidx.fragment.app.Fragment;
import com.android.messaging.Factory;
+import com.android.messaging.datamodel.data.ConversationMessageData;
import com.android.messaging.datamodel.data.MessageData;
import com.android.messaging.util.ConversationIdSet;
@@ -183,6 +184,12 @@ public abstract class UIIntents {
public abstract void launchHomeConversationSearchActivity(final Context context,
final String conversationId);
+ /**
+ * Launch home UI for a single message's details.
+ */
+ public abstract void launchHomeMessageDetailsActivity(final Context context,
+ final ConversationMessageData data);
+
/**
* Launch an external activity to handle a phone call
* @param phoneNumber the phone number to call
@@ -196,6 +203,11 @@ public abstract class UIIntents {
*/
public abstract void launchArchivedConversationsActivity(final Context context);
+ /**
+ * Launch an activity to show conversations in Trash.
+ */
+ public abstract void launchRecentlyDeletedConversationsActivity(final Context context);
+
/**
* Launch an activity to show blocked participants.
*/
diff --git a/app/src/main/java/com/android/messaging/ui/UIIntentsImpl.java b/app/src/main/java/com/android/messaging/ui/UIIntentsImpl.java
index a72ad3a..79b8d3c 100644
--- a/app/src/main/java/com/android/messaging/ui/UIIntentsImpl.java
+++ b/app/src/main/java/com/android/messaging/ui/UIIntentsImpl.java
@@ -45,6 +45,7 @@ import com.android.messaging.R;
import com.android.messaging.datamodel.ConversationImagePartsView;
import com.android.messaging.datamodel.MediaScratchFileProvider;
import com.android.messaging.datamodel.MessagingContentProvider;
+import com.android.messaging.datamodel.data.ConversationMessageData;
import com.android.messaging.datamodel.data.MessageData;
import com.android.messaging.datamodel.data.MessagePartData;
import com.android.messaging.datamodel.data.ParticipantData;
@@ -56,8 +57,10 @@ import com.android.messaging.ui.appsettings.SettingsActivity;
import com.android.messaging.ui.conversation.ConversationActivity;
import com.android.messaging.ui.conversation.LaunchConversationActivity;
import com.android.messaging.ui.conversationlist.ArchivedConversationListActivity;
+import com.android.messaging.ui.conversationlist.RecentlyDeletedConversationListActivity;
import com.android.messaging.ui.conversationlist.ConversationListActivity;
import com.android.messaging.ui.conversationlist.ForwardMessageActivity;
+import com.android.messaging.ui.home.HomeMessageDetailsActivity;
import com.android.messaging.ui.photoviewer.BuglePhotoViewActivity;
import com.android.messaging.util.Assert;
import com.android.messaging.util.ConversationIdSet;
@@ -208,6 +211,12 @@ public class UIIntentsImpl extends UIIntents {
context.startActivity(intent);
}
+ @Override
+ public void launchRecentlyDeletedConversationsActivity(final Context context) {
+ final Intent intent = new Intent(context, RecentlyDeletedConversationListActivity.class);
+ context.startActivity(intent);
+ }
+
@Override
public void launchBlockedParticipantsActivity(final Context context) {
final Intent intent = new Intent(context, BlockedParticipantsActivity.class);
@@ -280,6 +289,16 @@ public class UIIntentsImpl extends UIIntents {
context.startActivity(intent);
}
+ @Override
+ public void launchHomeMessageDetailsActivity(final Context context,
+ final ConversationMessageData data) {
+ HomeMessageDetailsActivity.start(context, data);
+ if (context instanceof Activity) {
+ ((Activity) context).overridePendingTransition(
+ R.anim.home_activity_open, R.anim.home_activity_hold);
+ }
+ }
+
@Override
public void launchPhoneCallActivity(final Context context, final String phoneNumber,
final Point clickPosition) {
diff --git a/app/src/main/java/com/android/messaging/ui/conversation/ConversationFragment.java b/app/src/main/java/com/android/messaging/ui/conversation/ConversationFragment.java
index 92632d8..f6009e8 100644
--- a/app/src/main/java/com/android/messaging/ui/conversation/ConversationFragment.java
+++ b/app/src/main/java/com/android/messaging/ui/conversation/ConversationFragment.java
@@ -115,6 +115,8 @@ import com.android.messaging.ui.home.HomeBottomSheet;
import com.android.messaging.ui.home.HomeComposeAttachPanel;
import com.android.messaging.ui.home.HomeEmptyState;
import com.android.messaging.ui.home.HomeGallerySheet;
+import com.android.messaging.ui.home.HomeModal;
+import com.android.messaging.ui.home.HomeToast;
import com.android.messaging.ui.home.HomeUiMotion;
import com.android.messaging.ui.home.StarredMessagesStore;
import com.android.messaging.ui.home.UnknownSenderPromptStore;
@@ -193,6 +195,7 @@ public class ConversationFragment extends Fragment implements ConversationDataLi
private TextView mMessageSelectionTitle;
private View mComposeContainer;
private View mComposeStage;
+ private View mTrashBar;
private View mMessageActionBar;
private View mMessageActionCopy;
private View mMessageActionForward;
@@ -354,7 +357,8 @@ public class ConversationFragment extends Fragment implements ConversationDataLi
result -> {
if (result.getResultCode() == Activity.RESULT_OK
&& result.getData() != null
- && mComposeMessageView != null) {
+ && mComposeMessageView != null
+ && !isConversationTrashed()) {
mComposeMessageView.applySharedLocation(result.getData());
}
});
@@ -371,7 +375,7 @@ public class ConversationFragment extends Fragment implements ConversationDataLi
public void onCreate(final Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
mGalleryPicker = new DocumentImagePicker(this, pendingItem -> {
- if (mComposeMessageView != null) {
+ if (mComposeMessageView != null && !isConversationTrashed()) {
mComposeMessageView.onPendingAttachmentAdded(pendingItem);
}
});
@@ -467,6 +471,7 @@ public class ConversationFragment extends Fragment implements ConversationDataLi
mEmptyContainer = view.findViewById(R.id.home_conversation_empty);
setupConversationHeader(view);
setupUnknownSenderBanner(view);
+ setupTrashComposeBar(view);
setupMessageSelectionChrome(view);
bindConversationEdgeFade(view);
final LinearLayoutManager manager = new LinearLayoutManager(getActivity());
@@ -673,7 +678,6 @@ public class ConversationFragment extends Fragment implements ConversationDataLi
final View copy = content.findViewById(R.id.home_message_menu_copy);
final View star = content.findViewById(R.id.home_message_menu_star);
final TextView starLabel = content.findViewById(R.id.home_message_menu_star_label);
- final View delete = content.findViewById(R.id.home_message_menu_delete);
final View selectMore = content.findViewById(R.id.home_message_menu_select_more);
final View details = content.findViewById(R.id.home_message_menu_details);
@@ -699,18 +703,16 @@ public class ConversationFragment extends Fragment implements ConversationDataLi
dismissMessageActionPopup();
dismissMessageSelection();
});
- delete.setOnClickListener(v -> {
- dismissMessageActionPopup();
- performSelectedMessageAction(R.id.home_message_action_delete);
- });
selectMore.setOnClickListener(v -> {
mClearSelectionOnPopupDismiss = false;
dismissMessageActionPopup();
selectMessage(messageView, mSelectedAttachment, false /* showPopup */);
});
details.setOnClickListener(v -> {
+ mClearSelectionOnPopupDismiss = false;
dismissMessageActionPopup();
- performSelectedMessageAction(R.id.home_message_action_details);
+ UIIntents.get().launchHomeMessageDetailsActivity(getActivity(), data);
+ dismissMessageSelection();
});
content.setLayoutParams(new ViewGroup.LayoutParams(
@@ -964,7 +966,7 @@ public class ConversationFragment extends Fragment implements ConversationDataLi
// On resume, check if there's a pending request for resuming message compose. This
// may happen when the user commits the contact selection for a group conversation and
// goes from compose back to the conversation fragment.
- if (mHost.shouldResumeComposeMessage()) {
+ if (!isConversationTrashed() && mHost.shouldResumeComposeMessage()) {
mComposeMessageView.resumeComposeMessage();
}
@@ -974,10 +976,8 @@ public class ConversationFragment extends Fragment implements ConversationDataLi
if (mConversationHeader != null) {
mConversationHeader.setVisibility(View.VISIBLE);
}
- if (mComposeContainer != null) {
- mComposeContainer.setVisibility(View.VISIBLE);
- }
}
+ updateTrashComposeLock();
if (mHost != null) {
mHost.invalidateActionBar();
}
@@ -1030,9 +1030,10 @@ public class ConversationFragment extends Fragment implements ConversationDataLi
menu.findItem(R.id.action_add_contact).setVisible(addContactActionVisible);
// See if we should show archive or unarchive.
+ final boolean isTrashed = data.getIsTrashed();
final boolean isArchived = data.getIsArchived();
- menu.findItem(R.id.action_archive).setVisible(!isArchived);
- menu.findItem(R.id.action_unarchive).setVisible(isArchived);
+ menu.findItem(R.id.action_archive).setVisible(!isArchived && !isTrashed);
+ menu.findItem(R.id.action_unarchive).setVisible(isArchived && !isTrashed);
// Call lives on the header pill; keep it out of the overflow.
menu.findItem(R.id.action_call).setVisible(false);
@@ -1069,18 +1070,7 @@ public class ConversationFragment extends Fragment implements ConversationDataLi
(new AddContactsConfirmationDialog(getActivity(), avatarUri, destination)).show();
return true;
} else if (itemId == R.id.action_delete) {
- if (isReadyForDeleteAction()) {
- new AlertDialog.Builder(getActivity(), R.style.AlertDialogTheme)
- .setTitle(getResources().getQuantityString(
- R.plurals.delete_conversations_confirmation_dialog_title, 1))
- .setPositiveButton(R.string.delete_conversation_confirmation_button,
- (dialog, button) -> deleteConversation())
- .setNegativeButton(R.string.delete_conversation_decline_button, null)
- .show();
- } else {
- warnOfMissingActionConditions(false /*sending*/,
- null /*commandToRunAfterActionConditionResolved*/);
- }
+ confirmDeleteConversation();
return true;
}
return super.onOptionsItemSelected(item);
@@ -1196,6 +1186,7 @@ public class ConversationFragment extends Fragment implements ConversationDataLi
mAdapter.notifyDataSetChanged();
updateHomeEmptyState();
updateUnknownSenderBanner();
+ updateTrashComposeLock();
mHost.invalidateActionBar();
}
@@ -1280,6 +1271,9 @@ public class ConversationFragment extends Fragment implements ConversationDataLi
@Override
public void sendMessage(final MessageData message) {
+ if (isConversationTrashed()) {
+ return;
+ }
if (isReadyForAction()) {
if (ensureKnownRecipients()) {
// Merge the caption text from attachments into the text body of the messages
@@ -1381,6 +1375,9 @@ public class ConversationFragment extends Fragment implements ConversationDataLi
}
public void retrySend(final String messageId) {
+ if (isConversationTrashed()) {
+ return;
+ }
if (isReadyForAction()) {
if (ensureKnownRecipients()) {
showSendingStatus(messageId);
@@ -1427,8 +1424,14 @@ public class ConversationFragment extends Fragment implements ConversationDataLi
public void deleteConversation() {
if (isReadyForDeleteAction()) {
- final Context context = getActivity();
- mBinding.getData().deleteConversation(mBinding);
+ if (mBinding.getData().getIsTrashed()) {
+ mBinding.getData().deleteConversation(mBinding);
+ } else {
+ mBinding.getData().moveConversationToTrash(mBinding);
+ if (getActivity() != null) {
+ HomeToast.show(getActivity(), R.string.home_trash_moved_toast_one);
+ }
+ }
closeConversation(mConversationId);
} else {
warnOfMissingActionConditions(false /*sending*/,
@@ -1458,6 +1461,7 @@ public class ConversationFragment extends Fragment implements ConversationDataLi
mRecyclerView.setVisibility(View.VISIBLE);
updateUnknownSenderBanner();
+ updateTrashComposeLock();
}
}
@@ -1934,6 +1938,34 @@ public class ConversationFragment extends Fragment implements ConversationDataLi
.show();
}
+ private void confirmReportConversationSpam() {
+ if (getActivity() == null || !mBinding.isBound()) {
+ return;
+ }
+ final ParticipantData other = mBinding.getData().getOtherParticipant();
+ if (other == null) {
+ HomeToast.show(getActivity(), R.string.home_report_spam_groups_only);
+ return;
+ }
+ HomeModal.builder(getActivity())
+ .icon(R.drawable.ic_home_menu_spam)
+ .title(R.string.home_report_spam_title)
+ .message(R.string.home_report_spam_message_one)
+ .negative(android.R.string.cancel, null)
+ .positive(R.string.home_unknown_sender_report_spam, () -> {
+ if (!mBinding.isBound()) {
+ return;
+ }
+ UpdateDestinationBlockedAction.updateDestinationBlocked(
+ other.getNormalizedDestination(), true, mConversationId,
+ BugleActionToasts.makeUpdateDestinationBlockedActionListener(
+ getActivity()));
+ HomeToast.show(getActivity(), R.string.home_report_spam_toast_one);
+ closeConversation(mConversationId);
+ })
+ .show();
+ }
+
private void updateUnknownSenderBanner() {
if (mUnknownSenderBanner == null || !mBinding.isBound()) {
return;
@@ -1962,6 +1994,54 @@ public class ConversationFragment extends Fragment implements ConversationDataLi
HomeUiMotion.unknownSenderBannerEnter(mUnknownSenderBanner);
}
+ private void setupTrashComposeBar(final View root) {
+ mTrashBar = root.findViewById(R.id.home_conversation_trash_bar);
+ final View restore = root.findViewById(R.id.home_conversation_trash_restore);
+ if (restore != null) {
+ restore.setOnClickListener(v -> restoreConversationFromTrash());
+ }
+ }
+
+ private boolean isConversationTrashed() {
+ return mBinding.isBound() && mBinding.getData().getIsTrashed();
+ }
+
+ private void restoreConversationFromTrash() {
+ if (!isConversationTrashed()) {
+ return;
+ }
+ mBinding.getData().restoreConversationFromTrash(mBinding);
+ if (getActivity() != null) {
+ HomeToast.show(getActivity(), R.string.home_trash_restored_toast);
+ }
+ }
+
+ private void updateTrashComposeLock() {
+ final boolean trashed = isConversationTrashed() && mSelectedMessage == null;
+ if (mTrashBar != null) {
+ mTrashBar.setVisibility(trashed ? View.VISIBLE : View.GONE);
+ }
+ if (mComposeContainer != null) {
+ mComposeContainer.setVisibility(trashed ? View.GONE : View.VISIBLE);
+ }
+ if (trashed) {
+ HomeComposeAttachPanel.dismissIfShowing(getActivity());
+ HomeGallerySheet.dismissIfShowing(getActivity());
+ if (mComposeMessageView != null) {
+ mComposeMessageView.hideAllComposeInputs(false /* animate */);
+ if (getActivity() != null) {
+ ImeUtil.get().hideImeKeyboard(getActivity(), mComposeMessageView);
+ }
+ }
+ }
+ if (getView() != null) {
+ getView().post(() -> applyConversationEdgeFade(
+ getView().findViewById(R.id.conversation_fade_top),
+ getView().findViewById(R.id.conversation_fade_bottom),
+ getResources().getDimensionPixelSize(R.dimen.home_conversation_edge_fade)));
+ }
+ }
+
private void hideUnknownSenderBanner(final boolean animate) {
if (mUnknownSenderBanner == null) {
return;
@@ -2205,6 +2285,11 @@ public class ConversationFragment extends Fragment implements ConversationDataLi
getResources().getDimensionPixelSize(R.dimen.home_conversation_edge_fade)));
}
HomeUiMotion.swapBottomDock(mComposeContainer, mMessageActionBar, active, null);
+ if (!active) {
+ updateTrashComposeLock();
+ } else if (mTrashBar != null) {
+ mTrashBar.setVisibility(View.GONE);
+ }
}
private void refreshMessageActionButtons() {
@@ -2218,7 +2303,8 @@ public class ConversationFragment extends Fragment implements ConversationDataLi
setMessageActionVisible(mMessageActionShare, canForward);
setMessageActionVisible(mMessageActionDetails, true);
setMessageActionVisible(mMessageActionSave, mSelectedAttachment != null);
- setMessageActionVisible(mMessageActionSend, data.getShowResendMessage());
+ setMessageActionVisible(mMessageActionSend,
+ data.getShowResendMessage() && !isConversationTrashed());
setMessageActionVisible(mMessageActionDownload, data.getShowDownloadMessage());
setMessageActionVisible(mMessageActionDelete, true);
}
@@ -2261,9 +2347,7 @@ public class ConversationFragment extends Fragment implements ConversationDataLi
ClipData.newPlainText(null /* label */, data.getText()));
dismissMessageSelection();
} else if (actionId == R.id.home_message_action_details) {
- MessageDetailsDialog.show(
- getActivity(), data, mBinding.getData().getParticipants(),
- mBinding.getData().getSelfParticipantById(data.getSelfParticipantId()));
+ UIIntents.get().launchHomeMessageDetailsActivity(getActivity(), data);
dismissMessageSelection();
} else if (actionId == R.id.home_message_action_share) {
shareSelectedMessage(data);
@@ -2349,20 +2433,37 @@ public class ConversationFragment extends Fragment implements ConversationDataLi
UIIntents.get().launchHomeConversationSearchActivity(getActivity(), mConversationId);
});
+ final boolean trashed = data.getIsTrashed();
final boolean archived = data.getIsArchived();
final TextView archiveLabel = content.findViewById(R.id.popover_overflow_archive_label);
- archiveLabel.setText(archived
- ? R.string.home_profile_option_unarchive
- : R.string.home_profile_option_archive);
+ archiveLabel.setText(trashed
+ ? R.string.home_archive_restore
+ : (archived
+ ? R.string.home_profile_option_unarchive
+ : R.string.home_profile_option_archive));
content.findViewById(R.id.popover_overflow_archive).setOnClickListener(v -> {
dismissConversationOverflowPopover();
- if (archived) {
+ if (trashed) {
+ mBinding.getData().restoreConversationFromTrash(mBinding);
+ if (getActivity() != null) {
+ HomeToast.show(getActivity(), R.string.home_trash_restored_toast);
+ }
+ } else if (archived) {
mBinding.getData().unarchiveConversation(mBinding);
} else {
mBinding.getData().archiveConversation(mBinding);
closeConversation(mConversationId);
}
});
+ final View spamRow = content.findViewById(R.id.popover_overflow_spam);
+ if (spamRow != null) {
+ final boolean canReportSpam = !trashed && participant != null;
+ spamRow.setVisibility(canReportSpam ? View.VISIBLE : View.GONE);
+ spamRow.setOnClickListener(v -> {
+ dismissConversationOverflowPopover();
+ confirmReportConversationSpam();
+ });
+ }
content.findViewById(R.id.popover_overflow_delete).setOnClickListener(v -> {
dismissConversationOverflowPopover();
confirmDeleteConversation();
@@ -2371,8 +2472,9 @@ public class ConversationFragment extends Fragment implements ConversationDataLi
content.measure(
View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED),
View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED));
+ final int width = content.getMeasuredWidth();
final PopupWindow popup = new PopupWindow(content,
- content.getMeasuredWidth(),
+ width,
ViewGroup.LayoutParams.WRAP_CONTENT,
true);
popup.setOutsideTouchable(true);
@@ -2396,7 +2498,7 @@ public class ConversationFragment extends Fragment implements ConversationDataLi
} else {
windowRight = getResources().getDisplayMetrics().widthPixels;
}
- final int desiredLeft = windowRight - endInset - content.getMeasuredWidth();
+ final int desiredLeft = windowRight - endInset - width;
final int xOff = desiredLeft - anchorLoc[0];
popup.showAsDropDown(anchor, xOff, 8);
HomeUiMotion.popoverEnter(content, true);
@@ -2428,16 +2530,22 @@ public class ConversationFragment extends Fragment implements ConversationDataLi
}
private void confirmDeleteConversation() {
- if (getActivity() == null) {
+ if (getActivity() == null || !mBinding.isBound()) {
return;
}
if (isReadyForDeleteAction()) {
- new AlertDialog.Builder(getActivity(), R.style.Home_Dialog)
- .setTitle(getResources().getQuantityString(
- R.plurals.delete_conversations_confirmation_dialog_title, 1))
- .setPositiveButton(R.string.delete_conversation_confirmation_button,
- (dialog, button) -> deleteConversation())
- .setNegativeButton(R.string.delete_conversation_decline_button, null)
+ final boolean trashed = mBinding.getData().getIsTrashed();
+ HomeModal.builder(getActivity())
+ .icon(R.drawable.ic_home_menu_trash)
+ .title(trashed
+ ? getString(R.string.home_trash_permanent_title)
+ : getResources().getQuantityString(
+ R.plurals.delete_conversations_confirmation_dialog_title, 1))
+ .message(trashed
+ ? getString(R.string.home_trash_permanent_message_one)
+ : getString(R.string.home_delete_alert_message_one))
+ .negative(R.string.delete_conversation_decline_button, null)
+ .positive(R.string.home_delete_confirm, this::deleteConversation)
.show();
} else {
warnOfMissingActionConditions(false /*sending*/,
diff --git a/app/src/main/java/com/android/messaging/ui/conversationlist/AbstractConversationListActivity.java b/app/src/main/java/com/android/messaging/ui/conversationlist/AbstractConversationListActivity.java
index 17d315a..ae8a2ce 100644
--- a/app/src/main/java/com/android/messaging/ui/conversationlist/AbstractConversationListActivity.java
+++ b/app/src/main/java/com/android/messaging/ui/conversationlist/AbstractConversationListActivity.java
@@ -25,6 +25,7 @@ import android.content.Intent;
import android.content.res.Resources;
import android.net.Uri;
import android.os.Bundle;
+import android.view.ActionMode;
import android.view.View;
import androidx.activity.result.ActivityResultLauncher;
import androidx.activity.result.contract.ActivityResultContracts;
@@ -35,6 +36,7 @@ import androidx.appcompat.app.ActionBar;
import com.android.messaging.R;
import com.android.messaging.datamodel.action.DeleteConversationAction;
import com.android.messaging.datamodel.action.UpdateConversationArchiveStatusAction;
+import com.android.messaging.datamodel.action.UpdateConversationTrashStatusAction;
import com.android.messaging.datamodel.action.UpdateDestinationBlockedAction;
import com.android.messaging.datamodel.data.ConversationListData;
import com.android.messaging.datamodel.data.ConversationListItemData;
@@ -45,6 +47,8 @@ import com.android.messaging.datamodel.action.MarkAsUnreadAction;
import com.android.messaging.ui.UIIntents;
import com.android.messaging.ui.contact.AddContactsConfirmationDialog;
import com.android.messaging.ui.home.HomeDeleteAlertDialog;
+import com.android.messaging.ui.home.HomeInboxMenu;
+import com.android.messaging.ui.home.HomeModal;
import com.android.messaging.ui.home.HomeToast;
import com.android.messaging.ui.conversationlist.ConversationListFragment.ConversationListFragmentHost;
import com.android.messaging.ui.conversationlist.MultiSelectActionModeCallback.SelectedConversation;
@@ -96,7 +100,123 @@ public abstract class AbstractConversationListActivity extends BugleActionBarAct
return false;
}
+ protected boolean isTrashList() {
+ return false;
+ }
+
+ @Override
+ protected boolean shouldShowContextualActionBar(final ActionMode.Callback callback) {
+ if (useHomeSelectionChrome() && callback instanceof MultiSelectActionModeCallback) {
+ return !((MultiSelectActionModeCallback) callback).useHomeSelectionChrome();
+ }
+ return super.shouldShowContextualActionBar(callback);
+ }
+
+ public void enterHomeEditMode() {
+ if (!hasVisibleConversations()) {
+ HomeToast.show(this, R.string.home_toast_nothing_to_select);
+ return;
+ }
+ if (!isInConversationListSelectMode()) {
+ startMultiSelectActionMode();
+ }
+ }
+
+ private boolean hasVisibleConversations() {
+ return mConversationListFragment != null
+ && mConversationListFragment.getVisibleConversationCount() > 0;
+ }
+
+ public void onSubListSelectionMode(final boolean enabled) {
+ onSubListSelectionCount(0);
+ HomeInboxMenu.setSelecting(this, enabled);
+ }
+
+ public void onSubListSelectionCount(final int count) {
+ final int visible = mConversationListFragment == null
+ ? 0 : mConversationListFragment.getVisibleConversationCount();
+ final boolean hasWork = count > 0 || visible > 0;
+ if (isTrashList()) {
+ HomeInboxMenu.setSelectAction(this,
+ count > 0 ? R.drawable.ic_delete_small_dark : R.drawable.ic_home_menu_trash,
+ count > 0 ? R.string.home_delete_confirm : R.string.home_empty_trash_action,
+ hasWork, true);
+ } else {
+ HomeInboxMenu.setSelectAction(this, R.drawable.ic_home_menu_archive,
+ count > 0 ? R.string.home_sub_unarchive : R.string.home_sub_unarchive_all,
+ hasWork, false);
+ }
+ }
+
+ public void onSubListPrimaryAction() {
+ if (isTrashList()) {
+ emptyTrashOrDeleteSelected();
+ } else {
+ unarchiveAllOrSelected();
+ }
+ }
+
+ private void unarchiveAllOrSelected() {
+ final Collection selected = getSelectedConversationsIfAny();
+ if (selected != null && !selected.isEmpty()) {
+ restoreSelectedConversations();
+ return;
+ }
+ if (mConversationListFragment == null) {
+ return;
+ }
+ final List items =
+ mConversationListFragment.snapshotVisibleConversations();
+ if (items.isEmpty()) {
+ HomeToast.show(this, R.string.home_toast_nothing_to_unarchive);
+ return;
+ }
+ for (final ConversationListItemData item : items) {
+ UpdateConversationArchiveStatusAction.unarchiveConversation(item.getConversationId());
+ }
+ if (items.size() == 1) {
+ HomeToast.show(this, R.string.home_profile_unarchived_toast);
+ } else {
+ HomeToast.show(this, getString(R.string.home_sub_unarchived_toast_many, items.size()));
+ }
+ exitMultiSelectState();
+ }
+
+ private void emptyTrashOrDeleteSelected() {
+ final Collection selected = getSelectedConversationsIfAny();
+ if (selected != null && !selected.isEmpty()) {
+ confirmPermanentDelete(selected);
+ return;
+ }
+ if (mConversationListFragment == null) {
+ return;
+ }
+ final List items =
+ mConversationListFragment.snapshotVisibleConversations();
+ if (items.isEmpty()) {
+ HomeToast.show(this, R.string.home_toast_nothing_to_empty);
+ return;
+ }
+ final ArrayList all = new ArrayList<>(items.size());
+ for (final ConversationListItemData item : items) {
+ all.add(new SelectedConversation(item));
+ }
+ confirmEmptyAll(all);
+ }
+
+ private Collection getSelectedConversationsIfAny() {
+ if (!isInConversationListSelectMode()) {
+ return null;
+ }
+ final MultiSelectActionModeCallback callback =
+ (MultiSelectActionModeCallback) getActionModeCallback();
+ return callback.getSelectedConversations();
+ }
+
protected void startMultiSelectActionMode() {
+ if (mConversationListFragment == null) {
+ return;
+ }
final boolean homeChrome = useHomeSelectionChrome();
if (homeChrome) {
hideHomeContextualActionBar();
@@ -112,6 +232,9 @@ public abstract class AbstractConversationListActivity extends BugleActionBarAct
public void exitMultiSelectState() {
dismissActionMode();
+ if (mConversationListFragment == null) {
+ return;
+ }
if (useHomeSelectionChrome()) {
mConversationListFragment.setHomeSelectionMode(false);
} else {
@@ -138,6 +261,10 @@ public abstract class AbstractConversationListActivity extends BugleActionBarAct
if (conversations.isEmpty()) {
return;
}
+ if (isTrashList()) {
+ confirmPermanentDelete(conversations);
+ return;
+ }
if (useHomeSelectionChrome()) {
showHomeDeleteAlert(conversations);
} else {
@@ -149,8 +276,13 @@ public abstract class AbstractConversationListActivity extends BugleActionBarAct
if (data == null) {
return;
}
- showHomeDeleteAlert(java.util.Collections.singletonList(
- new MultiSelectActionModeCallback.SelectedConversation(data)));
+ final java.util.List one = java.util.Collections.singletonList(
+ new MultiSelectActionModeCallback.SelectedConversation(data));
+ if (isTrashList()) {
+ confirmPermanentDelete(one);
+ return;
+ }
+ showHomeDeleteAlert(one);
}
public void markSelectedConversationsUnread() {
@@ -166,6 +298,109 @@ public abstract class AbstractConversationListActivity extends BugleActionBarAct
exitMultiSelectState();
}
+ public void restoreSelectedConversations() {
+ if (!isInConversationListSelectMode()) {
+ return;
+ }
+ final MultiSelectActionModeCallback callback =
+ (MultiSelectActionModeCallback) getActionModeCallback();
+ final Collection conversations =
+ callback.getSelectedConversations();
+ if (conversations.isEmpty()) {
+ return;
+ }
+ for (final SelectedConversation conversation : conversations) {
+ if (isTrashList()) {
+ UpdateConversationTrashStatusAction.restoreFromTrash(conversation.conversationId);
+ } else {
+ UpdateConversationArchiveStatusAction.unarchiveConversation(
+ conversation.conversationId);
+ }
+ }
+ HomeToast.show(this, isTrashList()
+ ? R.string.home_trash_restored_toast
+ : R.string.home_profile_unarchived_toast);
+ exitMultiSelectState();
+ }
+
+ public void archiveSelectedConversations() {
+ if (!isInConversationListSelectMode()) {
+ return;
+ }
+ final MultiSelectActionModeCallback callback =
+ (MultiSelectActionModeCallback) getActionModeCallback();
+ final Collection conversations =
+ callback.getSelectedConversations();
+ if (conversations.isEmpty()) {
+ return;
+ }
+ int count = 0;
+ for (final SelectedConversation conversation : conversations) {
+ UpdateConversationArchiveStatusAction.archiveConversation(
+ conversation.conversationId);
+ count++;
+ }
+ if (count == 1) {
+ HomeToast.show(this, R.string.home_profile_archived_toast);
+ } else {
+ HomeToast.show(this, getString(R.string.home_selection_archived_toast_many, count));
+ }
+ exitMultiSelectState();
+ }
+
+ public void reportSelectedConversationsSpam() {
+ if (!isInConversationListSelectMode()) {
+ return;
+ }
+ final MultiSelectActionModeCallback callback =
+ (MultiSelectActionModeCallback) getActionModeCallback();
+ final Collection conversations = new ArrayList<>(
+ callback.getSelectedConversations());
+ int reportableCount = 0;
+ for (final SelectedConversation conversation : conversations) {
+ if (!conversation.isGroup
+ && conversation.otherParticipantNormalizedDestination != null) {
+ reportableCount++;
+ }
+ }
+ if (reportableCount == 0) {
+ HomeToast.show(this, R.string.home_report_spam_groups_only);
+ return;
+ }
+ final int reportable = reportableCount;
+ final String message = reportable == 1
+ ? getString(R.string.home_report_spam_message_one)
+ : getString(R.string.home_report_spam_message_many, reportable);
+ HomeModal.builder(this)
+ .icon(R.drawable.ic_home_menu_spam)
+ .title(R.string.home_report_spam_title)
+ .message(message)
+ .negative(android.R.string.cancel, null)
+ .positive(R.string.home_unknown_sender_report_spam, () ->
+ performReportSelectedSpam(conversations, reportable))
+ .show();
+ }
+
+ private void performReportSelectedSpam(
+ final Collection conversations, final int reportable) {
+ for (final SelectedConversation conversation : conversations) {
+ if (conversation.isGroup
+ || conversation.otherParticipantNormalizedDestination == null) {
+ continue;
+ }
+ UpdateDestinationBlockedAction.updateDestinationBlocked(
+ conversation.otherParticipantNormalizedDestination, true,
+ conversation.conversationId,
+ NOOP_BLOCK_LISTENER);
+ }
+ if (reportable == 1) {
+ HomeToast.show(this, R.string.home_report_spam_toast_one);
+ } else {
+ HomeToast.show(this, getString(R.string.home_report_spam_toast_many, reportable));
+ }
+ exitMultiSelectState();
+ }
+
protected boolean isInConversationListSelectMode() {
return getActionModeCallback() instanceof MultiSelectActionModeCallback;
}
@@ -181,17 +416,7 @@ public abstract class AbstractConversationListActivity extends BugleActionBarAct
return;
}
HomeDeleteAlertDialog.show(this, conversations.size(),
- new HomeDeleteAlertDialog.Listener() {
- @Override
- public void onDelete() {
- performDeleteConversations(conversations);
- }
-
- @Override
- public void onDeleteAndRegisterSpam() {
- performDeleteAndRegisterSpam(conversations);
- }
- });
+ () -> performDeleteConversations(conversations));
}
private boolean ensureDefaultSmsAppForDelete() {
@@ -217,50 +442,67 @@ public abstract class AbstractConversationListActivity extends BugleActionBarAct
private void performDeleteConversations(
final Collection conversations) {
- performDeleteConversations(conversations, false /* reportSpam */);
- }
-
- private void performDeleteConversations(
- final Collection conversations, final boolean reportSpam) {
final int count = conversations.size();
- for (final SelectedConversation conversation : conversations) {
- DeleteConversationAction.deleteConversation(
- conversation.conversationId,
- conversation.timestamp);
+ if (isTrashList()) {
+ for (final SelectedConversation conversation : conversations) {
+ DeleteConversationAction.deleteConversation(
+ conversation.conversationId,
+ conversation.timestamp);
+ }
+ showDeleteCompletedToast(count);
+ } else {
+ for (final SelectedConversation conversation : conversations) {
+ UpdateConversationTrashStatusAction.moveToTrash(conversation.conversationId);
+ }
+ if (useHomeSelectionChrome() && count > 0) {
+ HomeToast.show(this, count == 1
+ ? getString(R.string.home_trash_moved_toast_one)
+ : getString(R.string.home_trash_moved_toast_many, count));
+ }
+ }
+ if (isInConversationListSelectMode()) {
+ exitMultiSelectState();
}
- showDeleteCompletedToast(count, reportSpam);
- exitMultiSelectState();
}
- private void showDeleteCompletedToast(final int count, final boolean reportSpam) {
+ private void confirmPermanentDelete(final Collection conversations) {
+ if (!ensureDefaultSmsAppForDelete()) {
+ return;
+ }
+ final int count = conversations.size();
+ HomeModal.builder(this)
+ .icon(R.drawable.ic_home_menu_trash)
+ .title(R.string.home_trash_permanent_title)
+ .message(count == 1
+ ? getString(R.string.home_trash_permanent_message_one)
+ : getString(R.string.home_trash_permanent_message_many, count))
+ .negative(android.R.string.cancel, null)
+ .positive(R.string.home_delete_confirm, () ->
+ performDeleteConversations(conversations))
+ .show();
+ }
+
+ private void confirmEmptyAll(final Collection conversations) {
+ if (!ensureDefaultSmsAppForDelete()) {
+ return;
+ }
+ HomeModal.builder(this)
+ .icon(R.drawable.ic_home_menu_trash)
+ .title(R.string.home_empty_all_title)
+ .message(R.string.home_empty_all_message)
+ .negative(android.R.string.cancel, null)
+ .positive(R.string.home_empty_all_confirm, () ->
+ performDeleteConversations(conversations))
+ .show();
+ }
+
+ private void showDeleteCompletedToast(final int count) {
if (!useHomeSelectionChrome() || count <= 0) {
return;
}
- final String message;
- if (reportSpam) {
- message = count == 1
- ? getString(R.string.home_delete_spam_toast_one)
- : getString(R.string.home_delete_spam_toast_many, count);
- } else {
- message = count == 1
- ? getString(R.string.home_delete_toast_one)
- : getString(R.string.home_delete_toast_many, count);
- }
- HomeToast.show(this, message);
- }
-
- private void performDeleteAndRegisterSpam(
- final Collection conversations) {
- for (final SelectedConversation conversation : conversations) {
- if (!conversation.isGroup
- && conversation.otherParticipantNormalizedDestination != null) {
- UpdateDestinationBlockedAction.updateDestinationBlocked(
- conversation.otherParticipantNormalizedDestination, true,
- conversation.conversationId,
- NOOP_BLOCK_LISTENER);
- }
- }
- performDeleteConversations(conversations, true /* registerSpam */);
+ HomeToast.show(this, count == 1
+ ? getString(R.string.home_delete_toast_one)
+ : getString(R.string.home_delete_toast_many, count));
}
@Override
diff --git a/app/src/main/java/com/android/messaging/ui/conversationlist/ArchivedConversationListActivity.java b/app/src/main/java/com/android/messaging/ui/conversationlist/ArchivedConversationListActivity.java
index 77e0a76..2abdcbe 100644
--- a/app/src/main/java/com/android/messaging/ui/conversationlist/ArchivedConversationListActivity.java
+++ b/app/src/main/java/com/android/messaging/ui/conversationlist/ArchivedConversationListActivity.java
@@ -22,6 +22,7 @@ import android.view.MenuItem;
import androidx.appcompat.app.ActionBar;
import com.android.messaging.R;
+import com.android.messaging.ui.home.HomeInboxMenu;
import com.android.messaging.ui.home.HomeSubHeader;
public class ArchivedConversationListActivity extends AbstractConversationListActivity {
@@ -33,7 +34,8 @@ public class ArchivedConversationListActivity extends AbstractConversationListAc
if (getSupportActionBar() != null) {
getSupportActionBar().hide();
}
- HomeSubHeader.bind(this, R.string.home_recently_deleted_title, v -> onActionBarHome());
+ HomeSubHeader.bind(this, R.string.home_archived_title, v -> onActionBarHome());
+ HomeInboxMenu.bind(this, HomeInboxMenu.Screen.ARCHIVE);
if (savedInstanceState == null) {
mConversationListFragment =
@@ -87,7 +89,7 @@ public class ArchivedConversationListActivity extends AbstractConversationListAc
@Override
public boolean isSwipeAnimatable() {
- return false;
+ return !isInConversationListSelectMode();
}
@Override
diff --git a/app/src/main/java/com/android/messaging/ui/conversationlist/ConversationListActivity.java b/app/src/main/java/com/android/messaging/ui/conversationlist/ConversationListActivity.java
index a9efd65..c26d3a2 100644
--- a/app/src/main/java/com/android/messaging/ui/conversationlist/ConversationListActivity.java
+++ b/app/src/main/java/com/android/messaging/ui/conversationlist/ConversationListActivity.java
@@ -18,7 +18,6 @@
package com.android.messaging.ui.conversationlist;
import android.os.Bundle;
-import android.view.ActionMode;
import android.window.SplashScreenView;
import android.view.Menu;
import android.view.MenuItem;
@@ -52,14 +51,6 @@ public class ConversationListActivity extends AbstractConversationListActivity {
return true;
}
- @Override
- protected boolean shouldShowContextualActionBar(final ActionMode.Callback callback) {
- if (callback instanceof MultiSelectActionModeCallback) {
- return !((MultiSelectActionModeCallback) callback).useHomeSelectionChrome();
- }
- return true;
- }
-
@Override
protected void updateActionBar(final ActionBar actionBar) {
if (actionBar == null) {
@@ -152,12 +143,6 @@ public class ConversationListActivity extends AbstractConversationListActivity {
UIIntents.get().launchHomeAutomationActivity(this);
}
- public void enterHomeEditMode() {
- if (!isInConversationListSelectMode()) {
- startMultiSelectActionMode();
- }
- }
-
@Override
public boolean isSwipeAnimatable() {
return !isInConversationListSelectMode();
diff --git a/app/src/main/java/com/android/messaging/ui/conversationlist/ConversationListAdapter.java b/app/src/main/java/com/android/messaging/ui/conversationlist/ConversationListAdapter.java
index f09591e..9331119 100644
--- a/app/src/main/java/com/android/messaging/ui/conversationlist/ConversationListAdapter.java
+++ b/app/src/main/java/com/android/messaging/ui/conversationlist/ConversationListAdapter.java
@@ -34,6 +34,8 @@ import com.android.messaging.datamodel.data.ConversationListItemData;
import com.android.messaging.ui.CursorRecyclerAdapter;
import com.android.messaging.ui.home.HomeCategoryClassifier;
import com.android.messaging.ui.home.HomeCategoryStore;
+import com.android.messaging.ui.home.HomeInboxFilter;
+import com.android.messaging.ui.home.HomeSubListSearch;
import com.android.messaging.ui.home.PinnedConversationStore;
import java.util.ArrayList;
@@ -51,6 +53,8 @@ public class ConversationListAdapter
public static final int FILTER_ALL = 0;
public static final int FILTER_UNREAD = 1;
public static final int FILTER_GROUPS = 2;
+ public static final int FILTER_WORK = 3;
+ public static final int FILTER_PERSONAL = 4;
private static final int VIEW_TYPE_PINNED_HEADER = 1;
private static final int VIEW_TYPE_CONVERSATION = 2;
@@ -61,6 +65,7 @@ public class ConversationListAdapter
private final ConversationListItemData mFilterProbe = new ConversationListItemData();
private int mFilterMode = FILTER_ALL;
private String mCategoryId = HomeCategoryStore.ID_ALL;
+ private String mSearchQuery = "";
private int[] mDisplayPositions = new int[0];
private View mPinnedStripRoot;
private boolean mPinnedHeaderVisible;
@@ -116,6 +121,24 @@ public class ConversationListAdapter
return mFilterMode;
}
+ public void setSearchQuery(final String query) {
+ final String next = query == null ? "" : query.trim();
+ if (next.equals(mSearchQuery)) {
+ return;
+ }
+ mSearchQuery = next;
+ rebuildFilterIndex();
+ notifyDataSetChanged();
+ }
+
+ public String getSearchQuery() {
+ return mSearchQuery;
+ }
+
+ public boolean hasSearchQuery() {
+ return !mSearchQuery.isEmpty();
+ }
+
public void setCategoryId(final String categoryId) {
final String next = categoryId == null ? HomeCategoryStore.ID_ALL : categoryId;
if (next.equals(mCategoryId)) {
@@ -156,6 +179,28 @@ public class ConversationListAdapter
return items;
}
+ /** Visible (filtered) conversation rows, not the raw cursor. */
+ public List snapshotVisibleConversations() {
+ final Cursor cursor = getCursor();
+ if (cursor == null || mDisplayPositions.length == 0) {
+ return Collections.emptyList();
+ }
+ final List items = new ArrayList<>(mDisplayPositions.length);
+ final int saved = cursor.getPosition();
+ for (final int position : mDisplayPositions) {
+ if (!cursor.moveToPosition(position)) {
+ continue;
+ }
+ final ConversationListItemData item = new ConversationListItemData();
+ item.bind(cursor);
+ items.add(item);
+ }
+ if (saved >= 0 && saved < cursor.getCount()) {
+ cursor.moveToPosition(saved);
+ }
+ return items;
+ }
+
public void notifySelectionChromeChanged() {
final int count = getItemCount();
if (count > 0) {
@@ -294,6 +339,18 @@ public class ConversationListAdapter
if (mFilterMode == FILTER_GROUPS && mFilterProbe.getParticipantCount() <= 1) {
return false;
}
+ if (mFilterMode == FILTER_WORK && !HomeInboxFilter.matchesWork(mContext, mFilterProbe)) {
+ return false;
+ }
+ if (mFilterMode == FILTER_PERSONAL
+ && !HomeInboxFilter.matchesPersonal(mContext, mFilterProbe)) {
+ return false;
+ }
+ if (!HomeSubListSearch.matches(mSearchQuery,
+ mFilterProbe.getName(), mFilterProbe.getSnippetText(),
+ mFilterProbe.getSnippetSenderName())) {
+ return false;
+ }
return HomeCategoryClassifier.matches(mContext, mCategoryId, mFilterProbe);
}
diff --git a/app/src/main/java/com/android/messaging/ui/conversationlist/ConversationListFragment.java b/app/src/main/java/com/android/messaging/ui/conversationlist/ConversationListFragment.java
index 40e2609..ea551d3 100644
--- a/app/src/main/java/com/android/messaging/ui/conversationlist/ConversationListFragment.java
+++ b/app/src/main/java/com/android/messaging/ui/conversationlist/ConversationListFragment.java
@@ -28,6 +28,7 @@ import android.graphics.Rect;
import android.graphics.RenderEffect;
import android.graphics.Shader;
import android.telephony.PhoneNumberUtils;
+import android.telephony.SubscriptionInfo;
import android.net.Uri;
import android.os.Bundle;
import android.os.Parcelable;
@@ -37,6 +38,7 @@ import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
+import android.view.MotionEvent;
import android.view.Gravity;
import android.view.HapticFeedbackConstants;
import android.view.View;
@@ -66,6 +68,10 @@ import com.android.messaging.datamodel.MessagingContentProvider;
import com.android.messaging.datamodel.data.ConversationMessageData;
import com.android.messaging.datamodel.action.MarkAsReadAction;
import com.android.messaging.datamodel.action.MarkAsUnreadAction;
+import com.android.messaging.datamodel.action.UpdateConversationArchiveStatusAction;
+import com.android.messaging.datamodel.action.UpdateConversationTrashStatusAction;
+import com.android.messaging.datamodel.action.UpdateDestinationBlockedAction;
+import com.android.messaging.datamodel.action.UpdateMessageNotificationAction;
import com.android.messaging.datamodel.binding.Binding;
import com.android.messaging.datamodel.binding.BindingBase;
import com.android.messaging.datamodel.data.ConversationListData;
@@ -76,6 +82,7 @@ import com.android.messaging.ui.home.HomeCategoryAddModal;
import com.android.messaging.ui.home.HomeCategoryAssignSheet;
import com.android.messaging.ui.home.HomeCategoryClassifier;
import com.android.messaging.ui.home.HomeCategoryStore;
+import com.android.messaging.ui.home.HomeInboxFilter;
import com.android.messaging.ui.home.HomeModal;
import com.android.messaging.ui.home.HomeEmptyState;
import com.android.messaging.ui.SnackBarInteraction;
@@ -86,6 +93,7 @@ import com.android.messaging.util.ContactUtil;
import com.android.messaging.util.Dates;
import com.android.messaging.util.PhoneUtils;
import com.android.messaging.ui.home.HomePinnedStrip;
+import com.android.messaging.ui.home.HomeSubListSearch;
import com.android.messaging.ui.home.MutedConversationStore;
import com.android.messaging.ui.home.HomeToast;
import com.android.messaging.ui.home.HomeUiMotion;
@@ -108,10 +116,12 @@ import java.util.Set;
public class ConversationListFragment extends Fragment implements ConversationListDataListener,
ConversationListItemView.HostInterface {
private static final String BUNDLE_ARCHIVED_MODE = "archived_mode";
+ private static final String BUNDLE_TRASH_MODE = "trash_mode";
private static final String BUNDLE_FORWARD_MESSAGE_MODE = "forward_message_mode";
private MenuItem mShowBlockedMenuItem;
private boolean mArchiveMode;
+ private boolean mTrashMode;
private boolean mBlockedAvailable;
private boolean mForwardMessageMode;
@@ -134,7 +144,16 @@ public class ConversationListFragment extends Fragment implements ConversationLi
private View mHomeBottomNav;
private View mHomeSelectionBar;
private ImageButton mHomeSelectionDelete;
- private TextView mHomeReadAllButton;
+ private View mHomeSelectionUnread;
+ private TextView mHomeSelectionUnreadLabel;
+ private View mHomeSelectionSpam;
+ private View mHomeSelectionArchive;
+ private View mHomeSelectionDock;
+ private View mHomeSelectionTabs;
+ private View mHomeSelectionIndicator;
+ private int mHomeSelectionUnreadNaturalWidth;
+ private int mHomeSelectionSpamNaturalWidth;
+ private int mHomeSelectionArchiveNaturalWidth;
private int mHomeSelectionCount;
private View mHomeMessagesTop;
private View mHomeCategoryCollapse;
@@ -149,6 +168,7 @@ public class ConversationListFragment extends Fragment implements ConversationLi
private View mHomePinnedStrip;
private TextView mHomeEditButton;
private ImageButton mHomePinnedDoneButton;
+ private View mHomeSelectionDone;
private boolean mHomePinnedEditMode;
private View mHomeNavDock;
private View mHomeNavMessages;
@@ -189,6 +209,7 @@ public class ConversationListFragment extends Fragment implements ConversationLi
"homeSearchBarExpanded";
private static final String SAVED_INSTANCE_STATE_HOME_TAB = "homeTab";
private static final String SAVED_INSTANCE_STATE_PINNED_EDIT = "homePinnedEdit";
+ private static final String SAVED_INSTANCE_STATE_FILTER = "homeFilter";
private static final int HOME_TAB_MESSAGES = 0;
private static final int HOME_TAB_AUTOMATE = 1;
private static final int SEARCH_NEWEST_EDGE_SLOP_DP = 48;
@@ -236,10 +257,18 @@ public class ConversationListFragment extends Fragment implements ConversationLi
return createConversationListFragment(BUNDLE_ARCHIVED_MODE);
}
+ public static ConversationListFragment createTrashConversationListFragment() {
+ return createConversationListFragment(BUNDLE_TRASH_MODE);
+ }
+
public static ConversationListFragment createForwardMessageConversationListFragment() {
return createConversationListFragment(BUNDLE_FORWARD_MESSAGE_MODE);
}
+ private boolean isSubList() {
+ return mArchiveMode || mTrashMode || mForwardMessageMode;
+ }
+
public static ConversationListFragment createConversationListFragment(String modeKeyName) {
final ConversationListFragment fragment = new ConversationListFragment();
if (modeKeyName != null) {
@@ -258,6 +287,10 @@ public class ConversationListFragment extends Fragment implements ConversationLi
super.onCreate(bundle);
mListBinding.getData().init(LoaderManager.getInstance(this), mListBinding);
mAdapter = new ConversationListAdapter(getActivity(), null, this);
+ if (bundle != null) {
+ mAdapter.setFilterMode(bundle.getInt(SAVED_INSTANCE_STATE_FILTER,
+ ConversationListAdapter.FILTER_ALL));
+ }
}
@Override
@@ -275,8 +308,7 @@ public class ConversationListFragment extends Fragment implements ConversationLi
}
public void setScrolledToNewestConversationIfNeeded() {
- if (!mArchiveMode
- && !mForwardMessageMode
+ if (!isSubList()
&& isScrolledToFirstConversation()
&& mHost.hasWindowFocus()) {
mListBinding.getData().setScrolledToNewestConversation(true);
@@ -320,9 +352,7 @@ public class ConversationListFragment extends Fragment implements ConversationLi
}
private void updateListTopFade() {
- final boolean listActive = mHomeTab == HOME_TAB_MESSAGES
- && !mArchiveMode
- && !mForwardMessageMode;
+ final boolean listActive = isSubList() || mHomeTab == HOME_TAB_MESSAGES;
setFadeVisible(mListTopFade, listActive && !isListFlushAtTop());
setFadeVisible(mListBottomFade, listActive && !isListFlushAtBottom());
}
@@ -397,7 +427,7 @@ public class ConversationListFragment extends Fragment implements ConversationLi
mListBinding.getData().setScrolledToNewestConversation(false);
}
- if (!mArchiveMode && !mForwardMessageMode) {
+ if (!isSubList()) {
handleSearchBarByScrollOffset(dy);
}
updateListTopFade();
@@ -424,12 +454,33 @@ public class ConversationListFragment extends Fragment implements ConversationLi
mHomeBottomNav = rootView.findViewById(R.id.home_bottom_nav);
mHomeSelectionBar = rootView.findViewById(R.id.home_selection_bar);
mHomeSelectionDelete = rootView.findViewById(R.id.home_selection_delete);
- mHomeReadAllButton = rootView.findViewById(R.id.home_read_all_button);
+ mHomeSelectionUnread = rootView.findViewById(R.id.home_selection_unread);
+ mHomeSelectionSpam = rootView.findViewById(R.id.home_selection_spam);
+ mHomeSelectionArchive = rootView.findViewById(R.id.home_selection_archive);
+ mHomeSelectionDock = rootView.findViewById(R.id.home_selection_actions);
+ mHomeSelectionTabs = rootView.findViewById(R.id.home_selection_tabs);
+ mHomeSelectionIndicator = rootView.findViewById(R.id.home_selection_indicator);
+ if (mHomeSelectionUnread != null) {
+ mHomeSelectionUnreadLabel = mHomeSelectionUnread.findViewById(R.id.home_nav_tab_label);
+ bindHomeSelectionTab(mHomeSelectionUnread, R.drawable.ic_home_menu_unread,
+ R.string.home_read_all, true);
+ }
+ if (mHomeSelectionSpam != null) {
+ bindHomeSelectionTab(mHomeSelectionSpam, R.drawable.ic_home_menu_spam,
+ R.string.home_selection_spam, false);
+ }
+ if (mHomeSelectionArchive != null) {
+ bindHomeSelectionTab(mHomeSelectionArchive, R.drawable.ic_home_menu_archive,
+ R.string.home_selection_archive, false);
+ }
+ applyHomeSelectionTabEnabled(mHomeSelectionSpam, false);
+ applyHomeSelectionTabEnabled(mHomeSelectionArchive, false);
+ bindHomeSelectionDockMetrics();
mHomeComposeContainer = rootView.findViewById(R.id.home_compose_container);
mStartNewConversationButton = rootView.findViewById(R.id.home_nav_search);
mHomeAutomationCreateButton = rootView.findViewById(R.id.home_nav_automation_create);
- if (mArchiveMode || mForwardMessageMode) {
+ if (isSubList()) {
rootView.setFitsSystemWindows(false);
if (mHomeComposeContainer != null) {
mHomeComposeContainer.setVisibility(View.GONE);
@@ -496,6 +547,8 @@ public class ConversationListFragment extends Fragment implements ConversationLi
updateHomeEditButtonForPinnedMode();
}
+ bindSubListSearch();
+
// The root view has a non-null background, which by default is deemed by the framework
// to be a "transition group," where all child views are animated together during an
// activity transition. However, we want each individual items in the recycler view to
@@ -506,6 +559,45 @@ public class ConversationListFragment extends Fragment implements ConversationLi
return rootView;
}
+ private void bindHomeSelectionDockMetrics() {
+ if (mHomeSelectionUnread == null) {
+ return;
+ }
+ mHomeSelectionUnread.addOnLayoutChangeListener((v, l, t, r, b, ol, ot, or, ob) ->
+ applyHomeSelectionDockMetrics());
+ mHomeSelectionUnread.post(this::applyHomeSelectionDockMetrics);
+ }
+
+ private void applyHomeSelectionDockMetrics() {
+ if (mHomeSelectionUnreadNaturalWidth <= 0 && mHomeSelectionUnread != null) {
+ mHomeSelectionUnreadNaturalWidth = measureNavTabNaturalWidth(mHomeSelectionUnread);
+ }
+ if (mHomeSelectionSpamNaturalWidth <= 0 && mHomeSelectionSpam != null) {
+ mHomeSelectionSpamNaturalWidth = measureNavTabNaturalWidth(mHomeSelectionSpam);
+ }
+ if (mHomeSelectionArchiveNaturalWidth <= 0 && mHomeSelectionArchive != null) {
+ mHomeSelectionArchiveNaturalWidth = measureNavTabNaturalWidth(mHomeSelectionArchive);
+ }
+ applyHomeNavTabActiveSize(mHomeSelectionUnread, mHomeSelectionUnreadNaturalWidth);
+ applyHomeNavTabActiveSize(mHomeSelectionSpam, mHomeSelectionSpamNaturalWidth);
+ applyHomeNavTabActiveSize(mHomeSelectionArchive, mHomeSelectionArchiveNaturalWidth);
+ slideHomeSelectionIndicator();
+ if (mHomeSelectionDock != null) {
+ mHomeSelectionDock.post(() -> alignDockToGuideline(mHomeSelectionDock));
+ }
+ }
+
+ private void slideHomeSelectionIndicator() {
+ if (mHomeSelectionUnread == null || mHomeSelectionIndicator == null
+ || mHomeSelectionTabs == null || mHomeSelectionUnread.getWidth() <= 0) {
+ return;
+ }
+ final float x = mHomeSelectionTabs.getLeft() + mHomeSelectionUnread.getLeft();
+ final float y = mHomeSelectionTabs.getTop() + mHomeSelectionUnread.getTop();
+ HomeUiMotion.slideNavIndicator(mHomeSelectionIndicator, x, y,
+ mHomeSelectionUnread.getWidth(), mHomeSelectionUnread.getHeight(), false);
+ }
+
private void bindHomeNavMetrics(final View gap) {
mHomeNavTabGap = gap;
if (mHomeNavMessages == null) {
@@ -536,22 +628,26 @@ public class ConversationListFragment extends Fragment implements ConversationLi
applyHomeNavTabActiveSize(mHomeNavMessages, mHomeNavMessagesNaturalWidth);
applyHomeNavTabActiveSize(mHomeNavAutomate, mHomeNavAutomateNaturalWidth);
if (mHomeNavDock != null) {
- mHomeNavDock.post(this::alignHomeNavDockToGuideline);
+ mHomeNavDock.post(() -> alignDockToGuideline(mHomeNavDock));
}
}
/** Slide only the Messages/Automation pill to the 20dp screen guideline; compose stays put. */
private void alignHomeNavDockToGuideline() {
- if (mHomeNavDock == null || mRootView == null || mHomeNavDock.getWidth() <= 0) {
+ alignDockToGuideline(mHomeNavDock);
+ }
+
+ private void alignDockToGuideline(final View dock) {
+ if (dock == null || mRootView == null || dock.getWidth() <= 0) {
return;
}
final int guideline = getResources().getDimensionPixelSize(R.dimen.looply_screen_padding);
- mHomeNavDock.setTranslationX(0f);
+ dock.setTranslationX(0f);
final int[] dockLoc = new int[2];
final int[] rootLoc = new int[2];
- mHomeNavDock.getLocationOnScreen(dockLoc);
+ dock.getLocationOnScreen(dockLoc);
mRootView.getLocationOnScreen(rootLoc);
- mHomeNavDock.setTranslationX(guideline - (dockLoc[0] - rootLoc[0]));
+ dock.setTranslationX(guideline - (dockLoc[0] - rootLoc[0]));
}
private void applyHomeNavTabActiveSize(final View tab, final int natural) {
@@ -629,9 +725,28 @@ public class ConversationListFragment extends Fragment implements ConversationLi
private void hideHomeChrome(final View rootView) {
setGone(rootView, R.id.home_messages_top);
+ setGone(rootView, R.id.home_category_collapse);
setGone(rootView, R.id.home_search_bar_container);
setGone(rootView, R.id.home_bottom_nav);
setGone(rootView, R.id.home_selection_bar);
+ setGone(rootView, R.id.home_automation_pane);
+ setGone(rootView, R.id.home_compose_container);
+ if (rootView instanceof ViewGroup && ((ViewGroup) rootView).getChildCount() > 0) {
+ ((ViewGroup) rootView).getChildAt(0).setFitsSystemWindows(false);
+ }
+ }
+
+ private void bindSubListSearch() {
+ if (!isSubList() || getActivity() == null) {
+ return;
+ }
+ HomeSubListSearch.bind(requireActivity(), mRecyclerView, query -> {
+ if (mAdapter == null) {
+ return;
+ }
+ mAdapter.setSearchQuery(query);
+ updateEmptyListUi(mAdapter.getConversationItemCount() == 0);
+ });
}
private static void setGone(final View root, final int id) {
@@ -849,16 +964,34 @@ public class ConversationListFragment extends Fragment implements ConversationLi
navAutomate.setOnClickListener(v -> selectHomeTab(HOME_TAB_AUTOMATE));
}
- final View selectionDone = rootView.findViewById(R.id.home_selection_done);
- if (selectionDone != null) {
- selectionDone.setOnClickListener(v -> {
+ mHomeSelectionDone = rootView.findViewById(R.id.home_selection_done);
+ if (mHomeSelectionDone != null) {
+ mHomeSelectionDone.setOnClickListener(v -> {
if (getActivity() instanceof AbstractConversationListActivity) {
((AbstractConversationListActivity) getActivity()).exitMultiSelectState();
}
});
}
- if (mHomeReadAllButton != null) {
- mHomeReadAllButton.setOnClickListener(v -> onHomeReadAllOrUnreadClick());
+ if (mHomeSelectionUnread != null) {
+ mHomeSelectionUnread.setOnClickListener(v -> onHomeReadAllOrUnreadClick());
+ }
+ if (mHomeSelectionSpam != null) {
+ mHomeSelectionSpam.setOnClickListener(v -> {
+ if (mHomeSelectionCount > 0
+ && getActivity() instanceof AbstractConversationListActivity) {
+ ((AbstractConversationListActivity) getActivity())
+ .reportSelectedConversationsSpam();
+ }
+ });
+ }
+ if (mHomeSelectionArchive != null) {
+ mHomeSelectionArchive.setOnClickListener(v -> {
+ if (mHomeSelectionCount > 0
+ && getActivity() instanceof AbstractConversationListActivity) {
+ ((AbstractConversationListActivity) getActivity())
+ .archiveSelectedConversations();
+ }
+ });
}
if (mHomeSelectionDelete != null) {
mHomeSelectionDelete.setOnClickListener(v -> {
@@ -915,7 +1048,7 @@ public class ConversationListFragment extends Fragment implements ConversationLi
private void showHomeListProfilePopup(final ConversationListItemData data,
final View anchorView) {
- if (mHomeListProfilePopup == null || data == null || mArchiveMode || mForwardMessageMode
+ if (mHomeListProfilePopup == null || data == null || isSubList()
|| isSelectionMode()) {
return;
}
@@ -1230,7 +1363,20 @@ public class ConversationListFragment extends Fragment implements ConversationLi
}
public void setHomeSelectionMode(final boolean enabled) {
- if (mArchiveMode || mForwardMessageMode || mRootView == null) {
+ if (mRootView == null) {
+ return;
+ }
+ if (isSubList()) {
+ if (getActivity() instanceof AbstractConversationListActivity) {
+ ((AbstractConversationListActivity) getActivity()).onSubListSelectionMode(enabled);
+ }
+ mRootView.post(() -> {
+ if (!isAdded()) {
+ return;
+ }
+ applySubListSelectPadding(enabled);
+ refreshListSelectionChrome();
+ });
return;
}
if (enabled) {
@@ -1244,9 +1390,20 @@ public class ConversationListFragment extends Fragment implements ConversationLi
HomeUiMotion.replaceHeaderInstant(
enabled ? mHomeHeader : mHomeSelectionHeader,
enabled ? mHomeSelectionHeader : mHomeHeader);
+ if (mHomeSelectionDone != null) {
+ if (enabled) {
+ HomeUiMotion.swapOverlayChip(null, mHomeSelectionDone);
+ } else {
+ mHomeSelectionDone.setScaleX(1f);
+ mHomeSelectionDone.setScaleY(1f);
+ mHomeSelectionDone.setAlpha(1f);
+ }
+ }
refreshListSelectionChrome();
HomeUiMotion.swapBottomDock(mHomeBottomNav, mHomeSelectionBar, enabled, () -> {
- if (!enabled) {
+ if (enabled) {
+ applyHomeSelectionDockMetrics();
+ } else {
updateHomeMainNavVisibility(false);
}
});
@@ -1261,7 +1418,7 @@ public class ConversationListFragment extends Fragment implements ConversationLi
}
private void selectHomeTab(final int tab) {
- if (mArchiveMode || mForwardMessageMode || mRootView == null) {
+ if (isSubList() || mRootView == null) {
return;
}
if (isSelectionMode() && tab != HOME_TAB_MESSAGES) {
@@ -1283,8 +1440,8 @@ public class ConversationListFragment extends Fragment implements ConversationLi
} else {
setCategoryStripShown(false, false);
}
- final CharSequence title = getString(showMessages
- ? R.string.home_title : R.string.home_automation_title);
+ final CharSequence title = showMessages
+ ? homeMessagesTitle() : getString(R.string.home_automation_title);
if (mHomeTitle != null) {
if (animate) {
HomeUiMotion.fadeSwapText(mHomeTitle, title);
@@ -1343,7 +1500,7 @@ public class ConversationListFragment extends Fragment implements ConversationLi
* action mode alone
*/
private void updateHomeMainNavVisibility(final boolean homeSelectionChromeActive) {
- if (mHomeBottomNav == null || mArchiveMode || mForwardMessageMode) {
+ if (mHomeBottomNav == null || isSubList()) {
return;
}
final boolean hideForSelection = homeSelectionChromeActive || isSelectionMode();
@@ -1360,7 +1517,7 @@ public class ConversationListFragment extends Fragment implements ConversationLi
}
private void updateHomeComposeButtonVisibility(final boolean animate) {
- if (mArchiveMode || mForwardMessageMode) {
+ if (isSubList()) {
return;
}
final boolean showDock = isResumed() && !isSelectionMode();
@@ -1447,16 +1604,29 @@ public class ConversationListFragment extends Fragment implements ConversationLi
}
public void updateHomeSelectionBar(final int selectedCount) {
+ if (isSubList()) {
+ if (getActivity() instanceof AbstractConversationListActivity) {
+ ((AbstractConversationListActivity) getActivity())
+ .onSubListSelectionCount(selectedCount);
+ }
+ return;
+ }
mHomeSelectionCount = selectedCount;
- if (mHomeReadAllButton != null) {
- mHomeReadAllButton.setText(selectedCount > 0
+ if (mHomeSelectionUnreadLabel != null) {
+ mHomeSelectionUnreadLabel.setText(selectedCount > 0
? R.string.home_mark_unread
: R.string.home_read_all);
+ mHomeSelectionUnreadNaturalWidth = 0;
+ if (mHomeSelectionUnread != null) {
+ mHomeSelectionUnread.post(this::applyHomeSelectionDockMetrics);
+ }
}
+ final boolean enabled = selectedCount > 0;
+ applyHomeSelectionTabEnabled(mHomeSelectionSpam, enabled);
+ applyHomeSelectionTabEnabled(mHomeSelectionArchive, enabled);
if (mHomeSelectionDelete == null) {
return;
}
- final boolean enabled = selectedCount > 0;
mHomeSelectionDelete.setEnabled(enabled);
mHomeSelectionDelete.setAlpha(1f);
mHomeSelectionDelete.setImageAlpha(enabled ? 255 : 102);
@@ -1466,6 +1636,49 @@ public class ConversationListFragment extends Fragment implements ConversationLi
android.content.res.ColorStateList.valueOf(tintColor));
}
+ private void bindHomeSelectionTab(final View tab, final int iconRes, final int labelRes,
+ final boolean selected) {
+ if (tab == null) {
+ return;
+ }
+ final ImageView icon = tab.findViewById(R.id.home_nav_tab_icon);
+ final TextView label = tab.findViewById(R.id.home_nav_tab_label);
+ if (icon != null) {
+ icon.setImageResource(iconRes);
+ }
+ if (label != null) {
+ label.setText(labelRes);
+ }
+ applyHomeSelectionTabStyle(tab, selected);
+ }
+
+ private void applyHomeSelectionTabStyle(final View tab, final boolean selected) {
+ if (tab == null) {
+ return;
+ }
+ tab.setBackgroundResource(android.R.color.transparent);
+ final int color = ContextCompat.getColor(requireContext(),
+ selected ? R.color.home_nav_active_content : R.color.home_nav_inactive);
+ final ImageView icon = tab.findViewById(R.id.home_nav_tab_icon);
+ final TextView label = tab.findViewById(R.id.home_nav_tab_label);
+ if (icon != null) {
+ icon.setImageTintList(android.content.res.ColorStateList.valueOf(color));
+ }
+ if (label != null) {
+ label.setTextColor(color);
+ label.setTypeface(null, selected ? android.graphics.Typeface.BOLD
+ : android.graphics.Typeface.NORMAL);
+ }
+ }
+
+ private void applyHomeSelectionTabEnabled(final View tab, final boolean enabled) {
+ if (tab == null) {
+ return;
+ }
+ tab.setEnabled(enabled);
+ tab.setAlpha(enabled ? 1f : 0.4f);
+ }
+
private void onHomeReadAllOrUnreadClick() {
if (mHomeSelectionCount > 0
&& getActivity() instanceof AbstractConversationListActivity) {
@@ -1475,6 +1688,30 @@ public class ConversationListFragment extends Fragment implements ConversationLi
markAllVisibleConversationsRead();
}
+ private void applySubListSelectPadding(final boolean enabled) {
+ if (mRecyclerView == null || !isSubList()) {
+ return;
+ }
+ mRecyclerView.setPadding(
+ mRecyclerView.getPaddingLeft(),
+ mRecyclerView.getPaddingTop(),
+ mRecyclerView.getPaddingRight(),
+ getResources().getDimensionPixelSize(enabled
+ ? R.dimen.home_sub_select_list_padding
+ : R.dimen.looply_space_lg));
+ }
+
+ public List snapshotVisibleConversations() {
+ if (mAdapter == null) {
+ return java.util.Collections.emptyList();
+ }
+ return mAdapter.snapshotVisibleConversations();
+ }
+
+ public int getVisibleConversationCount() {
+ return mAdapter == null ? 0 : mAdapter.getConversationItemCount();
+ }
+
private void markAllVisibleConversationsRead() {
mAdapter.forEachVisibleConversationId(MarkAsReadAction::markAsRead);
}
@@ -1596,6 +1833,39 @@ public class ConversationListFragment extends Fragment implements ConversationLi
final boolean muted = MutedConversationStore.isMuted(requireContext(),
data.getConversationId());
final boolean read = data.getIsRead();
+ final View restoreRow = content.findViewById(R.id.popover_conversation_restore);
+ if (restoreRow != null) {
+ restoreRow.setVisibility((mArchiveMode || mTrashMode) ? View.VISIBLE : View.GONE);
+ restoreRow.setOnClickListener(v -> {
+ dismissHomePopover();
+ if (mTrashMode) {
+ UpdateConversationTrashStatusAction.restoreFromTrash(data.getConversationId());
+ HomeToast.show(requireContext(), R.string.home_trash_restored_toast);
+ } else {
+ UpdateConversationArchiveStatusAction.unarchiveConversation(
+ data.getConversationId());
+ HomeToast.show(requireContext(), R.string.home_profile_unarchived_toast);
+ }
+ });
+ }
+ final View pinRow = content.findViewById(R.id.popover_conversation_pin);
+ if (pinRow != null) {
+ pinRow.setVisibility(isSubList() ? View.GONE : View.VISIBLE);
+ }
+ final View categoryRow = content.findViewById(R.id.popover_conversation_category);
+ if (categoryRow != null) {
+ categoryRow.setVisibility(isSubList() ? View.GONE : View.VISIBLE);
+ }
+ final View archiveRow = content.findViewById(R.id.popover_conversation_archive);
+ if (archiveRow != null) {
+ archiveRow.setVisibility((mArchiveMode || mTrashMode) ? View.GONE : View.VISIBLE);
+ }
+ final View spamRow = content.findViewById(R.id.popover_conversation_spam);
+ if (spamRow != null) {
+ final boolean canReportSpam = !mTrashMode && !data.getIsGroup()
+ && !TextUtils.isEmpty(data.getOtherParticipantNormalizedDestination());
+ spamRow.setVisibility(canReportSpam ? View.VISIBLE : View.GONE);
+ }
final TextView pinLabel = content.findViewById(R.id.popover_conversation_pin_label);
if (pinLabel != null) {
@@ -1621,6 +1891,10 @@ public class ConversationListFragment extends Fragment implements ConversationLi
? R.drawable.ic_home_bell
: R.drawable.ic_home_bell_off);
}
+ final View muteRow = content.findViewById(R.id.popover_conversation_mute);
+ if (muteRow != null) {
+ muteRow.setVisibility(mTrashMode ? View.GONE : View.VISIBLE);
+ }
content.findViewById(R.id.popover_conversation_pin).setOnClickListener(v -> {
dismissHomePopover();
@@ -1650,6 +1924,14 @@ public class ConversationListFragment extends Fragment implements ConversationLi
dismissHomePopover();
showAssignHomeCategory(data);
});
+ content.findViewById(R.id.popover_conversation_archive).setOnClickListener(v -> {
+ dismissHomePopover();
+ archiveConversationFromPopover(data);
+ });
+ content.findViewById(R.id.popover_conversation_spam).setOnClickListener(v -> {
+ dismissHomePopover();
+ confirmReportConversationSpam(data);
+ });
content.findViewById(R.id.popover_conversation_delete).setOnClickListener(v -> {
dismissHomePopover();
confirmDeleteConversation(data);
@@ -1664,6 +1946,14 @@ public class ConversationListFragment extends Fragment implements ConversationLi
HomeToast.show(requireContext(), muted
? R.string.home_conversation_muted_toast
: R.string.home_conversation_unmuted_toast);
+ if (mAdapter != null) {
+ final int adapterPosition =
+ mAdapter.getAdapterPositionForConversationId(data.getConversationId());
+ if (adapterPosition >= 0) {
+ mAdapter.notifyItemChanged(adapterPosition);
+ }
+ }
+ UpdateMessageNotificationAction.updateMessageNotification();
}
private void confirmDeleteConversation(final ConversationListItemData data) {
@@ -1672,6 +1962,37 @@ public class ConversationListFragment extends Fragment implements ConversationLi
}
}
+ private void archiveConversationFromPopover(final ConversationListItemData data) {
+ if (data.getIsArchived()) {
+ UpdateConversationArchiveStatusAction.unarchiveConversation(data.getConversationId());
+ HomeToast.show(requireContext(), R.string.home_profile_unarchived_toast);
+ return;
+ }
+ UpdateConversationArchiveStatusAction.archiveConversation(data.getConversationId());
+ HomeToast.show(requireContext(), R.string.home_profile_archived_toast);
+ }
+
+ private void confirmReportConversationSpam(final ConversationListItemData data) {
+ if (data.getIsGroup()
+ || TextUtils.isEmpty(data.getOtherParticipantNormalizedDestination())) {
+ HomeToast.show(requireContext(), R.string.home_report_spam_groups_only);
+ return;
+ }
+ HomeModal.builder(requireContext())
+ .icon(R.drawable.ic_home_menu_spam)
+ .title(R.string.home_report_spam_title)
+ .message(R.string.home_report_spam_message_one)
+ .negative(android.R.string.cancel, null)
+ .positive(R.string.home_unknown_sender_report_spam, () -> {
+ UpdateDestinationBlockedAction.updateDestinationBlocked(
+ data.getOtherParticipantNormalizedDestination(), true,
+ data.getConversationId(),
+ (action, success, block, destination) -> { });
+ HomeToast.show(requireContext(), R.string.home_report_spam_toast_one);
+ })
+ .show();
+ }
+
private static final int PREVIEW_MESSAGE_LIMIT = 3;
private static final class PreviewMessage {
@@ -1813,6 +2134,16 @@ public class ConversationListFragment extends Fragment implements ConversationLi
}
host.addView(content, new FrameLayout.LayoutParams(
ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT));
+ content.setOnTouchListener((v, event) -> {
+ if (event.getActionMasked() != MotionEvent.ACTION_DOWN) {
+ return false;
+ }
+ if (isPeekTouchOnCard(content, event)) {
+ return false;
+ }
+ dismissHomePopover();
+ return true;
+ });
final PopupWindow popup = new PopupWindow(host,
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.MATCH_PARENT,
@@ -1918,16 +2249,38 @@ public class ConversationListFragment extends Fragment implements ConversationLi
content.setY(y);
}
+ private static boolean isPeekTouchOnCard(final View content, final MotionEvent event) {
+ final View preview = content.findViewById(R.id.popover_preview_card);
+ final View menu = content.findViewById(R.id.popover_menu_card);
+ if (preview != null || menu != null) {
+ return isEventInside(preview, event) || isEventInside(menu, event);
+ }
+ return isEventInside(content, event);
+ }
+
+ private static boolean isEventInside(final View view, final MotionEvent event) {
+ if (view == null || view.getVisibility() != View.VISIBLE) {
+ return false;
+ }
+ final int[] loc = new int[2];
+ view.getLocationOnScreen(loc);
+ final float x = event.getRawX();
+ final float y = event.getRawY();
+ return x >= loc[0] && x < loc[0] + view.getWidth()
+ && y >= loc[1] && y < loc[1] + view.getHeight();
+ }
+
private PopupWindow showHomePopover(final View anchor, final int layoutResId,
final boolean alignEnd) {
dismissHomePopover();
final View content = LayoutInflater.from(requireContext())
.inflate(layoutResId, null, false);
+ final int width = getResources().getDimensionPixelSize(R.dimen.home_popover_width);
content.measure(
- View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED),
+ View.MeasureSpec.makeMeasureSpec(width, View.MeasureSpec.EXACTLY),
View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED));
final PopupWindow popup = new PopupWindow(content,
- content.getMeasuredWidth(),
+ width,
ViewGroup.LayoutParams.WRAP_CONTENT,
true);
popup.setOutsideTouchable(true);
@@ -1954,9 +2307,9 @@ public class ConversationListFragment extends Fragment implements ConversationLi
windowRight = getResources().getDisplayMetrics().widthPixels;
}
int xOff = alignEnd
- ? windowRight - endInset - content.getMeasuredWidth() - anchorLoc[0]
+ ? windowRight - endInset - width - anchorLoc[0]
: 0;
- final int overflow = (anchorLoc[0] + xOff + content.getMeasuredWidth())
+ final int overflow = (anchorLoc[0] + xOff + width)
- (windowRight - endInset);
if (overflow > 0) {
xOff -= overflow;
@@ -1970,6 +2323,8 @@ public class ConversationListFragment extends Fragment implements ConversationLi
private void showHomeFilterPopover(final View anchor) {
final PopupWindow popup = showHomePopover(anchor, R.layout.home_popover_filter, true);
final View content = popup.getContentView();
+ bindHomeFilterPopoverSims(content);
+ bindHomeFilterPopoverChecks(content);
content.findViewById(R.id.popover_messages).setOnClickListener(v -> {
selectHomeChip(ConversationListAdapter.FILTER_ALL);
dismissHomePopover();
@@ -1978,21 +2333,26 @@ public class ConversationListFragment extends Fragment implements ConversationLi
dismissHomePopover();
launchHomeSubScreen(() -> UIIntents.get().launchHomeSpamActivity(requireActivity()));
});
- content.findViewById(R.id.popover_recently_deleted).setOnClickListener(v -> {
+ content.findViewById(R.id.popover_archive).setOnClickListener(v -> {
dismissHomePopover();
launchHomeSubScreen(() -> UIIntents.get().launchArchivedConversationsActivity(
requireActivity()));
});
+ content.findViewById(R.id.popover_recently_deleted).setOnClickListener(v -> {
+ dismissHomePopover();
+ launchHomeSubScreen(() -> UIIntents.get().launchRecentlyDeletedConversationsActivity(
+ requireActivity()));
+ });
content.findViewById(R.id.popover_unread).setOnClickListener(v -> {
selectHomeChip(ConversationListAdapter.FILTER_UNREAD);
dismissHomePopover();
});
content.findViewById(R.id.popover_work).setOnClickListener(v -> {
+ selectHomeChip(ConversationListAdapter.FILTER_WORK);
dismissHomePopover();
- HomeToast.show(requireContext(), R.string.home_popover_coming_soon);
});
content.findViewById(R.id.popover_personal).setOnClickListener(v -> {
- selectHomeChip(ConversationListAdapter.FILTER_ALL);
+ selectHomeChip(ConversationListAdapter.FILTER_PERSONAL);
dismissHomePopover();
});
content.findViewById(R.id.popover_share_app).setOnClickListener(v -> {
@@ -2009,6 +2369,54 @@ public class ConversationListFragment extends Fragment implements ConversationLi
});
}
+ private void bindHomeFilterPopoverSims(final View content) {
+ final Context context = content.getContext();
+ final SubscriptionInfo work = HomeInboxFilter.workSubscription(context);
+ final SubscriptionInfo personal = HomeInboxFilter.personalSubscription(context);
+ final boolean distinctWork = work != null && personal != null
+ && work.getSubscriptionId() != personal.getSubscriptionId();
+ final View workRow = content.findViewById(R.id.popover_work);
+ if (workRow != null) {
+ workRow.setVisibility(distinctWork ? View.VISIBLE : View.GONE);
+ }
+ setHomeFilterText(content, R.id.popover_work_badge, HomeInboxFilter.workSimName(context));
+ setHomeFilterText(content, R.id.popover_work_label, HomeInboxFilter.workSimName(context));
+ setHomeFilterText(content, R.id.popover_personal_badge,
+ HomeInboxFilter.personalSimName(context));
+ setHomeFilterText(content, R.id.popover_personal_label,
+ HomeInboxFilter.personalSimName(context));
+ }
+
+ private static void setHomeFilterText(final View content, final int id, final String text) {
+ final View view = content.findViewById(id);
+ if (view instanceof TextView) {
+ ((TextView) view).setText(text);
+ }
+ }
+
+ private void bindHomeFilterPopoverChecks(final View content) {
+ final int mode = mAdapter == null
+ ? ConversationListAdapter.FILTER_ALL : mAdapter.getFilterMode();
+ setHomeFilterCheck(content, R.id.popover_messages_check,
+ mode == ConversationListAdapter.FILTER_ALL);
+ setHomeFilterCheck(content, R.id.popover_unread_check,
+ mode == ConversationListAdapter.FILTER_UNREAD);
+ setHomeFilterCheck(content, R.id.popover_work_check,
+ mode == ConversationListAdapter.FILTER_WORK);
+ setHomeFilterCheck(content, R.id.popover_personal_check,
+ mode == ConversationListAdapter.FILTER_PERSONAL);
+ setHomeFilterCheck(content, R.id.popover_spam_check, false);
+ setHomeFilterCheck(content, R.id.popover_archive_check, false);
+ setHomeFilterCheck(content, R.id.popover_recently_deleted_check, false);
+ }
+
+ private static void setHomeFilterCheck(final View content, final int id, final boolean on) {
+ final View check = content.findViewById(id);
+ if (check != null) {
+ check.setVisibility(on ? View.VISIBLE : View.INVISIBLE);
+ }
+ }
+
private void shareThisApp() {
final Context context = getContext();
if (context == null) {
@@ -2023,12 +2431,16 @@ public class ConversationListFragment extends Fragment implements ConversationLi
}
private void showHomeEditPopover(final View anchor) {
+ if (getVisibleConversationCount() == 0) {
+ HomeToast.show(requireContext(), R.string.home_toast_nothing_to_edit);
+ return;
+ }
final PopupWindow popup = showHomePopover(anchor, R.layout.home_popover_edit, true);
final View content = popup.getContentView();
content.findViewById(R.id.popover_select_messages).setOnClickListener(v -> {
dismissHomePopoverImmediate();
- if (getActivity() instanceof ConversationListActivity) {
- ((ConversationListActivity) getActivity()).enterHomeEditMode();
+ if (getActivity() instanceof AbstractConversationListActivity) {
+ ((AbstractConversationListActivity) getActivity()).enterHomeEditMode();
}
});
content.findViewById(R.id.popover_edit_pinned).setOnClickListener(v -> {
@@ -2037,13 +2449,42 @@ public class ConversationListFragment extends Fragment implements ConversationLi
}
private void selectHomeChip(final int filterMode) {
- mAdapter.setFilterMode(filterMode);
+ if (mHomeTab != HOME_TAB_MESSAGES) {
+ selectHomeTab(HOME_TAB_MESSAGES);
+ }
+ if (mAdapter != null) {
+ mAdapter.setFilterMode(filterMode);
+ }
+ updateHomeFilterTitle();
bindHomeCategoryStrip();
- updateEmptyListUi(mAdapter.getConversationItemCount() == 0);
+ updateEmptyListUi(mAdapter != null && mAdapter.getConversationItemCount() == 0);
+ }
+
+ private CharSequence homeMessagesTitle() {
+ if (mAdapter == null) {
+ return getString(R.string.home_title);
+ }
+ switch (mAdapter.getFilterMode()) {
+ case ConversationListAdapter.FILTER_UNREAD:
+ return getString(R.string.home_popover_unread);
+ case ConversationListAdapter.FILTER_WORK:
+ return HomeInboxFilter.workSimName(requireContext());
+ case ConversationListAdapter.FILTER_PERSONAL:
+ return HomeInboxFilter.personalSimName(requireContext());
+ default:
+ return getString(R.string.home_title);
+ }
+ }
+
+ private void updateHomeFilterTitle() {
+ if (mHomeTitle == null || mHomeTab != HOME_TAB_MESSAGES || isSubList()) {
+ return;
+ }
+ mHomeTitle.setText(homeMessagesTitle());
}
private void bindHomeCategoryStrip() {
- if (mHomeCategoryRow == null || mArchiveMode || mForwardMessageMode) {
+ if (mHomeCategoryRow == null || isSubList()) {
return;
}
final Context context = getContext();
@@ -2259,9 +2700,11 @@ public class ConversationListFragment extends Fragment implements ConversationLi
final Bundle arguments = getArguments();
if (arguments != null) {
mArchiveMode = arguments.getBoolean(BUNDLE_ARCHIVED_MODE, false);
+ mTrashMode = arguments.getBoolean(BUNDLE_TRASH_MODE, false);
mForwardMessageMode = arguments.getBoolean(BUNDLE_FORWARD_MESSAGE_MODE, false);
}
- mListBinding.bind(DataModel.get().createConversationListData(context, this, mArchiveMode));
+ mListBinding.bind(DataModel.get().createConversationListData(
+ context, this, mArchiveMode, mTrashMode));
}
@@ -2274,6 +2717,9 @@ public class ConversationListFragment extends Fragment implements ConversationLi
outState.putBoolean(SAVED_INSTANCE_STATE_SEARCH_BAR_EXPANDED, mSearchBarShown);
outState.putInt(SAVED_INSTANCE_STATE_HOME_TAB, mHomeTab);
outState.putBoolean(SAVED_INSTANCE_STATE_PINNED_EDIT, mHomePinnedEditMode);
+ if (mAdapter != null) {
+ outState.putInt(SAVED_INSTANCE_STATE_FILTER, mAdapter.getFilterMode());
+ }
}
@Override
@@ -2281,7 +2727,7 @@ public class ConversationListFragment extends Fragment implements ConversationLi
super.onPause();
dismissHomeListProfilePopup(false);
dismissHomePopover();
- if (mHomeBottomNav != null && !mArchiveMode && !mForwardMessageMode) {
+ if (mHomeBottomNav != null && !isSubList()) {
mHomeBottomNav.setVisibility(View.GONE);
}
mListState = mRecyclerView.getLayoutManager().onSaveInstanceState();
@@ -2392,7 +2838,7 @@ public class ConversationListFragment extends Fragment implements ConversationLi
@Override
public boolean shouldShowConversationPinButton() {
- return mHomePinnedEditMode && !mArchiveMode && !mForwardMessageMode
+ return mHomePinnedEditMode && !isSubList()
&& mHomeTab == HOME_TAB_MESSAGES;
}
@@ -2434,10 +2880,30 @@ public class ConversationListFragment extends Fragment implements ConversationLi
mEmptyListMessageView.setContent(R.drawable.ic_home_nav_messages,
R.string.home_empty_messages_sync_title,
R.string.home_empty_messages_sync_body);
+ } else if (mAdapter.hasSearchQuery()) {
+ mEmptyListMessageView.setContent(R.drawable.ic_home_search_line,
+ getString(R.string.home_empty_sub_search_title),
+ getString(R.string.home_empty_sub_search_body, mAdapter.getSearchQuery()));
} else if (mArchiveMode) {
mEmptyListMessageView.setContent(R.drawable.ic_home_menu_archive,
R.string.home_empty_archive_title,
R.string.home_empty_archive_body);
+ } else if (mTrashMode) {
+ mEmptyListMessageView.setContent(R.drawable.ic_home_menu_trash,
+ R.string.home_empty_trash_title,
+ R.string.home_empty_trash_body);
+ } else if (mAdapter.getFilterMode() == ConversationListAdapter.FILTER_UNREAD) {
+ mEmptyListMessageView.setContent(R.drawable.ic_home_menu_unread,
+ R.string.home_empty_unread_title,
+ R.string.home_empty_unread_body);
+ } else if (mAdapter.getFilterMode() == ConversationListAdapter.FILTER_WORK) {
+ mEmptyListMessageView.setContent(R.drawable.ic_home_nav_messages,
+ R.string.home_empty_work_title,
+ R.string.home_empty_work_body);
+ } else if (mAdapter.getFilterMode() == ConversationListAdapter.FILTER_PERSONAL) {
+ mEmptyListMessageView.setContent(R.drawable.ic_home_nav_messages,
+ R.string.home_empty_personal_title,
+ R.string.home_empty_personal_body);
} else if (!HomeCategoryStore.ID_ALL.equals(mAdapter.getCategoryId())
|| mAdapter.getFilterMode() != ConversationListAdapter.FILTER_ALL) {
mEmptyListMessageView.setContent(R.drawable.ic_home_nav_messages,
@@ -2448,7 +2914,7 @@ public class ConversationListFragment extends Fragment implements ConversationLi
R.string.home_empty_messages_title,
R.string.home_empty_messages_body);
}
- mEmptyListMessageView.setReserveDock(!mArchiveMode && !mForwardMessageMode);
+ mEmptyListMessageView.setReserveDock(!isSubList());
mEmptyListMessageView.setVisibility(View.VISIBLE);
} else {
mEmptyListMessageView.setVisibility(View.GONE);
@@ -2527,7 +2993,7 @@ public class ConversationListFragment extends Fragment implements ConversationLi
}
private void setPinnedEditMode(final boolean enabled, final boolean refreshList) {
- if (mArchiveMode || mForwardMessageMode) {
+ if (isSubList()) {
return;
}
if (enabled) {
@@ -2570,23 +3036,21 @@ public class ConversationListFragment extends Fragment implements ConversationLi
return;
}
if (mHomePinnedEditMode) {
- mHomeEditButton.setVisibility(View.GONE);
+ HomeUiMotion.swapOverlayChip(mHomeEditButton, mHomePinnedDoneButton);
if (mHomePinnedDoneButton != null) {
- mHomePinnedDoneButton.setVisibility(View.VISIBLE);
mHomePinnedDoneButton.setOnClickListener(v -> setPinnedEditMode(false));
}
} else {
- mHomeEditButton.setVisibility(View.VISIBLE);
- mHomeEditButton.setText(R.string.home_edit);
- mHomeEditButton.setOnClickListener(this::showHomeEditPopover);
- if (mHomePinnedDoneButton != null) {
- mHomePinnedDoneButton.setVisibility(View.GONE);
+ HomeUiMotion.swapOverlayChip(mHomePinnedDoneButton, mHomeEditButton);
+ if (mHomeEditButton != null) {
+ mHomeEditButton.setText(R.string.home_edit);
+ mHomeEditButton.setOnClickListener(this::showHomeEditPopover);
}
}
}
private void refreshPinnedStrip() {
- if (mHomePinnedStrip == null || mArchiveMode || mForwardMessageMode
+ if (mHomePinnedStrip == null || isSubList()
|| mHomeTab != HOME_TAB_MESSAGES) {
mAdapter.setPinnedHeaderVisible(false);
return;
diff --git a/app/src/main/java/com/android/messaging/ui/conversationlist/ConversationListItemView.java b/app/src/main/java/com/android/messaging/ui/conversationlist/ConversationListItemView.java
index 4f92458..7fdbcf0 100644
--- a/app/src/main/java/com/android/messaging/ui/conversationlist/ConversationListItemView.java
+++ b/app/src/main/java/com/android/messaging/ui/conversationlist/ConversationListItemView.java
@@ -53,6 +53,7 @@ import com.android.messaging.Factory;
import com.android.messaging.R;
import com.android.messaging.datamodel.MessagingContentProvider;
import com.android.messaging.datamodel.action.UpdateConversationArchiveStatusAction;
+import com.android.messaging.datamodel.action.UpdateConversationTrashStatusAction;
import com.android.messaging.datamodel.data.ConversationListItemData;
import com.android.messaging.datamodel.data.ConversationMessageData;
import com.android.messaging.datamodel.data.MessageData;
@@ -63,6 +64,8 @@ import com.android.messaging.ui.AudioAttachmentView;
import com.android.messaging.ui.ContactIconView;
import com.android.messaging.ui.SnackBar;
import com.android.messaging.ui.SnackBarInteraction;
+import com.android.messaging.ui.home.HomeToast;
+import com.android.messaging.ui.home.MutedConversationStore;
import com.android.messaging.util.Assert;
import com.android.messaging.util.ImageUtils;
import com.android.messaging.util.PhoneUtils;
@@ -155,6 +158,7 @@ public class ConversationListItemView extends FrameLayout implements OnClickList
private ContactIconView mContactIconView;
private ImageView mContactCheckmarkView;
private ImageView mFailedStatusIconView;
+ private ImageView mMuteBadgeView;
private ImageView mCrossSwipeArchiveLeftImageView;
private View mCrossSwipeRightActions;
private ImageView mCrossSwipeMuteView;
@@ -196,6 +200,7 @@ public class ConversationListItemView extends FrameLayout implements OnClickList
mContactIconView = findViewById(R.id.conversation_icon);
mContactCheckmarkView = findViewById(R.id.conversation_checkmark);
mFailedStatusIconView = findViewById(R.id.conversation_failed_status_icon);
+ mMuteBadgeView = findViewById(R.id.conversation_mute_badge);
mCrossSwipeArchiveLeftImageView = findViewById(R.id.crossSwipeArchiveIconLeft);
mCrossSwipeRightActions = findViewById(R.id.cross_swipe_right_actions);
mCrossSwipeMuteView = findViewById(R.id.cross_swipe_mute);
@@ -813,6 +818,15 @@ public class ConversationListItemView extends FrameLayout implements OnClickList
mContactCheckmarkView.setVisibility(checkmarkVisiblity);
mFailedStatusIconView.setVisibility(failStatusVisiblity);
+ final boolean muted = MutedConversationStore.isMuted(getContext(),
+ mData.getConversationId());
+ final boolean trashed = mData.getIsTrashed();
+ if (mMuteBadgeView != null) {
+ mMuteBadgeView.setVisibility(
+ muted && !trashed && failStatusVisiblity != VISIBLE && !isSelected
+ ? VISIBLE : GONE);
+ }
+ updateSwipeMuteButton(muted, trashed);
updatePinButton(selectionMode, resources);
updateListDividerInset(selectionMode);
@@ -931,6 +945,31 @@ public class ConversationListItemView extends FrameLayout implements OnClickList
&& screenY <= actionLoc[1] + mCrossSwipeRightActions.getHeight();
}
+ private void updateSwipeMuteButton(final boolean muted, final boolean trashed) {
+ if (mCrossSwipeMuteView == null) {
+ return;
+ }
+ mCrossSwipeMuteView.setVisibility(trashed ? GONE : VISIBLE);
+ if (mCrossSwipeDeleteView != null) {
+ final ViewGroup.MarginLayoutParams lp =
+ (ViewGroup.MarginLayoutParams) mCrossSwipeDeleteView.getLayoutParams();
+ if (lp != null) {
+ lp.setMarginStart(trashed ? 0
+ : getResources().getDimensionPixelSize(R.dimen.looply_space_sm));
+ mCrossSwipeDeleteView.setLayoutParams(lp);
+ }
+ }
+ if (trashed) {
+ return;
+ }
+ mCrossSwipeMuteView.setImageResource(muted
+ ? R.drawable.ic_home_bell
+ : R.drawable.ic_home_bell_off);
+ mCrossSwipeMuteView.setContentDescription(getResources().getString(muted
+ ? R.string.home_swipe_unmute
+ : R.string.home_swipe_mute));
+ }
+
public void setSwipeTranslationX(final float translationX) {
mSwipeableContainer.setTranslationX(translationX);
if (translationX == 0) {
@@ -967,9 +1006,24 @@ public class ConversationListItemView extends FrameLayout implements OnClickList
final String conversationId = mData.getConversationId();
if (PrefsUtils.isSwipeRightToDeleteEnabled()
&& swipeDirection == ConversationListSwipeHelper.SWIPE_DIRECTION_RIGHT) {
+ final boolean alreadyTrashed = mData.getIsTrashed();
mData.deleteConversation();
- UiUtils.showSnackBar(getContext(), getRootView(),
- getResources().getString(R.string.conversation_deleted));
+ if (alreadyTrashed) {
+ UiUtils.showSnackBar(getContext(), getRootView(),
+ getResources().getString(R.string.conversation_deleted));
+ } else {
+ HomeToast.show(getContext(), R.string.home_trash_moved_toast_one);
+ }
+ return;
+ }
+ if (mData.getIsTrashed()) {
+ UpdateConversationTrashStatusAction.restoreFromTrash(conversationId);
+ HomeToast.show(getContext(), R.string.home_trash_restored_toast);
+ return;
+ }
+ if (mData.getIsArchived()) {
+ UpdateConversationArchiveStatusAction.unarchiveConversation(conversationId);
+ HomeToast.show(getContext(), R.string.home_profile_unarchived_toast);
return;
}
UpdateConversationArchiveStatusAction.archiveConversation(conversationId);
diff --git a/app/src/main/java/com/android/messaging/ui/conversationlist/RecentlyDeletedConversationListActivity.java b/app/src/main/java/com/android/messaging/ui/conversationlist/RecentlyDeletedConversationListActivity.java
new file mode 100644
index 0000000..921ec48
--- /dev/null
+++ b/app/src/main/java/com/android/messaging/ui/conversationlist/RecentlyDeletedConversationListActivity.java
@@ -0,0 +1,105 @@
+/*
+ * Copyright (C) 2015 The Android Open Source Project
+ * Copyright (C) 2024-2025 The LineageOS Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.android.messaging.ui.conversationlist;
+
+import android.os.Bundle;
+import android.view.MenuItem;
+
+import androidx.appcompat.app.ActionBar;
+
+import com.android.messaging.R;
+import com.android.messaging.ui.home.HomeInboxMenu;
+import com.android.messaging.ui.home.HomeSubHeader;
+
+public class RecentlyDeletedConversationListActivity extends AbstractConversationListActivity {
+
+ @Override
+ protected void onCreate(final Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.home_archived_activity);
+ if (getSupportActionBar() != null) {
+ getSupportActionBar().hide();
+ }
+ HomeSubHeader.bind(this, R.string.home_recently_deleted_title, v -> onActionBarHome());
+ HomeInboxMenu.bind(this, HomeInboxMenu.Screen.TRASH);
+
+ if (savedInstanceState == null) {
+ mConversationListFragment =
+ ConversationListFragment.createTrashConversationListFragment();
+ getSupportFragmentManager()
+ .beginTransaction()
+ .replace(R.id.home_archived_list_host, mConversationListFragment)
+ .commit();
+ } else {
+ mConversationListFragment = (ConversationListFragment) getSupportFragmentManager()
+ .findFragmentById(R.id.home_archived_list_host);
+ }
+ }
+
+ @Override
+ protected boolean useHomeSelectionChrome() {
+ return true;
+ }
+
+ @Override
+ protected boolean isTrashList() {
+ return true;
+ }
+
+ @Override
+ protected void updateActionBar(final ActionBar actionBar) {
+ if (actionBar == null) {
+ return;
+ }
+ actionBar.hide();
+ super.updateActionBar(actionBar);
+ }
+
+ @Override
+ public void onBackPressed() {
+ if (isInConversationListSelectMode()) {
+ exitMultiSelectState();
+ } else {
+ super.onBackPressed();
+ }
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(final MenuItem menuItem) {
+ if (menuItem.getItemId() == android.R.id.home) {
+ onActionBarHome();
+ return true;
+ }
+ return super.onOptionsItemSelected(menuItem);
+ }
+
+ @Override
+ public void onActionBarHome() {
+ onBackPressed();
+ }
+
+ @Override
+ public boolean isSwipeAnimatable() {
+ return !isInConversationListSelectMode();
+ }
+
+ @Override
+ public void finish() {
+ super.finish();
+ overridePendingTransition(R.anim.home_activity_hold, R.anim.home_activity_close);
+ }
+}
diff --git a/app/src/main/java/com/android/messaging/ui/home/HomeAppLockActivity.java b/app/src/main/java/com/android/messaging/ui/home/HomeAppLockActivity.java
index 31cf14c..216c92a 100644
--- a/app/src/main/java/com/android/messaging/ui/home/HomeAppLockActivity.java
+++ b/app/src/main/java/com/android/messaging/ui/home/HomeAppLockActivity.java
@@ -5,6 +5,7 @@ import android.text.Editable;
import android.text.TextUtils;
import android.text.TextWatcher;
import android.text.method.PasswordTransformationMethod;
+import android.view.Gravity;
import android.view.KeyEvent;
import android.view.LayoutInflater;
import android.view.View;
@@ -13,6 +14,7 @@ import android.view.inputmethod.EditorInfo;
import android.widget.EditText;
import android.widget.FrameLayout;
import android.widget.ImageView;
+import android.widget.LinearLayout;
import android.widget.TextView;
import androidx.activity.OnBackPressedCallback;
@@ -70,9 +72,15 @@ public class HomeAppLockActivity extends BugleActionBarActivity {
if (isPinMode()) {
inflater.inflate(R.layout.home_lock_pin_body, body, true);
bindPinPad(body);
+ if (mMode != HomeAppLock.MODE_UNLOCK) {
+ relocateLockPrompt(body);
+ }
} else if (isPatternMode()) {
inflater.inflate(R.layout.home_lock_pattern_body, body, true);
bindPattern(body);
+ if (mMode != HomeAppLock.MODE_UNLOCK) {
+ relocateLockPrompt(body);
+ }
} else {
inflater.inflate(R.layout.home_lock_password_body, body, true);
bindPassword(body);
@@ -108,6 +116,35 @@ public class HomeAppLockActivity extends BugleActionBarActivity {
}
}
+ private void relocateLockPrompt(final View host) {
+ final ViewGroup prompt = host.findViewById(R.id.home_lock_prompt);
+ if (prompt == null) {
+ return;
+ }
+ if (mSubtitle != null) {
+ mSubtitle.setGravity(Gravity.CENTER);
+ mSubtitle.setTextAlignment(View.TEXT_ALIGNMENT_CENTER);
+ final LinearLayout.LayoutParams lp = new LinearLayout.LayoutParams(
+ ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT);
+ lp.bottomMargin = getResources().getDimensionPixelSize(R.dimen.looply_space_sm);
+ mSubtitle.setLayoutParams(lp);
+ moveChild(prompt, mSubtitle, 0);
+ }
+ if (mError != null) {
+ moveChild(prompt, mError, mSubtitle == null ? 0 : 1);
+ }
+ }
+
+ private static void moveChild(final ViewGroup parent, final View child, final int index) {
+ if (parent == null || child == null) {
+ return;
+ }
+ if (child.getParent() instanceof ViewGroup) {
+ ((ViewGroup) child.getParent()).removeView(child);
+ }
+ parent.addView(child, Math.min(index, parent.getChildCount()));
+ }
+
private void bindPinPad(final View host) {
mPinDots = host.findViewById(R.id.home_lock_pin_dots);
final ViewGroup keypad = host.findViewById(R.id.home_lock_pin_keypad);
diff --git a/app/src/main/java/com/android/messaging/ui/home/HomeDeleteAlertDialog.java b/app/src/main/java/com/android/messaging/ui/home/HomeDeleteAlertDialog.java
index 8cef22c..ad53a0f 100644
--- a/app/src/main/java/com/android/messaging/ui/home/HomeDeleteAlertDialog.java
+++ b/app/src/main/java/com/android/messaging/ui/home/HomeDeleteAlertDialog.java
@@ -17,25 +17,16 @@
package com.android.messaging.ui.home;
import android.content.Context;
-import android.graphics.drawable.ColorDrawable;
-import android.view.LayoutInflater;
-import android.view.View;
-import android.view.Window;
-import android.view.WindowManager;
-import android.widget.TextView;
import androidx.annotation.NonNull;
-import androidx.appcompat.app.AlertDialog;
import com.android.messaging.R;
-/** Centered iOS-style alert for delete / delete-and-register-spam from home selection mode. */
+/** Home modal for moving conversations to Trash from the conversation list. */
public final class HomeDeleteAlertDialog {
public interface Listener {
void onDelete();
-
- void onDeleteAndRegisterSpam();
}
private HomeDeleteAlertDialog() {
@@ -43,51 +34,17 @@ public final class HomeDeleteAlertDialog {
public static void show(@NonNull final Context context, final int conversationCount,
@NonNull final Listener listener) {
- final View content = LayoutInflater.from(context).inflate(
- R.layout.home_delete_alert_dialog, null);
-
- final TextView message = content.findViewById(R.id.home_delete_alert_message);
- if (message != null) {
- if (conversationCount == 1) {
- message.setText(R.string.home_delete_alert_message_one);
- } else {
- message.setText(context.getString(
- R.string.home_delete_alert_message_many, conversationCount));
- }
- }
-
- final AlertDialog dialog = new AlertDialog.Builder(context, R.style.Home_AlertDialog)
- .setView(content)
- .setCancelable(true)
- .create();
-
- final View delete = content.findViewById(R.id.home_delete_alert_delete);
- if (delete != null) {
- delete.setOnClickListener(v -> {
- dialog.dismiss();
- listener.onDelete();
- });
- }
-
- final View deleteSpam = content.findViewById(R.id.home_delete_alert_delete_spam);
- if (deleteSpam != null) {
- deleteSpam.setOnClickListener(v -> {
- dialog.dismiss();
- listener.onDeleteAndRegisterSpam();
- });
- }
-
- dialog.show();
-
- final Window window = dialog.getWindow();
- if (window != null) {
- window.setBackgroundDrawable(new ColorDrawable(android.graphics.Color.TRANSPARENT));
- final int margin = context.getResources().getDimensionPixelSize(
- R.dimen.home_alert_dialog_horizontal_margin);
- window.setLayout(
- WindowManager.LayoutParams.MATCH_PARENT,
- WindowManager.LayoutParams.WRAP_CONTENT);
- window.getDecorView().setPadding(margin, 0, margin, 0);
- }
+ final String title = context.getResources().getQuantityString(
+ R.plurals.delete_conversations_confirmation_dialog_title, conversationCount);
+ final String message = conversationCount == 1
+ ? context.getString(R.string.home_delete_alert_message_one)
+ : context.getString(R.string.home_delete_alert_message_many, conversationCount);
+ HomeModal.builder(context)
+ .icon(R.drawable.ic_home_menu_trash)
+ .title(title)
+ .message(message)
+ .positive(R.string.home_delete_confirm, listener::onDelete)
+ .negative(R.string.delete_conversation_decline_button, null)
+ .show();
}
}
diff --git a/app/src/main/java/com/android/messaging/ui/home/HomeInboxFilter.java b/app/src/main/java/com/android/messaging/ui/home/HomeInboxFilter.java
new file mode 100644
index 0000000..41f29a7
--- /dev/null
+++ b/app/src/main/java/com/android/messaging/ui/home/HomeInboxFilter.java
@@ -0,0 +1,139 @@
+package com.android.messaging.ui.home;
+
+import android.content.Context;
+import android.telephony.SubscriptionInfo;
+import android.text.TextUtils;
+
+import com.android.messaging.R;
+import com.android.messaging.datamodel.data.ConversationListItemData;
+import com.android.messaging.util.SimNameUtil;
+
+import java.util.List;
+import java.util.Locale;
+
+/** Work / Personal inbox filters: dual-SIM when present, otherwise people vs business. */
+public final class HomeInboxFilter {
+ private HomeInboxFilter() {
+ }
+
+ public static boolean matchesWork(final Context context, final ConversationListItemData item) {
+ if (item == null) {
+ return false;
+ }
+ if (item.isEnterprise()) {
+ return true;
+ }
+ final List infos = SimNameUtil.getActiveSubscriptions();
+ if (infos.size() >= 2) {
+ return isWorkSubscription(context, infos,
+ SimNameUtil.getSubscriptionForSelfId(item.getSelfId()));
+ }
+ return HomeCategoryClassifier.isPromotion(item);
+ }
+
+ public static boolean matchesPersonal(final Context context,
+ final ConversationListItemData item) {
+ if (item == null) {
+ return false;
+ }
+ if (item.isEnterprise()) {
+ return false;
+ }
+ final List infos = SimNameUtil.getActiveSubscriptions();
+ if (infos.size() >= 2) {
+ return !isWorkSubscription(context, infos,
+ SimNameUtil.getSubscriptionForSelfId(item.getSelfId()));
+ }
+ return HomeCategoryClassifier.isPersonal(item);
+ }
+
+ public static String workSimName(final Context context) {
+ return SimNameUtil.getDisplayName(context, workSubscription(context));
+ }
+
+ public static String personalSimName(final Context context) {
+ return SimNameUtil.getDisplayName(context, personalSubscription(context));
+ }
+
+ public static SubscriptionInfo workSubscription(final Context context) {
+ final List infos = SimNameUtil.getActiveSubscriptions();
+ if (infos.isEmpty()) {
+ return null;
+ }
+ for (final SubscriptionInfo info : infos) {
+ if (info != null && isWorkSubscription(context, infos, info)) {
+ return info;
+ }
+ }
+ return infos.get(infos.size() - 1);
+ }
+
+ public static SubscriptionInfo personalSubscription(final Context context) {
+ final List infos = SimNameUtil.getActiveSubscriptions();
+ if (infos.isEmpty()) {
+ return null;
+ }
+ for (final SubscriptionInfo info : infos) {
+ if (info != null && !isWorkSubscription(context, infos, info)) {
+ return info;
+ }
+ }
+ return infos.get(0);
+ }
+
+ private static boolean isWorkSubscription(final Context context,
+ final List infos, final SubscriptionInfo match) {
+ if (match == null || infos == null || infos.isEmpty()) {
+ return false;
+ }
+ final boolean namedWork = looksLike(context, match, true);
+ final boolean namedPersonal = looksLike(context, match, false);
+ if (namedWork != namedPersonal) {
+ return namedWork;
+ }
+ int minSlot = Integer.MAX_VALUE;
+ int maxSlot = Integer.MIN_VALUE;
+ for (final SubscriptionInfo info : infos) {
+ if (info == null) {
+ continue;
+ }
+ final int slot = info.getSimSlotIndex();
+ minSlot = Math.min(minSlot, slot);
+ maxSlot = Math.max(maxSlot, slot);
+ }
+ if (minSlot != maxSlot) {
+ return match.getSimSlotIndex() == maxSlot;
+ }
+ return infos.indexOf(match) == infos.size() - 1;
+ }
+
+ private static boolean looksLike(final Context context, final SubscriptionInfo info,
+ final boolean work) {
+ final CharSequence displayName = info.getDisplayName();
+ if (TextUtils.isEmpty(displayName)) {
+ return false;
+ }
+ final String name = displayName.toString().toLowerCase(Locale.ROOT);
+ if (work) {
+ final String label = context.getString(R.string.home_chip_work)
+ .toLowerCase(Locale.ROOT);
+ return containsToken(name, "work")
+ || containsToken(name, "office")
+ || name.contains("업무")
+ || name.contains("회사")
+ || (!TextUtils.isEmpty(label) && name.contains(label));
+ }
+ final String label = context.getString(R.string.home_chip_personal)
+ .toLowerCase(Locale.ROOT);
+ return containsToken(name, "personal")
+ || containsToken(name, "private")
+ || containsToken(name, "home")
+ || name.contains("개인")
+ || name.contains("집")
+ || (!TextUtils.isEmpty(label) && name.contains(label));
+ }
+
+ private static boolean containsToken(final String haystack, final String token) {
+ return haystack.equals(token) || haystack.contains(token);
+ }
+}
diff --git a/app/src/main/java/com/android/messaging/ui/home/HomeInboxMenu.java b/app/src/main/java/com/android/messaging/ui/home/HomeInboxMenu.java
new file mode 100644
index 0000000..9d8a1a4
--- /dev/null
+++ b/app/src/main/java/com/android/messaging/ui/home/HomeInboxMenu.java
@@ -0,0 +1,281 @@
+package com.android.messaging.ui.home;
+
+import android.app.Activity;
+import android.content.Context;
+import android.content.Intent;
+import android.graphics.drawable.ColorDrawable;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ImageView;
+import android.widget.PopupWindow;
+import android.widget.TextView;
+
+import com.android.messaging.R;
+import com.android.messaging.ui.UIIntents;
+import com.android.messaging.ui.conversationlist.AbstractConversationListActivity;
+
+/** Edit + filter chrome shared by home sub-lists (Spam / Archive / Trash). */
+public final class HomeInboxMenu {
+
+ public enum Screen {
+ MESSAGES,
+ SPAM,
+ ARCHIVE,
+ TRASH
+ }
+
+ private HomeInboxMenu() {
+ }
+
+ public static void bind(final Activity activity, final Screen screen) {
+ if (activity == null) {
+ return;
+ }
+ final View actions = activity.findViewById(R.id.home_sub_header_inbox_actions);
+ final View edit = activity.findViewById(R.id.home_sub_header_edit);
+ final View filter = activity.findViewById(R.id.home_sub_header_filter);
+ if (actions == null || filter == null) {
+ return;
+ }
+ actions.setVisibility(View.VISIBLE);
+ filter.setOnClickListener(v -> showFilter(activity, v, screen));
+ bindSelectAction(activity, v -> {
+ if (activity instanceof AbstractConversationListActivity) {
+ ((AbstractConversationListActivity) activity).onSubListPrimaryAction();
+ } else if (activity instanceof HomeSpamActivity) {
+ ((HomeSpamActivity) activity).onSelectPrimaryAction();
+ }
+ });
+ if (edit == null) {
+ return;
+ }
+ edit.setTag(null);
+ if (edit instanceof TextView) {
+ final TextView text = (TextView) edit;
+ text.setText(R.string.home_select);
+ text.setTextColor(activity.getColor(R.color.home_text_primary));
+ text.setCompoundDrawablesRelative(null, null, null, null);
+ text.setCompoundDrawablePadding(0);
+ }
+ edit.setVisibility(View.VISIBLE);
+ final View.OnClickListener toggleSelect = v -> {
+ if (activity instanceof AbstractConversationListActivity) {
+ final AbstractConversationListActivity list =
+ (AbstractConversationListActivity) activity;
+ if (list.isSelectionMode()) {
+ list.exitMultiSelectState();
+ } else {
+ list.enterHomeEditMode();
+ }
+ return;
+ }
+ if (activity instanceof HomeSpamActivity) {
+ ((HomeSpamActivity) activity).toggleSelectMode();
+ }
+ };
+ edit.setOnClickListener(toggleSelect);
+ final View done = activity.findViewById(R.id.home_sub_header_done);
+ if (done != null) {
+ done.setOnClickListener(toggleSelect);
+ }
+ }
+
+ public static void setSelecting(final Activity activity, final boolean selecting) {
+ final View actions = activity == null
+ ? null : activity.findViewById(R.id.home_sub_header_inbox_actions);
+ final View done = activity == null ? null : activity.findViewById(R.id.home_sub_header_done);
+ if (selecting) {
+ HomeUiMotion.swapOverlayChip(actions, done);
+ } else {
+ HomeUiMotion.swapOverlayChip(done, actions);
+ }
+ final View bar = activity == null ? null : activity.findViewById(R.id.home_sub_select_bar);
+ HomeUiMotion.swapSubListDock(bar, selecting);
+ }
+
+ public static void setSelectAction(final Activity activity, final int iconRes,
+ final int textResId, final boolean enabled, final boolean destructive) {
+ final View tab = activity == null
+ ? null : activity.findViewById(R.id.home_sub_select_action);
+ if (tab == null) {
+ return;
+ }
+ tab.setEnabled(enabled);
+ tab.setAlpha(enabled ? 1f : 0.4f);
+ final int color = activity.getColor(destructive
+ ? R.color.home_destructive
+ : R.color.home_nav_inactive);
+ final ImageView icon = tab.findViewById(R.id.home_nav_tab_icon);
+ if (icon != null) {
+ if (iconRes != 0) {
+ icon.setImageResource(iconRes);
+ }
+ icon.setImageTintList(android.content.res.ColorStateList.valueOf(color));
+ }
+ final TextView label = tab.findViewById(R.id.home_nav_tab_label);
+ if (label != null) {
+ label.setText(textResId);
+ label.setTextColor(color);
+ label.setTypeface(null, android.graphics.Typeface.NORMAL);
+ }
+ }
+
+ public static void bindSelectAction(final Activity activity,
+ final View.OnClickListener onAction) {
+ final View action = activity == null
+ ? null : activity.findViewById(R.id.home_sub_select_action);
+ if (action != null) {
+ action.setOnClickListener(onAction);
+ }
+ }
+
+ private static void showFilter(final Activity activity, final View anchor,
+ final Screen screen) {
+ final PopupWindow popup = showPopover(activity, anchor, R.layout.home_popover_filter);
+ final View content = popup.getContentView();
+ bindFilterSims(content);
+ bindFilterChecks(content, screen);
+ setGone(content, R.id.popover_unread);
+ setGone(content, R.id.popover_work);
+ setGone(content, R.id.popover_personal);
+ setGone(content, R.id.popover_filter_by_label);
+ setGone(content, R.id.popover_filter_by_divider);
+ content.findViewById(R.id.popover_messages).setOnClickListener(v -> {
+ popup.dismiss();
+ if (screen != Screen.MESSAGES) {
+ activity.finish();
+ }
+ });
+ content.findViewById(R.id.popover_spam).setOnClickListener(v -> {
+ popup.dismiss();
+ openScreen(activity, screen, Screen.SPAM,
+ () -> UIIntents.get().launchHomeSpamActivity(activity));
+ });
+ content.findViewById(R.id.popover_archive).setOnClickListener(v -> {
+ popup.dismiss();
+ openScreen(activity, screen, Screen.ARCHIVE,
+ () -> UIIntents.get().launchArchivedConversationsActivity(activity));
+ });
+ content.findViewById(R.id.popover_recently_deleted).setOnClickListener(v -> {
+ popup.dismiss();
+ openScreen(activity, screen, Screen.TRASH,
+ () -> UIIntents.get().launchRecentlyDeletedConversationsActivity(activity));
+ });
+ content.findViewById(R.id.popover_share_app).setOnClickListener(v -> {
+ popup.dismiss();
+ shareThisApp(activity);
+ });
+ content.findViewById(R.id.popover_settings).setOnClickListener(v -> {
+ popup.dismiss();
+ UIIntents.get().launchSettingsActivity(activity);
+ activity.overridePendingTransition(R.anim.home_activity_open,
+ R.anim.home_activity_hold);
+ });
+ }
+
+ private static void showEdit(final Activity activity, final View anchor) {
+ final PopupWindow popup = showPopover(activity, anchor, R.layout.home_popover_edit);
+ final View content = popup.getContentView();
+ final View pinned = content.findViewById(R.id.popover_edit_pinned);
+ if (pinned != null) {
+ pinned.setVisibility(View.GONE);
+ }
+ content.findViewById(R.id.popover_select_messages).setOnClickListener(v -> {
+ popup.dismiss();
+ if (activity instanceof AbstractConversationListActivity) {
+ ((AbstractConversationListActivity) activity).enterHomeEditMode();
+ }
+ });
+ }
+
+ private static void openScreen(final Activity activity, final Screen current,
+ final Screen target, final Runnable launch) {
+ if (current == target) {
+ return;
+ }
+ launch.run();
+ activity.finish();
+ activity.overridePendingTransition(R.anim.home_activity_open, R.anim.home_activity_hold);
+ }
+
+ private static void bindFilterSims(final View content) {
+ final Context context = content.getContext();
+ setFilterText(content, R.id.popover_work_badge, HomeInboxFilter.workSimName(context));
+ setFilterText(content, R.id.popover_work_label, HomeInboxFilter.workSimName(context));
+ setFilterText(content, R.id.popover_personal_badge,
+ HomeInboxFilter.personalSimName(context));
+ setFilterText(content, R.id.popover_personal_label,
+ HomeInboxFilter.personalSimName(context));
+ }
+
+ private static void bindFilterChecks(final View content, final Screen screen) {
+ setCheck(content, R.id.popover_messages_check, screen == Screen.MESSAGES);
+ setCheck(content, R.id.popover_spam_check, screen == Screen.SPAM);
+ setCheck(content, R.id.popover_archive_check, screen == Screen.ARCHIVE);
+ setCheck(content, R.id.popover_recently_deleted_check, screen == Screen.TRASH);
+ setCheck(content, R.id.popover_unread_check, false);
+ setCheck(content, R.id.popover_work_check, false);
+ setCheck(content, R.id.popover_personal_check, false);
+ }
+
+ private static void setCheck(final View content, final int id, final boolean on) {
+ final View check = content.findViewById(id);
+ if (check != null) {
+ check.setVisibility(on ? View.VISIBLE : View.INVISIBLE);
+ }
+ }
+
+ private static void setFilterText(final View content, final int id, final String text) {
+ final View view = content.findViewById(id);
+ if (view instanceof TextView) {
+ ((TextView) view).setText(text);
+ }
+ }
+
+ private static void setGone(final View content, final int id) {
+ final View view = content.findViewById(id);
+ if (view != null) {
+ view.setVisibility(View.GONE);
+ }
+ }
+
+ private static PopupWindow showPopover(final Activity activity, final View anchor,
+ final int layoutResId) {
+ final View content = LayoutInflater.from(activity).inflate(layoutResId, null, false);
+ final int width = activity.getResources().getDimensionPixelSize(R.dimen.home_popover_width);
+ content.measure(
+ View.MeasureSpec.makeMeasureSpec(width, View.MeasureSpec.EXACTLY),
+ View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED));
+ final PopupWindow popup = new PopupWindow(content, width,
+ ViewGroup.LayoutParams.WRAP_CONTENT, true);
+ popup.setOutsideTouchable(true);
+ popup.setFocusable(true);
+ popup.setElevation(16f);
+ popup.setBackgroundDrawable(new ColorDrawable(android.graphics.Color.TRANSPARENT));
+ final int endInset = activity.getResources().getDimensionPixelSize(
+ R.dimen.looply_screen_padding);
+ final int[] anchorLoc = new int[2];
+ anchor.getLocationOnScreen(anchorLoc);
+ final int windowRight = activity.getResources().getDisplayMetrics().widthPixels;
+ int xOff = windowRight - endInset - width - anchorLoc[0];
+ final int overflow = (anchorLoc[0] + xOff + width) - (windowRight - endInset);
+ if (overflow > 0) {
+ xOff -= overflow;
+ }
+ popup.showAsDropDown(anchor, xOff, 8);
+ HomeUiMotion.popoverEnter(content, true);
+ return popup;
+ }
+
+ private static void shareThisApp(final Activity activity) {
+ final String storeUrl = "https://play.google.com/store/apps/details?id="
+ + activity.getPackageName();
+ final Intent share = new Intent(Intent.ACTION_SEND);
+ share.setType("text/plain");
+ share.putExtra(Intent.EXTRA_TEXT,
+ activity.getString(R.string.home_share_app_text, storeUrl));
+ activity.startActivity(Intent.createChooser(share,
+ activity.getString(R.string.home_popover_share_app)));
+ }
+}
diff --git a/app/src/main/java/com/android/messaging/ui/home/HomeMessageDetailsActivity.java b/app/src/main/java/com/android/messaging/ui/home/HomeMessageDetailsActivity.java
new file mode 100644
index 0000000..4865387
--- /dev/null
+++ b/app/src/main/java/com/android/messaging/ui/home/HomeMessageDetailsActivity.java
@@ -0,0 +1,214 @@
+package com.android.messaging.ui.home;
+
+import android.content.Context;
+import android.content.Intent;
+import android.os.Bundle;
+import android.support.v7.mms.pdu.PduHeaders;
+import android.text.TextUtils;
+import android.text.format.DateUtils;
+import android.text.format.Formatter;
+import android.view.Gravity;
+import android.view.View;
+import android.widget.FrameLayout;
+import android.widget.TextView;
+
+import com.android.messaging.R;
+import com.android.messaging.datamodel.data.ConversationMessageData;
+import com.android.messaging.datamodel.data.MessagePartData;
+import com.android.messaging.sms.MmsUtils;
+import com.android.messaging.ui.BugleActionBarActivity;
+
+import java.util.List;
+
+/** Home sub-screen for a single message's details. */
+public class HomeMessageDetailsActivity extends BugleActionBarActivity {
+
+ static final String EXTRA_PREVIEW = "preview";
+ static final String EXTRA_INCOMING = "incoming";
+ static final String EXTRA_SMS = "sms";
+ static final String EXTRA_SENT = "sent";
+ static final String EXTRA_RECEIVED = "received";
+ static final String EXTRA_SEND_COMPLETE = "send_complete";
+ static final String EXTRA_PRIORITY = "priority";
+ static final String EXTRA_SUBJECT = "subject";
+ static final String EXTRA_SIZE = "size";
+
+ public static void start(final Context context, final ConversationMessageData data) {
+ if (context == null || data == null) {
+ return;
+ }
+ final Intent intent = new Intent(context, HomeMessageDetailsActivity.class);
+ intent.putExtra(EXTRA_PREVIEW, previewText(context, data));
+ intent.putExtra(EXTRA_INCOMING, data.getIsIncoming());
+ intent.putExtra(EXTRA_SMS, data.getIsSms());
+ intent.putExtra(EXTRA_SENT, data.getSentTimeStamp());
+ intent.putExtra(EXTRA_RECEIVED, data.getReceivedTimeStamp());
+ intent.putExtra(EXTRA_SEND_COMPLETE, data.getIsSendComplete());
+ intent.putExtra(EXTRA_PRIORITY, data.getSmsPriority());
+ intent.putExtra(EXTRA_SUBJECT, data.getMmsSubject());
+ intent.putExtra(EXTRA_SIZE, data.getSmsMessageSize());
+ context.startActivity(intent);
+ }
+
+ @Override
+ protected void onCreate(final Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.home_message_details_activity);
+ if (getSupportActionBar() != null) {
+ getSupportActionBar().hide();
+ }
+ HomeSubHeader.bind(this, R.string.home_message_details_title, v -> finish());
+ bindDetails(getIntent());
+ }
+
+ private void bindDetails(final Intent intent) {
+ if (intent == null) {
+ finish();
+ return;
+ }
+ final TextView bubble = findViewById(R.id.home_message_details_bubble);
+ final boolean incoming = intent.getBooleanExtra(EXTRA_INCOMING, true);
+ bubble.setText(intent.getStringExtra(EXTRA_PREVIEW));
+ final FrameLayout.LayoutParams lp = (FrameLayout.LayoutParams) bubble.getLayoutParams();
+ if (incoming) {
+ lp.gravity = Gravity.START;
+ bubble.setBackgroundResource(R.drawable.home_bg_preview_bubble_in);
+ bubble.setTextColor(getColor(R.color.home_text_primary));
+ } else {
+ lp.gravity = Gravity.END;
+ bubble.setBackgroundResource(R.drawable.home_bg_preview_bubble_out);
+ bubble.setTextColor(getColor(R.color.looply_bubble_outgoing_text));
+ }
+ bubble.setLayoutParams(lp);
+
+ final boolean sms = intent.getBooleanExtra(EXTRA_SMS, true);
+ final TextView type = findViewById(R.id.home_message_details_type);
+ type.setText(sms ? R.string.text_message : R.string.multimedia_message);
+
+ bindStatus(intent);
+ bindPriority(intent.getIntExtra(EXTRA_PRIORITY, PduHeaders.PRIORITY_NORMAL));
+ bindOptional(R.id.home_message_details_subject_section,
+ R.id.home_message_details_subject,
+ MmsUtils.cleanseMmsSubject(getResources(), intent.getStringExtra(EXTRA_SUBJECT)));
+ final int size = intent.getIntExtra(EXTRA_SIZE, 0);
+ if (size > 0) {
+ bindOptional(R.id.home_message_details_size_section,
+ R.id.home_message_details_size,
+ Formatter.formatFileSize(this, size));
+ }
+ }
+
+ private void bindStatus(final Intent intent) {
+ final boolean incoming = intent.getBooleanExtra(EXTRA_INCOMING, true);
+ final boolean sendComplete = intent.getBooleanExtra(EXTRA_SEND_COMPLETE, false);
+ final long sent = intent.getLongExtra(EXTRA_SENT, MmsUtils.INVALID_TIMESTAMP);
+ final long received = intent.getLongExtra(EXTRA_RECEIVED, MmsUtils.INVALID_TIMESTAMP);
+
+ final boolean showReceived = incoming && received != MmsUtils.INVALID_TIMESTAMP;
+ final boolean showSent;
+ final long sentDisplay;
+ if (incoming) {
+ showSent = sent != MmsUtils.INVALID_TIMESTAMP;
+ sentDisplay = sent;
+ } else if (sendComplete && received != MmsUtils.INVALID_TIMESTAMP) {
+ showSent = true;
+ sentDisplay = received;
+ } else {
+ showSent = sent != MmsUtils.INVALID_TIMESTAMP;
+ sentDisplay = sent;
+ }
+
+ setRow(R.id.home_message_details_received_row,
+ R.id.home_message_details_received, showReceived, received);
+ setRow(R.id.home_message_details_sent_row,
+ R.id.home_message_details_sent, showSent, sentDisplay);
+ findViewById(R.id.home_message_details_status_divider)
+ .setVisibility(showReceived && showSent ? View.VISIBLE : View.GONE);
+ findViewById(R.id.home_message_details_status_section)
+ .setVisibility(showReceived || showSent ? View.VISIBLE : View.GONE);
+ }
+
+ private void bindPriority(final int priority) {
+ final View section = findViewById(R.id.home_message_details_priority_section);
+ final TextView value = findViewById(R.id.home_message_details_priority);
+ final int label;
+ switch (priority) {
+ case PduHeaders.PRIORITY_HIGH:
+ label = R.string.priority_high;
+ break;
+ case PduHeaders.PRIORITY_LOW:
+ label = R.string.priority_low;
+ break;
+ case PduHeaders.PRIORITY_NORMAL:
+ default:
+ label = R.string.priority_normal;
+ break;
+ }
+ value.setText(label);
+ section.setVisibility(View.VISIBLE);
+ }
+
+ private void bindOptional(final int sectionId, final int valueId, final CharSequence text) {
+ final View section = findViewById(sectionId);
+ final TextView value = findViewById(valueId);
+ if (TextUtils.isEmpty(text)) {
+ section.setVisibility(View.GONE);
+ return;
+ }
+ value.setText(text);
+ section.setVisibility(View.VISIBLE);
+ }
+
+ private void setRow(final int rowId, final int valueId, final boolean show, final long time) {
+ final View row = findViewById(rowId);
+ final TextView value = findViewById(valueId);
+ if (!show || time == MmsUtils.INVALID_TIMESTAMP) {
+ row.setVisibility(View.GONE);
+ return;
+ }
+ value.setText(formatTime(time));
+ row.setVisibility(View.VISIBLE);
+ }
+
+ private String formatTime(final long time) {
+ return DateUtils.formatDateTime(this, time,
+ DateUtils.FORMAT_SHOW_DATE
+ | DateUtils.FORMAT_SHOW_YEAR
+ | DateUtils.FORMAT_SHOW_TIME
+ | DateUtils.FORMAT_ABBREV_MONTH);
+ }
+
+ private static String previewText(final Context context, final ConversationMessageData data) {
+ final String text = data.getText();
+ if (!TextUtils.isEmpty(text)) {
+ return text;
+ }
+ final List attachments = data.getAttachments();
+ if (attachments != null) {
+ for (final MessagePartData part : attachments) {
+ if (part.isImage()) {
+ return context.getString(R.string.home_message_details_photo);
+ }
+ if (part.isVideo()) {
+ return context.getString(R.string.home_message_details_video);
+ }
+ if (part.isAudio()) {
+ return context.getString(R.string.home_message_details_audio);
+ }
+ if (part.isVCard()) {
+ return context.getString(R.string.home_message_details_contact);
+ }
+ }
+ if (!attachments.isEmpty()) {
+ return context.getString(R.string.home_message_details_attachment);
+ }
+ }
+ return context.getString(R.string.home_message_details_attachment);
+ }
+
+ @Override
+ public void finish() {
+ super.finish();
+ overridePendingTransition(R.anim.home_activity_hold, R.anim.home_activity_close);
+ }
+}
diff --git a/app/src/main/java/com/android/messaging/ui/home/HomeModal.java b/app/src/main/java/com/android/messaging/ui/home/HomeModal.java
index e0a4424..172c480 100644
--- a/app/src/main/java/com/android/messaging/ui/home/HomeModal.java
+++ b/app/src/main/java/com/android/messaging/ui/home/HomeModal.java
@@ -97,8 +97,10 @@ public final class HomeModal {
private CharSequence mTitle;
private CharSequence mMessage;
private CharSequence mPositive;
+ private CharSequence mNeutral;
private CharSequence mNegative;
private Action mOnPositive;
+ private Action mOnNeutral;
private Action mOnNegative;
private View mCustom;
private boolean mCancelable = true;
@@ -149,6 +151,18 @@ public final class HomeModal {
return this;
}
+ public Builder neutral(@StringRes final int labelRes, @Nullable final Action action) {
+ mNeutral = mContext.getString(labelRes);
+ mOnNeutral = action;
+ return this;
+ }
+
+ public Builder neutral(final CharSequence label, @Nullable final Action action) {
+ mNeutral = label;
+ mOnNeutral = action;
+ return this;
+ }
+
public Builder negative(@StringRes final int labelRes, @Nullable final Action action) {
mNegative = mContext.getString(labelRes);
mOnNegative = action;
@@ -195,9 +209,11 @@ public final class HomeModal {
}
final TextView positive = content.findViewById(R.id.home_modal_positive);
+ final TextView neutral = content.findViewById(R.id.home_modal_neutral);
final TextView negative = content.findViewById(R.id.home_modal_negative);
- bindButton(negative, options.mNegative, options.mOnNegative, dialog, true);
bindButton(positive, options.mPositive, options.mOnPositive, dialog, true);
+ bindButton(neutral, options.mNeutral, options.mOnNeutral, dialog, true);
+ bindButton(negative, options.mNegative, options.mOnNegative, dialog, true);
dialog.show();
diff --git a/app/src/main/java/com/android/messaging/ui/home/HomeProfileActivity.java b/app/src/main/java/com/android/messaging/ui/home/HomeProfileActivity.java
index c47a9d8..3627d72 100644
--- a/app/src/main/java/com/android/messaging/ui/home/HomeProfileActivity.java
+++ b/app/src/main/java/com/android/messaging/ui/home/HomeProfileActivity.java
@@ -403,7 +403,16 @@ public class HomeProfileActivity extends BugleActionBarActivity
}
private ParticipantData getOtherParticipant() {
- return mBinding.isBound() ? mBinding.getData().getOtherParticipant() : null;
+ if (mBinding.isBound()) {
+ final ParticipantData bound = mBinding.getData().getOtherParticipant();
+ if (bound != null) {
+ return bound;
+ }
+ }
+ if (mParticipants != null && mParticipants.size() == 1) {
+ return mParticipants.get(0);
+ }
+ return null;
}
private String getCallablePhoneNumber() {
@@ -460,15 +469,19 @@ public class HomeProfileActivity extends BugleActionBarActivity
private void showRenameDialog() {
final View content = LayoutInflater.from(this)
- .inflate(R.layout.home_profile_rename_dialog, null);
+ .inflate(R.layout.home_profile_rename_dialog, null, false);
final EditText field = content.findViewById(R.id.home_profile_rename_field);
- field.setText(mNameView.getText());
- field.setSelection(field.getText().length());
- new AlertDialog.Builder(this, R.style.Home_Dialog)
- .setTitle(R.string.home_profile_rename)
- .setView(content)
- .setNegativeButton(android.R.string.cancel, null)
- .setPositiveButton(R.string.home_profile_rename_save, (dialog, which) -> {
+ final CharSequence current = mNameView == null ? "" : mNameView.getText();
+ field.setText(current);
+ if (!TextUtils.isEmpty(current)) {
+ field.setSelection(current.length());
+ }
+ final HomeModal.Handle handle = HomeModal.builder(this)
+ .icon(R.drawable.ic_home_edit)
+ .title(R.string.home_profile_rename)
+ .custom(content)
+ .negative(android.R.string.cancel, null)
+ .positive(R.string.home_profile_rename_save, () -> {
final String name = field.getText().toString().trim();
// An empty name would hide the conversation everywhere it is listed.
if (!TextUtils.isEmpty(name)) {
@@ -476,6 +489,15 @@ public class HomeProfileActivity extends BugleActionBarActivity
}
})
.show();
+ handle.dialog.setOnDismissListener(dialog -> {
+ if (!isFinishing() && mConversation != null) {
+ updateViews();
+ }
+ });
+ field.post(() -> {
+ field.requestFocus();
+ ImeUtil.get().showImeKeyboard(this, field);
+ });
}
private void openNotificationSettings() {
@@ -522,15 +544,27 @@ public class HomeProfileActivity extends BugleActionBarActivity
}
private void confirmDelete() {
- new AlertDialog.Builder(this, R.style.Home_Dialog)
- .setTitle(getResources().getQuantityString(
- R.plurals.delete_conversations_confirmation_dialog_title, 1))
- .setNegativeButton(R.string.delete_conversation_decline_button, null)
- .setPositiveButton(R.string.delete_conversation_confirmation_button,
- (dialog, which) -> {
- mConversation.deleteConversation();
- closeConversation();
- })
+ if (mConversation == null) {
+ return;
+ }
+ final boolean trashed = mConversation.getIsTrashed();
+ HomeModal.builder(this)
+ .icon(R.drawable.ic_home_menu_trash)
+ .title(trashed
+ ? getString(R.string.home_trash_permanent_title)
+ : getResources().getQuantityString(
+ R.plurals.delete_conversations_confirmation_dialog_title, 1))
+ .message(trashed
+ ? getString(R.string.home_trash_permanent_message_one)
+ : getString(R.string.home_delete_alert_message_one))
+ .negative(R.string.delete_conversation_decline_button, null)
+ .positive(R.string.home_delete_confirm, () -> {
+ mConversation.deleteConversation();
+ if (!trashed) {
+ HomeToast.show(this, R.string.home_trash_moved_toast_one);
+ }
+ closeConversation();
+ })
.show();
}
diff --git a/app/src/main/java/com/android/messaging/ui/home/HomeSpamActivity.java b/app/src/main/java/com/android/messaging/ui/home/HomeSpamActivity.java
index 48ce7c4..cbc2f1f 100644
--- a/app/src/main/java/com/android/messaging/ui/home/HomeSpamActivity.java
+++ b/app/src/main/java/com/android/messaging/ui/home/HomeSpamActivity.java
@@ -17,14 +17,14 @@
package com.android.messaging.ui.home;
import android.os.Bundle;
-import android.widget.ImageButton;
-import android.widget.TextView;
import com.android.messaging.R;
import com.android.messaging.ui.BugleActionBarActivity;
/** Home-style screen to view and remove spam-registered (blocked) numbers. */
-public class HomeSpamActivity extends BugleActionBarActivity {
+public class HomeSpamActivity extends BugleActionBarActivity implements HomeSpamFragment.Host {
+
+ private boolean mSelecting;
@Override
protected void onCreate(final Bundle savedInstanceState) {
@@ -34,16 +34,62 @@ public class HomeSpamActivity extends BugleActionBarActivity {
getSupportActionBar().hide();
}
- final ImageButton back = findViewById(R.id.home_sub_header_back);
- if (back != null) {
- back.setOnClickListener(v -> finish());
+ HomeSubHeader.bind(this, R.string.home_spam_title, v -> onBackPressed());
+ HomeInboxMenu.bind(this, HomeInboxMenu.Screen.SPAM);
+ }
+
+ public void toggleSelectMode() {
+ if (!mSelecting) {
+ final HomeSpamFragment fragment = fragment();
+ if (fragment == null || fragment.getVisibleCount() == 0) {
+ HomeToast.show(this, R.string.home_toast_nothing_to_select);
+ return;
+ }
}
- final TextView title = findViewById(R.id.home_sub_header_title);
- if (title != null) {
- title.setText(R.string.home_spam_title);
+ setSelecting(!mSelecting);
+ }
+
+ public void onSelectPrimaryAction() {
+ final HomeSpamFragment fragment = fragment();
+ if (fragment != null) {
+ fragment.unblockSelectedOrAll();
}
}
+ void setSelecting(final boolean selecting) {
+ mSelecting = selecting;
+ final HomeSpamFragment fragment = fragment();
+ if (fragment != null) {
+ fragment.setSelecting(selecting);
+ } else {
+ onSpamSelectionChanged(0, 0, selecting);
+ }
+ HomeInboxMenu.setSelecting(this, selecting);
+ }
+
+ @Override
+ public void onSpamSelectionChanged(final int selectedCount, final int visibleCount,
+ final boolean selecting) {
+ mSelecting = selecting;
+ HomeInboxMenu.setSelectAction(this, R.drawable.ic_home_menu_spam,
+ selectedCount > 0 ? R.string.home_sub_unblock : R.string.home_sub_unblock_all,
+ visibleCount > 0, false);
+ }
+
+ @Override
+ public void onBackPressed() {
+ if (mSelecting) {
+ setSelecting(false);
+ return;
+ }
+ super.onBackPressed();
+ }
+
+ private HomeSpamFragment fragment() {
+ return (HomeSpamFragment) getSupportFragmentManager()
+ .findFragmentById(R.id.home_spam_fragment_host);
+ }
+
@Override
public void finish() {
super.finish();
diff --git a/app/src/main/java/com/android/messaging/ui/home/HomeSpamFragment.java b/app/src/main/java/com/android/messaging/ui/home/HomeSpamFragment.java
index 6725b3a..6373c5e 100644
--- a/app/src/main/java/com/android/messaging/ui/home/HomeSpamFragment.java
+++ b/app/src/main/java/com/android/messaging/ui/home/HomeSpamFragment.java
@@ -1,31 +1,20 @@
-/*
- * Copyright (C) 2025 The LineageOS Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
package com.android.messaging.ui.home;
import android.content.Context;
import android.database.Cursor;
import android.os.Bundle;
+import android.text.TextUtils;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
-import android.widget.ListView;
-import androidx.cursoradapter.widget.CursorAdapter;
+import android.widget.ImageView;
+import android.widget.TextView;
+
+import androidx.annotation.NonNull;
import androidx.fragment.app.Fragment;
import androidx.loader.app.LoaderManager;
+import androidx.recyclerview.widget.LinearLayoutManager;
+import androidx.recyclerview.widget.RecyclerView;
import com.android.messaging.R;
import com.android.messaging.datamodel.DataModel;
@@ -33,43 +22,177 @@ import com.android.messaging.datamodel.binding.Binding;
import com.android.messaging.datamodel.binding.BindingBase;
import com.android.messaging.datamodel.data.BlockedParticipantsData;
import com.android.messaging.datamodel.data.BlockedParticipantsData.BlockedParticipantsDataListener;
-import com.android.messaging.ui.BlockedParticipantListItemView;
-import com.android.messaging.util.Assert;
+import com.android.messaging.datamodel.data.ParticipantListItemData;
+import com.android.messaging.ui.ContactIconView;
+import com.android.messaging.ui.CursorRecyclerAdapter;
-/** Home UI list of spam-registered (blocked) senders with unblock on tap. */
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+/** Home UI list of spam-registered (blocked) senders. Tap Unblock to restore the number. */
public class HomeSpamFragment extends Fragment implements BlockedParticipantsDataListener {
- private ListView mListView;
+ public interface Host {
+ void onSpamSelectionChanged(int selectedCount, int visibleCount, boolean selecting);
+ }
+
+ private RecyclerView mListView;
private HomeEmptyState mEmptyView;
- private BlockedParticipantListAdapter mAdapter;
+ private SpamAdapter mAdapter;
+ private Host mHost;
+ private boolean mSelecting;
+ private final Set mSelectedDestinations = new HashSet<>();
private final Binding mBinding = BindingBase.createBinding(this);
@Override
public View onCreateView(final LayoutInflater inflater, final ViewGroup container,
final Bundle savedInstanceState) {
- final View root = inflater.inflate(R.layout.blocked_participants_fragment, container,
- false);
- mListView = root.findViewById(android.R.id.list);
- mAdapter = new BlockedParticipantListAdapter(requireContext(), null);
+ final View root = inflater.inflate(R.layout.home_spam_fragment, container, false);
+ mListView = root.findViewById(R.id.home_spam_list);
+ mAdapter = new SpamAdapter(requireContext());
+ mListView.setLayoutManager(new LinearLayoutManager(requireContext()));
mListView.setAdapter(mAdapter);
mBinding.bind(DataModel.get().createBlockedParticipantsData(requireContext(), this));
mBinding.getData().init(LoaderManager.getInstance(this), mBinding);
- final View activityRoot = requireActivity().findViewById(R.id.home_spam_empty);
- if (activityRoot instanceof HomeEmptyState) {
- mEmptyView = (HomeEmptyState) activityRoot;
+ final View activityEmpty = requireActivity().findViewById(R.id.home_spam_empty);
+ if (activityEmpty instanceof HomeEmptyState) {
+ mEmptyView = (HomeEmptyState) activityEmpty;
mEmptyView.setContent(R.drawable.ic_home_menu_spam,
R.string.home_empty_spam_title,
R.string.home_empty_spam_body);
}
+ HomeSubListSearch.bind(requireActivity(), mListView, query -> {
+ if (mAdapter != null) {
+ mAdapter.setSearchQuery(query);
+ pruneSelection();
+ updateEmptyState();
+ notifyHost();
+ }
+ });
+ HomeSubListSearch.bindListFades(mListView,
+ root.findViewById(R.id.home_list_top_fade),
+ root.findViewById(R.id.home_list_bottom_fade));
+ if (getActivity() instanceof Host) {
+ mHost = (Host) getActivity();
+ }
+ notifyHost();
return root;
}
+ void setSelecting(final boolean selecting) {
+ mSelecting = selecting;
+ if (!selecting) {
+ mSelectedDestinations.clear();
+ }
+ notifyHost();
+ final View list = mListView;
+ if (list != null) {
+ list.post(() -> {
+ if (!isAdded() || mListView == null) {
+ return;
+ }
+ applySelectPadding();
+ if (mAdapter != null) {
+ mAdapter.notifyDataSetChanged();
+ }
+ });
+ } else {
+ applySelectPadding();
+ if (mAdapter != null) {
+ mAdapter.notifyDataSetChanged();
+ }
+ }
+ }
+
+ int getSelectedCount() {
+ return mSelectedDestinations.size();
+ }
+
+ int getVisibleCount() {
+ return mAdapter == null ? 0 : mAdapter.getItemCount();
+ }
+
+ void unblockSelectedOrAll() {
+ final Context context = getContext();
+ if (context == null || mAdapter == null) {
+ return;
+ }
+ final List targets = mSelectedDestinations.isEmpty()
+ ? mAdapter.snapshotVisible()
+ : mAdapter.snapshotSelected(mSelectedDestinations);
+ if (targets.isEmpty()) {
+ HomeToast.show(context, R.string.home_toast_nothing_to_unblock);
+ return;
+ }
+ for (final ParticipantListItemData data : targets) {
+ data.unblockQuietly(context);
+ }
+ HomeToast.show(context, R.string.home_sub_unblocked_toast);
+ if (getActivity() instanceof HomeSpamActivity) {
+ ((HomeSpamActivity) getActivity()).setSelecting(false);
+ } else {
+ setSelecting(false);
+ }
+ }
+
+ private void applySelectPadding() {
+ if (mListView == null) {
+ return;
+ }
+ mListView.setPadding(
+ mListView.getPaddingLeft(),
+ mListView.getPaddingTop(),
+ mListView.getPaddingRight(),
+ getResources().getDimensionPixelSize(mSelecting
+ ? R.dimen.home_sub_select_list_padding
+ : R.dimen.looply_space_lg));
+ }
+
+ private void notifyHost() {
+ if (mHost != null) {
+ mHost.onSpamSelectionChanged(mSelectedDestinations.size(),
+ getVisibleCount(), mSelecting);
+ }
+ }
+
+ private void onRowClicked(final ParticipantListItemData data) {
+ if (mSelecting) {
+ toggleSelected(data);
+ } else {
+ confirmUnblock(data);
+ }
+ }
+
+ private void toggleSelected(final ParticipantListItemData data) {
+ if (data == null) {
+ return;
+ }
+ final String destination = data.getNormalizedDestination();
+ if (TextUtils.isEmpty(destination)) {
+ return;
+ }
+ if (!mSelectedDestinations.add(destination)) {
+ mSelectedDestinations.remove(destination);
+ }
+ if (mAdapter != null) {
+ mAdapter.notifyDataSetChanged();
+ }
+ notifyHost();
+ }
+
+ private boolean isSelected(final ParticipantListItemData data) {
+ return data != null && mSelectedDestinations.contains(data.getNormalizedDestination());
+ }
+
@Override
public void onDestroyView() {
super.onDestroyView();
mEmptyView = null;
mListView = null;
+ mAdapter = null;
}
@Override
@@ -80,33 +203,249 @@ public class HomeSpamFragment extends Fragment implements BlockedParticipantsDat
@Override
public void onBlockedParticipantsCursorUpdated(final Cursor cursor) {
- mAdapter.swapCursor(cursor);
- final boolean empty = mAdapter.getCount() == 0;
+ if (mAdapter != null) {
+ mAdapter.swapCursor(cursor);
+ pruneSelection();
+ }
+ updateEmptyState();
+ notifyHost();
+ }
+
+ private void pruneSelection() {
+ if (mAdapter == null || mSelectedDestinations.isEmpty()) {
+ return;
+ }
+ final HashSet visible = new HashSet<>();
+ for (final ParticipantListItemData data : mAdapter.snapshotVisible()) {
+ visible.add(data.getNormalizedDestination());
+ }
+ mSelectedDestinations.retainAll(visible);
+ }
+
+ private void updateEmptyState() {
+ final boolean empty = mAdapter == null || mAdapter.getItemCount() == 0;
if (mListView != null) {
mListView.setVisibility(empty ? View.GONE : View.VISIBLE);
}
if (mEmptyView != null) {
+ if (empty && mAdapter != null && mAdapter.hasSearchQuery()) {
+ mEmptyView.setContent(R.drawable.ic_home_search_line,
+ getString(R.string.home_empty_sub_search_title),
+ getString(R.string.home_empty_sub_search_body, mAdapter.getSearchQuery()));
+ } else if (empty) {
+ mEmptyView.setContent(R.drawable.ic_home_menu_spam,
+ R.string.home_empty_spam_title,
+ R.string.home_empty_spam_body);
+ }
mEmptyView.setVisibility(empty ? View.VISIBLE : View.GONE);
}
}
- private class BlockedParticipantListAdapter extends CursorAdapter {
+ private void confirmUnblock(final ParticipantListItemData data) {
+ final Context context = getContext();
+ if (context == null || data == null) {
+ return;
+ }
+ final String name = data.getDisplayName();
+ HomeModal.builder(context)
+ .icon(R.drawable.ic_home_menu_spam)
+ .title(R.string.home_spam_unblock_title)
+ .message(context.getString(R.string.home_spam_unblock_message, name))
+ .negative(android.R.string.cancel, null)
+ .positive(R.string.home_spam_unblock_action, () -> data.unblock(context))
+ .show();
+ }
- BlockedParticipantListAdapter(final Context context, final Cursor cursor) {
- super(context, cursor, 0);
+ private final class SpamAdapter extends CursorRecyclerAdapter {
+ private String mSearchQuery = "";
+ private int[] mDisplayPositions = new int[0];
+
+ SpamAdapter(final Context context) {
+ super(context, null, 0);
+ setHasStableIds(true);
+ }
+
+ void setSearchQuery(final String query) {
+ final String next = query == null ? "" : query.trim();
+ if (next.equals(mSearchQuery)) {
+ return;
+ }
+ mSearchQuery = next;
+ rebuildFilterIndex();
+ notifyDataSetChanged();
+ }
+
+ String getSearchQuery() {
+ return mSearchQuery;
+ }
+
+ boolean hasSearchQuery() {
+ return !mSearchQuery.isEmpty();
}
@Override
- public View newView(final Context context, final Cursor cursor, final ViewGroup parent) {
- return LayoutInflater.from(context).inflate(
- R.layout.blocked_participant_list_item_view, parent, false);
+ public Cursor swapCursor(final Cursor newCursor) {
+ final Cursor old = super.swapCursor(newCursor);
+ rebuildFilterIndex();
+ notifyDataSetChanged();
+ return old;
}
@Override
- public void bindView(final View view, final Context context, final Cursor cursor) {
- Assert.isTrue(view instanceof BlockedParticipantListItemView);
- final BlockedParticipantListItemView item = (BlockedParticipantListItemView) view;
- item.bind(mBinding.getData().createParticipantListItemData(cursor));
+ public int getItemCount() {
+ return mDisplayPositions.length;
+ }
+
+ @Override
+ public long getItemId(final int position) {
+ return super.getItemId(mapPosition(position));
+ }
+
+ @Override
+ public void onBindViewHolder(@NonNull final SpamHolder holder, final int position) {
+ super.onBindViewHolder(holder, mapPosition(position));
+ }
+
+ @Override
+ public SpamHolder createViewHolder(final Context context, final ViewGroup parent,
+ final int viewType) {
+ final View item = LayoutInflater.from(context).inflate(
+ R.layout.home_spam_list_item, parent, false);
+ return new SpamHolder(item);
+ }
+
+ @Override
+ public void bindViewHolder(final SpamHolder holder, final Context context,
+ final Cursor cursor) {
+ if (!mBinding.isBound()) {
+ return;
+ }
+ holder.bind(mBinding.getData().createParticipantListItemData(cursor));
+ }
+
+ private int mapPosition(final int position) {
+ if (position < 0 || position >= mDisplayPositions.length) {
+ return position;
+ }
+ return mDisplayPositions[position];
+ }
+
+ private void rebuildFilterIndex() {
+ final Cursor cursor = getCursor();
+ if (cursor == null || cursor.getCount() == 0) {
+ mDisplayPositions = new int[0];
+ return;
+ }
+ final int saved = cursor.getPosition();
+ final int[] matches = new int[cursor.getCount()];
+ int count = 0;
+ for (int i = 0; i < cursor.getCount(); i++) {
+ if (!cursor.moveToPosition(i)) {
+ continue;
+ }
+ final ParticipantListItemData data =
+ mBinding.isBound() ? mBinding.getData().createParticipantListItemData(cursor)
+ : null;
+ if (data == null) {
+ continue;
+ }
+ if (HomeSubListSearch.matches(mSearchQuery, data.getDisplayName(),
+ data.getDetails(), data.getNormalizedDestination())) {
+ matches[count++] = i;
+ }
+ }
+ if (saved >= 0 && saved < cursor.getCount()) {
+ cursor.moveToPosition(saved);
+ }
+ mDisplayPositions = new int[count];
+ System.arraycopy(matches, 0, mDisplayPositions, 0, count);
+ }
+
+ List snapshotVisible() {
+ return snapshotMatching(null);
+ }
+
+ List snapshotSelected(final Set destinations) {
+ return snapshotMatching(destinations);
+ }
+
+ private List snapshotMatching(final Set destinations) {
+ final List out = new ArrayList<>();
+ final Cursor cursor = getCursor();
+ if (cursor == null || !mBinding.isBound()) {
+ return out;
+ }
+ final int saved = cursor.getPosition();
+ for (int i = 0; i < mDisplayPositions.length; i++) {
+ if (!cursor.moveToPosition(mDisplayPositions[i])) {
+ continue;
+ }
+ final ParticipantListItemData data =
+ mBinding.getData().createParticipantListItemData(cursor);
+ if (data == null) {
+ continue;
+ }
+ if (destinations == null
+ || destinations.contains(data.getNormalizedDestination())) {
+ out.add(data);
+ }
+ }
+ if (saved >= 0 && saved < cursor.getCount()) {
+ cursor.moveToPosition(saved);
+ }
+ return out;
+ }
+ }
+
+ private final class SpamHolder extends RecyclerView.ViewHolder {
+ private final ContactIconView mAvatar;
+ private final TextView mName;
+ private final TextView mDetails;
+ private final View mUnblock;
+ private final View mIndicator;
+ private final View mRing;
+ private final ImageView mCheck;
+ private ParticipantListItemData mData;
+
+ SpamHolder(final View itemView) {
+ super(itemView);
+ mAvatar = itemView.findViewById(R.id.home_spam_avatar);
+ mName = itemView.findViewById(R.id.home_spam_name);
+ mDetails = itemView.findViewById(R.id.home_spam_details);
+ mUnblock = itemView.findViewById(R.id.home_spam_unblock);
+ mIndicator = itemView.findViewById(R.id.home_spam_selection_indicator);
+ mRing = itemView.findViewById(R.id.home_spam_selection_ring);
+ mCheck = itemView.findViewById(R.id.home_spam_selection_check);
+ itemView.setOnClickListener(v -> onRowClicked(mData));
+ mUnblock.setOnClickListener(v -> confirmUnblock(mData));
+ }
+
+ void bind(final ParticipantListItemData data) {
+ mData = data;
+ mName.setText(data.getDisplayName());
+ final String details = data.getDetails();
+ if (TextUtils.isEmpty(details)
+ || TextUtils.equals(details, data.getDisplayName())) {
+ mDetails.setText(R.string.home_spam_item_hint);
+ } else {
+ mDetails.setText(details);
+ }
+ mAvatar.setImageResourceUri(data.getAvatarUri(), data.getContactId(),
+ data.getLookupKey(), data.getNormalizedDestination());
+ mAvatar.setClickable(!mSelecting);
+ mUnblock.setVisibility(mSelecting ? View.GONE : View.VISIBLE);
+ if (mIndicator != null) {
+ mIndicator.setVisibility(mSelecting ? View.VISIBLE : View.GONE);
+ }
+ final boolean selected = mSelecting && isSelected(data);
+ if (mRing != null) {
+ mRing.setBackgroundResource(selected
+ ? R.drawable.home_bg_selection_ring_checked
+ : R.drawable.home_bg_selection_ring);
+ }
+ if (mCheck != null) {
+ mCheck.setVisibility(selected ? View.VISIBLE : View.GONE);
+ }
}
}
}
diff --git a/app/src/main/java/com/android/messaging/ui/home/HomeSubListSearch.java b/app/src/main/java/com/android/messaging/ui/home/HomeSubListSearch.java
new file mode 100644
index 0000000..b88e084
--- /dev/null
+++ b/app/src/main/java/com/android/messaging/ui/home/HomeSubListSearch.java
@@ -0,0 +1,363 @@
+package com.android.messaging.ui.home;
+
+import android.animation.Animator;
+import android.animation.AnimatorListenerAdapter;
+import android.animation.ValueAnimator;
+import android.app.Activity;
+import android.text.Editable;
+import android.text.TextUtils;
+import android.text.TextWatcher;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.inputmethod.EditorInfo;
+import android.widget.EditText;
+import android.widget.ImageButton;
+
+import androidx.annotation.NonNull;
+import androidx.recyclerview.widget.LinearLayoutManager;
+import androidx.recyclerview.widget.RecyclerView;
+
+import com.android.messaging.R;
+import com.android.messaging.util.ImeUtil;
+import com.android.messaging.util.UiUtils;
+
+/** Binds the shared home-style search field on Spam / Archive / Trash lists. */
+public final class HomeSubListSearch {
+
+ private static final int ANIM_MS = 220;
+ private static final int SCROLL_THRESHOLD_DP = 10;
+
+ public interface Listener {
+ void onQueryChanged(String query);
+ }
+
+ private static final class Chrome {
+ View collapse;
+ View search;
+ EditText field;
+ int naturalHeight;
+ boolean shown;
+ int accum;
+ ValueAnimator heightAnimator;
+ }
+
+ private HomeSubListSearch() {
+ }
+
+ public static boolean matches(final String query, final String... fields) {
+ if (TextUtils.isEmpty(query)) {
+ return true;
+ }
+ final String needle = query.trim().toLowerCase();
+ if (needle.isEmpty()) {
+ return true;
+ }
+ final String digits = digitsOnly(needle);
+ for (final String field : fields) {
+ if (TextUtils.isEmpty(field)) {
+ continue;
+ }
+ if (field.toLowerCase().contains(needle)) {
+ return true;
+ }
+ if (!digits.isEmpty() && digitsOnly(field).contains(digits)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ public static void bind(final Activity activity, final Listener listener) {
+ bind(activity, null, listener);
+ }
+
+ public static void bind(final Activity activity, final RecyclerView list,
+ final Listener listener) {
+ if (activity == null || listener == null) {
+ return;
+ }
+ final EditText field = activity.findViewById(R.id.home_sub_search_field);
+ final ImageButton clear = activity.findViewById(R.id.home_sub_search_clear);
+ if (field == null) {
+ return;
+ }
+ field.addTextChangedListener(new TextWatcher() {
+ @Override
+ public void beforeTextChanged(final CharSequence s, final int start, final int count,
+ final int after) {
+ }
+
+ @Override
+ public void onTextChanged(final CharSequence s, final int start, final int before,
+ final int count) {
+ }
+
+ @Override
+ public void afterTextChanged(final Editable s) {
+ final String query = s == null ? "" : s.toString();
+ if (clear != null) {
+ clear.setVisibility(TextUtils.isEmpty(query) ? View.GONE : View.VISIBLE);
+ }
+ listener.onQueryChanged(query);
+ }
+ });
+ field.setOnEditorActionListener((v, actionId, event) -> {
+ if (actionId == EditorInfo.IME_ACTION_SEARCH || actionId == EditorInfo.IME_ACTION_DONE) {
+ ImeUtil.get().hideImeKeyboard(activity, field);
+ return true;
+ }
+ return false;
+ });
+ if (clear != null) {
+ clear.setOnClickListener(v -> field.setText(""));
+ }
+ attachScrollChrome(activity, list);
+ }
+
+ public static void bindListFades(final RecyclerView list, final View topFade,
+ final View bottomFade) {
+ if (list == null) {
+ return;
+ }
+ final Runnable update = () -> {
+ setFadeVisible(topFade, !isListFlushAtTop(list));
+ setFadeVisible(bottomFade, !isListFlushAtBottom(list));
+ };
+ list.addOnScrollListener(new RecyclerView.OnScrollListener() {
+ @Override
+ public void onScrolled(@NonNull final RecyclerView recyclerView, final int dx,
+ final int dy) {
+ update.run();
+ }
+ });
+ list.addOnLayoutChangeListener((v, l, t, r, b, ol, ot, or, ob) -> update.run());
+ list.post(update);
+ }
+
+ private static void setFadeVisible(final View fade, final boolean show) {
+ if (fade == null) {
+ return;
+ }
+ final int visibility = show ? View.VISIBLE : View.GONE;
+ if (fade.getVisibility() != visibility) {
+ fade.setVisibility(visibility);
+ }
+ }
+
+ private static void attachScrollChrome(final Activity activity, final RecyclerView list) {
+ if (activity == null || list == null) {
+ return;
+ }
+ final View collapse = activity.findViewById(R.id.home_sub_search_collapse);
+ final View search = activity.findViewById(R.id.home_sub_search_box);
+ final EditText field = activity.findViewById(R.id.home_sub_search_field);
+ if (collapse == null || search == null) {
+ return;
+ }
+ final Chrome chrome = new Chrome();
+ chrome.collapse = collapse;
+ chrome.search = search;
+ chrome.field = field;
+ search.setVisibility(View.INVISIBLE);
+ search.setAlpha(1f);
+ setChromeInteractive(search, false);
+ applyCollapseHeight(collapse, 0, false);
+ collapse.addOnLayoutChangeListener((v, l, t, r, b, ol, ot, or, ob) -> {
+ if (chrome.shown && v.getHeight() > 0) {
+ chrome.naturalHeight = v.getHeight();
+ }
+ });
+ if (field != null) {
+ field.setOnFocusChangeListener((v, hasFocus) -> {
+ if (hasFocus) {
+ setCollapseShown(chrome, true, true);
+ }
+ });
+ }
+ list.post(() -> applyChromeSlot(chrome, chrome.shown, false));
+ list.addOnScrollListener(new RecyclerView.OnScrollListener() {
+ @Override
+ public void onScrolled(@NonNull final RecyclerView recyclerView, final int dx,
+ final int dy) {
+ if (dy == 0 || isSearchPinned(chrome)) {
+ return;
+ }
+ if (isScrolledToFirstItem(recyclerView) && dy < 0) {
+ chrome.accum = 0;
+ if (field != null) {
+ ImeUtil.get().hideImeKeyboard(activity, field);
+ }
+ setCollapseShown(chrome, false, true);
+ return;
+ }
+ chrome.accum += dy;
+ final int threshold = dp(recyclerView, SCROLL_THRESHOLD_DP);
+ if (chrome.accum > threshold) {
+ chrome.accum = 0;
+ setCollapseShown(chrome, true, true);
+ } else if (chrome.accum < -threshold) {
+ chrome.accum = 0;
+ if (field != null) {
+ ImeUtil.get().hideImeKeyboard(activity, field);
+ }
+ setCollapseShown(chrome, false, true);
+ }
+ }
+ });
+ }
+
+ private static boolean isSearchPinned(final Chrome chrome) {
+ if (chrome.field == null) {
+ return false;
+ }
+ return chrome.field.hasFocus() || !TextUtils.isEmpty(chrome.field.getText());
+ }
+
+ private static boolean isScrolledToFirstItem(final RecyclerView list) {
+ final RecyclerView.LayoutManager manager = list.getLayoutManager();
+ if (!(manager instanceof LinearLayoutManager)) {
+ return !list.canScrollVertically(-1);
+ }
+ final int first = ((LinearLayoutManager) manager).findFirstCompletelyVisibleItemPosition();
+ return first != RecyclerView.NO_POSITION && first <= 0;
+ }
+
+ private static boolean isListFlushAtTop(final RecyclerView list) {
+ if (list.getAdapter() == null || list.getAdapter().getItemCount() == 0) {
+ return true;
+ }
+ final RecyclerView.LayoutManager manager = list.getLayoutManager();
+ if (!(manager instanceof LinearLayoutManager)) {
+ return !list.canScrollVertically(-1);
+ }
+ final LinearLayoutManager linear = (LinearLayoutManager) manager;
+ if (linear.findFirstVisibleItemPosition() > 0) {
+ return false;
+ }
+ final View first = linear.findViewByPosition(0);
+ if (first == null) {
+ return !list.canScrollVertically(-1);
+ }
+ return first.getTop() >= list.getPaddingTop() - 1;
+ }
+
+ private static boolean isListFlushAtBottom(final RecyclerView list) {
+ if (list.getAdapter() == null || list.getAdapter().getItemCount() == 0) {
+ return true;
+ }
+ return !list.canScrollVertically(1);
+ }
+
+ /** Messages chrome: search slides in a clipped slot; slot is 0 when hidden so the list is flush. */
+ private static void setCollapseShown(final Chrome chrome, final boolean shown,
+ final boolean animate) {
+ if (chrome.shown == shown) {
+ return;
+ }
+ chrome.shown = shown;
+ applyChromeSlot(chrome, shown, animate);
+ }
+
+ private static void applyChromeSlot(final Chrome chrome, final boolean searchShown,
+ final boolean animate) {
+ final int slot = resolveNaturalHeight(chrome);
+ final float searchY = searchShown ? 0f : -slot;
+ final int targetHeight = searchShown ? slot : 0;
+ chrome.search.animate().cancel();
+ if (chrome.heightAnimator != null) {
+ chrome.heightAnimator.cancel();
+ chrome.heightAnimator = null;
+ }
+ chrome.search.setAlpha(1f);
+ setChromeInteractive(chrome.search, searchShown);
+ if (!animate || slot <= 0) {
+ chrome.search.setTranslationY(searchY);
+ chrome.search.setVisibility(searchShown ? View.VISIBLE : View.INVISIBLE);
+ applyCollapseHeight(chrome.collapse, targetHeight, searchShown);
+ return;
+ }
+ chrome.search.setVisibility(View.VISIBLE);
+ chrome.search.animate()
+ .translationY(searchY)
+ .setDuration(ANIM_MS)
+ .setInterpolator(UiUtils.DEFAULT_INTERPOLATOR)
+ .withEndAction(() -> {
+ if (!chrome.shown) {
+ chrome.search.setVisibility(View.INVISIBLE);
+ }
+ });
+ final int from = chrome.collapse.getHeight();
+ final ValueAnimator va = ValueAnimator.ofInt(from, targetHeight);
+ chrome.heightAnimator = va;
+ va.setDuration(ANIM_MS);
+ va.setInterpolator(UiUtils.DEFAULT_INTERPOLATOR);
+ va.addUpdateListener(a -> applyCollapseHeight(chrome.collapse,
+ (int) a.getAnimatedValue(), false));
+ va.addListener(new AnimatorListenerAdapter() {
+ @Override
+ public void onAnimationEnd(final Animator animation) {
+ applyCollapseHeight(chrome.collapse, targetHeight, searchShown);
+ chrome.heightAnimator = null;
+ }
+ });
+ va.start();
+ }
+
+ private static void applyCollapseHeight(final View collapse, final int height,
+ final boolean wrap) {
+ final ViewGroup.LayoutParams lp = collapse.getLayoutParams();
+ if (lp == null) {
+ return;
+ }
+ lp.height = wrap && height > 0 ? ViewGroup.LayoutParams.WRAP_CONTENT : height;
+ collapse.setLayoutParams(lp);
+ }
+
+ private static void setChromeInteractive(final View view, final boolean interactive) {
+ if (view == null) {
+ return;
+ }
+ view.setClickable(interactive);
+ view.setImportantForAccessibility(interactive
+ ? View.IMPORTANT_FOR_ACCESSIBILITY_YES
+ : View.IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS);
+ if (view instanceof ViewGroup) {
+ ((ViewGroup) view).setDescendantFocusability(interactive
+ ? ViewGroup.FOCUS_AFTER_DESCENDANTS
+ : ViewGroup.FOCUS_BLOCK_DESCENDANTS);
+ }
+ view.setOnTouchListener(interactive ? null : (v, event) -> true);
+ }
+
+ private static int resolveNaturalHeight(final Chrome chrome) {
+ if (chrome.naturalHeight > 0) {
+ return chrome.naturalHeight;
+ }
+ if (chrome.search.getHeight() > 0) {
+ chrome.naturalHeight = chrome.search.getHeight();
+ return chrome.naturalHeight;
+ }
+ final int width = Math.max(chrome.collapse.getWidth(), chrome.search.getWidth());
+ chrome.search.measure(
+ View.MeasureSpec.makeMeasureSpec(Math.max(width, 0),
+ width > 0 ? View.MeasureSpec.EXACTLY : View.MeasureSpec.UNSPECIFIED),
+ View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED));
+ chrome.naturalHeight = chrome.search.getMeasuredHeight();
+ return chrome.naturalHeight;
+ }
+
+ private static int dp(final View view, final int dp) {
+ return (int) (dp * view.getResources().getDisplayMetrics().density + 0.5f);
+ }
+
+ private static String digitsOnly(final String value) {
+ final StringBuilder out = new StringBuilder(value.length());
+ for (int i = 0; i < value.length(); i++) {
+ final char c = value.charAt(i);
+ if (Character.isDigit(c)) {
+ out.append(c);
+ }
+ }
+ return out.toString();
+ }
+}
diff --git a/app/src/main/java/com/android/messaging/ui/home/HomeUiMotion.java b/app/src/main/java/com/android/messaging/ui/home/HomeUiMotion.java
index c396d93..08c35d6 100644
--- a/app/src/main/java/com/android/messaging/ui/home/HomeUiMotion.java
+++ b/app/src/main/java/com/android/messaging/ui/home/HomeUiMotion.java
@@ -282,6 +282,97 @@ public final class HomeUiMotion {
}
}
+ /**
+ * Overlay chip swap used by Select/Edit ↔ round check (list, archive, spam).
+ * Incoming pops in (scale + fade); outgoing shrinks out.
+ */
+ public static void swapOverlayChip(final View outgoing, final View incoming) {
+ if (incoming == null && outgoing == null) {
+ return;
+ }
+ final boolean alreadyIn = incoming != null
+ && incoming.getVisibility() == View.VISIBLE
+ && incoming.getAlpha() > 0.99f
+ && outgoing != null
+ && outgoing.getVisibility() != View.VISIBLE;
+ if (alreadyIn) {
+ return;
+ }
+ if (!enabled()) {
+ applyOverlayChipInstant(outgoing, incoming);
+ return;
+ }
+ if (outgoing != null) {
+ outgoing.setTag(R.id.home_sub_header_done, null);
+ outgoing.animate().cancel();
+ if (outgoing.getVisibility() == View.VISIBLE) {
+ beginLayer(outgoing);
+ outgoing.animate()
+ .alpha(0f)
+ .scaleX(0.6f)
+ .scaleY(0.6f)
+ .setDuration(DOCK_SWAP_MS)
+ .setInterpolator(EASE_MATERIAL)
+ .withEndAction(() -> {
+ outgoing.setVisibility(View.INVISIBLE);
+ outgoing.setAlpha(1f);
+ outgoing.setScaleX(1f);
+ outgoing.setScaleY(1f);
+ endLayer(outgoing);
+ })
+ .start();
+ } else if (outgoing.getVisibility() != View.INVISIBLE) {
+ outgoing.setVisibility(View.INVISIBLE);
+ }
+ }
+ if (incoming != null) {
+ incoming.animate().cancel();
+ incoming.setVisibility(View.VISIBLE);
+ incoming.setAlpha(0f);
+ incoming.setScaleX(0.6f);
+ incoming.setScaleY(0.6f);
+ beginLayer(incoming);
+ final Runnable play = () -> incoming.animate()
+ .alpha(1f)
+ .scaleX(1f)
+ .scaleY(1f)
+ .setDuration(DOCK_SWAP_MS)
+ .setInterpolator(EASE_MATERIAL)
+ .withEndAction(() -> endLayer(incoming))
+ .start();
+ if (incoming.getWidth() == 0) {
+ final Object token = new Object();
+ incoming.setTag(R.id.home_sub_header_done, token);
+ incoming.post(() -> {
+ if (incoming.getTag(R.id.home_sub_header_done) != token) {
+ return;
+ }
+ play.run();
+ });
+ } else {
+ incoming.setTag(R.id.home_sub_header_done, null);
+ play.run();
+ }
+ }
+ }
+
+ private static void applyOverlayChipInstant(final View outgoing, final View incoming) {
+ if (incoming != null) {
+ incoming.animate().cancel();
+ incoming.setVisibility(View.VISIBLE);
+ incoming.setAlpha(1f);
+ incoming.setScaleX(1f);
+ incoming.setScaleY(1f);
+ }
+ if (outgoing != null && outgoing != incoming) {
+ outgoing.animate().cancel();
+ outgoing.setVisibility(View.INVISIBLE);
+ outgoing.setAlpha(1f);
+ outgoing.setScaleX(1f);
+ outgoing.setScaleY(1f);
+ }
+ }
+
/**
* Swaps two headers that share a vertical container, so they are never both laid out.
*/
@@ -429,6 +520,71 @@ public final class HomeUiMotion {
}
}
+ /**
+ * Spam / Archive / Trash selection dock. Animates the inner pill after the overlay has
+ * laid out so a GONE→VISIBLE snap does not cut the scale+fade short.
+ */
+ public static void swapSubListDock(final View bar, final boolean selectionActive) {
+ if (bar == null) {
+ return;
+ }
+ final View pill = bar.findViewById(R.id.home_sub_select_pill);
+ final View target = pill != null ? pill : bar;
+ target.animate().cancel();
+ bar.setTag(R.id.home_sub_select_bar, null);
+ if (!enabled()) {
+ bar.setVisibility(selectionActive ? View.VISIBLE : View.GONE);
+ resetDockTransform(target);
+ return;
+ }
+ if (selectionActive) {
+ final boolean showing = bar.getVisibility() == View.VISIBLE
+ && target.getAlpha() > 0.99f
+ && target.getScaleX() > 0.99f;
+ if (showing) {
+ return;
+ }
+ bar.setVisibility(View.VISIBLE);
+ target.setAlpha(0f);
+ target.setScaleX(0.86f);
+ target.setScaleY(0.86f);
+ beginLayer(target);
+ final Object token = new Object();
+ bar.setTag(R.id.home_sub_select_bar, token);
+ bar.post(() -> {
+ if (bar.getTag(R.id.home_sub_select_bar) != token) {
+ return;
+ }
+ target.animate()
+ .alpha(1f)
+ .scaleX(1f)
+ .scaleY(1f)
+ .setDuration(DOCK_SWAP_MS)
+ .setInterpolator(EASE_MATERIAL)
+ .withEndAction(() -> endLayer(target))
+ .start();
+ });
+ return;
+ }
+ if (bar.getVisibility() != View.VISIBLE) {
+ resetDockTransform(target);
+ return;
+ }
+ beginLayer(target);
+ target.animate()
+ .alpha(0f)
+ .scaleX(0.86f)
+ .scaleY(0.86f)
+ .setDuration(DOCK_SWAP_MS)
+ .setInterpolator(EASE_MATERIAL)
+ .withEndAction(() -> {
+ bar.setVisibility(View.GONE);
+ resetDockTransform(target);
+ endLayer(target);
+ })
+ .start();
+ }
+
/** Main bottom nav ↔ selection action dock; both sit stacked at the bottom of a frame. */
public static void swapBottomDock(final View mainNav, final View selectionBar,
final boolean selectionActive, final Runnable onEnd) {
@@ -445,6 +601,23 @@ public final class HomeUiMotion {
}
return;
}
+ final boolean barVisible = selectionBar != null
+ && selectionBar.getVisibility() == View.VISIBLE
+ && selectionBar.getAlpha() > 0.99f;
+ if (selectionActive && barVisible) {
+ if (onEnd != null) {
+ onEnd.run();
+ }
+ return;
+ }
+ if (!selectionActive && (selectionBar == null
+ || selectionBar.getVisibility() != View.VISIBLE)
+ && (mainNav == null || mainNav.getVisibility() == View.VISIBLE)) {
+ if (onEnd != null) {
+ onEnd.run();
+ }
+ return;
+ }
if (mainNav != null) {
mainNav.animate().cancel();
}
@@ -456,14 +629,17 @@ public final class HomeUiMotion {
if (selectionActive) {
if (mainNav != null && mainNav.getVisibility() == View.VISIBLE) {
+ beginLayer(mainNav);
mainNav.animate()
.alpha(0f)
+ .scaleX(0.88f)
+ .scaleY(0.88f)
.setDuration(DOCK_SWAP_MS)
.setInterpolator(EASE_MATERIAL)
.withEndAction(() -> {
mainNav.setVisibility(View.GONE);
- mainNav.setAlpha(1f);
- mainNav.setTranslationY(0f);
+ resetDockTransform(mainNav);
+ endLayer(mainNav);
})
.start();
} else if (mainNav != null) {
@@ -472,26 +648,41 @@ public final class HomeUiMotion {
if (selectionBar != null) {
selectionBar.setVisibility(View.VISIBLE);
selectionBar.setAlpha(0f);
- selectionBar.setTranslationY(0f);
- selectionBar.animate()
+ selectionBar.setScaleX(0.86f);
+ selectionBar.setScaleY(0.86f);
+ beginLayer(selectionBar);
+ final Runnable play = () -> selectionBar.animate()
.alpha(1f)
+ .scaleX(1f)
+ .scaleY(1f)
.setDuration(DOCK_SWAP_MS)
.setInterpolator(EASE_MATERIAL)
- .withEndAction(finish)
+ .withEndAction(() -> {
+ endLayer(selectionBar);
+ finish.run();
+ })
.start();
+ if (selectionBar.getWidth() == 0) {
+ selectionBar.post(play);
+ } else {
+ play.run();
+ }
} else {
finish.run();
}
} else {
if (selectionBar != null && selectionBar.getVisibility() == View.VISIBLE) {
+ beginLayer(selectionBar);
selectionBar.animate()
.alpha(0f)
+ .scaleX(0.86f)
+ .scaleY(0.86f)
.setDuration(DOCK_SWAP_MS)
.setInterpolator(EASE_MATERIAL)
.withEndAction(() -> {
selectionBar.setVisibility(View.GONE);
- selectionBar.setAlpha(1f);
- selectionBar.setTranslationY(0f);
+ resetDockTransform(selectionBar);
+ endLayer(selectionBar);
})
.start();
} else if (selectionBar != null) {
@@ -500,12 +691,19 @@ public final class HomeUiMotion {
if (mainNav != null) {
mainNav.setVisibility(View.VISIBLE);
mainNav.setAlpha(0f);
- mainNav.setTranslationY(0f);
+ mainNav.setScaleX(0.88f);
+ mainNav.setScaleY(0.88f);
+ beginLayer(mainNav);
mainNav.animate()
.alpha(1f)
+ .scaleX(1f)
+ .scaleY(1f)
.setDuration(DOCK_SWAP_MS)
.setInterpolator(EASE_MATERIAL)
- .withEndAction(finish)
+ .withEndAction(() -> {
+ endLayer(mainNav);
+ finish.run();
+ })
.start();
} else {
finish.run();
@@ -731,16 +929,21 @@ public final class HomeUiMotion {
final boolean selectionActive) {
if (mainNav != null) {
mainNav.setVisibility(selectionActive ? View.GONE : View.VISIBLE);
- mainNav.setAlpha(1f);
- mainNav.setTranslationY(0f);
+ resetDockTransform(mainNav);
}
if (selectionBar != null) {
selectionBar.setVisibility(selectionActive ? View.VISIBLE : View.GONE);
- selectionBar.setAlpha(1f);
- selectionBar.setTranslationY(0f);
+ resetDockTransform(selectionBar);
}
}
+ private static void resetDockTransform(final View view) {
+ view.setAlpha(1f);
+ view.setTranslationY(0f);
+ view.setScaleX(1f);
+ view.setScaleY(1f);
+ }
+
public static void popoverResetInstant(final View content) {
if (content == null) {
return;
diff --git a/app/src/main/java/com/android/messaging/util/SimNameUtil.java b/app/src/main/java/com/android/messaging/util/SimNameUtil.java
index a6f7c41..23e7c10 100644
--- a/app/src/main/java/com/android/messaging/util/SimNameUtil.java
+++ b/app/src/main/java/com/android/messaging/util/SimNameUtil.java
@@ -8,6 +8,7 @@ import android.text.TextUtils;
import com.android.messaging.R;
import com.android.messaging.datamodel.data.ParticipantData;
+import java.util.Collections;
import java.util.List;
public final class SimNameUtil {
@@ -25,28 +26,45 @@ public final class SimNameUtil {
}
}
- public static String getSimDisplayName(final Context context, final String selfId) {
- List infos;
+ public static List getActiveSubscriptions() {
try {
- infos = PhoneUtils.getDefault().getActiveSubscriptionInfoList();
+ final List infos =
+ PhoneUtils.getDefault().getActiveSubscriptionInfoList();
+ if (infos == null) {
+ return Collections.emptyList();
+ }
+ return infos;
} catch (final Exception ignored) {
- infos = null;
+ return Collections.emptyList();
}
- if (infos == null || infos.isEmpty()) {
- return context.getString(R.string.sim_slot_identifier, "1");
+ }
+
+ public static SubscriptionInfo getSubscriptionForSelfId(final String selfId) {
+ final List infos = getActiveSubscriptions();
+ if (infos.isEmpty()) {
+ return null;
}
- SubscriptionInfo match = infos.get(0);
- if (infos.size() > 1 && !TextUtils.isEmpty(selfId)) {
+ if (!TextUtils.isEmpty(selfId)) {
for (final SubscriptionInfo info : infos) {
final String participantId =
ParticipantData.getCachedParticipantId(info.getSubscriptionId());
if (TextUtils.equals(participantId, selfId)) {
- match = info;
- break;
+ return info;
}
}
}
- return formatName(context, match);
+ return infos.get(0);
+ }
+
+ public static String getDisplayName(final Context context, final SubscriptionInfo info) {
+ if (info == null) {
+ return context.getString(R.string.sim_slot_identifier, "1");
+ }
+ return formatName(context, info);
+ }
+
+ public static String getSimDisplayName(final Context context, final String selfId) {
+ return getDisplayName(context, getSubscriptionForSelfId(selfId));
}
private static final int NAME_SOURCE_USER_INPUT = 2;
diff --git a/app/src/main/java/com/android/messaging/widget/WidgetConversationListService.java b/app/src/main/java/com/android/messaging/widget/WidgetConversationListService.java
index 096783b..603d0f6 100644
--- a/app/src/main/java/com/android/messaging/widget/WidgetConversationListService.java
+++ b/app/src/main/java/com/android/messaging/widget/WidgetConversationListService.java
@@ -68,7 +68,7 @@ public class WidgetConversationListService extends RemoteViewsService {
protected Cursor doQuery() {
return mContext.getContentResolver().query(MessagingContentProvider.CONVERSATIONS_URI,
ConversationListItemData.PROJECTION,
- ConversationListData.WHERE_NOT_ARCHIVED,
+ ConversationListData.WHERE_INBOX,
null, // selection args
ConversationListData.SORT_ORDER);
}
diff --git a/app/src/main/res/drawable/home_bg_alert_dialog.xml b/app/src/main/res/drawable/home_bg_details_card.xml
similarity index 80%
rename from app/src/main/res/drawable/home_bg_alert_dialog.xml
rename to app/src/main/res/drawable/home_bg_details_card.xml
index 8463ce6..70e0f42 100644
--- a/app/src/main/res/drawable/home_bg_alert_dialog.xml
+++ b/app/src/main/res/drawable/home_bg_details_card.xml
@@ -1,6 +1,6 @@
-
+
diff --git a/app/src/main/res/drawable/home_bg_mute_badge.xml b/app/src/main/res/drawable/home_bg_mute_badge.xml
new file mode 100644
index 0000000..6c311db
--- /dev/null
+++ b/app/src/main/res/drawable/home_bg_mute_badge.xml
@@ -0,0 +1,17 @@
+
+
+ -
+
+
+
+
+ -
+
+
+
+
+
diff --git a/app/src/main/res/drawable/home_bg_unblock_pill.xml b/app/src/main/res/drawable/home_bg_unblock_pill.xml
new file mode 100644
index 0000000..80939a2
--- /dev/null
+++ b/app/src/main/res/drawable/home_bg_unblock_pill.xml
@@ -0,0 +1,10 @@
+
+
+ -
+
+
+
+
+
+
diff --git a/app/src/main/res/layout/conversation_fragment.xml b/app/src/main/res/layout/conversation_fragment.xml
index a8a6919..81563f7 100644
--- a/app/src/main/res/layout/conversation_fragment.xml
+++ b/app/src/main/res/layout/conversation_fragment.xml
@@ -141,6 +141,8 @@
android:layout_width="match_parent"
android:layout_height="wrap_content" />
+
+
diff --git a/app/src/main/res/layout/conversation_list_fragment.xml b/app/src/main/res/layout/conversation_list_fragment.xml
index e596bd4..34d25be 100644
--- a/app/src/main/res/layout/conversation_list_fragment.xml
+++ b/app/src/main/res/layout/conversation_list_fragment.xml
@@ -400,41 +400,70 @@
android:layout_gravity="bottom"
android:clipChildren="false"
android:clipToPadding="false"
- android:gravity="center_vertical"
+ android:gravity="center"
android:orientation="horizontal"
android:paddingStart="@dimen/looply_space_md"
android:paddingEnd="@dimen/looply_space_md"
android:paddingBottom="@dimen/looply_space_md"
android:visibility="gone">
-
+ android:paddingStart="@dimen/home_nav_item_padding"
+ android:paddingTop="@dimen/home_nav_item_padding"
+ android:paddingEnd="@dimen/home_nav_item_padding"
+ android:paddingBottom="@dimen/home_nav_dock_padding_bottom">
-
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/layout/conversation_list_item_view.xml b/app/src/main/res/layout/conversation_list_item_view.xml
index 7de1cff..e2e152d 100644
--- a/app/src/main/res/layout/conversation_list_item_view.xml
+++ b/app/src/main/res/layout/conversation_list_item_view.xml
@@ -151,7 +151,9 @@
+ android:layout_height="wrap_content"
+ android:clipChildren="false"
+ android:clipToPadding="false">
+
-
+ android:layout_height="0dp"
+ android:background="@color/home_bg"
+ android:clipChildren="true"
+ android:clipToPadding="true">
+
+
+
+ android:background="@color/home_bg"
+ android:clipChildren="false"
+ android:clipToPadding="false">
+
+
+
+
+
diff --git a/app/src/main/res/layout/home_conversation_trash_bar.xml b/app/src/main/res/layout/home_conversation_trash_bar.xml
new file mode 100644
index 0000000..e1b29f7
--- /dev/null
+++ b/app/src/main/res/layout/home_conversation_trash_bar.xml
@@ -0,0 +1,36 @@
+
+
+
+
+
+
+
diff --git a/app/src/main/res/layout/home_delete_alert_dialog.xml b/app/src/main/res/layout/home_delete_alert_dialog.xml
deleted file mode 100644
index ea60000..0000000
--- a/app/src/main/res/layout/home_delete_alert_dialog.xml
+++ /dev/null
@@ -1,44 +0,0 @@
-
-
-
-
-
-
-
-
-
diff --git a/app/src/main/res/layout/home_lock_pattern_body.xml b/app/src/main/res/layout/home_lock_pattern_body.xml
index 02df0b8..7ea88d1 100644
--- a/app/src/main/res/layout/home_lock_pattern_body.xml
+++ b/app/src/main/res/layout/home_lock_pattern_body.xml
@@ -5,17 +5,20 @@
android:gravity="center_horizontal"
android:orientation="vertical">
-
+ android:layout_weight="1"
+ android:gravity="center"
+ android:orientation="vertical">
-
+ android:layout_height="wrap_content"
+ android:layout_marginTop="@dimen/looply_space_lg" />
+
-
+
+
+
+
+ android:layout_height="wrap_content" />
diff --git a/app/src/main/res/layout/home_message_action_popup.xml b/app/src/main/res/layout/home_message_action_popup.xml
index 9d4a755..47936bf 100644
--- a/app/src/main/res/layout/home_message_action_popup.xml
+++ b/app/src/main/res/layout/home_message_action_popup.xml
@@ -80,28 +80,6 @@
android:text="@string/home_message_menu_star" />
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/layout/home_modal.xml b/app/src/main/res/layout/home_modal.xml
index 68a0eba..773bbf0 100644
--- a/app/src/main/res/layout/home_modal.xml
+++ b/app/src/main/res/layout/home_modal.xml
@@ -95,6 +95,18 @@
android:textStyle="bold"
android:visibility="gone" />
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
@@ -46,10 +47,9 @@
@@ -68,10 +68,9 @@
@@ -91,13 +90,33 @@
+
+
+
+
+
+
+
diff --git a/app/src/main/res/layout/home_popover_filter.xml b/app/src/main/res/layout/home_popover_filter.xml
index 90049af..838c47d 100644
--- a/app/src/main/res/layout/home_popover_filter.xml
+++ b/app/src/main/res/layout/home_popover_filter.xml
@@ -24,7 +24,7 @@
android:layout_marginEnd="8dp"
android:contentDescription="@null"
android:src="@drawable/ic_home_check"
- android:visibility="visible" />
+ android:visibility="invisible" />
-
+ android:layout_marginEnd="8dp"
+ android:contentDescription="@null"
+ android:src="@drawable/ic_home_check"
+ android:visibility="invisible" />
-
+ android:layout_marginEnd="8dp"
+ android:contentDescription="@null"
+ android:src="@drawable/ic_home_check"
+ android:visibility="invisible" />
+
+
+
+
+
+
+
+
+
-
+ android:layout_marginEnd="8dp"
+ android:contentDescription="@null"
+ android:src="@drawable/ic_home_check"
+ android:visibility="invisible" />
-
+ android:layout_marginEnd="8dp"
+ android:contentDescription="@null"
+ android:src="@drawable/ic_home_check"
+ android:visibility="invisible" />
-
+
+ android:ellipsize="end"
+ android:maxWidth="72dp"
+ android:maxLines="1" />
+ android:layout_weight="1" />
-
+ android:layout_marginEnd="8dp"
+ android:contentDescription="@null"
+ android:src="@drawable/ic_home_check"
+ android:visibility="invisible" />
+ android:ellipsize="end"
+ android:maxWidth="72dp"
+ android:maxLines="1" />
+ android:layout_weight="1" />
+ android:orientation="vertical">
+ android:maxLines="1" />
diff --git a/app/src/main/res/layout/home_spam_activity.xml b/app/src/main/res/layout/home_spam_activity.xml
index a7bfcdb..2aba730 100644
--- a/app/src/main/res/layout/home_spam_activity.xml
+++ b/app/src/main/res/layout/home_spam_activity.xml
@@ -1,28 +1,32 @@
-
+ android:layout_height="0dp"
+ android:background="@color/home_bg"
+ android:clipChildren="true"
+ android:clipToPadding="true">
+
+
+
+ android:layout_weight="1"
+ android:clipChildren="false"
+ android:clipToPadding="false">
+
+
diff --git a/app/src/main/res/layout/home_spam_fragment.xml b/app/src/main/res/layout/home_spam_fragment.xml
new file mode 100644
index 0000000..c8ba658
--- /dev/null
+++ b/app/src/main/res/layout/home_spam_fragment.xml
@@ -0,0 +1,41 @@
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/layout/home_spam_list_item.xml b/app/src/main/res/layout/home_spam_list_item.xml
new file mode 100644
index 0000000..66127d5
--- /dev/null
+++ b/app/src/main/res/layout/home_spam_list_item.xml
@@ -0,0 +1,108 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/layout/home_sub_header.xml b/app/src/main/res/layout/home_sub_header.xml
index 5c04ff3..fd3a5a4 100644
--- a/app/src/main/res/layout/home_sub_header.xml
+++ b/app/src/main/res/layout/home_sub_header.xml
@@ -1,54 +1,124 @@
-
-
+
-
+
-
-
+
-
+
+
-
+
+
+
+
+
-
+ android:layout_gravity="end|center_vertical"
+ android:clipChildren="false"
+ android:clipToPadding="false"
+ android:gravity="center_vertical"
+ android:orientation="horizontal"
+ android:visibility="gone">
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/layout/home_sub_search_field.xml b/app/src/main/res/layout/home_sub_search_field.xml
new file mode 100644
index 0000000..0afc0ae
--- /dev/null
+++ b/app/src/main/res/layout/home_sub_search_field.xml
@@ -0,0 +1,63 @@
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/layout/home_sub_select_bar.xml b/app/src/main/res/layout/home_sub_select_bar.xml
new file mode 100644
index 0000000..e3d5e0e
--- /dev/null
+++ b/app/src/main/res/layout/home_sub_select_bar.xml
@@ -0,0 +1,38 @@
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/values-ko/looply_strings.xml b/app/src/main/res/values-ko/looply_strings.xml
index 85d2a7c..e5bc66d 100644
--- a/app/src/main/res/values-ko/looply_strings.xml
+++ b/app/src/main/res/values-ko/looply_strings.xml
@@ -14,6 +14,19 @@
Message Q
프로필
편집
+ 선택
+ 모두 스팸 해제
+ 스팸 해제
+ 모두 보관 해제
+ 보관 해제
+ 모두 비우기
+ 편집할 대화가 없습니다
+ 선택할 항목이 없습니다
+ 휴지통이 비어 있습니다
+ 차단 해제할 스팸이 없습니다
+ 보관 해제할 대화가 없습니다
+ 스팸을 해제했습니다
+ 대화 %1$d개의 보관을 해제했습니다
검색
개인
업무
@@ -142,10 +155,18 @@
메시지를 자동으로 처리할 규칙을 만드세요.
대화가 없습니다
새 채팅을 시작하면 여기에 표시됩니다.
+ 안 읽은 대화가 없습니다
+ 안 읽은 대화가 여기에 표시됩니다.
+ 업무 대화가 없습니다
+ 업무 SIM이나 업무 관련 메시지가 여기에 표시됩니다.
+ 개인 대화가 없습니다
+ 개인 SIM이나 지인과의 대화가 여기에 표시됩니다.
대화를 불러오는 중
잠시 후 메시지가 여기에 나타납니다.
보관된 대화가 없습니다
보관한 채팅은 복원하거나 삭제할 때까지 여기에 있습니다.
+ 휴지통이 비어 있습니다
+ 삭제한 채팅은 30일 동안 여기에 있다가 영구 삭제됩니다.
스팸 번호가 없습니다
차단된 발신자가 여기에 표시됩니다. 항목을 눌러 해제하세요.
표시할 연락처가 없습니다
@@ -172,11 +193,20 @@
안 읽음
완료
선택한 메시지 삭제
+ 스팸신고
+ 보관함
+ 대화 %1$d개를 보관했습니다
+ 스팸으로 신고할까요?
+ 이 발신자를 차단하고 대화를 스팸으로 보냅니다.
+ 발신자 %1$d명을 차단하고 대화를 스팸으로 보냅니다.
+ 그룹 대화는 스팸으로 신고할 수 없습니다
+ 스팸으로 신고했습니다
+ 대화 %1$d개를 스팸으로 신고했습니다
1개 선택됨
복사
전달
공유
- 상세
+ 세부정보
저장
다시 보내기
다운로드
@@ -191,18 +221,43 @@
별표 해제
삭제
더 선택
- 상세
- 이 대화가 삭제됩니다.\n\n스팸으로 신고하면 발신자도 차단 목록에 추가됩니다.
- 선택한 대화 %1$d개가 삭제됩니다.\n\n스팸으로 신고하면 1:1 대화 발신자도 차단 목록에 추가됩니다.
+ 세부정보
+ 세부정보
+ 상태
+ 수신됨
+ 전송됨
+ 유형
+ 우선순위
+ 제목
+ 크기
+ 사진
+ 동영상
+ 음성
+ 연락처
+ 첨부파일
+ 이 대화는 휴지통으로 옮겨지고 30일 후 영구 삭제됩니다.
+ 선택한 대화 %1$d개가 휴지통으로 옮겨지고 30일 후 영구 삭제됩니다.
삭제
- 삭제하고 스팸 신고
대화를 삭제했습니다
대화 %1$d개를 삭제했습니다
- 삭제하고 스팸으로 신고했습니다
- 대화 %1$d개를 삭제하고 스팸으로 신고했습니다
+ 휴지통으로 옮겼습니다
+ 대화 %1$d개를 휴지통으로 옮겼습니다
+ 휴지통에서 복원했습니다
+ 이 대화는 휴지통에 있습니다. 메시지를 보내려면 복원하세요.
+ 지금 삭제할까요?
+ 이 대화가 바로 삭제됩니다. 되돌릴 수 없습니다.
+ 대화 %1$d개가 바로 삭제됩니다. 되돌릴 수 없습니다.
+ 모두 비울까요?
+ 휴지통의 모든 대화가 바로 삭제됩니다. 되돌릴 수 없습니다.
+ 휴지통 메시지 삭제
스팸
목록의 번호에서 온 메시지는 차단됩니다. 항목을 눌러 제거하세요.
스팸 번호가 없습니다.
+ 차단된 발신자
+ 이 번호를 차단 해제할까요?
+ %1$s에서 온 메시지와 알림을 다시 받게 됩니다.
+ 차단 해제
+ 복원
뒤로
닫기
추가
@@ -211,10 +266,13 @@
메시지
스팸
- 보관함
- 보관함
- 보관한 대화는 복원하거나 삭제할 때까지 여기에 있습니다.
- 보관된 대화가 없습니다
+ 보관함
+ 휴지통
+ 보관함
+ 보관한 대화는 복원하거나 삭제할 때까지 여기에 있습니다.
+ 휴지통
+ 삭제한 대화는 30일 동안 여기에 있다가 영구 삭제됩니다.
+ 휴지통이 비어 있습니다
필터
안 읽음
업무
@@ -235,6 +293,9 @@
검색
메시지 또는 연락처 검색
+ 검색
+ 결과가 없습니다
+ “%1$s”와 일치하는 항목이 없습니다.
지우기
초기화
음성 검색
@@ -373,6 +434,7 @@
안 읽음으로 표시했습니다
읽음으로 표시했습니다
알림 숨기기
+ 알림 켜기
삭제
보관
기본 SMS 앱을 Messages로 설정하세요
diff --git a/app/src/main/res/values/looply_dimens.xml b/app/src/main/res/values/looply_dimens.xml
index 8af0307..c5c635e 100644
--- a/app/src/main/res/values/looply_dimens.xml
+++ b/app/src/main/res/values/looply_dimens.xml
@@ -99,6 +99,7 @@
-4dp
20dp
8dp
+ 18dp
12dp
22dp
@@ -122,6 +123,9 @@
52dp
55dp
5dp
+ 104dp
+ 148dp
+ 20dp
6dp
3dp
diff --git a/app/src/main/res/values/looply_strings.xml b/app/src/main/res/values/looply_strings.xml
index 5770074..d6eeb7d 100644
--- a/app/src/main/res/values/looply_strings.xml
+++ b/app/src/main/res/values/looply_strings.xml
@@ -15,6 +15,19 @@
Message Q
Profile
Edit
+ Select
+ Unblock all
+ Unblock
+ Unarchive all
+ Unarchive
+ Empty All
+ Nothing to edit
+ Nothing to select
+ Trash is empty
+ No spam to unblock
+ No conversations to unarchive
+ Unblocked
+ %1$d conversations unarchived
Search
Personal
Work
@@ -143,10 +156,18 @@
Create rules to handle messages automatically.
No conversations yet
Start a new chat and it will show up here.
+ You\'re all caught up
+ Unread conversations will show up here.
+ No work conversations
+ Chats on your work SIM, and business messages, appear here.
+ No personal conversations
+ Chats on your personal SIM, and messages from people you know, appear here.
Loading conversations
Your messages will appear here in a moment.
No archived conversations
Archived chats stay here until you restore or delete them.
+ Trash is empty
+ Deleted chats stay here for 30 days, then they are permanently deleted.
No spam numbers yet
Blocked senders will appear here. Tap an entry to unblock.
No contacts to show
@@ -173,6 +194,15 @@
Unread
Done
Delete selected messages
+ Spam
+ Archive
+ %1$d conversations archived
+ Report as spam?
+ This sender will be blocked and the conversation moved to Spam.
+ %1$d senders will be blocked and their conversations moved to Spam.
+ Group conversations can\'t be reported as spam
+ Reported as spam
+ %1$d conversations reported as spam
1 selected
Copy
Forward
@@ -193,17 +223,42 @@
Delete
Select more
Details
- This conversation will be deleted.\n\nReporting it as spam also adds the sender to your blocked list.
- The %1$d selected conversations will be deleted.\n\nReporting them as spam also adds the senders of one-to-one conversations to your blocked list.
+ Details
+ Status
+ Received
+ Sent
+ Type
+ Priority
+ Subject
+ Size
+ Photo
+ Video
+ Audio
+ Contact
+ Attachment
+ This conversation will move to Trash and be permanently deleted after 30 days.
+ The %1$d selected conversations will move to Trash and be permanently deleted after 30 days.
Delete
- Delete and report spam
Conversation deleted
%1$d conversations deleted
- Deleted and reported as spam
- %1$d conversations deleted and reported as spam
+ Moved to Trash
+ %1$d conversations moved to Trash
+ Restored from Trash
+ This conversation is in Trash. Restore it to send messages.
+ Delete permanently?
+ This conversation will be deleted now. This can\'t be undone.
+ %1$d conversations will be deleted now. This can\'t be undone.
+ Empty All?
+ All conversations in trash will be deleted now. This can\'t be undone.
+ Delete trash messages
Spam
Messages from listed numbers are blocked. Tap an entry to remove it.
No spam numbers yet.
+ Blocked sender
+ Unblock this number?
+ You\'ll receive messages and notifications from %1$s again.
+ Unblock
+ Restore
Back
Close
Add
@@ -213,10 +268,13 @@
Messages
Spam
- Archive
- Archive
- Archived conversations stay here until you restore or delete them.
- No archived conversations
+ Archive
+ Trash
+ Archive
+ Archived conversations stay here until you restore or delete them.
+ Trash
+ Deleted conversations stay here for 30 days, then they are permanently deleted.
+ Trash is empty
Filter By
Unread
Work
@@ -238,6 +296,9 @@
Search
Search messages or contacts
+ Search
+ No results
+ Nothing matches “%1$s”.
Clear
Reset
Voice search
@@ -380,6 +441,7 @@
Marked as unread
Marked as read
Hide alerts
+ Show alerts
Delete
Archive
Set Messages as the default SMS app
diff --git a/app/src/main/res/values/versions.xml b/app/src/main/res/values/versions.xml
index 14c6b47..a021372 100644
--- a/app/src/main/res/values/versions.xml
+++ b/app/src/main/res/values/versions.xml
@@ -16,7 +16,7 @@
-->
- 2
+ 3