diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index a789ef2..482cbaa 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -44,6 +44,8 @@
+
+
+
+
+
+
+
+
+
+
-
+
+ android:theme="@style/Home.SubScreen" />
+
+
+
0) {
+ updateTelephonyReadStatus(db, latestMessageId);
+ MessagingContentProvider.notifyMessagesChanged(conversationId);
+ MessagingContentProvider.notifyConversationListChanged();
+ }
+ db.setTransactionSuccessful();
+ } finally {
+ db.endTransaction();
+ }
+ return null;
+ }
+
+ private static void updateTelephonyReadStatus(final DatabaseWrapper db,
+ final String messageId) {
+ Assert.isNotMainThread();
+ try (Cursor cursor = db.query(DatabaseHelper.MESSAGES_TABLE,
+ new String[] { MessageColumns.SMS_MESSAGE_URI },
+ MessageColumns._ID + "=?",
+ new String[] { messageId },
+ null, null, null)) {
+ if (!cursor.moveToFirst()) {
+ return;
+ }
+ final String smsMessageUri = cursor.getString(0);
+ if (TextUtils.isEmpty(smsMessageUri)) {
+ return;
+ }
+ final ContentValues telephonyValues = new ContentValues();
+ telephonyValues.put("read", 0);
+ telephonyValues.put("seen", 0);
+ Factory.get().getApplicationContext().getContentResolver().update(
+ Uri.parse(smsMessageUri),
+ telephonyValues,
+ null,
+ null);
+ }
+ }
+
+ private MarkAsUnreadAction(final Parcel in) {
+ super(in);
+ }
+
+ public static final Parcelable.Creator CREATOR =
+ new Parcelable.Creator<>() {
+ @Override
+ public MarkAsUnreadAction createFromParcel(final Parcel in) {
+ return new MarkAsUnreadAction(in);
+ }
+
+ @Override
+ public MarkAsUnreadAction[] newArray(final int size) {
+ return new MarkAsUnreadAction[size];
+ }
+ };
+
+ @Override
+ public void writeToParcel(@NonNull final Parcel parcel, final int flags) {
+ writeActionToParcel(parcel, flags);
+ }
+}
diff --git a/app/src/main/java/com/android/messaging/datamodel/action/RenameConversationAction.java b/app/src/main/java/com/android/messaging/datamodel/action/RenameConversationAction.java
new file mode 100644
index 0000000..b069e25
--- /dev/null
+++ b/app/src/main/java/com/android/messaging/datamodel/action/RenameConversationAction.java
@@ -0,0 +1,95 @@
+/*
+ * 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.datamodel.action;
+
+import android.content.ContentValues;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.text.TextUtils;
+
+import androidx.annotation.NonNull;
+
+import com.android.messaging.datamodel.BugleDatabaseOperations;
+import com.android.messaging.datamodel.DataModel;
+import com.android.messaging.datamodel.DatabaseHelper.ConversationColumns;
+import com.android.messaging.datamodel.DatabaseWrapper;
+import com.android.messaging.datamodel.MessagingContentProvider;
+
+/**
+ * Gives a conversation a custom name, used when creating a group chat. The name lives only in
+ * the local database, so it is visible to this device only.
+ */
+public class RenameConversationAction extends Action implements Parcelable {
+ private static final String KEY_CONVERSATION_ID = "conversation_id";
+ private static final String KEY_NAME = "name";
+
+ public static void renameConversation(final String conversationId, final String name) {
+ final RenameConversationAction action = new RenameConversationAction(conversationId, name);
+ action.start();
+ }
+
+ private RenameConversationAction(final String conversationId, final String name) {
+ actionParameters.putString(KEY_CONVERSATION_ID, conversationId);
+ actionParameters.putString(KEY_NAME, name);
+ }
+
+ @Override
+ protected Object executeAction() {
+ final String conversationId = actionParameters.getString(KEY_CONVERSATION_ID);
+ final String name = actionParameters.getString(KEY_NAME);
+ if (TextUtils.isEmpty(conversationId) || TextUtils.isEmpty(name)) {
+ return null;
+ }
+
+ final DatabaseWrapper db = DataModel.get().getDatabase();
+ db.beginTransaction();
+ try {
+ final ContentValues values = new ContentValues();
+ values.put(ConversationColumns.NAME, name);
+ BugleDatabaseOperations.updateConversationRowIfExists(db, conversationId, values);
+ db.setTransactionSuccessful();
+ } finally {
+ db.endTransaction();
+ }
+
+ MessagingContentProvider.notifyConversationMetadataChanged(conversationId);
+ MessagingContentProvider.notifyConversationListChanged();
+ return null;
+ }
+
+ private RenameConversationAction(final Parcel in) {
+ super(in);
+ }
+
+ public static final Parcelable.Creator CREATOR =
+ new Parcelable.Creator<>() {
+ @Override
+ public RenameConversationAction createFromParcel(final Parcel in) {
+ return new RenameConversationAction(in);
+ }
+
+ @Override
+ public RenameConversationAction[] newArray(final int size) {
+ return new RenameConversationAction[size];
+ }
+ };
+
+ @Override
+ public void writeToParcel(@NonNull final Parcel parcel, final int flags) {
+ writeActionToParcel(parcel, flags);
+ }
+}
diff --git a/app/src/main/java/com/android/messaging/datamodel/data/ContactPickerData.java b/app/src/main/java/com/android/messaging/datamodel/data/ContactPickerData.java
index 068c34d..8ed5d9a 100644
--- a/app/src/main/java/com/android/messaging/datamodel/data/ContactPickerData.java
+++ b/app/src/main/java/com/android/messaging/datamodel/data/ContactPickerData.java
@@ -20,6 +20,7 @@ package com.android.messaging.datamodel.data;
import android.content.Context;
import android.database.Cursor;
import android.os.Bundle;
+import android.text.TextUtils;
import androidx.annotation.NonNull;
import androidx.loader.app.LoaderManager;
@@ -46,6 +47,13 @@ public class ContactPickerData extends BindableData implements
void onAllContactsCursorUpdated(Cursor data);
void onFrequentContactsCursorUpdated(Cursor data);
void onContactCustomColorLoaded(ContactPickerData data);
+
+ /**
+ * Results for the latest {@link ContactPickerData#setSearchText}, or null once the
+ * search is cleared.
+ */
+ default void onFilteredContactsCursorUpdated(Cursor data) {
+ }
}
private static final String BINDING_ID = "bindingId";
@@ -53,6 +61,7 @@ public class ContactPickerData extends BindableData implements
private LoaderManager mLoaderManager;
private ContactPickerDataListener mListener;
private final FrequentContactsCursorBuilder mFrequentContactsCursorBuilder;
+ private String mSearchText;
public ContactPickerData(final Context context, final ContactPickerDataListener listener) {
mListener = listener;
@@ -63,6 +72,7 @@ public class ContactPickerData extends BindableData implements
private static final int ALL_CONTACTS_LOADER = 1;
private static final int FREQUENT_CONTACTS_LOADER = 2;
private static final int PARTICIPANT_LOADER = 3;
+ private static final int FILTERED_CONTACTS_LOADER = 4;
@NonNull
@Override
@@ -81,6 +91,9 @@ public class ContactPickerData extends BindableData implements
return new BoundCursorLoader(bindingId, mContext,
MessagingContentProvider.PARTICIPANTS_URI,
ParticipantData.ParticipantsQuery.PROJECTION, null, null, null);
+ case FILTERED_CONTACTS_LOADER:
+ return ContactUtil.filterDestination(mContext, mSearchText)
+ .createBoundCursorLoader(bindingId);
default:
Assert.fail("Unknown loader id for contact picker!");
break;
@@ -108,12 +121,16 @@ public class ContactPickerData extends BindableData implements
case PARTICIPANT_LOADER:
mListener.onContactCustomColorLoaded(this);
break;
+ case FILTERED_CONTACTS_LOADER:
+ mListener.onFilteredContactsCursorUpdated(data);
+ break;
default:
Assert.fail("Unknown loader id for contact picker!");
break;
}
- if (loader.getId() != PARTICIPANT_LOADER) {
+ if (loader.getId() == ALL_CONTACTS_LOADER
+ || loader.getId() == FREQUENT_CONTACTS_LOADER) {
// The frequent contacts cursor to be used in the UI depends on results from both
// all contacts and frequent contacts loader, and we don't know which will finish
// first. Therefore, try to build the cursor and notify the listener if it's
@@ -151,6 +168,9 @@ public class ContactPickerData extends BindableData implements
case PARTICIPANT_LOADER:
mListener.onContactCustomColorLoaded(this);
break;
+ case FILTERED_CONTACTS_LOADER:
+ mListener.onFilteredContactsCursorUpdated(null);
+ break;
default:
Assert.fail("Unknown loader id for contact picker!");
break;
@@ -170,6 +190,33 @@ public class ContactPickerData extends BindableData implements
mLoaderManager.initLoader(PARTICIPANT_LOADER, args, this);
}
+ /**
+ * Narrows the contact list down to destinations matching {@code searchText}. Passing empty
+ * text clears the search and reports a null cursor.
+ */
+ public void setSearchText(final BindingBase binding,
+ final String searchText) {
+ if (mLoaderManager == null) {
+ return;
+ }
+ final String trimmed = searchText == null ? null : searchText.trim();
+ final String normalized = TextUtils.isEmpty(trimmed) ? null : trimmed;
+ if (TextUtils.equals(mSearchText, normalized)) {
+ return;
+ }
+ mSearchText = normalized;
+ if (mSearchText == null) {
+ mLoaderManager.destroyLoader(FILTERED_CONTACTS_LOADER);
+ if (mListener != null) {
+ mListener.onFilteredContactsCursorUpdated(null);
+ }
+ return;
+ }
+ final Bundle args = new Bundle();
+ args.putString(BINDING_ID, binding.getBindingId());
+ mLoaderManager.restartLoader(FILTERED_CONTACTS_LOADER, args, this);
+ }
+
@Override
protected void unregisterListeners() {
mListener = null;
@@ -180,6 +227,7 @@ public class ContactPickerData extends BindableData implements
mLoaderManager.destroyLoader(ALL_CONTACTS_LOADER);
mLoaderManager.destroyLoader(FREQUENT_CONTACTS_LOADER);
mLoaderManager.destroyLoader(PARTICIPANT_LOADER);
+ mLoaderManager.destroyLoader(FILTERED_CONTACTS_LOADER);
mLoaderManager = null;
}
mFrequentContactsCursorBuilder.resetBuilder();
diff --git a/app/src/main/java/com/android/messaging/datamodel/data/ConversationMessageData.java b/app/src/main/java/com/android/messaging/datamodel/data/ConversationMessageData.java
index dd7e4ca..33d1f86 100644
--- a/app/src/main/java/com/android/messaging/datamodel/data/ConversationMessageData.java
+++ b/app/src/main/java/com/android/messaging/datamodel/data/ConversationMessageData.java
@@ -619,6 +619,21 @@ public class ConversationMessageData {
+ CONVERSATION_MESSAGES_QUERY_SQL_GROUP_BY;
}
+ /**
+ * Same rows as {@link #getConversationMessagesQuerySql}, narrowed to the messages that have
+ * a part matching the search term. Takes the conversation id then the LIKE pattern.
+ */
+ public static String getConversationMessagesSearchQuerySql() {
+ return CONVERSATION_MESSAGES_QUERY_SQL
+ + " AND "
+ + DatabaseHelper.MESSAGES_TABLE + "." + MessageColumns.CONVERSATION_ID + "=?"
+ + " AND "
+ + DatabaseHelper.MESSAGES_TABLE + "." + MessageColumns._ID + " IN (SELECT "
+ + PartColumns.MESSAGE_ID + " FROM " + DatabaseHelper.PARTS_TABLE
+ + " WHERE " + PartColumns.TEXT + " LIKE ? ESCAPE '\\'))"
+ + CONVERSATION_MESSAGES_QUERY_SQL_GROUP_BY;
+ }
+
static String getConversationMessageIdsQuerySql() {
return CONVERSATION_MESSAGES_IDS_QUERY_SQL
+ " AND "
diff --git a/app/src/main/java/com/android/messaging/datamodel/data/PeopleAndOptionsData.java b/app/src/main/java/com/android/messaging/datamodel/data/ConversationProfileData.java
similarity index 50%
rename from app/src/main/java/com/android/messaging/datamodel/data/PeopleAndOptionsData.java
rename to app/src/main/java/com/android/messaging/datamodel/data/ConversationProfileData.java
index 1dbdb7c..5de5384 100644
--- a/app/src/main/java/com/android/messaging/datamodel/data/PeopleAndOptionsData.java
+++ b/app/src/main/java/com/android/messaging/datamodel/data/ConversationProfileData.java
@@ -1,6 +1,6 @@
/*
* Copyright (C) 2015 The Android Open Source Project
- * Copyright (C) 2024 The LineageOS Project
+ * 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.
@@ -38,125 +38,122 @@ import com.android.messaging.util.LogUtil;
import java.util.List;
/**
- * Services data needs for PeopleAndOptionsFragment.
+ * Serves the conversation metadata and the participant list needed by the conversation profile
+ * screen.
*/
-public class PeopleAndOptionsData extends BindableData implements
+public class ConversationProfileData extends BindableData implements
LoaderManager.LoaderCallbacks {
- public interface PeopleAndOptionsDataListener {
- void onOptionsCursorUpdated(PeopleAndOptionsData data, Cursor cursor);
- void onParticipantsListLoaded(PeopleAndOptionsData data,
+ public interface ConversationProfileDataListener {
+ void onConversationMetadataUpdated(ConversationProfileData data,
+ ConversationListItemData conversation);
+ void onParticipantsListLoaded(ConversationProfileData data,
List participants);
}
private static final String BINDING_ID = "bindingId";
+ private static final int CONVERSATION_METADATA_LOADER = 1;
+ private static final int PARTICIPANT_LOADER = 2;
+
private final Context mContext;
private final String mConversationId;
private final ConversationParticipantsData mParticipantData;
private LoaderManager mLoaderManager;
- private PeopleAndOptionsDataListener mListener;
+ private ConversationProfileDataListener mListener;
- public PeopleAndOptionsData(final String conversationId, final Context context,
- final PeopleAndOptionsDataListener listener) {
+ public ConversationProfileData(final String conversationId, final Context context,
+ final ConversationProfileDataListener listener) {
mListener = listener;
mContext = context;
mConversationId = conversationId;
mParticipantData = new ConversationParticipantsData();
}
- private static final int CONVERSATION_OPTIONS_LOADER = 1;
- private static final int PARTICIPANT_LOADER = 2;
-
@NonNull
@Override
public Loader onCreateLoader(final int id, final Bundle args) {
final String bindingId = args.getString(BINDING_ID);
- // Check if data still bound to the requesting ui element
- if (isBound(bindingId)) {
- switch (id) {
- case CONVERSATION_OPTIONS_LOADER: {
- final Uri uri =
- MessagingContentProvider.buildConversationMetadataUri(mConversationId);
- return new BoundCursorLoader(bindingId, mContext, uri,
- new String[]{}, null, null, null);
- }
-
- case PARTICIPANT_LOADER: {
- final Uri uri =
- MessagingContentProvider
- .buildConversationParticipantsUri(mConversationId);
- return new BoundCursorLoader(bindingId, mContext, uri,
- ParticipantData.ParticipantsQuery.PROJECTION, null, null, null);
- }
-
- default:
- Assert.fail("Unknown loader id for PeopleAndOptionsFragment!");
- break;
- }
- } else {
- LogUtil.w(LogUtil.BUGLE_TAG, "Loader created after unbinding PeopleAndOptionsFragment");
+ if (!isBound(bindingId)) {
+ LogUtil.w(LogUtil.BUGLE_TAG, "Loader created after unbinding conversation profile");
+ return null;
+ }
+ switch (id) {
+ case CONVERSATION_METADATA_LOADER: {
+ final Uri uri =
+ MessagingContentProvider.buildConversationMetadataUri(mConversationId);
+ return new BoundCursorLoader(bindingId, mContext, uri,
+ ConversationListItemData.PROJECTION, null, null, null);
+ }
+
+ case PARTICIPANT_LOADER: {
+ final Uri uri =
+ MessagingContentProvider.buildConversationParticipantsUri(mConversationId);
+ return new BoundCursorLoader(bindingId, mContext, uri,
+ ParticipantData.ParticipantsQuery.PROJECTION, null, null, null);
+ }
+
+ default:
+ Assert.fail("Unknown loader id for the conversation profile!");
+ return null;
}
- return null;
}
- /**
- * {@inheritDoc}
- */
@Override
public void onLoadFinished(@NonNull final Loader loader, final Cursor data) {
final BoundCursorLoader cursorLoader = (BoundCursorLoader) loader;
- if (isBound(cursorLoader.getBindingId())) {
- switch (loader.getId()) {
- case CONVERSATION_OPTIONS_LOADER:
- mListener.onOptionsCursorUpdated(this, data);
- break;
-
- case PARTICIPANT_LOADER:
- mParticipantData.bind(data);
- mListener.onParticipantsListLoaded(this,
- mParticipantData.getParticipantListExcludingSelf());
- break;
-
- default:
- Assert.fail("Unknown loader id for PeopleAndOptionsFragment!");
- break;
+ if (!isBound(cursorLoader.getBindingId()) || mListener == null) {
+ LogUtil.w(LogUtil.BUGLE_TAG, "Loader finished after unbinding conversation profile");
+ return;
+ }
+ switch (loader.getId()) {
+ case CONVERSATION_METADATA_LOADER: {
+ ConversationListItemData conversation = null;
+ if (data != null && data.moveToFirst()) {
+ conversation = new ConversationListItemData();
+ conversation.bind(data);
+ }
+ mListener.onConversationMetadataUpdated(this, conversation);
+ break;
}
- } else {
- LogUtil.w(LogUtil.BUGLE_TAG,
- "Loader finished after unbinding PeopleAndOptionsFragment");
+
+ case PARTICIPANT_LOADER:
+ mParticipantData.bind(data);
+ mListener.onParticipantsListLoaded(this,
+ mParticipantData.getParticipantListExcludingSelf());
+ break;
+
+ default:
+ Assert.fail("Unknown loader id for the conversation profile!");
+ break;
}
}
- /**
- * {@inheritDoc}
- */
@Override
public void onLoaderReset(@NonNull final Loader loader) {
final BoundCursorLoader cursorLoader = (BoundCursorLoader) loader;
- if (isBound(cursorLoader.getBindingId())) {
- switch (loader.getId()) {
- case CONVERSATION_OPTIONS_LOADER:
- mListener.onOptionsCursorUpdated(this, null);
- break;
+ if (!isBound(cursorLoader.getBindingId()) || mListener == null) {
+ return;
+ }
+ switch (loader.getId()) {
+ case CONVERSATION_METADATA_LOADER:
+ mListener.onConversationMetadataUpdated(this, null);
+ break;
- case PARTICIPANT_LOADER:
- mParticipantData.bind(null);
- break;
+ case PARTICIPANT_LOADER:
+ mParticipantData.bind(null);
+ break;
- default:
- Assert.fail("Unknown loader id for PeopleAndOptionsFragment!");
- break;
- }
- } else {
- LogUtil.w(LogUtil.BUGLE_TAG, "Loader reset after unbinding PeopleAndOptionsFragment");
+ default:
+ Assert.fail("Unknown loader id for the conversation profile!");
+ break;
}
}
public void init(final LoaderManager loaderManager,
- final BindingBase binding) {
+ final BindingBase binding) {
final Bundle args = new Bundle();
args.putString(BINDING_ID, binding.getBindingId());
mLoaderManager = loaderManager;
- mLoaderManager.initLoader(CONVERSATION_OPTIONS_LOADER, args, this);
+ mLoaderManager.initLoader(CONVERSATION_METADATA_LOADER, args, this);
mLoaderManager.initLoader(PARTICIPANT_LOADER, args, this);
}
@@ -166,17 +163,16 @@ public class PeopleAndOptionsData extends BindableData implements
// This could be null if we bind but the caller doesn't init the BindableData
if (mLoaderManager != null) {
- mLoaderManager.destroyLoader(CONVERSATION_OPTIONS_LOADER);
+ mLoaderManager.destroyLoader(CONVERSATION_METADATA_LOADER);
mLoaderManager.destroyLoader(PARTICIPANT_LOADER);
mLoaderManager = null;
}
}
- public void setDestinationBlocked(final BindingBase binding,
+ public void setDestinationBlocked(final BindingBase binding,
final boolean blocked) {
- final String bindingId = binding.getBindingId();
final ParticipantData participantData = mParticipantData.getOtherParticipant();
- if (isBound(bindingId) && participantData != null) {
+ if (isBound(binding.getBindingId()) && participantData != null) {
UpdateDestinationBlockedAction.updateDestinationBlocked(
participantData.getNormalizedDestination(),
blocked, mConversationId,
@@ -184,6 +180,11 @@ public class PeopleAndOptionsData extends BindableData implements
}
}
+ /** The single other participant of a one to one conversation, or null for a group. */
+ public ParticipantData getOtherParticipant() {
+ return mParticipantData.getOtherParticipant();
+ }
+
public String getConversationId() {
return mConversationId;
}
diff --git a/app/src/main/java/com/android/messaging/datamodel/data/ConversationSearchData.java b/app/src/main/java/com/android/messaging/datamodel/data/ConversationSearchData.java
new file mode 100644
index 0000000..ecda726
--- /dev/null
+++ b/app/src/main/java/com/android/messaging/datamodel/data/ConversationSearchData.java
@@ -0,0 +1,132 @@
+/*
+ * 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.datamodel.data;
+
+import android.content.Context;
+import android.database.Cursor;
+import android.os.Bundle;
+import android.text.TextUtils;
+
+import androidx.annotation.NonNull;
+import androidx.loader.app.LoaderManager;
+import androidx.loader.content.Loader;
+
+import com.android.messaging.datamodel.BoundCursorLoader;
+import com.android.messaging.datamodel.MessagingContentProvider;
+import com.android.messaging.datamodel.binding.BindableData;
+import com.android.messaging.datamodel.binding.BindingBase;
+import com.android.messaging.util.Assert;
+import com.android.messaging.util.LogUtil;
+
+/**
+ * Searches the messages of a single conversation. No loader runs until
+ * {@link #setSearchText} gets a non empty term.
+ */
+public class ConversationSearchData extends BindableData implements
+ LoaderManager.LoaderCallbacks {
+ public interface ConversationSearchDataListener {
+ /** Results for the latest search term, or null once the search is cleared. */
+ void onConversationSearchCursorUpdated(Cursor cursor);
+ }
+
+ private static final String BINDING_ID = "bindingId";
+ private static final int SEARCH_LOADER = 1;
+
+ private final Context mContext;
+ private final String mConversationId;
+ private LoaderManager mLoaderManager;
+ private ConversationSearchDataListener mListener;
+ private String mSearchText;
+
+ public ConversationSearchData(final Context context, final String conversationId,
+ final ConversationSearchDataListener listener) {
+ mContext = context;
+ mConversationId = conversationId;
+ mListener = listener;
+ }
+
+ public void init(final LoaderManager loaderManager) {
+ mLoaderManager = loaderManager;
+ }
+
+ public void setSearchText(final BindingBase binding,
+ final String searchText) {
+ if (mLoaderManager == null) {
+ return;
+ }
+ final String trimmed = searchText == null ? null : searchText.trim();
+ final String normalized = TextUtils.isEmpty(trimmed) ? null : trimmed;
+ if (TextUtils.equals(mSearchText, normalized)) {
+ return;
+ }
+ mSearchText = normalized;
+ if (mSearchText == null) {
+ mLoaderManager.destroyLoader(SEARCH_LOADER);
+ if (mListener != null) {
+ mListener.onConversationSearchCursorUpdated(null);
+ }
+ return;
+ }
+ final Bundle args = new Bundle();
+ args.putString(BINDING_ID, binding.getBindingId());
+ mLoaderManager.restartLoader(SEARCH_LOADER, args, this);
+ }
+
+ public String getSearchText() {
+ return mSearchText;
+ }
+
+ @NonNull
+ @Override
+ public Loader onCreateLoader(final int id, final Bundle args) {
+ Assert.isTrue(id == SEARCH_LOADER);
+ final String bindingId = args.getString(BINDING_ID);
+ if (!isBound(bindingId)) {
+ LogUtil.w(LogUtil.BUGLE_TAG, "Loader created after unbinding conversation search");
+ return null;
+ }
+ return new BoundCursorLoader(bindingId, mContext,
+ MessagingContentProvider.buildConversationMessagesSearchUri(
+ mConversationId, mSearchText),
+ null, null, null, null);
+ }
+
+ @Override
+ public void onLoadFinished(@NonNull final Loader loader, final Cursor cursor) {
+ final BoundCursorLoader cursorLoader = (BoundCursorLoader) loader;
+ if (isBound(cursorLoader.getBindingId()) && mListener != null) {
+ mListener.onConversationSearchCursorUpdated(cursor);
+ }
+ }
+
+ @Override
+ public void onLoaderReset(@NonNull final Loader loader) {
+ final BoundCursorLoader cursorLoader = (BoundCursorLoader) loader;
+ if (isBound(cursorLoader.getBindingId()) && mListener != null) {
+ mListener.onConversationSearchCursorUpdated(null);
+ }
+ }
+
+ @Override
+ protected void unregisterListeners() {
+ mListener = null;
+ if (mLoaderManager != null) {
+ mLoaderManager.destroyLoader(SEARCH_LOADER);
+ mLoaderManager = null;
+ }
+ }
+}
diff --git a/app/src/main/java/com/android/messaging/datamodel/data/PeopleOptionsItemData.java b/app/src/main/java/com/android/messaging/datamodel/data/PeopleOptionsItemData.java
deleted file mode 100644
index 6669325..0000000
--- a/app/src/main/java/com/android/messaging/datamodel/data/PeopleOptionsItemData.java
+++ /dev/null
@@ -1,79 +0,0 @@
-/*
- * Copyright (C) 2015 The Android Open Source Project
- * Copyright (C) 2024 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.datamodel.data;
-
-import android.content.Context;
-import android.database.Cursor;
-
-import com.android.messaging.R;
-import com.android.messaging.util.Assert;
-
-public class PeopleOptionsItemData {
- // Identification for each setting that's surfaced to the UI layer.
- public static final int SETTING_NOTIFICATION = 0;
- public static final int SETTING_BLOCKED = 1;
- public static final int SETTINGS_COUNT = 2;
-
- private String mTitle;
- private int mItemId;
- private ParticipantData mOtherParticipant;
-
- private final Context mContext;
-
- public PeopleOptionsItemData(final Context context) {
- mContext = context;
- }
-
- /**
- * Bind to a specific setting column on conversation metadata cursor. (Note
- * that it binds to columns because it treats individual columns of the cursor as
- * separate options to display for the conversation, e.g. notification settings).
- */
- public void bind(
- final Cursor cursor, final ParticipantData otherParticipant, final int settingType) {
- mItemId = settingType;
- mOtherParticipant = otherParticipant;
-
- switch (settingType) {
- case SETTING_NOTIFICATION:
- mTitle = mContext.getString(R.string.notifications_enabled_conversation_pref_title);
- break;
-
- case SETTING_BLOCKED:
- Assert.notNull(otherParticipant);
- final int resourceId = otherParticipant.isBlocked() ?
- R.string.unblock_contact_title : R.string.block_contact_title;
- mTitle = mContext.getString(resourceId, otherParticipant.getDisplayDestination());
- break;
-
- default:
- Assert.fail("Unsupported conversation option type!");
- }
- }
-
- public String getTitle() {
- return mTitle;
- }
-
- public int getItemId() {
- return mItemId;
- }
-
- public ParticipantData getOtherParticipant() {
- return mOtherParticipant;
- }
-}
diff --git a/app/src/main/java/com/android/messaging/ui/ContactIconView.java b/app/src/main/java/com/android/messaging/ui/ContactIconView.java
index 333bec4..e778071 100644
--- a/app/src/main/java/com/android/messaging/ui/ContactIconView.java
+++ b/app/src/main/java/com/android/messaging/ui/ContactIconView.java
@@ -41,8 +41,10 @@ public class ContactIconView extends AsyncImageView {
private static final int NORMAL_ICON_SIZE_ID = 0;
private static final int LARGE_ICON_SIZE_ID = 1;
private static final int SMALL_ICON_SIZE_ID = 2;
+ private static final int PROFILE_POPUP_ICON_SIZE_ID = 3;
protected final int mIconSize;
+ private final boolean mCropToCircle;
private final int mColorPressedId;
private long mContactId;
@@ -58,6 +60,7 @@ public class ContactIconView extends AsyncImageView {
final TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.ContactIconView);
final int iconSizeId = a.getInt(R.styleable.ContactIconView_contactIconSize, 0);
+ boolean cropToCircle = true;
switch (iconSizeId) {
case NORMAL_ICON_SIZE_ID:
mIconSize = (int) resources.getDimension(
@@ -71,11 +74,17 @@ public class ContactIconView extends AsyncImageView {
mIconSize = (int) resources.getDimension(
R.dimen.contact_icon_view_small_size);
break;
+ case PROFILE_POPUP_ICON_SIZE_ID:
+ mIconSize = (int) resources.getDimension(
+ R.dimen.home_list_profile_avatar_size);
+ cropToCircle = false;
+ break;
default:
// For the compiler, something has to be set even with the assert.
mIconSize = 0;
Assert.fail("Unsupported ContactIconView icon size attribute");
}
+ mCropToCircle = cropToCircle;
mColorPressedId = resources.getColor(R.color.contact_avatar_pressed_color,
context.getTheme());
@@ -118,7 +127,8 @@ public class ContactIconView extends AsyncImageView {
if (AvatarUriUtil.TYPE_GROUP_URI.equals(avatarType)) {
setImageResourceId(new AvatarGroupRequestDescriptor(uri, mIconSize, mIconSize));
} else {
- setImageResourceId(new AvatarRequestDescriptor(uri, mIconSize, mIconSize));
+ setImageResourceId(new AvatarRequestDescriptor(uri, mIconSize, mIconSize,
+ mCropToCircle));
}
}
diff --git a/app/src/main/java/com/android/messaging/ui/UIIntents.java b/app/src/main/java/com/android/messaging/ui/UIIntents.java
index b8a08fd..b0179b9 100644
--- a/app/src/main/java/com/android/messaging/ui/UIIntents.java
+++ b/app/src/main/java/com/android/messaging/ui/UIIntents.java
@@ -166,9 +166,15 @@ public abstract class UIIntents {
public abstract void launchAddContactActivity(final Context context, final String destination);
/**
- * Launch an activity to show people & options for a given conversation.
+ * Launch home UI to show the profile of a given conversation.
*/
- public abstract void launchPeopleAndOptionsActivity(final Activity context,
+ public abstract void launchHomeProfileActivity(final Activity activity,
+ final String conversationId);
+
+ /**
+ * Launch home UI to search the messages of a given conversation.
+ */
+ public abstract void launchHomeConversationSearchActivity(final Context context,
final String conversationId);
/**
@@ -189,6 +195,21 @@ public abstract class UIIntents {
*/
public abstract void launchBlockedParticipantsActivity(final Context context);
+ /**
+ * Launch home UI to manage spam-registered (blocked) senders.
+ */
+ public abstract void launchHomeSpamActivity(final Context context);
+
+ /**
+ * Launch home UI to search messages.
+ */
+ public abstract void launchHomeSearchActivity(final Context context);
+
+ /**
+ * Launch home UI to start a new chat.
+ */
+ public abstract void launchHomeComposeActivity(final Context context);
+
/**
* Launch an activity to show a class zero message
*/
diff --git a/app/src/main/java/com/android/messaging/ui/UIIntentsImpl.java b/app/src/main/java/com/android/messaging/ui/UIIntentsImpl.java
index bfa0c70..d7e597e 100644
--- a/app/src/main/java/com/android/messaging/ui/UIIntentsImpl.java
+++ b/app/src/main/java/com/android/messaging/ui/UIIntentsImpl.java
@@ -57,7 +57,6 @@ import com.android.messaging.ui.conversation.LaunchConversationActivity;
import com.android.messaging.ui.conversationlist.ArchivedConversationListActivity;
import com.android.messaging.ui.conversationlist.ConversationListActivity;
import com.android.messaging.ui.conversationlist.ForwardMessageActivity;
-import com.android.messaging.ui.conversationsettings.PeopleAndOptionsActivity;
import com.android.messaging.ui.photoviewer.BuglePhotoViewActivity;
import com.android.messaging.util.Assert;
import com.android.messaging.util.ConversationIdSet;
@@ -215,11 +214,42 @@ public class UIIntentsImpl extends UIIntents {
}
@Override
- public void launchPeopleAndOptionsActivity(final Activity activity,
- final String conversationId) {
- final Intent intent = new Intent(activity, PeopleAndOptionsActivity.class);
+ public void launchHomeSpamActivity(final Context context) {
+ final Intent intent = new Intent(context,
+ com.android.messaging.ui.home.HomeSpamActivity.class);
+ context.startActivity(intent);
+ }
+
+ @Override
+ public void launchHomeSearchActivity(final Context context) {
+ final Intent intent = new Intent(context,
+ com.android.messaging.ui.home.HomeSearchActivity.class);
+ context.startActivity(intent);
+ }
+
+ @Override
+ public void launchHomeComposeActivity(final Context context) {
+ final Intent intent = new Intent(context,
+ com.android.messaging.ui.home.HomeComposeActivity.class);
+ context.startActivity(intent);
+ }
+
+ @Override
+ public void launchHomeProfileActivity(final Activity activity, final String conversationId) {
+ final Intent intent = new Intent(activity,
+ com.android.messaging.ui.home.HomeProfileActivity.class);
intent.putExtra(UI_INTENT_EXTRA_CONVERSATION_ID, conversationId);
activity.startActivityForResult(intent, 0);
+ activity.overridePendingTransition(R.anim.home_activity_open, R.anim.home_activity_hold);
+ }
+
+ @Override
+ public void launchHomeConversationSearchActivity(final Context context,
+ final String conversationId) {
+ final Intent intent = new Intent(context,
+ com.android.messaging.ui.home.HomeConversationSearchActivity.class);
+ intent.putExtra(UI_INTENT_EXTRA_CONVERSATION_ID, conversationId);
+ context.startActivity(intent);
}
@Override
diff --git a/app/src/main/java/com/android/messaging/ui/conversation/ComposeMessageView.java b/app/src/main/java/com/android/messaging/ui/conversation/ComposeMessageView.java
index 9204fa0..f690691 100644
--- a/app/src/main/java/com/android/messaging/ui/conversation/ComposeMessageView.java
+++ b/app/src/main/java/com/android/messaging/ui/conversation/ComposeMessageView.java
@@ -35,11 +35,14 @@ import android.util.AttributeSet;
import android.view.ContextThemeWrapper;
import android.view.KeyEvent;
import android.view.View;
+import android.view.ViewGroup;
import android.view.accessibility.AccessibilityEvent;
import android.view.inputmethod.EditorInfo;
import android.widget.ImageButton;
+import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.TextView;
+import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.appcompat.app.ActionBar;
@@ -65,6 +68,7 @@ import com.android.messaging.ui.AttachmentPreview;
import com.android.messaging.ui.BugleActionBarActivity;
import com.android.messaging.ui.PlainTextEditText;
import com.android.messaging.ui.conversation.ConversationInputManager.ConversationInputSink;
+import com.android.messaging.ui.home.HomeUiMotion;
import com.android.messaging.util.AccessibilityUtil;
import com.android.messaging.util.Assert;
import com.android.messaging.util.AvatarUriUtil;
@@ -102,6 +106,7 @@ public class ComposeMessageView extends LinearLayout
boolean tooManyVideos);
void notifyOfAttachmentLoadFailed();
void showAttachmentChooser();
+ void launchSystemGalleryPicker();
boolean shouldShowSubjectEditor();
boolean shouldHideAttachmentsWhenSimSelectorShown();
Uri getSelfSendButtonIconUri();
@@ -128,6 +133,13 @@ public class ComposeMessageView extends LinearLayout
private ImageButton mDeleteSubjectButton;
private AttachmentPreview mAttachmentPreview;
private ImageButton mAttachMediaButton;
+ private View mAttachmentSheetContainer;
+ private View mComposeBottomStack;
+ private View mComposeBar;
+ private boolean mAttachmentSheetOpen;
+ private int mComposeBarPaddingBottom;
+ private ImageButton mVoiceButton;
+ private View mSendColumn;
private final Binding mBinding;
private IComposeMessageViewHost mHost;
@@ -283,10 +295,37 @@ public class ComposeMessageView extends LinearLayout
});
mAttachMediaButton = findViewById(R.id.attach_media_button);
- mAttachMediaButton.setOnClickListener(clickView -> {
- // Showing the media picker is treated as starting to compose the message.
- mInputManager.showHideMediaPicker(true /* show */, true /* animate */);
- });
+ mAttachmentSheetContainer = findViewById(R.id.home_compose_attachment_sheet_container);
+ mComposeBottomStack = findViewById(R.id.home_compose_bottom_stack);
+ mComposeBar = findViewById(R.id.home_conversation_compose_bar);
+ if (mComposeBar != null) {
+ mComposeBarPaddingBottom = mComposeBar.getPaddingBottom();
+ }
+ setupComposeAttachmentSheetTiles();
+ updateComposeBottomStackBackground();
+ mAttachMediaButton.setOnClickListener(clickView -> toggleComposeAttachmentSheet());
+
+ final ImageButton emojiButton = findViewById(R.id.home_compose_emoji_button);
+ if (emojiButton != null) {
+ emojiButton.setOnClickListener(v -> mComposeEditText.requestFocus());
+ }
+ final ImageButton galleryButton = findViewById(R.id.home_compose_gallery_button);
+ if (galleryButton != null) {
+ galleryButton.setOnClickListener(v -> {
+ closeComposeAttachmentSheet(true /* animate */, () -> {
+ if (mHost != null) {
+ mHost.launchSystemGalleryPicker();
+ }
+ });
+ });
+ }
+
+ mVoiceButton = findViewById(R.id.home_conversation_voice_button);
+ mSendColumn = findViewById(R.id.home_conversation_send_column);
+ if (mVoiceButton != null) {
+ mVoiceButton.setOnClickListener(v -> Toast.makeText(mOriginalContext,
+ R.string.home_conversation_voice_coming_soon, Toast.LENGTH_SHORT).show());
+ }
mAttachmentPreview = findViewById(R.id.attachment_draft_view);
mAttachmentPreview.setComposeMessageView(this);
@@ -666,7 +705,7 @@ public class ComposeMessageView extends LinearLayout
mMessageBodySize.setVisibility(View.VISIBLE);
}
} else {
- mMessageBodySize.setVisibility(View.INVISIBLE);
+ mMessageBodySize.setVisibility(View.GONE);
}
} else { // SMS case
// Update the SMS text counter.
@@ -685,7 +724,7 @@ public class ComposeMessageView extends LinearLayout
mMessageBodySize.setText(counterText);
mMessageBodySize.setVisibility(View.VISIBLE);
} else {
- mMessageBodySize.setVisibility(View.INVISIBLE);
+ mMessageBodySize.setVisibility(View.GONE);
}
}
@@ -724,6 +763,7 @@ public class ComposeMessageView extends LinearLayout
setSendButtonAccessibility(sendWidgetMode);
mSendWidgetMode = sendWidgetMode;
}
+ updateVoiceAndSendColumnVisibility(sendWidgetMode);
// Update the text hint on the message box depending on the attachment type.
final int attachmentCount = attachments.size();
@@ -795,6 +835,16 @@ public class ComposeMessageView extends LinearLayout
}
}
+ private void updateVoiceAndSendColumnVisibility(final int sendWidgetMode) {
+ if (mVoiceButton == null || mSendColumn == null) {
+ return;
+ }
+ final boolean showSendColumn = sendWidgetMode == SEND_WIDGET_MODE_SEND_BUTTON
+ || sendWidgetMode == SEND_WIDGET_MODE_SIM_SELECTOR;
+ mSendColumn.setVisibility(showSendColumn ? View.VISIBLE : View.GONE);
+ mVoiceButton.setVisibility(showSendColumn ? View.GONE : View.VISIBLE);
+ }
+
private void setSendButtonAccessibility(final int sendWidgetMode) {
switch (sendWidgetMode) {
case SEND_WIDGET_MODE_SELF_AVATAR:
@@ -920,6 +970,9 @@ public class ComposeMessageView extends LinearLayout
}
public void hideAllComposeInputs(final boolean animate) {
+ if (mAttachmentSheetOpen) {
+ closeComposeAttachmentSheet(animate, null);
+ }
mInputManager.hideAllInputs(animate);
}
@@ -928,10 +981,17 @@ public class ComposeMessageView extends LinearLayout
}
public void resetMediaPickerState() {
+ if (mAttachmentSheetOpen) {
+ closeComposeAttachmentSheet(false /* animate */, null);
+ }
mInputManager.resetMediaPickerState();
}
public boolean onBackPressed() {
+ if (mAttachmentSheetOpen) {
+ closeComposeAttachmentSheet(true /* animate */, null);
+ return true;
+ }
return mInputManager.onBackPressed();
}
@@ -982,4 +1042,167 @@ public class ComposeMessageView extends LinearLayout
mAttachMediaButton.setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_NO);
}
}
+
+ private void setupComposeAttachmentSheetTiles() {
+ if (mAttachmentSheetContainer == null) {
+ return;
+ }
+ bindComposeAttachmentTile(R.id.home_mediapicker_tile_gallery,
+ R.drawable.ic_home_image_line, R.string.home_mediapicker_gallery,
+ ConversationInputManager.MEDIA_PICKER_TARGET_GALLERY);
+ bindComposeAttachmentTile(R.id.home_mediapicker_tile_camera,
+ R.drawable.ic_home_camera_line, R.string.home_mediapicker_camera,
+ ConversationInputManager.MEDIA_PICKER_TARGET_CAMERA);
+ bindComposeAttachmentTile(R.id.home_mediapicker_tile_contact,
+ R.drawable.ic_home_menu_person, R.string.home_mediapicker_contact,
+ ConversationInputManager.MEDIA_PICKER_TARGET_CONTACT);
+ bindComposeAttachmentTile(R.id.home_mediapicker_tile_file,
+ R.drawable.ic_attachment_dark, R.string.home_mediapicker_file,
+ ConversationInputManager.MEDIA_PICKER_TARGET_GALLERY);
+ bindComposeAttachmentTile(R.id.home_mediapicker_tile_gif,
+ R.drawable.ic_home_star_line, R.string.home_mediapicker_gif, -1);
+ bindComposeAttachmentTile(R.id.home_mediapicker_tile_sticker,
+ R.drawable.ic_home_emoji, R.string.home_mediapicker_sticker, -1);
+ bindComposeAttachmentTile(R.id.home_mediapicker_tile_schedule,
+ R.drawable.ic_home_bell, R.string.home_mediapicker_schedule, -1);
+ bindComposeAttachmentTile(R.id.home_mediapicker_tile_location_once,
+ R.drawable.ic_home_place_line, R.string.home_mediapicker_location_once, -1);
+ bindComposeAttachmentTile(R.id.home_mediapicker_tile_location_live,
+ R.drawable.ic_home_location_live, R.string.home_mediapicker_location_live, -1);
+ }
+
+ private void bindComposeAttachmentTile(final int tileRootId, final int iconRes,
+ final int labelRes, final int mediaPickerTarget) {
+ final View tile = findViewById(tileRootId);
+ if (tile == null) {
+ return;
+ }
+ final ImageView icon = tile.findViewById(R.id.home_mediapicker_tile_icon);
+ final TextView label = tile.findViewById(R.id.home_mediapicker_tile_label);
+ if (icon != null) {
+ icon.setImageResource(iconRes);
+ }
+ if (label != null) {
+ label.setText(labelRes);
+ }
+ tile.setOnClickListener(v -> {
+ if (mediaPickerTarget >= 0) {
+ openMediaFromAttachmentSheet(mediaPickerTarget);
+ } else {
+ Toast.makeText(mOriginalContext, R.string.home_popover_coming_soon,
+ Toast.LENGTH_SHORT).show();
+ }
+ });
+ }
+
+ private void toggleComposeAttachmentSheet() {
+ if (mAttachmentSheetOpen) {
+ closeComposeAttachmentSheet(true /* animate */, null);
+ } else {
+ if (mInputManager != null) {
+ mInputManager.showHideImeKeyboard(false, true);
+ mInputManager.showHideMediaPicker(false, false);
+ }
+ mAttachmentSheetOpen = true;
+ updateAttachButtonContentDescription();
+ updateComposeBottomStackBackground();
+ HomeUiMotion.composeAttachmentSheetOpen(mAttachmentSheetContainer, mAttachMediaButton);
+ }
+ }
+
+ private void closeComposeAttachmentSheet(final boolean animate, final Runnable endAction) {
+ if (!mAttachmentSheetOpen || mAttachmentSheetContainer == null) {
+ if (endAction != null) {
+ endAction.run();
+ }
+ return;
+ }
+ mAttachmentSheetOpen = false;
+ updateAttachButtonContentDescription();
+ updateComposeBottomStackBackground();
+ if (animate) {
+ HomeUiMotion.composeAttachmentSheetClose(mAttachmentSheetContainer, mAttachMediaButton,
+ endAction);
+ } else {
+ mAttachmentSheetContainer.setVisibility(View.GONE);
+ final ViewGroup.LayoutParams params = mAttachmentSheetContainer.getLayoutParams();
+ params.height = 0;
+ mAttachmentSheetContainer.setLayoutParams(params);
+ mAttachmentSheetContainer.setTranslationY(0f);
+ mAttachmentSheetContainer.setAlpha(1f);
+ if (mAttachMediaButton != null) {
+ mAttachMediaButton.setImageResource(R.drawable.ic_home_compose_plus);
+ mAttachMediaButton.setRotation(0f);
+ }
+ if (endAction != null) {
+ endAction.run();
+ }
+ }
+ }
+
+ private void updateComposeBottomStackBackground() {
+ if (mComposeBottomStack == null) {
+ return;
+ }
+ if (mAttachmentSheetOpen) {
+ mComposeBottomStack.setBackgroundColor(
+ getResources().getColor(R.color.home_bg, mComposeBottomStack.getContext().getTheme()));
+ mComposeBottomStack.setClipToOutline(false);
+ if (mComposeBar != null) {
+ mComposeBar.setBackgroundColor(
+ getResources().getColor(R.color.home_bg, mComposeBar.getContext().getTheme()));
+ final int gap = getResources().getDimensionPixelSize(
+ R.dimen.home_compose_attach_sheet_gap);
+ mComposeBar.setPadding(mComposeBar.getPaddingLeft(), mComposeBar.getPaddingTop(),
+ mComposeBar.getPaddingRight(), gap);
+ }
+ if (mMessageBodySize != null && mMessageBodySize.getVisibility() != View.VISIBLE) {
+ mMessageBodySize.setVisibility(View.GONE);
+ }
+ } else {
+ mComposeBottomStack.setClipToOutline(false);
+ mComposeBottomStack.setBackgroundColor(
+ getResources().getColor(R.color.home_bg, mComposeBottomStack.getContext().getTheme()));
+ if (mComposeBar != null) {
+ mComposeBar.setBackgroundColor(
+ getResources().getColor(R.color.home_bg, mComposeBar.getContext().getTheme()));
+ mComposeBar.setPadding(mComposeBar.getPaddingLeft(), mComposeBar.getPaddingTop(),
+ mComposeBar.getPaddingRight(), mComposeBarPaddingBottom);
+ }
+ if (mMessageBodySize != null && mMessageBodySize.getVisibility() != View.VISIBLE) {
+ mMessageBodySize.setVisibility(View.GONE);
+ }
+ }
+ }
+
+ private void openMediaFromAttachmentSheet(final int mediaPickerTarget) {
+ if (mediaPickerTarget == ConversationInputManager.MEDIA_PICKER_TARGET_GALLERY) {
+ closeComposeAttachmentSheet(false /* animate */, null);
+ mHost.launchSystemGalleryPicker();
+ return;
+ }
+ if (mediaPickerTarget == ConversationInputManager.MEDIA_PICKER_TARGET_CONTACT) {
+ if (mInputManager != null) {
+ mInputManager.showHideMediaPicker(true /* show */, false /* animate */,
+ mediaPickerTarget);
+ }
+ closeComposeAttachmentSheet(false /* animate */, null);
+ return;
+ }
+ closeComposeAttachmentSheet(true /* animate */, () -> {
+ if (mInputManager != null) {
+ mInputManager.showHideMediaPicker(true /* show */, true /* animate */,
+ mediaPickerTarget);
+ }
+ });
+ }
+
+ private void updateAttachButtonContentDescription() {
+ if (mAttachMediaButton == null) {
+ return;
+ }
+ mAttachMediaButton.setContentDescription(getResources().getText(mAttachmentSheetOpen
+ ? R.string.home_sub_header_close
+ : R.string.attachMediaButtonContentDescription));
+ }
}
diff --git a/app/src/main/java/com/android/messaging/ui/conversation/ConversationActivity.java b/app/src/main/java/com/android/messaging/ui/conversation/ConversationActivity.java
index 3bf75ad..75d9ce2 100644
--- a/app/src/main/java/com/android/messaging/ui/conversation/ConversationActivity.java
+++ b/app/src/main/java/com/android/messaging/ui/conversation/ConversationActivity.java
@@ -61,6 +61,10 @@ public class ConversationActivity extends BugleActionBarActivity
// Tracks whether onPause is called.
private boolean mIsPaused;
+ // Photo Picker is a system Activity; skip its close animation so a black window
+ // does not slide down over the conversation when the handle is dismissed.
+ private boolean mSuppressNextResumeTransition;
+
@Override
protected void onCreate(final Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
@@ -146,9 +150,17 @@ public class ConversationActivity extends BugleActionBarActivity
}
}
+ public void suppressNextResumeTransition() {
+ mSuppressNextResumeTransition = true;
+ }
+
@Override
protected void onResume() {
super.onResume();
+ if (mSuppressNextResumeTransition) {
+ overridePendingTransition(0, 0);
+ mSuppressNextResumeTransition = false;
+ }
// we need to reset the mInstanceStateSaved flag since we may have just been restored from
// a previous onStop() instead of an onDestroy().
diff --git a/app/src/main/java/com/android/messaging/ui/conversation/ConversationFragment.java b/app/src/main/java/com/android/messaging/ui/conversation/ConversationFragment.java
index 68fbb21..d18075a 100644
--- a/app/src/main/java/com/android/messaging/ui/conversation/ConversationFragment.java
+++ b/app/src/main/java/com/android/messaging/ui/conversation/ConversationFragment.java
@@ -43,11 +43,15 @@ import android.telephony.PhoneNumberUtils;
import android.text.TextUtils;
import android.view.ActionMode;
import android.view.Display;
+import android.view.HapticFeedbackConstants;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View;
+import android.widget.ImageButton;
+import android.widget.PopupMenu;
+import android.widget.Toast;
import android.view.ViewConfiguration;
import android.view.ViewGroup;
import android.widget.TextView;
@@ -72,7 +76,9 @@ import androidx.recyclerview.widget.RecyclerView.ViewHolder;
import com.android.messaging.R;
import com.android.messaging.datamodel.DataModel;
import com.android.messaging.datamodel.MessagingContentProvider;
+import com.android.messaging.datamodel.action.BugleActionToasts;
import com.android.messaging.datamodel.action.InsertNewMessageAction;
+import com.android.messaging.datamodel.action.UpdateDestinationBlockedAction;
import com.android.messaging.datamodel.binding.Binding;
import com.android.messaging.datamodel.binding.BindingBase;
import com.android.messaging.datamodel.binding.ImmutableBindingRef;
@@ -88,20 +94,27 @@ import com.android.messaging.datamodel.data.ParticipantData;
import com.android.messaging.datamodel.data.SubscriptionListData.SubscriptionListEntry;
import com.android.messaging.ui.AttachmentPreview;
import com.android.messaging.ui.BugleActionBarActivity;
+import com.android.messaging.ui.ContactIconView;
import com.android.messaging.ui.ConversationDrawables;
import com.android.messaging.ui.SnackBar;
import com.android.messaging.ui.UIIntents;
import com.android.messaging.ui.animation.PopupTransitionAnimation;
+import com.android.messaging.ui.home.HomeGallerySheet;
+import com.android.messaging.ui.home.HomeUiMotion;
+import com.android.messaging.ui.home.UnknownSenderPromptStore;
import com.android.messaging.ui.attachmentchooser.AttachmentChooserActivity;
import com.android.messaging.ui.contact.AddContactsConfirmationDialog;
import com.android.messaging.ui.conversation.ComposeMessageView.IComposeMessageViewHost;
import com.android.messaging.ui.conversation.ConversationInputManager.ConversationInputHost;
import com.android.messaging.ui.conversation.ConversationMessageView.ConversationMessageViewHost;
+import com.android.messaging.ui.mediapicker.DocumentImagePicker;
import com.android.messaging.ui.mediapicker.MediaPicker;
import com.android.messaging.util.AccessibilityUtil;
import com.android.messaging.util.Assert;
import com.android.messaging.util.AvatarUriUtil;
+import com.android.messaging.util.Dates;
import com.android.messaging.util.ChangeDefaultSmsAppHelper;
+import com.android.messaging.util.ContactUtil;
import com.android.messaging.util.ImeUtil;
import com.android.messaging.util.LogUtil;
import com.android.messaging.util.PhoneUtils;
@@ -142,11 +155,35 @@ public class ConversationFragment extends Fragment implements ConversationDataLi
public static final int MESSAGE_ANIMATION_MAX_WAIT = 500;
private ComposeMessageView mComposeMessageView;
+ private DocumentImagePicker mGalleryPicker;
private RecyclerView mRecyclerView;
private ConversationMessageAdapter mAdapter;
private ConversationFastScroller mFastScroller;
private View mConversationComposeDivider;
+ private View mEmptyContainer;
+ private TextView mEmptyTime;
+ private TextView mEmptyStatus;
+ private View mConversationHeader;
+ private TextView mConversationHeaderTitle;
+ private ContactIconView mConversationHeaderAvatar;
+ private ImageButton mConversationHeaderOverflow;
+ private boolean mOptionsMenuVisible = true;
+ private View mUnknownSenderBanner;
+ private TextView mUnknownSenderTitle;
+ private boolean mUnknownSenderBannerVisible;
+ private View mMessageSelectionHeader;
+ private TextView mMessageSelectionTitle;
+ private View mComposeContainer;
+ private View mMessageActionBar;
+ private View mMessageActionCopy;
+ private View mMessageActionForward;
+ private View mMessageActionShare;
+ private View mMessageActionDetails;
+ private View mMessageActionSave;
+ private View mMessageActionSend;
+ private View mMessageActionDownload;
+ private View mMessageActionDelete;
private ChangeDefaultSmsAppHelper mChangeDefaultSmsAppHelper;
private String mConversationId;
@@ -267,133 +304,6 @@ public class ConversationFragment extends Fragment implements ConversationDataLi
}
};
- private final ActionMode.Callback mMessageActionModeCallback = new ActionMode.Callback() {
- @Override
- public boolean onCreateActionMode(final ActionMode actionMode, final Menu menu) {
- if (mSelectedMessage == null) {
- return false;
- }
- final ConversationMessageData data = mSelectedMessage.getData();
- final MenuInflater menuInflater = getActivity().getMenuInflater();
- menuInflater.inflate(R.menu.conversation_fragment_select_menu, menu);
- menu.findItem(R.id.action_download).setVisible(data.getShowDownloadMessage());
- menu.findItem(R.id.action_send).setVisible(data.getShowResendMessage());
-
- // ShareActionProvider does not work with ActionMode. So we use a normal menu item.
- menu.findItem(R.id.share_message_menu).setVisible(data.getCanForwardMessage());
- menu.findItem(R.id.save_attachment).setVisible(mSelectedAttachment != null);
- menu.findItem(R.id.forward_message_menu).setVisible(data.getCanForwardMessage());
-
- // TODO: We may want to support copying attachments in the future, but it's
- // unclear which attachment to pick when we make this context menu at the message level
- // instead of the part level
- menu.findItem(R.id.copy_text).setVisible(data.getCanCopyMessageToClipboard());
-
- return true;
- }
-
- @Override
- public boolean onPrepareActionMode(final ActionMode actionMode, final Menu menu) {
- return true;
- }
-
- @Override
- public boolean onActionItemClicked(final ActionMode actionMode, final MenuItem menuItem) {
- final ConversationMessageData data = mSelectedMessage.getData();
- final String messageId = data.getMessageId();
- int itemId = menuItem.getItemId();
- if (itemId == R.id.save_attachment) {
- final SaveAttachmentTask saveAttachmentTask = new SaveAttachmentTask(
- getActivity());
- for (final MessagePartData part : data.getAttachments()) {
- saveAttachmentTask.addAttachmentToSave(part.getContentUri(),
- part.getContentType());
- }
- if (saveAttachmentTask.getAttachmentCount() > 0) {
- saveAttachmentTask.execute();
- mHost.dismissActionMode();
- }
- return true;
- } else if (itemId == R.id.action_delete_message) {
- if (mSelectedMessage != null) {
- deleteMessage(messageId);
- }
- return true;
- } else if (itemId == R.id.action_download) {
- if (mSelectedMessage != null) {
- retryDownload(messageId);
- mHost.dismissActionMode();
- }
- return true;
- } else if (itemId == R.id.action_send) {
- if (mSelectedMessage != null) {
- retrySend(messageId);
- mHost.dismissActionMode();
- }
- return true;
- } else if (itemId == R.id.copy_text) {
- Assert.isTrue(data.hasText());
- final ClipboardManager clipboard = (ClipboardManager) getActivity()
- .getSystemService(Context.CLIPBOARD_SERVICE);
- clipboard.setPrimaryClip(
- ClipData.newPlainText(null /* label */, data.getText()));
- mHost.dismissActionMode();
- return true;
- } else if (itemId == R.id.details_menu) {
- MessageDetailsDialog.show(
- getActivity(), data, mBinding.getData().getParticipants(),
- mBinding.getData().getSelfParticipantById(data.getSelfParticipantId()));
- mHost.dismissActionMode();
- return true;
- } else if (itemId == R.id.share_message_menu) {
- shareMessage(data);
- mHost.dismissActionMode();
- return true;
- } else if (itemId == R.id.forward_message_menu) {
- // TODO: Currently we are forwarding one part at a time, instead of
- // the entire message. Change this to forwarding the entire message when we
- // use message-based cursor in conversation.
- final MessageData message = mBinding.getData().createForwardedMessage(data);
- UIIntents.get().launchForwardMessageActivity(getActivity(), message);
- mHost.dismissActionMode();
- return true;
- }
- return false;
- }
-
- private void shareMessage(final ConversationMessageData data) {
- // Figure out what to share.
- MessagePartData attachmentToShare = mSelectedAttachment;
- // If the user long-pressed on the background, we will share the text (if any)
- // or the first attachment.
- if (mSelectedAttachment == null
- && TextUtil.isAllWhitespace(data.getText())) {
- final List attachments = data.getAttachments();
- if (attachments.size() > 0) {
- attachmentToShare = attachments.get(0);
- }
- }
-
- final Intent shareIntent = new Intent();
- shareIntent.setAction(Intent.ACTION_SEND);
- if (attachmentToShare == null) {
- shareIntent.putExtra(Intent.EXTRA_TEXT, data.getText());
- shareIntent.setType("text/plain");
- } else {
- shareIntent.putExtra(
- Intent.EXTRA_STREAM, attachmentToShare.getContentUri());
- shareIntent.setType(attachmentToShare.getContentType());
- }
- final CharSequence title = getResources().getText(R.string.action_share);
- startActivity(Intent.createChooser(shareIntent, title));
- }
-
- @Override
- public void onDestroyActionMode(final ActionMode actionMode) {
- selectMessage(null);
- }
- };
-
private final ActivityResultLauncher mLauncher = registerForActivityResult(
new ActivityResultContracts.StartActivityForResult(), result -> {
if (result.getResultCode() == Activity.RESULT_OK) {
@@ -419,6 +329,11 @@ public class ConversationFragment extends Fragment implements ConversationDataLi
@Override
public void onCreate(final Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
+ mGalleryPicker = new DocumentImagePicker(this, pendingItem -> {
+ if (mComposeMessageView != null) {
+ mComposeMessageView.onPendingAttachmentAdded(pendingItem);
+ }
+ });
mAdapter = new ConversationMessageAdapter(getActivity(), null, this,
null,
// Sets the item click listener on the Recycler item views.
@@ -427,6 +342,7 @@ public class ConversationFragment extends Fragment implements ConversationDataLi
handleMessageClick(messageView);
},
view -> {
+ view.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS);
selectMessage((ConversationMessageView) view);
return true;
}
@@ -507,6 +423,12 @@ public class ConversationFragment extends Fragment implements ConversationDataLi
final Bundle savedInstanceState) {
final View view = inflater.inflate(R.layout.conversation_fragment, container, false);
mRecyclerView = view.findViewById(android.R.id.list);
+ mEmptyContainer = view.findViewById(R.id.home_conversation_empty);
+ mEmptyTime = view.findViewById(R.id.home_conversation_empty_time);
+ mEmptyStatus = view.findViewById(R.id.home_conversation_empty_status);
+ setupConversationHeader(view);
+ setupUnknownSenderBanner(view);
+ setupMessageSelectionChrome(view);
final LinearLayoutManager manager = new LinearLayoutManager(getActivity());
manager.setStackFromEnd(true);
manager.setReverseLayout(false);
@@ -672,13 +594,19 @@ public class ConversationFragment extends Fragment implements ConversationDataLi
mSelectedMessage = messageView;
if (mSelectedMessage == null) {
mAdapter.setSelectedMessage(null);
- mHost.dismissActionMode();
mSelectedAttachment = null;
+ updateMessageSelectionChrome(false);
+ updateUnknownSenderBanner();
return;
}
mSelectedAttachment = attachment;
mAdapter.setSelectedMessage(messageView.getData().getMessageId());
- mHost.startActionMode(mMessageActionModeCallback);
+ updateMessageSelectionChrome(true);
+ updateUnknownSenderBanner();
+ }
+
+ private void dismissMessageSelection() {
+ selectMessage(null);
}
@Override
@@ -732,7 +660,7 @@ public class ConversationFragment extends Fragment implements ConversationDataLi
@Override
public void onCreateOptionsMenu(@NonNull final Menu menu,
@NonNull final MenuInflater inflater) {
- if (mHost.getActionMode() != null) {
+ if (mSelectedMessage != null) {
return;
}
@@ -746,7 +674,9 @@ public class ConversationFragment extends Fragment implements ConversationDataLi
// See if we can show add contact action.
final ParticipantData participant = data.getOtherParticipant();
final boolean addContactActionVisible = (participant != null
- && TextUtils.isEmpty(participant.getLookupKey()));
+ && !ContactUtil.isParticipantInAddressBook(getActivity(),
+ participant.getContactId(), participant.getLookupKey(),
+ participant.getNormalizedDestination()));
menu.findItem(R.id.action_add_contact).setVisible(addContactActionVisible);
// See if we should show archive or unarchive.
@@ -765,32 +695,14 @@ public class ConversationFragment extends Fragment implements ConversationDataLi
int itemId = item.getItemId();
if (itemId == R.id.action_people_and_options) {
Assert.isTrue(mBinding.getData().getParticipantsLoaded());
- UIIntents.get().launchPeopleAndOptionsActivity(getActivity(), mConversationId);
+ UIIntents.get().launchHomeProfileActivity(getActivity(), mConversationId);
+ return true;
+ } else if (itemId == R.id.action_search_conversation) {
+ UIIntents.get().launchHomeConversationSearchActivity(getActivity(), mConversationId);
return true;
} else if (itemId == R.id.action_call) {
- final String phoneNumber = mBinding.getData().getParticipantPhoneNumber();
- Assert.notNull(phoneNumber);
- // Can't make a call to emergency numbers using ACTION_CALL.
- if (PhoneNumberUtils.isEmergencyNumber(phoneNumber)) {
- UiUtils.showToast(R.string.disallow_emergency_call);
- } else {
- final View targetView = getActivity().findViewById(R.id.action_call);
- Point centerPoint;
- if (targetView != null) {
- final int[] screenLocation = new int[2];
- targetView.getLocationOnScreen(screenLocation);
- final int centerX = screenLocation[0] + targetView.getWidth() / 2;
- final int centerY = screenLocation[1] + targetView.getHeight() / 2;
- centerPoint = new Point(centerX, centerY);
- } else {
- // In the overflow menu, just use the center of the screen.
- final Display display =
- getActivity().getWindowManager().getDefaultDisplay();
- centerPoint = new Point(display.getWidth() / 2, display.getHeight() / 2);
- }
- UIIntents.get()
- .launchPhoneCallActivity(getActivity(), phoneNumber, centerPoint);
- }
+ placeConversationCall(getActivity() != null
+ ? getActivity().findViewById(R.id.action_call) : null);
return true;
} else if (itemId == R.id.action_archive) {
mBinding.getData().archiveConversation(mBinding);
@@ -904,6 +816,8 @@ public class ConversationFragment extends Fragment implements ConversationDataLi
}
}
+ updateHomeEmptyState();
+ updateUnknownSenderBanner();
mHost.invalidateActionBar();
}
@@ -931,6 +845,9 @@ public class ConversationFragment extends Fragment implements ConversationDataLi
invalidateOptionsMenu();
mHost.onConversationMetadataUpdated();
mAdapter.notifyDataSetChanged();
+ updateHomeEmptyState();
+ updateUnknownSenderBanner();
+ mHost.invalidateActionBar();
}
public void setConversationInfo(final Context context, final String conversationId,
@@ -940,6 +857,7 @@ public class ConversationFragment extends Fragment implements ConversationDataLi
if (!mBinding.isBound()) {
mConversationId = conversationId;
mIncomingDraft = draftData;
+ mUnknownSenderBannerVisible = false;
mBinding.bind(DataModel.get().createConversationData(context, this, conversationId));
} else {
Assert.isTrue(TextUtils.equals(mBinding.getData().getConversationId(), conversationId));
@@ -1118,12 +1036,12 @@ public class ConversationFragment extends Fragment implements ConversationDataLi
(dialog, which) ->
mBinding.getData().deleteMessage(mBinding, messageId))
.setNegativeButton(android.R.string.cancel, null);
- builder.setOnDismissListener(dialog -> mHost.dismissActionMode());
+ builder.setOnDismissListener(dialog -> dismissMessageSelection());
builder.create().show();
} else {
warnOfMissingActionConditions(false /*sending*/,
null /*commandToRunAfterActionConditionResolved*/);
- mHost.dismissActionMode();
+ dismissMessageSelection();
}
}
@@ -1159,6 +1077,7 @@ public class ConversationFragment extends Fragment implements ConversationDataLi
mHost.invalidateActionBar();
mRecyclerView.setVisibility(View.VISIBLE);
+ updateUnknownSenderBanner();
}
}
@@ -1187,6 +1106,13 @@ public class ConversationFragment extends Fragment implements ConversationDataLi
}
public boolean onBackPressed() {
+ if (HomeGallerySheet.dismissIfShowing(getActivity())) {
+ return true;
+ }
+ if (mSelectedMessage != null) {
+ dismissMessageSelection();
+ return true;
+ }
if (mComposeMessageView.onBackPressed()) {
return true;
}
@@ -1366,7 +1292,11 @@ public class ConversationFragment extends Fragment implements ConversationDataLi
@Override
public void setOptionsMenuVisibility(final boolean visible) {
+ mOptionsMenuVisible = visible;
setHasOptionsMenu(visible);
+ if (mConversationHeaderOverflow != null) {
+ mConversationHeaderOverflow.setVisibility(visible ? View.VISIBLE : View.GONE);
+ }
}
@Override
@@ -1385,6 +1315,9 @@ public class ConversationFragment extends Fragment implements ConversationDataLi
@Override
public void dismissActionMode() {
+ if (mSelectedMessage != null) {
+ dismissMessageSelection();
+ }
mHost.dismissActionMode();
}
@@ -1454,6 +1387,13 @@ public class ConversationFragment extends Fragment implements ConversationDataLi
builder.show();
}
+ @Override
+ public void launchSystemGalleryPicker() {
+ if (mGalleryPicker != null) {
+ mGalleryPicker.launchPicker();
+ }
+ }
+
@Override
public void showAttachmentChooser() {
showAttachmentChooser(mConversationId, getActivity());
@@ -1467,62 +1407,444 @@ public class ConversationFragment extends Fragment implements ConversationDataLi
}
private void updateActionAndStatusBarColor(final ActionBar actionBar) {
- final int themeColor = ConversationDrawables.get().getConversationThemeColor();
- actionBar.setBackgroundDrawable(new ColorDrawable(themeColor));
- UiUtils.setStatusBarColor(getActivity(), themeColor);
+ final int barColor = getResources().getColor(R.color.home_conversation_chrome_bg,
+ getActivity().getTheme());
+ actionBar.setBackgroundDrawable(new ColorDrawable(barColor));
+ UiUtils.setStatusBarColor(getActivity(), barColor);
}
- public void updateActionBar(final ActionBar actionBar) {
- if (mComposeMessageView == null || !mComposeMessageView.updateActionBar(actionBar)) {
- updateActionAndStatusBarColor(actionBar);
- // We update this regardless of whether or not the action bar is showing so that we
- // don't get a race when it reappears.
- actionBar.setDisplayOptions(ActionBar.DISPLAY_SHOW_CUSTOM);
- actionBar.setDisplayHomeAsUpEnabled(true);
- // Reset the back arrow to its default
- actionBar.setHomeAsUpIndicator(0);
- View customView = actionBar.getCustomView();
- if (customView == null || customView.getId() != R.id.conversation_title_container) {
- final LayoutInflater inflator = (LayoutInflater)
- getActivity().getSystemService(Context.LAYOUT_INFLATER_SERVICE);
- customView = inflator.inflate(R.layout.action_bar_conversation_name, null);
- customView.setOnClickListener(v -> onBackPressed());
- actionBar.setCustomView(customView);
- }
+ private void updateHomeEmptyState() {
+ if (mEmptyContainer == null || mAdapter == null) {
+ return;
+ }
+ final boolean empty = mAdapter.getItemCount() == 0;
+ mEmptyContainer.setVisibility(empty ? View.VISIBLE : View.GONE);
+ if (!empty) {
+ return;
+ }
+ if (mEmptyTime != null) {
+ mEmptyTime.setText(Dates.getMessageTimeString(System.currentTimeMillis()));
+ }
+ if (mEmptyStatus != null) {
+ final String name = getConversationName();
+ mEmptyStatus.setText(getString(R.string.home_conversation_empty_status,
+ TextUtils.isEmpty(name) ? getString(R.string.app_name) : name));
+ }
+ }
- final TextView conversationNameView = customView.findViewById(R.id.conversation_title);
+ private void setupUnknownSenderBanner(final View root) {
+ mUnknownSenderBanner = root.findViewById(R.id.home_unknown_sender_banner);
+ mUnknownSenderTitle = root.findViewById(R.id.home_unknown_sender_title);
+ if (mUnknownSenderBanner == null) {
+ return;
+ }
+ final View dismiss = root.findViewById(R.id.home_unknown_sender_dismiss);
+ if (dismiss != null) {
+ dismiss.setOnClickListener(v -> {
+ UnknownSenderPromptStore.setDismissed(requireContext(), mConversationId);
+ hideUnknownSenderBanner(true /* animate */);
+ });
+ }
+ final View reportSpam = root.findViewById(R.id.home_unknown_sender_report_spam);
+ if (reportSpam != null) {
+ reportSpam.setOnClickListener(v -> {
+ if (!mBinding.isBound()) {
+ return;
+ }
+ final ParticipantData other = mBinding.getData().getOtherParticipant();
+ if (other != null) {
+ confirmBlockUnknownSender(other);
+ }
+ });
+ }
+ final View addContact = root.findViewById(R.id.home_unknown_sender_add_contact);
+ if (addContact != null) {
+ addContact.setOnClickListener(v -> {
+ if (!mBinding.isBound()) {
+ return;
+ }
+ final ParticipantData participant = mBinding.getData().getOtherParticipant();
+ if (participant == null) {
+ return;
+ }
+ new AddContactsConfirmationDialog(getActivity(),
+ AvatarUriUtil.createAvatarUri(participant),
+ participant.getNormalizedDestination()).show();
+ });
+ }
+ }
+
+ private void confirmBlockUnknownSender(final ParticipantData other) {
+ if (getActivity() == null) {
+ return;
+ }
+ new AlertDialog.Builder(getActivity(), R.style.Home_Dialog)
+ .setTitle(getString(R.string.block_confirmation_title,
+ other.getDisplayDestination()))
+ .setMessage(R.string.block_confirmation_message)
+ .setNegativeButton(android.R.string.cancel, null)
+ .setPositiveButton(android.R.string.ok, (dialog, which) -> {
+ UpdateDestinationBlockedAction.updateDestinationBlocked(
+ other.getNormalizedDestination(), true, mConversationId,
+ BugleActionToasts.makeUpdateDestinationBlockedActionListener(
+ getActivity()));
+ UnknownSenderPromptStore.setDismissed(requireContext(), mConversationId);
+ hideUnknownSenderBanner(false /* animate */);
+ })
+ .show();
+ }
+
+ private void updateUnknownSenderBanner() {
+ if (mUnknownSenderBanner == null || !mBinding.isBound()) {
+ return;
+ }
+ final ConversationData data = mBinding.getData();
+ final ParticipantData other = data.getOtherParticipant();
+ final boolean show = mSelectedMessage == null
+ && data.getParticipantsLoaded()
+ && other != null
+ && TextUtils.isEmpty(other.getLookupKey())
+ && !other.isBlocked()
+ && !UnknownSenderPromptStore.isDismissed(requireContext(), mConversationId);
+ if (!show) {
+ hideUnknownSenderBanner(false /* animate */);
+ return;
+ }
+ if (mUnknownSenderTitle != null) {
+ mUnknownSenderTitle.setText(getString(R.string.home_unknown_sender_title,
+ other.getDisplayDestination()));
+ }
+ if (mUnknownSenderBannerVisible) {
+ mUnknownSenderBanner.setVisibility(View.VISIBLE);
+ return;
+ }
+ mUnknownSenderBannerVisible = true;
+ HomeUiMotion.unknownSenderBannerEnter(mUnknownSenderBanner);
+ }
+
+ private void hideUnknownSenderBanner(final boolean animate) {
+ if (mUnknownSenderBanner == null) {
+ return;
+ }
+ if (!mUnknownSenderBannerVisible
+ && mUnknownSenderBanner.getVisibility() != View.VISIBLE) {
+ return;
+ }
+ mUnknownSenderBannerVisible = false;
+ if (animate) {
+ HomeUiMotion.unknownSenderBannerHide(mUnknownSenderBanner, null);
+ } else {
+ mUnknownSenderBanner.animate().cancel();
+ mUnknownSenderBanner.setVisibility(View.GONE);
+ mUnknownSenderBanner.setAlpha(1f);
+ mUnknownSenderBanner.setTranslationY(0f);
+ }
+ }
+
+ private void setupConversationHeader(final View root) {
+ mConversationHeader = root.findViewById(R.id.conversation_header);
+ final View back = root.findViewById(R.id.conversation_header_back);
+ if (back != null) {
+ back.setOnClickListener(v -> {
+ if (getActivity() != null) {
+ getActivity().onBackPressed();
+ }
+ });
+ }
+ mConversationHeaderTitle = root.findViewById(R.id.conversation_header_title);
+ mConversationHeaderAvatar = root.findViewById(R.id.conversation_header_avatar);
+ if (mConversationHeaderAvatar != null) {
+ mConversationHeaderAvatar.setOnClickListener(v -> openConversationProfileDetails());
+ }
+ if (mConversationHeaderTitle != null) {
+ mConversationHeaderTitle.setOnClickListener(v -> openConversationProfileDetails());
+ }
+ mConversationHeaderOverflow = root.findViewById(R.id.conversation_header_overflow);
+ if (mConversationHeaderOverflow != null) {
+ mConversationHeaderOverflow.setOnClickListener(this::showConversationOverflowMenu);
+ }
+ }
+
+ private void openConversationProfileDetails() {
+ if (getActivity() == null || TextUtils.isEmpty(mConversationId)) {
+ return;
+ }
+ if (mBinding.isBound()) {
+ final ParticipantData other = mBinding.getData().getOtherParticipant();
+ if (other != null && ContactUtil.openContactDetail(getActivity(),
+ other.getContactId(), other.getLookupKey(),
+ other.getNormalizedDestination())) {
+ return;
+ }
+ }
+ UIIntents.get().launchHomeProfileActivity(getActivity(), mConversationId);
+ }
+
+ private void setupMessageSelectionChrome(final View root) {
+ mMessageSelectionHeader = root.findViewById(R.id.home_message_selection_header);
+ mMessageSelectionTitle = root.findViewById(R.id.home_message_selection_title);
+ mComposeContainer = root.findViewById(R.id.message_compose_view_container);
+ mMessageActionBar = root.findViewById(R.id.home_message_action_bar);
+ mMessageActionCopy = root.findViewById(R.id.home_message_action_copy);
+ mMessageActionForward = root.findViewById(R.id.home_message_action_forward);
+ mMessageActionShare = root.findViewById(R.id.home_message_action_share);
+ mMessageActionDetails = root.findViewById(R.id.home_message_action_details);
+ mMessageActionSave = root.findViewById(R.id.home_message_action_save);
+ mMessageActionSend = root.findViewById(R.id.home_message_action_send);
+ mMessageActionDownload = root.findViewById(R.id.home_message_action_download);
+ mMessageActionDelete = root.findViewById(R.id.home_message_action_delete);
+
+ final View done = root.findViewById(R.id.home_message_selection_done);
+ if (done != null) {
+ done.setOnClickListener(v -> dismissMessageSelection());
+ }
+ if (mMessageActionCopy != null) {
+ mMessageActionCopy.setOnClickListener(v -> performSelectedMessageAction(
+ R.id.home_message_action_copy));
+ }
+ if (mMessageActionForward != null) {
+ mMessageActionForward.setOnClickListener(v -> performSelectedMessageAction(
+ R.id.home_message_action_forward));
+ }
+ if (mMessageActionShare != null) {
+ mMessageActionShare.setOnClickListener(v -> performSelectedMessageAction(
+ R.id.home_message_action_share));
+ }
+ if (mMessageActionDetails != null) {
+ mMessageActionDetails.setOnClickListener(v -> performSelectedMessageAction(
+ R.id.home_message_action_details));
+ }
+ if (mMessageActionSave != null) {
+ mMessageActionSave.setOnClickListener(v -> performSelectedMessageAction(
+ R.id.home_message_action_save));
+ }
+ if (mMessageActionSend != null) {
+ mMessageActionSend.setOnClickListener(v -> performSelectedMessageAction(
+ R.id.home_message_action_send));
+ }
+ if (mMessageActionDownload != null) {
+ mMessageActionDownload.setOnClickListener(v -> performSelectedMessageAction(
+ R.id.home_message_action_download));
+ }
+ if (mMessageActionDelete != null) {
+ mMessageActionDelete.setOnClickListener(v -> performSelectedMessageAction(
+ R.id.home_message_action_delete));
+ }
+ }
+
+ private void updateMessageSelectionChrome(final boolean active) {
+ if (mMessageSelectionHeader == null || mMessageActionBar == null) {
+ return;
+ }
+ refreshMessageActionButtons();
+ if (active) {
+ final View outgoingHeader = mConversationHeader;
+ HomeUiMotion.replaceHeaderInstant(outgoingHeader, mMessageSelectionHeader);
+ } else {
+ HomeUiMotion.replaceHeaderInstant(mMessageSelectionHeader, mConversationHeader);
+ mHost.invalidateActionBar();
+ }
+ HomeUiMotion.swapBottomDock(mComposeContainer, mMessageActionBar, active, null);
+ }
+
+ private void refreshMessageActionButtons() {
+ if (mSelectedMessage == null) {
+ return;
+ }
+ final ConversationMessageData data = mSelectedMessage.getData();
+ setMessageActionVisible(mMessageActionCopy, data.getCanCopyMessageToClipboard());
+ final boolean canForward = data.getCanForwardMessage();
+ setMessageActionVisible(mMessageActionForward, canForward);
+ setMessageActionVisible(mMessageActionShare, canForward);
+ setMessageActionVisible(mMessageActionDetails, true);
+ setMessageActionVisible(mMessageActionSave, mSelectedAttachment != null);
+ setMessageActionVisible(mMessageActionSend, data.getShowResendMessage());
+ setMessageActionVisible(mMessageActionDownload, data.getShowDownloadMessage());
+ setMessageActionVisible(mMessageActionDelete, true);
+ }
+
+ private static void setMessageActionVisible(final View action, final boolean visible) {
+ if (action != null) {
+ action.setVisibility(visible ? View.VISIBLE : View.GONE);
+ }
+ }
+
+ private void performSelectedMessageAction(final int actionId) {
+ if (mSelectedMessage == null || getActivity() == null) {
+ return;
+ }
+ final ConversationMessageData data = mSelectedMessage.getData();
+ final String messageId = data.getMessageId();
+ if (actionId == R.id.home_message_action_save) {
+ final SaveAttachmentTask saveAttachmentTask = new SaveAttachmentTask(getActivity());
+ for (final MessagePartData part : data.getAttachments()) {
+ saveAttachmentTask.addAttachmentToSave(part.getContentUri(),
+ part.getContentType());
+ }
+ if (saveAttachmentTask.getAttachmentCount() > 0) {
+ saveAttachmentTask.execute();
+ dismissMessageSelection();
+ }
+ } else if (actionId == R.id.home_message_action_delete) {
+ deleteMessage(messageId);
+ } else if (actionId == R.id.home_message_action_download) {
+ retryDownload(messageId);
+ dismissMessageSelection();
+ } else if (actionId == R.id.home_message_action_send) {
+ retrySend(messageId);
+ dismissMessageSelection();
+ } else if (actionId == R.id.home_message_action_copy) {
+ Assert.isTrue(data.hasText());
+ final ClipboardManager clipboard = (ClipboardManager) getActivity()
+ .getSystemService(Context.CLIPBOARD_SERVICE);
+ clipboard.setPrimaryClip(
+ 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()));
+ dismissMessageSelection();
+ } else if (actionId == R.id.home_message_action_share) {
+ shareSelectedMessage(data);
+ dismissMessageSelection();
+ } else if (actionId == R.id.home_message_action_forward) {
+ final MessageData message = mBinding.getData().createForwardedMessage(data);
+ UIIntents.get().launchForwardMessageActivity(getActivity(), message);
+ dismissMessageSelection();
+ }
+ }
+
+ private void shareSelectedMessage(final ConversationMessageData data) {
+ MessagePartData attachmentToShare = mSelectedAttachment;
+ if (mSelectedAttachment == null && TextUtil.isAllWhitespace(data.getText())) {
+ final List attachments = data.getAttachments();
+ if (!attachments.isEmpty()) {
+ attachmentToShare = attachments.get(0);
+ }
+ }
+ final Intent shareIntent = new Intent();
+ shareIntent.setAction(Intent.ACTION_SEND);
+ if (attachmentToShare == null) {
+ shareIntent.putExtra(Intent.EXTRA_TEXT, data.getText());
+ shareIntent.setType("text/plain");
+ } else {
+ shareIntent.putExtra(Intent.EXTRA_STREAM, attachmentToShare.getContentUri());
+ shareIntent.setType(attachmentToShare.getContentType());
+ }
+ final CharSequence title = getResources().getText(R.string.action_share);
+ startActivity(Intent.createChooser(shareIntent, title));
+ }
+
+ private void placeConversationCall(final View anchorView) {
+ if (!mBinding.isBound() || getActivity() == null) {
+ return;
+ }
+ final String phoneNumber = mBinding.getData().getParticipantPhoneNumber();
+ if (phoneNumber == null) {
+ return;
+ }
+ if (PhoneNumberUtils.isEmergencyNumber(phoneNumber)) {
+ UiUtils.showToast(R.string.disallow_emergency_call);
+ return;
+ }
+ Point centerPoint;
+ if (anchorView != null) {
+ final int[] screenLocation = new int[2];
+ anchorView.getLocationOnScreen(screenLocation);
+ centerPoint = new Point(screenLocation[0] + anchorView.getWidth() / 2,
+ screenLocation[1] + anchorView.getHeight() / 2);
+ } else {
+ final Display display = getActivity().getWindowManager().getDefaultDisplay();
+ centerPoint = new Point(display.getWidth() / 2, display.getHeight() / 2);
+ }
+ UIIntents.get().launchPhoneCallActivity(getActivity(), phoneNumber, centerPoint);
+ }
+
+ private void showConversationOverflowMenu(final View anchor) {
+ if (!mOptionsMenuVisible || getActivity() == null) {
+ return;
+ }
+ final PopupMenu popup = new PopupMenu(requireContext(), anchor);
+ popup.setOnMenuItemClickListener(this::onOptionsItemSelected);
+ onCreateOptionsMenu(popup.getMenu(), requireActivity().getMenuInflater());
+ onPrepareOptionsMenu(popup.getMenu());
+ popup.show();
+ }
+
+ private void bindConversationHeaderAvatar(final ContactIconView avatar) {
+ if (avatar == null || !mBinding.isBound()) {
+ return;
+ }
+ final ConversationData data = mBinding.getData();
+ final ParticipantData other = data.getOtherParticipant();
+ if (other != null) {
+ avatar.setImageResourceUri(AvatarUriUtil.createAvatarUri(other),
+ other.getContactId(), other.getLookupKey(),
+ other.getNormalizedDestination());
+ } else if (!TextUtils.isEmpty(data.getIcon())) {
+ avatar.setImageResourceUri(Uri.parse(data.getIcon()), 0, null, null);
+ }
+ avatar.setImageClickHandlerDisabled(true);
+ }
+
+ private void updateHomeConversationHeader() {
+ if (mSelectedMessage != null) {
+ return;
+ }
+ if (mConversationHeader == null) {
+ return;
+ }
+ bindConversationHeaderContent();
+ }
+
+ private void bindConversationHeaderContent() {
+ if (mConversationHeader == null) {
+ return;
+ }
+ bindConversationHeaderAvatar(mConversationHeaderAvatar);
+
+ final TextView titleView = mConversationHeaderTitle;
+ if (titleView != null) {
final String conversationName = getConversationName();
if (!TextUtils.isEmpty(conversationName)) {
- // RTL : To format conversation title if it happens to be phone numbers.
final BidiFormatter bidiFormatter = BidiFormatter.getInstance();
final String formattedName = bidiFormatter.unicodeWrap(
UiUtils.commaEllipsize(
conversationName,
- conversationNameView.getPaint(),
- conversationNameView.getWidth(),
+ titleView.getPaint(),
+ titleView.getWidth(),
getString(R.string.plus_one),
getString(R.string.plus_n)).toString(),
TextDirectionHeuristicsCompat.LTR);
- conversationNameView.setText(formattedName);
- // In case phone numbers are mixed in the conversation name, we need to vocalize it.
+ titleView.setText(formattedName);
final String vocalizedConversationName =
AccessibilityUtil.getVocalizedPhoneNumber(getResources(), conversationName);
- conversationNameView.setContentDescription(vocalizedConversationName);
- getActivity().setTitle(conversationName);
- } else {
+ titleView.setContentDescription(vocalizedConversationName);
+ if (getActivity() != null) {
+ getActivity().setTitle(conversationName);
+ }
+ } else if (getActivity() != null) {
final String appName = getString(R.string.app_name);
- conversationNameView.setText(appName);
+ titleView.setText(appName);
getActivity().setTitle(appName);
}
-
- // When conversation is showing and media picker is not showing, then hide the action
- // bar only when we are in landscape mode, with IME open.
- if (mHost.isImeOpen() && UiUtils.isLandscapeMode()) {
- actionBar.hide();
- } else {
- actionBar.show();
- }
}
+
+ final boolean hideForIme = mHost.isImeOpen() && UiUtils.isLandscapeMode();
+ mConversationHeader.setVisibility(hideForIme ? View.GONE : View.VISIBLE);
+ }
+
+ public void updateActionBar(final ActionBar actionBar) {
+ if (mComposeMessageView != null && mComposeMessageView.updateActionBar(actionBar)) {
+ if (mConversationHeader != null) {
+ mConversationHeader.setVisibility(View.GONE);
+ }
+ return;
+ }
+ updateActionAndStatusBarColor(actionBar);
+ actionBar.hide();
+ updateHomeConversationHeader();
}
@Override
diff --git a/app/src/main/java/com/android/messaging/ui/conversation/ConversationInputManager.java b/app/src/main/java/com/android/messaging/ui/conversation/ConversationInputManager.java
index 0a5f328..3d35bd1 100644
--- a/app/src/main/java/com/android/messaging/ui/conversation/ConversationInputManager.java
+++ b/app/src/main/java/com/android/messaging/ui/conversation/ConversationInputManager.java
@@ -50,6 +50,13 @@ import java.util.Collection;
* as the other components).
*/
public class ConversationInputManager implements ConversationInput.ConversationInputBase {
+
+ public static final int MEDIA_PICKER_TARGET_DEFAULT = 0;
+ public static final int MEDIA_PICKER_TARGET_CAMERA = 1;
+ public static final int MEDIA_PICKER_TARGET_GALLERY = 2;
+ public static final int MEDIA_PICKER_TARGET_AUDIO = 3;
+ public static final int MEDIA_PICKER_TARGET_CONTACT = 4;
+
/**
* The host component where all input components are contained. This is typically the
* conversation fragment but may be mocked in test code.
@@ -189,6 +196,18 @@ public class ConversationInputManager implements ConversationInput.ConversationI
}
public void showHideMediaPicker(final boolean show, final boolean animate) {
+ showHideMediaPicker(show, animate, MEDIA_PICKER_TARGET_DEFAULT);
+ }
+
+ public boolean isMediaPickerOpen() {
+ return mMediaInput.mShowing;
+ }
+
+ public void showHideMediaPicker(final boolean show, final boolean animate,
+ final int mediaPickerTarget) {
+ if (show) {
+ mMediaInput.setOpenTarget(mediaPickerTarget);
+ }
showHideInternal(mMediaInput, show, animate);
}
@@ -322,6 +341,11 @@ public class ConversationInputManager implements ConversationInput.ConversationI
}
private MediaPicker mMediaPicker;
+ private int mOpenTarget = MEDIA_PICKER_TARGET_DEFAULT;
+
+ void setOpenTarget(final int openTarget) {
+ mOpenTarget = openTarget;
+ }
@Override
public boolean show(boolean animate) {
@@ -393,7 +417,26 @@ public class ConversationInputManager implements ConversationInput.ConversationI
});
}
- mMediaPicker.open(MediaPicker.MEDIA_TYPE_DEFAULT, animate);
+ switch (mOpenTarget) {
+ case MEDIA_PICKER_TARGET_CAMERA:
+ mMediaPicker.openCamera(animate);
+ break;
+ case MEDIA_PICKER_TARGET_GALLERY:
+ mMediaPicker.openGallery(animate);
+ // System picker only ā do not keep the in-app panel marked open.
+ return false;
+ case MEDIA_PICKER_TARGET_AUDIO:
+ mMediaPicker.openAudio(animate);
+ break;
+ case MEDIA_PICKER_TARGET_CONTACT:
+ mMediaPicker.openContact(animate);
+ // System picker only ā do not keep the in-app panel marked open.
+ return false;
+ default:
+ mMediaPicker.open(MediaPicker.MEDIA_TYPE_DEFAULT, animate);
+ break;
+ }
+ mOpenTarget = MEDIA_PICKER_TARGET_DEFAULT;
return isOpen();
}
diff --git a/app/src/main/java/com/android/messaging/ui/conversationlist/AbstractConversationListActivity.java b/app/src/main/java/com/android/messaging/ui/conversationlist/AbstractConversationListActivity.java
index ecc97f7..1011768 100644
--- a/app/src/main/java/com/android/messaging/ui/conversationlist/AbstractConversationListActivity.java
+++ b/app/src/main/java/com/android/messaging/ui/conversationlist/AbstractConversationListActivity.java
@@ -26,6 +26,7 @@ import android.content.res.Resources;
import android.net.Uri;
import android.os.Bundle;
import android.view.View;
+import android.widget.Toast;
import androidx.activity.result.ActivityResultLauncher;
import androidx.activity.result.contract.ActivityResultContracts;
@@ -42,8 +43,10 @@ import com.android.messaging.datamodel.data.ConversationListItemData;
import com.android.messaging.ui.BugleActionBarActivity;
import com.android.messaging.ui.SnackBar;
import com.android.messaging.ui.SnackBarInteraction;
+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.conversationlist.ConversationListFragment.ConversationListFragmentHost;
import com.android.messaging.ui.conversationlist.MultiSelectActionModeCallback.SelectedConversation;
import com.android.messaging.util.ChangeDefaultSmsAppHelper;
@@ -61,6 +64,10 @@ import java.util.List;
public abstract class AbstractConversationListActivity extends BugleActionBarActivity
implements ConversationListFragmentHost, MultiSelectActionModeCallback.Listener {
+ private static final UpdateDestinationBlockedAction.UpdateDestinationBlockedActionListener
+ NOOP_BLOCK_LISTENER =
+ (action, success, block, destination) -> { };
+
protected ConversationListFragment mConversationListFragment;
private ChangeDefaultSmsAppHelper mChangeDefaultSmsAppHelper;
@@ -91,21 +98,27 @@ public abstract class AbstractConversationListActivity extends BugleActionBarAct
}
protected void startMultiSelectActionMode() {
- startActionMode(new MultiSelectActionModeCallback(this, useHomeSelectionChrome()));
- mConversationListFragment.dismissFab();
- if (useHomeSelectionChrome()) {
- mConversationListFragment.setHomeSelectionMode(true);
+ final boolean homeChrome = useHomeSelectionChrome();
+ if (homeChrome) {
hideHomeContextualActionBar();
+ mConversationListFragment.dismissFab(true);
+ }
+ startActionMode(new MultiSelectActionModeCallback(this, homeChrome));
+ if (homeChrome) {
+ mConversationListFragment.setHomeSelectionMode(true);
+ } else {
+ mConversationListFragment.dismissFab();
}
}
public void exitMultiSelectState() {
+ dismissActionMode();
if (useHomeSelectionChrome()) {
mConversationListFragment.setHomeSelectionMode(false);
+ } else {
+ mConversationListFragment.updateUi();
}
mConversationListFragment.showFab();
- dismissActionMode();
- mConversationListFragment.updateUi();
}
protected void hideHomeContextualActionBar() {
@@ -121,7 +134,28 @@ public abstract class AbstractConversationListActivity extends BugleActionBarAct
}
final MultiSelectActionModeCallback callback =
(MultiSelectActionModeCallback) getActionModeCallback();
- onActionBarDelete(callback.getSelectedConversations());
+ final Collection conversations =
+ callback.getSelectedConversations();
+ if (conversations.isEmpty()) {
+ return;
+ }
+ if (useHomeSelectionChrome()) {
+ showHomeDeleteAlert(conversations);
+ } else {
+ onActionBarDelete(conversations);
+ }
+ }
+
+ public void markSelectedConversationsUnread() {
+ if (!isInConversationListSelectMode()) {
+ return;
+ }
+ final MultiSelectActionModeCallback callback =
+ (MultiSelectActionModeCallback) getActionModeCallback();
+ for (final MultiSelectActionModeCallback.SelectedConversation conversation
+ : callback.getSelectedConversations()) {
+ MarkAsUnreadAction.markAsUnread(conversation.conversationId);
+ }
}
protected boolean isInConversationListSelectMode() {
@@ -133,23 +167,97 @@ public abstract class AbstractConversationListActivity extends BugleActionBarAct
return isInConversationListSelectMode();
}
+ private void showHomeDeleteAlert(
+ final Collection conversations) {
+ if (!ensureDefaultSmsAppForDelete()) {
+ return;
+ }
+ HomeDeleteAlertDialog.show(this, conversations.size(),
+ new HomeDeleteAlertDialog.Listener() {
+ @Override
+ public void onDelete() {
+ performDeleteConversations(conversations);
+ }
+
+ @Override
+ public void onDeleteAndRegisterSpam() {
+ performDeleteAndRegisterSpam(conversations);
+ }
+ });
+ }
+
+ private boolean ensureDefaultSmsAppForDelete() {
+ if (PhoneUtils.getDefault().isDefaultSmsApp()) {
+ return true;
+ }
+ // TODO: figure out a good way to combine this with the implementation in
+ // ConversationFragment doing similar things
+ final Activity activity = this;
+ UiUtils.showSnackBarWithCustomAction(this,
+ getWindow().getDecorView().getRootView(),
+ getString(R.string.requires_default_sms_app),
+ SnackBar.Action.createCustomAction(() -> {
+ final Intent intent =
+ UIIntents.get().getChangeDefaultSmsAppIntent(activity);
+ mLauncher.launch(intent);
+ },
+ getString(R.string.requires_default_sms_change_button)),
+ null /* interactions */,
+ null /* placement */);
+ return false;
+ }
+
+ private void performDeleteConversations(
+ final Collection conversations) {
+ performDeleteConversations(conversations, false /* reportSpam */);
+ }
+
+ private void performDeleteConversations(
+ final Collection conversations, final boolean reportSpam) {
+ final int count = conversations.size();
+ for (final SelectedConversation conversation : conversations) {
+ DeleteConversationAction.deleteConversation(
+ conversation.conversationId,
+ conversation.timestamp);
+ }
+ showDeleteCompletedToast(count, reportSpam);
+ exitMultiSelectState();
+ }
+
+ private void showDeleteCompletedToast(final int count, final boolean reportSpam) {
+ if (!useHomeSelectionChrome() || count <= 0) {
+ return;
+ }
+ final String message;
+ if (reportSpam) {
+ message = count == 1
+ ? getString(R.string.home_delete_spam_toast_one)
+ : getString(R.string.home_delete_spam_toast_many, count);
+ } else {
+ message = count == 1
+ ? getString(R.string.home_delete_toast_one)
+ : getString(R.string.home_delete_toast_many, count);
+ }
+ Toast.makeText(this, message, Toast.LENGTH_SHORT).show();
+ }
+
+ private void performDeleteAndRegisterSpam(
+ final Collection conversations) {
+ for (final SelectedConversation conversation : conversations) {
+ if (!conversation.isGroup
+ && conversation.otherParticipantNormalizedDestination != null) {
+ UpdateDestinationBlockedAction.updateDestinationBlocked(
+ conversation.otherParticipantNormalizedDestination, true,
+ conversation.conversationId,
+ NOOP_BLOCK_LISTENER);
+ }
+ }
+ performDeleteConversations(conversations, true /* registerSpam */);
+ }
+
@Override
public void onActionBarDelete(final Collection conversations) {
- if (!PhoneUtils.getDefault().isDefaultSmsApp()) {
- // TODO: figure out a good way to combine this with the implementation in
- // ConversationFragment doing similar things
- final Activity activity = this;
- UiUtils.showSnackBarWithCustomAction(this,
- getWindow().getDecorView().getRootView(),
- getString(R.string.requires_default_sms_app),
- SnackBar.Action.createCustomAction(() -> {
- final Intent intent =
- UIIntents.get().getChangeDefaultSmsAppIntent(activity);
- mLauncher.launch(intent);
- },
- getString(R.string.requires_default_sms_change_button)),
- null /* interactions */,
- null /* placement */);
+ if (!ensureDefaultSmsAppForDelete()) {
return;
}
@@ -158,14 +266,7 @@ public abstract class AbstractConversationListActivity extends BugleActionBarAct
R.plurals.delete_conversations_confirmation_dialog_title,
conversations.size()))
.setPositiveButton(R.string.delete_conversation_confirmation_button,
- (dialog, button) -> {
- for (final SelectedConversation conversation : conversations) {
- DeleteConversationAction.deleteConversation(
- conversation.conversationId,
- conversation.timestamp);
- }
- exitMultiSelectState();
- })
+ (dialog, button) -> performDeleteConversations(conversations))
.setNegativeButton(R.string.delete_conversation_decline_button, null)
.show();
}
@@ -256,7 +357,7 @@ public abstract class AbstractConversationListActivity extends BugleActionBarAct
@Override
public void onSelectionCountChanged(final int count) {
if (useHomeSelectionChrome()) {
- mConversationListFragment.updateHomeSelectionDeleteEnabled(count);
+ mConversationListFragment.updateHomeSelectionBar(count);
hideHomeContextualActionBar();
}
}
diff --git a/app/src/main/java/com/android/messaging/ui/conversationlist/ConversationListAdapter.java b/app/src/main/java/com/android/messaging/ui/conversationlist/ConversationListAdapter.java
index 10d9ba6..c8d4a93 100644
--- a/app/src/main/java/com/android/messaging/ui/conversationlist/ConversationListAdapter.java
+++ b/app/src/main/java/com/android/messaging/ui/conversationlist/ConversationListAdapter.java
@@ -53,6 +53,7 @@ public class ConversationListAdapter
private static final int VIEW_TYPE_PINNED_HEADER = 1;
private static final int VIEW_TYPE_CONVERSATION = 2;
private static final long PINNED_HEADER_ITEM_ID = -9001L;
+ static final Object PAYLOAD_SELECTION_CHROME = new Object();
private final ConversationListItemView.HostInterface mClivHostInterface;
private final ConversationListItemData mFilterProbe = new ConversationListItemData();
@@ -112,6 +113,13 @@ public class ConversationListAdapter
return mFilterMode;
}
+ public void notifySelectionChromeChanged() {
+ final int count = getItemCount();
+ if (count > 0) {
+ notifyItemRangeChanged(0, count, PAYLOAD_SELECTION_CHROME);
+ }
+ }
+
public void refreshPinOrdering() {
refreshPinOrderingAnimated();
}
@@ -323,6 +331,22 @@ public class ConversationListAdapter
return createConversationViewHolder(parent);
}
+ @Override
+ public void onBindViewHolder(@NonNull final RecyclerView.ViewHolder holder,
+ final int position, @NonNull final List