Messaging: Split trash from archive and match home selection chrome

Inbox delete now parks conversations in a 30-day trash, and spam/archive/trash lists share the home select/search chrome.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-09-29 06:16:01 +09:00
co-authored by Cursor
parent 47d735be2c
commit a6b23f0f3c
68 changed files with 4477 additions and 555 deletions
@@ -533,6 +533,7 @@ public class BugleDatabaseOperations {
// the sender is blocked, or we have been told to keep it archived.
if (!keepArchived) {
values.put(ConversationColumns.ARCHIVE_STATUS, 0);
values.put(ConversationColumns.TRASH_TIMESTAMP, 0);
}
final MessageData message = readMessage(dbWrapper, messageId);
@@ -575,6 +576,20 @@ public class BugleDatabaseOperations {
updateConversationRowIfExists(dbWrapper, conversationId, values);
}
@DoesNotRunOnMainThread
public static void updateConversationTrashTimestampInTransaction(
final DatabaseWrapper dbWrapper, final String conversationId,
final long trashTimestamp) {
Assert.isNotMainThread();
Assert.isTrue(dbWrapper.getDatabase().inTransaction());
final ContentValues values = new ContentValues();
values.put(ConversationColumns.TRASH_TIMESTAMP, trashTimestamp);
if (trashTimestamp > 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);
@@ -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)) {
@@ -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);
@@ -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();
@@ -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,
@@ -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
@@ -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();
@@ -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;
@@ -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<String> ids = new ArrayList<>();
final ArrayList<Long> 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<PurgeExpiredTrashAction> 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);
}
}
@@ -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) {
@@ -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<UpdateConversationTrashStatusAction> 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);
}
}
@@ -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<ConversationData> 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<ConversationData> binding) {
Assert.isTrue(binding.getData() == this);
UpdateConversationTrashStatusAction.moveToTrash(mConversationId);
}
public void restoreConversationFromTrash(final BindingBase<ConversationData> binding) {
Assert.isTrue(binding.getData() == this);
UpdateConversationTrashStatusAction.restoreFromTrash(mConversationId);
}
public ConversationParticipantsData getParticipants() {
return mParticipantData;
}
@@ -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<String> 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;
}
}
@@ -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 = ", ";
@@ -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,
@@ -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;
@@ -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.
*/
@@ -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) {
@@ -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*/,
@@ -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<SelectedConversation> selected = getSelectedConversationsIfAny();
if (selected != null && !selected.isEmpty()) {
restoreSelectedConversations();
return;
}
if (mConversationListFragment == null) {
return;
}
final List<ConversationListItemData> 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<SelectedConversation> selected = getSelectedConversationsIfAny();
if (selected != null && !selected.isEmpty()) {
confirmPermanentDelete(selected);
return;
}
if (mConversationListFragment == null) {
return;
}
final List<ConversationListItemData> items =
mConversationListFragment.snapshotVisibleConversations();
if (items.isEmpty()) {
HomeToast.show(this, R.string.home_toast_nothing_to_empty);
return;
}
final ArrayList<SelectedConversation> all = new ArrayList<>(items.size());
for (final ConversationListItemData item : items) {
all.add(new SelectedConversation(item));
}
confirmEmptyAll(all);
}
private Collection<SelectedConversation> 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<SelectedConversation> 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<SelectedConversation> 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<SelectedConversation> 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<SelectedConversation> 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<SelectedConversation> 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<SelectedConversation> conversations) {
performDeleteConversations(conversations, false /* reportSpam */);
}
private void performDeleteConversations(
final Collection<SelectedConversation> 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<SelectedConversation> 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<SelectedConversation> 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<SelectedConversation> 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
@@ -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
@@ -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();
@@ -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<ConversationListItemData> snapshotVisibleConversations() {
final Cursor cursor = getCursor();
if (cursor == null || mDisplayPositions.length == 0) {
return Collections.emptyList();
}
final List<ConversationListItemData> 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);
}
@@ -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<ConversationListItemData> 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;
@@ -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);
@@ -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);
}
}
@@ -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);
@@ -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();
}
}
@@ -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<SubscriptionInfo> 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<SubscriptionInfo> 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<SubscriptionInfo> 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<SubscriptionInfo> 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<SubscriptionInfo> 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);
}
}
@@ -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)));
}
}
@@ -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<MessagePartData> 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);
}
}
@@ -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();
@@ -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();
}
@@ -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();
@@ -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<String> mSelectedDestinations = new HashSet<>();
private final Binding<BlockedParticipantsData> 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<ParticipantListItemData> 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<String> 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<SpamHolder> {
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<ParticipantListItemData> snapshotVisible() {
return snapshotMatching(null);
}
List<ParticipantListItemData> snapshotSelected(final Set<String> destinations) {
return snapshotMatching(destinations);
}
private List<ParticipantListItemData> snapshotMatching(final Set<String> destinations) {
final List<ParticipantListItemData> 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);
}
}
}
}
@@ -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();
}
}
@@ -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;
@@ -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<SubscriptionInfo> infos;
public static List<SubscriptionInfo> getActiveSubscriptions() {
try {
infos = PhoneUtils.getDefault().getActiveSubscriptionInfoList();
final List<SubscriptionInfo> 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<SubscriptionInfo> 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;
@@ -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);
}