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
+16
View File
@@ -146,6 +146,14 @@
<activity
android:name=".ui.conversationlist.ArchivedConversationListActivity"
android:configChanges="orientation|screenSize|keyboardHidden"
android:label="@string/home_archived_title"
android:parentActivityName="com.android.messaging.ui.conversationlist.ConversationListActivity"
android:screenOrientation="user"
android:theme="@style/Home.SubScreen" />
<activity
android:name=".ui.conversationlist.RecentlyDeletedConversationListActivity"
android:configChanges="orientation|screenSize|keyboardHidden"
android:label="@string/home_recently_deleted_title"
android:parentActivityName="com.android.messaging.ui.conversationlist.ConversationListActivity"
android:screenOrientation="user"
@@ -332,6 +340,14 @@
android:theme="@style/Home.SubScreen"
android:windowSoftInputMode="adjustResize|stateVisible" />
<activity
android:name=".ui.home.HomeMessageDetailsActivity"
android:configChanges="orientation|screenSize|keyboardHidden"
android:label="@string/home_message_details_title"
android:parentActivityName="com.android.messaging.ui.conversation.ConversationActivity"
android:screenOrientation="user"
android:theme="@style/Home.SubScreen" />
<!-- License -->
<activity
android:name=".ui.LicenseActivity"
@@ -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() {
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();
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
archiveLabel.setText(trashed
? R.string.home_archive_restore
: (archived
? R.string.home_profile_option_unarchive
: R.string.home_profile_option_archive);
: 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(
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))
.setPositiveButton(R.string.delete_conversation_confirmation_button,
(dialog, button) -> deleteConversation())
.setNegativeButton(R.string.delete_conversation_decline_button, null)
.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();
if (isTrashList()) {
for (final SelectedConversation conversation : conversations) {
DeleteConversationAction.deleteConversation(
conversation.conversationId,
conversation.timestamp);
}
showDeleteCompletedToast(count, reportSpam);
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();
}
}
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
HomeToast.show(this, 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 */);
: 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) {
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);
HomeUiMotion.swapOverlayChip(mHomePinnedDoneButton, mHomeEditButton);
if (mHomeEditButton != null) {
mHomeEditButton.setText(R.string.home_edit);
mHomeEditButton.setOnClickListener(this::showHomeEditPopover);
if (mHomePinnedDoneButton != null) {
mHomePinnedDoneButton.setVisibility(View.GONE);
}
}
}
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();
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,13 +544,25 @@ public class HomeProfileActivity extends BugleActionBarActivity
}
private void confirmDelete() {
new AlertDialog.Builder(this, R.style.Home_Dialog)
.setTitle(getResources().getQuantityString(
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))
.setNegativeButton(R.string.delete_conversation_decline_button, null)
.setPositiveButton(R.string.delete_conversation_confirmation_button,
(dialog, which) -> {
.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,15 +34,61 @@ 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);
}
final TextView title = findViewById(R.id.home_sub_header_title);
if (title != null) {
title.setText(R.string.home_spam_title);
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;
}
}
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() {
@@ -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) {
if (mAdapter != null) {
mAdapter.swapCursor(cursor);
final boolean empty = mAdapter.getCount() == 0;
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");
}
SubscriptionInfo match = infos.get(0);
if (infos.size() > 1 && !TextUtils.isEmpty(selfId)) {
public static SubscriptionInfo getSubscriptionForSelfId(final String selfId) {
final List<SubscriptionInfo> infos = getActiveSubscriptions();
if (infos.isEmpty()) {
return null;
}
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);
}
@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<solid android:color="@color/home_surface" />
<corners android:radius="@dimen/looply_radius_lg" />
<solid android:color="@color/home_row_bg" />
</shape>
@@ -0,0 +1,17 @@
<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item>
<shape android:shape="oval">
<solid android:color="@color/home_bg" />
</shape>
</item>
<item
android:bottom="1.5dp"
android:left="1.5dp"
android:right="1.5dp"
android:top="1.5dp">
<shape android:shape="oval">
<solid android:color="@color/home_sim_badge_bg" />
</shape>
</item>
</layer-list>
@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<ripple xmlns:android="http://schemas.android.com/apk/res/android"
android:color="@color/home_surface_elevated">
<item>
<shape android:shape="rectangle">
<solid android:color="@color/home_chip_bg" />
<corners android:radius="@dimen/looply_radius_pill" />
</shape>
</item>
</ripple>
@@ -141,6 +141,8 @@
android:layout_width="match_parent"
android:layout_height="wrap_content" />
<include layout="@layout/home_conversation_trash_bar" />
<include layout="@layout/home_conversation_message_action_bar" />
</FrameLayout>
</com.android.messaging.ui.conversation.ConversationChatAreaLayout>
@@ -400,41 +400,70 @@
android:layout_gravity="bottom"
android:clipChildren="false"
android:clipToPadding="false"
android:gravity="center_vertical"
android:gravity="center"
android:orientation="horizontal"
android:paddingStart="@dimen/looply_space_md"
android:paddingEnd="@dimen/looply_space_md"
android:paddingBottom="@dimen/looply_space_md"
android:visibility="gone">
<TextView
android:id="@+id/home_read_all_button"
<FrameLayout
android:id="@+id/home_selection_actions"
android:layout_width="wrap_content"
android:layout_height="@dimen/home_selection_dock_height"
android:layout_height="wrap_content"
android:layout_marginEnd="10dp"
android:background="@drawable/home_bg_dock_pill"
android:clipChildren="false"
android:clipToPadding="false"
android:elevation="6dp"
android:gravity="center"
android:minWidth="120dp"
android:outlineAmbientShadowColor="@color/home_pill_shadow_ambient"
android:outlineSpotShadowColor="@color/home_pill_shadow_spot"
android:paddingStart="20dp"
android:paddingEnd="20dp"
android:paddingBottom="@dimen/home_selection_dock_shadow_inset"
android:text="@string/home_read_all"
android:textColor="@color/home_text_primary"
android:textSize="15sp" />
android:paddingStart="@dimen/home_nav_item_padding"
android:paddingTop="@dimen/home_nav_item_padding"
android:paddingEnd="@dimen/home_nav_item_padding"
android:paddingBottom="@dimen/home_nav_dock_padding_bottom">
<View
android:id="@+id/home_selection_indicator"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_weight="1" />
android:background="@drawable/home_bg_nav_active_primary" />
<LinearLayout
android:id="@+id/home_selection_tabs"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:baselineAligned="false"
android:gravity="center_vertical"
android:orientation="horizontal">
<include
android:id="@+id/home_selection_unread"
layout="@layout/home_bottom_nav_tab_item"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical" />
<include
android:id="@+id/home_selection_spam"
layout="@layout/home_bottom_nav_tab_item"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical" />
<include
android:id="@+id/home_selection_archive"
layout="@layout/home_bottom_nav_tab_item"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical" />
</LinearLayout>
</FrameLayout>
<ImageButton
android:id="@+id/home_selection_delete"
android:layout_width="@dimen/home_selection_dock_height"
android:layout_height="@dimen/home_selection_dock_height"
android:layout_width="@dimen/home_bottom_dock_height"
android:layout_height="@dimen/home_bottom_dock_height"
android:background="@drawable/home_bg_dock_fab"
android:clipChildren="false"
android:clipToPadding="false"
@@ -443,7 +472,10 @@
android:enabled="false"
android:outlineAmbientShadowColor="@color/home_pill_shadow_ambient"
android:outlineSpotShadowColor="@color/home_pill_shadow_spot"
android:paddingBottom="@dimen/home_dock_fab_icon_lift"
android:paddingStart="3dp"
android:paddingTop="2dp"
android:paddingEnd="3dp"
android:paddingBottom="@dimen/home_bottom_dock_shadow_inset"
android:scaleType="centerInside"
app:srcCompat="@drawable/ic_delete_small_dark"
app:tint="@color/home_nav_inactive" />
@@ -151,7 +151,9 @@
<FrameLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content">
android:layout_height="wrap_content"
android:clipChildren="false"
android:clipToPadding="false">
<com.android.messaging.ui.ContactIconView
android:id="@+id/conversation_icon"
android:layout_width="@dimen/home_list_avatar_size"
@@ -179,6 +181,18 @@
android:importantForAccessibility="no"
android:contentDescription="@null"
android:background="@android:color/transparent" />
<ImageView
android:id="@+id/conversation_mute_badge"
android:layout_width="@dimen/home_mute_badge_size"
android:layout_height="@dimen/home_mute_badge_size"
android:layout_gravity="bottom|end"
android:background="@drawable/home_bg_mute_badge"
android:contentDescription="@string/home_conversation_muted_toast"
android:padding="3.5dp"
android:scaleType="fitCenter"
android:src="@drawable/ic_home_bell_off"
android:tint="@color/home_sim_badge_text"
android:visibility="gone" />
</FrameLayout>
<LinearLayout
@@ -3,25 +3,39 @@
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/home_bg"
android:clipChildren="false"
android:clipToPadding="false"
android:fitsSystemWindows="true"
android:orientation="vertical">
<include layout="@layout/home_sub_header" />
<TextView
<FrameLayout
android:id="@+id/home_sub_search_collapse"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingStart="@dimen/looply_screen_padding"
android:paddingEnd="@dimen/looply_screen_padding"
android:paddingBottom="@dimen/looply_space_sm"
android:text="@string/home_recently_deleted_subtitle"
android:textColor="@color/home_text_secondary"
android:textSize="14sp" />
android:layout_height="0dp"
android:background="@color/home_bg"
android:clipChildren="true"
android:clipToPadding="true">
<include layout="@layout/home_sub_search_field" />
</FrameLayout>
<FrameLayout
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
android:background="@color/home_bg"
android:clipChildren="false"
android:clipToPadding="false">
<FrameLayout
android:id="@+id/home_archived_list_host"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
android:background="@color/home_bg" />
android:layout_height="match_parent"
android:clipChildren="false"
android:clipToPadding="false" />
<include layout="@layout/home_sub_select_bar" />
</FrameLayout>
</LinearLayout>
@@ -0,0 +1,36 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/home_conversation_trash_bar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center_horizontal"
android:orientation="vertical"
android:paddingStart="@dimen/looply_space_lg"
android:paddingTop="@dimen/looply_space_md"
android:paddingEnd="@dimen/looply_space_lg"
android:paddingBottom="@dimen/looply_space_lg"
android:visibility="gone">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:includeFontPadding="false"
android:lineSpacingExtra="3dp"
android:text="@string/home_trash_compose_message"
android:textColor="@color/home_text_secondary"
android:textSize="14sp" />
<TextView
android:id="@+id/home_conversation_trash_restore"
android:layout_width="match_parent"
android:layout_height="@dimen/home_modal_button_height"
android:layout_marginTop="@dimen/looply_space_md"
android:background="@drawable/home_bg_modal_primary"
android:gravity="center"
android:includeFontPadding="false"
android:text="@string/home_archive_restore"
android:textColor="@android:color/white"
android:textSize="15sp"
android:textStyle="bold" />
</LinearLayout>
@@ -1,44 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@drawable/home_bg_alert_dialog"
android:orientation="vertical"
android:paddingStart="@dimen/looply_space_lg"
android:paddingTop="@dimen/looply_space_xl"
android:paddingEnd="@dimen/looply_space_lg"
android:paddingBottom="@dimen/looply_space_lg">
<TextView
android:id="@+id/home_delete_alert_message"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:lineSpacingExtra="4dp"
android:textColor="@color/home_text_primary"
android:textSize="17sp" />
<TextView
android:id="@+id/home_delete_alert_delete"
android:layout_width="match_parent"
android:layout_height="@dimen/looply_button_height"
android:layout_marginTop="@dimen/looply_space_xl"
android:background="@drawable/home_bg_sheet_action"
android:gravity="center"
android:text="@string/home_delete_confirm"
android:textColor="@color/home_destructive"
android:textSize="17sp"
android:textStyle="bold" />
<TextView
android:id="@+id/home_delete_alert_delete_spam"
android:layout_width="match_parent"
android:layout_height="@dimen/looply_button_height"
android:layout_marginTop="@dimen/looply_space_sm"
android:background="@drawable/home_bg_sheet_action"
android:gravity="center"
android:text="@string/home_delete_confirm_spam"
android:textColor="@color/home_destructive"
android:textSize="17sp"
android:textStyle="bold" />
</LinearLayout>
@@ -5,17 +5,20 @@
android:gravity="center_horizontal"
android:orientation="vertical">
<FrameLayout
<LinearLayout
android:id="@+id/home_lock_prompt"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1">
android:layout_weight="1"
android:gravity="center"
android:orientation="vertical">
<com.android.messaging.ui.home.HomePatternLockView
android:id="@+id/home_lock_pattern"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_gravity="center" />
</FrameLayout>
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/looply_space_lg" />
</LinearLayout>
<TextView
android:id="@+id/home_lock_pattern_recorded"
+12 -5
View File
@@ -2,20 +2,27 @@
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center_horizontal"
android:orientation="vertical">
<LinearLayout
android:id="@+id/home_lock_prompt"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
android:gravity="center"
android:orientation="vertical">
<include
android:id="@+id/home_lock_pin_dots"
layout="@layout/home_lock_pin_dots"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/looply_space_lg" />
</LinearLayout>
<include
android:id="@+id/home_lock_pin_keypad"
layout="@layout/home_lock_pin_keypad"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_marginTop="@dimen/looply_space_xl"
android:layout_weight="1" />
android:layout_height="wrap_content" />
</LinearLayout>
@@ -80,28 +80,6 @@
android:text="@string/home_message_menu_star" />
</LinearLayout>
<LinearLayout
android:id="@+id/home_message_menu_delete"
style="@style/Home.PopoverItem"
android:layout_width="match_parent"
android:layout_height="48dp">
<ImageView
android:layout_width="22dp"
android:layout_height="22dp"
android:contentDescription="@null"
android:importantForAccessibility="no"
app:srcCompat="@drawable/ic_home_menu_trash"
app:tint="@color/home_text_primary" />
<TextView
style="@style/Home.PopoverLabel"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="14dp"
android:text="@string/home_message_menu_delete" />
</LinearLayout>
<LinearLayout
android:id="@+id/home_message_menu_select_more"
style="@style/Home.PopoverItem"
@@ -0,0 +1,216 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/home_bg"
android:fitsSystemWindows="true"
android:orientation="vertical">
<include layout="@layout/home_sub_header" />
<ScrollView
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
android:clipToPadding="false"
android:paddingStart="@dimen/looply_space_lg"
android:paddingTop="@dimen/looply_space_md"
android:paddingEnd="@dimen/looply_space_lg"
android:paddingBottom="@dimen/looply_screen_padding"
android:scrollbars="none">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<FrameLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@drawable/home_bg_details_card"
android:padding="@dimen/looply_space_lg">
<TextView
android:id="@+id/home_message_details_bubble"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="start"
android:background="@drawable/home_bg_preview_bubble_in"
android:includeFontPadding="false"
android:lineSpacingMultiplier="1.15"
android:maxWidth="@dimen/looply_bubble_max_width"
android:paddingStart="@dimen/looply_bubble_padding_h"
android:paddingTop="@dimen/looply_bubble_padding_v"
android:paddingEnd="@dimen/looply_bubble_padding_h"
android:paddingBottom="@dimen/looply_bubble_padding_v"
android:textColor="@color/home_text_primary"
android:textSize="15sp" />
</FrameLayout>
<LinearLayout
android:id="@+id/home_message_details_status_section"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/looply_space_lg"
android:orientation="vertical">
<TextView
style="@style/Home.SettingsSection"
android:text="@string/home_message_details_status" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@drawable/home_bg_details_card"
android:orientation="vertical">
<LinearLayout
android:id="@+id/home_message_details_received_row"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center_vertical"
android:minHeight="52dp"
android:orientation="horizontal"
android:paddingStart="@dimen/looply_space_lg"
android:paddingEnd="@dimen/looply_space_lg">
<TextView
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:includeFontPadding="false"
android:text="@string/home_message_details_received"
android:textColor="@color/home_text_primary"
android:textSize="16sp" />
<TextView
android:id="@+id/home_message_details_received"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:includeFontPadding="false"
android:textColor="@color/home_text_secondary"
android:textSize="14sp" />
</LinearLayout>
<View
android:id="@+id/home_message_details_status_divider"
android:layout_width="match_parent"
android:layout_height="1dp"
android:layout_marginStart="@dimen/looply_space_lg"
android:background="@color/home_divider" />
<LinearLayout
android:id="@+id/home_message_details_sent_row"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center_vertical"
android:minHeight="52dp"
android:orientation="horizontal"
android:paddingStart="@dimen/looply_space_lg"
android:paddingEnd="@dimen/looply_space_lg">
<TextView
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:includeFontPadding="false"
android:text="@string/home_message_details_sent"
android:textColor="@color/home_text_primary"
android:textSize="16sp" />
<TextView
android:id="@+id/home_message_details_sent"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:includeFontPadding="false"
android:textColor="@color/home_text_secondary"
android:textSize="14sp" />
</LinearLayout>
</LinearLayout>
</LinearLayout>
<TextView
style="@style/Home.SettingsSection"
android:layout_marginTop="@dimen/looply_space_lg"
android:text="@string/home_message_details_type" />
<TextView
android:id="@+id/home_message_details_type"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:includeFontPadding="false"
android:paddingStart="@dimen/looply_space_md"
android:paddingEnd="@dimen/looply_space_md"
android:textColor="@color/home_text_primary"
android:textSize="16sp" />
<LinearLayout
android:id="@+id/home_message_details_priority_section"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<TextView
style="@style/Home.SettingsSection"
android:layout_marginTop="@dimen/looply_space_lg"
android:text="@string/home_message_details_priority" />
<TextView
android:id="@+id/home_message_details_priority"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:includeFontPadding="false"
android:paddingStart="@dimen/looply_space_md"
android:paddingEnd="@dimen/looply_space_md"
android:textColor="@color/home_text_primary"
android:textSize="16sp" />
</LinearLayout>
<LinearLayout
android:id="@+id/home_message_details_subject_section"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:visibility="gone">
<TextView
style="@style/Home.SettingsSection"
android:layout_marginTop="@dimen/looply_space_lg"
android:text="@string/home_message_details_subject" />
<TextView
android:id="@+id/home_message_details_subject"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:includeFontPadding="false"
android:paddingStart="@dimen/looply_space_md"
android:paddingEnd="@dimen/looply_space_md"
android:textColor="@color/home_text_primary"
android:textSize="16sp" />
</LinearLayout>
<LinearLayout
android:id="@+id/home_message_details_size_section"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:visibility="gone">
<TextView
style="@style/Home.SettingsSection"
android:layout_marginTop="@dimen/looply_space_lg"
android:text="@string/home_message_details_size" />
<TextView
android:id="@+id/home_message_details_size"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:includeFontPadding="false"
android:paddingStart="@dimen/looply_space_md"
android:paddingEnd="@dimen/looply_space_md"
android:textColor="@color/home_text_primary"
android:textSize="16sp" />
</LinearLayout>
</LinearLayout>
</ScrollView>
</LinearLayout>
+12
View File
@@ -95,6 +95,18 @@
android:textStyle="bold"
android:visibility="gone" />
<TextView
android:id="@+id/home_modal_neutral"
android:layout_width="match_parent"
android:layout_height="@dimen/home_modal_button_height"
android:layout_marginTop="@dimen/looply_space_sm"
android:background="@drawable/home_bg_modal_cancel"
android:gravity="center"
android:includeFontPadding="false"
android:textColor="@color/home_text_primary"
android:textSize="15sp"
android:visibility="gone" />
<TextView
android:id="@+id/home_modal_negative"
android:layout_width="match_parent"
@@ -5,18 +5,20 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:clipToPadding="false"
android:clickable="false"
android:fadeScrollbars="true"
android:fillViewport="false"
android:overScrollMode="ifContentScrolls"
android:scrollbars="none">
<LinearLayout
android:layout_width="match_parent"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical">
<LinearLayout
android:layout_width="match_parent"
android:id="@+id/popover_preview_card"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@drawable/home_bg_conversation_preview"
android:elevation="16dp"
@@ -34,6 +36,7 @@
</LinearLayout>
<LinearLayout
android:id="@+id/popover_menu_card"
android:layout_width="@dimen/home_popover_width"
android:layout_height="wrap_content"
android:layout_marginTop="10dp"
@@ -43,6 +46,29 @@
android:paddingTop="6dp"
android:paddingBottom="6dp">
<LinearLayout
android:id="@+id/popover_conversation_restore"
style="@style/Home.PopoverItem"
android:layout_width="match_parent"
android:layout_height="44dp"
android:visibility="gone">
<ImageView
android:layout_width="22dp"
android:layout_height="22dp"
android:contentDescription="@null"
android:src="@drawable/ic_home_menu_archive"
android:tint="@color/home_text_primary" />
<TextView
style="@style/Home.PopoverLabel"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="12dp"
android:layout_weight="1"
android:text="@string/home_archive_restore" />
</LinearLayout>
<LinearLayout
android:id="@+id/popover_conversation_pin"
style="@style/Home.PopoverItem"
@@ -136,6 +162,51 @@
android:text="@string/home_category_assign" />
</LinearLayout>
<LinearLayout
android:id="@+id/popover_conversation_archive"
style="@style/Home.PopoverItem"
android:layout_width="match_parent"
android:layout_height="44dp">
<ImageView
android:layout_width="22dp"
android:layout_height="22dp"
android:contentDescription="@null"
android:src="@drawable/ic_home_menu_archive"
android:tint="@color/home_text_primary" />
<TextView
android:id="@+id/popover_conversation_archive_label"
style="@style/Home.PopoverLabel"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="12dp"
android:layout_weight="1"
android:text="@string/home_profile_option_archive" />
</LinearLayout>
<LinearLayout
android:id="@+id/popover_conversation_spam"
style="@style/Home.PopoverItem"
android:layout_width="match_parent"
android:layout_height="44dp">
<ImageView
android:layout_width="22dp"
android:layout_height="22dp"
android:contentDescription="@null"
android:src="@drawable/ic_home_menu_spam"
android:tint="@color/home_text_primary" />
<TextView
style="@style/Home.PopoverLabel"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="12dp"
android:layout_weight="1"
android:text="@string/home_unknown_sender_report_spam" />
</LinearLayout>
<LinearLayout
android:id="@+id/popover_conversation_delete"
style="@style/Home.PopoverItem"
@@ -1,12 +1,14 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="@dimen/home_popover_width"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@drawable/home_bg_popover"
android:elevation="12dp"
android:minWidth="168dp"
android:orientation="vertical"
android:paddingTop="6dp"
android:paddingEnd="12dp"
android:paddingBottom="6dp">
<LinearLayout
@@ -24,10 +26,9 @@
<TextView
style="@style/Home.PopoverLabel"
android:layout_width="0dp"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="12dp"
android:layout_weight="1"
android:text="@string/home_profile_action_add_people" />
</LinearLayout>
@@ -46,10 +47,9 @@
<TextView
style="@style/Home.PopoverLabel"
android:layout_width="0dp"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="12dp"
android:layout_weight="1"
android:text="@string/home_profile_title" />
</LinearLayout>
@@ -68,10 +68,9 @@
<TextView
style="@style/Home.PopoverLabel"
android:layout_width="0dp"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="12dp"
android:layout_weight="1"
android:text="@string/home_profile_action_search" />
</LinearLayout>
@@ -91,13 +90,33 @@
<TextView
android:id="@+id/popover_overflow_archive_label"
style="@style/Home.PopoverLabel"
android:layout_width="0dp"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="12dp"
android:layout_weight="1"
android:text="@string/home_profile_option_archive" />
</LinearLayout>
<LinearLayout
android:id="@+id/popover_overflow_spam"
style="@style/Home.PopoverItem"
android:layout_width="match_parent"
android:layout_height="44dp">
<ImageView
android:layout_width="22dp"
android:layout_height="22dp"
android:contentDescription="@null"
android:src="@drawable/ic_home_menu_spam"
android:tint="@color/home_text_primary" />
<TextView
style="@style/Home.PopoverLabel"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="12dp"
android:text="@string/home_unknown_sender_report_spam" />
</LinearLayout>
<LinearLayout
android:id="@+id/popover_overflow_delete"
style="@style/Home.PopoverItem"
@@ -113,10 +132,9 @@
<TextView
style="@style/Home.PopoverLabel"
android:layout_width="0dp"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="12dp"
android:layout_weight="1"
android:text="@string/home_conversation_delete"
android:textColor="@color/home_destructive" />
</LinearLayout>
+82 -21
View File
@@ -24,7 +24,7 @@
android:layout_marginEnd="8dp"
android:contentDescription="@null"
android:src="@drawable/ic_home_check"
android:visibility="visible" />
android:visibility="invisible" />
<ImageView
android:layout_width="22dp"
@@ -48,10 +48,14 @@
android:layout_width="match_parent"
android:layout_height="44dp">
<View
<ImageView
android:id="@+id/popover_spam_check"
android:layout_width="18dp"
android:layout_height="18dp"
android:layout_marginEnd="8dp" />
android:layout_marginEnd="8dp"
android:contentDescription="@null"
android:src="@drawable/ic_home_check"
android:visibility="invisible" />
<ImageView
android:layout_width="22dp"
@@ -70,15 +74,19 @@
</LinearLayout>
<LinearLayout
android:id="@+id/popover_recently_deleted"
android:id="@+id/popover_archive"
style="@style/Home.PopoverItem"
android:layout_width="match_parent"
android:layout_height="44dp">
<View
<ImageView
android:id="@+id/popover_archive_check"
android:layout_width="18dp"
android:layout_height="18dp"
android:layout_marginEnd="8dp" />
android:layout_marginEnd="8dp"
android:contentDescription="@null"
android:src="@drawable/ic_home_check"
android:visibility="invisible" />
<ImageView
android:layout_width="22dp"
@@ -87,6 +95,37 @@
android:src="@drawable/ic_home_menu_archive"
android:tint="@color/home_text_primary" />
<TextView
style="@style/Home.PopoverLabel"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="12dp"
android:layout_weight="1"
android:text="@string/home_popover_archive" />
</LinearLayout>
<LinearLayout
android:id="@+id/popover_recently_deleted"
style="@style/Home.PopoverItem"
android:layout_width="match_parent"
android:layout_height="44dp">
<ImageView
android:id="@+id/popover_recently_deleted_check"
android:layout_width="18dp"
android:layout_height="18dp"
android:layout_marginEnd="8dp"
android:contentDescription="@null"
android:src="@drawable/ic_home_check"
android:visibility="invisible" />
<ImageView
android:layout_width="22dp"
android:layout_height="22dp"
android:contentDescription="@null"
android:src="@drawable/ic_home_menu_trash"
android:tint="@color/home_text_primary" />
<TextView
style="@style/Home.PopoverLabel"
android:layout_width="0dp"
@@ -97,6 +136,7 @@
</LinearLayout>
<View
android:id="@+id/popover_filter_by_divider"
android:layout_width="match_parent"
android:layout_height="1dp"
android:layout_marginTop="4dp"
@@ -104,6 +144,7 @@
android:background="@color/home_divider" />
<TextView
android:id="@+id/popover_filter_by_label"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingStart="16dp"
@@ -120,10 +161,14 @@
android:layout_width="match_parent"
android:layout_height="44dp">
<View
<ImageView
android:id="@+id/popover_unread_check"
android:layout_width="18dp"
android:layout_height="18dp"
android:layout_marginEnd="8dp" />
android:layout_marginEnd="8dp"
android:contentDescription="@null"
android:src="@drawable/ic_home_check"
android:visibility="invisible" />
<ImageView
android:layout_width="22dp"
@@ -149,33 +194,41 @@
android:clipChildren="false"
android:clipToPadding="false">
<View
<ImageView
android:id="@+id/popover_work_check"
android:layout_width="18dp"
android:layout_height="18dp"
android:layout_marginEnd="8dp" />
android:layout_marginEnd="8dp"
android:contentDescription="@null"
android:src="@drawable/ic_home_check"
android:visibility="invisible" />
<!-- Same 22dp icon column as other rows; badge may extend left into the check spacer. -->
<!-- Same icon column as other rows; badge can grow to the SIM name used in the list. -->
<FrameLayout
android:layout_width="22dp"
android:layout_width="wrap_content"
android:layout_height="22dp"
android:minWidth="22dp"
android:clipChildren="false"
android:clipToPadding="false">
<TextView
android:id="@+id/popover_work_badge"
style="@style/Home.SimBadge"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical|end"
android:text="@string/home_chip_work" />
android:ellipsize="end"
android:maxWidth="72dp"
android:maxLines="1" />
</FrameLayout>
<TextView
android:id="@+id/popover_work_label"
style="@style/Home.PopoverLabel"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="12dp"
android:layout_weight="1"
android:text="@string/home_popover_work" />
android:layout_weight="1" />
</LinearLayout>
<LinearLayout
@@ -186,32 +239,40 @@
android:clipChildren="false"
android:clipToPadding="false">
<View
<ImageView
android:id="@+id/popover_personal_check"
android:layout_width="18dp"
android:layout_height="18dp"
android:layout_marginEnd="8dp" />
android:layout_marginEnd="8dp"
android:contentDescription="@null"
android:src="@drawable/ic_home_check"
android:visibility="invisible" />
<FrameLayout
android:layout_width="22dp"
android:layout_width="wrap_content"
android:layout_height="22dp"
android:minWidth="22dp"
android:clipChildren="false"
android:clipToPadding="false">
<TextView
android:id="@+id/popover_personal_badge"
style="@style/Home.SimBadge"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical|end"
android:text="@string/home_chip_personal" />
android:ellipsize="end"
android:maxWidth="72dp"
android:maxLines="1" />
</FrameLayout>
<TextView
android:id="@+id/popover_personal_label"
style="@style/Home.PopoverLabel"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="12dp"
android:layout_weight="1"
android:text="@string/home_popover_personal" />
android:layout_weight="1" />
</LinearLayout>
<View
@@ -2,30 +2,21 @@
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:paddingStart="@dimen/looply_screen_padding"
android:paddingTop="@dimen/looply_space_sm"
android:paddingEnd="@dimen/looply_screen_padding">
android:orientation="vertical">
<EditText
android:id="@+id/home_profile_rename_field"
android:layout_width="match_parent"
android:layout_height="@dimen/looply_input_height"
android:background="@drawable/home_bg_input_field"
style="@style/Home.ModalInput"
android:hint="@string/home_profile_rename_hint"
android:imeOptions="actionDone"
android:inputType="textCapSentences"
android:maxLines="1"
android:paddingStart="@dimen/looply_space_lg"
android:paddingEnd="@dimen/looply_space_lg"
android:textColor="@color/home_text_primary"
android:textColorHint="@color/home_text_tertiary"
android:textSize="16sp" />
android:maxLines="1" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/looply_space_sm"
android:gravity="center"
android:text="@string/home_profile_rename_note"
android:textColor="@color/home_text_secondary"
android:textSize="13sp" />
+16 -10
View File
@@ -1,28 +1,32 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/home_bg"
android:clipChildren="false"
android:clipToPadding="false"
android:fitsSystemWindows="true"
android:orientation="vertical">
<include layout="@layout/home_sub_header" />
<TextView
<FrameLayout
android:id="@+id/home_sub_search_collapse"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingStart="@dimen/looply_screen_padding"
android:paddingEnd="@dimen/looply_screen_padding"
android:paddingBottom="@dimen/looply_space_sm"
android:text="@string/home_spam_subtitle"
android:textColor="@color/home_text_secondary"
android:textSize="14sp" />
android:layout_height="0dp"
android:background="@color/home_bg"
android:clipChildren="true"
android:clipToPadding="true">
<include layout="@layout/home_sub_search_field" />
</FrameLayout>
<FrameLayout
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1">
android:layout_weight="1"
android:clipChildren="false"
android:clipToPadding="false">
<androidx.fragment.app.FragmentContainerView
android:id="@+id/home_spam_fragment_host"
@@ -36,5 +40,7 @@
android:layout_width="match_parent"
android:layout_height="match_parent"
android:visibility="gone" />
<include layout="@layout/home_sub_select_bar" />
</FrameLayout>
</LinearLayout>
@@ -0,0 +1,41 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/home_bg"
android:clipChildren="false"
android:clipToPadding="false">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/home_spam_list"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/home_bg"
android:clipToPadding="false"
android:overScrollMode="ifContentScrolls"
android:paddingBottom="@dimen/looply_space_lg"
android:scrollbars="none" />
<com.android.messaging.ui.home.HomePassThroughView
android:id="@+id/home_list_top_fade"
android:layout_width="match_parent"
android:layout_height="@dimen/home_list_top_fade_height"
android:layout_gravity="top"
android:background="@drawable/home_bg_list_top_fade"
android:clickable="false"
android:focusable="false"
android:importantForAccessibility="no"
android:visibility="gone" />
<com.android.messaging.ui.home.HomePassThroughView
android:id="@+id/home_list_bottom_fade"
android:layout_width="match_parent"
android:layout_height="@dimen/home_list_bottom_fade_height"
android:layout_gravity="bottom"
android:layout_marginBottom="@dimen/home_list_bottom_fade_margin"
android:background="@drawable/home_bg_list_bottom_fade"
android:clickable="false"
android:focusable="false"
android:importantForAccessibility="no"
android:visibility="gone" />
</FrameLayout>
@@ -0,0 +1,108 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?android:attr/selectableItemBackground"
android:gravity="center_vertical"
android:minHeight="72dp"
android:orientation="horizontal"
android:paddingStart="@dimen/looply_space_md"
android:paddingTop="@dimen/looply_space_sm"
android:paddingEnd="@dimen/looply_space_lg"
android:paddingBottom="@dimen/looply_space_md">
<FrameLayout
android:id="@+id/home_spam_selection_indicator"
android:layout_width="@dimen/home_selection_indicator_size"
android:layout_height="@dimen/home_selection_indicator_size"
android:layout_marginEnd="@dimen/looply_space_sm"
android:visibility="gone">
<View
android:id="@+id/home_spam_selection_ring"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@drawable/home_bg_selection_ring" />
<ImageView
android:id="@+id/home_spam_selection_check"
android:layout_width="@dimen/home_selection_check_size"
android:layout_height="@dimen/home_selection_check_size"
android:layout_gravity="center"
android:contentDescription="@null"
android:src="@drawable/ic_checkmark_light"
android:visibility="gone" />
</FrameLayout>
<com.android.messaging.ui.ContactIconView
android:id="@+id/home_spam_avatar"
android:layout_width="@dimen/home_list_avatar_size"
android:layout_height="@dimen/home_list_avatar_size"
android:contentDescription="@null"
app:contactIconSize="normal" />
<LinearLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/looply_space_md"
android:layout_weight="1"
android:orientation="vertical">
<TextView
android:id="@+id/home_spam_name"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:ellipsize="end"
android:includeFontPadding="false"
android:maxLines="1"
android:textColor="@color/home_text_primary"
android:textSize="16sp"
android:textStyle="bold" />
<TextView
android:id="@+id/home_spam_details"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="2dp"
android:ellipsize="end"
android:includeFontPadding="false"
android:maxLines="1"
android:textColor="@color/home_text_secondary"
android:textSize="14sp" />
</LinearLayout>
<LinearLayout
android:id="@+id/home_spam_unblock"
android:layout_width="wrap_content"
android:layout_height="32dp"
android:layout_marginStart="@dimen/looply_space_sm"
android:background="@drawable/home_bg_unblock_pill"
android:clickable="true"
android:contentDescription="@string/home_spam_unblock_action"
android:focusable="true"
android:gravity="center"
android:orientation="horizontal"
android:paddingStart="12dp"
android:paddingEnd="14dp">
<ImageView
android:layout_width="16dp"
android:layout_height="16dp"
android:contentDescription="@null"
android:scaleType="fitCenter"
android:src="@drawable/ic_home_menu_spam"
android:tint="@color/home_text_primary" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="6dp"
android:includeFontPadding="false"
android:text="@string/home_spam_unblock_action"
android:textColor="@color/home_text_primary"
android:textSize="13sp"
android:textStyle="bold" />
</LinearLayout>
</LinearLayout>
+75 -5
View File
@@ -1,21 +1,29 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Shared header for home sub screens: back arrow plus a left aligned title. -->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@color/home_bg"
android:gravity="center_vertical"
android:clipChildren="false"
android:clipToPadding="false"
android:minHeight="@dimen/home_header_height"
android:orientation="horizontal"
android:paddingStart="8dp"
android:paddingEnd="12dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:gravity="center_vertical"
android:orientation="horizontal">
<include layout="@layout/home_back_button" />
<TextView
android:id="@+id/home_sub_header_title"
style="@style/Home.SubHeaderTitle" />
style="@style/Home.SubHeaderTitle"
android:layout_marginEnd="@dimen/home_header_title_inset" />
<!-- Shown instead of the back arrow when the screen is presented as a sheet. -->
<include
@@ -51,4 +59,66 @@
android:textColor="@color/home_accent"
android:textSize="16sp"
android:visibility="gone" />
</LinearLayout>
</LinearLayout>
<LinearLayout
android:id="@+id/home_sub_header_inbox_actions"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="end|center_vertical"
android:clipChildren="false"
android:clipToPadding="false"
android:gravity="center_vertical"
android:orientation="horizontal"
android:visibility="gone">
<TextView
android:id="@+id/home_sub_header_edit"
android:layout_width="wrap_content"
android:layout_height="45dp"
android:background="@drawable/home_bg_dock_pill"
android:clipChildren="false"
android:clipToPadding="false"
android:elevation="6dp"
android:gravity="center"
android:minWidth="64dp"
android:outlineAmbientShadowColor="@color/home_pill_shadow_ambient"
android:outlineSpotShadowColor="@color/home_pill_shadow_spot"
android:paddingStart="18dp"
android:paddingEnd="18dp"
android:paddingBottom="5dp"
android:text="@string/home_select"
android:textColor="@color/home_text_primary"
android:textSize="15sp" />
<ImageButton
android:id="@+id/home_sub_header_filter"
android:layout_width="40dp"
android:layout_height="45dp"
android:layout_marginStart="@dimen/looply_space_sm"
android:background="@drawable/home_bg_circle"
android:clipChildren="false"
android:clipToPadding="false"
android:contentDescription="@string/home_filter_content_description"
android:elevation="6dp"
android:outlineAmbientShadowColor="@color/home_pill_shadow_ambient"
android:outlineSpotShadowColor="@color/home_pill_shadow_spot"
android:padding="9dp"
android:paddingBottom="5dp"
android:scaleType="fitCenter"
android:src="@drawable/ic_home_menu_lines"
android:tint="@color/home_text_primary" />
</LinearLayout>
<ImageButton
android:id="@+id/home_sub_header_done"
android:layout_width="36dp"
android:layout_height="36dp"
android:layout_gravity="end|center_vertical"
android:background="@android:color/transparent"
android:contentDescription="@string/home_selection_done"
android:padding="0dp"
android:scaleType="fitCenter"
android:visibility="invisible"
app:srcCompat="@drawable/ic_checkmark_circle_primary" />
</FrameLayout>
@@ -0,0 +1,63 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/home_sub_search_box"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@color/home_bg"
android:orientation="vertical"
android:paddingStart="12dp"
android:paddingTop="4dp"
android:paddingEnd="12dp"
android:paddingBottom="4dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="@dimen/home_search_field_height"
android:addStatesFromChildren="true"
android:background="@drawable/home_bg_search_field_box"
android:gravity="center_vertical"
android:orientation="horizontal"
android:paddingStart="@dimen/looply_space_md"
android:paddingEnd="@dimen/looply_space_sm">
<ImageView
android:layout_width="18dp"
android:layout_height="18dp"
android:contentDescription="@null"
android:scaleType="fitCenter"
android:src="@drawable/ic_home_search_line"
android:tint="@color/home_search_hint" />
<EditText
android:id="@+id/home_sub_search_field"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_marginStart="@dimen/looply_space_sm"
android:layout_weight="1"
android:background="@android:color/transparent"
android:gravity="center_vertical|start"
android:hint="@string/home_sub_list_search_hint"
android:imeOptions="actionSearch"
android:includeFontPadding="false"
android:inputType="text"
android:maxLines="1"
android:textColor="@color/home_text_primary"
android:textColorHint="@color/home_search_hint"
android:textCursorDrawable="@drawable/home_cursor"
android:textSize="16sp" />
<ImageButton
android:id="@+id/home_sub_search_clear"
android:layout_width="36dp"
android:layout_height="36dp"
android:background="@android:color/transparent"
android:contentDescription="@string/home_search_clear"
android:focusable="false"
android:padding="8dp"
android:scaleType="centerInside"
android:visibility="gone"
app:srcCompat="@drawable/ic_home_close"
app:tint="@color/home_text_secondary" />
</LinearLayout>
</LinearLayout>
@@ -0,0 +1,38 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/home_sub_select_bar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="bottom"
android:clipChildren="false"
android:clipToPadding="false"
android:paddingStart="@dimen/looply_space_md"
android:paddingEnd="@dimen/looply_space_md"
android:paddingBottom="@dimen/looply_space_md"
android:visibility="gone">
<FrameLayout
android:id="@+id/home_sub_select_pill"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:background="@drawable/home_bg_dock_pill"
android:clipChildren="false"
android:clipToPadding="false"
android:elevation="6dp"
android:minWidth="@dimen/home_sub_select_pill_min_width"
android:outlineAmbientShadowColor="@color/home_pill_shadow_ambient"
android:outlineSpotShadowColor="@color/home_pill_shadow_spot"
android:paddingStart="@dimen/home_sub_select_pill_padding_h"
android:paddingTop="@dimen/home_nav_item_padding"
android:paddingEnd="@dimen/home_sub_select_pill_padding_h"
android:paddingBottom="@dimen/home_nav_dock_padding_bottom">
<include
android:id="@+id/home_sub_select_action"
layout="@layout/home_bottom_nav_tab_item"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center" />
</FrameLayout>
</FrameLayout>
+73 -11
View File
@@ -14,6 +14,19 @@
<string name="home_title">Message Q</string>
<string name="home_title_avatar_content_description">프로필</string>
<string name="home_edit">편집</string>
<string name="home_select">선택</string>
<string name="home_sub_unblock_all">모두 스팸 해제</string>
<string name="home_sub_unblock">스팸 해제</string>
<string name="home_sub_unarchive_all">모두 보관 해제</string>
<string name="home_sub_unarchive">보관 해제</string>
<string name="home_empty_trash_action">모두 비우기</string>
<string name="home_toast_nothing_to_edit">편집할 대화가 없습니다</string>
<string name="home_toast_nothing_to_select">선택할 항목이 없습니다</string>
<string name="home_toast_nothing_to_empty">휴지통이 비어 있습니다</string>
<string name="home_toast_nothing_to_unblock">차단 해제할 스팸이 없습니다</string>
<string name="home_toast_nothing_to_unarchive">보관 해제할 대화가 없습니다</string>
<string name="home_sub_unblocked_toast">스팸을 해제했습니다</string>
<string name="home_sub_unarchived_toast_many">대화 %1$d개의 보관을 해제했습니다</string>
<string name="home_search_hint">검색</string>
<string name="home_chip_personal">개인</string>
<string name="home_chip_work">업무</string>
@@ -142,10 +155,18 @@
<string name="home_automation_empty_body">메시지를 자동으로 처리할 규칙을 만드세요.</string>
<string name="home_empty_messages_title">대화가 없습니다</string>
<string name="home_empty_messages_body">새 채팅을 시작하면 여기에 표시됩니다.</string>
<string name="home_empty_unread_title">안 읽은 대화가 없습니다</string>
<string name="home_empty_unread_body">안 읽은 대화가 여기에 표시됩니다.</string>
<string name="home_empty_work_title">업무 대화가 없습니다</string>
<string name="home_empty_work_body">업무 SIM이나 업무 관련 메시지가 여기에 표시됩니다.</string>
<string name="home_empty_personal_title">개인 대화가 없습니다</string>
<string name="home_empty_personal_body">개인 SIM이나 지인과의 대화가 여기에 표시됩니다.</string>
<string name="home_empty_messages_sync_title">대화를 불러오는 중</string>
<string name="home_empty_messages_sync_body">잠시 후 메시지가 여기에 나타납니다.</string>
<string name="home_empty_archive_title">보관된 대화가 없습니다</string>
<string name="home_empty_archive_body">보관한 채팅은 복원하거나 삭제할 때까지 여기에 있습니다.</string>
<string name="home_empty_trash_title">휴지통이 비어 있습니다</string>
<string name="home_empty_trash_body">삭제한 채팅은 30일 동안 여기에 있다가 영구 삭제됩니다.</string>
<string name="home_empty_spam_title">스팸 번호가 없습니다</string>
<string name="home_empty_spam_body">차단된 발신자가 여기에 표시됩니다. 항목을 눌러 해제하세요.</string>
<string name="home_empty_contacts_title">표시할 연락처가 없습니다</string>
@@ -172,11 +193,20 @@
<string name="home_mark_unread">안 읽음</string>
<string name="home_selection_done">완료</string>
<string name="home_selection_delete">선택한 메시지 삭제</string>
<string name="home_selection_spam">스팸신고</string>
<string name="home_selection_archive">보관함</string>
<string name="home_selection_archived_toast_many">대화 %1$d개를 보관했습니다</string>
<string name="home_report_spam_title">스팸으로 신고할까요?</string>
<string name="home_report_spam_message_one">이 발신자를 차단하고 대화를 스팸으로 보냅니다.</string>
<string name="home_report_spam_message_many">발신자 %1$d명을 차단하고 대화를 스팸으로 보냅니다.</string>
<string name="home_report_spam_groups_only">그룹 대화는 스팸으로 신고할 수 없습니다</string>
<string name="home_report_spam_toast_one">스팸으로 신고했습니다</string>
<string name="home_report_spam_toast_many">대화 %1$d개를 스팸으로 신고했습니다</string>
<string name="home_message_selection_title">1개 선택됨</string>
<string name="home_message_action_copy">복사</string>
<string name="home_message_action_forward">전달</string>
<string name="home_message_action_share">공유</string>
<string name="home_message_action_details">상세</string>
<string name="home_message_action_details">세부정보</string>
<string name="home_message_action_save">저장</string>
<string name="home_message_action_send">다시 보내기</string>
<string name="home_message_action_download">다운로드</string>
@@ -191,18 +221,43 @@
<string name="home_message_menu_unstar">별표 해제</string>
<string name="home_message_menu_delete">삭제</string>
<string name="home_message_menu_select_more">더 선택</string>
<string name="home_message_menu_details">상세</string>
<string name="home_delete_alert_message_one">이 대화가 삭제됩니다.\n\n스팸으로 신고하면 발신자도 차단 목록에 추가됩니다.</string>
<string name="home_delete_alert_message_many">선택한 대화 %1$d개가 삭제됩니다.\n\n스팸으로 신고하면 1:1 대화 발신자도 차단 목록에 추가됩니다.</string>
<string name="home_message_menu_details">세부정보</string>
<string name="home_message_details_title">세부정보</string>
<string name="home_message_details_status">상태</string>
<string name="home_message_details_received">수신됨</string>
<string name="home_message_details_sent">전송됨</string>
<string name="home_message_details_type">유형</string>
<string name="home_message_details_priority">우선순위</string>
<string name="home_message_details_subject">제목</string>
<string name="home_message_details_size">크기</string>
<string name="home_message_details_photo">사진</string>
<string name="home_message_details_video">동영상</string>
<string name="home_message_details_audio">음성</string>
<string name="home_message_details_contact">연락처</string>
<string name="home_message_details_attachment">첨부파일</string>
<string name="home_delete_alert_message_one">이 대화는 휴지통으로 옮겨지고 30일 후 영구 삭제됩니다.</string>
<string name="home_delete_alert_message_many">선택한 대화 %1$d개가 휴지통으로 옮겨지고 30일 후 영구 삭제됩니다.</string>
<string name="home_delete_confirm">삭제</string>
<string name="home_delete_confirm_spam">삭제하고 스팸 신고</string>
<string name="home_delete_toast_one">대화를 삭제했습니다</string>
<string name="home_delete_toast_many">대화 %1$d개를 삭제했습니다</string>
<string name="home_delete_spam_toast_one">삭제하고 스팸으로 신고했습니다</string>
<string name="home_delete_spam_toast_many">대화 %1$d개를 삭제하고 스팸으로 신고했습니다</string>
<string name="home_trash_moved_toast_one">휴지통으로 옮겼습니다</string>
<string name="home_trash_moved_toast_many">대화 %1$d개를 휴지통으로 옮겼습니다</string>
<string name="home_trash_restored_toast">휴지통에서 복원했습니다</string>
<string name="home_trash_compose_message">이 대화는 휴지통에 있습니다. 메시지를 보내려면 복원하세요.</string>
<string name="home_trash_permanent_title">지금 삭제할까요?</string>
<string name="home_trash_permanent_message_one">이 대화가 바로 삭제됩니다. 되돌릴 수 없습니다.</string>
<string name="home_trash_permanent_message_many">대화 %1$d개가 바로 삭제됩니다. 되돌릴 수 없습니다.</string>
<string name="home_empty_all_title">모두 비울까요?</string>
<string name="home_empty_all_message">휴지통의 모든 대화가 바로 삭제됩니다. 되돌릴 수 없습니다.</string>
<string name="home_empty_all_confirm">휴지통 메시지 삭제</string>
<string name="home_spam_title">스팸</string>
<string name="home_spam_subtitle">목록의 번호에서 온 메시지는 차단됩니다. 항목을 눌러 제거하세요.</string>
<string name="home_spam_empty">스팸 번호가 없습니다.</string>
<string name="home_spam_item_hint">차단된 발신자</string>
<string name="home_spam_unblock_title">이 번호를 차단 해제할까요?</string>
<string name="home_spam_unblock_message">%1$s에서 온 메시지와 알림을 다시 받게 됩니다.</string>
<string name="home_spam_unblock_action">차단 해제</string>
<string name="home_archive_restore">복원</string>
<string name="home_sub_header_back">뒤로</string>
<string name="home_sub_header_close">닫기</string>
<string name="home_add_content_description">추가</string>
@@ -211,10 +266,13 @@
<string name="home_popover_messages">메시지</string>
<string name="home_popover_spam">스팸</string>
<string name="home_popover_recently_deleted">보관함</string>
<string name="home_recently_deleted_title">보관함</string>
<string name="home_recently_deleted_subtitle">보관한 대화는 복원하거나 삭제할 때까지 여기에 있습니다.</string>
<string name="home_recently_deleted_empty">보관된 대화가 없습니다</string>
<string name="home_popover_archive">보관함</string>
<string name="home_popover_recently_deleted">휴지통</string>
<string name="home_archived_title">보관함</string>
<string name="home_archived_subtitle">보관한 대화는 복원하거나 삭제할 때까지 여기에 있습니다.</string>
<string name="home_recently_deleted_title">휴지통</string>
<string name="home_recently_deleted_subtitle">삭제한 대화는 30일 동안 여기에 있다가 영구 삭제됩니다.</string>
<string name="home_recently_deleted_empty">휴지통이 비어 있습니다</string>
<string name="home_popover_filter_by">필터</string>
<string name="home_popover_unread">안 읽음</string>
<string name="home_popover_work">업무</string>
@@ -235,6 +293,9 @@
<string name="home_search_title">검색</string>
<string name="home_search_field_hint">메시지 또는 연락처 검색</string>
<string name="home_sub_list_search_hint">검색</string>
<string name="home_empty_sub_search_title">결과가 없습니다</string>
<string name="home_empty_sub_search_body">“%1$s”와 일치하는 항목이 없습니다.</string>
<string name="home_search_clear">지우기</string>
<string name="home_search_reset">초기화</string>
<string name="home_search_voice">음성 검색</string>
@@ -373,6 +434,7 @@
<string name="home_conversation_marked_unread_toast">안 읽음으로 표시했습니다</string>
<string name="home_conversation_marked_read_toast">읽음으로 표시했습니다</string>
<string name="home_swipe_mute">알림 숨기기</string>
<string name="home_swipe_unmute">알림 켜기</string>
<string name="home_swipe_delete">삭제</string>
<string name="home_swipe_archive">보관</string>
<string name="home_requires_default_sms_app">기본 SMS 앱을 Messages로 설정하세요</string>
@@ -99,6 +99,7 @@
<dimen name="home_pinned_remove_offset">-4dp</dimen>
<dimen name="looply_badge_size">20dp</dimen>
<dimen name="home_unread_dot_size">8dp</dimen>
<dimen name="home_mute_badge_size">18dp</dimen>
<!-- dot width + trailing margin (looply_space_xs) -->
<dimen name="home_unread_dot_slot_width">12dp</dimen>
<dimen name="home_selection_indicator_size">22dp</dimen>
@@ -122,6 +123,9 @@
<dimen name="home_compose_group_button_height">52dp</dimen>
<dimen name="home_selection_dock_height">55dp</dimen>
<dimen name="home_selection_dock_shadow_inset">5dp</dimen>
<dimen name="home_sub_select_list_padding">104dp</dimen>
<dimen name="home_sub_select_pill_min_width">148dp</dimen>
<dimen name="home_sub_select_pill_padding_h">20dp</dimen>
<!-- Square inset for dock FAB face: (size - shadow_depth) / 2 on square buttons -->
<dimen name="home_dock_fab_shadow_depth">6dp</dimen>
<dimen name="home_dock_fab_circle_inset">3dp</dimen>
+71 -9
View File
@@ -15,6 +15,19 @@
<string name="home_title" translatable="false">Message Q</string>
<string name="home_title_avatar_content_description" translatable="false">Profile</string>
<string name="home_edit" translatable="false">Edit</string>
<string name="home_select" translatable="false">Select</string>
<string name="home_sub_unblock_all" translatable="false">Unblock all</string>
<string name="home_sub_unblock" translatable="false">Unblock</string>
<string name="home_sub_unarchive_all" translatable="false">Unarchive all</string>
<string name="home_sub_unarchive" translatable="false">Unarchive</string>
<string name="home_empty_trash_action" translatable="false">Empty All</string>
<string name="home_toast_nothing_to_edit" translatable="false">Nothing to edit</string>
<string name="home_toast_nothing_to_select" translatable="false">Nothing to select</string>
<string name="home_toast_nothing_to_empty" translatable="false">Trash is empty</string>
<string name="home_toast_nothing_to_unblock" translatable="false">No spam to unblock</string>
<string name="home_toast_nothing_to_unarchive" translatable="false">No conversations to unarchive</string>
<string name="home_sub_unblocked_toast" translatable="false">Unblocked</string>
<string name="home_sub_unarchived_toast_many" translatable="false">%1$d conversations unarchived</string>
<string name="home_search_hint" translatable="false">Search</string>
<string name="home_chip_personal" translatable="false">Personal</string>
<string name="home_chip_work" translatable="false">Work</string>
@@ -143,10 +156,18 @@
<string name="home_automation_empty_body" translatable="false">Create rules to handle messages automatically.</string>
<string name="home_empty_messages_title" translatable="false">No conversations yet</string>
<string name="home_empty_messages_body" translatable="false">Start a new chat and it will show up here.</string>
<string name="home_empty_unread_title" translatable="false">You\'re all caught up</string>
<string name="home_empty_unread_body" translatable="false">Unread conversations will show up here.</string>
<string name="home_empty_work_title" translatable="false">No work conversations</string>
<string name="home_empty_work_body" translatable="false">Chats on your work SIM, and business messages, appear here.</string>
<string name="home_empty_personal_title" translatable="false">No personal conversations</string>
<string name="home_empty_personal_body" translatable="false">Chats on your personal SIM, and messages from people you know, appear here.</string>
<string name="home_empty_messages_sync_title" translatable="false">Loading conversations</string>
<string name="home_empty_messages_sync_body" translatable="false">Your messages will appear here in a moment.</string>
<string name="home_empty_archive_title" translatable="false">No archived conversations</string>
<string name="home_empty_archive_body" translatable="false">Archived chats stay here until you restore or delete them.</string>
<string name="home_empty_trash_title" translatable="false">Trash is empty</string>
<string name="home_empty_trash_body" translatable="false">Deleted chats stay here for 30 days, then they are permanently deleted.</string>
<string name="home_empty_spam_title" translatable="false">No spam numbers yet</string>
<string name="home_empty_spam_body" translatable="false">Blocked senders will appear here. Tap an entry to unblock.</string>
<string name="home_empty_contacts_title" translatable="false">No contacts to show</string>
@@ -173,6 +194,15 @@
<string name="home_mark_unread" translatable="false">Unread</string>
<string name="home_selection_done" translatable="false">Done</string>
<string name="home_selection_delete" translatable="false">Delete selected messages</string>
<string name="home_selection_spam" translatable="false">Spam</string>
<string name="home_selection_archive" translatable="false">Archive</string>
<string name="home_selection_archived_toast_many" translatable="false">%1$d conversations archived</string>
<string name="home_report_spam_title" translatable="false">Report as spam?</string>
<string name="home_report_spam_message_one" translatable="false">This sender will be blocked and the conversation moved to Spam.</string>
<string name="home_report_spam_message_many" translatable="false">%1$d senders will be blocked and their conversations moved to Spam.</string>
<string name="home_report_spam_groups_only" translatable="false">Group conversations can\'t be reported as spam</string>
<string name="home_report_spam_toast_one" translatable="false">Reported as spam</string>
<string name="home_report_spam_toast_many" translatable="false">%1$d conversations reported as spam</string>
<string name="home_message_selection_title" translatable="false">1 selected</string>
<string name="home_message_action_copy" translatable="false">Copy</string>
<string name="home_message_action_forward" translatable="false">Forward</string>
@@ -193,17 +223,42 @@
<string name="home_message_menu_delete" translatable="false">Delete</string>
<string name="home_message_menu_select_more" translatable="false">Select more</string>
<string name="home_message_menu_details" translatable="false">Details</string>
<string name="home_delete_alert_message_one" translatable="false">This conversation will be deleted.\n\nReporting it as spam also adds the sender to your blocked list.</string>
<string name="home_delete_alert_message_many" translatable="false">The %1$d selected conversations will be deleted.\n\nReporting them as spam also adds the senders of one-to-one conversations to your blocked list.</string>
<string name="home_message_details_title" translatable="false">Details</string>
<string name="home_message_details_status" translatable="false">Status</string>
<string name="home_message_details_received" translatable="false">Received</string>
<string name="home_message_details_sent" translatable="false">Sent</string>
<string name="home_message_details_type" translatable="false">Type</string>
<string name="home_message_details_priority" translatable="false">Priority</string>
<string name="home_message_details_subject" translatable="false">Subject</string>
<string name="home_message_details_size" translatable="false">Size</string>
<string name="home_message_details_photo" translatable="false">Photo</string>
<string name="home_message_details_video" translatable="false">Video</string>
<string name="home_message_details_audio" translatable="false">Audio</string>
<string name="home_message_details_contact" translatable="false">Contact</string>
<string name="home_message_details_attachment" translatable="false">Attachment</string>
<string name="home_delete_alert_message_one" translatable="false">This conversation will move to Trash and be permanently deleted after 30 days.</string>
<string name="home_delete_alert_message_many" translatable="false">The %1$d selected conversations will move to Trash and be permanently deleted after 30 days.</string>
<string name="home_delete_confirm" translatable="false">Delete</string>
<string name="home_delete_confirm_spam" translatable="false">Delete and report spam</string>
<string name="home_delete_toast_one" translatable="false">Conversation deleted</string>
<string name="home_delete_toast_many" translatable="false">%1$d conversations deleted</string>
<string name="home_delete_spam_toast_one" translatable="false">Deleted and reported as spam</string>
<string name="home_delete_spam_toast_many" translatable="false">%1$d conversations deleted and reported as spam</string>
<string name="home_trash_moved_toast_one" translatable="false">Moved to Trash</string>
<string name="home_trash_moved_toast_many" translatable="false">%1$d conversations moved to Trash</string>
<string name="home_trash_restored_toast" translatable="false">Restored from Trash</string>
<string name="home_trash_compose_message" translatable="false">This conversation is in Trash. Restore it to send messages.</string>
<string name="home_trash_permanent_title" translatable="false">Delete permanently?</string>
<string name="home_trash_permanent_message_one" translatable="false">This conversation will be deleted now. This can\'t be undone.</string>
<string name="home_trash_permanent_message_many" translatable="false">%1$d conversations will be deleted now. This can\'t be undone.</string>
<string name="home_empty_all_title" translatable="false">Empty All?</string>
<string name="home_empty_all_message" translatable="false">All conversations in trash will be deleted now. This can\'t be undone.</string>
<string name="home_empty_all_confirm" translatable="false">Delete trash messages</string>
<string name="home_spam_title" translatable="false">Spam</string>
<string name="home_spam_subtitle" translatable="false">Messages from listed numbers are blocked. Tap an entry to remove it.</string>
<string name="home_spam_empty" translatable="false">No spam numbers yet.</string>
<string name="home_spam_item_hint" translatable="false">Blocked sender</string>
<string name="home_spam_unblock_title" translatable="false">Unblock this number?</string>
<string name="home_spam_unblock_message" translatable="false">You\'ll receive messages and notifications from %1$s again.</string>
<string name="home_spam_unblock_action" translatable="false">Unblock</string>
<string name="home_archive_restore" translatable="false">Restore</string>
<string name="home_sub_header_back" translatable="false">Back</string>
<string name="home_sub_header_close" translatable="false">Close</string>
<string name="home_add_content_description" translatable="false">Add</string>
@@ -213,10 +268,13 @@
<!-- Popover menus -->
<string name="home_popover_messages" translatable="false">Messages</string>
<string name="home_popover_spam" translatable="false">Spam</string>
<string name="home_popover_recently_deleted" translatable="false">Archive</string>
<string name="home_recently_deleted_title" translatable="false">Archive</string>
<string name="home_recently_deleted_subtitle" translatable="false">Archived conversations stay here until you restore or delete them.</string>
<string name="home_recently_deleted_empty" translatable="false">No archived conversations</string>
<string name="home_popover_archive" translatable="false">Archive</string>
<string name="home_popover_recently_deleted" translatable="false">Trash</string>
<string name="home_archived_title" translatable="false">Archive</string>
<string name="home_archived_subtitle" translatable="false">Archived conversations stay here until you restore or delete them.</string>
<string name="home_recently_deleted_title" translatable="false">Trash</string>
<string name="home_recently_deleted_subtitle" translatable="false">Deleted conversations stay here for 30 days, then they are permanently deleted.</string>
<string name="home_recently_deleted_empty" translatable="false">Trash is empty</string>
<string name="home_popover_filter_by" translatable="false">Filter By</string>
<string name="home_popover_unread" translatable="false">Unread</string>
<string name="home_popover_work" translatable="false">Work</string>
@@ -238,6 +296,9 @@
<!-- Message search screen -->
<string name="home_search_title" translatable="false">Search</string>
<string name="home_search_field_hint" translatable="false">Search messages or contacts</string>
<string name="home_sub_list_search_hint" translatable="false">Search</string>
<string name="home_empty_sub_search_title" translatable="false">No results</string>
<string name="home_empty_sub_search_body" translatable="false">Nothing matches “%1$s”.</string>
<string name="home_search_clear" translatable="false">Clear</string>
<string name="home_search_reset" translatable="false">Reset</string>
<string name="home_search_voice" translatable="false">Voice search</string>
@@ -380,6 +441,7 @@
<string name="home_conversation_marked_unread_toast" translatable="false">Marked as unread</string>
<string name="home_conversation_marked_read_toast" translatable="false">Marked as read</string>
<string name="home_swipe_mute" translatable="false">Hide alerts</string>
<string name="home_swipe_unmute" translatable="false">Show alerts</string>
<string name="home_swipe_delete" translatable="false">Delete</string>
<string name="home_swipe_archive" translatable="false">Archive</string>
<string name="home_requires_default_sms_app" translatable="false">Set Messages as the default SMS app</string>
+1 -1
View File
@@ -16,7 +16,7 @@
-->
<resources>
<!-- DB version -->
<string name="database_version" translatable="false">2</string>
<string name="database_version" translatable="false">3</string>
<!-- Version for shared preferences. This is used for handling prefs migration when old pref
keys are moved or renamed. You don't need to bump up the version number if you are just