Messaging: Looply conversation profile, search, and in-app gallery
Replace People & Options with the home profile and search screens, and attach media from an in-conversation gallery sheet that avoids the system picker flash. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -44,6 +44,8 @@
|
|||||||
<uses-permission android:name="android.permission.DOWNLOAD_WITHOUT_NOTIFICATION" />
|
<uses-permission android:name="android.permission.DOWNLOAD_WITHOUT_NOTIFICATION" />
|
||||||
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
|
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
|
||||||
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
||||||
|
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES" />
|
||||||
|
<uses-permission android:name="android.permission.READ_MEDIA_VIDEO" />
|
||||||
|
|
||||||
<!-- Optional features -->
|
<!-- Optional features -->
|
||||||
<uses-feature
|
<uses-feature
|
||||||
@@ -155,6 +157,39 @@
|
|||||||
android:screenOrientation="user"
|
android:screenOrientation="user"
|
||||||
android:theme="@style/BugleTheme" />
|
android:theme="@style/BugleTheme" />
|
||||||
|
|
||||||
|
<activity
|
||||||
|
android:name=".ui.home.HomeSpamActivity"
|
||||||
|
android:configChanges="orientation|screenSize|keyboardHidden"
|
||||||
|
android:label="@string/home_spam_title"
|
||||||
|
android:parentActivityName="com.android.messaging.ui.conversationlist.ConversationListActivity"
|
||||||
|
android:screenOrientation="user"
|
||||||
|
android:theme="@style/Home.SubScreen" />
|
||||||
|
|
||||||
|
<activity
|
||||||
|
android:name=".ui.home.HomeSearchActivity"
|
||||||
|
android:configChanges="orientation|screenSize|keyboardHidden"
|
||||||
|
android:label="@string/home_search_field_hint"
|
||||||
|
android:parentActivityName="com.android.messaging.ui.conversationlist.ConversationListActivity"
|
||||||
|
android:screenOrientation="user"
|
||||||
|
android:theme="@style/Home.SubScreen"
|
||||||
|
android:windowSoftInputMode="adjustResize|stateVisible" />
|
||||||
|
|
||||||
|
<activity
|
||||||
|
android:name=".ui.home.HomeComposeActivity"
|
||||||
|
android:configChanges="orientation|screenSize|keyboardHidden"
|
||||||
|
android:label="@string/home_compose_title"
|
||||||
|
android:parentActivityName="com.android.messaging.ui.conversationlist.ConversationListActivity"
|
||||||
|
android:theme="@style/Home.BottomSheet"
|
||||||
|
android:windowSoftInputMode="adjustResize|stateHidden" />
|
||||||
|
|
||||||
|
<activity
|
||||||
|
android:name=".ui.home.HomeGroupNameActivity"
|
||||||
|
android:configChanges="orientation|screenSize|keyboardHidden"
|
||||||
|
android:label="@string/home_group_name_title"
|
||||||
|
android:parentActivityName="com.android.messaging.ui.conversationlist.ConversationListActivity"
|
||||||
|
android:theme="@style/Home.BottomSheet"
|
||||||
|
android:windowSoftInputMode="adjustResize|stateHidden" />
|
||||||
|
|
||||||
<!-- Full-screen photo viewer -->
|
<!-- Full-screen photo viewer -->
|
||||||
<activity
|
<activity
|
||||||
android:name=".ui.photoviewer.BuglePhotoViewActivity"
|
android:name=".ui.photoviewer.BuglePhotoViewActivity"
|
||||||
@@ -223,14 +258,24 @@
|
|||||||
</intent-filter>
|
</intent-filter>
|
||||||
</activity>
|
</activity>
|
||||||
|
|
||||||
<!-- People & Options -->
|
<!-- Conversation profile -->
|
||||||
<activity
|
<activity
|
||||||
android:name=".ui.conversationsettings.PeopleAndOptionsActivity"
|
android:name=".ui.home.HomeProfileActivity"
|
||||||
android:configChanges="orientation|screenSize|keyboardHidden"
|
android:configChanges="orientation|screenSize|keyboardHidden"
|
||||||
android:label="@string/people_and_options_activity_title"
|
android:label="@string/home_profile_title"
|
||||||
android:parentActivityName="com.android.messaging.ui.conversation.ConversationActivity"
|
android:parentActivityName="com.android.messaging.ui.conversation.ConversationActivity"
|
||||||
android:screenOrientation="user"
|
android:screenOrientation="user"
|
||||||
android:theme="@style/BugleTheme" />
|
android:theme="@style/Home.SubScreen" />
|
||||||
|
|
||||||
|
<!-- Conversation search -->
|
||||||
|
<activity
|
||||||
|
android:name=".ui.home.HomeConversationSearchActivity"
|
||||||
|
android:configChanges="orientation|screenSize|keyboardHidden"
|
||||||
|
android:label="@string/home_conversation_search_hint"
|
||||||
|
android:parentActivityName="com.android.messaging.ui.conversation.ConversationActivity"
|
||||||
|
android:screenOrientation="user"
|
||||||
|
android:theme="@style/Home.SubScreen"
|
||||||
|
android:windowSoftInputMode="adjustResize|stateVisible" />
|
||||||
|
|
||||||
<!-- License -->
|
<!-- License -->
|
||||||
<activity
|
<activity
|
||||||
|
|||||||
@@ -28,6 +28,8 @@ import com.android.messaging.datamodel.action.ActionService;
|
|||||||
import com.android.messaging.datamodel.action.BackgroundWorker;
|
import com.android.messaging.datamodel.action.BackgroundWorker;
|
||||||
import com.android.messaging.datamodel.data.BlockedParticipantsData;
|
import com.android.messaging.datamodel.data.BlockedParticipantsData;
|
||||||
import com.android.messaging.datamodel.data.BlockedParticipantsData.BlockedParticipantsDataListener;
|
import com.android.messaging.datamodel.data.BlockedParticipantsData.BlockedParticipantsDataListener;
|
||||||
|
import com.android.messaging.datamodel.data.ConversationSearchData;
|
||||||
|
import com.android.messaging.datamodel.data.ConversationSearchData.ConversationSearchDataListener;
|
||||||
import com.android.messaging.datamodel.data.ContactListItemData;
|
import com.android.messaging.datamodel.data.ContactListItemData;
|
||||||
import com.android.messaging.datamodel.data.ContactPickerData;
|
import com.android.messaging.datamodel.data.ContactPickerData;
|
||||||
import com.android.messaging.datamodel.data.ContactPickerData.ContactPickerDataListener;
|
import com.android.messaging.datamodel.data.ContactPickerData.ContactPickerDataListener;
|
||||||
@@ -42,9 +44,8 @@ import com.android.messaging.datamodel.data.MediaPickerData;
|
|||||||
import com.android.messaging.datamodel.data.MessagePartData;
|
import com.android.messaging.datamodel.data.MessagePartData;
|
||||||
import com.android.messaging.datamodel.data.ParticipantData;
|
import com.android.messaging.datamodel.data.ParticipantData;
|
||||||
import com.android.messaging.datamodel.data.ParticipantListItemData;
|
import com.android.messaging.datamodel.data.ParticipantListItemData;
|
||||||
import com.android.messaging.datamodel.data.PeopleAndOptionsData;
|
import com.android.messaging.datamodel.data.ConversationProfileData;
|
||||||
import com.android.messaging.datamodel.data.PeopleAndOptionsData.PeopleAndOptionsDataListener;
|
import com.android.messaging.datamodel.data.ConversationProfileData.ConversationProfileDataListener;
|
||||||
import com.android.messaging.datamodel.data.PeopleOptionsItemData;
|
|
||||||
import com.android.messaging.datamodel.data.SettingsData;
|
import com.android.messaging.datamodel.data.SettingsData;
|
||||||
import com.android.messaging.datamodel.data.SettingsData.SettingsDataListener;
|
import com.android.messaging.datamodel.data.SettingsData.SettingsDataListener;
|
||||||
import com.android.messaging.datamodel.data.SubscriptionListData;
|
import com.android.messaging.datamodel.data.SubscriptionListData;
|
||||||
@@ -83,10 +84,9 @@ public abstract class DataModel {
|
|||||||
public abstract LaunchConversationData createLaunchConversationData(
|
public abstract LaunchConversationData createLaunchConversationData(
|
||||||
LaunchConversationDataListener listener);
|
LaunchConversationDataListener listener);
|
||||||
|
|
||||||
public abstract PeopleOptionsItemData createPeopleOptionsItemData(final Context context);
|
public abstract ConversationProfileData createConversationProfileData(
|
||||||
|
final String conversationId, final Context context,
|
||||||
public abstract PeopleAndOptionsData createPeopleAndOptionsData(final String conversationId,
|
final ConversationProfileDataListener listener);
|
||||||
final Context context, final PeopleAndOptionsDataListener listener);
|
|
||||||
|
|
||||||
public abstract VCardContactItemData createVCardContactItemData(final Context context,
|
public abstract VCardContactItemData createVCardContactItemData(final Context context,
|
||||||
final MessagePartData data);
|
final MessagePartData data);
|
||||||
@@ -100,6 +100,9 @@ public abstract class DataModel {
|
|||||||
public abstract BlockedParticipantsData createBlockedParticipantsData(Context context,
|
public abstract BlockedParticipantsData createBlockedParticipantsData(Context context,
|
||||||
BlockedParticipantsDataListener listener);
|
BlockedParticipantsDataListener listener);
|
||||||
|
|
||||||
|
public abstract ConversationSearchData createConversationSearchData(Context context,
|
||||||
|
String conversationId, ConversationSearchDataListener listener);
|
||||||
|
|
||||||
public abstract SubscriptionListData createSubscriptonListData(Context context);
|
public abstract SubscriptionListData createSubscriptonListData(Context context);
|
||||||
|
|
||||||
public abstract SettingsData createSettingsData(Context context, SettingsDataListener listener);
|
public abstract SettingsData createSettingsData(Context context, SettingsDataListener listener);
|
||||||
|
|||||||
@@ -28,6 +28,8 @@ import com.android.messaging.datamodel.action.FixupMessageStatusOnStartupAction;
|
|||||||
import com.android.messaging.datamodel.action.ProcessPendingMessagesAction;
|
import com.android.messaging.datamodel.action.ProcessPendingMessagesAction;
|
||||||
import com.android.messaging.datamodel.data.BlockedParticipantsData;
|
import com.android.messaging.datamodel.data.BlockedParticipantsData;
|
||||||
import com.android.messaging.datamodel.data.BlockedParticipantsData.BlockedParticipantsDataListener;
|
import com.android.messaging.datamodel.data.BlockedParticipantsData.BlockedParticipantsDataListener;
|
||||||
|
import com.android.messaging.datamodel.data.ConversationSearchData;
|
||||||
|
import com.android.messaging.datamodel.data.ConversationSearchData.ConversationSearchDataListener;
|
||||||
import com.android.messaging.datamodel.data.ContactListItemData;
|
import com.android.messaging.datamodel.data.ContactListItemData;
|
||||||
import com.android.messaging.datamodel.data.ContactPickerData;
|
import com.android.messaging.datamodel.data.ContactPickerData;
|
||||||
import com.android.messaging.datamodel.data.ContactPickerData.ContactPickerDataListener;
|
import com.android.messaging.datamodel.data.ContactPickerData.ContactPickerDataListener;
|
||||||
@@ -42,9 +44,8 @@ import com.android.messaging.datamodel.data.MediaPickerData;
|
|||||||
import com.android.messaging.datamodel.data.MessagePartData;
|
import com.android.messaging.datamodel.data.MessagePartData;
|
||||||
import com.android.messaging.datamodel.data.ParticipantData;
|
import com.android.messaging.datamodel.data.ParticipantData;
|
||||||
import com.android.messaging.datamodel.data.ParticipantListItemData;
|
import com.android.messaging.datamodel.data.ParticipantListItemData;
|
||||||
import com.android.messaging.datamodel.data.PeopleAndOptionsData;
|
import com.android.messaging.datamodel.data.ConversationProfileData;
|
||||||
import com.android.messaging.datamodel.data.PeopleAndOptionsData.PeopleAndOptionsDataListener;
|
import com.android.messaging.datamodel.data.ConversationProfileData.ConversationProfileDataListener;
|
||||||
import com.android.messaging.datamodel.data.PeopleOptionsItemData;
|
|
||||||
import com.android.messaging.datamodel.data.SettingsData;
|
import com.android.messaging.datamodel.data.SettingsData;
|
||||||
import com.android.messaging.datamodel.data.SettingsData.SettingsDataListener;
|
import com.android.messaging.datamodel.data.SettingsData.SettingsDataListener;
|
||||||
import com.android.messaging.datamodel.data.SubscriptionListData;
|
import com.android.messaging.datamodel.data.SubscriptionListData;
|
||||||
@@ -107,6 +108,12 @@ public class DataModelImpl extends DataModel {
|
|||||||
return new BlockedParticipantsData(context, listener);
|
return new BlockedParticipantsData(context, listener);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ConversationSearchData createConversationSearchData(final Context context,
|
||||||
|
final String conversationId, final ConversationSearchDataListener listener) {
|
||||||
|
return new ConversationSearchData(context, conversationId, listener);
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public MediaPickerData createMediaPickerData(final Context context) {
|
public MediaPickerData createMediaPickerData(final Context context) {
|
||||||
return new MediaPickerData(context);
|
return new MediaPickerData(context);
|
||||||
@@ -119,14 +126,9 @@ public class DataModelImpl extends DataModel {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public PeopleOptionsItemData createPeopleOptionsItemData(final Context context) {
|
public ConversationProfileData createConversationProfileData(final String conversationId,
|
||||||
return new PeopleOptionsItemData(context);
|
final Context context, final ConversationProfileDataListener listener) {
|
||||||
}
|
return new ConversationProfileData(conversationId, context, listener);
|
||||||
|
|
||||||
@Override
|
|
||||||
public PeopleAndOptionsData createPeopleAndOptionsData(final String conversationId,
|
|
||||||
final Context context, final PeopleAndOptionsDataListener listener) {
|
|
||||||
return new PeopleAndOptionsData(conversationId, context, listener);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|||||||
@@ -70,6 +70,11 @@ public class MessagingContentProvider extends ContentProvider {
|
|||||||
public static final Uri CONVERSATION_MESSAGES_URI = Uri.parse(CONTENT_AUTHORITY +
|
public static final Uri CONVERSATION_MESSAGES_URI = Uri.parse(CONTENT_AUTHORITY +
|
||||||
MESSAGES_QUERY + "/conversation");
|
MESSAGES_QUERY + "/conversation");
|
||||||
|
|
||||||
|
public static final Uri CONVERSATION_MESSAGES_SEARCH_URI = Uri.parse(CONTENT_AUTHORITY +
|
||||||
|
MESSAGES_QUERY + "/search");
|
||||||
|
|
||||||
|
private static final String SEARCH_QUERY_PARAMETER = "q";
|
||||||
|
|
||||||
// Conversation participants query
|
// Conversation participants query
|
||||||
private static final String PARTICIPANTS_QUERY = "participants";
|
private static final String PARTICIPANTS_QUERY = "participants";
|
||||||
|
|
||||||
@@ -146,6 +151,7 @@ public class MessagingContentProvider extends ContentProvider {
|
|||||||
|
|
||||||
private static final int CONVERSATION_QUERY_CODE = 20;
|
private static final int CONVERSATION_QUERY_CODE = 20;
|
||||||
private static final int CONVERSATION_MESSAGES_QUERY_CODE = 30;
|
private static final int CONVERSATION_MESSAGES_QUERY_CODE = 30;
|
||||||
|
private static final int CONVERSATION_MESSAGES_SEARCH_QUERY_CODE = 35;
|
||||||
private static final int CONVERSATION_PARTICIPANTS_QUERY_CODE = 40;
|
private static final int CONVERSATION_PARTICIPANTS_QUERY_CODE = 40;
|
||||||
private static final int CONVERSATION_IMAGES_QUERY_CODE = 50;
|
private static final int CONVERSATION_IMAGES_QUERY_CODE = 50;
|
||||||
private static final int DRAFT_IMAGES_QUERY_CODE = 60;
|
private static final int DRAFT_IMAGES_QUERY_CODE = 60;
|
||||||
@@ -158,6 +164,8 @@ public class MessagingContentProvider extends ContentProvider {
|
|||||||
sURIMatcher.addURI(AUTHORITY, CONVERSATIONS_QUERY + "/*", CONVERSATION_QUERY_CODE);
|
sURIMatcher.addURI(AUTHORITY, CONVERSATIONS_QUERY + "/*", CONVERSATION_QUERY_CODE);
|
||||||
sURIMatcher.addURI(AUTHORITY, MESSAGES_QUERY + "/conversation/*",
|
sURIMatcher.addURI(AUTHORITY, MESSAGES_QUERY + "/conversation/*",
|
||||||
CONVERSATION_MESSAGES_QUERY_CODE);
|
CONVERSATION_MESSAGES_QUERY_CODE);
|
||||||
|
sURIMatcher.addURI(AUTHORITY, MESSAGES_QUERY + "/search/*",
|
||||||
|
CONVERSATION_MESSAGES_SEARCH_QUERY_CODE);
|
||||||
sURIMatcher.addURI(AUTHORITY, PARTICIPANTS_QUERY + "/conversation/*",
|
sURIMatcher.addURI(AUTHORITY, PARTICIPANTS_QUERY + "/conversation/*",
|
||||||
CONVERSATION_PARTICIPANTS_QUERY_CODE);
|
CONVERSATION_PARTICIPANTS_QUERY_CODE);
|
||||||
sURIMatcher.addURI(AUTHORITY, PARTICIPANTS_QUERY, PARTICIPANTS_QUERY_CODE);
|
sURIMatcher.addURI(AUTHORITY, PARTICIPANTS_QUERY, PARTICIPANTS_QUERY_CODE);
|
||||||
@@ -176,6 +184,17 @@ public class MessagingContentProvider extends ContentProvider {
|
|||||||
return builder.build();
|
return builder.build();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Build a uri for the messages of a conversation whose text matches {@code searchText}.
|
||||||
|
*/
|
||||||
|
public static Uri buildConversationMessagesSearchUri(final String conversationId,
|
||||||
|
final String searchText) {
|
||||||
|
return CONVERSATION_MESSAGES_SEARCH_URI.buildUpon()
|
||||||
|
.appendPath(conversationId)
|
||||||
|
.appendQueryParameter(SEARCH_QUERY_PARAMETER, searchText)
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
|
||||||
public static void notifyMessagesChanged(final String conversationId) {
|
public static void notifyMessagesChanged(final String conversationId) {
|
||||||
final Uri uri = buildConversationMessagesUri(conversationId);
|
final Uri uri = buildConversationMessagesUri(conversationId);
|
||||||
final Context context = Factory.get().getApplicationContext();
|
final Context context = Factory.get().getApplicationContext();
|
||||||
@@ -319,6 +338,19 @@ public class MessagingContentProvider extends ContentProvider {
|
|||||||
} else {
|
} else {
|
||||||
throw new IllegalArgumentException("Malformed URI " + uri);
|
throw new IllegalArgumentException("Malformed URI " + uri);
|
||||||
}
|
}
|
||||||
|
case CONVERSATION_MESSAGES_SEARCH_QUERY_CODE:
|
||||||
|
if (uri.getPathSegments().size() == 3) {
|
||||||
|
// Like the query above this is a raw query, so selection/sorting is invalid.
|
||||||
|
if (selection == null && selectionArgs == null && sortOrder == null) {
|
||||||
|
return queryConversationMessagesSearch(uri.getPathSegments().get(2),
|
||||||
|
uri.getQueryParameter(SEARCH_QUERY_PARAMETER));
|
||||||
|
} else {
|
||||||
|
throw new IllegalArgumentException(
|
||||||
|
"Cannot set selection or sort order with this query");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
throw new IllegalArgumentException("Malformed URI " + uri);
|
||||||
|
}
|
||||||
case CONVERSATION_IMAGES_QUERY_CODE:
|
case CONVERSATION_IMAGES_QUERY_CODE:
|
||||||
queryBuilder.setTables(ConversationImagePartsView.getViewName());
|
queryBuilder.setTables(ConversationImagePartsView.getViewName());
|
||||||
if (uri.getPathSegments().size() == 2) {
|
if (uri.getPathSegments().size() == 2) {
|
||||||
@@ -366,6 +398,24 @@ public class MessagingContentProvider extends ContentProvider {
|
|||||||
return cursor;
|
return cursor;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private Cursor queryConversationMessagesSearch(final String conversationId,
|
||||||
|
final String searchText) {
|
||||||
|
final String[] queryArgs = { conversationId, "%" + escapeLikePattern(searchText) + "%" };
|
||||||
|
final Cursor cursor = getDatabaseWrapper().rawQuery(
|
||||||
|
ConversationMessageData.getConversationMessagesSearchQuerySql(), queryArgs);
|
||||||
|
// Follow the plain message uri so that incoming messages refresh the results as well.
|
||||||
|
cursor.setNotificationUri(getContext().getContentResolver(),
|
||||||
|
buildConversationMessagesUri(conversationId));
|
||||||
|
return cursor;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String escapeLikePattern(final String text) {
|
||||||
|
if (TextUtils.isEmpty(text)) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
return text.replace("\\", "\\\\").replace("%", "\\%").replace("_", "\\_");
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String getType(@NonNull final Uri uri) {
|
public String getType(@NonNull final Uri uri) {
|
||||||
final StringBuilder sb = new
|
final StringBuilder sb = new
|
||||||
|
|||||||
@@ -0,0 +1,146 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2015 The Android Open Source Project
|
||||||
|
* Copyright (C) 2024-2025 The LineageOS Project
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.android.messaging.datamodel.action;
|
||||||
|
|
||||||
|
import android.content.ContentValues;
|
||||||
|
import android.database.Cursor;
|
||||||
|
import android.database.sqlite.SQLiteDoneException;
|
||||||
|
import android.database.sqlite.SQLiteStatement;
|
||||||
|
import android.net.Uri;
|
||||||
|
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;
|
||||||
|
import com.android.messaging.datamodel.DatabaseHelper.MessageColumns;
|
||||||
|
import com.android.messaging.datamodel.DatabaseWrapper;
|
||||||
|
import com.android.messaging.datamodel.MessagingContentProvider;
|
||||||
|
import com.android.messaging.Factory;
|
||||||
|
import com.android.messaging.util.Assert;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Marks the latest message in a conversation as unread (iOS-style thread unread).
|
||||||
|
*/
|
||||||
|
public class MarkAsUnreadAction extends Action implements Parcelable {
|
||||||
|
private static final String KEY_CONVERSATION_ID = "conversation_id";
|
||||||
|
|
||||||
|
public static void markAsUnread(final String conversationId) {
|
||||||
|
final MarkAsUnreadAction action = new MarkAsUnreadAction(conversationId);
|
||||||
|
action.start();
|
||||||
|
}
|
||||||
|
|
||||||
|
private MarkAsUnreadAction(final String conversationId) {
|
||||||
|
actionParameters.putString(KEY_CONVERSATION_ID, conversationId);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected Object executeAction() {
|
||||||
|
final String conversationId = actionParameters.getString(KEY_CONVERSATION_ID);
|
||||||
|
if (TextUtils.isEmpty(conversationId)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
final DatabaseWrapper db = DataModel.get().getDatabase();
|
||||||
|
db.beginTransaction();
|
||||||
|
try {
|
||||||
|
String latestMessageId;
|
||||||
|
try {
|
||||||
|
final SQLiteStatement latestMessageIdSql =
|
||||||
|
BugleDatabaseOperations.getQueryMessagesLatestMessageStatement(
|
||||||
|
db, conversationId);
|
||||||
|
latestMessageId = latestMessageIdSql.simpleQueryForString();
|
||||||
|
} catch (final SQLiteDoneException e) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (TextUtils.isEmpty(latestMessageId)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
final ContentValues values = new ContentValues();
|
||||||
|
values.put(MessageColumns.READ, 0);
|
||||||
|
values.put(MessageColumns.SEEN, 0);
|
||||||
|
|
||||||
|
final int count = db.update(DatabaseHelper.MESSAGES_TABLE, values,
|
||||||
|
MessageColumns._ID + "=? AND (" + MessageColumns.READ + " != 0 OR "
|
||||||
|
+ MessageColumns.SEEN + " != 0)",
|
||||||
|
new String[] { latestMessageId });
|
||||||
|
if (count > 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<MarkAsUnreadAction> 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
+95
@@ -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<RenameConversationAction> 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -20,6 +20,7 @@ package com.android.messaging.datamodel.data;
|
|||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.database.Cursor;
|
import android.database.Cursor;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
|
import android.text.TextUtils;
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
import androidx.loader.app.LoaderManager;
|
import androidx.loader.app.LoaderManager;
|
||||||
@@ -46,6 +47,13 @@ public class ContactPickerData extends BindableData implements
|
|||||||
void onAllContactsCursorUpdated(Cursor data);
|
void onAllContactsCursorUpdated(Cursor data);
|
||||||
void onFrequentContactsCursorUpdated(Cursor data);
|
void onFrequentContactsCursorUpdated(Cursor data);
|
||||||
void onContactCustomColorLoaded(ContactPickerData 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";
|
private static final String BINDING_ID = "bindingId";
|
||||||
@@ -53,6 +61,7 @@ public class ContactPickerData extends BindableData implements
|
|||||||
private LoaderManager mLoaderManager;
|
private LoaderManager mLoaderManager;
|
||||||
private ContactPickerDataListener mListener;
|
private ContactPickerDataListener mListener;
|
||||||
private final FrequentContactsCursorBuilder mFrequentContactsCursorBuilder;
|
private final FrequentContactsCursorBuilder mFrequentContactsCursorBuilder;
|
||||||
|
private String mSearchText;
|
||||||
|
|
||||||
public ContactPickerData(final Context context, final ContactPickerDataListener listener) {
|
public ContactPickerData(final Context context, final ContactPickerDataListener listener) {
|
||||||
mListener = 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 ALL_CONTACTS_LOADER = 1;
|
||||||
private static final int FREQUENT_CONTACTS_LOADER = 2;
|
private static final int FREQUENT_CONTACTS_LOADER = 2;
|
||||||
private static final int PARTICIPANT_LOADER = 3;
|
private static final int PARTICIPANT_LOADER = 3;
|
||||||
|
private static final int FILTERED_CONTACTS_LOADER = 4;
|
||||||
|
|
||||||
@NonNull
|
@NonNull
|
||||||
@Override
|
@Override
|
||||||
@@ -81,6 +91,9 @@ public class ContactPickerData extends BindableData implements
|
|||||||
return new BoundCursorLoader(bindingId, mContext,
|
return new BoundCursorLoader(bindingId, mContext,
|
||||||
MessagingContentProvider.PARTICIPANTS_URI,
|
MessagingContentProvider.PARTICIPANTS_URI,
|
||||||
ParticipantData.ParticipantsQuery.PROJECTION, null, null, null);
|
ParticipantData.ParticipantsQuery.PROJECTION, null, null, null);
|
||||||
|
case FILTERED_CONTACTS_LOADER:
|
||||||
|
return ContactUtil.filterDestination(mContext, mSearchText)
|
||||||
|
.createBoundCursorLoader(bindingId);
|
||||||
default:
|
default:
|
||||||
Assert.fail("Unknown loader id for contact picker!");
|
Assert.fail("Unknown loader id for contact picker!");
|
||||||
break;
|
break;
|
||||||
@@ -108,12 +121,16 @@ public class ContactPickerData extends BindableData implements
|
|||||||
case PARTICIPANT_LOADER:
|
case PARTICIPANT_LOADER:
|
||||||
mListener.onContactCustomColorLoaded(this);
|
mListener.onContactCustomColorLoaded(this);
|
||||||
break;
|
break;
|
||||||
|
case FILTERED_CONTACTS_LOADER:
|
||||||
|
mListener.onFilteredContactsCursorUpdated(data);
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
Assert.fail("Unknown loader id for contact picker!");
|
Assert.fail("Unknown loader id for contact picker!");
|
||||||
break;
|
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
|
// 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
|
// 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
|
// 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:
|
case PARTICIPANT_LOADER:
|
||||||
mListener.onContactCustomColorLoaded(this);
|
mListener.onContactCustomColorLoaded(this);
|
||||||
break;
|
break;
|
||||||
|
case FILTERED_CONTACTS_LOADER:
|
||||||
|
mListener.onFilteredContactsCursorUpdated(null);
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
Assert.fail("Unknown loader id for contact picker!");
|
Assert.fail("Unknown loader id for contact picker!");
|
||||||
break;
|
break;
|
||||||
@@ -170,6 +190,33 @@ public class ContactPickerData extends BindableData implements
|
|||||||
mLoaderManager.initLoader(PARTICIPANT_LOADER, args, this);
|
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<ContactPickerData> 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
|
@Override
|
||||||
protected void unregisterListeners() {
|
protected void unregisterListeners() {
|
||||||
mListener = null;
|
mListener = null;
|
||||||
@@ -180,6 +227,7 @@ public class ContactPickerData extends BindableData implements
|
|||||||
mLoaderManager.destroyLoader(ALL_CONTACTS_LOADER);
|
mLoaderManager.destroyLoader(ALL_CONTACTS_LOADER);
|
||||||
mLoaderManager.destroyLoader(FREQUENT_CONTACTS_LOADER);
|
mLoaderManager.destroyLoader(FREQUENT_CONTACTS_LOADER);
|
||||||
mLoaderManager.destroyLoader(PARTICIPANT_LOADER);
|
mLoaderManager.destroyLoader(PARTICIPANT_LOADER);
|
||||||
|
mLoaderManager.destroyLoader(FILTERED_CONTACTS_LOADER);
|
||||||
mLoaderManager = null;
|
mLoaderManager = null;
|
||||||
}
|
}
|
||||||
mFrequentContactsCursorBuilder.resetBuilder();
|
mFrequentContactsCursorBuilder.resetBuilder();
|
||||||
|
|||||||
@@ -619,6 +619,21 @@ public class ConversationMessageData {
|
|||||||
+ CONVERSATION_MESSAGES_QUERY_SQL_GROUP_BY;
|
+ 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() {
|
static String getConversationMessageIdsQuerySql() {
|
||||||
return CONVERSATION_MESSAGES_IDS_QUERY_SQL
|
return CONVERSATION_MESSAGES_IDS_QUERY_SQL
|
||||||
+ " AND "
|
+ " AND "
|
||||||
|
|||||||
+52
-51
@@ -1,6 +1,6 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright (C) 2015 The Android Open Source Project
|
* 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");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with the License.
|
* you may not use this file except in compliance with the License.
|
||||||
@@ -38,77 +38,82 @@ import com.android.messaging.util.LogUtil;
|
|||||||
import java.util.List;
|
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<Cursor> {
|
LoaderManager.LoaderCallbacks<Cursor> {
|
||||||
public interface PeopleAndOptionsDataListener {
|
public interface ConversationProfileDataListener {
|
||||||
void onOptionsCursorUpdated(PeopleAndOptionsData data, Cursor cursor);
|
void onConversationMetadataUpdated(ConversationProfileData data,
|
||||||
void onParticipantsListLoaded(PeopleAndOptionsData data,
|
ConversationListItemData conversation);
|
||||||
|
void onParticipantsListLoaded(ConversationProfileData data,
|
||||||
List<ParticipantData> participants);
|
List<ParticipantData> participants);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static final String BINDING_ID = "bindingId";
|
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 Context mContext;
|
||||||
private final String mConversationId;
|
private final String mConversationId;
|
||||||
private final ConversationParticipantsData mParticipantData;
|
private final ConversationParticipantsData mParticipantData;
|
||||||
private LoaderManager mLoaderManager;
|
private LoaderManager mLoaderManager;
|
||||||
private PeopleAndOptionsDataListener mListener;
|
private ConversationProfileDataListener mListener;
|
||||||
|
|
||||||
public PeopleAndOptionsData(final String conversationId, final Context context,
|
public ConversationProfileData(final String conversationId, final Context context,
|
||||||
final PeopleAndOptionsDataListener listener) {
|
final ConversationProfileDataListener listener) {
|
||||||
mListener = listener;
|
mListener = listener;
|
||||||
mContext = context;
|
mContext = context;
|
||||||
mConversationId = conversationId;
|
mConversationId = conversationId;
|
||||||
mParticipantData = new ConversationParticipantsData();
|
mParticipantData = new ConversationParticipantsData();
|
||||||
}
|
}
|
||||||
|
|
||||||
private static final int CONVERSATION_OPTIONS_LOADER = 1;
|
|
||||||
private static final int PARTICIPANT_LOADER = 2;
|
|
||||||
|
|
||||||
@NonNull
|
@NonNull
|
||||||
@Override
|
@Override
|
||||||
public Loader<Cursor> onCreateLoader(final int id, final Bundle args) {
|
public Loader<Cursor> onCreateLoader(final int id, final Bundle args) {
|
||||||
final String bindingId = args.getString(BINDING_ID);
|
final String bindingId = args.getString(BINDING_ID);
|
||||||
// Check if data still bound to the requesting ui element
|
if (!isBound(bindingId)) {
|
||||||
if (isBound(bindingId)) {
|
LogUtil.w(LogUtil.BUGLE_TAG, "Loader created after unbinding conversation profile");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
switch (id) {
|
switch (id) {
|
||||||
case CONVERSATION_OPTIONS_LOADER: {
|
case CONVERSATION_METADATA_LOADER: {
|
||||||
final Uri uri =
|
final Uri uri =
|
||||||
MessagingContentProvider.buildConversationMetadataUri(mConversationId);
|
MessagingContentProvider.buildConversationMetadataUri(mConversationId);
|
||||||
return new BoundCursorLoader(bindingId, mContext, uri,
|
return new BoundCursorLoader(bindingId, mContext, uri,
|
||||||
new String[]{}, null, null, null);
|
ConversationListItemData.PROJECTION, null, null, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
case PARTICIPANT_LOADER: {
|
case PARTICIPANT_LOADER: {
|
||||||
final Uri uri =
|
final Uri uri =
|
||||||
MessagingContentProvider
|
MessagingContentProvider.buildConversationParticipantsUri(mConversationId);
|
||||||
.buildConversationParticipantsUri(mConversationId);
|
|
||||||
return new BoundCursorLoader(bindingId, mContext, uri,
|
return new BoundCursorLoader(bindingId, mContext, uri,
|
||||||
ParticipantData.ParticipantsQuery.PROJECTION, null, null, null);
|
ParticipantData.ParticipantsQuery.PROJECTION, null, null, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
default:
|
default:
|
||||||
Assert.fail("Unknown loader id for PeopleAndOptionsFragment!");
|
Assert.fail("Unknown loader id for the conversation profile!");
|
||||||
break;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
LogUtil.w(LogUtil.BUGLE_TAG, "Loader created after unbinding PeopleAndOptionsFragment");
|
|
||||||
}
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* {@inheritDoc}
|
|
||||||
*/
|
|
||||||
@Override
|
@Override
|
||||||
public void onLoadFinished(@NonNull final Loader<Cursor> loader, final Cursor data) {
|
public void onLoadFinished(@NonNull final Loader<Cursor> loader, final Cursor data) {
|
||||||
final BoundCursorLoader cursorLoader = (BoundCursorLoader) loader;
|
final BoundCursorLoader cursorLoader = (BoundCursorLoader) loader;
|
||||||
if (isBound(cursorLoader.getBindingId())) {
|
if (!isBound(cursorLoader.getBindingId()) || mListener == null) {
|
||||||
|
LogUtil.w(LogUtil.BUGLE_TAG, "Loader finished after unbinding conversation profile");
|
||||||
|
return;
|
||||||
|
}
|
||||||
switch (loader.getId()) {
|
switch (loader.getId()) {
|
||||||
case CONVERSATION_OPTIONS_LOADER:
|
case CONVERSATION_METADATA_LOADER: {
|
||||||
mListener.onOptionsCursorUpdated(this, data);
|
ConversationListItemData conversation = null;
|
||||||
|
if (data != null && data.moveToFirst()) {
|
||||||
|
conversation = new ConversationListItemData();
|
||||||
|
conversation.bind(data);
|
||||||
|
}
|
||||||
|
mListener.onConversationMetadataUpdated(this, conversation);
|
||||||
break;
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
case PARTICIPANT_LOADER:
|
case PARTICIPANT_LOADER:
|
||||||
mParticipantData.bind(data);
|
mParticipantData.bind(data);
|
||||||
@@ -117,25 +122,20 @@ public class PeopleAndOptionsData extends BindableData implements
|
|||||||
break;
|
break;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
Assert.fail("Unknown loader id for PeopleAndOptionsFragment!");
|
Assert.fail("Unknown loader id for the conversation profile!");
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
LogUtil.w(LogUtil.BUGLE_TAG,
|
|
||||||
"Loader finished after unbinding PeopleAndOptionsFragment");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* {@inheritDoc}
|
|
||||||
*/
|
|
||||||
@Override
|
@Override
|
||||||
public void onLoaderReset(@NonNull final Loader<Cursor> loader) {
|
public void onLoaderReset(@NonNull final Loader<Cursor> loader) {
|
||||||
final BoundCursorLoader cursorLoader = (BoundCursorLoader) loader;
|
final BoundCursorLoader cursorLoader = (BoundCursorLoader) loader;
|
||||||
if (isBound(cursorLoader.getBindingId())) {
|
if (!isBound(cursorLoader.getBindingId()) || mListener == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
switch (loader.getId()) {
|
switch (loader.getId()) {
|
||||||
case CONVERSATION_OPTIONS_LOADER:
|
case CONVERSATION_METADATA_LOADER:
|
||||||
mListener.onOptionsCursorUpdated(this, null);
|
mListener.onConversationMetadataUpdated(this, null);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case PARTICIPANT_LOADER:
|
case PARTICIPANT_LOADER:
|
||||||
@@ -143,20 +143,17 @@ public class PeopleAndOptionsData extends BindableData implements
|
|||||||
break;
|
break;
|
||||||
|
|
||||||
default:
|
default:
|
||||||
Assert.fail("Unknown loader id for PeopleAndOptionsFragment!");
|
Assert.fail("Unknown loader id for the conversation profile!");
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
LogUtil.w(LogUtil.BUGLE_TAG, "Loader reset after unbinding PeopleAndOptionsFragment");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void init(final LoaderManager loaderManager,
|
public void init(final LoaderManager loaderManager,
|
||||||
final BindingBase<PeopleAndOptionsData> binding) {
|
final BindingBase<ConversationProfileData> binding) {
|
||||||
final Bundle args = new Bundle();
|
final Bundle args = new Bundle();
|
||||||
args.putString(BINDING_ID, binding.getBindingId());
|
args.putString(BINDING_ID, binding.getBindingId());
|
||||||
mLoaderManager = loaderManager;
|
mLoaderManager = loaderManager;
|
||||||
mLoaderManager.initLoader(CONVERSATION_OPTIONS_LOADER, args, this);
|
mLoaderManager.initLoader(CONVERSATION_METADATA_LOADER, args, this);
|
||||||
mLoaderManager.initLoader(PARTICIPANT_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
|
// This could be null if we bind but the caller doesn't init the BindableData
|
||||||
if (mLoaderManager != null) {
|
if (mLoaderManager != null) {
|
||||||
mLoaderManager.destroyLoader(CONVERSATION_OPTIONS_LOADER);
|
mLoaderManager.destroyLoader(CONVERSATION_METADATA_LOADER);
|
||||||
mLoaderManager.destroyLoader(PARTICIPANT_LOADER);
|
mLoaderManager.destroyLoader(PARTICIPANT_LOADER);
|
||||||
mLoaderManager = null;
|
mLoaderManager = null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setDestinationBlocked(final BindingBase<PeopleAndOptionsData> binding,
|
public void setDestinationBlocked(final BindingBase<ConversationProfileData> binding,
|
||||||
final boolean blocked) {
|
final boolean blocked) {
|
||||||
final String bindingId = binding.getBindingId();
|
|
||||||
final ParticipantData participantData = mParticipantData.getOtherParticipant();
|
final ParticipantData participantData = mParticipantData.getOtherParticipant();
|
||||||
if (isBound(bindingId) && participantData != null) {
|
if (isBound(binding.getBindingId()) && participantData != null) {
|
||||||
UpdateDestinationBlockedAction.updateDestinationBlocked(
|
UpdateDestinationBlockedAction.updateDestinationBlocked(
|
||||||
participantData.getNormalizedDestination(),
|
participantData.getNormalizedDestination(),
|
||||||
blocked, mConversationId,
|
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() {
|
public String getConversationId() {
|
||||||
return mConversationId;
|
return mConversationId;
|
||||||
}
|
}
|
||||||
@@ -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<Cursor> {
|
||||||
|
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<ConversationSearchData> 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<Cursor> 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<Cursor> 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<Cursor> 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -41,8 +41,10 @@ public class ContactIconView extends AsyncImageView {
|
|||||||
private static final int NORMAL_ICON_SIZE_ID = 0;
|
private static final int NORMAL_ICON_SIZE_ID = 0;
|
||||||
private static final int LARGE_ICON_SIZE_ID = 1;
|
private static final int LARGE_ICON_SIZE_ID = 1;
|
||||||
private static final int SMALL_ICON_SIZE_ID = 2;
|
private static final int SMALL_ICON_SIZE_ID = 2;
|
||||||
|
private static final int PROFILE_POPUP_ICON_SIZE_ID = 3;
|
||||||
|
|
||||||
protected final int mIconSize;
|
protected final int mIconSize;
|
||||||
|
private final boolean mCropToCircle;
|
||||||
private final int mColorPressedId;
|
private final int mColorPressedId;
|
||||||
|
|
||||||
private long mContactId;
|
private long mContactId;
|
||||||
@@ -58,6 +60,7 @@ public class ContactIconView extends AsyncImageView {
|
|||||||
final TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.ContactIconView);
|
final TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.ContactIconView);
|
||||||
|
|
||||||
final int iconSizeId = a.getInt(R.styleable.ContactIconView_contactIconSize, 0);
|
final int iconSizeId = a.getInt(R.styleable.ContactIconView_contactIconSize, 0);
|
||||||
|
boolean cropToCircle = true;
|
||||||
switch (iconSizeId) {
|
switch (iconSizeId) {
|
||||||
case NORMAL_ICON_SIZE_ID:
|
case NORMAL_ICON_SIZE_ID:
|
||||||
mIconSize = (int) resources.getDimension(
|
mIconSize = (int) resources.getDimension(
|
||||||
@@ -71,11 +74,17 @@ public class ContactIconView extends AsyncImageView {
|
|||||||
mIconSize = (int) resources.getDimension(
|
mIconSize = (int) resources.getDimension(
|
||||||
R.dimen.contact_icon_view_small_size);
|
R.dimen.contact_icon_view_small_size);
|
||||||
break;
|
break;
|
||||||
|
case PROFILE_POPUP_ICON_SIZE_ID:
|
||||||
|
mIconSize = (int) resources.getDimension(
|
||||||
|
R.dimen.home_list_profile_avatar_size);
|
||||||
|
cropToCircle = false;
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
// For the compiler, something has to be set even with the assert.
|
// For the compiler, something has to be set even with the assert.
|
||||||
mIconSize = 0;
|
mIconSize = 0;
|
||||||
Assert.fail("Unsupported ContactIconView icon size attribute");
|
Assert.fail("Unsupported ContactIconView icon size attribute");
|
||||||
}
|
}
|
||||||
|
mCropToCircle = cropToCircle;
|
||||||
mColorPressedId = resources.getColor(R.color.contact_avatar_pressed_color,
|
mColorPressedId = resources.getColor(R.color.contact_avatar_pressed_color,
|
||||||
context.getTheme());
|
context.getTheme());
|
||||||
|
|
||||||
@@ -118,7 +127,8 @@ public class ContactIconView extends AsyncImageView {
|
|||||||
if (AvatarUriUtil.TYPE_GROUP_URI.equals(avatarType)) {
|
if (AvatarUriUtil.TYPE_GROUP_URI.equals(avatarType)) {
|
||||||
setImageResourceId(new AvatarGroupRequestDescriptor(uri, mIconSize, mIconSize));
|
setImageResourceId(new AvatarGroupRequestDescriptor(uri, mIconSize, mIconSize));
|
||||||
} else {
|
} else {
|
||||||
setImageResourceId(new AvatarRequestDescriptor(uri, mIconSize, mIconSize));
|
setImageResourceId(new AvatarRequestDescriptor(uri, mIconSize, mIconSize,
|
||||||
|
mCropToCircle));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -166,9 +166,15 @@ public abstract class UIIntents {
|
|||||||
public abstract void launchAddContactActivity(final Context context, final String destination);
|
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);
|
final String conversationId);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -189,6 +195,21 @@ public abstract class UIIntents {
|
|||||||
*/
|
*/
|
||||||
public abstract void launchBlockedParticipantsActivity(final Context context);
|
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
|
* Launch an activity to show a class zero message
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -57,7 +57,6 @@ import com.android.messaging.ui.conversation.LaunchConversationActivity;
|
|||||||
import com.android.messaging.ui.conversationlist.ArchivedConversationListActivity;
|
import com.android.messaging.ui.conversationlist.ArchivedConversationListActivity;
|
||||||
import com.android.messaging.ui.conversationlist.ConversationListActivity;
|
import com.android.messaging.ui.conversationlist.ConversationListActivity;
|
||||||
import com.android.messaging.ui.conversationlist.ForwardMessageActivity;
|
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.ui.photoviewer.BuglePhotoViewActivity;
|
||||||
import com.android.messaging.util.Assert;
|
import com.android.messaging.util.Assert;
|
||||||
import com.android.messaging.util.ConversationIdSet;
|
import com.android.messaging.util.ConversationIdSet;
|
||||||
@@ -215,11 +214,42 @@ public class UIIntentsImpl extends UIIntents {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void launchPeopleAndOptionsActivity(final Activity activity,
|
public void launchHomeSpamActivity(final Context context) {
|
||||||
final String conversationId) {
|
final Intent intent = new Intent(context,
|
||||||
final Intent intent = new Intent(activity, PeopleAndOptionsActivity.class);
|
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);
|
intent.putExtra(UI_INTENT_EXTRA_CONVERSATION_ID, conversationId);
|
||||||
activity.startActivityForResult(intent, 0);
|
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
|
@Override
|
||||||
|
|||||||
@@ -35,11 +35,14 @@ import android.util.AttributeSet;
|
|||||||
import android.view.ContextThemeWrapper;
|
import android.view.ContextThemeWrapper;
|
||||||
import android.view.KeyEvent;
|
import android.view.KeyEvent;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
|
import android.view.ViewGroup;
|
||||||
import android.view.accessibility.AccessibilityEvent;
|
import android.view.accessibility.AccessibilityEvent;
|
||||||
import android.view.inputmethod.EditorInfo;
|
import android.view.inputmethod.EditorInfo;
|
||||||
import android.widget.ImageButton;
|
import android.widget.ImageButton;
|
||||||
|
import android.widget.ImageView;
|
||||||
import android.widget.LinearLayout;
|
import android.widget.LinearLayout;
|
||||||
import android.widget.TextView;
|
import android.widget.TextView;
|
||||||
|
import android.widget.Toast;
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
import androidx.appcompat.app.ActionBar;
|
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.BugleActionBarActivity;
|
||||||
import com.android.messaging.ui.PlainTextEditText;
|
import com.android.messaging.ui.PlainTextEditText;
|
||||||
import com.android.messaging.ui.conversation.ConversationInputManager.ConversationInputSink;
|
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.AccessibilityUtil;
|
||||||
import com.android.messaging.util.Assert;
|
import com.android.messaging.util.Assert;
|
||||||
import com.android.messaging.util.AvatarUriUtil;
|
import com.android.messaging.util.AvatarUriUtil;
|
||||||
@@ -102,6 +106,7 @@ public class ComposeMessageView extends LinearLayout
|
|||||||
boolean tooManyVideos);
|
boolean tooManyVideos);
|
||||||
void notifyOfAttachmentLoadFailed();
|
void notifyOfAttachmentLoadFailed();
|
||||||
void showAttachmentChooser();
|
void showAttachmentChooser();
|
||||||
|
void launchSystemGalleryPicker();
|
||||||
boolean shouldShowSubjectEditor();
|
boolean shouldShowSubjectEditor();
|
||||||
boolean shouldHideAttachmentsWhenSimSelectorShown();
|
boolean shouldHideAttachmentsWhenSimSelectorShown();
|
||||||
Uri getSelfSendButtonIconUri();
|
Uri getSelfSendButtonIconUri();
|
||||||
@@ -128,6 +133,13 @@ public class ComposeMessageView extends LinearLayout
|
|||||||
private ImageButton mDeleteSubjectButton;
|
private ImageButton mDeleteSubjectButton;
|
||||||
private AttachmentPreview mAttachmentPreview;
|
private AttachmentPreview mAttachmentPreview;
|
||||||
private ImageButton mAttachMediaButton;
|
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<DraftMessageData> mBinding;
|
private final Binding<DraftMessageData> mBinding;
|
||||||
private IComposeMessageViewHost mHost;
|
private IComposeMessageViewHost mHost;
|
||||||
@@ -283,10 +295,37 @@ public class ComposeMessageView extends LinearLayout
|
|||||||
});
|
});
|
||||||
|
|
||||||
mAttachMediaButton = findViewById(R.id.attach_media_button);
|
mAttachMediaButton = findViewById(R.id.attach_media_button);
|
||||||
mAttachMediaButton.setOnClickListener(clickView -> {
|
mAttachmentSheetContainer = findViewById(R.id.home_compose_attachment_sheet_container);
|
||||||
// Showing the media picker is treated as starting to compose the message.
|
mComposeBottomStack = findViewById(R.id.home_compose_bottom_stack);
|
||||||
mInputManager.showHideMediaPicker(true /* show */, true /* animate */);
|
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 = findViewById(R.id.attachment_draft_view);
|
||||||
mAttachmentPreview.setComposeMessageView(this);
|
mAttachmentPreview.setComposeMessageView(this);
|
||||||
@@ -666,7 +705,7 @@ public class ComposeMessageView extends LinearLayout
|
|||||||
mMessageBodySize.setVisibility(View.VISIBLE);
|
mMessageBodySize.setVisibility(View.VISIBLE);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
mMessageBodySize.setVisibility(View.INVISIBLE);
|
mMessageBodySize.setVisibility(View.GONE);
|
||||||
}
|
}
|
||||||
} else { // SMS case
|
} else { // SMS case
|
||||||
// Update the SMS text counter.
|
// Update the SMS text counter.
|
||||||
@@ -685,7 +724,7 @@ public class ComposeMessageView extends LinearLayout
|
|||||||
mMessageBodySize.setText(counterText);
|
mMessageBodySize.setText(counterText);
|
||||||
mMessageBodySize.setVisibility(View.VISIBLE);
|
mMessageBodySize.setVisibility(View.VISIBLE);
|
||||||
} else {
|
} else {
|
||||||
mMessageBodySize.setVisibility(View.INVISIBLE);
|
mMessageBodySize.setVisibility(View.GONE);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -724,6 +763,7 @@ public class ComposeMessageView extends LinearLayout
|
|||||||
setSendButtonAccessibility(sendWidgetMode);
|
setSendButtonAccessibility(sendWidgetMode);
|
||||||
mSendWidgetMode = sendWidgetMode;
|
mSendWidgetMode = sendWidgetMode;
|
||||||
}
|
}
|
||||||
|
updateVoiceAndSendColumnVisibility(sendWidgetMode);
|
||||||
|
|
||||||
// Update the text hint on the message box depending on the attachment type.
|
// Update the text hint on the message box depending on the attachment type.
|
||||||
final int attachmentCount = attachments.size();
|
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) {
|
private void setSendButtonAccessibility(final int sendWidgetMode) {
|
||||||
switch (sendWidgetMode) {
|
switch (sendWidgetMode) {
|
||||||
case SEND_WIDGET_MODE_SELF_AVATAR:
|
case SEND_WIDGET_MODE_SELF_AVATAR:
|
||||||
@@ -920,6 +970,9 @@ public class ComposeMessageView extends LinearLayout
|
|||||||
}
|
}
|
||||||
|
|
||||||
public void hideAllComposeInputs(final boolean animate) {
|
public void hideAllComposeInputs(final boolean animate) {
|
||||||
|
if (mAttachmentSheetOpen) {
|
||||||
|
closeComposeAttachmentSheet(animate, null);
|
||||||
|
}
|
||||||
mInputManager.hideAllInputs(animate);
|
mInputManager.hideAllInputs(animate);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -928,10 +981,17 @@ public class ComposeMessageView extends LinearLayout
|
|||||||
}
|
}
|
||||||
|
|
||||||
public void resetMediaPickerState() {
|
public void resetMediaPickerState() {
|
||||||
|
if (mAttachmentSheetOpen) {
|
||||||
|
closeComposeAttachmentSheet(false /* animate */, null);
|
||||||
|
}
|
||||||
mInputManager.resetMediaPickerState();
|
mInputManager.resetMediaPickerState();
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean onBackPressed() {
|
public boolean onBackPressed() {
|
||||||
|
if (mAttachmentSheetOpen) {
|
||||||
|
closeComposeAttachmentSheet(true /* animate */, null);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
return mInputManager.onBackPressed();
|
return mInputManager.onBackPressed();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -982,4 +1042,167 @@ public class ComposeMessageView extends LinearLayout
|
|||||||
mAttachMediaButton.setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_NO);
|
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));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -61,6 +61,10 @@ public class ConversationActivity extends BugleActionBarActivity
|
|||||||
// Tracks whether onPause is called.
|
// Tracks whether onPause is called.
|
||||||
private boolean mIsPaused;
|
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
|
@Override
|
||||||
protected void onCreate(final Bundle savedInstanceState) {
|
protected void onCreate(final Bundle savedInstanceState) {
|
||||||
super.onCreate(savedInstanceState);
|
super.onCreate(savedInstanceState);
|
||||||
@@ -146,9 +150,17 @@ public class ConversationActivity extends BugleActionBarActivity
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void suppressNextResumeTransition() {
|
||||||
|
mSuppressNextResumeTransition = true;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void onResume() {
|
protected void onResume() {
|
||||||
super.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
|
// we need to reset the mInstanceStateSaved flag since we may have just been restored from
|
||||||
// a previous onStop() instead of an onDestroy().
|
// a previous onStop() instead of an onDestroy().
|
||||||
|
|||||||
+514
-192
@@ -43,11 +43,15 @@ import android.telephony.PhoneNumberUtils;
|
|||||||
import android.text.TextUtils;
|
import android.text.TextUtils;
|
||||||
import android.view.ActionMode;
|
import android.view.ActionMode;
|
||||||
import android.view.Display;
|
import android.view.Display;
|
||||||
|
import android.view.HapticFeedbackConstants;
|
||||||
import android.view.LayoutInflater;
|
import android.view.LayoutInflater;
|
||||||
import android.view.Menu;
|
import android.view.Menu;
|
||||||
import android.view.MenuInflater;
|
import android.view.MenuInflater;
|
||||||
import android.view.MenuItem;
|
import android.view.MenuItem;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
|
import android.widget.ImageButton;
|
||||||
|
import android.widget.PopupMenu;
|
||||||
|
import android.widget.Toast;
|
||||||
import android.view.ViewConfiguration;
|
import android.view.ViewConfiguration;
|
||||||
import android.view.ViewGroup;
|
import android.view.ViewGroup;
|
||||||
import android.widget.TextView;
|
import android.widget.TextView;
|
||||||
@@ -72,7 +76,9 @@ import androidx.recyclerview.widget.RecyclerView.ViewHolder;
|
|||||||
import com.android.messaging.R;
|
import com.android.messaging.R;
|
||||||
import com.android.messaging.datamodel.DataModel;
|
import com.android.messaging.datamodel.DataModel;
|
||||||
import com.android.messaging.datamodel.MessagingContentProvider;
|
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.InsertNewMessageAction;
|
||||||
|
import com.android.messaging.datamodel.action.UpdateDestinationBlockedAction;
|
||||||
import com.android.messaging.datamodel.binding.Binding;
|
import com.android.messaging.datamodel.binding.Binding;
|
||||||
import com.android.messaging.datamodel.binding.BindingBase;
|
import com.android.messaging.datamodel.binding.BindingBase;
|
||||||
import com.android.messaging.datamodel.binding.ImmutableBindingRef;
|
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.datamodel.data.SubscriptionListData.SubscriptionListEntry;
|
||||||
import com.android.messaging.ui.AttachmentPreview;
|
import com.android.messaging.ui.AttachmentPreview;
|
||||||
import com.android.messaging.ui.BugleActionBarActivity;
|
import com.android.messaging.ui.BugleActionBarActivity;
|
||||||
|
import com.android.messaging.ui.ContactIconView;
|
||||||
import com.android.messaging.ui.ConversationDrawables;
|
import com.android.messaging.ui.ConversationDrawables;
|
||||||
import com.android.messaging.ui.SnackBar;
|
import com.android.messaging.ui.SnackBar;
|
||||||
import com.android.messaging.ui.UIIntents;
|
import com.android.messaging.ui.UIIntents;
|
||||||
import com.android.messaging.ui.animation.PopupTransitionAnimation;
|
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.attachmentchooser.AttachmentChooserActivity;
|
||||||
import com.android.messaging.ui.contact.AddContactsConfirmationDialog;
|
import com.android.messaging.ui.contact.AddContactsConfirmationDialog;
|
||||||
import com.android.messaging.ui.conversation.ComposeMessageView.IComposeMessageViewHost;
|
import com.android.messaging.ui.conversation.ComposeMessageView.IComposeMessageViewHost;
|
||||||
import com.android.messaging.ui.conversation.ConversationInputManager.ConversationInputHost;
|
import com.android.messaging.ui.conversation.ConversationInputManager.ConversationInputHost;
|
||||||
import com.android.messaging.ui.conversation.ConversationMessageView.ConversationMessageViewHost;
|
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.ui.mediapicker.MediaPicker;
|
||||||
import com.android.messaging.util.AccessibilityUtil;
|
import com.android.messaging.util.AccessibilityUtil;
|
||||||
import com.android.messaging.util.Assert;
|
import com.android.messaging.util.Assert;
|
||||||
import com.android.messaging.util.AvatarUriUtil;
|
import com.android.messaging.util.AvatarUriUtil;
|
||||||
|
import com.android.messaging.util.Dates;
|
||||||
import com.android.messaging.util.ChangeDefaultSmsAppHelper;
|
import com.android.messaging.util.ChangeDefaultSmsAppHelper;
|
||||||
|
import com.android.messaging.util.ContactUtil;
|
||||||
import com.android.messaging.util.ImeUtil;
|
import com.android.messaging.util.ImeUtil;
|
||||||
import com.android.messaging.util.LogUtil;
|
import com.android.messaging.util.LogUtil;
|
||||||
import com.android.messaging.util.PhoneUtils;
|
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;
|
public static final int MESSAGE_ANIMATION_MAX_WAIT = 500;
|
||||||
|
|
||||||
private ComposeMessageView mComposeMessageView;
|
private ComposeMessageView mComposeMessageView;
|
||||||
|
private DocumentImagePicker mGalleryPicker;
|
||||||
private RecyclerView mRecyclerView;
|
private RecyclerView mRecyclerView;
|
||||||
private ConversationMessageAdapter mAdapter;
|
private ConversationMessageAdapter mAdapter;
|
||||||
private ConversationFastScroller mFastScroller;
|
private ConversationFastScroller mFastScroller;
|
||||||
|
|
||||||
private View mConversationComposeDivider;
|
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 ChangeDefaultSmsAppHelper mChangeDefaultSmsAppHelper;
|
||||||
|
|
||||||
private String mConversationId;
|
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<MessagePartData> 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<Intent> mLauncher = registerForActivityResult(
|
private final ActivityResultLauncher<Intent> mLauncher = registerForActivityResult(
|
||||||
new ActivityResultContracts.StartActivityForResult(), result -> {
|
new ActivityResultContracts.StartActivityForResult(), result -> {
|
||||||
if (result.getResultCode() == Activity.RESULT_OK) {
|
if (result.getResultCode() == Activity.RESULT_OK) {
|
||||||
@@ -419,6 +329,11 @@ public class ConversationFragment extends Fragment implements ConversationDataLi
|
|||||||
@Override
|
@Override
|
||||||
public void onCreate(final Bundle savedInstanceState) {
|
public void onCreate(final Bundle savedInstanceState) {
|
||||||
super.onCreate(savedInstanceState);
|
super.onCreate(savedInstanceState);
|
||||||
|
mGalleryPicker = new DocumentImagePicker(this, pendingItem -> {
|
||||||
|
if (mComposeMessageView != null) {
|
||||||
|
mComposeMessageView.onPendingAttachmentAdded(pendingItem);
|
||||||
|
}
|
||||||
|
});
|
||||||
mAdapter = new ConversationMessageAdapter(getActivity(), null, this,
|
mAdapter = new ConversationMessageAdapter(getActivity(), null, this,
|
||||||
null,
|
null,
|
||||||
// Sets the item click listener on the Recycler item views.
|
// Sets the item click listener on the Recycler item views.
|
||||||
@@ -427,6 +342,7 @@ public class ConversationFragment extends Fragment implements ConversationDataLi
|
|||||||
handleMessageClick(messageView);
|
handleMessageClick(messageView);
|
||||||
},
|
},
|
||||||
view -> {
|
view -> {
|
||||||
|
view.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS);
|
||||||
selectMessage((ConversationMessageView) view);
|
selectMessage((ConversationMessageView) view);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -507,6 +423,12 @@ public class ConversationFragment extends Fragment implements ConversationDataLi
|
|||||||
final Bundle savedInstanceState) {
|
final Bundle savedInstanceState) {
|
||||||
final View view = inflater.inflate(R.layout.conversation_fragment, container, false);
|
final View view = inflater.inflate(R.layout.conversation_fragment, container, false);
|
||||||
mRecyclerView = view.findViewById(android.R.id.list);
|
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());
|
final LinearLayoutManager manager = new LinearLayoutManager(getActivity());
|
||||||
manager.setStackFromEnd(true);
|
manager.setStackFromEnd(true);
|
||||||
manager.setReverseLayout(false);
|
manager.setReverseLayout(false);
|
||||||
@@ -672,13 +594,19 @@ public class ConversationFragment extends Fragment implements ConversationDataLi
|
|||||||
mSelectedMessage = messageView;
|
mSelectedMessage = messageView;
|
||||||
if (mSelectedMessage == null) {
|
if (mSelectedMessage == null) {
|
||||||
mAdapter.setSelectedMessage(null);
|
mAdapter.setSelectedMessage(null);
|
||||||
mHost.dismissActionMode();
|
|
||||||
mSelectedAttachment = null;
|
mSelectedAttachment = null;
|
||||||
|
updateMessageSelectionChrome(false);
|
||||||
|
updateUnknownSenderBanner();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
mSelectedAttachment = attachment;
|
mSelectedAttachment = attachment;
|
||||||
mAdapter.setSelectedMessage(messageView.getData().getMessageId());
|
mAdapter.setSelectedMessage(messageView.getData().getMessageId());
|
||||||
mHost.startActionMode(mMessageActionModeCallback);
|
updateMessageSelectionChrome(true);
|
||||||
|
updateUnknownSenderBanner();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void dismissMessageSelection() {
|
||||||
|
selectMessage(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -732,7 +660,7 @@ public class ConversationFragment extends Fragment implements ConversationDataLi
|
|||||||
@Override
|
@Override
|
||||||
public void onCreateOptionsMenu(@NonNull final Menu menu,
|
public void onCreateOptionsMenu(@NonNull final Menu menu,
|
||||||
@NonNull final MenuInflater inflater) {
|
@NonNull final MenuInflater inflater) {
|
||||||
if (mHost.getActionMode() != null) {
|
if (mSelectedMessage != null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -746,7 +674,9 @@ public class ConversationFragment extends Fragment implements ConversationDataLi
|
|||||||
// See if we can show add contact action.
|
// See if we can show add contact action.
|
||||||
final ParticipantData participant = data.getOtherParticipant();
|
final ParticipantData participant = data.getOtherParticipant();
|
||||||
final boolean addContactActionVisible = (participant != null
|
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);
|
menu.findItem(R.id.action_add_contact).setVisible(addContactActionVisible);
|
||||||
|
|
||||||
// See if we should show archive or unarchive.
|
// See if we should show archive or unarchive.
|
||||||
@@ -765,32 +695,14 @@ public class ConversationFragment extends Fragment implements ConversationDataLi
|
|||||||
int itemId = item.getItemId();
|
int itemId = item.getItemId();
|
||||||
if (itemId == R.id.action_people_and_options) {
|
if (itemId == R.id.action_people_and_options) {
|
||||||
Assert.isTrue(mBinding.getData().getParticipantsLoaded());
|
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;
|
return true;
|
||||||
} else if (itemId == R.id.action_call) {
|
} else if (itemId == R.id.action_call) {
|
||||||
final String phoneNumber = mBinding.getData().getParticipantPhoneNumber();
|
placeConversationCall(getActivity() != null
|
||||||
Assert.notNull(phoneNumber);
|
? getActivity().findViewById(R.id.action_call) : null);
|
||||||
// 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);
|
|
||||||
}
|
|
||||||
return true;
|
return true;
|
||||||
} else if (itemId == R.id.action_archive) {
|
} else if (itemId == R.id.action_archive) {
|
||||||
mBinding.getData().archiveConversation(mBinding);
|
mBinding.getData().archiveConversation(mBinding);
|
||||||
@@ -904,6 +816,8 @@ public class ConversationFragment extends Fragment implements ConversationDataLi
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
updateHomeEmptyState();
|
||||||
|
updateUnknownSenderBanner();
|
||||||
mHost.invalidateActionBar();
|
mHost.invalidateActionBar();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -931,6 +845,9 @@ public class ConversationFragment extends Fragment implements ConversationDataLi
|
|||||||
invalidateOptionsMenu();
|
invalidateOptionsMenu();
|
||||||
mHost.onConversationMetadataUpdated();
|
mHost.onConversationMetadataUpdated();
|
||||||
mAdapter.notifyDataSetChanged();
|
mAdapter.notifyDataSetChanged();
|
||||||
|
updateHomeEmptyState();
|
||||||
|
updateUnknownSenderBanner();
|
||||||
|
mHost.invalidateActionBar();
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setConversationInfo(final Context context, final String conversationId,
|
public void setConversationInfo(final Context context, final String conversationId,
|
||||||
@@ -940,6 +857,7 @@ public class ConversationFragment extends Fragment implements ConversationDataLi
|
|||||||
if (!mBinding.isBound()) {
|
if (!mBinding.isBound()) {
|
||||||
mConversationId = conversationId;
|
mConversationId = conversationId;
|
||||||
mIncomingDraft = draftData;
|
mIncomingDraft = draftData;
|
||||||
|
mUnknownSenderBannerVisible = false;
|
||||||
mBinding.bind(DataModel.get().createConversationData(context, this, conversationId));
|
mBinding.bind(DataModel.get().createConversationData(context, this, conversationId));
|
||||||
} else {
|
} else {
|
||||||
Assert.isTrue(TextUtils.equals(mBinding.getData().getConversationId(), conversationId));
|
Assert.isTrue(TextUtils.equals(mBinding.getData().getConversationId(), conversationId));
|
||||||
@@ -1118,12 +1036,12 @@ public class ConversationFragment extends Fragment implements ConversationDataLi
|
|||||||
(dialog, which) ->
|
(dialog, which) ->
|
||||||
mBinding.getData().deleteMessage(mBinding, messageId))
|
mBinding.getData().deleteMessage(mBinding, messageId))
|
||||||
.setNegativeButton(android.R.string.cancel, null);
|
.setNegativeButton(android.R.string.cancel, null);
|
||||||
builder.setOnDismissListener(dialog -> mHost.dismissActionMode());
|
builder.setOnDismissListener(dialog -> dismissMessageSelection());
|
||||||
builder.create().show();
|
builder.create().show();
|
||||||
} else {
|
} else {
|
||||||
warnOfMissingActionConditions(false /*sending*/,
|
warnOfMissingActionConditions(false /*sending*/,
|
||||||
null /*commandToRunAfterActionConditionResolved*/);
|
null /*commandToRunAfterActionConditionResolved*/);
|
||||||
mHost.dismissActionMode();
|
dismissMessageSelection();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1159,6 +1077,7 @@ public class ConversationFragment extends Fragment implements ConversationDataLi
|
|||||||
mHost.invalidateActionBar();
|
mHost.invalidateActionBar();
|
||||||
|
|
||||||
mRecyclerView.setVisibility(View.VISIBLE);
|
mRecyclerView.setVisibility(View.VISIBLE);
|
||||||
|
updateUnknownSenderBanner();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1187,6 +1106,13 @@ public class ConversationFragment extends Fragment implements ConversationDataLi
|
|||||||
}
|
}
|
||||||
|
|
||||||
public boolean onBackPressed() {
|
public boolean onBackPressed() {
|
||||||
|
if (HomeGallerySheet.dismissIfShowing(getActivity())) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (mSelectedMessage != null) {
|
||||||
|
dismissMessageSelection();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
if (mComposeMessageView.onBackPressed()) {
|
if (mComposeMessageView.onBackPressed()) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -1366,7 +1292,11 @@ public class ConversationFragment extends Fragment implements ConversationDataLi
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void setOptionsMenuVisibility(final boolean visible) {
|
public void setOptionsMenuVisibility(final boolean visible) {
|
||||||
|
mOptionsMenuVisible = visible;
|
||||||
setHasOptionsMenu(visible);
|
setHasOptionsMenu(visible);
|
||||||
|
if (mConversationHeaderOverflow != null) {
|
||||||
|
mConversationHeaderOverflow.setVisibility(visible ? View.VISIBLE : View.GONE);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -1385,6 +1315,9 @@ public class ConversationFragment extends Fragment implements ConversationDataLi
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void dismissActionMode() {
|
public void dismissActionMode() {
|
||||||
|
if (mSelectedMessage != null) {
|
||||||
|
dismissMessageSelection();
|
||||||
|
}
|
||||||
mHost.dismissActionMode();
|
mHost.dismissActionMode();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1454,6 +1387,13 @@ public class ConversationFragment extends Fragment implements ConversationDataLi
|
|||||||
builder.show();
|
builder.show();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void launchSystemGalleryPicker() {
|
||||||
|
if (mGalleryPicker != null) {
|
||||||
|
mGalleryPicker.launchPicker();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void showAttachmentChooser() {
|
public void showAttachmentChooser() {
|
||||||
showAttachmentChooser(mConversationId, getActivity());
|
showAttachmentChooser(mConversationId, getActivity());
|
||||||
@@ -1467,62 +1407,444 @@ public class ConversationFragment extends Fragment implements ConversationDataLi
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void updateActionAndStatusBarColor(final ActionBar actionBar) {
|
private void updateActionAndStatusBarColor(final ActionBar actionBar) {
|
||||||
final int themeColor = ConversationDrawables.get().getConversationThemeColor();
|
final int barColor = getResources().getColor(R.color.home_conversation_chrome_bg,
|
||||||
actionBar.setBackgroundDrawable(new ColorDrawable(themeColor));
|
getActivity().getTheme());
|
||||||
UiUtils.setStatusBarColor(getActivity(), themeColor);
|
actionBar.setBackgroundDrawable(new ColorDrawable(barColor));
|
||||||
|
UiUtils.setStatusBarColor(getActivity(), barColor);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void updateActionBar(final ActionBar actionBar) {
|
private void updateHomeEmptyState() {
|
||||||
if (mComposeMessageView == null || !mComposeMessageView.updateActionBar(actionBar)) {
|
if (mEmptyContainer == null || mAdapter == null) {
|
||||||
updateActionAndStatusBarColor(actionBar);
|
return;
|
||||||
// We update this regardless of whether or not the action bar is showing so that we
|
}
|
||||||
// don't get a race when it reappears.
|
final boolean empty = mAdapter.getItemCount() == 0;
|
||||||
actionBar.setDisplayOptions(ActionBar.DISPLAY_SHOW_CUSTOM);
|
mEmptyContainer.setVisibility(empty ? View.VISIBLE : View.GONE);
|
||||||
actionBar.setDisplayHomeAsUpEnabled(true);
|
if (!empty) {
|
||||||
// Reset the back arrow to its default
|
return;
|
||||||
actionBar.setHomeAsUpIndicator(0);
|
}
|
||||||
View customView = actionBar.getCustomView();
|
if (mEmptyTime != null) {
|
||||||
if (customView == null || customView.getId() != R.id.conversation_title_container) {
|
mEmptyTime.setText(Dates.getMessageTimeString(System.currentTimeMillis()));
|
||||||
final LayoutInflater inflator = (LayoutInflater)
|
}
|
||||||
getActivity().getSystemService(Context.LAYOUT_INFLATER_SERVICE);
|
if (mEmptyStatus != null) {
|
||||||
customView = inflator.inflate(R.layout.action_bar_conversation_name, null);
|
final String name = getConversationName();
|
||||||
customView.setOnClickListener(v -> onBackPressed());
|
mEmptyStatus.setText(getString(R.string.home_conversation_empty_status,
|
||||||
actionBar.setCustomView(customView);
|
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<MessagePartData> 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();
|
final String conversationName = getConversationName();
|
||||||
if (!TextUtils.isEmpty(conversationName)) {
|
if (!TextUtils.isEmpty(conversationName)) {
|
||||||
// RTL : To format conversation title if it happens to be phone numbers.
|
|
||||||
final BidiFormatter bidiFormatter = BidiFormatter.getInstance();
|
final BidiFormatter bidiFormatter = BidiFormatter.getInstance();
|
||||||
final String formattedName = bidiFormatter.unicodeWrap(
|
final String formattedName = bidiFormatter.unicodeWrap(
|
||||||
UiUtils.commaEllipsize(
|
UiUtils.commaEllipsize(
|
||||||
conversationName,
|
conversationName,
|
||||||
conversationNameView.getPaint(),
|
titleView.getPaint(),
|
||||||
conversationNameView.getWidth(),
|
titleView.getWidth(),
|
||||||
getString(R.string.plus_one),
|
getString(R.string.plus_one),
|
||||||
getString(R.string.plus_n)).toString(),
|
getString(R.string.plus_n)).toString(),
|
||||||
TextDirectionHeuristicsCompat.LTR);
|
TextDirectionHeuristicsCompat.LTR);
|
||||||
conversationNameView.setText(formattedName);
|
titleView.setText(formattedName);
|
||||||
// In case phone numbers are mixed in the conversation name, we need to vocalize it.
|
|
||||||
final String vocalizedConversationName =
|
final String vocalizedConversationName =
|
||||||
AccessibilityUtil.getVocalizedPhoneNumber(getResources(), conversationName);
|
AccessibilityUtil.getVocalizedPhoneNumber(getResources(), conversationName);
|
||||||
conversationNameView.setContentDescription(vocalizedConversationName);
|
titleView.setContentDescription(vocalizedConversationName);
|
||||||
|
if (getActivity() != null) {
|
||||||
getActivity().setTitle(conversationName);
|
getActivity().setTitle(conversationName);
|
||||||
} else {
|
}
|
||||||
|
} else if (getActivity() != null) {
|
||||||
final String appName = getString(R.string.app_name);
|
final String appName = getString(R.string.app_name);
|
||||||
conversationNameView.setText(appName);
|
titleView.setText(appName);
|
||||||
getActivity().setTitle(appName);
|
getActivity().setTitle(appName);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// When conversation is showing and media picker is not showing, then hide the action
|
final boolean hideForIme = mHost.isImeOpen() && UiUtils.isLandscapeMode();
|
||||||
// bar only when we are in landscape mode, with IME open.
|
mConversationHeader.setVisibility(hideForIme ? View.GONE : View.VISIBLE);
|
||||||
if (mHost.isImeOpen() && UiUtils.isLandscapeMode()) {
|
}
|
||||||
|
|
||||||
|
public void updateActionBar(final ActionBar actionBar) {
|
||||||
|
if (mComposeMessageView != null && mComposeMessageView.updateActionBar(actionBar)) {
|
||||||
|
if (mConversationHeader != null) {
|
||||||
|
mConversationHeader.setVisibility(View.GONE);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
updateActionAndStatusBarColor(actionBar);
|
||||||
actionBar.hide();
|
actionBar.hide();
|
||||||
} else {
|
updateHomeConversationHeader();
|
||||||
actionBar.show();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|||||||
@@ -50,6 +50,13 @@ import java.util.Collection;
|
|||||||
* as the other components).
|
* as the other components).
|
||||||
*/
|
*/
|
||||||
public class ConversationInputManager implements ConversationInput.ConversationInputBase {
|
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
|
* The host component where all input components are contained. This is typically the
|
||||||
* conversation fragment but may be mocked in test code.
|
* 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) {
|
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);
|
showHideInternal(mMediaInput, show, animate);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -322,6 +341,11 @@ public class ConversationInputManager implements ConversationInput.ConversationI
|
|||||||
}
|
}
|
||||||
|
|
||||||
private MediaPicker mMediaPicker;
|
private MediaPicker mMediaPicker;
|
||||||
|
private int mOpenTarget = MEDIA_PICKER_TARGET_DEFAULT;
|
||||||
|
|
||||||
|
void setOpenTarget(final int openTarget) {
|
||||||
|
mOpenTarget = openTarget;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean show(boolean animate) {
|
public boolean show(boolean animate) {
|
||||||
@@ -393,7 +417,26 @@ public class ConversationInputManager implements ConversationInput.ConversationI
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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);
|
mMediaPicker.open(MediaPicker.MEDIA_TYPE_DEFAULT, animate);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
mOpenTarget = MEDIA_PICKER_TARGET_DEFAULT;
|
||||||
|
|
||||||
return isOpen();
|
return isOpen();
|
||||||
}
|
}
|
||||||
|
|||||||
+119
-18
@@ -26,6 +26,7 @@ import android.content.res.Resources;
|
|||||||
import android.net.Uri;
|
import android.net.Uri;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
|
import android.widget.Toast;
|
||||||
|
|
||||||
import androidx.activity.result.ActivityResultLauncher;
|
import androidx.activity.result.ActivityResultLauncher;
|
||||||
import androidx.activity.result.contract.ActivityResultContracts;
|
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.BugleActionBarActivity;
|
||||||
import com.android.messaging.ui.SnackBar;
|
import com.android.messaging.ui.SnackBar;
|
||||||
import com.android.messaging.ui.SnackBarInteraction;
|
import com.android.messaging.ui.SnackBarInteraction;
|
||||||
|
import com.android.messaging.datamodel.action.MarkAsUnreadAction;
|
||||||
import com.android.messaging.ui.UIIntents;
|
import com.android.messaging.ui.UIIntents;
|
||||||
import com.android.messaging.ui.contact.AddContactsConfirmationDialog;
|
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.ConversationListFragment.ConversationListFragmentHost;
|
||||||
import com.android.messaging.ui.conversationlist.MultiSelectActionModeCallback.SelectedConversation;
|
import com.android.messaging.ui.conversationlist.MultiSelectActionModeCallback.SelectedConversation;
|
||||||
import com.android.messaging.util.ChangeDefaultSmsAppHelper;
|
import com.android.messaging.util.ChangeDefaultSmsAppHelper;
|
||||||
@@ -61,6 +64,10 @@ import java.util.List;
|
|||||||
public abstract class AbstractConversationListActivity extends BugleActionBarActivity
|
public abstract class AbstractConversationListActivity extends BugleActionBarActivity
|
||||||
implements ConversationListFragmentHost, MultiSelectActionModeCallback.Listener {
|
implements ConversationListFragmentHost, MultiSelectActionModeCallback.Listener {
|
||||||
|
|
||||||
|
private static final UpdateDestinationBlockedAction.UpdateDestinationBlockedActionListener
|
||||||
|
NOOP_BLOCK_LISTENER =
|
||||||
|
(action, success, block, destination) -> { };
|
||||||
|
|
||||||
protected ConversationListFragment mConversationListFragment;
|
protected ConversationListFragment mConversationListFragment;
|
||||||
|
|
||||||
private ChangeDefaultSmsAppHelper mChangeDefaultSmsAppHelper;
|
private ChangeDefaultSmsAppHelper mChangeDefaultSmsAppHelper;
|
||||||
@@ -91,21 +98,27 @@ public abstract class AbstractConversationListActivity extends BugleActionBarAct
|
|||||||
}
|
}
|
||||||
|
|
||||||
protected void startMultiSelectActionMode() {
|
protected void startMultiSelectActionMode() {
|
||||||
startActionMode(new MultiSelectActionModeCallback(this, useHomeSelectionChrome()));
|
final boolean homeChrome = useHomeSelectionChrome();
|
||||||
mConversationListFragment.dismissFab();
|
if (homeChrome) {
|
||||||
if (useHomeSelectionChrome()) {
|
|
||||||
mConversationListFragment.setHomeSelectionMode(true);
|
|
||||||
hideHomeContextualActionBar();
|
hideHomeContextualActionBar();
|
||||||
|
mConversationListFragment.dismissFab(true);
|
||||||
|
}
|
||||||
|
startActionMode(new MultiSelectActionModeCallback(this, homeChrome));
|
||||||
|
if (homeChrome) {
|
||||||
|
mConversationListFragment.setHomeSelectionMode(true);
|
||||||
|
} else {
|
||||||
|
mConversationListFragment.dismissFab();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void exitMultiSelectState() {
|
public void exitMultiSelectState() {
|
||||||
|
dismissActionMode();
|
||||||
if (useHomeSelectionChrome()) {
|
if (useHomeSelectionChrome()) {
|
||||||
mConversationListFragment.setHomeSelectionMode(false);
|
mConversationListFragment.setHomeSelectionMode(false);
|
||||||
|
} else {
|
||||||
|
mConversationListFragment.updateUi();
|
||||||
}
|
}
|
||||||
mConversationListFragment.showFab();
|
mConversationListFragment.showFab();
|
||||||
dismissActionMode();
|
|
||||||
mConversationListFragment.updateUi();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected void hideHomeContextualActionBar() {
|
protected void hideHomeContextualActionBar() {
|
||||||
@@ -121,7 +134,28 @@ public abstract class AbstractConversationListActivity extends BugleActionBarAct
|
|||||||
}
|
}
|
||||||
final MultiSelectActionModeCallback callback =
|
final MultiSelectActionModeCallback callback =
|
||||||
(MultiSelectActionModeCallback) getActionModeCallback();
|
(MultiSelectActionModeCallback) getActionModeCallback();
|
||||||
onActionBarDelete(callback.getSelectedConversations());
|
final Collection<SelectedConversation> 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() {
|
protected boolean isInConversationListSelectMode() {
|
||||||
@@ -133,9 +167,29 @@ public abstract class AbstractConversationListActivity extends BugleActionBarAct
|
|||||||
return isInConversationListSelectMode();
|
return isInConversationListSelectMode();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void showHomeDeleteAlert(
|
||||||
|
final Collection<SelectedConversation> conversations) {
|
||||||
|
if (!ensureDefaultSmsAppForDelete()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
HomeDeleteAlertDialog.show(this, conversations.size(),
|
||||||
|
new HomeDeleteAlertDialog.Listener() {
|
||||||
@Override
|
@Override
|
||||||
public void onActionBarDelete(final Collection<SelectedConversation> conversations) {
|
public void onDelete() {
|
||||||
if (!PhoneUtils.getDefault().isDefaultSmsApp()) {
|
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
|
// TODO: figure out a good way to combine this with the implementation in
|
||||||
// ConversationFragment doing similar things
|
// ConversationFragment doing similar things
|
||||||
final Activity activity = this;
|
final Activity activity = this;
|
||||||
@@ -150,6 +204,60 @@ public abstract class AbstractConversationListActivity extends BugleActionBarAct
|
|||||||
getString(R.string.requires_default_sms_change_button)),
|
getString(R.string.requires_default_sms_change_button)),
|
||||||
null /* interactions */,
|
null /* interactions */,
|
||||||
null /* placement */);
|
null /* placement */);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void performDeleteConversations(
|
||||||
|
final Collection<SelectedConversation> conversations) {
|
||||||
|
performDeleteConversations(conversations, false /* reportSpam */);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void performDeleteConversations(
|
||||||
|
final Collection<SelectedConversation> conversations, final boolean reportSpam) {
|
||||||
|
final int count = conversations.size();
|
||||||
|
for (final SelectedConversation conversation : conversations) {
|
||||||
|
DeleteConversationAction.deleteConversation(
|
||||||
|
conversation.conversationId,
|
||||||
|
conversation.timestamp);
|
||||||
|
}
|
||||||
|
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<SelectedConversation> conversations) {
|
||||||
|
for (final SelectedConversation conversation : conversations) {
|
||||||
|
if (!conversation.isGroup
|
||||||
|
&& conversation.otherParticipantNormalizedDestination != null) {
|
||||||
|
UpdateDestinationBlockedAction.updateDestinationBlocked(
|
||||||
|
conversation.otherParticipantNormalizedDestination, true,
|
||||||
|
conversation.conversationId,
|
||||||
|
NOOP_BLOCK_LISTENER);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
performDeleteConversations(conversations, true /* registerSpam */);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onActionBarDelete(final Collection<SelectedConversation> conversations) {
|
||||||
|
if (!ensureDefaultSmsAppForDelete()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -158,14 +266,7 @@ public abstract class AbstractConversationListActivity extends BugleActionBarAct
|
|||||||
R.plurals.delete_conversations_confirmation_dialog_title,
|
R.plurals.delete_conversations_confirmation_dialog_title,
|
||||||
conversations.size()))
|
conversations.size()))
|
||||||
.setPositiveButton(R.string.delete_conversation_confirmation_button,
|
.setPositiveButton(R.string.delete_conversation_confirmation_button,
|
||||||
(dialog, button) -> {
|
(dialog, button) -> performDeleteConversations(conversations))
|
||||||
for (final SelectedConversation conversation : conversations) {
|
|
||||||
DeleteConversationAction.deleteConversation(
|
|
||||||
conversation.conversationId,
|
|
||||||
conversation.timestamp);
|
|
||||||
}
|
|
||||||
exitMultiSelectState();
|
|
||||||
})
|
|
||||||
.setNegativeButton(R.string.delete_conversation_decline_button, null)
|
.setNegativeButton(R.string.delete_conversation_decline_button, null)
|
||||||
.show();
|
.show();
|
||||||
}
|
}
|
||||||
@@ -256,7 +357,7 @@ public abstract class AbstractConversationListActivity extends BugleActionBarAct
|
|||||||
@Override
|
@Override
|
||||||
public void onSelectionCountChanged(final int count) {
|
public void onSelectionCountChanged(final int count) {
|
||||||
if (useHomeSelectionChrome()) {
|
if (useHomeSelectionChrome()) {
|
||||||
mConversationListFragment.updateHomeSelectionDeleteEnabled(count);
|
mConversationListFragment.updateHomeSelectionBar(count);
|
||||||
hideHomeContextualActionBar();
|
hideHomeContextualActionBar();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+26
-2
@@ -53,6 +53,7 @@ public class ConversationListAdapter
|
|||||||
private static final int VIEW_TYPE_PINNED_HEADER = 1;
|
private static final int VIEW_TYPE_PINNED_HEADER = 1;
|
||||||
private static final int VIEW_TYPE_CONVERSATION = 2;
|
private static final int VIEW_TYPE_CONVERSATION = 2;
|
||||||
private static final long PINNED_HEADER_ITEM_ID = -9001L;
|
private static final long PINNED_HEADER_ITEM_ID = -9001L;
|
||||||
|
static final Object PAYLOAD_SELECTION_CHROME = new Object();
|
||||||
|
|
||||||
private final ConversationListItemView.HostInterface mClivHostInterface;
|
private final ConversationListItemView.HostInterface mClivHostInterface;
|
||||||
private final ConversationListItemData mFilterProbe = new ConversationListItemData();
|
private final ConversationListItemData mFilterProbe = new ConversationListItemData();
|
||||||
@@ -112,6 +113,13 @@ public class ConversationListAdapter
|
|||||||
return mFilterMode;
|
return mFilterMode;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void notifySelectionChromeChanged() {
|
||||||
|
final int count = getItemCount();
|
||||||
|
if (count > 0) {
|
||||||
|
notifyItemRangeChanged(0, count, PAYLOAD_SELECTION_CHROME);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public void refreshPinOrdering() {
|
public void refreshPinOrdering() {
|
||||||
refreshPinOrderingAnimated();
|
refreshPinOrderingAnimated();
|
||||||
}
|
}
|
||||||
@@ -323,6 +331,22 @@ public class ConversationListAdapter
|
|||||||
return createConversationViewHolder(parent);
|
return createConversationViewHolder(parent);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onBindViewHolder(@NonNull final RecyclerView.ViewHolder holder,
|
||||||
|
final int position, @NonNull final List<Object> payloads) {
|
||||||
|
if (!payloads.isEmpty() && payloads.contains(PAYLOAD_SELECTION_CHROME)) {
|
||||||
|
if (holder instanceof PinnedHeaderViewHolder) {
|
||||||
|
attachPinnedStripToHeader((PinnedHeaderViewHolder) holder);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (holder instanceof ConversationListViewHolder) {
|
||||||
|
((ConversationListViewHolder) holder).mView.rebindSelectionChrome();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
onBindViewHolder(holder, position);
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onBindViewHolder(@NonNull final RecyclerView.ViewHolder holder,
|
public void onBindViewHolder(@NonNull final RecyclerView.ViewHolder holder,
|
||||||
final int position) {
|
final int position) {
|
||||||
@@ -348,8 +372,8 @@ public class ConversationListAdapter
|
|||||||
container.setLayoutParams(new RecyclerView.LayoutParams(
|
container.setLayoutParams(new RecyclerView.LayoutParams(
|
||||||
ViewGroup.LayoutParams.MATCH_PARENT,
|
ViewGroup.LayoutParams.MATCH_PARENT,
|
||||||
ViewGroup.LayoutParams.WRAP_CONTENT));
|
ViewGroup.LayoutParams.WRAP_CONTENT));
|
||||||
container.setClipChildren(true);
|
container.setClipChildren(false);
|
||||||
container.setClipToPadding(true);
|
container.setClipToPadding(false);
|
||||||
return new PinnedHeaderViewHolder(container);
|
return new PinnedHeaderViewHolder(container);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+465
-112
@@ -20,17 +20,22 @@ import android.app.Activity;
|
|||||||
import android.app.AlertDialog;
|
import android.app.AlertDialog;
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.database.Cursor;
|
import android.database.Cursor;
|
||||||
|
import android.graphics.Point;
|
||||||
import android.graphics.Rect;
|
import android.graphics.Rect;
|
||||||
|
import android.telephony.PhoneNumberUtils;
|
||||||
import android.net.Uri;
|
import android.net.Uri;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.os.Parcelable;
|
import android.os.Parcelable;
|
||||||
|
import android.text.TextUtils;
|
||||||
import android.view.LayoutInflater;
|
import android.view.LayoutInflater;
|
||||||
import android.view.Menu;
|
import android.view.Menu;
|
||||||
import android.view.MenuInflater;
|
import android.view.MenuInflater;
|
||||||
import android.view.MenuItem;
|
import android.view.MenuItem;
|
||||||
|
import android.view.Gravity;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
import android.view.ViewGroup;
|
import android.view.ViewGroup;
|
||||||
import android.view.ViewGroup.MarginLayoutParams;
|
import android.view.ViewGroup.MarginLayoutParams;
|
||||||
|
import android.widget.FrameLayout;
|
||||||
import android.view.ViewPropertyAnimator;
|
import android.view.ViewPropertyAnimator;
|
||||||
import android.view.accessibility.AccessibilityManager;
|
import android.view.accessibility.AccessibilityManager;
|
||||||
import android.widget.AbsListView;
|
import android.widget.AbsListView;
|
||||||
@@ -57,11 +62,16 @@ import com.android.messaging.datamodel.binding.BindingBase;
|
|||||||
import com.android.messaging.datamodel.data.ConversationListData;
|
import com.android.messaging.datamodel.data.ConversationListData;
|
||||||
import com.android.messaging.datamodel.data.ConversationListData.ConversationListDataListener;
|
import com.android.messaging.datamodel.data.ConversationListData.ConversationListDataListener;
|
||||||
import com.android.messaging.datamodel.data.ConversationListItemData;
|
import com.android.messaging.datamodel.data.ConversationListItemData;
|
||||||
|
import com.android.messaging.ui.ContactIconView;
|
||||||
import com.android.messaging.ui.ListEmptyView;
|
import com.android.messaging.ui.ListEmptyView;
|
||||||
import com.android.messaging.ui.SnackBarInteraction;
|
import com.android.messaging.ui.SnackBarInteraction;
|
||||||
import com.android.messaging.ui.UIIntents;
|
import com.android.messaging.ui.UIIntents;
|
||||||
import com.android.messaging.ui.home.HomeContactsFragment;
|
import com.android.messaging.ui.contact.AddContactsConfirmationDialog;
|
||||||
|
import com.android.messaging.util.AvatarUriUtil;
|
||||||
|
import com.android.messaging.util.ContactUtil;
|
||||||
|
import com.android.messaging.util.PhoneUtils;
|
||||||
import com.android.messaging.ui.home.HomePinnedStrip;
|
import com.android.messaging.ui.home.HomePinnedStrip;
|
||||||
|
import com.android.messaging.ui.home.HomeUiMotion;
|
||||||
import com.android.messaging.ui.home.PinnedConversationStore;
|
import com.android.messaging.ui.home.PinnedConversationStore;
|
||||||
import com.android.messaging.util.AccessibilityUtil;
|
import com.android.messaging.util.AccessibilityUtil;
|
||||||
import com.android.messaging.util.ImeUtil;
|
import com.android.messaging.util.ImeUtil;
|
||||||
@@ -105,19 +115,26 @@ public class ConversationListFragment extends Fragment implements ConversationLi
|
|||||||
private View mHomeBottomNav;
|
private View mHomeBottomNav;
|
||||||
private View mHomeSelectionBar;
|
private View mHomeSelectionBar;
|
||||||
private ImageButton mHomeSelectionDelete;
|
private ImageButton mHomeSelectionDelete;
|
||||||
|
private TextView mHomeReadAllButton;
|
||||||
|
private int mHomeSelectionCount;
|
||||||
private View mHomeMessagesTop;
|
private View mHomeMessagesTop;
|
||||||
private View mHomePinnedStrip;
|
private View mHomePinnedStrip;
|
||||||
private TextView mHomeEditButton;
|
private TextView mHomeEditButton;
|
||||||
private ImageButton mHomePinnedDoneButton;
|
private ImageButton mHomePinnedDoneButton;
|
||||||
private boolean mHomePinnedEditMode;
|
private boolean mHomePinnedEditMode;
|
||||||
private ViewGroup mHomeContactsHost;
|
|
||||||
private View mHomeNavMessages;
|
private View mHomeNavMessages;
|
||||||
private View mHomeNavContacts;
|
|
||||||
private ImageView mHomeNavMessagesIcon;
|
private ImageView mHomeNavMessagesIcon;
|
||||||
private ImageView mHomeNavContactsIcon;
|
|
||||||
private TextView mHomeNavMessagesLabel;
|
private TextView mHomeNavMessagesLabel;
|
||||||
private TextView mHomeNavContactsLabel;
|
|
||||||
private int mHomeTab = HOME_TAB_MESSAGES;
|
private int mHomeTab = HOME_TAB_MESSAGES;
|
||||||
|
private View mHomeListProfilePopup;
|
||||||
|
private View mHomeListProfileScrim;
|
||||||
|
private View mHomeListProfileContent;
|
||||||
|
private ContactIconView mHomeListProfileAvatar;
|
||||||
|
private View mHomeListProfileActionCall;
|
||||||
|
private View mHomeListProfileActionAddContact;
|
||||||
|
private View mHomeListProfileActions;
|
||||||
|
private ConversationListItemData mHomeListProfileData;
|
||||||
|
|
||||||
private RecyclerView mRecyclerView;
|
private RecyclerView mRecyclerView;
|
||||||
private View mHomeComposeContainer;
|
private View mHomeComposeContainer;
|
||||||
private ImageButton mStartNewConversationButton;
|
private ImageButton mStartNewConversationButton;
|
||||||
@@ -132,9 +149,7 @@ public class ConversationListFragment extends Fragment implements ConversationLi
|
|||||||
"homeSearchBarExpanded";
|
"homeSearchBarExpanded";
|
||||||
private static final String SAVED_INSTANCE_STATE_HOME_TAB = "homeTab";
|
private static final String SAVED_INSTANCE_STATE_HOME_TAB = "homeTab";
|
||||||
private static final String SAVED_INSTANCE_STATE_PINNED_EDIT = "homePinnedEdit";
|
private static final String SAVED_INSTANCE_STATE_PINNED_EDIT = "homePinnedEdit";
|
||||||
private static final String HOME_CONTACTS_FRAGMENT_TAG = "home_contacts";
|
|
||||||
private static final int HOME_TAB_MESSAGES = 0;
|
private static final int HOME_TAB_MESSAGES = 0;
|
||||||
private static final int HOME_TAB_CONTACTS = 1;
|
|
||||||
private static final int SEARCH_NEWEST_EDGE_SLOP_DP = 48;
|
private static final int SEARCH_NEWEST_EDGE_SLOP_DP = 48;
|
||||||
private static final int SEARCH_BAR_ANIM_MS = 280;
|
private static final int SEARCH_BAR_ANIM_MS = 280;
|
||||||
private static final int SEARCH_BAR_SLIDE_FALLBACK_DP = 72;
|
private static final int SEARCH_BAR_SLIDE_FALLBACK_DP = 72;
|
||||||
@@ -147,8 +162,19 @@ public class ConversationListFragment extends Fragment implements ConversationLi
|
|||||||
private final ConversationListItemData mPinnedRowProbe = new ConversationListItemData();
|
private final ConversationListItemData mPinnedRowProbe = new ConversationListItemData();
|
||||||
private final HomePinnedStrip.Listener mPinnedStripListener = new HomePinnedStrip.Listener() {
|
private final HomePinnedStrip.Listener mPinnedStripListener = new HomePinnedStrip.Listener() {
|
||||||
@Override
|
@Override
|
||||||
public void onPinnedConversationClicked(final String conversationId) {
|
public void onPinnedProfileClicked(final String conversationId, final View avatarAnchor) {
|
||||||
openConversationById(conversationId);
|
final ConversationListItemData data = loadConversationListItemData(conversationId);
|
||||||
|
if (data != null) {
|
||||||
|
showHomeListProfilePopup(data, avatarAnchor);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onPinnedConversationOpen(final String conversationId) {
|
||||||
|
final ConversationListItemData data = loadConversationListItemData(conversationId);
|
||||||
|
if (data != null && mHost != null) {
|
||||||
|
mHost.onConversationClick(mListBinding.getData(), data, false, null);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -254,8 +280,8 @@ public class ConversationListFragment extends Fragment implements ConversationLi
|
|||||||
mRecyclerView.setLayoutManager(manager);
|
mRecyclerView.setLayoutManager(manager);
|
||||||
mRecyclerView.setHasFixedSize(false);
|
mRecyclerView.setHasFixedSize(false);
|
||||||
final DefaultItemAnimator itemAnimator = new DefaultItemAnimator();
|
final DefaultItemAnimator itemAnimator = new DefaultItemAnimator();
|
||||||
itemAnimator.setMoveDuration(220);
|
itemAnimator.setMoveDuration(240);
|
||||||
itemAnimator.setChangeDuration(180);
|
itemAnimator.setChangeDuration(260);
|
||||||
mRecyclerView.setItemAnimator(itemAnimator);
|
mRecyclerView.setItemAnimator(itemAnimator);
|
||||||
mHomePinnedStrip = inflater.inflate(R.layout.home_pinned_strip, rootView, false);
|
mHomePinnedStrip = inflater.inflate(R.layout.home_pinned_strip, rootView, false);
|
||||||
mAdapter.setPinnedStripRoot(mHomePinnedStrip);
|
mAdapter.setPinnedStripRoot(mHomePinnedStrip);
|
||||||
@@ -303,6 +329,7 @@ public class ConversationListFragment extends Fragment implements ConversationLi
|
|||||||
mHomeBottomNav = rootView.findViewById(R.id.home_bottom_nav);
|
mHomeBottomNav = rootView.findViewById(R.id.home_bottom_nav);
|
||||||
mHomeSelectionBar = rootView.findViewById(R.id.home_selection_bar);
|
mHomeSelectionBar = rootView.findViewById(R.id.home_selection_bar);
|
||||||
mHomeSelectionDelete = rootView.findViewById(R.id.home_selection_delete);
|
mHomeSelectionDelete = rootView.findViewById(R.id.home_selection_delete);
|
||||||
|
mHomeReadAllButton = rootView.findViewById(R.id.home_read_all_button);
|
||||||
|
|
||||||
mHomeComposeContainer = rootView.findViewById(R.id.home_compose_container);
|
mHomeComposeContainer = rootView.findViewById(R.id.home_compose_container);
|
||||||
mStartNewConversationButton = rootView.findViewById(R.id.home_nav_search);
|
mStartNewConversationButton = rootView.findViewById(R.id.home_nav_search);
|
||||||
@@ -313,27 +340,19 @@ public class ConversationListFragment extends Fragment implements ConversationLi
|
|||||||
hideHomeChrome(rootView);
|
hideHomeChrome(rootView);
|
||||||
} else {
|
} else {
|
||||||
if (mStartNewConversationButton != null) {
|
if (mStartNewConversationButton != null) {
|
||||||
mStartNewConversationButton.setOnClickListener(v -> {
|
mStartNewConversationButton.setOnClickListener(v -> launchHomeSubScreen(
|
||||||
if (mHost != null) {
|
() -> UIIntents.get().launchHomeComposeActivity(requireActivity()),
|
||||||
mHost.onCreateConversationClick();
|
R.anim.home_sheet_open));
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
setupHomeChrome(rootView);
|
setupHomeChrome(rootView);
|
||||||
mHomeMessagesTop = rootView.findViewById(R.id.home_messages_top);
|
mHomeMessagesTop = rootView.findViewById(R.id.home_messages_top);
|
||||||
mHomeContactsHost = rootView.findViewById(R.id.home_contacts_host);
|
|
||||||
mHomeNavMessages = rootView.findViewById(R.id.home_nav_messages);
|
mHomeNavMessages = rootView.findViewById(R.id.home_nav_messages);
|
||||||
mHomeNavContacts = rootView.findViewById(R.id.home_nav_contacts);
|
|
||||||
bindHomeNavTab(mHomeNavMessages, R.drawable.ic_home_nav_messages,
|
bindHomeNavTab(mHomeNavMessages, R.drawable.ic_home_nav_messages,
|
||||||
R.string.home_nav_messages);
|
R.string.home_nav_messages);
|
||||||
bindHomeNavTab(mHomeNavContacts, R.drawable.ic_home_nav_contacts,
|
|
||||||
R.string.home_nav_contacts);
|
|
||||||
bindHomeNavTab(rootView.findViewById(R.id.home_nav_automate),
|
bindHomeNavTab(rootView.findViewById(R.id.home_nav_automate),
|
||||||
R.drawable.ic_home_nav_automate, R.string.home_nav_automate);
|
R.drawable.ic_home_nav_automate, R.string.home_nav_automate);
|
||||||
mHomeNavMessagesIcon = mHomeNavMessages.findViewById(R.id.home_nav_tab_icon);
|
mHomeNavMessagesIcon = mHomeNavMessages.findViewById(R.id.home_nav_tab_icon);
|
||||||
mHomeNavContactsIcon = mHomeNavContacts.findViewById(R.id.home_nav_tab_icon);
|
|
||||||
mHomeNavMessagesLabel = mHomeNavMessages.findViewById(R.id.home_nav_tab_label);
|
mHomeNavMessagesLabel = mHomeNavMessages.findViewById(R.id.home_nav_tab_label);
|
||||||
mHomeNavContactsLabel = mHomeNavContacts.findViewById(R.id.home_nav_tab_label);
|
|
||||||
if (savedInstanceState != null) {
|
if (savedInstanceState != null) {
|
||||||
mHomeTab = savedInstanceState.getInt(SAVED_INSTANCE_STATE_HOME_TAB,
|
mHomeTab = savedInstanceState.getInt(SAVED_INSTANCE_STATE_HOME_TAB,
|
||||||
HOME_TAB_MESSAGES);
|
HOME_TAB_MESSAGES);
|
||||||
@@ -370,7 +389,6 @@ public class ConversationListFragment extends Fragment implements ConversationLi
|
|||||||
|
|
||||||
private void hideHomeChrome(final View rootView) {
|
private void hideHomeChrome(final View rootView) {
|
||||||
setGone(rootView, R.id.home_messages_top);
|
setGone(rootView, R.id.home_messages_top);
|
||||||
setGone(rootView, R.id.home_contacts_host);
|
|
||||||
setGone(rootView, R.id.home_search_bar_container);
|
setGone(rootView, R.id.home_search_bar_container);
|
||||||
setGone(rootView, R.id.home_bottom_nav);
|
setGone(rootView, R.id.home_bottom_nav);
|
||||||
setGone(rootView, R.id.home_selection_bar);
|
setGone(rootView, R.id.home_selection_bar);
|
||||||
@@ -538,7 +556,6 @@ public class ConversationListFragment extends Fragment implements ConversationLi
|
|||||||
final View searchBarContainer = rootView.findViewById(R.id.home_search_bar_container);
|
final View searchBarContainer = rootView.findViewById(R.id.home_search_bar_container);
|
||||||
final View searchBar = rootView.findViewById(R.id.home_search_bar);
|
final View searchBar = rootView.findViewById(R.id.home_search_bar);
|
||||||
final View navMessages = rootView.findViewById(R.id.home_nav_messages);
|
final View navMessages = rootView.findViewById(R.id.home_nav_messages);
|
||||||
final View navContacts = rootView.findViewById(R.id.home_nav_contacts);
|
|
||||||
final View navAutomate = rootView.findViewById(R.id.home_nav_automate);
|
final View navAutomate = rootView.findViewById(R.id.home_nav_automate);
|
||||||
|
|
||||||
if (editButton instanceof TextView) {
|
if (editButton instanceof TextView) {
|
||||||
@@ -549,11 +566,10 @@ public class ConversationListFragment extends Fragment implements ConversationLi
|
|||||||
if (filterButton != null) {
|
if (filterButton != null) {
|
||||||
filterButton.setOnClickListener(this::showHomeFilterPopover);
|
filterButton.setOnClickListener(this::showHomeFilterPopover);
|
||||||
}
|
}
|
||||||
final View.OnClickListener openCompose = v ->
|
|
||||||
UIIntents.get().launchCreateNewConversationActivity(requireActivity(), null);
|
|
||||||
mSearchBar = searchBarContainer != null ? searchBarContainer : searchBar;
|
mSearchBar = searchBarContainer != null ? searchBarContainer : searchBar;
|
||||||
if (searchBar != null) {
|
if (searchBar != null) {
|
||||||
searchBar.setOnClickListener(openCompose);
|
searchBar.setOnClickListener(v -> launchHomeSubScreen(
|
||||||
|
() -> UIIntents.get().launchHomeSearchActivity(requireActivity())));
|
||||||
}
|
}
|
||||||
if (mSearchBar != null) {
|
if (mSearchBar != null) {
|
||||||
setupSearchBarScroll(mSearchBar);
|
setupSearchBarScroll(mSearchBar);
|
||||||
@@ -562,9 +578,6 @@ public class ConversationListFragment extends Fragment implements ConversationLi
|
|||||||
if (navMessages != null) {
|
if (navMessages != null) {
|
||||||
navMessages.setOnClickListener(v -> selectHomeTab(HOME_TAB_MESSAGES));
|
navMessages.setOnClickListener(v -> selectHomeTab(HOME_TAB_MESSAGES));
|
||||||
}
|
}
|
||||||
if (navContacts != null) {
|
|
||||||
navContacts.setOnClickListener(v -> selectHomeTab(HOME_TAB_CONTACTS));
|
|
||||||
}
|
|
||||||
if (navAutomate != null) {
|
if (navAutomate != null) {
|
||||||
navAutomate.setOnClickListener(v ->
|
navAutomate.setOnClickListener(v ->
|
||||||
Toast.makeText(requireContext(), R.string.home_automate_coming_soon,
|
Toast.makeText(requireContext(), R.string.home_automate_coming_soon,
|
||||||
@@ -579,9 +592,8 @@ public class ConversationListFragment extends Fragment implements ConversationLi
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
final View readAll = rootView.findViewById(R.id.home_read_all_button);
|
if (mHomeReadAllButton != null) {
|
||||||
if (readAll != null) {
|
mHomeReadAllButton.setOnClickListener(v -> onHomeReadAllOrUnreadClick());
|
||||||
readAll.setOnClickListener(v -> markAllVisibleConversationsRead());
|
|
||||||
}
|
}
|
||||||
if (mHomeSelectionDelete != null) {
|
if (mHomeSelectionDelete != null) {
|
||||||
mHomeSelectionDelete.setOnClickListener(v -> {
|
mHomeSelectionDelete.setOnClickListener(v -> {
|
||||||
@@ -591,36 +603,296 @@ public class ConversationListFragment extends Fragment implements ConversationLi
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
setupHomeListProfilePopup(rootView);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void setupHomeListProfilePopup(final View rootView) {
|
||||||
|
mHomeListProfilePopup = rootView.findViewById(R.id.home_list_profile_popup);
|
||||||
|
if (mHomeListProfilePopup == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
mHomeListProfileScrim = mHomeListProfilePopup.findViewById(R.id.home_list_profile_scrim);
|
||||||
|
mHomeListProfileContent = mHomeListProfilePopup.findViewById(R.id.home_list_profile_content);
|
||||||
|
mHomeListProfileAvatar = mHomeListProfilePopup.findViewById(R.id.home_list_profile_avatar);
|
||||||
|
mHomeListProfileActionCall =
|
||||||
|
mHomeListProfilePopup.findViewById(R.id.home_list_profile_action_call);
|
||||||
|
mHomeListProfileActionAddContact =
|
||||||
|
mHomeListProfilePopup.findViewById(R.id.home_list_profile_action_add_contact);
|
||||||
|
mHomeListProfileActions =
|
||||||
|
mHomeListProfilePopup.findViewById(R.id.home_list_profile_actions);
|
||||||
|
|
||||||
|
final View dismissTarget = mHomeListProfileScrim != null
|
||||||
|
? mHomeListProfileScrim : mHomeListProfilePopup;
|
||||||
|
dismissTarget.setOnClickListener(v -> dismissHomeListProfilePopup(true));
|
||||||
|
|
||||||
|
final View message = mHomeListProfilePopup.findViewById(R.id.home_list_profile_action_message);
|
||||||
|
if (message != null) {
|
||||||
|
message.setOnClickListener(v -> {
|
||||||
|
final ConversationListItemData data = mHomeListProfileData;
|
||||||
|
dismissHomeListProfilePopup(false);
|
||||||
|
if (data != null && mHost != null) {
|
||||||
|
mHost.onConversationClick(mListBinding.getData(), data, false, null);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (mHomeListProfileActionCall != null) {
|
||||||
|
mHomeListProfileActionCall.setOnClickListener(v -> placeListProfileCall());
|
||||||
|
}
|
||||||
|
if (mHomeListProfileActionAddContact != null) {
|
||||||
|
mHomeListProfileActionAddContact.setOnClickListener(v -> addListProfileContact());
|
||||||
|
}
|
||||||
|
final View info = mHomeListProfilePopup.findViewById(R.id.home_list_profile_action_info);
|
||||||
|
if (info != null) {
|
||||||
|
info.setOnClickListener(v -> openListProfileDetails());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void showHomeListProfilePopup(final ConversationListItemData data,
|
||||||
|
final View anchorView) {
|
||||||
|
if (mHomeListProfilePopup == null || data == null || mArchiveMode || mForwardMessageMode
|
||||||
|
|| isSelectionMode()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
dismissHomePopoverImmediate();
|
||||||
|
final String previousProfileId = mHomeListProfileData != null
|
||||||
|
? mHomeListProfileData.getConversationId() : null;
|
||||||
|
final boolean sameProfile = data.getConversationId() != null
|
||||||
|
&& data.getConversationId().equals(previousProfileId)
|
||||||
|
&& mHomeListProfilePopup.getVisibility() == View.VISIBLE;
|
||||||
|
if (sameProfile) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
mHomeListProfileData = data;
|
||||||
|
Uri iconUri = null;
|
||||||
|
if (data.getIcon() != null) {
|
||||||
|
iconUri = Uri.parse(data.getIcon());
|
||||||
|
}
|
||||||
|
if (mHomeListProfileAvatar != null) {
|
||||||
|
mHomeListProfileAvatar.setImageClickHandlerDisabled(true);
|
||||||
|
mHomeListProfileAvatar.setImageResourceUri(iconUri, data.getParticipantContactId(),
|
||||||
|
data.getParticipantLookupKey(), data.getOtherParticipantNormalizedDestination());
|
||||||
|
}
|
||||||
|
final boolean isGroup = data.getIsGroup();
|
||||||
|
final boolean canCall = !isGroup && PhoneUtils.getDefault().isVoiceCapable()
|
||||||
|
&& !TextUtils.isEmpty(data.getOtherParticipantNormalizedDestination());
|
||||||
|
if (mHomeListProfileActionCall != null) {
|
||||||
|
mHomeListProfileActionCall.setVisibility(canCall ? View.VISIBLE : View.GONE);
|
||||||
|
}
|
||||||
|
final boolean canAddContact = !isGroup
|
||||||
|
&& !TextUtils.isEmpty(data.getOtherParticipantNormalizedDestination())
|
||||||
|
&& !ContactUtil.isParticipantInAddressBook(requireContext(),
|
||||||
|
data.getParticipantContactId(), data.getParticipantLookupKey(),
|
||||||
|
data.getOtherParticipantNormalizedDestination());
|
||||||
|
if (mHomeListProfileActionAddContact != null) {
|
||||||
|
mHomeListProfileActionAddContact.setVisibility(
|
||||||
|
canAddContact ? View.VISIBLE : View.GONE);
|
||||||
|
}
|
||||||
|
updateListProfileActionsPillWidth();
|
||||||
|
if (mHomeListProfileScrim != null) {
|
||||||
|
mHomeListProfileScrim.setAlpha(0f);
|
||||||
|
}
|
||||||
|
mHomeListProfileContent.setAlpha(0f);
|
||||||
|
mHomeListProfilePopup.setVisibility(View.INVISIBLE);
|
||||||
|
positionListProfilePopup(anchorView, () -> {
|
||||||
|
mHomeListProfilePopup.setVisibility(View.VISIBLE);
|
||||||
|
HomeUiMotion.listProfilePopupEnter(mHomeListProfileScrim, mHomeListProfileContent,
|
||||||
|
anchorView);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private void updateListProfileActionsPillWidth() {
|
||||||
|
if (mHomeListProfileActions == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
int visibleActions = 0;
|
||||||
|
final View message = mHomeListProfilePopup.findViewById(
|
||||||
|
R.id.home_list_profile_action_message);
|
||||||
|
final View info = mHomeListProfilePopup.findViewById(R.id.home_list_profile_action_info);
|
||||||
|
for (final View action : new View[] { message, mHomeListProfileActionCall,
|
||||||
|
mHomeListProfileActionAddContact, info }) {
|
||||||
|
if (action != null && action.getVisibility() == View.VISIBLE) {
|
||||||
|
visibleActions++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
final android.view.ViewGroup.LayoutParams layoutParams =
|
||||||
|
mHomeListProfileActions.getLayoutParams();
|
||||||
|
final int buttonSize = getResources().getDimensionPixelSize(
|
||||||
|
R.dimen.home_list_profile_action_button_size);
|
||||||
|
final int horizontalPadding = getResources().getDimensionPixelSize(
|
||||||
|
R.dimen.home_list_profile_actions_padding_h);
|
||||||
|
layoutParams.width = visibleActions * buttonSize + horizontalPadding * 2;
|
||||||
|
mHomeListProfileActions.setLayoutParams(layoutParams);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void positionListProfilePopup(final View anchorView, final Runnable onPositioned) {
|
||||||
|
if (mHomeListProfileContent == null || mHomeListProfilePopup == null) {
|
||||||
|
if (onPositioned != null) {
|
||||||
|
onPositioned.run();
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
resetListProfilePopupPosition();
|
||||||
|
mHomeListProfileContent.post(() -> {
|
||||||
|
if (anchorView != null) {
|
||||||
|
final int contentWidth = measureListProfilePopupContentWidth();
|
||||||
|
final int contentHeight = measureListProfilePopupContentHeight();
|
||||||
|
final int[] anchorInWindow = new int[2];
|
||||||
|
final int[] popupInWindow = new int[2];
|
||||||
|
anchorView.getLocationInWindow(anchorInWindow);
|
||||||
|
mHomeListProfilePopup.getLocationInWindow(popupInWindow);
|
||||||
|
final int screenPadding = getResources().getDimensionPixelSize(
|
||||||
|
R.dimen.looply_screen_padding);
|
||||||
|
final int gap = getResources().getDimensionPixelSize(
|
||||||
|
R.dimen.home_list_profile_popup_gap_below_avatar);
|
||||||
|
|
||||||
|
final float anchorCenterX = anchorInWindow[0] + anchorView.getWidth() * 0.5f;
|
||||||
|
float left = anchorCenterX - popupInWindow[0] - contentWidth * 0.5f;
|
||||||
|
final float maxLeft = mHomeListProfilePopup.getWidth() - contentWidth
|
||||||
|
- screenPadding;
|
||||||
|
left = Math.max(screenPadding, Math.min(left, maxLeft));
|
||||||
|
|
||||||
|
float top = anchorInWindow[1] - popupInWindow[1] + anchorView.getHeight() + gap;
|
||||||
|
final int maxTop = mHomeListProfilePopup.getHeight() - contentHeight
|
||||||
|
- screenPadding;
|
||||||
|
if (top > maxTop) {
|
||||||
|
top = anchorInWindow[1] - popupInWindow[1] - contentHeight - gap;
|
||||||
|
}
|
||||||
|
top = Math.max(screenPadding, Math.min(top, maxTop));
|
||||||
|
|
||||||
|
final FrameLayout.LayoutParams layoutParams = ensureListProfileContentLayoutParams();
|
||||||
|
layoutParams.gravity = Gravity.TOP | Gravity.START;
|
||||||
|
layoutParams.leftMargin = Math.round(left);
|
||||||
|
layoutParams.topMargin = Math.round(top);
|
||||||
|
mHomeListProfileContent.setLayoutParams(layoutParams);
|
||||||
|
}
|
||||||
|
mHomeListProfileContent.setTranslationX(0f);
|
||||||
|
mHomeListProfileContent.setTranslationY(0f);
|
||||||
|
if (onPositioned != null) {
|
||||||
|
mHomeListProfileContent.post(onPositioned);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private FrameLayout.LayoutParams ensureListProfileContentLayoutParams() {
|
||||||
|
ViewGroup.LayoutParams params = mHomeListProfileContent.getLayoutParams();
|
||||||
|
if (!(params instanceof FrameLayout.LayoutParams)) {
|
||||||
|
params = new FrameLayout.LayoutParams(
|
||||||
|
ViewGroup.LayoutParams.WRAP_CONTENT,
|
||||||
|
ViewGroup.LayoutParams.WRAP_CONTENT);
|
||||||
|
mHomeListProfileContent.setLayoutParams(params);
|
||||||
|
}
|
||||||
|
return (FrameLayout.LayoutParams) params;
|
||||||
|
}
|
||||||
|
|
||||||
|
private int measureListProfilePopupContentWidth() {
|
||||||
|
if (mHomeListProfileContent.getWidth() > 0) {
|
||||||
|
return mHomeListProfileContent.getWidth();
|
||||||
|
}
|
||||||
|
final int widthSpec = View.MeasureSpec.makeMeasureSpec(
|
||||||
|
mHomeListProfilePopup.getWidth(), View.MeasureSpec.AT_MOST);
|
||||||
|
final int heightSpec = View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED);
|
||||||
|
mHomeListProfileContent.measure(widthSpec, heightSpec);
|
||||||
|
return mHomeListProfileContent.getMeasuredWidth();
|
||||||
|
}
|
||||||
|
|
||||||
|
private int measureListProfilePopupContentHeight() {
|
||||||
|
if (mHomeListProfileContent.getHeight() > 0) {
|
||||||
|
return mHomeListProfileContent.getHeight();
|
||||||
|
}
|
||||||
|
final int widthSpec = View.MeasureSpec.makeMeasureSpec(
|
||||||
|
mHomeListProfilePopup.getWidth(), View.MeasureSpec.AT_MOST);
|
||||||
|
final int heightSpec = View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED);
|
||||||
|
mHomeListProfileContent.measure(widthSpec, heightSpec);
|
||||||
|
return mHomeListProfileContent.getMeasuredHeight();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void resetListProfilePopupPosition() {
|
||||||
|
if (mHomeListProfileContent == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
mHomeListProfileContent.setX(0f);
|
||||||
|
mHomeListProfileContent.setY(0f);
|
||||||
|
mHomeListProfileContent.setTranslationX(0f);
|
||||||
|
mHomeListProfileContent.setTranslationY(0f);
|
||||||
|
final FrameLayout.LayoutParams layoutParams = ensureListProfileContentLayoutParams();
|
||||||
|
layoutParams.gravity = Gravity.TOP | Gravity.START;
|
||||||
|
layoutParams.leftMargin = 0;
|
||||||
|
layoutParams.topMargin = 0;
|
||||||
|
mHomeListProfileContent.setLayoutParams(layoutParams);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void dismissHomeListProfilePopup(final boolean animate) {
|
||||||
|
if (mHomeListProfilePopup == null
|
||||||
|
|| mHomeListProfilePopup.getVisibility() != View.VISIBLE) {
|
||||||
|
mHomeListProfileData = null;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
final Runnable finish = () -> {
|
||||||
|
mHomeListProfilePopup.setVisibility(View.GONE);
|
||||||
|
resetListProfilePopupPosition();
|
||||||
|
mHomeListProfileData = null;
|
||||||
|
};
|
||||||
|
if (animate) {
|
||||||
|
HomeUiMotion.listProfilePopupExit(mHomeListProfileScrim, mHomeListProfileContent,
|
||||||
|
finish);
|
||||||
|
} else {
|
||||||
|
finish.run();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void placeListProfileCall() {
|
||||||
|
if (mHomeListProfileData == null || getActivity() == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
final String phoneNumber = mHomeListProfileData.getOtherParticipantNormalizedDestination();
|
||||||
|
if (TextUtils.isEmpty(phoneNumber)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (PhoneNumberUtils.isEmergencyNumber(phoneNumber)) {
|
||||||
|
UiUtils.showToast(R.string.disallow_emergency_call);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
dismissHomeListProfilePopup(false);
|
||||||
|
final Point center = new Point(
|
||||||
|
getResources().getDisplayMetrics().widthPixels / 2,
|
||||||
|
getResources().getDisplayMetrics().heightPixels / 2);
|
||||||
|
UIIntents.get().launchPhoneCallActivity(getActivity(), phoneNumber, center);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void openListProfileDetails() {
|
||||||
|
final ConversationListItemData data = mHomeListProfileData;
|
||||||
|
dismissHomeListProfilePopup(false);
|
||||||
|
if (data == null || getActivity() == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!data.getIsGroup() && ContactUtil.openContactDetail(requireContext(),
|
||||||
|
data.getParticipantContactId(), data.getParticipantLookupKey(),
|
||||||
|
data.getOtherParticipantNormalizedDestination())) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
UIIntents.get().launchHomeProfileActivity(getActivity(), data.getConversationId());
|
||||||
|
}
|
||||||
|
|
||||||
|
private void addListProfileContact() {
|
||||||
|
if (mHomeListProfileData == null || getActivity() == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
final String destination = mHomeListProfileData.getOtherParticipantNormalizedDestination();
|
||||||
|
if (TextUtils.isEmpty(destination)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
final Uri avatarUri = AvatarUriUtil.createAvatarUri(null, mHomeListProfileData.getName(),
|
||||||
|
destination, mHomeListProfileData.getParticipantLookupKey());
|
||||||
|
dismissHomeListProfilePopup(false);
|
||||||
|
new AddContactsConfirmationDialog(getActivity(), avatarUri, destination).show();
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setHomeSelectionMode(final boolean enabled) {
|
public void setHomeSelectionMode(final boolean enabled) {
|
||||||
if (mArchiveMode || mForwardMessageMode || mRootView == null) {
|
if (mArchiveMode || mForwardMessageMode || mRootView == null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (mHomeHeader != null) {
|
|
||||||
mHomeHeader.setVisibility(enabled ? View.GONE : View.VISIBLE);
|
|
||||||
}
|
|
||||||
if (mHomeSelectionHeader != null) {
|
|
||||||
mHomeSelectionHeader.setVisibility(enabled ? View.VISIBLE : View.GONE);
|
|
||||||
}
|
|
||||||
if (enabled && mHomeBottomNav != null) {
|
|
||||||
mHomeBottomNav.setVisibility(View.GONE);
|
|
||||||
}
|
|
||||||
if (mHomeSelectionBar != null) {
|
|
||||||
mHomeSelectionBar.setVisibility(enabled ? View.VISIBLE : View.GONE);
|
|
||||||
}
|
|
||||||
if (enabled) {
|
|
||||||
setPinnedEditMode(false);
|
|
||||||
updateHomeComposeButtonVisibility();
|
|
||||||
refreshPinnedStrip();
|
|
||||||
if (mHomeMessagesTop != null) {
|
|
||||||
mHomeMessagesTop.bringToFront();
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
updateHomeMainNavVisibility();
|
|
||||||
refreshPinnedStrip();
|
|
||||||
}
|
|
||||||
if (enabled) {
|
if (enabled) {
|
||||||
|
setPinnedEditMode(false, false);
|
||||||
mSearchBarShown = false;
|
mSearchBarShown = false;
|
||||||
cancelSearchBarAnimation();
|
cancelSearchBarAnimation();
|
||||||
if (mSearchBar != null) {
|
if (mSearchBar != null) {
|
||||||
@@ -630,8 +902,27 @@ public class ConversationListFragment extends Fragment implements ConversationLi
|
|||||||
}
|
}
|
||||||
updateListPaddingForSearchBar();
|
updateListPaddingForSearchBar();
|
||||||
}
|
}
|
||||||
updateHomeSelectionDeleteEnabled(0);
|
updateHomeSelectionBar(0);
|
||||||
updateUi();
|
updateHomeComposeButtonVisibility();
|
||||||
|
// Both headers live in the same vertical container, so they must never be visible at
|
||||||
|
// once or the list below them jumps by a full header height.
|
||||||
|
HomeUiMotion.replaceHeaderInstant(
|
||||||
|
enabled ? mHomeHeader : mHomeSelectionHeader,
|
||||||
|
enabled ? mHomeSelectionHeader : mHomeHeader);
|
||||||
|
refreshListSelectionChrome();
|
||||||
|
HomeUiMotion.swapBottomDock(mHomeBottomNav, mHomeSelectionBar, enabled, () -> {
|
||||||
|
if (!enabled) {
|
||||||
|
updateHomeMainNavVisibility(false);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Rebinds only the per-row selection affordances, leaving avatars and text untouched. */
|
||||||
|
private void refreshListSelectionChrome() {
|
||||||
|
if (mRecyclerView == null || mAdapter == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
mAdapter.notifySelectionChromeChanged();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void selectHomeTab(final int tab) {
|
private void selectHomeTab(final int tab) {
|
||||||
@@ -646,12 +937,6 @@ public class ConversationListFragment extends Fragment implements ConversationLi
|
|||||||
if (mHomeMessagesTop != null) {
|
if (mHomeMessagesTop != null) {
|
||||||
mHomeMessagesTop.setVisibility(showMessages ? View.VISIBLE : View.GONE);
|
mHomeMessagesTop.setVisibility(showMessages ? View.VISIBLE : View.GONE);
|
||||||
}
|
}
|
||||||
if (mHomeContactsHost != null) {
|
|
||||||
mHomeContactsHost.setVisibility(showMessages ? View.GONE : View.VISIBLE);
|
|
||||||
if (!showMessages) {
|
|
||||||
ensureHomeContactsFragment();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (!showMessages) {
|
if (!showMessages) {
|
||||||
mSearchBarShown = false;
|
mSearchBarShown = false;
|
||||||
cancelSearchBarAnimation();
|
cancelSearchBarAnimation();
|
||||||
@@ -676,10 +961,19 @@ public class ConversationListFragment extends Fragment implements ConversationLi
|
|||||||
|
|
||||||
/** Main tabs show nav; selection mode and other activities hide it ({@link #onPause}). */
|
/** Main tabs show nav; selection mode and other activities hide it ({@link #onPause}). */
|
||||||
private void updateHomeMainNavVisibility() {
|
private void updateHomeMainNavVisibility() {
|
||||||
|
updateHomeMainNavVisibility(isSelectionMode());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param homeSelectionChromeActive home selection UI (dock + selection bar), not legacy
|
||||||
|
* action mode alone
|
||||||
|
*/
|
||||||
|
private void updateHomeMainNavVisibility(final boolean homeSelectionChromeActive) {
|
||||||
if (mHomeBottomNav == null || mArchiveMode || mForwardMessageMode) {
|
if (mHomeBottomNav == null || mArchiveMode || mForwardMessageMode) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (isSelectionMode()) {
|
final boolean hideForSelection = homeSelectionChromeActive || isSelectionMode();
|
||||||
|
if (hideForSelection) {
|
||||||
mHomeBottomNav.setVisibility(View.GONE);
|
mHomeBottomNav.setVisibility(View.GONE);
|
||||||
} else {
|
} else {
|
||||||
mHomeBottomNav.setVisibility(isResumed() ? View.VISIBLE : View.GONE);
|
mHomeBottomNav.setVisibility(isResumed() ? View.VISIBLE : View.GONE);
|
||||||
@@ -705,6 +999,10 @@ public class ConversationListFragment extends Fragment implements ConversationLi
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void launchHomeSubScreen(final Runnable action) {
|
private void launchHomeSubScreen(final Runnable action) {
|
||||||
|
launchHomeSubScreen(action, R.anim.home_activity_open);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void launchHomeSubScreen(final Runnable action, final int enterAnim) {
|
||||||
if (mHomeBottomNav != null) {
|
if (mHomeBottomNav != null) {
|
||||||
mHomeBottomNav.setVisibility(View.GONE);
|
mHomeBottomNav.setVisibility(View.GONE);
|
||||||
}
|
}
|
||||||
@@ -712,25 +1010,15 @@ public class ConversationListFragment extends Fragment implements ConversationLi
|
|||||||
mHomeComposeContainer.setVisibility(View.GONE);
|
mHomeComposeContainer.setVisibility(View.GONE);
|
||||||
}
|
}
|
||||||
action.run();
|
action.run();
|
||||||
|
final Activity activity = getActivity();
|
||||||
|
if (activity != null) {
|
||||||
|
activity.overridePendingTransition(enterAnim, R.anim.home_activity_hold);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void ensureHomeContactsFragment() {
|
|
||||||
if (mHomeContactsHost == null
|
|
||||||
|| getChildFragmentManager().findFragmentById(R.id.home_contacts_host) != null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
getChildFragmentManager().beginTransaction()
|
|
||||||
.replace(R.id.home_contacts_host, new HomeContactsFragment(),
|
|
||||||
HOME_CONTACTS_FRAGMENT_TAG)
|
|
||||||
.commit();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void updateHomeNavHighlight() {
|
private void updateHomeNavHighlight() {
|
||||||
final boolean messagesActive = mHomeTab == HOME_TAB_MESSAGES;
|
|
||||||
applyHomeNavTabStyle(mHomeNavMessages, mHomeNavMessagesIcon, mHomeNavMessagesLabel,
|
applyHomeNavTabStyle(mHomeNavMessages, mHomeNavMessagesIcon, mHomeNavMessagesLabel,
|
||||||
messagesActive);
|
mHomeTab == HOME_TAB_MESSAGES);
|
||||||
applyHomeNavTabStyle(mHomeNavContacts, mHomeNavContactsIcon, mHomeNavContactsLabel,
|
|
||||||
!messagesActive);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void applyHomeNavTabStyle(final View tab, final ImageView icon, final TextView label,
|
private void applyHomeNavTabStyle(final View tab, final ImageView icon, final TextView label,
|
||||||
@@ -755,7 +1043,13 @@ public class ConversationListFragment extends Fragment implements ConversationLi
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void updateHomeSelectionDeleteEnabled(final int selectedCount) {
|
public void updateHomeSelectionBar(final int selectedCount) {
|
||||||
|
mHomeSelectionCount = selectedCount;
|
||||||
|
if (mHomeReadAllButton != null) {
|
||||||
|
mHomeReadAllButton.setText(selectedCount > 0
|
||||||
|
? R.string.home_mark_unread
|
||||||
|
: R.string.home_read_all);
|
||||||
|
}
|
||||||
if (mHomeSelectionDelete == null) {
|
if (mHomeSelectionDelete == null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -763,6 +1057,19 @@ public class ConversationListFragment extends Fragment implements ConversationLi
|
|||||||
mHomeSelectionDelete.setEnabled(enabled);
|
mHomeSelectionDelete.setEnabled(enabled);
|
||||||
mHomeSelectionDelete.setAlpha(1f);
|
mHomeSelectionDelete.setAlpha(1f);
|
||||||
mHomeSelectionDelete.setImageAlpha(enabled ? 255 : 102);
|
mHomeSelectionDelete.setImageAlpha(enabled ? 255 : 102);
|
||||||
|
final int tintColor = ContextCompat.getColor(requireContext(),
|
||||||
|
enabled ? R.color.home_destructive : R.color.home_nav_inactive);
|
||||||
|
mHomeSelectionDelete.setImageTintList(
|
||||||
|
android.content.res.ColorStateList.valueOf(tintColor));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void onHomeReadAllOrUnreadClick() {
|
||||||
|
if (mHomeSelectionCount > 0
|
||||||
|
&& getActivity() instanceof AbstractConversationListActivity) {
|
||||||
|
((AbstractConversationListActivity) getActivity()).markSelectedConversationsUnread();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
markAllVisibleConversationsRead();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void markAllVisibleConversationsRead() {
|
private void markAllVisibleConversationsRead() {
|
||||||
@@ -772,10 +1079,44 @@ public class ConversationListFragment extends Fragment implements ConversationLi
|
|||||||
private PopupWindow mHomePopover;
|
private PopupWindow mHomePopover;
|
||||||
|
|
||||||
private void dismissHomePopover() {
|
private void dismissHomePopover() {
|
||||||
if (mHomePopover != null && mHomePopover.isShowing()) {
|
dismissHomePopoverAndThen(null);
|
||||||
mHomePopover.dismiss();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** No exit animation — use when opening selection mode to avoid stacked motion. */
|
||||||
|
private void dismissHomePopoverImmediate() {
|
||||||
|
if (mHomePopover == null || !mHomePopover.isShowing()) {
|
||||||
mHomePopover = null;
|
mHomePopover = null;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
final View content = mHomePopover.getContentView();
|
||||||
|
if (content != null) {
|
||||||
|
content.animate().cancel();
|
||||||
|
HomeUiMotion.popoverResetInstant(content);
|
||||||
|
}
|
||||||
|
final PopupWindow popup = mHomePopover;
|
||||||
|
mHomePopover = null;
|
||||||
|
popup.dismiss();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void dismissHomePopoverAndThen(final Runnable afterDismiss) {
|
||||||
|
if (mHomePopover == null || !mHomePopover.isShowing()) {
|
||||||
|
mHomePopover = null;
|
||||||
|
if (afterDismiss != null) {
|
||||||
|
afterDismiss.run();
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
final PopupWindow popup = mHomePopover;
|
||||||
|
final View content = popup.getContentView();
|
||||||
|
mHomePopover = null;
|
||||||
|
HomeUiMotion.popoverExit(content, () -> {
|
||||||
|
if (popup.isShowing()) {
|
||||||
|
popup.dismiss();
|
||||||
|
}
|
||||||
|
if (afterDismiss != null) {
|
||||||
|
afterDismiss.run();
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private PopupWindow showHomePopover(final View anchor, final int layoutResId,
|
private PopupWindow showHomePopover(final View anchor, final int layoutResId,
|
||||||
@@ -804,6 +1145,7 @@ public class ConversationListFragment extends Fragment implements ConversationLi
|
|||||||
? Math.max(0, anchor.getWidth() - content.getMeasuredWidth())
|
? Math.max(0, anchor.getWidth() - content.getMeasuredWidth())
|
||||||
: 0;
|
: 0;
|
||||||
popup.showAsDropDown(anchor, xOff, 8);
|
popup.showAsDropDown(anchor, xOff, 8);
|
||||||
|
HomeUiMotion.popoverEnter(content, alignEnd);
|
||||||
mHomePopover = popup;
|
mHomePopover = popup;
|
||||||
return popup;
|
return popup;
|
||||||
}
|
}
|
||||||
@@ -817,8 +1159,7 @@ public class ConversationListFragment extends Fragment implements ConversationLi
|
|||||||
});
|
});
|
||||||
content.findViewById(R.id.popover_spam).setOnClickListener(v -> {
|
content.findViewById(R.id.popover_spam).setOnClickListener(v -> {
|
||||||
dismissHomePopover();
|
dismissHomePopover();
|
||||||
launchHomeSubScreen(() -> UIIntents.get().launchBlockedParticipantsActivity(
|
launchHomeSubScreen(() -> UIIntents.get().launchHomeSpamActivity(requireActivity()));
|
||||||
requireActivity()));
|
|
||||||
});
|
});
|
||||||
content.findViewById(R.id.popover_recently_deleted).setOnClickListener(v -> {
|
content.findViewById(R.id.popover_recently_deleted).setOnClickListener(v -> {
|
||||||
dismissHomePopover();
|
dismissHomePopover();
|
||||||
@@ -852,14 +1193,13 @@ public class ConversationListFragment extends Fragment implements ConversationLi
|
|||||||
final PopupWindow popup = showHomePopover(anchor, R.layout.home_popover_edit, false);
|
final PopupWindow popup = showHomePopover(anchor, R.layout.home_popover_edit, false);
|
||||||
final View content = popup.getContentView();
|
final View content = popup.getContentView();
|
||||||
content.findViewById(R.id.popover_select_messages).setOnClickListener(v -> {
|
content.findViewById(R.id.popover_select_messages).setOnClickListener(v -> {
|
||||||
dismissHomePopover();
|
dismissHomePopoverImmediate();
|
||||||
if (getActivity() instanceof ConversationListActivity) {
|
if (getActivity() instanceof ConversationListActivity) {
|
||||||
((ConversationListActivity) getActivity()).enterHomeEditMode();
|
((ConversationListActivity) getActivity()).enterHomeEditMode();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
content.findViewById(R.id.popover_edit_pinned).setOnClickListener(v -> {
|
content.findViewById(R.id.popover_edit_pinned).setOnClickListener(v -> {
|
||||||
dismissHomePopover();
|
dismissHomePopoverAndThen(() -> setPinnedEditMode(true));
|
||||||
setPinnedEditMode(true);
|
|
||||||
});
|
});
|
||||||
content.findViewById(R.id.popover_name_photo).setOnClickListener(v -> {
|
content.findViewById(R.id.popover_name_photo).setOnClickListener(v -> {
|
||||||
dismissHomePopover();
|
dismissHomePopover();
|
||||||
@@ -981,6 +1321,12 @@ public class ConversationListFragment extends Fragment implements ConversationLi
|
|||||||
conversationView);
|
conversationView);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onConversationAvatarClicked(final ConversationListItemData data,
|
||||||
|
final View avatarView) {
|
||||||
|
showHomeListProfilePopup(data, avatarView);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* {@inheritDoc} from ConversationListItemView.HostInterface
|
* {@inheritDoc} from ConversationListItemView.HostInterface
|
||||||
*/
|
*/
|
||||||
@@ -1016,9 +1362,6 @@ public class ConversationListFragment extends Fragment implements ConversationLi
|
|||||||
mAdapter.notifyItemChanged(adapterPosition);
|
mAdapter.notifyItemChanged(adapterPosition);
|
||||||
}
|
}
|
||||||
refreshPinnedStrip();
|
refreshPinnedStrip();
|
||||||
if (!wasPinned && mRecyclerView != null) {
|
|
||||||
mRecyclerView.smoothScrollToPosition(mAdapter.getFirstConversationAdapterPosition());
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -1063,14 +1406,27 @@ public class ConversationListFragment extends Fragment implements ConversationLi
|
|||||||
}
|
}
|
||||||
|
|
||||||
public void dismissFab() {
|
public void dismissFab() {
|
||||||
// To prevent clicking while animating.
|
dismissFab(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @param instant skip slide animation (home selection chrome). */
|
||||||
|
public void dismissFab(final boolean instant) {
|
||||||
|
if (mStartNewConversationButton == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
mStartNewConversationButton.setEnabled(false);
|
mStartNewConversationButton.setEnabled(false);
|
||||||
|
mStartNewConversationButton.animate().cancel();
|
||||||
final MarginLayoutParams lp =
|
final MarginLayoutParams lp =
|
||||||
(MarginLayoutParams) mStartNewConversationButton.getLayoutParams();
|
(MarginLayoutParams) mStartNewConversationButton.getLayoutParams();
|
||||||
final float fabWidthWithLeftRightMargin = mStartNewConversationButton.getWidth()
|
final float fabWidthWithLeftRightMargin = mStartNewConversationButton.getWidth()
|
||||||
+ lp.leftMargin + lp.rightMargin;
|
+ lp.leftMargin + lp.rightMargin;
|
||||||
final int direction = AccessibilityUtil.isLayoutRtl(mStartNewConversationButton) ? -1 : 1;
|
final int direction = AccessibilityUtil.isLayoutRtl(mStartNewConversationButton) ? -1 : 1;
|
||||||
getNormalizedFabAnimator().translationX(direction * fabWidthWithLeftRightMargin);
|
final float offScreen = direction * fabWidthWithLeftRightMargin;
|
||||||
|
if (instant) {
|
||||||
|
mStartNewConversationButton.setTranslationX(offScreen);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
getNormalizedFabAnimator().translationX(offScreen);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void showFab() {
|
public void showFab() {
|
||||||
@@ -1101,6 +1457,10 @@ public class ConversationListFragment extends Fragment implements ConversationLi
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void setPinnedEditMode(final boolean enabled) {
|
private void setPinnedEditMode(final boolean enabled) {
|
||||||
|
setPinnedEditMode(enabled, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void setPinnedEditMode(final boolean enabled, final boolean refreshList) {
|
||||||
if (mArchiveMode || mForwardMessageMode) {
|
if (mArchiveMode || mForwardMessageMode) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -1122,9 +1482,11 @@ public class ConversationListFragment extends Fragment implements ConversationLi
|
|||||||
mHomePinnedEditMode = enabled;
|
mHomePinnedEditMode = enabled;
|
||||||
updateHomeEditButtonForPinnedMode();
|
updateHomeEditButtonForPinnedMode();
|
||||||
updateSearchBarForPinnedEditMode();
|
updateSearchBarForPinnedEditMode();
|
||||||
|
if (refreshList) {
|
||||||
refreshPinnedStrip();
|
refreshPinnedStrip();
|
||||||
updateUi();
|
updateUi();
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private void updateSearchBarForPinnedEditMode() {
|
private void updateSearchBarForPinnedEditMode() {
|
||||||
if (mSearchBar == null) {
|
if (mSearchBar == null) {
|
||||||
@@ -1171,8 +1533,8 @@ public class ConversationListFragment extends Fragment implements ConversationLi
|
|||||||
final Cursor cursor = mAdapter.getCursor();
|
final Cursor cursor = mAdapter.getCursor();
|
||||||
final boolean editMode = mHomePinnedEditMode && !isSelectionMode();
|
final boolean editMode = mHomePinnedEditMode && !isSelectionMode();
|
||||||
final List<String> pinnedIds = PinnedConversationStore.getPinnedIds(requireContext());
|
final List<String> pinnedIds = PinnedConversationStore.getPinnedIds(requireContext());
|
||||||
HomePinnedStrip.bind(mHomePinnedStrip, cursor, pinnedIds,
|
HomePinnedStrip.bind(mHomePinnedStrip, cursor, pinnedIds, editMode,
|
||||||
editMode, isSelectionMode(), mPinnedStripListener);
|
mPinnedStripListener);
|
||||||
final boolean showHeader = pinnedIds != null && !pinnedIds.isEmpty()
|
final boolean showHeader = pinnedIds != null && !pinnedIds.isEmpty()
|
||||||
&& cursor != null
|
&& cursor != null
|
||||||
&& mHomePinnedStrip.getVisibility() == View.VISIBLE;
|
&& mHomePinnedStrip.getVisibility() == View.VISIBLE;
|
||||||
@@ -1213,27 +1575,18 @@ public class ConversationListFragment extends Fragment implements ConversationLi
|
|||||||
.show();
|
.show();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void openConversationById(final String conversationId) {
|
private ConversationListItemData loadConversationListItemData(final String conversationId) {
|
||||||
if (mHost == null) {
|
if (mAdapter == null || TextUtils.isEmpty(conversationId)) {
|
||||||
return;
|
return null;
|
||||||
}
|
}
|
||||||
final Cursor cursor = mAdapter.getCursor();
|
final android.database.Cursor cursor = mAdapter.getCursor();
|
||||||
if (cursor == null) {
|
if (cursor == null) {
|
||||||
return;
|
return null;
|
||||||
}
|
}
|
||||||
final int saved = cursor.getPosition();
|
final ConversationListItemData data = new ConversationListItemData();
|
||||||
for (int i = 0; i < cursor.getCount(); i++) {
|
if (!HomePinnedStrip.bindConversationListItem(cursor, conversationId, data)) {
|
||||||
if (!cursor.moveToPosition(i)) {
|
return null;
|
||||||
continue;
|
|
||||||
}
|
|
||||||
mPinnedRowProbe.bind(cursor);
|
|
||||||
if (conversationId.equals(mPinnedRowProbe.getConversationId())) {
|
|
||||||
mHost.onConversationClick(mListBinding.getData(), mPinnedRowProbe, false, null);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (saved >= 0 && saved < cursor.getCount()) {
|
|
||||||
cursor.moveToPosition(saved);
|
|
||||||
}
|
}
|
||||||
|
return data;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+46
-27
@@ -87,6 +87,8 @@ public class ConversationListItemView extends FrameLayout implements OnClickList
|
|||||||
boolean isConversationSelected(final String conversationId);
|
boolean isConversationSelected(final String conversationId);
|
||||||
void onConversationClicked(final ConversationListItemData conversationListItemData,
|
void onConversationClicked(final ConversationListItemData conversationListItemData,
|
||||||
boolean isLongClick, final ConversationListItemView conversationView);
|
boolean isLongClick, final ConversationListItemView conversationView);
|
||||||
|
void onConversationAvatarClicked(final ConversationListItemData conversationListItemData,
|
||||||
|
final View avatarView);
|
||||||
boolean isSwipeAnimatable();
|
boolean isSwipeAnimatable();
|
||||||
List<SnackBarInteraction> getSnackBarInteractions();
|
List<SnackBarInteraction> getSnackBarInteractions();
|
||||||
void startFullScreenPhotoViewer(final Uri initialPhoto, final Rect initialPhotoBounds,
|
void startFullScreenPhotoViewer(final Uri initialPhoto, final Rect initialPhotoBounds,
|
||||||
@@ -218,22 +220,14 @@ public class ConversationListItemView extends FrameLayout implements OnClickList
|
|||||||
private void setWorkProfileIcon() {
|
private void setWorkProfileIcon() {
|
||||||
mWorkProfileIconView.setVisibility(mData.isEnterprise() ? View.VISIBLE : View.GONE);
|
mWorkProfileIconView.setVisibility(mData.isEnterprise() ? View.VISIBLE : View.GONE);
|
||||||
if (mCategoryChipView != null) {
|
if (mCategoryChipView != null) {
|
||||||
final boolean selectionMode =
|
|
||||||
mHostInterface != null && mHostInterface.isSelectionMode();
|
|
||||||
if (selectionMode) {
|
|
||||||
mCategoryChipView.setVisibility(View.GONE);
|
|
||||||
} else {
|
|
||||||
mCategoryChipView.setVisibility(View.VISIBLE);
|
mCategoryChipView.setVisibility(View.VISIBLE);
|
||||||
mCategoryChipView.setText(mData.isEnterprise()
|
mCategoryChipView.setText(mData.isEnterprise()
|
||||||
? R.string.home_chip_work
|
? R.string.home_chip_work
|
||||||
: R.string.home_chip_personal);
|
: R.string.home_chip_personal);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
private void setConversationName() {
|
private void setConversationName() {
|
||||||
final boolean selectionMode =
|
|
||||||
mHostInterface != null && mHostInterface.isSelectionMode();
|
|
||||||
final boolean showUnreadDot = !mData.getIsRead() && !mData.getShowDraft();
|
final boolean showUnreadDot = !mData.getIsRead() && !mData.getShowDraft();
|
||||||
if (mUnreadDotSlotView != null) {
|
if (mUnreadDotSlotView != null) {
|
||||||
mUnreadDotSlotView.setVisibility(View.VISIBLE);
|
mUnreadDotSlotView.setVisibility(View.VISIBLE);
|
||||||
@@ -241,10 +235,7 @@ public class ConversationListItemView extends FrameLayout implements OnClickList
|
|||||||
if (mUnreadDotView != null) {
|
if (mUnreadDotView != null) {
|
||||||
mUnreadDotView.setVisibility(showUnreadDot ? View.VISIBLE : View.INVISIBLE);
|
mUnreadDotView.setVisibility(showUnreadDot ? View.VISIBLE : View.INVISIBLE);
|
||||||
}
|
}
|
||||||
if (selectionMode) {
|
if (mData.getIsRead() || mData.getShowDraft()) {
|
||||||
mConversationNameView.setTextColor(mListItemReadColor);
|
|
||||||
mConversationNameView.setTypeface(mListItemUnreadTypeface);
|
|
||||||
} else if (mData.getIsRead() || mData.getShowDraft()) {
|
|
||||||
mConversationNameView.setTextColor(mListItemReadColor);
|
mConversationNameView.setTextColor(mListItemReadColor);
|
||||||
mConversationNameView.setTypeface(mListItemReadTypeface);
|
mConversationNameView.setTypeface(mListItemReadTypeface);
|
||||||
} else {
|
} else {
|
||||||
@@ -561,17 +552,14 @@ public class ConversationListItemView extends FrameLayout implements OnClickList
|
|||||||
final String snippetText = getSnippetText();
|
final String snippetText = getSnippetText();
|
||||||
|
|
||||||
final boolean selectionMode = mHostInterface.isSelectionMode();
|
final boolean selectionMode = mHostInterface.isSelectionMode();
|
||||||
if (selectionMode) {
|
if (mData.getIsRead() || mData.getShowDraft()) {
|
||||||
maxLines = 0;
|
|
||||||
typeface = mListItemReadTypeface;
|
|
||||||
} else if (mData.getIsRead() || mData.getShowDraft()) {
|
|
||||||
maxLines = TextUtils.isEmpty(snippetText) ? 0 : NO_UNREAD_SNIPPET_LINE_COUNT;
|
maxLines = TextUtils.isEmpty(snippetText) ? 0 : NO_UNREAD_SNIPPET_LINE_COUNT;
|
||||||
typeface = mListItemReadTypeface;
|
typeface = mListItemReadTypeface;
|
||||||
} else {
|
} else {
|
||||||
maxLines = TextUtils.isEmpty(snippetText) ? 0 : UNREAD_SNIPPET_LINE_COUNT;
|
maxLines = TextUtils.isEmpty(snippetText) ? 0 : UNREAD_SNIPPET_LINE_COUNT;
|
||||||
typeface = mListItemUnreadTypeface;
|
typeface = mListItemUnreadTypeface;
|
||||||
}
|
}
|
||||||
color = getResources().getColor(R.color.home_text_secondary, getContext().getTheme());
|
color = getResources().getColor(R.color.home_text_snippet, getContext().getTheme());
|
||||||
|
|
||||||
mSnippetTextView.setMaxLines(maxLines);
|
mSnippetTextView.setMaxLines(maxLines);
|
||||||
mSnippetTextView.setTextColor(color);
|
mSnippetTextView.setTextColor(color);
|
||||||
@@ -579,14 +567,9 @@ public class ConversationListItemView extends FrameLayout implements OnClickList
|
|||||||
mSubjectTextView.setTextColor(color);
|
mSubjectTextView.setTextColor(color);
|
||||||
mSubjectTextView.setTypeface(typeface, typefaceStyle);
|
mSubjectTextView.setTypeface(typeface, typefaceStyle);
|
||||||
|
|
||||||
if (selectionMode) {
|
|
||||||
mSnippetTextView.setVisibility(GONE);
|
|
||||||
mSubjectTextView.setVisibility(GONE);
|
|
||||||
} else {
|
|
||||||
mSnippetTextView.setVisibility(VISIBLE);
|
mSnippetTextView.setVisibility(VISIBLE);
|
||||||
setSnippet();
|
setSnippet();
|
||||||
setSubject();
|
setSubject();
|
||||||
}
|
|
||||||
setConversationName();
|
setConversationName();
|
||||||
setWorkProfileIcon();
|
setWorkProfileIcon();
|
||||||
setContentDescription(buildContentDescription(resources, mData,
|
setContentDescription(buildContentDescription(resources, mData,
|
||||||
@@ -616,8 +599,10 @@ public class ConversationListItemView extends FrameLayout implements OnClickList
|
|||||||
mTimestampTextView.setText(resources.getString(
|
mTimestampTextView.setText(resources.getString(
|
||||||
R.string.conversation_list_item_view_draft_message));
|
R.string.conversation_list_item_view_draft_message));
|
||||||
} else {
|
} else {
|
||||||
mTimestampTextView.setTextColor(resources.getColor(R.color.home_text_secondary,
|
final int timestampColor = (mData.getIsRead() || mData.getShowDraft())
|
||||||
getContext().getTheme()));
|
? resources.getColor(R.color.home_text_secondary, getContext().getTheme())
|
||||||
|
: color;
|
||||||
|
mTimestampTextView.setTextColor(timestampColor);
|
||||||
mTimestampTextView.setTypeface(mListItemReadTypeface, typefaceStyle);
|
mTimestampTextView.setTypeface(mListItemReadTypeface, typefaceStyle);
|
||||||
final String formattedTimestamp = mData.getFormattedTimestamp();
|
final String formattedTimestamp = mData.getFormattedTimestamp();
|
||||||
if (mData.getIsSendRequested()) {
|
if (mData.getIsSendRequested()) {
|
||||||
@@ -647,9 +632,17 @@ public class ConversationListItemView extends FrameLayout implements OnClickList
|
|||||||
mContactIconView.setImageResourceUri(iconUri, mData.getParticipantContactId(),
|
mContactIconView.setImageResourceUri(iconUri, mData.getParticipantContactId(),
|
||||||
mData.getParticipantLookupKey(), mData.getOtherParticipantNormalizedDestination());
|
mData.getParticipantLookupKey(), mData.getOtherParticipantNormalizedDestination());
|
||||||
mContactIconView.setVisibility(contactIconVisibility);
|
mContactIconView.setVisibility(contactIconVisibility);
|
||||||
|
mContactIconView.setImageClickHandlerDisabled(true);
|
||||||
mContactIconView.setOnLongClickListener(this);
|
mContactIconView.setOnLongClickListener(this);
|
||||||
mContactIconView.setClickable(!mHostInterface.isSelectionMode());
|
final boolean avatarInteractive = !mHostInterface.isSelectionMode();
|
||||||
mContactIconView.setLongClickable(!mHostInterface.isSelectionMode());
|
mContactIconView.setClickable(avatarInteractive);
|
||||||
|
mContactIconView.setLongClickable(avatarInteractive);
|
||||||
|
if (avatarInteractive) {
|
||||||
|
mContactIconView.setOnClickListener(v -> mHostInterface.onConversationAvatarClicked(
|
||||||
|
mData, mContactIconView));
|
||||||
|
} else {
|
||||||
|
mContactIconView.setOnClickListener(null);
|
||||||
|
}
|
||||||
|
|
||||||
mContactCheckmarkView.setVisibility(checkmarkVisiblity);
|
mContactCheckmarkView.setVisibility(checkmarkVisiblity);
|
||||||
mFailedStatusIconView.setVisibility(failStatusVisiblity);
|
mFailedStatusIconView.setVisibility(failStatusVisiblity);
|
||||||
@@ -707,6 +700,29 @@ public class ConversationListItemView extends FrameLayout implements OnClickList
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Lightweight refresh when toggling home selection chrome. */
|
||||||
|
public void rebindSelectionChrome() {
|
||||||
|
if (mHostInterface == null || mData == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
final Resources resources = getContext().getResources();
|
||||||
|
final boolean selectionMode = mHostInterface.isSelectionMode();
|
||||||
|
final boolean isSelected = mHostInterface.isConversationSelected(
|
||||||
|
mData.getConversationId());
|
||||||
|
setSelected(isSelected);
|
||||||
|
updateSelectionIndicator(selectionMode, isSelected, resources);
|
||||||
|
mContactIconView.setClickable(!selectionMode);
|
||||||
|
mContactIconView.setLongClickable(!selectionMode);
|
||||||
|
if (!selectionMode) {
|
||||||
|
mContactIconView.setOnClickListener(v -> mHostInterface.onConversationAvatarClicked(
|
||||||
|
mData, mContactIconView));
|
||||||
|
} else {
|
||||||
|
mContactIconView.setOnClickListener(null);
|
||||||
|
}
|
||||||
|
updatePinButton(selectionMode, resources);
|
||||||
|
updateListDividerInset(selectionMode);
|
||||||
|
}
|
||||||
|
|
||||||
public boolean isSwipeAnimatable() {
|
public boolean isSwipeAnimatable() {
|
||||||
return mHostInterface.isSwipeAnimatable();
|
return mHostInterface.isSwipeAnimatable();
|
||||||
}
|
}
|
||||||
@@ -813,7 +829,10 @@ public class ConversationListItemView extends FrameLayout implements OnClickList
|
|||||||
}
|
}
|
||||||
|
|
||||||
private boolean processClick(final View v, final boolean isLongClick) {
|
private boolean processClick(final View v, final boolean isLongClick) {
|
||||||
Assert.isTrue(v == mSwipeableContainer || v == mContactIconView || v == mImagePreviewView);
|
if (v == mContactIconView) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
Assert.isTrue(v == mSwipeableContainer || v == mImagePreviewView);
|
||||||
Assert.notNull(mData.getName());
|
Assert.notNull(mData.getName());
|
||||||
|
|
||||||
if (mHostInterface != null) {
|
if (mHostInterface != null) {
|
||||||
|
|||||||
+5
-2
@@ -104,9 +104,9 @@ public class ConversationListSwipeHelper implements OnItemTouchListener {
|
|||||||
mInitialY = event.getY();
|
mInitialY = event.getY();
|
||||||
|
|
||||||
final View viewAtPoint = mRecyclerView.findChildViewUnder(mInitialX, mInitialY);
|
final View viewAtPoint = mRecyclerView.findChildViewUnder(mInitialX, mInitialY);
|
||||||
|
if (viewAtPoint instanceof ConversationListItemView) {
|
||||||
final ConversationListItemView child = (ConversationListItemView) viewAtPoint;
|
final ConversationListItemView child = (ConversationListItemView) viewAtPoint;
|
||||||
if (viewAtPoint instanceof ConversationListItemView &&
|
if (child.isSwipeAnimatable()) {
|
||||||
child != null && child.isSwipeAnimatable()) {
|
|
||||||
// Begin detecting swipe on the target for the rest of the gesture.
|
// Begin detecting swipe on the target for the rest of the gesture.
|
||||||
mListItemView = child;
|
mListItemView = child;
|
||||||
if (mListItemView.isAnimating()) {
|
if (mListItemView.isAnimating()) {
|
||||||
@@ -115,6 +115,9 @@ public class ConversationListSwipeHelper implements OnItemTouchListener {
|
|||||||
} else {
|
} else {
|
||||||
mListItemView = null;
|
mListItemView = null;
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
mListItemView = null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case MotionEvent.ACTION_MOVE:
|
case MotionEvent.ACTION_MOVE:
|
||||||
|
|||||||
-66
@@ -1,66 +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.ui.conversationsettings;
|
|
||||||
|
|
||||||
import android.os.Bundle;
|
|
||||||
import android.view.MenuItem;
|
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
|
||||||
import androidx.fragment.app.Fragment;
|
|
||||||
|
|
||||||
import com.android.messaging.R;
|
|
||||||
import com.android.messaging.ui.BugleActionBarActivity;
|
|
||||||
import com.android.messaging.ui.UIIntents;
|
|
||||||
import com.android.messaging.util.Assert;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Shows a list of participants in a conversation.
|
|
||||||
*/
|
|
||||||
public class PeopleAndOptionsActivity extends BugleActionBarActivity {
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void onCreate(final Bundle savedInstanceState) {
|
|
||||||
super.onCreate(savedInstanceState);
|
|
||||||
setContentView(R.layout.people_and_options_activity);
|
|
||||||
|
|
||||||
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onAttachFragment(@NonNull final Fragment fragment) {
|
|
||||||
if (fragment instanceof PeopleAndOptionsFragment) {
|
|
||||||
final String conversationId =
|
|
||||||
getIntent().getStringExtra(UIIntents.UI_INTENT_EXTRA_CONVERSATION_ID);
|
|
||||||
Assert.notNull(conversationId);
|
|
||||||
final PeopleAndOptionsFragment peopleAndOptionsFragment =
|
|
||||||
(PeopleAndOptionsFragment) fragment;
|
|
||||||
peopleAndOptionsFragment.setConversationId(conversationId);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean onOptionsItemSelected(@NonNull final MenuItem item) {
|
|
||||||
if (item.getItemId() == android.R.id.home) {
|
|
||||||
// Treat the home press as back press so that when we go back to
|
|
||||||
// ConversationActivity, it doesn't lose its original intent (conversation id etc.)
|
|
||||||
onBackPressed();
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return super.onOptionsItemSelected(item);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-314
@@ -1,314 +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.ui.conversationsettings;
|
|
||||||
|
|
||||||
import android.app.Activity;
|
|
||||||
import android.app.AlertDialog;
|
|
||||||
import android.app.NotificationManager;
|
|
||||||
import android.content.Context;
|
|
||||||
import android.content.Intent;
|
|
||||||
import android.content.res.Resources;
|
|
||||||
import android.database.Cursor;
|
|
||||||
import android.os.Bundle;
|
|
||||||
import android.provider.Settings;
|
|
||||||
import android.view.LayoutInflater;
|
|
||||||
import android.view.View;
|
|
||||||
import android.view.ViewGroup;
|
|
||||||
import android.widget.ArrayAdapter;
|
|
||||||
import android.widget.BaseAdapter;
|
|
||||||
import android.widget.ListView;
|
|
||||||
import android.widget.TextView;
|
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
|
||||||
import androidx.fragment.app.Fragment;
|
|
||||||
import androidx.loader.app.LoaderManager;
|
|
||||||
|
|
||||||
import com.android.messaging.R;
|
|
||||||
import com.android.messaging.datamodel.DataModel;
|
|
||||||
import com.android.messaging.datamodel.binding.Binding;
|
|
||||||
import com.android.messaging.datamodel.binding.BindingBase;
|
|
||||||
import com.android.messaging.datamodel.data.ParticipantData;
|
|
||||||
import com.android.messaging.datamodel.data.ParticipantListItemData;
|
|
||||||
import com.android.messaging.datamodel.data.PeopleAndOptionsData;
|
|
||||||
import com.android.messaging.datamodel.data.PeopleAndOptionsData.PeopleAndOptionsDataListener;
|
|
||||||
import com.android.messaging.datamodel.data.PeopleOptionsItemData;
|
|
||||||
import com.android.messaging.datamodel.data.PersonItemData;
|
|
||||||
import com.android.messaging.ui.CompositeAdapter;
|
|
||||||
import com.android.messaging.ui.PersonItemView;
|
|
||||||
import com.android.messaging.ui.conversation.ConversationActivity;
|
|
||||||
import com.android.messaging.util.Assert;
|
|
||||||
import com.android.messaging.util.NotificationsUtil;
|
|
||||||
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Shows a list of participants of a conversation and displays options.
|
|
||||||
*/
|
|
||||||
public class PeopleAndOptionsFragment extends Fragment
|
|
||||||
implements PeopleAndOptionsDataListener, PeopleOptionsItemView.HostInterface {
|
|
||||||
private ListView mListView;
|
|
||||||
private OptionsListAdapter mOptionsListAdapter;
|
|
||||||
private PeopleListAdapter mPeopleListAdapter;
|
|
||||||
private List<ParticipantData> mOtherParticipants;
|
|
||||||
private final Binding<PeopleAndOptionsData> mBinding =
|
|
||||||
BindingBase.createBinding(this);
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onCreate(final Bundle savedInstanceState) {
|
|
||||||
super.onCreate(savedInstanceState);
|
|
||||||
mBinding.getData().init(LoaderManager.getInstance(this), mBinding);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public View onCreateView(final LayoutInflater inflater, final ViewGroup container,
|
|
||||||
final Bundle savedInstanceState) {
|
|
||||||
final View view = inflater.inflate(R.layout.people_and_options_fragment, container, false);
|
|
||||||
mListView = view.findViewById(android.R.id.list);
|
|
||||||
mPeopleListAdapter = new PeopleListAdapter(getActivity());
|
|
||||||
mOptionsListAdapter = new OptionsListAdapter();
|
|
||||||
final CompositeAdapter compositeAdapter = new CompositeAdapter(getActivity());
|
|
||||||
compositeAdapter.addPartition(new PeopleAndOptionsPartition(mOptionsListAdapter,
|
|
||||||
R.string.general_settings_title, false));
|
|
||||||
compositeAdapter.addPartition(new PeopleAndOptionsPartition(mPeopleListAdapter,
|
|
||||||
R.string.participant_list_title, true));
|
|
||||||
mListView.setAdapter(compositeAdapter);
|
|
||||||
return view;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onDestroy() {
|
|
||||||
super.onDestroy();
|
|
||||||
mBinding.unbind();
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setConversationId(final String conversationId) {
|
|
||||||
Assert.isTrue(getView() == null);
|
|
||||||
Assert.notNull(conversationId);
|
|
||||||
mBinding.bind(DataModel.get().createPeopleAndOptionsData(conversationId, getActivity(),
|
|
||||||
this));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onOptionsCursorUpdated(final PeopleAndOptionsData data, final Cursor cursor) {
|
|
||||||
Assert.isTrue(cursor == null || cursor.getCount() == 1);
|
|
||||||
mBinding.ensureBound(data);
|
|
||||||
mOptionsListAdapter.swapCursor(cursor);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onParticipantsListLoaded(final PeopleAndOptionsData data,
|
|
||||||
final List<ParticipantData> participants) {
|
|
||||||
mBinding.ensureBound(data);
|
|
||||||
mPeopleListAdapter.updateParticipants(participants);
|
|
||||||
mOtherParticipants = participants;
|
|
||||||
final ParticipantData otherParticipant = participants.size() == 1 ?
|
|
||||||
participants.get(0) : null;
|
|
||||||
mOptionsListAdapter.setOtherParticipant(otherParticipant);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onOptionsItemViewClicked(final PeopleOptionsItemData item) {
|
|
||||||
if (item.getItemId() == PeopleOptionsItemData.SETTING_NOTIFICATION) {
|
|
||||||
ArrayList<String> participantsNames = new ArrayList<>();
|
|
||||||
for (ParticipantData participant : mOtherParticipants) {
|
|
||||||
participantsNames.add(participant.getDisplayName(true));
|
|
||||||
}
|
|
||||||
NotificationsUtil.createNotificationChannelGroup(getActivity(),
|
|
||||||
NotificationsUtil.CONVERSATION_GROUP_NAME,
|
|
||||||
R.string.notification_channel_messages_title);
|
|
||||||
NotificationsUtil.createNotificationChannel(getActivity(),
|
|
||||||
mBinding.getData().getConversationId(),
|
|
||||||
String.join(", ", participantsNames),
|
|
||||||
NotificationManager.IMPORTANCE_HIGH,
|
|
||||||
NotificationsUtil.CONVERSATION_GROUP_NAME);
|
|
||||||
Intent intent = new Intent(Settings.ACTION_APP_NOTIFICATION_SETTINGS);
|
|
||||||
intent.putExtra(Settings.EXTRA_APP_PACKAGE, getContext().getPackageName());
|
|
||||||
startActivity(intent);
|
|
||||||
} else if (item.getItemId() == PeopleOptionsItemData.SETTING_BLOCKED) {
|
|
||||||
if (item.getOtherParticipant().isBlocked()) {
|
|
||||||
mBinding.getData().setDestinationBlocked(mBinding, false);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
final Resources res = getResources();
|
|
||||||
final Activity activity = getActivity();
|
|
||||||
new AlertDialog.Builder(activity, R.style.AlertDialogTheme)
|
|
||||||
.setTitle(res.getString(R.string.block_confirmation_title,
|
|
||||||
item.getOtherParticipant().getDisplayDestination()))
|
|
||||||
.setMessage(res.getString(R.string.block_confirmation_message))
|
|
||||||
.setNegativeButton(android.R.string.cancel, null)
|
|
||||||
.setPositiveButton(android.R.string.ok, (arg0, arg1) -> {
|
|
||||||
mBinding.getData().setDestinationBlocked(mBinding, true);
|
|
||||||
activity.setResult(ConversationActivity.FINISH_RESULT_CODE);
|
|
||||||
activity.finish();
|
|
||||||
})
|
|
||||||
.create()
|
|
||||||
.show();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A simple adapter that takes a conversation metadata cursor and binds
|
|
||||||
* PeopleAndOptionsItemViews to individual COLUMNS of the first cursor record. (Note
|
|
||||||
* that this is not a CursorAdapter because it treats individual columns of the cursor as
|
|
||||||
* separate options to display for the conversation, e.g. notification settings).
|
|
||||||
*/
|
|
||||||
private class OptionsListAdapter extends BaseAdapter {
|
|
||||||
private Cursor mOptionsCursor;
|
|
||||||
private ParticipantData mOtherParticipantData;
|
|
||||||
|
|
||||||
public Cursor swapCursor(final Cursor newCursor) {
|
|
||||||
final Cursor oldCursor = mOptionsCursor;
|
|
||||||
if (newCursor != oldCursor) {
|
|
||||||
mOptionsCursor = newCursor;
|
|
||||||
notifyDataSetChanged();
|
|
||||||
}
|
|
||||||
return oldCursor;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setOtherParticipant(final ParticipantData participantData) {
|
|
||||||
if (mOtherParticipantData != participantData) {
|
|
||||||
mOtherParticipantData = participantData;
|
|
||||||
notifyDataSetChanged();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public int getCount() {
|
|
||||||
int count = PeopleOptionsItemData.SETTINGS_COUNT;
|
|
||||||
if (mOtherParticipantData == null) {
|
|
||||||
count--;
|
|
||||||
}
|
|
||||||
return mOptionsCursor == null ? 0 : count;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Object getItem(final int position) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public long getItemId(final int position) {
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public View getView(final int position, final View convertView, final ViewGroup parent) {
|
|
||||||
final PeopleOptionsItemView itemView;
|
|
||||||
if (convertView != null && convertView instanceof PeopleOptionsItemView) {
|
|
||||||
itemView = (PeopleOptionsItemView) convertView;
|
|
||||||
} else {
|
|
||||||
final LayoutInflater inflater = (LayoutInflater) getActivity()
|
|
||||||
.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
|
|
||||||
itemView = (PeopleOptionsItemView)
|
|
||||||
inflater.inflate(R.layout.people_options_item_view, parent, false);
|
|
||||||
}
|
|
||||||
mOptionsCursor.moveToFirst();
|
|
||||||
itemView.bind(mOptionsCursor, position, mOtherParticipantData,
|
|
||||||
PeopleAndOptionsFragment.this);
|
|
||||||
return itemView;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* An adapter that takes a list of ParticipantData and displays them as a list of
|
|
||||||
* ParticipantListItemViews.
|
|
||||||
*/
|
|
||||||
private class PeopleListAdapter extends ArrayAdapter<ParticipantData> {
|
|
||||||
public PeopleListAdapter(final Context context) {
|
|
||||||
super(context, R.layout.people_list_item_view, new ArrayList<>());
|
|
||||||
}
|
|
||||||
|
|
||||||
public void updateParticipants(final List<ParticipantData> newList) {
|
|
||||||
clear();
|
|
||||||
addAll(newList);
|
|
||||||
notifyDataSetChanged();
|
|
||||||
}
|
|
||||||
|
|
||||||
@NonNull
|
|
||||||
@Override
|
|
||||||
public View getView(final int position, final View convertView,
|
|
||||||
@NonNull final ViewGroup parent) {
|
|
||||||
PersonItemView itemView;
|
|
||||||
final ParticipantData item = getItem(position);
|
|
||||||
if (convertView != null && convertView instanceof PersonItemView) {
|
|
||||||
itemView = (PersonItemView) convertView;
|
|
||||||
} else {
|
|
||||||
final LayoutInflater inflater = (LayoutInflater) getContext()
|
|
||||||
.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
|
|
||||||
itemView = (PersonItemView) inflater.inflate(R.layout.people_list_item_view, parent,
|
|
||||||
false);
|
|
||||||
}
|
|
||||||
final ParticipantListItemData itemData =
|
|
||||||
DataModel.get().createParticipantListItemData(item);
|
|
||||||
itemView.bind(itemData);
|
|
||||||
|
|
||||||
// Any click on the row should have the same effect as clicking the avatar icon
|
|
||||||
final PersonItemView itemViewClosure = itemView;
|
|
||||||
itemView.setListener(new PersonItemView.PersonItemViewListener() {
|
|
||||||
@Override
|
|
||||||
public void onPersonClicked(final PersonItemData data) {
|
|
||||||
itemViewClosure.performClickOnAvatar();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean onPersonLongClicked(final PersonItemData data) {
|
|
||||||
if (mBinding.isBound()) {
|
|
||||||
final CopyContactDetailDialog dialog = new CopyContactDetailDialog(
|
|
||||||
getContext(), data.getDetails());
|
|
||||||
dialog.show();
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
return itemView;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Represents a partition/section in the People & Options list (e.g. "general options" and
|
|
||||||
* "people in this conversation" sections).
|
|
||||||
*/
|
|
||||||
private class PeopleAndOptionsPartition extends CompositeAdapter.Partition {
|
|
||||||
private final int mHeaderResId;
|
|
||||||
private final boolean mNeedDivider;
|
|
||||||
|
|
||||||
public PeopleAndOptionsPartition(final BaseAdapter adapter, final int headerResId,
|
|
||||||
final boolean needDivider) {
|
|
||||||
super(true /* showIfEmpty */, true /* hasHeader */, adapter);
|
|
||||||
mHeaderResId = headerResId;
|
|
||||||
mNeedDivider = needDivider;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public View getHeaderView(final View convertView, final ViewGroup parentView) {
|
|
||||||
View view;
|
|
||||||
if (convertView != null && convertView.getId() == R.id.people_and_options_header) {
|
|
||||||
view = convertView;
|
|
||||||
} else {
|
|
||||||
view = LayoutInflater.from(getActivity()).inflate(
|
|
||||||
R.layout.people_and_options_section_header, parentView, false);
|
|
||||||
}
|
|
||||||
final TextView text = view.findViewById(R.id.header_text);
|
|
||||||
final View divider = view.findViewById(R.id.divider);
|
|
||||||
text.setText(mHeaderResId);
|
|
||||||
divider.setVisibility(mNeedDivider ? View.VISIBLE : View.GONE);
|
|
||||||
return view;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-70
@@ -1,70 +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.ui.conversationsettings;
|
|
||||||
|
|
||||||
import android.content.Context;
|
|
||||||
import android.database.Cursor;
|
|
||||||
import androidx.appcompat.widget.SwitchCompat;
|
|
||||||
|
|
||||||
import android.util.AttributeSet;
|
|
||||||
import android.widget.LinearLayout;
|
|
||||||
import android.widget.TextView;
|
|
||||||
|
|
||||||
import com.android.messaging.R;
|
|
||||||
import com.android.messaging.datamodel.DataModel;
|
|
||||||
import com.android.messaging.datamodel.data.ParticipantData;
|
|
||||||
import com.android.messaging.datamodel.data.PeopleOptionsItemData;
|
|
||||||
import com.android.messaging.util.Assert;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The view for a single entry in the options section of people & options activity.
|
|
||||||
*/
|
|
||||||
public class PeopleOptionsItemView extends LinearLayout {
|
|
||||||
/**
|
|
||||||
* Implemented by the host of this view that handles options click event.
|
|
||||||
*/
|
|
||||||
public interface HostInterface {
|
|
||||||
void onOptionsItemViewClicked(PeopleOptionsItemData item);
|
|
||||||
}
|
|
||||||
|
|
||||||
private TextView mTitle;
|
|
||||||
private TextView mSubtitle;
|
|
||||||
private SwitchCompat mSwitch;
|
|
||||||
private final PeopleOptionsItemData mData;
|
|
||||||
private HostInterface mHostInterface;
|
|
||||||
|
|
||||||
public PeopleOptionsItemView(final Context context, final AttributeSet attrs) {
|
|
||||||
super(context, attrs);
|
|
||||||
mData = DataModel.get().createPeopleOptionsItemData(context);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void onFinishInflate () {
|
|
||||||
super.onFinishInflate();
|
|
||||||
mTitle = findViewById(R.id.title);
|
|
||||||
setOnClickListener(v -> mHostInterface.onOptionsItemViewClicked(mData));
|
|
||||||
}
|
|
||||||
|
|
||||||
public void bind(final Cursor cursor, final int columnIndex, ParticipantData otherParticipant,
|
|
||||||
final HostInterface hostInterface) {
|
|
||||||
Assert.isTrue(columnIndex < PeopleOptionsItemData.SETTINGS_COUNT && columnIndex >= 0);
|
|
||||||
mData.bind(cursor, otherParticipant, columnIndex);
|
|
||||||
mHostInterface = hostInterface;
|
|
||||||
|
|
||||||
mTitle.setText(mData.getTitle());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,227 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2025 The LineageOS Project
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.android.messaging.ui.home;
|
||||||
|
|
||||||
|
import android.content.Intent;
|
||||||
|
import android.os.Bundle;
|
||||||
|
import android.text.Editable;
|
||||||
|
import android.text.TextUtils;
|
||||||
|
import android.text.TextWatcher;
|
||||||
|
import android.view.LayoutInflater;
|
||||||
|
import android.view.View;
|
||||||
|
import android.widget.EditText;
|
||||||
|
import android.widget.HorizontalScrollView;
|
||||||
|
import android.widget.ImageButton;
|
||||||
|
import android.widget.LinearLayout;
|
||||||
|
import android.widget.TextView;
|
||||||
|
|
||||||
|
import com.android.ex.chips.RecipientEntry;
|
||||||
|
import com.android.messaging.R;
|
||||||
|
import com.android.messaging.datamodel.data.ParticipantData;
|
||||||
|
import com.android.messaging.ui.BugleActionBarActivity;
|
||||||
|
import com.android.messaging.ui.ContactIconView;
|
||||||
|
import com.android.messaging.util.AvatarUriUtil;
|
||||||
|
import com.android.messaging.util.ContactRecipientEntryUtils;
|
||||||
|
import com.android.messaging.util.ImeUtil;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/** Home-style "new chat" screen: recipient field, group entry point and contact list. */
|
||||||
|
public class HomeComposeActivity extends BugleActionBarActivity
|
||||||
|
implements HomeContactsFragment.GroupSelectionHost {
|
||||||
|
|
||||||
|
/** Matches the duration of {@code R.anim.home_sheet_open}. */
|
||||||
|
private static final long SHEET_ENTER_MS = 300L;
|
||||||
|
private static final int REQUEST_GROUP_NAME = 1;
|
||||||
|
private static final int MIN_GROUP_SIZE = 2;
|
||||||
|
|
||||||
|
private EditText mRecipientField;
|
||||||
|
private TextView mHeaderTitle;
|
||||||
|
private View mCreateGroupButton;
|
||||||
|
private HorizontalScrollView mChipScroll;
|
||||||
|
private LinearLayout mChipContainer;
|
||||||
|
private TextView mChipHint;
|
||||||
|
private TextView mNextButton;
|
||||||
|
private boolean mGroupMode;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onCreate(final Bundle savedInstanceState) {
|
||||||
|
super.onCreate(savedInstanceState);
|
||||||
|
setContentView(R.layout.home_compose_activity);
|
||||||
|
if (getSupportActionBar() != null) {
|
||||||
|
getSupportActionBar().hide();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Presented as a sheet, so the header closes instead of navigating back.
|
||||||
|
final ImageButton back = findViewById(R.id.home_sub_header_back);
|
||||||
|
back.setVisibility(View.GONE);
|
||||||
|
final ImageButton close = findViewById(R.id.home_sub_header_close);
|
||||||
|
close.setVisibility(View.VISIBLE);
|
||||||
|
close.setOnClickListener(v -> finish());
|
||||||
|
|
||||||
|
mHeaderTitle = findViewById(R.id.home_sub_header_title);
|
||||||
|
mHeaderTitle.setText(R.string.home_compose_title);
|
||||||
|
|
||||||
|
final View scrim = findViewById(R.id.home_compose_scrim);
|
||||||
|
// The tap that opened the sheet can land here while it is still sliding up, so the
|
||||||
|
// dismiss handler only arms once the entry animation is done.
|
||||||
|
scrim.postDelayed(() -> scrim.setOnClickListener(v -> finish()), SHEET_ENTER_MS);
|
||||||
|
|
||||||
|
mChipScroll = findViewById(R.id.home_compose_chip_scroll);
|
||||||
|
mChipContainer = findViewById(R.id.home_compose_chips);
|
||||||
|
mChipHint = findViewById(R.id.home_compose_chip_hint);
|
||||||
|
mNextButton = findViewById(R.id.home_compose_next);
|
||||||
|
mNextButton.setOnClickListener(v -> openGroupNameStep());
|
||||||
|
|
||||||
|
mRecipientField = findViewById(R.id.home_compose_recipient);
|
||||||
|
mRecipientField.addTextChangedListener(new TextWatcher() {
|
||||||
|
@Override
|
||||||
|
public void beforeTextChanged(final CharSequence s, final int start, final int count,
|
||||||
|
final int after) {
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onTextChanged(final CharSequence s, final int start, final int before,
|
||||||
|
final int count) {
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void afterTextChanged(final Editable s) {
|
||||||
|
final HomeContactsFragment fragment = getContactsFragment();
|
||||||
|
if (fragment != null) {
|
||||||
|
fragment.setFilterText(s.toString());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
mRecipientField.setOnEditorActionListener((v, actionId, event) -> {
|
||||||
|
final HomeContactsFragment fragment = getContactsFragment();
|
||||||
|
return fragment != null && fragment.startConversationWithFilterText();
|
||||||
|
});
|
||||||
|
|
||||||
|
mCreateGroupButton = findViewById(R.id.home_compose_create_group);
|
||||||
|
mCreateGroupButton.setOnClickListener(v -> enterGroupMode());
|
||||||
|
}
|
||||||
|
|
||||||
|
private HomeContactsFragment getContactsFragment() {
|
||||||
|
return (HomeContactsFragment) getSupportFragmentManager()
|
||||||
|
.findFragmentById(R.id.home_compose_fragment_host);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void enterGroupMode() {
|
||||||
|
mGroupMode = true;
|
||||||
|
ImeUtil.get().hideImeKeyboard(this, mRecipientField);
|
||||||
|
mHeaderTitle.setText(R.string.home_compose_group_title);
|
||||||
|
mRecipientField.setText("");
|
||||||
|
mRecipientField.setVisibility(View.GONE);
|
||||||
|
mCreateGroupButton.setVisibility(View.GONE);
|
||||||
|
mChipScroll.setVisibility(View.VISIBLE);
|
||||||
|
mChipHint.setVisibility(View.VISIBLE);
|
||||||
|
mNextButton.setVisibility(View.VISIBLE);
|
||||||
|
final HomeContactsFragment fragment = getContactsFragment();
|
||||||
|
if (fragment != null) {
|
||||||
|
fragment.setGroupMode(true);
|
||||||
|
}
|
||||||
|
updateNextButton(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void exitGroupMode() {
|
||||||
|
mGroupMode = false;
|
||||||
|
mHeaderTitle.setText(R.string.home_compose_title);
|
||||||
|
mRecipientField.setVisibility(View.VISIBLE);
|
||||||
|
mCreateGroupButton.setVisibility(View.VISIBLE);
|
||||||
|
mChipScroll.setVisibility(View.GONE);
|
||||||
|
mChipHint.setVisibility(View.GONE);
|
||||||
|
mNextButton.setVisibility(View.GONE);
|
||||||
|
mChipContainer.removeAllViews();
|
||||||
|
final HomeContactsFragment fragment = getContactsFragment();
|
||||||
|
if (fragment != null) {
|
||||||
|
fragment.setGroupMode(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onGroupSelectionChanged(final List<RecipientEntry> selected) {
|
||||||
|
mChipContainer.removeAllViews();
|
||||||
|
final LayoutInflater inflater = LayoutInflater.from(this);
|
||||||
|
for (final RecipientEntry entry : selected) {
|
||||||
|
final View chip = inflater.inflate(R.layout.home_compose_chip, mChipContainer, false);
|
||||||
|
final ContactIconView avatar = chip.findViewById(R.id.home_compose_chip_avatar);
|
||||||
|
final ParticipantData participant = ParticipantData.getFromRecipientEntry(entry);
|
||||||
|
avatar.setImageResourceUri(AvatarUriUtil.createAvatarUri(participant),
|
||||||
|
participant.getContactId(), participant.getLookupKey(),
|
||||||
|
participant.getNormalizedDestination());
|
||||||
|
avatar.setImageClickHandlerDisabled(true);
|
||||||
|
final TextView name = chip.findViewById(R.id.home_compose_chip_name);
|
||||||
|
name.setText(ContactRecipientEntryUtils.getDisplayNameForContactList(entry));
|
||||||
|
final LinearLayout.LayoutParams params =
|
||||||
|
(LinearLayout.LayoutParams) chip.getLayoutParams();
|
||||||
|
params.setMarginEnd(getResources().getDimensionPixelSize(R.dimen.looply_space_xs));
|
||||||
|
mChipContainer.addView(chip, params);
|
||||||
|
}
|
||||||
|
mChipHint.setVisibility(selected.isEmpty() ? View.VISIBLE : View.GONE);
|
||||||
|
updateNextButton(selected.size());
|
||||||
|
}
|
||||||
|
|
||||||
|
private void updateNextButton(final int selectedCount) {
|
||||||
|
final boolean enabled = selectedCount >= MIN_GROUP_SIZE;
|
||||||
|
mNextButton.setEnabled(enabled);
|
||||||
|
mNextButton.setAlpha(enabled ? 1f : 0.4f);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void openGroupNameStep() {
|
||||||
|
final HomeContactsFragment fragment = getContactsFragment();
|
||||||
|
if (fragment == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
final ArrayList<ParticipantData> participants = fragment.getSelectedParticipants();
|
||||||
|
if (participants.size() < MIN_GROUP_SIZE) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
startActivityForResult(HomeGroupNameActivity.buildIntent(this, participants),
|
||||||
|
REQUEST_GROUP_NAME);
|
||||||
|
overridePendingTransition(R.anim.home_sheet_open, R.anim.home_activity_hold);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onActivityResult(final int requestCode, final int resultCode,
|
||||||
|
final Intent data) {
|
||||||
|
super.onActivityResult(requestCode, resultCode, data);
|
||||||
|
if (requestCode == REQUEST_GROUP_NAME && resultCode == RESULT_OK) {
|
||||||
|
// The conversation is open, so this sheet should not be behind it.
|
||||||
|
finish();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onBackPressed() {
|
||||||
|
if (mGroupMode) {
|
||||||
|
exitGroupMode();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
super.onBackPressed();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void finish() {
|
||||||
|
if (mRecipientField != null && !TextUtils.isEmpty(mRecipientField.getText())) {
|
||||||
|
ImeUtil.get().hideImeKeyboard(this, mRecipientField);
|
||||||
|
}
|
||||||
|
super.finish();
|
||||||
|
overridePendingTransition(R.anim.home_activity_hold, R.anim.home_sheet_close);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,25 +2,25 @@ package com.android.messaging.ui.home;
|
|||||||
|
|
||||||
import android.database.Cursor;
|
import android.database.Cursor;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
|
import android.text.TextUtils;
|
||||||
import android.view.LayoutInflater;
|
import android.view.LayoutInflater;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
import android.view.ViewGroup;
|
import android.view.ViewGroup;
|
||||||
import android.widget.ListView;
|
import android.widget.ListView;
|
||||||
import android.widget.Toast;
|
import android.widget.TextView;
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
import androidx.fragment.app.Fragment;
|
import androidx.fragment.app.Fragment;
|
||||||
import androidx.loader.app.LoaderManager;
|
import androidx.loader.app.LoaderManager;
|
||||||
|
|
||||||
|
import com.android.ex.chips.RecipientEntry;
|
||||||
import com.android.messaging.R;
|
import com.android.messaging.R;
|
||||||
import com.android.messaging.datamodel.DataModel;
|
import com.android.messaging.datamodel.DataModel;
|
||||||
import com.android.messaging.datamodel.action.GetOrCreateConversationAction;
|
|
||||||
import com.android.messaging.datamodel.action.ActionMonitor;
|
import com.android.messaging.datamodel.action.ActionMonitor;
|
||||||
|
import com.android.messaging.datamodel.action.GetOrCreateConversationAction;
|
||||||
import com.android.messaging.datamodel.action.GetOrCreateConversationAction.GetOrCreateConversationActionListener;
|
import com.android.messaging.datamodel.action.GetOrCreateConversationAction.GetOrCreateConversationActionListener;
|
||||||
import com.android.messaging.datamodel.action.GetOrCreateConversationAction.GetOrCreateConversationActionMonitor;
|
import com.android.messaging.datamodel.action.GetOrCreateConversationAction.GetOrCreateConversationActionMonitor;
|
||||||
import com.android.messaging.util.Assert;
|
|
||||||
import com.android.messaging.util.Assert.RunsOnMainThread;
|
|
||||||
import com.android.messaging.datamodel.binding.Binding;
|
import com.android.messaging.datamodel.binding.Binding;
|
||||||
import com.android.messaging.datamodel.binding.BindingBase;
|
import com.android.messaging.datamodel.binding.BindingBase;
|
||||||
import com.android.messaging.datamodel.data.ContactListItemData;
|
import com.android.messaging.datamodel.data.ContactListItemData;
|
||||||
@@ -29,17 +29,38 @@ import com.android.messaging.datamodel.data.ContactPickerData.ContactPickerDataL
|
|||||||
import com.android.messaging.datamodel.data.ParticipantData;
|
import com.android.messaging.datamodel.data.ParticipantData;
|
||||||
import com.android.messaging.ui.UIIntents;
|
import com.android.messaging.ui.UIIntents;
|
||||||
import com.android.messaging.ui.contact.ContactListItemView;
|
import com.android.messaging.ui.contact.ContactListItemView;
|
||||||
|
import com.android.messaging.util.Assert;
|
||||||
|
import com.android.messaging.util.Assert.RunsOnMainThread;
|
||||||
|
import com.android.messaging.util.ContactRecipientEntryUtils;
|
||||||
import com.android.messaging.util.ContactUtil;
|
import com.android.messaging.util.ContactUtil;
|
||||||
import com.android.messaging.util.LogUtil;
|
import com.android.messaging.util.LogUtil;
|
||||||
|
import com.android.messaging.util.PhoneUtils;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
|
import java.util.LinkedHashMap;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Contact list for the home "new chat" screen. Tapping a row opens that conversation, or picks
|
||||||
|
* the contact when the host screen is building a group.
|
||||||
|
*/
|
||||||
public class HomeContactsFragment extends Fragment implements ContactPickerDataListener,
|
public class HomeContactsFragment extends Fragment implements ContactPickerDataListener,
|
||||||
ContactListItemView.HostInterface, GetOrCreateConversationActionListener {
|
ContactListItemView.HostInterface, GetOrCreateConversationActionListener {
|
||||||
|
|
||||||
|
/** Implemented by a host that lets the user pick several contacts at once. */
|
||||||
|
public interface GroupSelectionHost {
|
||||||
|
void onGroupSelectionChanged(List<RecipientEntry> selected);
|
||||||
|
}
|
||||||
|
|
||||||
private final Binding<ContactPickerData> mBinding = BindingBase.createBinding(this);
|
private final Binding<ContactPickerData> mBinding = BindingBase.createBinding(this);
|
||||||
|
private final LinkedHashMap<String, RecipientEntry> mSelected = new LinkedHashMap<>();
|
||||||
private HomeContactListAdapter mAdapter;
|
private HomeContactListAdapter mAdapter;
|
||||||
private GetOrCreateConversationActionMonitor mMonitor;
|
private GetOrCreateConversationActionMonitor mMonitor;
|
||||||
|
private TextView mEmptyView;
|
||||||
|
private ContactListItemView mSendToView;
|
||||||
|
private Cursor mAllContactsCursor;
|
||||||
|
private String mFilterText;
|
||||||
|
private boolean mGroupMode;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onCreate(@Nullable final Bundle savedInstanceState) {
|
public void onCreate(@Nullable final Bundle savedInstanceState) {
|
||||||
@@ -61,23 +82,58 @@ public class HomeContactsFragment extends Fragment implements ContactPickerDataL
|
|||||||
public void onViewCreated(@NonNull final View view, @Nullable final Bundle savedInstanceState) {
|
public void onViewCreated(@NonNull final View view, @Nullable final Bundle savedInstanceState) {
|
||||||
super.onViewCreated(view, savedInstanceState);
|
super.onViewCreated(view, savedInstanceState);
|
||||||
final ListView listView = view.findViewById(R.id.home_contacts_list);
|
final ListView listView = view.findViewById(R.id.home_contacts_list);
|
||||||
|
mEmptyView = view.findViewById(R.id.home_contacts_empty);
|
||||||
|
mSendToView = view.findViewById(R.id.home_contacts_send_to);
|
||||||
mAdapter = new HomeContactListAdapter(requireContext(), null, this);
|
mAdapter = new HomeContactListAdapter(requireContext(), null, this);
|
||||||
listView.setAdapter(mAdapter);
|
listView.setAdapter(mAdapter);
|
||||||
|
updateResultViews();
|
||||||
|
}
|
||||||
|
|
||||||
view.findViewById(R.id.home_contacts_search).setOnClickListener(v ->
|
/** Switches between opening a conversation on tap and collecting group members. */
|
||||||
Toast.makeText(requireContext(), R.string.home_popover_coming_soon,
|
public void setGroupMode(final boolean groupMode) {
|
||||||
Toast.LENGTH_SHORT).show());
|
if (mGroupMode == groupMode) {
|
||||||
view.findViewById(R.id.home_contacts_invite).setOnClickListener(v ->
|
return;
|
||||||
Toast.makeText(requireContext(), R.string.home_popover_coming_soon,
|
}
|
||||||
Toast.LENGTH_SHORT).show());
|
mGroupMode = groupMode;
|
||||||
view.findViewById(R.id.home_contacts_recent_calls).setOnClickListener(v ->
|
if (!groupMode) {
|
||||||
Toast.makeText(requireContext(), R.string.home_popover_coming_soon,
|
mSelected.clear();
|
||||||
Toast.LENGTH_SHORT).show());
|
}
|
||||||
view.findViewById(R.id.home_contacts_sort).setOnClickListener(v ->
|
notifyGroupSelectionChanged();
|
||||||
Toast.makeText(requireContext(), R.string.home_popover_coming_soon,
|
if (mAdapter != null) {
|
||||||
Toast.LENGTH_SHORT).show());
|
mAdapter.notifyDataSetChanged();
|
||||||
view.findViewById(R.id.home_contacts_add).setOnClickListener(v ->
|
}
|
||||||
UIIntents.get().launchCreateNewConversationActivity(requireActivity(), null));
|
}
|
||||||
|
|
||||||
|
public ArrayList<ParticipantData> getSelectedParticipants() {
|
||||||
|
final ArrayList<ParticipantData> participants = new ArrayList<>(mSelected.size());
|
||||||
|
for (final RecipientEntry entry : mSelected.values()) {
|
||||||
|
participants.add(ParticipantData.getFromRecipientEntry(entry));
|
||||||
|
}
|
||||||
|
return participants;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Narrows the list as the host screen's recipient field is typed into. */
|
||||||
|
public void setFilterText(final String filterText) {
|
||||||
|
mFilterText = filterText;
|
||||||
|
if (mBinding.isBound()) {
|
||||||
|
mBinding.getData().setSearchText(mBinding, filterText);
|
||||||
|
}
|
||||||
|
updateResultViews();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Starts a conversation with the raw text currently typed, if it can be messaged.
|
||||||
|
*
|
||||||
|
* @return whether the text was a usable destination
|
||||||
|
*/
|
||||||
|
public boolean startConversationWithFilterText() {
|
||||||
|
final String destination = mFilterText == null ? null : mFilterText.trim();
|
||||||
|
if (TextUtils.isEmpty(destination)
|
||||||
|
|| !PhoneUtils.isValidSmsMmsDestination(destination)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
startConversation(ContactRecipientEntryUtils.constructSendToDestinationEntry(destination));
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -95,7 +151,19 @@ public class HomeContactsFragment extends Fragment implements ContactPickerDataL
|
|||||||
@Override
|
@Override
|
||||||
public void onAllContactsCursorUpdated(final Cursor data) {
|
public void onAllContactsCursorUpdated(final Cursor data) {
|
||||||
mBinding.ensureBound();
|
mBinding.ensureBound();
|
||||||
|
mAllContactsCursor = data;
|
||||||
|
if (TextUtils.isEmpty(mFilterText)) {
|
||||||
mAdapter.changeCursor(data);
|
mAdapter.changeCursor(data);
|
||||||
|
updateResultViews();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onFilteredContactsCursorUpdated(final Cursor data) {
|
||||||
|
mBinding.ensureBound();
|
||||||
|
// A null cursor means the search was cleared, so fall back to the full list.
|
||||||
|
mAdapter.changeCursor(data != null ? data : mAllContactsCursor);
|
||||||
|
updateResultViews();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -112,29 +180,56 @@ public class HomeContactsFragment extends Fragment implements ContactPickerDataL
|
|||||||
@Override
|
@Override
|
||||||
public void onContactListItemClicked(final ContactListItemData item,
|
public void onContactListItemClicked(final ContactListItemData item,
|
||||||
final ContactListItemView view) {
|
final ContactListItemView view) {
|
||||||
|
if (!mGroupMode) {
|
||||||
|
startConversation(item.getRecipientEntry());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
final RecipientEntry entry = item.getRecipientEntry();
|
||||||
|
final String key = selectionKey(entry);
|
||||||
|
if (mSelected.remove(key) == null) {
|
||||||
|
mSelected.put(key, entry);
|
||||||
|
}
|
||||||
|
notifyGroupSelectionChanged();
|
||||||
|
if (mAdapter != null) {
|
||||||
|
mAdapter.notifyDataSetChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String selectionKey(final RecipientEntry entry) {
|
||||||
|
return PhoneUtils.getDefault().getCanonicalBySystemLocale(entry.getDestination());
|
||||||
|
}
|
||||||
|
|
||||||
|
private void notifyGroupSelectionChanged() {
|
||||||
|
if (getActivity() instanceof GroupSelectionHost) {
|
||||||
|
((GroupSelectionHost) getActivity()).onGroupSelectionChanged(
|
||||||
|
new ArrayList<>(mSelected.values()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void startConversation(final RecipientEntry entry) {
|
||||||
if (mMonitor != null) {
|
if (mMonitor != null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
final ArrayList<ParticipantData> participants = new ArrayList<>(1);
|
final ArrayList<ParticipantData> participants = new ArrayList<>(1);
|
||||||
participants.add(ParticipantData.getFromRecipientEntry(item.getRecipientEntry()));
|
participants.add(ParticipantData.getFromRecipientEntry(entry));
|
||||||
mMonitor = GetOrCreateConversationAction.getOrCreateConversation(participants, null,
|
mMonitor = GetOrCreateConversationAction.getOrCreateConversation(participants, null, this);
|
||||||
this);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean isContactSelected(final ContactListItemData item) {
|
public boolean isContactSelected(final ContactListItemData item) {
|
||||||
return false;
|
return mGroupMode && mSelected.containsKey(selectionKey(item.getRecipientEntry()));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@RunsOnMainThread
|
@RunsOnMainThread
|
||||||
public void onGetOrCreateConversationSucceeded(final ActionMonitor monitor,
|
public void onGetOrCreateConversationSucceeded(final ActionMonitor monitor, final Object data,
|
||||||
final Object data, final String conversationId) {
|
final String conversationId) {
|
||||||
Assert.isTrue(monitor == mMonitor);
|
Assert.isTrue(monitor == mMonitor);
|
||||||
mMonitor = null;
|
mMonitor = null;
|
||||||
if (conversationId != null) {
|
if (conversationId != null && getActivity() != null) {
|
||||||
UIIntents.get().launchConversationActivity(requireActivity(), conversationId, null,
|
UIIntents.get().launchConversationActivity(requireActivity(), conversationId, null,
|
||||||
null, false);
|
null, false);
|
||||||
|
getActivity().finish();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -145,4 +240,41 @@ public class HomeContactsFragment extends Fragment implements ContactPickerDataL
|
|||||||
LogUtil.e(LogUtil.BUGLE_TAG, "HomeContactsFragment onGetOrCreateConversationFailed");
|
LogUtil.e(LogUtil.BUGLE_TAG, "HomeContactsFragment onGetOrCreateConversationFailed");
|
||||||
mMonitor = null;
|
mMonitor = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Shows the typed destination as a tappable row whenever the filter matched no contact, so a
|
||||||
|
* number that is not in the address book can still start a conversation.
|
||||||
|
*/
|
||||||
|
private void updateResultViews() {
|
||||||
|
if (mEmptyView == null || mSendToView == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
final String destination = mFilterText == null ? null : mFilterText.trim();
|
||||||
|
final boolean filtering = !TextUtils.isEmpty(destination);
|
||||||
|
final boolean noMatches = mAdapter == null || mAdapter.getCount() == 0;
|
||||||
|
final boolean showSendTo = filtering && noMatches
|
||||||
|
&& PhoneUtils.isValidSmsMmsDestination(destination);
|
||||||
|
|
||||||
|
if (showSendTo) {
|
||||||
|
final RecipientEntry entry =
|
||||||
|
ContactRecipientEntryUtils.constructSendToDestinationEntry(destination);
|
||||||
|
mSendToView.bind(entry, getString(R.string.home_compose_send_to,
|
||||||
|
ContactRecipientEntryUtils.formatDestination(entry)), null, this, true, false);
|
||||||
|
// The RecipientEntry bind path is meant for dropdowns, which handle clicks themselves.
|
||||||
|
mSendToView.setOnClickListener(v -> startConversation(entry));
|
||||||
|
mSendToView.setVisibility(View.VISIBLE);
|
||||||
|
} else {
|
||||||
|
mSendToView.setVisibility(View.GONE);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!noMatches || showSendTo) {
|
||||||
|
mEmptyView.setVisibility(View.GONE);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
mEmptyView.setText(filtering ? R.string.home_compose_no_results
|
||||||
|
: ContactUtil.hasReadContactsPermission()
|
||||||
|
? R.string.home_compose_empty
|
||||||
|
: R.string.home_compose_permission_required);
|
||||||
|
mEmptyView.setVisibility(View.VISIBLE);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,233 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2025 The LineageOS Project
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.android.messaging.ui.home;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
import android.database.Cursor;
|
||||||
|
import android.os.Bundle;
|
||||||
|
import android.text.Editable;
|
||||||
|
import android.text.Spannable;
|
||||||
|
import android.text.SpannableString;
|
||||||
|
import android.text.TextUtils;
|
||||||
|
import android.text.TextWatcher;
|
||||||
|
import android.text.style.BackgroundColorSpan;
|
||||||
|
import android.view.LayoutInflater;
|
||||||
|
import android.view.View;
|
||||||
|
import android.view.ViewGroup;
|
||||||
|
import android.widget.EditText;
|
||||||
|
import android.widget.ImageButton;
|
||||||
|
import android.widget.ImageView;
|
||||||
|
import android.widget.ListView;
|
||||||
|
import android.widget.TextView;
|
||||||
|
|
||||||
|
import androidx.cursoradapter.widget.CursorAdapter;
|
||||||
|
import androidx.loader.app.LoaderManager;
|
||||||
|
|
||||||
|
import com.android.messaging.R;
|
||||||
|
import com.android.messaging.datamodel.DataModel;
|
||||||
|
import com.android.messaging.datamodel.binding.Binding;
|
||||||
|
import com.android.messaging.datamodel.binding.BindingBase;
|
||||||
|
import com.android.messaging.datamodel.data.ConversationMessageData;
|
||||||
|
import com.android.messaging.datamodel.data.ConversationSearchData;
|
||||||
|
import com.android.messaging.datamodel.data.ConversationSearchData.ConversationSearchDataListener;
|
||||||
|
import com.android.messaging.ui.BugleActionBarActivity;
|
||||||
|
import com.android.messaging.ui.ContactIconView;
|
||||||
|
import com.android.messaging.ui.UIIntents;
|
||||||
|
import com.android.messaging.util.AvatarUriUtil;
|
||||||
|
import com.android.messaging.util.Dates;
|
||||||
|
import com.android.messaging.util.ImeUtil;
|
||||||
|
|
||||||
|
/** Searches the messages of a single conversation as the user types. */
|
||||||
|
public class HomeConversationSearchActivity extends BugleActionBarActivity
|
||||||
|
implements ConversationSearchDataListener {
|
||||||
|
|
||||||
|
private final Binding<ConversationSearchData> mBinding = BindingBase.createBinding(this);
|
||||||
|
|
||||||
|
private String mConversationId;
|
||||||
|
private EditText mSearchField;
|
||||||
|
private ImageButton mClearButton;
|
||||||
|
private ListView mListView;
|
||||||
|
private View mIdleContainer;
|
||||||
|
private TextView mEmptyView;
|
||||||
|
private SearchResultAdapter mAdapter;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onCreate(final Bundle savedInstanceState) {
|
||||||
|
super.onCreate(savedInstanceState);
|
||||||
|
setContentView(R.layout.home_conversation_search_activity);
|
||||||
|
if (getSupportActionBar() != null) {
|
||||||
|
getSupportActionBar().hide();
|
||||||
|
}
|
||||||
|
|
||||||
|
mConversationId = getIntent().getStringExtra(UIIntents.UI_INTENT_EXTRA_CONVERSATION_ID);
|
||||||
|
if (TextUtils.isEmpty(mConversationId)) {
|
||||||
|
finish();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
mListView = findViewById(R.id.home_conversation_search_list);
|
||||||
|
mIdleContainer = findViewById(R.id.home_conversation_search_idle);
|
||||||
|
mEmptyView = findViewById(R.id.home_conversation_search_empty);
|
||||||
|
mAdapter = new SearchResultAdapter(this);
|
||||||
|
mListView.setAdapter(mAdapter);
|
||||||
|
mListView.setOnItemClickListener((parent, view, position, id) -> openConversation());
|
||||||
|
|
||||||
|
HomeSubHeader.bind(this, R.string.home_search_title, v -> finish());
|
||||||
|
|
||||||
|
mClearButton = findViewById(R.id.home_conversation_search_clear);
|
||||||
|
mClearButton.setOnClickListener(v -> mSearchField.setText(""));
|
||||||
|
|
||||||
|
mSearchField = findViewById(R.id.home_conversation_search_field);
|
||||||
|
mSearchField.addTextChangedListener(new TextWatcher() {
|
||||||
|
@Override
|
||||||
|
public void beforeTextChanged(final CharSequence s, final int start, final int count,
|
||||||
|
final int after) {
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onTextChanged(final CharSequence s, final int start, final int before,
|
||||||
|
final int count) {
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void afterTextChanged(final Editable s) {
|
||||||
|
mClearButton.setVisibility(TextUtils.isEmpty(s) ? View.GONE : View.VISIBLE);
|
||||||
|
mBinding.getData().setSearchText(mBinding, s.toString());
|
||||||
|
updateEmptyView();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
mSearchField.setOnEditorActionListener((v, actionId, event) -> {
|
||||||
|
ImeUtil.get().hideImeKeyboard(this, mSearchField);
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
|
||||||
|
mBinding.bind(DataModel.get().createConversationSearchData(this, mConversationId, this));
|
||||||
|
mBinding.getData().init(LoaderManager.getInstance(this));
|
||||||
|
|
||||||
|
mSearchField.requestFocus();
|
||||||
|
mSearchField.post(() -> ImeUtil.get().showImeKeyboard(this, mSearchField));
|
||||||
|
updateEmptyView();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onDestroy() {
|
||||||
|
super.onDestroy();
|
||||||
|
mBinding.unbind();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onConversationSearchCursorUpdated(final Cursor cursor) {
|
||||||
|
mAdapter.swapCursor(cursor);
|
||||||
|
updateEmptyView();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void updateEmptyView() {
|
||||||
|
final String searchText = mBinding.isBound() ? mBinding.getData().getSearchText() : null;
|
||||||
|
final ImageView idleIcon = mIdleContainer == null ? null
|
||||||
|
: mIdleContainer.findViewById(R.id.home_conversation_search_idle_icon);
|
||||||
|
if (TextUtils.isEmpty(searchText)) {
|
||||||
|
mEmptyView.setText(R.string.home_conversation_search_prompt);
|
||||||
|
if (idleIcon != null) {
|
||||||
|
idleIcon.setVisibility(View.VISIBLE);
|
||||||
|
}
|
||||||
|
if (mIdleContainer != null) {
|
||||||
|
mIdleContainer.setVisibility(View.VISIBLE);
|
||||||
|
} else {
|
||||||
|
mEmptyView.setVisibility(View.VISIBLE);
|
||||||
|
}
|
||||||
|
mListView.setVisibility(View.GONE);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
final boolean empty = mAdapter.getCount() == 0;
|
||||||
|
mEmptyView.setText(getString(R.string.home_conversation_search_empty, searchText));
|
||||||
|
if (idleIcon != null) {
|
||||||
|
idleIcon.setVisibility(View.GONE);
|
||||||
|
}
|
||||||
|
if (mIdleContainer != null) {
|
||||||
|
mIdleContainer.setVisibility(empty ? View.VISIBLE : View.GONE);
|
||||||
|
} else {
|
||||||
|
mEmptyView.setVisibility(empty ? View.VISIBLE : View.GONE);
|
||||||
|
}
|
||||||
|
mListView.setVisibility(empty ? View.GONE : View.VISIBLE);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void openConversation() {
|
||||||
|
// Bugle cannot open a conversation anchored on a single message, so land on the latest.
|
||||||
|
UIIntents.get().launchConversationActivity(this, mConversationId, null, null, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void finish() {
|
||||||
|
if (mSearchField != null) {
|
||||||
|
ImeUtil.get().hideImeKeyboard(this, mSearchField);
|
||||||
|
}
|
||||||
|
super.finish();
|
||||||
|
overridePendingTransition(R.anim.home_activity_hold, R.anim.home_activity_close);
|
||||||
|
}
|
||||||
|
|
||||||
|
private class SearchResultAdapter extends CursorAdapter {
|
||||||
|
|
||||||
|
SearchResultAdapter(final Context context) {
|
||||||
|
super(context, null, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public View newView(final Context context, final Cursor cursor, final ViewGroup parent) {
|
||||||
|
return LayoutInflater.from(context)
|
||||||
|
.inflate(R.layout.home_conversation_search_item, parent, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void bindView(final View view, final Context context, final Cursor cursor) {
|
||||||
|
final ConversationMessageData message = new ConversationMessageData();
|
||||||
|
message.bind(cursor);
|
||||||
|
|
||||||
|
final ContactIconView avatar =
|
||||||
|
view.findViewById(R.id.home_conversation_search_avatar);
|
||||||
|
avatar.setImageResourceUri(AvatarUriUtil.createAvatarUri(
|
||||||
|
message.getSenderProfilePhotoUri(), message.getSenderFullName(),
|
||||||
|
message.getSenderNormalizedDestination(),
|
||||||
|
message.getSenderContactLookupKey()),
|
||||||
|
message.getSenderContactId(), message.getSenderContactLookupKey(),
|
||||||
|
message.getSenderNormalizedDestination());
|
||||||
|
avatar.setImageClickHandlerDisabled(true);
|
||||||
|
|
||||||
|
((TextView) view.findViewById(R.id.home_conversation_search_sender)).setText(
|
||||||
|
message.getIsIncoming() ? message.getSenderDisplayName()
|
||||||
|
: getString(R.string.home_conversation_search_you));
|
||||||
|
((TextView) view.findViewById(R.id.home_conversation_search_time)).setText(
|
||||||
|
Dates.getConversationTimeString(message.getReceivedTimeStamp()));
|
||||||
|
((TextView) view.findViewById(R.id.home_conversation_search_text))
|
||||||
|
.setText(highlight(message.getText()));
|
||||||
|
}
|
||||||
|
|
||||||
|
private CharSequence highlight(final String text) {
|
||||||
|
final String searchText = mBinding.getData().getSearchText();
|
||||||
|
if (TextUtils.isEmpty(text) || TextUtils.isEmpty(searchText)) {
|
||||||
|
return text;
|
||||||
|
}
|
||||||
|
final int start = text.toLowerCase().indexOf(searchText.toLowerCase());
|
||||||
|
if (start < 0) {
|
||||||
|
return text;
|
||||||
|
}
|
||||||
|
final SpannableString spannable = new SpannableString(text);
|
||||||
|
spannable.setSpan(new BackgroundColorSpan(getColor(R.color.home_accent_soft)),
|
||||||
|
start, start + searchText.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||||
|
return spannable;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,94 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2025 The LineageOS Project
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.android.messaging.ui.home;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
import android.graphics.drawable.ColorDrawable;
|
||||||
|
import android.view.LayoutInflater;
|
||||||
|
import android.view.View;
|
||||||
|
import android.view.Window;
|
||||||
|
import android.view.WindowManager;
|
||||||
|
import android.widget.TextView;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.appcompat.app.AlertDialog;
|
||||||
|
|
||||||
|
import com.android.messaging.R;
|
||||||
|
|
||||||
|
/** Centered iOS-style alert for delete / delete-and-register-spam from home selection mode. */
|
||||||
|
public final class HomeDeleteAlertDialog {
|
||||||
|
|
||||||
|
public interface Listener {
|
||||||
|
void onDelete();
|
||||||
|
|
||||||
|
void onDeleteAndRegisterSpam();
|
||||||
|
}
|
||||||
|
|
||||||
|
private HomeDeleteAlertDialog() {
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void show(@NonNull final Context context, final int conversationCount,
|
||||||
|
@NonNull final Listener listener) {
|
||||||
|
final View content = LayoutInflater.from(context).inflate(
|
||||||
|
R.layout.home_delete_alert_dialog, null);
|
||||||
|
|
||||||
|
final TextView message = content.findViewById(R.id.home_delete_alert_message);
|
||||||
|
if (message != null) {
|
||||||
|
if (conversationCount == 1) {
|
||||||
|
message.setText(R.string.home_delete_alert_message_one);
|
||||||
|
} else {
|
||||||
|
message.setText(context.getString(
|
||||||
|
R.string.home_delete_alert_message_many, conversationCount));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final AlertDialog dialog = new AlertDialog.Builder(context, R.style.Home_AlertDialog)
|
||||||
|
.setView(content)
|
||||||
|
.setCancelable(true)
|
||||||
|
.create();
|
||||||
|
|
||||||
|
final View delete = content.findViewById(R.id.home_delete_alert_delete);
|
||||||
|
if (delete != null) {
|
||||||
|
delete.setOnClickListener(v -> {
|
||||||
|
dialog.dismiss();
|
||||||
|
listener.onDelete();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
final View deleteSpam = content.findViewById(R.id.home_delete_alert_delete_spam);
|
||||||
|
if (deleteSpam != null) {
|
||||||
|
deleteSpam.setOnClickListener(v -> {
|
||||||
|
dialog.dismiss();
|
||||||
|
listener.onDeleteAndRegisterSpam();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
dialog.show();
|
||||||
|
|
||||||
|
final Window window = dialog.getWindow();
|
||||||
|
if (window != null) {
|
||||||
|
window.setBackgroundDrawable(new ColorDrawable(android.graphics.Color.TRANSPARENT));
|
||||||
|
final int margin = context.getResources().getDimensionPixelSize(
|
||||||
|
R.dimen.home_alert_dialog_horizontal_margin);
|
||||||
|
window.setLayout(
|
||||||
|
WindowManager.LayoutParams.MATCH_PARENT,
|
||||||
|
WindowManager.LayoutParams.WRAP_CONTENT);
|
||||||
|
window.getDecorView().setPadding(margin, 0, margin, 0);
|
||||||
|
}
|
||||||
|
HomeUiMotion.dialogEnter(content);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,434 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2026 The LineageOS Project
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
package com.android.messaging.ui.home;
|
||||||
|
|
||||||
|
import android.app.Activity;
|
||||||
|
import android.content.ContentUris;
|
||||||
|
import android.database.Cursor;
|
||||||
|
import android.net.Uri;
|
||||||
|
import android.os.Handler;
|
||||||
|
import android.os.Looper;
|
||||||
|
import android.provider.MediaStore;
|
||||||
|
import android.view.Gravity;
|
||||||
|
import android.view.LayoutInflater;
|
||||||
|
import android.view.MotionEvent;
|
||||||
|
import android.view.View;
|
||||||
|
import android.view.ViewGroup;
|
||||||
|
import android.view.ViewTreeObserver;
|
||||||
|
import android.view.inputmethod.InputMethodManager;
|
||||||
|
import android.widget.FrameLayout;
|
||||||
|
import android.widget.ImageView;
|
||||||
|
import android.widget.TextView;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.annotation.Nullable;
|
||||||
|
import androidx.core.view.ViewCompat;
|
||||||
|
import androidx.core.view.WindowInsetsCompat;
|
||||||
|
import androidx.recyclerview.widget.GridLayoutManager;
|
||||||
|
import androidx.recyclerview.widget.RecyclerView;
|
||||||
|
|
||||||
|
import com.android.messaging.R;
|
||||||
|
import com.android.messaging.datamodel.media.MessagePartVideoThumbnailRequestDescriptor;
|
||||||
|
import com.android.messaging.datamodel.media.UriImageRequestDescriptor;
|
||||||
|
import com.android.messaging.ui.AsyncImageView;
|
||||||
|
import com.android.messaging.util.OsUtil;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.concurrent.ExecutorService;
|
||||||
|
import java.util.concurrent.Executors;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* In-conversation gallery sheet. Avoids the system Photo Picker overlay, which flashes on open
|
||||||
|
* and drops a black window when the handle is slid down.
|
||||||
|
*/
|
||||||
|
public final class HomeGallerySheet {
|
||||||
|
|
||||||
|
public interface Callback {
|
||||||
|
void onMediaPicked(@NonNull List<Uri> uris);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final int SPAN_COUNT = 3;
|
||||||
|
private static final String[] PROJECTION = new String[] {
|
||||||
|
MediaStore.Files.FileColumns._ID,
|
||||||
|
MediaStore.Files.FileColumns.MEDIA_TYPE,
|
||||||
|
MediaStore.Files.FileColumns.MIME_TYPE,
|
||||||
|
};
|
||||||
|
|
||||||
|
private HomeGallerySheet() {
|
||||||
|
}
|
||||||
|
|
||||||
|
public static boolean dismissIfShowing(@Nullable final Activity activity) {
|
||||||
|
if (activity == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
final View root = activity.findViewById(R.id.home_gallery_root);
|
||||||
|
if (root == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
final Object dismiss = root.getTag();
|
||||||
|
if (dismiss instanceof Runnable) {
|
||||||
|
((Runnable) dismiss).run();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
final ViewGroup parent = (ViewGroup) root.getParent();
|
||||||
|
if (parent != null) {
|
||||||
|
parent.removeView(root);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void show(@NonNull final Activity activity, @NonNull final Callback callback,
|
||||||
|
final Runnable onRequestPermission) {
|
||||||
|
if (activity.findViewById(R.id.home_gallery_root) != null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (OsUtil.hasMediaPermissions()) {
|
||||||
|
final ExecutorService executor = Executors.newSingleThreadExecutor();
|
||||||
|
final Handler handler = new Handler(Looper.getMainLooper());
|
||||||
|
executor.execute(() -> {
|
||||||
|
final List<MediaItem> items = queryMedia(activity);
|
||||||
|
handler.post(() -> {
|
||||||
|
showReady(activity, callback, onRequestPermission, items);
|
||||||
|
executor.shutdown();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
showReady(activity, callback, onRequestPermission, Collections.emptyList());
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void showReady(@NonNull final Activity activity,
|
||||||
|
@NonNull final Callback callback, final Runnable onRequestPermission,
|
||||||
|
@NonNull final List<MediaItem> items) {
|
||||||
|
if (activity.isFinishing() || activity.findViewById(R.id.home_gallery_root) != null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
ViewGroup host = activity.findViewById(R.id.conversation_and_compose_container);
|
||||||
|
if (host == null) {
|
||||||
|
host = activity.findViewById(android.R.id.content);
|
||||||
|
}
|
||||||
|
if (host == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
final View focused = activity.getCurrentFocus();
|
||||||
|
if (focused != null) {
|
||||||
|
final InputMethodManager imm = activity.getSystemService(InputMethodManager.class);
|
||||||
|
if (imm != null) {
|
||||||
|
imm.hideSoftInputFromWindow(focused.getWindowToken(), 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final View content = LayoutInflater.from(activity).inflate(
|
||||||
|
R.layout.home_gallery_sheet, host, false);
|
||||||
|
final View scrim = content.findViewById(R.id.home_gallery_scrim);
|
||||||
|
final View sheet = content.findViewById(R.id.home_gallery_sheet);
|
||||||
|
final View header = content.findViewById(R.id.home_gallery_header);
|
||||||
|
final RecyclerView grid = content.findViewById(R.id.home_gallery_grid);
|
||||||
|
final View empty = content.findViewById(R.id.home_gallery_empty);
|
||||||
|
final TextView emptyMessage = content.findViewById(R.id.home_gallery_empty_message);
|
||||||
|
final View allow = content.findViewById(R.id.home_gallery_allow);
|
||||||
|
|
||||||
|
final int screenHeight = activity.getResources().getDisplayMetrics().heightPixels;
|
||||||
|
if (scrim != null) {
|
||||||
|
scrim.setAlpha(0f);
|
||||||
|
}
|
||||||
|
sheet.setTranslationY(screenHeight);
|
||||||
|
|
||||||
|
final boolean[] closing = new boolean[] { false };
|
||||||
|
final Runnable dismissAnimated = () -> {
|
||||||
|
if (closing[0]) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
closing[0] = true;
|
||||||
|
HomeUiMotion.gallerySheetClose(scrim, sheet, () -> {
|
||||||
|
if (content.getParent() instanceof ViewGroup) {
|
||||||
|
((ViewGroup) content.getParent()).removeView(content);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
content.setTag(dismissAnimated);
|
||||||
|
|
||||||
|
scrim.setOnClickListener(v -> dismissAnimated.run());
|
||||||
|
bindDragToDismiss(header, scrim, sheet, dismissAnimated);
|
||||||
|
|
||||||
|
final Adapter adapter = new Adapter(uri -> {
|
||||||
|
dismissAnimated.run();
|
||||||
|
callback.onMediaPicked(Collections.singletonList(uri));
|
||||||
|
});
|
||||||
|
grid.setLayoutManager(new GridLayoutManager(activity, SPAN_COUNT));
|
||||||
|
grid.setAdapter(adapter);
|
||||||
|
grid.addItemDecoration(new GapDecoration(
|
||||||
|
activity.getResources().getDimensionPixelSize(R.dimen.home_gallery_grid_gap)));
|
||||||
|
|
||||||
|
if (allow != null) {
|
||||||
|
allow.setOnClickListener(v -> {
|
||||||
|
if (onRequestPermission != null) {
|
||||||
|
dismissAnimated.run();
|
||||||
|
onRequestPermission.run();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
final int sheetTop = resolveSheetTop(activity, host);
|
||||||
|
final FrameLayout.LayoutParams overlayLp = new FrameLayout.LayoutParams(
|
||||||
|
ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT);
|
||||||
|
overlayLp.topMargin = sheetTop;
|
||||||
|
content.setLayoutParams(overlayLp);
|
||||||
|
|
||||||
|
final int hostHeight = host.getHeight() > 0 ? host.getHeight() : screenHeight;
|
||||||
|
final float sheetFraction = activity.getResources().getFraction(
|
||||||
|
R.fraction.home_gallery_sheet_height, 1, 1);
|
||||||
|
final FrameLayout.LayoutParams sheetLp = new FrameLayout.LayoutParams(
|
||||||
|
ViewGroup.LayoutParams.MATCH_PARENT,
|
||||||
|
Math.max(1, Math.round(hostHeight * sheetFraction)),
|
||||||
|
Gravity.BOTTOM);
|
||||||
|
sheet.setLayoutParams(sheetLp);
|
||||||
|
|
||||||
|
adapter.setItems(items);
|
||||||
|
if (items.isEmpty()) {
|
||||||
|
empty.setVisibility(View.VISIBLE);
|
||||||
|
if (!OsUtil.hasMediaPermissions()) {
|
||||||
|
emptyMessage.setText(R.string.home_gallery_permission);
|
||||||
|
allow.setVisibility(View.VISIBLE);
|
||||||
|
} else {
|
||||||
|
emptyMessage.setText(R.string.home_gallery_empty);
|
||||||
|
allow.setVisibility(View.GONE);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
empty.setVisibility(View.GONE);
|
||||||
|
allow.setVisibility(View.GONE);
|
||||||
|
}
|
||||||
|
|
||||||
|
host.addView(content);
|
||||||
|
content.getViewTreeObserver().addOnPreDrawListener(
|
||||||
|
new ViewTreeObserver.OnPreDrawListener() {
|
||||||
|
@Override
|
||||||
|
public boolean onPreDraw() {
|
||||||
|
if (content.getViewTreeObserver().isAlive()) {
|
||||||
|
content.getViewTreeObserver().removeOnPreDrawListener(this);
|
||||||
|
}
|
||||||
|
final int height = sheet.getHeight() > 0 ? sheet.getHeight() : screenHeight;
|
||||||
|
sheet.setTranslationY(height);
|
||||||
|
if (scrim != null) {
|
||||||
|
scrim.setAlpha(0f);
|
||||||
|
}
|
||||||
|
HomeUiMotion.gallerySheetOpen(scrim, sheet);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void bindDragToDismiss(final View header, final View scrim, final View sheet,
|
||||||
|
final Runnable dismissAnimated) {
|
||||||
|
header.setOnTouchListener(new View.OnTouchListener() {
|
||||||
|
private float mDownY;
|
||||||
|
private boolean mDragging;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean onTouch(final View v, final MotionEvent event) {
|
||||||
|
switch (event.getActionMasked()) {
|
||||||
|
case MotionEvent.ACTION_DOWN:
|
||||||
|
mDownY = event.getRawY();
|
||||||
|
mDragging = false;
|
||||||
|
return true;
|
||||||
|
case MotionEvent.ACTION_MOVE: {
|
||||||
|
final float dy = Math.max(0f, event.getRawY() - mDownY);
|
||||||
|
if (dy > 0f) {
|
||||||
|
mDragging = true;
|
||||||
|
}
|
||||||
|
sheet.setTranslationY(dy);
|
||||||
|
final int height = Math.max(sheet.getHeight(), 1);
|
||||||
|
scrim.setAlpha(1f - Math.min(1f, dy / height));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
case MotionEvent.ACTION_UP:
|
||||||
|
case MotionEvent.ACTION_CANCEL: {
|
||||||
|
final float dy = Math.max(0f, event.getRawY() - mDownY);
|
||||||
|
final int height = Math.max(sheet.getHeight(), 1);
|
||||||
|
if (mDragging && dy > height * 0.18f) {
|
||||||
|
dismissAnimated.run();
|
||||||
|
} else {
|
||||||
|
sheet.animate().translationY(0f).setDuration(180).start();
|
||||||
|
scrim.animate().alpha(1f).setDuration(180).start();
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int resolveSheetTop(@NonNull final Activity activity,
|
||||||
|
@NonNull final View host) {
|
||||||
|
final View conversationHeader = activity.findViewById(R.id.conversation_header);
|
||||||
|
if (conversationHeader != null && conversationHeader.getHeight() > 0) {
|
||||||
|
final int[] headerLoc = new int[2];
|
||||||
|
final int[] hostLoc = new int[2];
|
||||||
|
conversationHeader.getLocationInWindow(headerLoc);
|
||||||
|
host.getLocationInWindow(hostLoc);
|
||||||
|
return Math.max(0, headerLoc[1] + conversationHeader.getHeight() - hostLoc[1]);
|
||||||
|
}
|
||||||
|
int statusTop = 0;
|
||||||
|
final WindowInsetsCompat insets = ViewCompat.getRootWindowInsets(host);
|
||||||
|
if (insets != null) {
|
||||||
|
statusTop = insets.getInsets(WindowInsetsCompat.Type.statusBars()).top;
|
||||||
|
}
|
||||||
|
return statusTop + activity.getResources().getDimensionPixelSize(
|
||||||
|
R.dimen.home_header_height);
|
||||||
|
}
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
private static List<MediaItem> queryMedia(final Activity activity) {
|
||||||
|
final List<MediaItem> items = new ArrayList<>();
|
||||||
|
final boolean images = OsUtil.hasPermission(android.Manifest.permission.READ_MEDIA_IMAGES);
|
||||||
|
final boolean videos = OsUtil.hasPermission(android.Manifest.permission.READ_MEDIA_VIDEO);
|
||||||
|
if (!images && !videos) {
|
||||||
|
return items;
|
||||||
|
}
|
||||||
|
final StringBuilder selection = new StringBuilder();
|
||||||
|
final List<String> args = new ArrayList<>();
|
||||||
|
if (images) {
|
||||||
|
selection.append(MediaStore.Files.FileColumns.MEDIA_TYPE).append("=?");
|
||||||
|
args.add(String.valueOf(MediaStore.Files.FileColumns.MEDIA_TYPE_IMAGE));
|
||||||
|
}
|
||||||
|
if (videos) {
|
||||||
|
if (selection.length() > 0) {
|
||||||
|
selection.append(" OR ");
|
||||||
|
}
|
||||||
|
selection.append(MediaStore.Files.FileColumns.MEDIA_TYPE).append("=?");
|
||||||
|
args.add(String.valueOf(MediaStore.Files.FileColumns.MEDIA_TYPE_VIDEO));
|
||||||
|
}
|
||||||
|
try (Cursor cursor = activity.getContentResolver().query(
|
||||||
|
MediaStore.Files.getContentUri("external"),
|
||||||
|
PROJECTION,
|
||||||
|
selection.toString(),
|
||||||
|
args.toArray(new String[0]),
|
||||||
|
MediaStore.Files.FileColumns.DATE_ADDED + " DESC")) {
|
||||||
|
if (cursor == null) {
|
||||||
|
return items;
|
||||||
|
}
|
||||||
|
final int idIndex = cursor.getColumnIndexOrThrow(MediaStore.Files.FileColumns._ID);
|
||||||
|
final int typeIndex = cursor.getColumnIndexOrThrow(
|
||||||
|
MediaStore.Files.FileColumns.MEDIA_TYPE);
|
||||||
|
while (cursor.moveToNext()) {
|
||||||
|
final long id = cursor.getLong(idIndex);
|
||||||
|
final int type = cursor.getInt(typeIndex);
|
||||||
|
final boolean video = type == MediaStore.Files.FileColumns.MEDIA_TYPE_VIDEO;
|
||||||
|
final Uri uri = ContentUris.withAppendedId(
|
||||||
|
video ? MediaStore.Video.Media.EXTERNAL_CONTENT_URI
|
||||||
|
: MediaStore.Images.Media.EXTERNAL_CONTENT_URI, id);
|
||||||
|
items.add(new MediaItem(uri, video));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return items;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final class MediaItem {
|
||||||
|
final Uri uri;
|
||||||
|
final boolean video;
|
||||||
|
|
||||||
|
MediaItem(final Uri uri, final boolean video) {
|
||||||
|
this.uri = uri;
|
||||||
|
this.video = video;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final class Adapter extends RecyclerView.Adapter<Holder> {
|
||||||
|
interface Listener {
|
||||||
|
void onItemClick(Uri uri);
|
||||||
|
}
|
||||||
|
|
||||||
|
private final Listener mListener;
|
||||||
|
private final List<MediaItem> mItems = new ArrayList<>();
|
||||||
|
private int mThumbSize;
|
||||||
|
|
||||||
|
Adapter(final Listener listener) {
|
||||||
|
mListener = listener;
|
||||||
|
}
|
||||||
|
|
||||||
|
void setItems(final List<MediaItem> items) {
|
||||||
|
mItems.clear();
|
||||||
|
mItems.addAll(items);
|
||||||
|
notifyDataSetChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
@Override
|
||||||
|
public Holder onCreateViewHolder(@NonNull final ViewGroup parent, final int viewType) {
|
||||||
|
final View view = LayoutInflater.from(parent.getContext())
|
||||||
|
.inflate(R.layout.home_gallery_item, parent, false);
|
||||||
|
if (mThumbSize <= 0) {
|
||||||
|
mThumbSize = parent.getResources().getDisplayMetrics().widthPixels / SPAN_COUNT;
|
||||||
|
}
|
||||||
|
return new Holder(view);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onBindViewHolder(@NonNull final Holder holder, final int position) {
|
||||||
|
holder.bind(mItems.get(position), mThumbSize, mListener);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getItemCount() {
|
||||||
|
return mItems.size();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final class Holder extends RecyclerView.ViewHolder {
|
||||||
|
private final AsyncImageView mImage;
|
||||||
|
private final ImageView mVideoBadge;
|
||||||
|
|
||||||
|
Holder(final View itemView) {
|
||||||
|
super(itemView);
|
||||||
|
mImage = itemView.findViewById(R.id.home_gallery_item_image);
|
||||||
|
mVideoBadge = itemView.findViewById(R.id.home_gallery_item_video);
|
||||||
|
}
|
||||||
|
|
||||||
|
void bind(final MediaItem item, final int thumbSize, final Adapter.Listener listener) {
|
||||||
|
final ViewGroup.LayoutParams params = itemView.getLayoutParams();
|
||||||
|
params.height = thumbSize;
|
||||||
|
itemView.setLayoutParams(params);
|
||||||
|
if (item.video) {
|
||||||
|
mImage.setImageResourceId(new MessagePartVideoThumbnailRequestDescriptor(item.uri));
|
||||||
|
mVideoBadge.setVisibility(View.VISIBLE);
|
||||||
|
} else {
|
||||||
|
mImage.setImageResourceId(new UriImageRequestDescriptor(
|
||||||
|
item.uri, thumbSize, thumbSize, true, false, false, 0, 0));
|
||||||
|
mVideoBadge.setVisibility(View.GONE);
|
||||||
|
}
|
||||||
|
itemView.setOnClickListener(v -> listener.onItemClick(item.uri));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final class GapDecoration extends RecyclerView.ItemDecoration {
|
||||||
|
private final int mGap;
|
||||||
|
|
||||||
|
GapDecoration(final int gap) {
|
||||||
|
mGap = gap;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void getItemOffsets(@NonNull final android.graphics.Rect outRect,
|
||||||
|
@NonNull final View view, @NonNull final RecyclerView parent,
|
||||||
|
@NonNull final RecyclerView.State state) {
|
||||||
|
outRect.set(mGap, mGap, mGap, mGap);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,191 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2025 The LineageOS Project
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.android.messaging.ui.home;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
import android.content.Intent;
|
||||||
|
import android.os.Bundle;
|
||||||
|
import android.text.Editable;
|
||||||
|
import android.text.TextUtils;
|
||||||
|
import android.text.TextWatcher;
|
||||||
|
import android.view.View;
|
||||||
|
import android.widget.EditText;
|
||||||
|
import android.widget.ImageButton;
|
||||||
|
import android.widget.TextView;
|
||||||
|
|
||||||
|
import com.android.messaging.R;
|
||||||
|
import com.android.messaging.datamodel.action.ActionMonitor;
|
||||||
|
import com.android.messaging.datamodel.action.GetOrCreateConversationAction;
|
||||||
|
import com.android.messaging.datamodel.action.GetOrCreateConversationAction.GetOrCreateConversationActionListener;
|
||||||
|
import com.android.messaging.datamodel.action.GetOrCreateConversationAction.GetOrCreateConversationActionMonitor;
|
||||||
|
import com.android.messaging.datamodel.action.RenameConversationAction;
|
||||||
|
import com.android.messaging.datamodel.data.ParticipantData;
|
||||||
|
import com.android.messaging.ui.BugleActionBarActivity;
|
||||||
|
import com.android.messaging.ui.ContactIconView;
|
||||||
|
import com.android.messaging.ui.UIIntents;
|
||||||
|
import com.android.messaging.util.Assert;
|
||||||
|
import com.android.messaging.util.Assert.RunsOnMainThread;
|
||||||
|
import com.android.messaging.util.AvatarUriUtil;
|
||||||
|
import com.android.messaging.util.ImeUtil;
|
||||||
|
import com.android.messaging.util.LogUtil;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
|
||||||
|
/** Last step of the group flow: an optional local name for the conversation. */
|
||||||
|
public class HomeGroupNameActivity extends BugleActionBarActivity
|
||||||
|
implements GetOrCreateConversationActionListener {
|
||||||
|
|
||||||
|
private static final String EXTRA_PARTICIPANTS = "participants";
|
||||||
|
|
||||||
|
public static Intent buildIntent(final Context context,
|
||||||
|
final ArrayList<ParticipantData> participants) {
|
||||||
|
final Intent intent = new Intent(context, HomeGroupNameActivity.class);
|
||||||
|
intent.putParcelableArrayListExtra(EXTRA_PARTICIPANTS, participants);
|
||||||
|
return intent;
|
||||||
|
}
|
||||||
|
|
||||||
|
private ArrayList<ParticipantData> mParticipants;
|
||||||
|
private EditText mNameField;
|
||||||
|
private ImageButton mClearButton;
|
||||||
|
private GetOrCreateConversationActionMonitor mMonitor;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onCreate(final Bundle savedInstanceState) {
|
||||||
|
super.onCreate(savedInstanceState);
|
||||||
|
setContentView(R.layout.home_group_name_activity);
|
||||||
|
if (getSupportActionBar() != null) {
|
||||||
|
getSupportActionBar().hide();
|
||||||
|
}
|
||||||
|
|
||||||
|
mParticipants = getIntent().getParcelableArrayListExtra(EXTRA_PARTICIPANTS);
|
||||||
|
if (mParticipants == null || mParticipants.isEmpty()) {
|
||||||
|
finish();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final ImageButton back = findViewById(R.id.home_sub_header_back);
|
||||||
|
if (back != null) {
|
||||||
|
back.setOnClickListener(v -> finish());
|
||||||
|
}
|
||||||
|
final TextView title = findViewById(R.id.home_sub_header_title);
|
||||||
|
if (title != null) {
|
||||||
|
title.setText(R.string.home_group_name_title);
|
||||||
|
}
|
||||||
|
final View scrim = findViewById(R.id.home_group_name_scrim);
|
||||||
|
if (scrim != null) {
|
||||||
|
scrim.setOnClickListener(v -> finish());
|
||||||
|
}
|
||||||
|
|
||||||
|
bindAvatar(R.id.home_group_avatar_first, 0);
|
||||||
|
bindAvatar(R.id.home_group_avatar_second, 1);
|
||||||
|
|
||||||
|
mNameField = findViewById(R.id.home_group_name_field);
|
||||||
|
mClearButton = findViewById(R.id.home_group_name_clear);
|
||||||
|
mClearButton.setOnClickListener(v -> mNameField.setText(""));
|
||||||
|
mNameField.addTextChangedListener(new TextWatcher() {
|
||||||
|
@Override
|
||||||
|
public void beforeTextChanged(final CharSequence s, final int start, final int count,
|
||||||
|
final int after) {
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onTextChanged(final CharSequence s, final int start, final int before,
|
||||||
|
final int count) {
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void afterTextChanged(final Editable s) {
|
||||||
|
mClearButton.setVisibility(TextUtils.isEmpty(s) ? View.GONE : View.VISIBLE);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
mNameField.setOnEditorActionListener((v, actionId, event) -> {
|
||||||
|
createConversation();
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
|
||||||
|
findViewById(R.id.home_group_name_done).setOnClickListener(v -> createConversation());
|
||||||
|
}
|
||||||
|
|
||||||
|
private void bindAvatar(final int viewId, final int participantIndex) {
|
||||||
|
final ContactIconView view = findViewById(viewId);
|
||||||
|
if (view == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (participantIndex >= mParticipants.size()) {
|
||||||
|
view.setVisibility(View.GONE);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
final ParticipantData participant = mParticipants.get(participantIndex);
|
||||||
|
view.setImageResourceUri(AvatarUriUtil.createAvatarUri(participant),
|
||||||
|
participant.getContactId(), participant.getLookupKey(),
|
||||||
|
participant.getNormalizedDestination());
|
||||||
|
view.setImageClickHandlerDisabled(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void createConversation() {
|
||||||
|
if (mMonitor != null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
ImeUtil.get().hideImeKeyboard(this, mNameField);
|
||||||
|
mMonitor = GetOrCreateConversationAction.getOrCreateConversation(mParticipants, null, this);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@RunsOnMainThread
|
||||||
|
public void onGetOrCreateConversationSucceeded(final ActionMonitor monitor, final Object data,
|
||||||
|
final String conversationId) {
|
||||||
|
Assert.isTrue(monitor == mMonitor);
|
||||||
|
mMonitor = null;
|
||||||
|
if (conversationId == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
final String name = mNameField.getText().toString().trim();
|
||||||
|
if (!TextUtils.isEmpty(name)) {
|
||||||
|
RenameConversationAction.renameConversation(conversationId, name);
|
||||||
|
}
|
||||||
|
UIIntents.get().launchConversationActivity(this, conversationId, null, null, false);
|
||||||
|
// Tells the compose screen underneath to close too, so back returns to the list.
|
||||||
|
setResult(RESULT_OK);
|
||||||
|
finish();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@RunsOnMainThread
|
||||||
|
public void onGetOrCreateConversationFailed(final ActionMonitor monitor, final Object data) {
|
||||||
|
Assert.isTrue(monitor == mMonitor);
|
||||||
|
LogUtil.e(LogUtil.BUGLE_TAG, "HomeGroupNameActivity onGetOrCreateConversationFailed");
|
||||||
|
mMonitor = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onDestroy() {
|
||||||
|
super.onDestroy();
|
||||||
|
if (mMonitor != null) {
|
||||||
|
mMonitor.unregister();
|
||||||
|
mMonitor = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void finish() {
|
||||||
|
if (mNameField != null) {
|
||||||
|
ImeUtil.get().hideImeKeyboard(this, mNameField);
|
||||||
|
}
|
||||||
|
super.finish();
|
||||||
|
overridePendingTransition(R.anim.home_activity_hold, R.anim.home_sheet_close);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -52,7 +52,11 @@ public final class HomePinnedStrip {
|
|||||||
private static final int COLUMN_COUNT = 3;
|
private static final int COLUMN_COUNT = 3;
|
||||||
|
|
||||||
public interface Listener {
|
public interface Listener {
|
||||||
void onPinnedConversationClicked(String conversationId);
|
/** Same as tapping a list-row avatar: show the profile popup. */
|
||||||
|
void onPinnedProfileClicked(String conversationId, View avatarAnchor);
|
||||||
|
|
||||||
|
/** Same as tapping the list row (name / number): open the conversation. */
|
||||||
|
void onPinnedConversationOpen(String conversationId);
|
||||||
|
|
||||||
void onPinnedConversationUnpin(String conversationId);
|
void onPinnedConversationUnpin(String conversationId);
|
||||||
}
|
}
|
||||||
@@ -61,8 +65,7 @@ public final class HomePinnedStrip {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public static void bind(final View stripRoot, final Cursor cursor,
|
public static void bind(final View stripRoot, final Cursor cursor,
|
||||||
final List<String> pinnedIds, final boolean editMode, final boolean selectionMode,
|
final List<String> pinnedIds, final boolean editMode, final Listener listener) {
|
||||||
final Listener listener) {
|
|
||||||
if (stripRoot == null) {
|
if (stripRoot == null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -127,8 +130,9 @@ public final class HomePinnedStrip {
|
|||||||
}
|
}
|
||||||
icon.setImageResourceUri(iconUri, row.getParticipantContactId(),
|
icon.setImageResourceUri(iconUri, row.getParticipantContactId(),
|
||||||
row.getParticipantLookupKey(), row.getOtherParticipantNormalizedDestination());
|
row.getParticipantLookupKey(), row.getOtherParticipantNormalizedDestination());
|
||||||
|
icon.setImageClickHandlerDisabled(true);
|
||||||
label.setText(labelForPinnedRow(row, editMode));
|
label.setText(labelForPinnedRow(row, editMode));
|
||||||
bindUnreadPreview(context, row, editMode, selectionMode, previewContainer, preview,
|
bindUnreadPreview(context, row, editMode, previewContainer, preview,
|
||||||
unreadDot);
|
unreadDot);
|
||||||
if (editMode) {
|
if (editMode) {
|
||||||
remove.setVisibility(View.VISIBLE);
|
remove.setVisibility(View.VISIBLE);
|
||||||
@@ -144,12 +148,30 @@ public final class HomePinnedStrip {
|
|||||||
} else {
|
} else {
|
||||||
stopEditJiggle(avatarStack);
|
stopEditJiggle(avatarStack);
|
||||||
remove.setVisibility(View.GONE);
|
remove.setVisibility(View.GONE);
|
||||||
item.setOnClickListener(v -> {
|
final View.OnClickListener openProfile = v -> {
|
||||||
if (listener != null) {
|
if (listener != null) {
|
||||||
listener.onPinnedConversationClicked(id);
|
listener.onPinnedProfileClicked(id, icon != null ? icon : avatarStack);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
item.setOnClickListener(null);
|
||||||
|
if (pinnedCell != null) {
|
||||||
|
pinnedCell.setOnClickListener(openProfile);
|
||||||
|
}
|
||||||
|
if (avatarStack != null) {
|
||||||
|
avatarStack.setOnClickListener(openProfile);
|
||||||
|
}
|
||||||
|
icon.setOnClickListener(openProfile);
|
||||||
|
if (previewContainer != null) {
|
||||||
|
previewContainer.setOnClickListener(openProfile);
|
||||||
|
}
|
||||||
|
if (label != null) {
|
||||||
|
label.setOnClickListener(v -> {
|
||||||
|
if (listener != null) {
|
||||||
|
listener.onPinnedConversationOpen(id);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
}
|
||||||
currentRow.addView(item);
|
currentRow.addView(item);
|
||||||
itemIndex++;
|
itemIndex++;
|
||||||
column++;
|
column++;
|
||||||
@@ -288,7 +310,7 @@ public final class HomePinnedStrip {
|
|||||||
|
|
||||||
private static void bindUnreadPreview(final Context context,
|
private static void bindUnreadPreview(final Context context,
|
||||||
final ConversationListItemData row, final boolean editMode,
|
final ConversationListItemData row, final boolean editMode,
|
||||||
final boolean selectionMode, final View previewContainer, final TextView preview,
|
final View previewContainer, final TextView preview,
|
||||||
final View unreadDot) {
|
final View unreadDot) {
|
||||||
final boolean unread = !row.getIsRead() && !row.getShowDraft();
|
final boolean unread = !row.getIsRead() && !row.getShowDraft();
|
||||||
if (unreadDot != null) {
|
if (unreadDot != null) {
|
||||||
@@ -297,7 +319,7 @@ public final class HomePinnedStrip {
|
|||||||
if (previewContainer != null) {
|
if (previewContainer != null) {
|
||||||
previewContainer.setTranslationX(0f);
|
previewContainer.setTranslationX(0f);
|
||||||
}
|
}
|
||||||
if (editMode || selectionMode || previewContainer == null || preview == null) {
|
if (editMode || previewContainer == null || preview == null) {
|
||||||
if (previewContainer != null) {
|
if (previewContainer != null) {
|
||||||
previewContainer.setVisibility(View.GONE);
|
previewContainer.setVisibility(View.GONE);
|
||||||
}
|
}
|
||||||
@@ -398,6 +420,12 @@ public final class HomePinnedStrip {
|
|||||||
avatarStack.setRotation(0f);
|
avatarStack.setRotation(0f);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Binds {@code out} from {@code cursor} when {@code conversationId} matches a row. */
|
||||||
|
public static boolean bindConversationListItem(final Cursor cursor,
|
||||||
|
final String conversationId, final ConversationListItemData out) {
|
||||||
|
return bindRow(cursor, conversationId, out);
|
||||||
|
}
|
||||||
|
|
||||||
private static boolean bindRow(final Cursor cursor, final String conversationId,
|
private static boolean bindRow(final Cursor cursor, final String conversationId,
|
||||||
final ConversationListItemData out) {
|
final ConversationListItemData out) {
|
||||||
final int saved = cursor.getPosition();
|
final int saved = cursor.getPosition();
|
||||||
|
|||||||
@@ -0,0 +1,469 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2025 The LineageOS Project
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.android.messaging.ui.home;
|
||||||
|
|
||||||
|
import android.app.NotificationManager;
|
||||||
|
import android.content.Intent;
|
||||||
|
import android.graphics.Point;
|
||||||
|
import android.os.Bundle;
|
||||||
|
import android.provider.Settings;
|
||||||
|
import android.text.TextUtils;
|
||||||
|
import android.view.Display;
|
||||||
|
import android.view.Gravity;
|
||||||
|
import android.view.LayoutInflater;
|
||||||
|
import android.view.View;
|
||||||
|
import android.view.ViewGroup;
|
||||||
|
import android.widget.EditText;
|
||||||
|
import android.widget.ImageButton;
|
||||||
|
import android.widget.ImageView;
|
||||||
|
import android.widget.LinearLayout;
|
||||||
|
import android.widget.PopupMenu;
|
||||||
|
import android.widget.TextView;
|
||||||
|
import android.widget.Toast;
|
||||||
|
|
||||||
|
import androidx.annotation.DrawableRes;
|
||||||
|
import androidx.appcompat.app.AlertDialog;
|
||||||
|
import androidx.annotation.IdRes;
|
||||||
|
import androidx.annotation.StringRes;
|
||||||
|
import androidx.loader.app.LoaderManager;
|
||||||
|
|
||||||
|
import com.android.messaging.R;
|
||||||
|
import com.android.messaging.datamodel.DataModel;
|
||||||
|
import com.android.messaging.datamodel.action.UpdateConversationArchiveStatusAction;
|
||||||
|
import com.android.messaging.datamodel.action.RenameConversationAction;
|
||||||
|
import com.android.messaging.datamodel.binding.Binding;
|
||||||
|
import com.android.messaging.datamodel.binding.BindingBase;
|
||||||
|
import com.android.messaging.datamodel.data.ConversationListItemData;
|
||||||
|
import com.android.messaging.datamodel.data.ConversationProfileData;
|
||||||
|
import com.android.messaging.datamodel.data.ConversationProfileData.ConversationProfileDataListener;
|
||||||
|
import com.android.messaging.datamodel.data.ParticipantData;
|
||||||
|
import com.android.messaging.ui.BugleActionBarActivity;
|
||||||
|
import com.android.messaging.ui.ContactIconView;
|
||||||
|
import com.android.messaging.ui.UIIntents;
|
||||||
|
import com.android.messaging.ui.contact.AddContactsConfirmationDialog;
|
||||||
|
import com.android.messaging.ui.conversation.ConversationActivity;
|
||||||
|
import com.android.messaging.ui.conversationsettings.CopyContactDetailDialog;
|
||||||
|
import com.android.messaging.util.AvatarUriUtil;
|
||||||
|
import com.android.messaging.util.ContactUtil;
|
||||||
|
import com.android.messaging.util.NotificationsUtil;
|
||||||
|
import com.android.messaging.util.PhoneUtils;
|
||||||
|
import com.android.messaging.util.UiUtils;
|
||||||
|
import com.google.common.base.Joiner;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/** Home UI conversation profile: name, shortcuts, options and the people involved. */
|
||||||
|
public class HomeProfileActivity extends BugleActionBarActivity
|
||||||
|
implements ConversationProfileDataListener {
|
||||||
|
|
||||||
|
private final Binding<ConversationProfileData> mBinding = BindingBase.createBinding(this);
|
||||||
|
|
||||||
|
private String mConversationId;
|
||||||
|
private ConversationListItemData mConversation;
|
||||||
|
private List<ParticipantData> mParticipants = new ArrayList<>();
|
||||||
|
|
||||||
|
private TextView mNameView;
|
||||||
|
private LinearLayout mParticipantsContainer;
|
||||||
|
private TextView mParticipantsHeader;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onCreate(final Bundle savedInstanceState) {
|
||||||
|
super.onCreate(savedInstanceState);
|
||||||
|
setContentView(R.layout.home_profile_activity);
|
||||||
|
if (getSupportActionBar() != null) {
|
||||||
|
getSupportActionBar().hide();
|
||||||
|
}
|
||||||
|
|
||||||
|
mConversationId = getIntent().getStringExtra(UIIntents.UI_INTENT_EXTRA_CONVERSATION_ID);
|
||||||
|
if (TextUtils.isEmpty(mConversationId)) {
|
||||||
|
finish();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final ImageButton back = findViewById(R.id.home_sub_header_back);
|
||||||
|
back.setOnClickListener(v -> finish());
|
||||||
|
final TextView headerTitle = findViewById(R.id.home_sub_header_title);
|
||||||
|
if (headerTitle != null) {
|
||||||
|
headerTitle.setVisibility(View.GONE);
|
||||||
|
}
|
||||||
|
|
||||||
|
mNameView = findViewById(R.id.home_profile_name);
|
||||||
|
mParticipantsContainer = findViewById(R.id.home_profile_participants);
|
||||||
|
mParticipantsHeader = findViewById(R.id.home_profile_participants_header);
|
||||||
|
findViewById(R.id.home_profile_rename).setOnClickListener(v -> showRenameDialog());
|
||||||
|
|
||||||
|
bindQuickAction(R.id.home_profile_action_call, R.drawable.ic_phone_small_light,
|
||||||
|
R.string.home_profile_action_call, v -> onPrimaryQuickAction());
|
||||||
|
bindQuickAction(R.id.home_profile_action_add_contact, R.drawable.ic_home_group_add,
|
||||||
|
R.string.home_profile_action_add_contact, v -> onSecondaryQuickAction());
|
||||||
|
bindQuickAction(R.id.home_profile_action_search, R.drawable.ic_home_search_line,
|
||||||
|
R.string.home_profile_action_search,
|
||||||
|
v -> UIIntents.get().launchHomeConversationSearchActivity(this, mConversationId));
|
||||||
|
|
||||||
|
mBinding.bind(DataModel.get().createConversationProfileData(mConversationId, this, this));
|
||||||
|
mBinding.getData().init(LoaderManager.getInstance(this), mBinding);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onDestroy() {
|
||||||
|
super.onDestroy();
|
||||||
|
mBinding.unbind();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onConversationMetadataUpdated(final ConversationProfileData data,
|
||||||
|
final ConversationListItemData conversation) {
|
||||||
|
mBinding.ensureBound(data);
|
||||||
|
mConversation = conversation;
|
||||||
|
updateViews();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onParticipantsListLoaded(final ConversationProfileData data,
|
||||||
|
final List<ParticipantData> participants) {
|
||||||
|
mBinding.ensureBound(data);
|
||||||
|
mParticipants = participants;
|
||||||
|
updateViews();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void updateViews() {
|
||||||
|
if (mConversation == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
updateAvatars();
|
||||||
|
updateName();
|
||||||
|
updateQuickActions();
|
||||||
|
updateOptions();
|
||||||
|
updateParticipants();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void updateAvatars() {
|
||||||
|
final View stack = findViewById(R.id.home_profile_avatar_stack);
|
||||||
|
final ContactIconView single = findViewById(R.id.home_profile_avatar_single);
|
||||||
|
final boolean isGroup = mParticipants.size() > 1;
|
||||||
|
stack.setVisibility(isGroup ? View.VISIBLE : View.GONE);
|
||||||
|
single.setVisibility(isGroup ? View.GONE : View.VISIBLE);
|
||||||
|
if (isGroup) {
|
||||||
|
bindAvatar(R.id.home_profile_avatar_first, 0);
|
||||||
|
bindAvatar(R.id.home_profile_avatar_second, 1);
|
||||||
|
} else {
|
||||||
|
bindAvatar(R.id.home_profile_avatar_single, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void bindAvatar(@IdRes final int viewId, final int index) {
|
||||||
|
final ContactIconView view = findViewById(viewId);
|
||||||
|
if (index >= mParticipants.size()) {
|
||||||
|
view.setVisibility(View.GONE);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
final ParticipantData participant = mParticipants.get(index);
|
||||||
|
view.setImageResourceUri(AvatarUriUtil.createAvatarUri(participant),
|
||||||
|
participant.getContactId(), participant.getLookupKey(),
|
||||||
|
participant.getNormalizedDestination());
|
||||||
|
view.setImageClickHandlerDisabled(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void updateName() {
|
||||||
|
String name = mConversation.getName();
|
||||||
|
if (TextUtils.isEmpty(name) && !mParticipants.isEmpty()) {
|
||||||
|
final List<String> names = new ArrayList<>(mParticipants.size());
|
||||||
|
for (final ParticipantData participant : mParticipants) {
|
||||||
|
names.add(participant.getDisplayName(mParticipants.size() == 1));
|
||||||
|
}
|
||||||
|
name = Joiner.on(", ").join(names);
|
||||||
|
}
|
||||||
|
mNameView.setText(name);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void updateQuickActions() {
|
||||||
|
final boolean isGroup = mParticipants.size() > 1;
|
||||||
|
final View primary = findViewById(R.id.home_profile_action_call);
|
||||||
|
final View secondary = findViewById(R.id.home_profile_action_add_contact);
|
||||||
|
if (isGroup) {
|
||||||
|
bindQuickAction(R.id.home_profile_action_call, R.drawable.ic_home_video_line,
|
||||||
|
R.string.home_profile_action_video, v -> onPrimaryQuickAction());
|
||||||
|
bindQuickAction(R.id.home_profile_action_add_contact, R.drawable.ic_home_group_add,
|
||||||
|
R.string.home_profile_action_add_people, v -> onSecondaryQuickAction());
|
||||||
|
primary.setVisibility(View.VISIBLE);
|
||||||
|
secondary.setVisibility(View.VISIBLE);
|
||||||
|
} else {
|
||||||
|
bindQuickAction(R.id.home_profile_action_call, R.drawable.ic_phone_small_light,
|
||||||
|
R.string.home_profile_action_call, v -> onPrimaryQuickAction());
|
||||||
|
bindQuickAction(R.id.home_profile_action_add_contact, R.drawable.ic_home_group_add,
|
||||||
|
R.string.home_profile_action_add_contact, v -> onSecondaryQuickAction());
|
||||||
|
primary.setVisibility(getCallablePhoneNumber() == null ? View.GONE : View.VISIBLE);
|
||||||
|
final ParticipantData other = getOtherParticipant();
|
||||||
|
final boolean canAddContact = other != null
|
||||||
|
&& !ContactUtil.isParticipantInAddressBook(this,
|
||||||
|
other.getContactId(), other.getLookupKey(),
|
||||||
|
other.getNormalizedDestination());
|
||||||
|
secondary.setVisibility(canAddContact ? View.VISIBLE : View.GONE);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void onPrimaryQuickAction() {
|
||||||
|
if (mParticipants.size() > 1) {
|
||||||
|
Toast.makeText(this, R.string.home_popover_coming_soon, Toast.LENGTH_SHORT).show();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
placeCall();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void onSecondaryQuickAction() {
|
||||||
|
if (mParticipants.size() > 1) {
|
||||||
|
Toast.makeText(this, R.string.home_popover_coming_soon, Toast.LENGTH_SHORT).show();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
addContact();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void updateOptions() {
|
||||||
|
bindOption(R.id.home_profile_option_notifications, R.drawable.ic_home_bell,
|
||||||
|
R.string.home_profile_option_notifications, false, v -> openNotificationSettings());
|
||||||
|
|
||||||
|
final boolean archived = mConversation.getIsArchived();
|
||||||
|
bindOption(R.id.home_profile_option_archive, R.drawable.ic_archive_small_dark,
|
||||||
|
archived ? R.string.home_profile_option_unarchive
|
||||||
|
: R.string.home_profile_option_archive,
|
||||||
|
false, v -> toggleArchived(archived));
|
||||||
|
|
||||||
|
final ParticipantData other = getOtherParticipant();
|
||||||
|
final View spamRow = findViewById(R.id.home_profile_option_spam);
|
||||||
|
if (other == null) {
|
||||||
|
spamRow.setVisibility(View.GONE);
|
||||||
|
} else {
|
||||||
|
spamRow.setVisibility(View.VISIBLE);
|
||||||
|
final boolean blocked = other.isBlocked();
|
||||||
|
bindOption(R.id.home_profile_option_spam, R.drawable.ic_home_menu_spam,
|
||||||
|
blocked ? R.string.home_profile_option_unblock
|
||||||
|
: R.string.home_profile_option_spam,
|
||||||
|
!blocked, v -> toggleBlocked(other, blocked));
|
||||||
|
}
|
||||||
|
|
||||||
|
bindOption(R.id.home_profile_option_delete, R.drawable.ic_home_menu_trash,
|
||||||
|
R.string.home_profile_option_delete, true, v -> confirmDelete());
|
||||||
|
}
|
||||||
|
|
||||||
|
private void updateParticipants() {
|
||||||
|
final int count = mParticipants.size();
|
||||||
|
mParticipantsHeader.setText(count == 1
|
||||||
|
? getString(R.string.home_profile_participants_one)
|
||||||
|
: getString(R.string.home_profile_participants_many, count));
|
||||||
|
|
||||||
|
final TextView addPeople = findViewById(R.id.home_profile_participants_add);
|
||||||
|
if (addPeople != null) {
|
||||||
|
final boolean isGroup = count > 1;
|
||||||
|
addPeople.setVisibility(isGroup ? View.VISIBLE : View.GONE);
|
||||||
|
addPeople.setOnClickListener(isGroup ? v -> Toast.makeText(this,
|
||||||
|
R.string.home_popover_coming_soon, Toast.LENGTH_SHORT).show() : null);
|
||||||
|
}
|
||||||
|
|
||||||
|
mParticipantsContainer.removeAllViews();
|
||||||
|
final LayoutInflater inflater = LayoutInflater.from(this);
|
||||||
|
for (final ParticipantData participant : mParticipants) {
|
||||||
|
final View row = inflater.inflate(R.layout.home_profile_participant_row,
|
||||||
|
mParticipantsContainer, false);
|
||||||
|
final ContactIconView avatar = row.findViewById(R.id.home_profile_participant_avatar);
|
||||||
|
avatar.setImageClickHandlerDisabled(true);
|
||||||
|
avatar.setImageResourceUri(AvatarUriUtil.createAvatarUri(participant),
|
||||||
|
participant.getContactId(), participant.getLookupKey(),
|
||||||
|
participant.getNormalizedDestination());
|
||||||
|
((TextView) row.findViewById(R.id.home_profile_participant_name))
|
||||||
|
.setText(participant.getDisplayName(true));
|
||||||
|
((TextView) row.findViewById(R.id.home_profile_participant_details))
|
||||||
|
.setText(participant.getDisplayDestination());
|
||||||
|
row.setOnClickListener(v -> ContactUtil.openContactDetail(this,
|
||||||
|
participant.getContactId(), participant.getLookupKey(),
|
||||||
|
participant.getNormalizedDestination()));
|
||||||
|
row.findViewById(R.id.home_profile_participant_menu)
|
||||||
|
.setOnClickListener(v -> showParticipantMenu(v, participant));
|
||||||
|
mParticipantsContainer.addView(row);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void bindQuickAction(@IdRes final int rootId, @DrawableRes final int iconRes,
|
||||||
|
@StringRes final int labelRes, final View.OnClickListener listener) {
|
||||||
|
final View root = findViewById(rootId);
|
||||||
|
final ImageView icon = root.findViewById(R.id.home_profile_quick_action_icon);
|
||||||
|
icon.setImageResource(iconRes);
|
||||||
|
// The shared icon set mixes white and black artwork, so always force the tint.
|
||||||
|
icon.setImageTintList(getColorStateList(R.color.home_text_primary));
|
||||||
|
((TextView) root.findViewById(R.id.home_profile_quick_action_label)).setText(labelRes);
|
||||||
|
root.findViewById(R.id.home_profile_quick_action_button).setOnClickListener(listener);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void bindOption(@IdRes final int rootId, @DrawableRes final int iconRes,
|
||||||
|
@StringRes final int labelRes, final boolean destructive,
|
||||||
|
final View.OnClickListener listener) {
|
||||||
|
final View root = findViewById(rootId);
|
||||||
|
final ImageView icon = root.findViewById(R.id.home_profile_option_icon);
|
||||||
|
final TextView label = root.findViewById(R.id.home_profile_option_label);
|
||||||
|
icon.setImageResource(iconRes);
|
||||||
|
icon.setImageTintList(getColorStateList(destructive
|
||||||
|
? R.color.home_destructive : R.color.home_text_primary));
|
||||||
|
label.setText(labelRes);
|
||||||
|
label.setTextColor(getColor(destructive
|
||||||
|
? R.color.home_destructive : R.color.home_text_primary));
|
||||||
|
root.setOnClickListener(listener);
|
||||||
|
}
|
||||||
|
|
||||||
|
private ParticipantData getOtherParticipant() {
|
||||||
|
return mBinding.isBound() ? mBinding.getData().getOtherParticipant() : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String getCallablePhoneNumber() {
|
||||||
|
if (!PhoneUtils.getDefault().isVoiceCapable()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
final ParticipantData other = getOtherParticipant();
|
||||||
|
if (other == null || other.isEmail()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
final String number = other.getSendDestination();
|
||||||
|
return TextUtils.isEmpty(number) ? null : number;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void placeCall() {
|
||||||
|
final String number = getCallablePhoneNumber();
|
||||||
|
if (number == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
final Display display = getWindowManager().getDefaultDisplay();
|
||||||
|
UIIntents.get().launchPhoneCallActivity(this, number,
|
||||||
|
new Point(display.getWidth() / 2, display.getHeight() / 2));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void addContact() {
|
||||||
|
final ParticipantData other = getOtherParticipant();
|
||||||
|
if (other == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
new AddContactsConfirmationDialog(this, AvatarUriUtil.createAvatarUri(other),
|
||||||
|
other.getNormalizedDestination()).show();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void showParticipantMenu(final View anchor, final ParticipantData participant) {
|
||||||
|
// Anchored to the trailing edge so the popup stays on screen.
|
||||||
|
final PopupMenu menu = new PopupMenu(this, anchor, Gravity.END);
|
||||||
|
menu.getMenu().add(R.string.home_profile_participant_copy)
|
||||||
|
.setOnMenuItemClickListener(item -> {
|
||||||
|
new CopyContactDetailDialog(this,
|
||||||
|
participant.getDisplayDestination()).show();
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
if (TextUtils.isEmpty(participant.getLookupKey())) {
|
||||||
|
menu.getMenu().add(R.string.home_profile_participant_add)
|
||||||
|
.setOnMenuItemClickListener(item -> {
|
||||||
|
new AddContactsConfirmationDialog(this,
|
||||||
|
AvatarUriUtil.createAvatarUri(participant),
|
||||||
|
participant.getNormalizedDestination()).show();
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
menu.show();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void showRenameDialog() {
|
||||||
|
final View content = LayoutInflater.from(this)
|
||||||
|
.inflate(R.layout.home_profile_rename_dialog, null);
|
||||||
|
final EditText field = content.findViewById(R.id.home_profile_rename_field);
|
||||||
|
field.setText(mNameView.getText());
|
||||||
|
field.setSelection(field.getText().length());
|
||||||
|
new AlertDialog.Builder(this, R.style.Home_Dialog)
|
||||||
|
.setTitle(R.string.home_profile_rename)
|
||||||
|
.setView(content)
|
||||||
|
.setNegativeButton(android.R.string.cancel, null)
|
||||||
|
.setPositiveButton(R.string.home_profile_rename_save, (dialog, which) -> {
|
||||||
|
final String name = field.getText().toString().trim();
|
||||||
|
// An empty name would hide the conversation everywhere it is listed.
|
||||||
|
if (!TextUtils.isEmpty(name)) {
|
||||||
|
RenameConversationAction.renameConversation(mConversationId, name);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.show();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void openNotificationSettings() {
|
||||||
|
final List<String> names = new ArrayList<>(mParticipants.size());
|
||||||
|
for (final ParticipantData participant : mParticipants) {
|
||||||
|
names.add(participant.getDisplayName(true));
|
||||||
|
}
|
||||||
|
NotificationsUtil.createNotificationChannelGroup(this,
|
||||||
|
NotificationsUtil.CONVERSATION_GROUP_NAME,
|
||||||
|
R.string.notification_channel_messages_title);
|
||||||
|
NotificationsUtil.createNotificationChannel(this, mConversationId,
|
||||||
|
String.join(", ", names), NotificationManager.IMPORTANCE_HIGH,
|
||||||
|
NotificationsUtil.CONVERSATION_GROUP_NAME);
|
||||||
|
final Intent intent = new Intent(Settings.ACTION_APP_NOTIFICATION_SETTINGS);
|
||||||
|
intent.putExtra(Settings.EXTRA_APP_PACKAGE, getPackageName());
|
||||||
|
startActivity(intent);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void toggleArchived(final boolean archived) {
|
||||||
|
if (archived) {
|
||||||
|
UpdateConversationArchiveStatusAction.unarchiveConversation(mConversationId);
|
||||||
|
UiUtils.showToast(R.string.home_profile_unarchived_toast);
|
||||||
|
} else {
|
||||||
|
UpdateConversationArchiveStatusAction.archiveConversation(mConversationId);
|
||||||
|
UiUtils.showToast(R.string.home_profile_archived_toast);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void toggleBlocked(final ParticipantData other, final boolean blocked) {
|
||||||
|
if (blocked) {
|
||||||
|
mBinding.getData().setDestinationBlocked(mBinding, false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
new AlertDialog.Builder(this, 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) -> {
|
||||||
|
mBinding.getData().setDestinationBlocked(mBinding, true);
|
||||||
|
closeConversation();
|
||||||
|
})
|
||||||
|
.show();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void confirmDelete() {
|
||||||
|
new AlertDialog.Builder(this, R.style.Home_Dialog)
|
||||||
|
.setTitle(getResources().getQuantityString(
|
||||||
|
R.plurals.delete_conversations_confirmation_dialog_title, 1))
|
||||||
|
.setNegativeButton(R.string.delete_conversation_decline_button, null)
|
||||||
|
.setPositiveButton(R.string.delete_conversation_confirmation_button,
|
||||||
|
(dialog, which) -> {
|
||||||
|
mConversation.deleteConversation();
|
||||||
|
closeConversation();
|
||||||
|
})
|
||||||
|
.show();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Tells the conversation behind this screen to close as well. */
|
||||||
|
private void closeConversation() {
|
||||||
|
setResult(ConversationActivity.FINISH_RESULT_CODE);
|
||||||
|
finish();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void finish() {
|
||||||
|
super.finish();
|
||||||
|
overridePendingTransition(R.anim.home_activity_hold, R.anim.home_activity_close);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,147 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2025 The LineageOS Project
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.android.messaging.ui.home;
|
||||||
|
|
||||||
|
import android.os.Bundle;
|
||||||
|
import android.text.Editable;
|
||||||
|
import android.text.TextUtils;
|
||||||
|
import android.text.TextWatcher;
|
||||||
|
import android.view.View;
|
||||||
|
import android.widget.EditText;
|
||||||
|
import android.widget.ImageButton;
|
||||||
|
import android.widget.ImageView;
|
||||||
|
import android.widget.TextView;
|
||||||
|
import android.widget.Toast;
|
||||||
|
|
||||||
|
import androidx.annotation.DrawableRes;
|
||||||
|
import androidx.annotation.IdRes;
|
||||||
|
import androidx.annotation.StringRes;
|
||||||
|
|
||||||
|
import com.android.messaging.R;
|
||||||
|
import com.android.messaging.ui.BugleActionBarActivity;
|
||||||
|
import com.android.messaging.util.ImeUtil;
|
||||||
|
|
||||||
|
/** Home-style message search entry point with quick filter tiles. */
|
||||||
|
public class HomeSearchActivity extends BugleActionBarActivity {
|
||||||
|
|
||||||
|
private EditText mSearchField;
|
||||||
|
private ImageButton mClearButton;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onCreate(final Bundle savedInstanceState) {
|
||||||
|
super.onCreate(savedInstanceState);
|
||||||
|
setContentView(R.layout.home_search_activity);
|
||||||
|
if (getSupportActionBar() != null) {
|
||||||
|
getSupportActionBar().hide();
|
||||||
|
}
|
||||||
|
|
||||||
|
HomeSubHeader.bind(this, R.string.home_search_title, v -> finish());
|
||||||
|
|
||||||
|
mSearchField = findViewById(R.id.home_search_field);
|
||||||
|
mClearButton = findViewById(R.id.home_search_clear);
|
||||||
|
if (mClearButton != null) {
|
||||||
|
mClearButton.setOnClickListener(v -> mSearchField.setText(""));
|
||||||
|
}
|
||||||
|
if (mSearchField != null) {
|
||||||
|
mSearchField.addTextChangedListener(new TextWatcher() {
|
||||||
|
@Override
|
||||||
|
public void beforeTextChanged(final CharSequence s, final int start,
|
||||||
|
final int count, final int after) {
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onTextChanged(final CharSequence s, final int start, final int before,
|
||||||
|
final int count) {
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void afterTextChanged(final Editable s) {
|
||||||
|
updateClearButton();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
mSearchField.setOnEditorActionListener((v, actionId, event) -> {
|
||||||
|
submitSearch();
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
mSearchField.requestFocus();
|
||||||
|
mSearchField.post(() -> ImeUtil.get().showImeKeyboard(this, mSearchField));
|
||||||
|
}
|
||||||
|
updateClearButton();
|
||||||
|
|
||||||
|
bindFilter(R.id.home_search_filter_unread, R.drawable.ic_home_menu_unread,
|
||||||
|
R.string.home_search_filter_unread);
|
||||||
|
bindFilter(R.id.home_search_filter_saved, R.drawable.ic_home_menu_person,
|
||||||
|
R.string.home_search_filter_saved);
|
||||||
|
bindFilter(R.id.home_search_filter_unknown, R.drawable.ic_home_menu_spam,
|
||||||
|
R.string.home_search_filter_unknown);
|
||||||
|
bindFilter(R.id.home_search_filter_starred, R.drawable.ic_home_star_line,
|
||||||
|
R.string.home_search_filter_starred);
|
||||||
|
bindFilter(R.id.home_search_filter_images, R.drawable.ic_home_image_line,
|
||||||
|
R.string.home_search_filter_images);
|
||||||
|
bindFilter(R.id.home_search_filter_videos, R.drawable.ic_home_video_line,
|
||||||
|
R.string.home_search_filter_videos);
|
||||||
|
bindFilter(R.id.home_search_filter_places, R.drawable.ic_home_place_line,
|
||||||
|
R.string.home_search_filter_places);
|
||||||
|
bindFilter(R.id.home_search_filter_links, R.drawable.ic_home_link_line,
|
||||||
|
R.string.home_search_filter_links);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void bindFilter(@IdRes final int tileId, @DrawableRes final int iconRes,
|
||||||
|
@StringRes final int labelRes) {
|
||||||
|
final View tile = findViewById(tileId);
|
||||||
|
if (tile == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
final ImageView icon = tile.findViewById(R.id.home_search_filter_icon);
|
||||||
|
final TextView label = tile.findViewById(R.id.home_search_filter_label);
|
||||||
|
if (icon != null) {
|
||||||
|
icon.setImageResource(iconRes);
|
||||||
|
}
|
||||||
|
if (label != null) {
|
||||||
|
label.setText(labelRes);
|
||||||
|
}
|
||||||
|
tile.setOnClickListener(v -> Toast.makeText(this,
|
||||||
|
getString(R.string.home_search_coming_soon, getString(labelRes)),
|
||||||
|
Toast.LENGTH_SHORT).show());
|
||||||
|
}
|
||||||
|
|
||||||
|
private void updateClearButton() {
|
||||||
|
if (mClearButton == null || mSearchField == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
mClearButton.setVisibility(
|
||||||
|
TextUtils.isEmpty(mSearchField.getText()) ? View.GONE : View.VISIBLE);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void submitSearch() {
|
||||||
|
if (mSearchField == null || TextUtils.isEmpty(mSearchField.getText())) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
ImeUtil.get().hideImeKeyboard(this, mSearchField);
|
||||||
|
Toast.makeText(this, getString(R.string.home_search_coming_soon,
|
||||||
|
mSearchField.getText().toString()), Toast.LENGTH_SHORT).show();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void finish() {
|
||||||
|
if (mSearchField != null) {
|
||||||
|
ImeUtil.get().hideImeKeyboard(this, mSearchField);
|
||||||
|
}
|
||||||
|
super.finish();
|
||||||
|
overridePendingTransition(R.anim.home_activity_hold, R.anim.home_activity_close);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2025 The LineageOS Project
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.android.messaging.ui.home;
|
||||||
|
|
||||||
|
import android.os.Bundle;
|
||||||
|
import android.widget.ImageButton;
|
||||||
|
import android.widget.TextView;
|
||||||
|
|
||||||
|
import com.android.messaging.R;
|
||||||
|
import com.android.messaging.ui.BugleActionBarActivity;
|
||||||
|
|
||||||
|
/** Home-style screen to view and remove spam-registered (blocked) numbers. */
|
||||||
|
public class HomeSpamActivity extends BugleActionBarActivity {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onCreate(final Bundle savedInstanceState) {
|
||||||
|
super.onCreate(savedInstanceState);
|
||||||
|
setContentView(R.layout.home_spam_activity);
|
||||||
|
if (getSupportActionBar() != null) {
|
||||||
|
getSupportActionBar().hide();
|
||||||
|
}
|
||||||
|
|
||||||
|
final ImageButton back = findViewById(R.id.home_sub_header_back);
|
||||||
|
if (back != null) {
|
||||||
|
back.setOnClickListener(v -> finish());
|
||||||
|
}
|
||||||
|
final TextView title = findViewById(R.id.home_sub_header_title);
|
||||||
|
if (title != null) {
|
||||||
|
title.setText(R.string.home_spam_title);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void finish() {
|
||||||
|
super.finish();
|
||||||
|
overridePendingTransition(R.anim.home_activity_hold, R.anim.home_activity_close);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,110 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2025 The LineageOS Project
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.android.messaging.ui.home;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
import android.database.Cursor;
|
||||||
|
import android.os.Bundle;
|
||||||
|
import android.view.LayoutInflater;
|
||||||
|
import android.view.View;
|
||||||
|
import android.view.ViewGroup;
|
||||||
|
import android.widget.ListView;
|
||||||
|
import android.widget.TextView;
|
||||||
|
import androidx.cursoradapter.widget.CursorAdapter;
|
||||||
|
import androidx.fragment.app.Fragment;
|
||||||
|
import androidx.loader.app.LoaderManager;
|
||||||
|
|
||||||
|
import com.android.messaging.R;
|
||||||
|
import com.android.messaging.datamodel.DataModel;
|
||||||
|
import com.android.messaging.datamodel.binding.Binding;
|
||||||
|
import com.android.messaging.datamodel.binding.BindingBase;
|
||||||
|
import com.android.messaging.datamodel.data.BlockedParticipantsData;
|
||||||
|
import com.android.messaging.datamodel.data.BlockedParticipantsData.BlockedParticipantsDataListener;
|
||||||
|
import com.android.messaging.ui.BlockedParticipantListItemView;
|
||||||
|
import com.android.messaging.util.Assert;
|
||||||
|
|
||||||
|
/** Home UI list of spam-registered (blocked) senders with unblock on tap. */
|
||||||
|
public class HomeSpamFragment extends Fragment implements BlockedParticipantsDataListener {
|
||||||
|
|
||||||
|
private ListView mListView;
|
||||||
|
private TextView mEmptyView;
|
||||||
|
private BlockedParticipantListAdapter mAdapter;
|
||||||
|
private final Binding<BlockedParticipantsData> mBinding = BindingBase.createBinding(this);
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public View onCreateView(final LayoutInflater inflater, final ViewGroup container,
|
||||||
|
final Bundle savedInstanceState) {
|
||||||
|
final View root = inflater.inflate(R.layout.blocked_participants_fragment, container,
|
||||||
|
false);
|
||||||
|
mListView = root.findViewById(android.R.id.list);
|
||||||
|
mAdapter = new BlockedParticipantListAdapter(requireContext(), null);
|
||||||
|
mListView.setAdapter(mAdapter);
|
||||||
|
mBinding.bind(DataModel.get().createBlockedParticipantsData(requireContext(), this));
|
||||||
|
mBinding.getData().init(LoaderManager.getInstance(this), mBinding);
|
||||||
|
|
||||||
|
final View activityRoot = requireActivity().findViewById(R.id.home_spam_empty);
|
||||||
|
if (activityRoot instanceof TextView) {
|
||||||
|
mEmptyView = (TextView) activityRoot;
|
||||||
|
}
|
||||||
|
return root;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onDestroyView() {
|
||||||
|
super.onDestroyView();
|
||||||
|
mEmptyView = null;
|
||||||
|
mListView = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onDestroy() {
|
||||||
|
super.onDestroy();
|
||||||
|
mBinding.unbind();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onBlockedParticipantsCursorUpdated(final Cursor cursor) {
|
||||||
|
mAdapter.swapCursor(cursor);
|
||||||
|
final boolean empty = mAdapter.getCount() == 0;
|
||||||
|
if (mListView != null) {
|
||||||
|
mListView.setVisibility(empty ? View.GONE : View.VISIBLE);
|
||||||
|
}
|
||||||
|
if (mEmptyView != null) {
|
||||||
|
mEmptyView.setVisibility(empty ? View.VISIBLE : View.GONE);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private class BlockedParticipantListAdapter extends CursorAdapter {
|
||||||
|
|
||||||
|
BlockedParticipantListAdapter(final Context context, final Cursor cursor) {
|
||||||
|
super(context, cursor, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public View newView(final Context context, final Cursor cursor, final ViewGroup parent) {
|
||||||
|
return LayoutInflater.from(context).inflate(
|
||||||
|
R.layout.blocked_participant_list_item_view, parent, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void bindView(final View view, final Context context, final Cursor cursor) {
|
||||||
|
Assert.isTrue(view instanceof BlockedParticipantListItemView);
|
||||||
|
final BlockedParticipantListItemView item = (BlockedParticipantListItemView) view;
|
||||||
|
item.bind(mBinding.getData().createParticipantListItemData(cursor));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2025 The LineageOS Project
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.android.messaging.ui.home;
|
||||||
|
|
||||||
|
import android.app.Activity;
|
||||||
|
import android.view.View;
|
||||||
|
import android.widget.ImageButton;
|
||||||
|
import android.widget.TextView;
|
||||||
|
|
||||||
|
import androidx.annotation.StringRes;
|
||||||
|
|
||||||
|
import com.android.messaging.R;
|
||||||
|
|
||||||
|
/** Binds the shared {@code home_sub_header} back control and title on sub-screens. */
|
||||||
|
public final class HomeSubHeader {
|
||||||
|
|
||||||
|
private HomeSubHeader() {
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void bind(final Activity activity, @StringRes final int titleResId,
|
||||||
|
final View.OnClickListener onBack) {
|
||||||
|
bind(activity, activity.getString(titleResId), onBack);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void bind(final Activity activity, final CharSequence title,
|
||||||
|
final View.OnClickListener onBack) {
|
||||||
|
final ImageButton back = activity.findViewById(R.id.home_sub_header_back);
|
||||||
|
if (back != null) {
|
||||||
|
back.setOnClickListener(onBack);
|
||||||
|
}
|
||||||
|
final TextView headerTitle = activity.findViewById(R.id.home_sub_header_title);
|
||||||
|
if (headerTitle != null && title != null) {
|
||||||
|
headerTitle.setText(title);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void bindBackOnly(final Activity activity,
|
||||||
|
final View.OnClickListener onBack) {
|
||||||
|
final ImageButton back = activity.findViewById(R.id.home_sub_header_back);
|
||||||
|
if (back != null) {
|
||||||
|
back.setOnClickListener(onBack);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,823 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2025 The LineageOS Project
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.android.messaging.ui.home;
|
||||||
|
|
||||||
|
import android.animation.ValueAnimator;
|
||||||
|
import android.view.View;
|
||||||
|
import android.view.ViewGroup;
|
||||||
|
import android.view.View.MeasureSpec;
|
||||||
|
import android.view.animation.PathInterpolator;
|
||||||
|
import android.widget.ImageButton;
|
||||||
|
|
||||||
|
import androidx.interpolator.view.animation.FastOutSlowInInterpolator;
|
||||||
|
|
||||||
|
import com.android.messaging.R;
|
||||||
|
|
||||||
|
import java.util.concurrent.atomic.AtomicBoolean;
|
||||||
|
|
||||||
|
/** Lightweight motion for Looply home chrome (popovers, selection mode, dialogs). */
|
||||||
|
public final class HomeUiMotion {
|
||||||
|
|
||||||
|
private static final int POPOVER_IN_MS = 280;
|
||||||
|
private static final int POPOVER_OUT_MS = 200;
|
||||||
|
private static final int DOCK_SWAP_MS = 220;
|
||||||
|
private static final int PROFILE_PANEL_MS = 320;
|
||||||
|
private static final int UNKNOWN_SENDER_BANNER_MS = 340;
|
||||||
|
private static final int LIST_PROFILE_POPUP_MS = 360;
|
||||||
|
|
||||||
|
private static final PathInterpolator EASE = new PathInterpolator(0.25f, 0.1f, 0.25f, 1f);
|
||||||
|
private static final FastOutSlowInInterpolator EASE_MATERIAL =
|
||||||
|
new FastOutSlowInInterpolator();
|
||||||
|
|
||||||
|
private HomeUiMotion() {
|
||||||
|
}
|
||||||
|
|
||||||
|
public static boolean enabled() {
|
||||||
|
return ValueAnimator.areAnimatorsEnabled();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void popoverEnter(final View content, final boolean pivotEnd) {
|
||||||
|
if (content == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!enabled()) {
|
||||||
|
resetPopoverTransform(content);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
content.setAlpha(0f);
|
||||||
|
content.setScaleX(0.96f);
|
||||||
|
content.setScaleY(0.96f);
|
||||||
|
beginLayer(content);
|
||||||
|
content.post(() -> {
|
||||||
|
content.setPivotX(pivotEnd ? content.getWidth() * 0.9f : content.getWidth() * 0.1f);
|
||||||
|
content.setPivotY(0f);
|
||||||
|
content.animate().cancel();
|
||||||
|
content.animate()
|
||||||
|
.alpha(1f)
|
||||||
|
.scaleX(1f)
|
||||||
|
.scaleY(1f)
|
||||||
|
.setDuration(POPOVER_IN_MS)
|
||||||
|
.setInterpolator(EASE)
|
||||||
|
.withEndAction(() -> endLayer(content))
|
||||||
|
.start();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void popoverExit(final View content, final Runnable endAction) {
|
||||||
|
if (content == null) {
|
||||||
|
if (endAction != null) {
|
||||||
|
endAction.run();
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!enabled()) {
|
||||||
|
if (endAction != null) {
|
||||||
|
endAction.run();
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
beginLayer(content);
|
||||||
|
content.animate().cancel();
|
||||||
|
content.animate()
|
||||||
|
.alpha(0f)
|
||||||
|
.scaleX(0.97f)
|
||||||
|
.scaleY(0.97f)
|
||||||
|
.setDuration(POPOVER_OUT_MS)
|
||||||
|
.setInterpolator(EASE)
|
||||||
|
.withEndAction(() -> {
|
||||||
|
endLayer(content);
|
||||||
|
resetPopoverTransform(content);
|
||||||
|
if (endAction != null) {
|
||||||
|
endAction.run();
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.start();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Swaps two headers that share a vertical container, so they are never both laid out.
|
||||||
|
*/
|
||||||
|
public static void replaceHeaderInstant(final View outgoing, final View incoming) {
|
||||||
|
if (incoming != null) {
|
||||||
|
incoming.animate().cancel();
|
||||||
|
incoming.setVisibility(View.VISIBLE);
|
||||||
|
incoming.setAlpha(1f);
|
||||||
|
incoming.setTranslationY(0f);
|
||||||
|
}
|
||||||
|
if (outgoing != null) {
|
||||||
|
outgoing.animate().cancel();
|
||||||
|
outgoing.setVisibility(View.GONE);
|
||||||
|
outgoing.setAlpha(1f);
|
||||||
|
outgoing.setTranslationY(0f);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Slides the in-conversation profile sheet down over the compact header (and reverses on close).
|
||||||
|
*/
|
||||||
|
public static void conversationProfilePanel(final View compactHeader, final View expandedPanel,
|
||||||
|
final View chevron, final boolean expand, final Runnable onEnd) {
|
||||||
|
final Runnable finish = once(onEnd);
|
||||||
|
if (compactHeader == null && expandedPanel == null) {
|
||||||
|
finish.run();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (compactHeader != null) {
|
||||||
|
compactHeader.animate().cancel();
|
||||||
|
}
|
||||||
|
if (expandedPanel != null) {
|
||||||
|
expandedPanel.animate().cancel();
|
||||||
|
}
|
||||||
|
if (chevron != null) {
|
||||||
|
chevron.animate().cancel();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!enabled()) {
|
||||||
|
if (expand) {
|
||||||
|
replaceHeaderInstant(compactHeader, expandedPanel);
|
||||||
|
if (chevron != null) {
|
||||||
|
chevron.setRotation(180f);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
replaceHeaderInstant(expandedPanel, compactHeader);
|
||||||
|
if (chevron != null) {
|
||||||
|
chevron.setRotation(0f);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
finish.run();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (expand) {
|
||||||
|
if (expandedPanel != null) {
|
||||||
|
expandedPanel.setVisibility(View.VISIBLE);
|
||||||
|
expandedPanel.setAlpha(0f);
|
||||||
|
expandedPanel.setTranslationY(-24f);
|
||||||
|
beginLayer(expandedPanel);
|
||||||
|
}
|
||||||
|
if (compactHeader != null) {
|
||||||
|
beginLayer(compactHeader);
|
||||||
|
}
|
||||||
|
if (chevron != null) {
|
||||||
|
chevron.animate()
|
||||||
|
.rotation(180f)
|
||||||
|
.setDuration(PROFILE_PANEL_MS)
|
||||||
|
.setInterpolator(EASE_MATERIAL)
|
||||||
|
.start();
|
||||||
|
}
|
||||||
|
if (compactHeader != null) {
|
||||||
|
compactHeader.animate()
|
||||||
|
.alpha(0f)
|
||||||
|
.translationY(-12f)
|
||||||
|
.setDuration(PROFILE_PANEL_MS)
|
||||||
|
.setInterpolator(EASE_MATERIAL)
|
||||||
|
.withEndAction(() -> {
|
||||||
|
compactHeader.setVisibility(View.GONE);
|
||||||
|
compactHeader.setAlpha(1f);
|
||||||
|
compactHeader.setTranslationY(0f);
|
||||||
|
endLayer(compactHeader);
|
||||||
|
})
|
||||||
|
.start();
|
||||||
|
}
|
||||||
|
if (expandedPanel != null) {
|
||||||
|
expandedPanel.post(() -> expandedPanel.animate()
|
||||||
|
.alpha(1f)
|
||||||
|
.translationY(0f)
|
||||||
|
.setDuration(PROFILE_PANEL_MS)
|
||||||
|
.setInterpolator(EASE_MATERIAL)
|
||||||
|
.withEndAction(() -> {
|
||||||
|
endLayer(expandedPanel);
|
||||||
|
finish.run();
|
||||||
|
})
|
||||||
|
.start());
|
||||||
|
} else {
|
||||||
|
finish.run();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
final boolean hasCompactHeader = compactHeader != null;
|
||||||
|
if (expandedPanel != null) {
|
||||||
|
beginLayer(expandedPanel);
|
||||||
|
expandedPanel.animate()
|
||||||
|
.alpha(0f)
|
||||||
|
.translationY(-24f)
|
||||||
|
.setDuration(PROFILE_PANEL_MS)
|
||||||
|
.setInterpolator(EASE_MATERIAL)
|
||||||
|
.withEndAction(() -> {
|
||||||
|
expandedPanel.setVisibility(View.GONE);
|
||||||
|
expandedPanel.setAlpha(1f);
|
||||||
|
expandedPanel.setTranslationY(0f);
|
||||||
|
endLayer(expandedPanel);
|
||||||
|
if (!hasCompactHeader) {
|
||||||
|
finish.run();
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.start();
|
||||||
|
}
|
||||||
|
if (chevron != null) {
|
||||||
|
chevron.animate()
|
||||||
|
.rotation(0f)
|
||||||
|
.setDuration(PROFILE_PANEL_MS)
|
||||||
|
.setInterpolator(EASE_MATERIAL)
|
||||||
|
.start();
|
||||||
|
}
|
||||||
|
if (compactHeader != null) {
|
||||||
|
compactHeader.setVisibility(View.VISIBLE);
|
||||||
|
compactHeader.setAlpha(0f);
|
||||||
|
compactHeader.setTranslationY(-12f);
|
||||||
|
beginLayer(compactHeader);
|
||||||
|
compactHeader.animate()
|
||||||
|
.alpha(1f)
|
||||||
|
.translationY(0f)
|
||||||
|
.setDuration(PROFILE_PANEL_MS)
|
||||||
|
.setInterpolator(EASE_MATERIAL)
|
||||||
|
.withEndAction(() -> {
|
||||||
|
endLayer(compactHeader);
|
||||||
|
finish.run();
|
||||||
|
})
|
||||||
|
.start();
|
||||||
|
} else if (expandedPanel == null) {
|
||||||
|
finish.run();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Main bottom nav ↔ selection action dock; both sit stacked at the bottom of a frame. */
|
||||||
|
public static void swapBottomDock(final View mainNav, final View selectionBar,
|
||||||
|
final boolean selectionActive, final Runnable onEnd) {
|
||||||
|
if (mainNav == null && selectionBar == null) {
|
||||||
|
if (onEnd != null) {
|
||||||
|
onEnd.run();
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!enabled()) {
|
||||||
|
applyBottomDockStateInstant(mainNav, selectionBar, selectionActive);
|
||||||
|
if (onEnd != null) {
|
||||||
|
onEnd.run();
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (mainNav != null) {
|
||||||
|
mainNav.animate().cancel();
|
||||||
|
}
|
||||||
|
if (selectionBar != null) {
|
||||||
|
selectionBar.animate().cancel();
|
||||||
|
}
|
||||||
|
|
||||||
|
final Runnable finish = once(onEnd);
|
||||||
|
|
||||||
|
if (selectionActive) {
|
||||||
|
if (mainNav != null && mainNav.getVisibility() == View.VISIBLE) {
|
||||||
|
mainNav.animate()
|
||||||
|
.alpha(0f)
|
||||||
|
.setDuration(DOCK_SWAP_MS)
|
||||||
|
.setInterpolator(EASE_MATERIAL)
|
||||||
|
.withEndAction(() -> {
|
||||||
|
mainNav.setVisibility(View.GONE);
|
||||||
|
mainNav.setAlpha(1f);
|
||||||
|
mainNav.setTranslationY(0f);
|
||||||
|
})
|
||||||
|
.start();
|
||||||
|
} else if (mainNav != null) {
|
||||||
|
mainNav.setVisibility(View.GONE);
|
||||||
|
}
|
||||||
|
if (selectionBar != null) {
|
||||||
|
selectionBar.setVisibility(View.VISIBLE);
|
||||||
|
selectionBar.setAlpha(0f);
|
||||||
|
selectionBar.setTranslationY(0f);
|
||||||
|
selectionBar.animate()
|
||||||
|
.alpha(1f)
|
||||||
|
.setDuration(DOCK_SWAP_MS)
|
||||||
|
.setInterpolator(EASE_MATERIAL)
|
||||||
|
.withEndAction(finish)
|
||||||
|
.start();
|
||||||
|
} else {
|
||||||
|
finish.run();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (selectionBar != null && selectionBar.getVisibility() == View.VISIBLE) {
|
||||||
|
selectionBar.animate()
|
||||||
|
.alpha(0f)
|
||||||
|
.setDuration(DOCK_SWAP_MS)
|
||||||
|
.setInterpolator(EASE_MATERIAL)
|
||||||
|
.withEndAction(() -> {
|
||||||
|
selectionBar.setVisibility(View.GONE);
|
||||||
|
selectionBar.setAlpha(1f);
|
||||||
|
selectionBar.setTranslationY(0f);
|
||||||
|
})
|
||||||
|
.start();
|
||||||
|
} else if (selectionBar != null) {
|
||||||
|
selectionBar.setVisibility(View.GONE);
|
||||||
|
}
|
||||||
|
if (mainNav != null) {
|
||||||
|
mainNav.setVisibility(View.VISIBLE);
|
||||||
|
mainNav.setAlpha(0f);
|
||||||
|
mainNav.setTranslationY(0f);
|
||||||
|
mainNav.animate()
|
||||||
|
.alpha(1f)
|
||||||
|
.setDuration(DOCK_SWAP_MS)
|
||||||
|
.setInterpolator(EASE_MATERIAL)
|
||||||
|
.withEndAction(finish)
|
||||||
|
.start();
|
||||||
|
} else {
|
||||||
|
finish.run();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Slides the unknown-sender save banner down into the message list (Google Messages). */
|
||||||
|
/** Home list avatar popup: expands from the tapped avatar anchor. */
|
||||||
|
public static void listProfilePopupEnter(final View scrim, final View content,
|
||||||
|
final View anchor) {
|
||||||
|
if (content == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!enabled()) {
|
||||||
|
if (scrim != null) {
|
||||||
|
scrim.setAlpha(1f);
|
||||||
|
}
|
||||||
|
content.setAlpha(1f);
|
||||||
|
content.setTranslationY(0f);
|
||||||
|
content.setScaleX(1f);
|
||||||
|
content.setScaleY(1f);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (scrim != null) {
|
||||||
|
scrim.setAlpha(0f);
|
||||||
|
scrim.animate().alpha(1f).setDuration(LIST_PROFILE_POPUP_MS)
|
||||||
|
.setInterpolator(EASE_MATERIAL).start();
|
||||||
|
}
|
||||||
|
content.animate().cancel();
|
||||||
|
content.setTranslationY(0f);
|
||||||
|
content.setAlpha(0f);
|
||||||
|
content.setScaleX(0.92f);
|
||||||
|
content.setScaleY(0.92f);
|
||||||
|
applyListProfilePopupPivot(content, anchor);
|
||||||
|
beginLayer(content);
|
||||||
|
content.animate()
|
||||||
|
.alpha(1f)
|
||||||
|
.scaleX(1f)
|
||||||
|
.scaleY(1f)
|
||||||
|
.setDuration(LIST_PROFILE_POPUP_MS)
|
||||||
|
.setInterpolator(EASE_MATERIAL)
|
||||||
|
.withEndAction(() -> endLayer(content))
|
||||||
|
.start();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void applyListProfilePopupPivot(final View content, final View anchor) {
|
||||||
|
if (anchor == null) {
|
||||||
|
content.setPivotX(content.getWidth() * 0.5f);
|
||||||
|
content.setPivotY(0f);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
final int[] anchorOnScreen = new int[2];
|
||||||
|
final int[] contentOnScreen = new int[2];
|
||||||
|
anchor.getLocationOnScreen(anchorOnScreen);
|
||||||
|
content.getLocationOnScreen(contentOnScreen);
|
||||||
|
content.setPivotX(anchorOnScreen[0] + anchor.getWidth() * 0.5f - contentOnScreen[0]);
|
||||||
|
content.setPivotY(anchorOnScreen[1] + anchor.getHeight() * 0.5f - contentOnScreen[1]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void listProfilePopupExit(final View scrim, final View content,
|
||||||
|
final Runnable endAction) {
|
||||||
|
if (content == null) {
|
||||||
|
if (endAction != null) {
|
||||||
|
endAction.run();
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!enabled()) {
|
||||||
|
if (endAction != null) {
|
||||||
|
endAction.run();
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
content.animate().cancel();
|
||||||
|
if (scrim != null) {
|
||||||
|
scrim.animate().alpha(0f).setDuration(LIST_PROFILE_POPUP_MS * 2 / 3)
|
||||||
|
.setInterpolator(EASE_MATERIAL).start();
|
||||||
|
}
|
||||||
|
beginLayer(content);
|
||||||
|
content.animate()
|
||||||
|
.alpha(0f)
|
||||||
|
.translationY(-24f)
|
||||||
|
.scaleX(0.96f)
|
||||||
|
.scaleY(0.96f)
|
||||||
|
.setDuration(LIST_PROFILE_POPUP_MS * 2 / 3)
|
||||||
|
.setInterpolator(EASE_MATERIAL)
|
||||||
|
.withEndAction(() -> {
|
||||||
|
content.setTranslationY(0f);
|
||||||
|
content.setScaleX(1f);
|
||||||
|
content.setScaleY(1f);
|
||||||
|
content.setAlpha(1f);
|
||||||
|
endLayer(content);
|
||||||
|
if (endAction != null) {
|
||||||
|
endAction.run();
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.start();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void unknownSenderBannerEnter(final View banner) {
|
||||||
|
if (banner == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!enabled()) {
|
||||||
|
banner.setVisibility(View.VISIBLE);
|
||||||
|
banner.setAlpha(1f);
|
||||||
|
banner.setTranslationY(0f);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
banner.animate().cancel();
|
||||||
|
banner.setVisibility(View.VISIBLE);
|
||||||
|
banner.setAlpha(0f);
|
||||||
|
banner.post(() -> {
|
||||||
|
final int height = banner.getHeight();
|
||||||
|
final float startY = -(height > 0 ? height : banner.getResources()
|
||||||
|
.getDimensionPixelSize(R.dimen.home_unknown_sender_banner_slide));
|
||||||
|
banner.setTranslationY(startY);
|
||||||
|
beginLayer(banner);
|
||||||
|
banner.animate()
|
||||||
|
.alpha(1f)
|
||||||
|
.translationY(0f)
|
||||||
|
.setDuration(UNKNOWN_SENDER_BANNER_MS)
|
||||||
|
.setInterpolator(EASE_MATERIAL)
|
||||||
|
.withEndAction(() -> endLayer(banner))
|
||||||
|
.start();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void unknownSenderBannerHide(final View banner, final Runnable endAction) {
|
||||||
|
if (banner == null) {
|
||||||
|
if (endAction != null) {
|
||||||
|
endAction.run();
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!enabled() || banner.getVisibility() != View.VISIBLE) {
|
||||||
|
if (endAction != null) {
|
||||||
|
endAction.run();
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
banner.animate().cancel();
|
||||||
|
beginLayer(banner);
|
||||||
|
final int height = banner.getHeight();
|
||||||
|
final float endY = -(height > 0 ? height : banner.getResources()
|
||||||
|
.getDimensionPixelSize(R.dimen.home_unknown_sender_banner_slide));
|
||||||
|
banner.animate()
|
||||||
|
.alpha(0f)
|
||||||
|
.translationY(endY)
|
||||||
|
.setDuration(UNKNOWN_SENDER_BANNER_MS * 2 / 3)
|
||||||
|
.setInterpolator(EASE_MATERIAL)
|
||||||
|
.withEndAction(() -> {
|
||||||
|
banner.setVisibility(View.GONE);
|
||||||
|
banner.setAlpha(1f);
|
||||||
|
banner.setTranslationY(0f);
|
||||||
|
endLayer(banner);
|
||||||
|
if (endAction != null) {
|
||||||
|
endAction.run();
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.start();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void dialogEnter(final View content) {
|
||||||
|
if (content == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!enabled()) {
|
||||||
|
content.setAlpha(1f);
|
||||||
|
content.setScaleX(1f);
|
||||||
|
content.setScaleY(1f);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
content.setAlpha(0f);
|
||||||
|
content.setScaleX(0.94f);
|
||||||
|
content.setScaleY(0.94f);
|
||||||
|
beginLayer(content);
|
||||||
|
content.animate()
|
||||||
|
.alpha(1f)
|
||||||
|
.scaleX(1f)
|
||||||
|
.scaleY(1f)
|
||||||
|
.setDuration(300)
|
||||||
|
.setInterpolator(EASE)
|
||||||
|
.withEndAction(() -> endLayer(content))
|
||||||
|
.start();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void applyBottomDockStateInstant(final View mainNav, final View selectionBar,
|
||||||
|
final boolean selectionActive) {
|
||||||
|
if (mainNav != null) {
|
||||||
|
mainNav.setVisibility(selectionActive ? View.GONE : View.VISIBLE);
|
||||||
|
mainNav.setAlpha(1f);
|
||||||
|
mainNav.setTranslationY(0f);
|
||||||
|
}
|
||||||
|
if (selectionBar != null) {
|
||||||
|
selectionBar.setVisibility(selectionActive ? View.VISIBLE : View.GONE);
|
||||||
|
selectionBar.setAlpha(1f);
|
||||||
|
selectionBar.setTranslationY(0f);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void popoverResetInstant(final View content) {
|
||||||
|
if (content == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
content.animate().cancel();
|
||||||
|
resetPopoverTransform(content);
|
||||||
|
content.setLayerType(View.LAYER_TYPE_NONE, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void resetPopoverTransform(final View content) {
|
||||||
|
content.setAlpha(1f);
|
||||||
|
content.setScaleX(1f);
|
||||||
|
content.setScaleY(1f);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void beginLayer(final View view) {
|
||||||
|
view.setLayerType(View.LAYER_TYPE_HARDWARE, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void endLayer(final View view) {
|
||||||
|
view.setLayerType(View.LAYER_TYPE_NONE, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Runnable once(final Runnable delegate) {
|
||||||
|
if (delegate == null) {
|
||||||
|
return () -> { };
|
||||||
|
}
|
||||||
|
final AtomicBoolean fired = new AtomicBoolean(false);
|
||||||
|
return () -> {
|
||||||
|
if (fired.compareAndSet(false, true)) {
|
||||||
|
delegate.run();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final int COMPOSE_ATTACH_SHEET_MS = 320;
|
||||||
|
private static ValueAnimator sComposeAttachHeightAnimator;
|
||||||
|
|
||||||
|
/** Expands sheet height so the compose bar is pushed up with the grid (reference Messages UI). */
|
||||||
|
public static void composeAttachmentSheetOpen(final View sheet,
|
||||||
|
final ImageButton attachButton) {
|
||||||
|
if (sheet == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
cancelComposeAttachHeightAnimator();
|
||||||
|
sheet.animate().cancel();
|
||||||
|
if (attachButton != null) {
|
||||||
|
attachButton.animate().cancel();
|
||||||
|
}
|
||||||
|
if (!enabled()) {
|
||||||
|
setComposeAttachSheetExpanded(sheet, true);
|
||||||
|
applyComposeAttachCloseIcon(attachButton);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
sheet.setVisibility(View.VISIBLE);
|
||||||
|
sheet.post(() -> {
|
||||||
|
final int targetHeight = measureComposeAttachSheetHeight(sheet);
|
||||||
|
animateComposeAttachSheetHeight(sheet, 0, targetHeight, () -> {
|
||||||
|
setComposeAttachSheetExpanded(sheet, true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
animateComposeAttachOpen(attachButton);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Collapses sheet height and restores the + icon. */
|
||||||
|
public static void composeAttachmentSheetClose(final View sheet, final ImageButton attachButton,
|
||||||
|
final Runnable endAction) {
|
||||||
|
if (sheet == null) {
|
||||||
|
if (endAction != null) {
|
||||||
|
endAction.run();
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
cancelComposeAttachHeightAnimator();
|
||||||
|
sheet.animate().cancel();
|
||||||
|
if (attachButton != null) {
|
||||||
|
attachButton.animate().cancel();
|
||||||
|
}
|
||||||
|
if (!enabled()) {
|
||||||
|
setComposeAttachSheetExpanded(sheet, false);
|
||||||
|
applyComposeAttachOpenIcon(attachButton);
|
||||||
|
if (endAction != null) {
|
||||||
|
endAction.run();
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
final int startHeight = sheet.getLayoutParams().height > 0 ? sheet.getLayoutParams().height
|
||||||
|
: sheet.getHeight();
|
||||||
|
animateComposeAttachSheetHeight(sheet, startHeight, 0, once(() -> {
|
||||||
|
setComposeAttachSheetExpanded(sheet, false);
|
||||||
|
if (endAction != null) {
|
||||||
|
endAction.run();
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
animateComposeAttachClose(attachButton);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void cancelComposeAttachHeightAnimator() {
|
||||||
|
if (sComposeAttachHeightAnimator != null) {
|
||||||
|
sComposeAttachHeightAnimator.cancel();
|
||||||
|
sComposeAttachHeightAnimator = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int measureComposeAttachSheetHeight(final View sheet) {
|
||||||
|
final ViewGroup.LayoutParams params = sheet.getLayoutParams();
|
||||||
|
final int savedHeight = params.height;
|
||||||
|
final int savedVisibility = sheet.getVisibility();
|
||||||
|
params.height = ViewGroup.LayoutParams.WRAP_CONTENT;
|
||||||
|
sheet.setVisibility(View.INVISIBLE);
|
||||||
|
final ViewGroup parent = (ViewGroup) sheet.getParent();
|
||||||
|
final int widthSpec = MeasureSpec.makeMeasureSpec(
|
||||||
|
parent != null && parent.getWidth() > 0 ? parent.getWidth()
|
||||||
|
: sheet.getResources().getDisplayMetrics().widthPixels,
|
||||||
|
MeasureSpec.EXACTLY);
|
||||||
|
sheet.measure(widthSpec, MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED));
|
||||||
|
final int measured = sheet.getMeasuredHeight();
|
||||||
|
params.height = savedHeight;
|
||||||
|
sheet.setVisibility(savedVisibility);
|
||||||
|
sheet.setLayoutParams(params);
|
||||||
|
return measured;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void animateComposeAttachSheetHeight(final View sheet, final int from,
|
||||||
|
final int to, final Runnable endAction) {
|
||||||
|
final ViewGroup.LayoutParams params = sheet.getLayoutParams();
|
||||||
|
params.height = from;
|
||||||
|
sheet.setLayoutParams(params);
|
||||||
|
sheet.setVisibility(View.VISIBLE);
|
||||||
|
sComposeAttachHeightAnimator = ValueAnimator.ofInt(from, to);
|
||||||
|
sComposeAttachHeightAnimator.setDuration(COMPOSE_ATTACH_SHEET_MS);
|
||||||
|
sComposeAttachHeightAnimator.setInterpolator(EASE_MATERIAL);
|
||||||
|
sComposeAttachHeightAnimator.addUpdateListener(animation -> {
|
||||||
|
params.height = (Integer) animation.getAnimatedValue();
|
||||||
|
sheet.setLayoutParams(params);
|
||||||
|
});
|
||||||
|
sComposeAttachHeightAnimator.addListener(new android.animation.AnimatorListenerAdapter() {
|
||||||
|
@Override
|
||||||
|
public void onAnimationEnd(final android.animation.Animator animation) {
|
||||||
|
sComposeAttachHeightAnimator = null;
|
||||||
|
if (endAction != null) {
|
||||||
|
endAction.run();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
sComposeAttachHeightAnimator.start();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void setComposeAttachSheetExpanded(final View sheet, final boolean expanded) {
|
||||||
|
final ViewGroup.LayoutParams params = sheet.getLayoutParams();
|
||||||
|
if (expanded) {
|
||||||
|
sheet.setVisibility(View.VISIBLE);
|
||||||
|
params.height = ViewGroup.LayoutParams.WRAP_CONTENT;
|
||||||
|
} else {
|
||||||
|
sheet.setVisibility(View.GONE);
|
||||||
|
params.height = 0;
|
||||||
|
}
|
||||||
|
sheet.setLayoutParams(params);
|
||||||
|
sheet.setTranslationY(0f);
|
||||||
|
sheet.setAlpha(1f);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void animateComposeAttachOpen(final ImageButton attachButton) {
|
||||||
|
if (attachButton == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
applyComposeAttachOpenIcon(attachButton);
|
||||||
|
beginLayer(attachButton);
|
||||||
|
attachButton.animate()
|
||||||
|
.rotation(90f)
|
||||||
|
.setDuration(COMPOSE_ATTACH_SHEET_MS)
|
||||||
|
.setInterpolator(EASE_MATERIAL)
|
||||||
|
.withEndAction(() -> {
|
||||||
|
applyComposeAttachCloseIcon(attachButton);
|
||||||
|
endLayer(attachButton);
|
||||||
|
})
|
||||||
|
.start();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void animateComposeAttachClose(final ImageButton attachButton) {
|
||||||
|
if (attachButton == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
applyComposeAttachCloseIcon(attachButton);
|
||||||
|
attachButton.setRotation(90f);
|
||||||
|
beginLayer(attachButton);
|
||||||
|
attachButton.animate()
|
||||||
|
.rotation(0f)
|
||||||
|
.setDuration(COMPOSE_ATTACH_SHEET_MS)
|
||||||
|
.setInterpolator(EASE_MATERIAL)
|
||||||
|
.withEndAction(() -> {
|
||||||
|
applyComposeAttachOpenIcon(attachButton);
|
||||||
|
endLayer(attachButton);
|
||||||
|
})
|
||||||
|
.start();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void applyComposeAttachOpenIcon(final ImageButton attachButton) {
|
||||||
|
if (attachButton == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
attachButton.setImageResource(R.drawable.ic_home_compose_plus);
|
||||||
|
attachButton.setRotation(0f);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void applyComposeAttachCloseIcon(final ImageButton attachButton) {
|
||||||
|
if (attachButton == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
attachButton.setImageResource(R.drawable.ic_home_close);
|
||||||
|
attachButton.setRotation(0f);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final int GALLERY_SHEET_MS = 280;
|
||||||
|
|
||||||
|
/** Slides the in-conversation gallery sheet up over a fading scrim. */
|
||||||
|
public static void gallerySheetOpen(final View scrim, final View sheet) {
|
||||||
|
if (sheet == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (scrim != null) {
|
||||||
|
scrim.animate().cancel();
|
||||||
|
}
|
||||||
|
sheet.animate().cancel();
|
||||||
|
final int height = sheet.getHeight() > 0 ? sheet.getHeight()
|
||||||
|
: sheet.getResources().getDisplayMetrics().heightPixels;
|
||||||
|
if (!enabled()) {
|
||||||
|
if (scrim != null) {
|
||||||
|
scrim.setAlpha(1f);
|
||||||
|
}
|
||||||
|
sheet.setTranslationY(0f);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (scrim != null) {
|
||||||
|
scrim.setAlpha(0f);
|
||||||
|
}
|
||||||
|
sheet.setTranslationY(height);
|
||||||
|
if (scrim != null) {
|
||||||
|
scrim.animate().alpha(1f).setDuration(GALLERY_SHEET_MS)
|
||||||
|
.setInterpolator(EASE_MATERIAL).start();
|
||||||
|
}
|
||||||
|
beginLayer(sheet);
|
||||||
|
sheet.animate()
|
||||||
|
.translationY(0f)
|
||||||
|
.setDuration(GALLERY_SHEET_MS)
|
||||||
|
.setInterpolator(EASE_MATERIAL)
|
||||||
|
.withEndAction(() -> endLayer(sheet))
|
||||||
|
.start();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Slides the gallery sheet down and fades the scrim; conversation stays visible. */
|
||||||
|
public static void gallerySheetClose(final View scrim, final View sheet,
|
||||||
|
final Runnable endAction) {
|
||||||
|
final Runnable finish = once(endAction);
|
||||||
|
if (sheet == null) {
|
||||||
|
if (scrim != null) {
|
||||||
|
scrim.setAlpha(0f);
|
||||||
|
}
|
||||||
|
finish.run();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (scrim != null) {
|
||||||
|
scrim.animate().cancel();
|
||||||
|
}
|
||||||
|
sheet.animate().cancel();
|
||||||
|
if (!enabled()) {
|
||||||
|
finish.run();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
final int height = sheet.getHeight() > 0 ? sheet.getHeight()
|
||||||
|
: sheet.getResources().getDisplayMetrics().heightPixels;
|
||||||
|
if (scrim != null) {
|
||||||
|
scrim.animate().alpha(0f).setDuration(GALLERY_SHEET_MS)
|
||||||
|
.setInterpolator(EASE_MATERIAL).start();
|
||||||
|
}
|
||||||
|
beginLayer(sheet);
|
||||||
|
sheet.animate()
|
||||||
|
.translationY(height)
|
||||||
|
.setDuration(GALLERY_SHEET_MS)
|
||||||
|
.setInterpolator(EASE_MATERIAL)
|
||||||
|
.withEndAction(() -> {
|
||||||
|
endLayer(sheet);
|
||||||
|
finish.run();
|
||||||
|
})
|
||||||
|
.start();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2025 The LineageOS Project
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.android.messaging.ui.home;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
import android.content.SharedPreferences;
|
||||||
|
import android.text.TextUtils;
|
||||||
|
|
||||||
|
/** Remembers when the user dismissed the unknown-sender banner for a conversation. */
|
||||||
|
public final class UnknownSenderPromptStore {
|
||||||
|
|
||||||
|
private static final String PREFS_NAME = "home_unknown_sender_prompt";
|
||||||
|
private static final String KEY_PREFIX = "dismissed_";
|
||||||
|
|
||||||
|
private UnknownSenderPromptStore() {
|
||||||
|
}
|
||||||
|
|
||||||
|
public static boolean isDismissed(final Context context, final String conversationId) {
|
||||||
|
if (TextUtils.isEmpty(conversationId)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return prefs(context).getBoolean(KEY_PREFIX + conversationId, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void setDismissed(final Context context, final String conversationId) {
|
||||||
|
if (TextUtils.isEmpty(conversationId)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
prefs(context).edit().putBoolean(KEY_PREFIX + conversationId, true).apply();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void clearDismissed(final Context context, final String conversationId) {
|
||||||
|
if (TextUtils.isEmpty(conversationId)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
prefs(context).edit().remove(KEY_PREFIX + conversationId).apply();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static SharedPreferences prefs(final Context context) {
|
||||||
|
return context.getApplicationContext().getSharedPreferences(PREFS_NAME,
|
||||||
|
Context.MODE_PRIVATE);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -177,4 +177,5 @@ class CameraMediaChooser extends MediaChooser {
|
|||||||
private static boolean hasCameraPermission() {
|
private static boolean hasCameraPermission() {
|
||||||
return OsUtil.hasPermission(Manifest.permission.CAMERA);
|
return OsUtil.hasPermission(Manifest.permission.CAMERA);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -112,17 +112,17 @@ class ContactMediaChooser extends MediaChooser {
|
|||||||
container /* root */,
|
container /* root */,
|
||||||
false /* attachToRoot */);
|
false /* attachToRoot */);
|
||||||
mEnabledView = view.findViewById(R.id.mediapicker_enabled);
|
mEnabledView = view.findViewById(R.id.mediapicker_enabled);
|
||||||
mEnabledView.setOnClickListener(v -> {
|
mEnabledView.setOnClickListener(v -> launchPicker());
|
||||||
// Launch an external picker to pick a contact as attachment.
|
return view;
|
||||||
final Intent intent = new Intent(Intent.ACTION_PICK, Contacts.CONTENT_URI);
|
}
|
||||||
|
|
||||||
|
void launchPicker() {
|
||||||
|
final Intent intent = new Intent(Intent.ACTION_PICK, Contacts.CONTENT_URI);
|
||||||
try {
|
try {
|
||||||
mPickerLauncher.launch(intent);
|
mPickerLauncher.launch(intent);
|
||||||
} catch (final ActivityNotFoundException ex) {
|
} catch (final ActivityNotFoundException ex) {
|
||||||
LogUtil.w(LogUtil.BUGLE_TAG, "Couldn't find activity:", ex);
|
LogUtil.w(LogUtil.BUGLE_TAG, "Couldn't find activity:", ex);
|
||||||
UiUtils.showToastAtBottom(R.string.activity_not_found_message);
|
UiUtils.showToastAtBottom(R.string.activity_not_found_message);
|
||||||
}
|
}
|
||||||
});
|
|
||||||
return view;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,27 +16,24 @@
|
|||||||
*/
|
*/
|
||||||
package com.android.messaging.ui.mediapicker;
|
package com.android.messaging.ui.mediapicker;
|
||||||
|
|
||||||
import android.content.Intent;
|
import android.app.Activity;
|
||||||
import android.net.Uri;
|
import android.net.Uri;
|
||||||
import android.os.Bundle;
|
|
||||||
import android.os.Handler;
|
import android.os.Handler;
|
||||||
import android.os.Looper;
|
import android.os.Looper;
|
||||||
|
|
||||||
import androidx.activity.result.ActivityResultLauncher;
|
import androidx.activity.result.ActivityResultLauncher;
|
||||||
import androidx.activity.result.PickVisualMediaRequest;
|
|
||||||
import androidx.activity.result.contract.ActivityResultContracts;
|
import androidx.activity.result.contract.ActivityResultContracts;
|
||||||
import androidx.fragment.app.Fragment;
|
import androidx.fragment.app.Fragment;
|
||||||
|
|
||||||
import com.android.messaging.Factory;
|
import com.android.messaging.Factory;
|
||||||
import com.android.messaging.datamodel.MediaScratchFileProvider;
|
import com.android.messaging.datamodel.MediaScratchFileProvider;
|
||||||
import com.android.messaging.datamodel.data.PendingAttachmentData;
|
import com.android.messaging.datamodel.data.PendingAttachmentData;
|
||||||
import com.android.messaging.ui.UIIntents;
|
import com.android.messaging.ui.home.HomeGallerySheet;
|
||||||
import com.android.messaging.util.BugleGservicesKeys;
|
|
||||||
import com.android.messaging.util.LogUtil;
|
|
||||||
import com.android.messaging.util.FileUtil;
|
import com.android.messaging.util.FileUtil;
|
||||||
import com.android.messaging.util.ImageUtils;
|
import com.android.messaging.util.ImageUtils;
|
||||||
|
import com.android.messaging.util.LogUtil;
|
||||||
|
import com.android.messaging.util.OsUtil;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.concurrent.ExecutorService;
|
import java.util.concurrent.ExecutorService;
|
||||||
import java.util.concurrent.Executors;
|
import java.util.concurrent.Executors;
|
||||||
@@ -67,9 +64,7 @@ public class DocumentImagePicker {
|
|||||||
// The listener on the picker events.
|
// The listener on the picker events.
|
||||||
private final SelectionListener mListener;
|
private final SelectionListener mListener;
|
||||||
|
|
||||||
private static final String EXTRA_PHOTO_URL = "photo_url";
|
private final ActivityResultLauncher<String[]> mPermissionLauncher;
|
||||||
|
|
||||||
private final ActivityResultLauncher<PickVisualMediaRequest> mPickMultipleMedia;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates a new instance of DocumentImagePicker.
|
* Creates a new instance of DocumentImagePicker.
|
||||||
@@ -81,24 +76,24 @@ public class DocumentImagePicker {
|
|||||||
mFragment = fragment;
|
mFragment = fragment;
|
||||||
mListener = listener;
|
mListener = listener;
|
||||||
|
|
||||||
mPickMultipleMedia = mFragment.registerForActivityResult(
|
mPermissionLauncher = mFragment.registerForActivityResult(
|
||||||
new ActivityResultContracts.PickMultipleVisualMedia(
|
new ActivityResultContracts.RequestMultiplePermissions(),
|
||||||
BugleGservicesKeys.MMS_ATTACHMENT_LIMIT_DEFAULT), uris -> {
|
result -> showGallerySheet());
|
||||||
// Callback is invoked after the user selects media items or closes the
|
|
||||||
// photo picker.
|
|
||||||
if (!uris.isEmpty()) {
|
|
||||||
onDocumentsPicked(uris);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Intent out to open an image/video from document picker.
|
* Intent out to open an image/video from document picker.
|
||||||
*/
|
*/
|
||||||
public void launchPicker() {
|
public void launchPicker() {
|
||||||
mPickMultipleMedia.launch(new PickVisualMediaRequest.Builder()
|
final Activity activity = mFragment.getActivity();
|
||||||
.setMediaType(ActivityResultContracts.PickVisualMedia.ImageAndVideo.INSTANCE)
|
if (activity == null) {
|
||||||
.build());
|
return;
|
||||||
|
}
|
||||||
|
if (!OsUtil.hasMediaPermissions()) {
|
||||||
|
mPermissionLauncher.launch(OsUtil.getMediaPermissions());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
showGallerySheet();
|
||||||
}
|
}
|
||||||
|
|
||||||
public void onDocumentsPicked(List<Uri> uris) {
|
public void onDocumentsPicked(List<Uri> uris) {
|
||||||
@@ -107,6 +102,14 @@ public class DocumentImagePicker {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void showGallerySheet() {
|
||||||
|
final Activity activity = mFragment.getActivity();
|
||||||
|
if (activity == null || activity.isFinishing()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
HomeGallerySheet.show(activity, this::onDocumentsPicked, this::launchPicker);
|
||||||
|
}
|
||||||
|
|
||||||
private void prepareDocumentForAttachment(final Uri documentUri) {
|
private void prepareDocumentForAttachment(final Uri documentUri) {
|
||||||
// Notify our listener with a PendingAttachmentData containing the metadata.
|
// Notify our listener with a PendingAttachmentData containing the metadata.
|
||||||
// Asynchronously get the content type for the picked image since
|
// Asynchronously get the content type for the picked image since
|
||||||
|
|||||||
@@ -65,13 +65,14 @@ class GalleryMediaChooser extends MediaChooser {
|
|||||||
false /* attachToRoot */);
|
false /* attachToRoot */);
|
||||||
|
|
||||||
final View enabledView = view.findViewById(R.id.mediapicker_enabled);
|
final View enabledView = view.findViewById(R.id.mediapicker_enabled);
|
||||||
enabledView.setOnClickListener(v -> {
|
enabledView.setOnClickListener(v -> launchPicker());
|
||||||
// Launch an external picker to pick item from document picker as attachment.
|
|
||||||
mDocumentImagePicker.launchPicker();
|
|
||||||
});
|
|
||||||
return view;
|
return view;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void launchPicker() {
|
||||||
|
mDocumentImagePicker.launchPicker();
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
int getActionBarTitleResId() {
|
int getActionBarTitleResId() {
|
||||||
return R.string.mediapicker_gallery_title;
|
return R.string.mediapicker_gallery_title;
|
||||||
|
|||||||
@@ -180,8 +180,16 @@ public class MediaPicker extends Fragment implements DraftMessageSubscriptionDat
|
|||||||
mOpen = false;
|
mOpen = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static final int OPEN_MODE_INVALID = -1;
|
||||||
|
private static final int OPEN_MODE_DEFAULT = 0;
|
||||||
|
private static final int OPEN_MODE_CAMERA = 1;
|
||||||
|
private static final int OPEN_MODE_GALLERY = 2;
|
||||||
|
private static final int OPEN_MODE_AUDIO = 3;
|
||||||
|
private static final int OPEN_MODE_CONTACT = 4;
|
||||||
|
|
||||||
private boolean mIsAttached;
|
private boolean mIsAttached;
|
||||||
private int mStartingMediaTypeOnAttach = MEDA_TYPE_INVALID;
|
private int mStartingMediaTypeOnAttach = MEDA_TYPE_INVALID;
|
||||||
|
private int mDeferredOpenMode = OPEN_MODE_INVALID;
|
||||||
private boolean mAnimateOnAttach;
|
private boolean mAnimateOnAttach;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -189,7 +197,16 @@ public class MediaPicker extends Fragment implements DraftMessageSubscriptionDat
|
|||||||
super.onAttach(context);
|
super.onAttach(context);
|
||||||
mIsAttached = true;
|
mIsAttached = true;
|
||||||
setSupportedMediaTypes(MEDIA_TYPE_ALL);
|
setSupportedMediaTypes(MEDIA_TYPE_ALL);
|
||||||
if (mStartingMediaTypeOnAttach != MEDA_TYPE_INVALID) {
|
if (mDeferredOpenMode == OPEN_MODE_GALLERY) {
|
||||||
|
launchGalleryPickerWithoutPanel();
|
||||||
|
mDeferredOpenMode = OPEN_MODE_INVALID;
|
||||||
|
} else if (mDeferredOpenMode == OPEN_MODE_CONTACT) {
|
||||||
|
launchContactPickerWithoutPanel();
|
||||||
|
mDeferredOpenMode = OPEN_MODE_INVALID;
|
||||||
|
} else if (mDeferredOpenMode != OPEN_MODE_INVALID && mDeferredOpenMode != OPEN_MODE_DEFAULT) {
|
||||||
|
performOpenMode(mDeferredOpenMode, mAnimateOnAttach);
|
||||||
|
mDeferredOpenMode = OPEN_MODE_INVALID;
|
||||||
|
} else if (mStartingMediaTypeOnAttach != MEDA_TYPE_INVALID) {
|
||||||
// open() was previously called. Do the pending open now.
|
// open() was previously called. Do the pending open now.
|
||||||
doOpen(mStartingMediaTypeOnAttach, mAnimateOnAttach);
|
doOpen(mStartingMediaTypeOnAttach, mAnimateOnAttach);
|
||||||
}
|
}
|
||||||
@@ -211,7 +228,6 @@ public class MediaPicker extends Fragment implements DraftMessageSubscriptionDat
|
|||||||
false);
|
false);
|
||||||
mMediaPickerPanel.setMediaPicker(this);
|
mMediaPickerPanel.setMediaPicker(this);
|
||||||
mTabStrip = mMediaPickerPanel.findViewById(R.id.mediapicker_tabstrip);
|
mTabStrip = mMediaPickerPanel.findViewById(R.id.mediapicker_tabstrip);
|
||||||
mTabStrip.setBackgroundColor(mThemeColor);
|
|
||||||
for (final MediaChooser chooser : mChoosers) {
|
for (final MediaChooser chooser : mChoosers) {
|
||||||
chooser.onCreateTabButton(inflater, mTabStrip);
|
chooser.onCreateTabButton(inflater, mTabStrip);
|
||||||
final ImageButton tabButton = chooser.getTabButton();
|
final ImageButton tabButton = chooser.getTabButton();
|
||||||
@@ -249,11 +265,20 @@ public class MediaPicker extends Fragment implements DraftMessageSubscriptionDat
|
|||||||
});
|
});
|
||||||
// Camera initialization is expensive, so don't realize offscreen pages if not needed.
|
// Camera initialization is expensive, so don't realize offscreen pages if not needed.
|
||||||
mViewPager.setOffscreenPageLimit(0);
|
mViewPager.setOffscreenPageLimit(0);
|
||||||
mViewPager.setAdapter(mPagerAdapter);
|
mViewPager.setAdapter(null);
|
||||||
|
mViewPager.setVisibility(View.GONE);
|
||||||
|
mMediaPickerPanel.setViewPagerSuppressed(true);
|
||||||
|
mMediaPickerPanel.setVisibility(View.GONE);
|
||||||
final boolean isTouchExplorationEnabled = AccessibilityUtil.isTouchExplorationEnabled(
|
final boolean isTouchExplorationEnabled = AccessibilityUtil.isTouchExplorationEnabled(
|
||||||
getActivity());
|
getActivity());
|
||||||
mMediaPickerPanel.setFullScreenOnly(isTouchExplorationEnabled);
|
mMediaPickerPanel.setFullScreenOnly(isTouchExplorationEnabled);
|
||||||
mMediaPickerPanel.setExpanded(mOpen, true, mEnabledChoosers.indexOf(mSelectedChooser));
|
if (mOpen) {
|
||||||
|
setHostContainerVisible(true);
|
||||||
|
mMediaPickerPanel.setViewPagerSuppressed(false);
|
||||||
|
mMediaPickerPanel.setExpanded(true, false, mEnabledChoosers.indexOf(mSelectedChooser));
|
||||||
|
} else {
|
||||||
|
setHostContainerVisible(false);
|
||||||
|
}
|
||||||
return mMediaPickerPanel;
|
return mMediaPickerPanel;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -262,11 +287,9 @@ public class MediaPicker extends Fragment implements DraftMessageSubscriptionDat
|
|||||||
super.onViewCreated(view, savedInstanceState);
|
super.onViewCreated(view, savedInstanceState);
|
||||||
|
|
||||||
for (final MediaChooser chooser : mChoosers) {
|
for (final MediaChooser chooser : mChoosers) {
|
||||||
final boolean enabled = (chooser.getSupportedMediaTypes() & mSupportedMediaTypes) !=
|
|
||||||
MEDIA_TYPE_NONE;
|
|
||||||
final ImageButton tabButton = chooser.getTabButton();
|
final ImageButton tabButton = chooser.getTabButton();
|
||||||
if (tabButton != null) {
|
if (tabButton != null) {
|
||||||
tabButton.setVisibility(enabled ? View.VISIBLE : View.GONE);
|
tabButton.setVisibility(View.GONE);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -291,10 +314,6 @@ public class MediaPicker extends Fragment implements DraftMessageSubscriptionDat
|
|||||||
*/
|
*/
|
||||||
public void setConversationThemeColor(final int themeColor) {
|
public void setConversationThemeColor(final int themeColor) {
|
||||||
mThemeColor = themeColor;
|
mThemeColor = themeColor;
|
||||||
if (mTabStrip != null) {
|
|
||||||
mTabStrip.setBackgroundColor(mThemeColor);
|
|
||||||
}
|
|
||||||
|
|
||||||
for (final MediaChooser chooser : mEnabledChoosers) {
|
for (final MediaChooser chooser : mEnabledChoosers) {
|
||||||
chooser.setThemeColor(mThemeColor);
|
chooser.setThemeColor(mThemeColor);
|
||||||
}
|
}
|
||||||
@@ -331,6 +350,7 @@ public class MediaPicker extends Fragment implements DraftMessageSubscriptionDat
|
|||||||
*/
|
*/
|
||||||
public void open(final int startingMediaType, final boolean animate) {
|
public void open(final int startingMediaType, final boolean animate) {
|
||||||
mOpen = true;
|
mOpen = true;
|
||||||
|
mDeferredOpenMode = OPEN_MODE_DEFAULT;
|
||||||
if (mIsAttached) {
|
if (mIsAttached) {
|
||||||
doOpen(startingMediaType, animate);
|
doOpen(startingMediaType, animate);
|
||||||
} else {
|
} else {
|
||||||
@@ -338,29 +358,162 @@ public class MediaPicker extends Fragment implements DraftMessageSubscriptionDat
|
|||||||
// we defer doing work as it may require an attached fragment (eg. calling
|
// we defer doing work as it may require an attached fragment (eg. calling
|
||||||
// Fragment#requestPermission)
|
// Fragment#requestPermission)
|
||||||
mStartingMediaTypeOnAttach = startingMediaType;
|
mStartingMediaTypeOnAttach = startingMediaType;
|
||||||
|
mDeferredOpenMode = OPEN_MODE_INVALID;
|
||||||
mAnimateOnAttach = animate;
|
mAnimateOnAttach = animate;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Opens the media picker on the camera chooser. */
|
||||||
|
public void openCamera(final boolean animate) {
|
||||||
|
openWithMode(OPEN_MODE_CAMERA, animate);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Opens the system gallery picker without showing the in-app chooser panel. */
|
||||||
|
public void openGallery(final boolean animate) {
|
||||||
|
if (mIsAttached) {
|
||||||
|
launchGalleryPickerWithoutPanel();
|
||||||
|
} else {
|
||||||
|
mDeferredOpenMode = OPEN_MODE_GALLERY;
|
||||||
|
mAnimateOnAttach = animate;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void hideInAppPanel() {
|
||||||
|
mOpen = false;
|
||||||
|
if (mMediaPickerPanel != null) {
|
||||||
|
mMediaPickerPanel.setViewPagerSuppressed(true);
|
||||||
|
mMediaPickerPanel.setExpanded(false, false, MediaPickerPanel.PAGE_NOT_SET);
|
||||||
|
mMediaPickerPanel.setVisibility(View.GONE);
|
||||||
|
if (mViewPager != null) {
|
||||||
|
mViewPager.setAdapter(null);
|
||||||
|
mViewPager.setVisibility(View.GONE);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
setHostContainerVisible(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void launchGalleryPickerWithoutPanel() {
|
||||||
|
hideInAppPanel();
|
||||||
|
for (final MediaChooser chooser : mEnabledChoosers) {
|
||||||
|
if (chooser instanceof GalleryMediaChooser) {
|
||||||
|
((GalleryMediaChooser) chooser).launchPicker();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void launchContactPickerWithoutPanel() {
|
||||||
|
hideInAppPanel();
|
||||||
|
for (final MediaChooser chooser : mEnabledChoosers) {
|
||||||
|
if (chooser instanceof ContactMediaChooser) {
|
||||||
|
((ContactMediaChooser) chooser).launchPicker();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Opens the media picker on the audio chooser. */
|
||||||
|
public void openAudio(final boolean animate) {
|
||||||
|
openWithMode(OPEN_MODE_AUDIO, animate);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Opens the system contact picker without showing the in-app chooser panel. */
|
||||||
|
public void openContact(final boolean animate) {
|
||||||
|
if (mIsAttached) {
|
||||||
|
launchContactPickerWithoutPanel();
|
||||||
|
} else {
|
||||||
|
mDeferredOpenMode = OPEN_MODE_CONTACT;
|
||||||
|
mAnimateOnAttach = animate;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void openWithMode(final int openMode, final boolean animate) {
|
||||||
|
mOpen = true;
|
||||||
|
mDeferredOpenMode = openMode;
|
||||||
|
if (mIsAttached) {
|
||||||
|
performOpenMode(openMode, animate);
|
||||||
|
mDeferredOpenMode = OPEN_MODE_INVALID;
|
||||||
|
} else {
|
||||||
|
mStartingMediaTypeOnAttach = MEDA_TYPE_INVALID;
|
||||||
|
mAnimateOnAttach = animate;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void performOpenMode(final int openMode, final boolean animate) {
|
||||||
|
mSelectedChooser = null;
|
||||||
|
if (openMode == OPEN_MODE_CAMERA) {
|
||||||
|
selectFirstChooserOfType(CameraMediaChooser.class);
|
||||||
|
} else if (openMode == OPEN_MODE_GALLERY) {
|
||||||
|
selectFirstChooserOfType(GalleryMediaChooser.class);
|
||||||
|
} else if (openMode == OPEN_MODE_AUDIO) {
|
||||||
|
selectFirstChooserOfType(AudioMediaChooser.class);
|
||||||
|
} else if (openMode == OPEN_MODE_CONTACT) {
|
||||||
|
selectFirstChooserOfType(ContactMediaChooser.class);
|
||||||
|
} else {
|
||||||
|
doOpen(MEDIA_TYPE_DEFAULT, animate);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (mSelectedChooser == null && !mEnabledChoosers.isEmpty()) {
|
||||||
|
selectChooser(mEnabledChoosers.get(0));
|
||||||
|
}
|
||||||
|
expandMediaPickerPanel(animate);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void selectFirstChooserOfType(final Class<?> chooserType) {
|
||||||
|
for (final MediaChooser chooser : mEnabledChoosers) {
|
||||||
|
if (chooserType.isInstance(chooser)) {
|
||||||
|
selectChooser(chooser);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void setHostContainerVisible(final boolean visible) {
|
||||||
|
final View container;
|
||||||
|
if (mMediaPickerPanel != null && mMediaPickerPanel.getParent() instanceof View
|
||||||
|
&& ((View) mMediaPickerPanel.getParent()).getId() == R.id.mediapicker_container) {
|
||||||
|
container = (View) mMediaPickerPanel.getParent();
|
||||||
|
} else if (getActivity() != null) {
|
||||||
|
container = getActivity().findViewById(R.id.mediapicker_container);
|
||||||
|
} else {
|
||||||
|
container = null;
|
||||||
|
}
|
||||||
|
if (container != null) {
|
||||||
|
container.setVisibility(visible ? View.VISIBLE : View.GONE);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void expandMediaPickerPanel(final boolean animate) {
|
||||||
|
if (mMediaPickerPanel == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setHostContainerVisible(true);
|
||||||
|
mMediaPickerPanel.setVisibility(View.VISIBLE);
|
||||||
|
mMediaPickerPanel.setViewPagerSuppressed(false);
|
||||||
|
if (mViewPager != null && mPagerAdapter != null) {
|
||||||
|
mViewPager.setAdapter(mPagerAdapter);
|
||||||
|
}
|
||||||
|
final boolean isTouchExplorationEnabled = AccessibilityUtil.isTouchExplorationEnabled(
|
||||||
|
Factory.get().getApplicationContext());
|
||||||
|
mMediaPickerPanel.setFullScreenOnly(isTouchExplorationEnabled);
|
||||||
|
final int startingPage = mSelectedChooser == null ? MediaPickerPanel.PAGE_NOT_SET
|
||||||
|
: mEnabledChoosers.indexOf(mSelectedChooser);
|
||||||
|
mMediaPickerPanel.setExpanded(true, animate, startingPage);
|
||||||
|
}
|
||||||
|
|
||||||
private void doOpen(int startingMediaType, final boolean animate) {
|
private void doOpen(int startingMediaType, final boolean animate) {
|
||||||
final boolean isTouchExplorationEnabled = AccessibilityUtil.isTouchExplorationEnabled(
|
final boolean isTouchExplorationEnabled = AccessibilityUtil.isTouchExplorationEnabled(
|
||||||
// getActivity() will be null at this point
|
// getActivity() will be null at this point
|
||||||
Factory.get().getApplicationContext());
|
Factory.get().getApplicationContext());
|
||||||
|
|
||||||
// If no specific starting type is specified (i.e. MEDIA_TYPE_DEFAULT), try to get the
|
|
||||||
// last opened chooser index from shared prefs.
|
|
||||||
if (startingMediaType == MEDIA_TYPE_DEFAULT) {
|
if (startingMediaType == MEDIA_TYPE_DEFAULT) {
|
||||||
final int selectedChooserIndex = mBinding.getData().getSelectedChooserIndex();
|
final int selectedChooserIndex = mBinding.getData().getSelectedChooserIndex();
|
||||||
if (selectedChooserIndex >= 0 && selectedChooserIndex < mEnabledChoosers.size()) {
|
if (selectedChooserIndex >= 0 && selectedChooserIndex < mEnabledChoosers.size()) {
|
||||||
selectChooser(mEnabledChoosers.get(selectedChooserIndex));
|
selectChooser(mEnabledChoosers.get(selectedChooserIndex));
|
||||||
} else {
|
} else if (isTouchExplorationEnabled) {
|
||||||
// This is the first time the picker is being used
|
|
||||||
if (isTouchExplorationEnabled) {
|
|
||||||
// Accessibility defaults to audio attachment mode.
|
|
||||||
startingMediaType = MEDIA_TYPE_AUDIO;
|
startingMediaType = MEDIA_TYPE_AUDIO;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
if (mSelectedChooser == null) {
|
if (mSelectedChooser == null) {
|
||||||
for (final MediaChooser chooser : mEnabledChoosers) {
|
for (final MediaChooser chooser : mEnabledChoosers) {
|
||||||
@@ -372,16 +525,11 @@ public class MediaPicker extends Fragment implements DraftMessageSubscriptionDat
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (mSelectedChooser == null) {
|
if (mSelectedChooser == null && !mEnabledChoosers.isEmpty()) {
|
||||||
// Fall back to the first chooser.
|
|
||||||
selectChooser(mEnabledChoosers.get(0));
|
selectChooser(mEnabledChoosers.get(0));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (mMediaPickerPanel != null) {
|
expandMediaPickerPanel(animate);
|
||||||
mMediaPickerPanel.setFullScreenOnly(isTouchExplorationEnabled);
|
|
||||||
mMediaPickerPanel.setExpanded(true, animate,
|
|
||||||
mEnabledChoosers.indexOf(mSelectedChooser));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @return True if the media picker is open */
|
/** @return True if the media picker is open */
|
||||||
@@ -413,7 +561,7 @@ public class MediaPicker extends Fragment implements DraftMessageSubscriptionDat
|
|||||||
}
|
}
|
||||||
final ImageButton tabButton = chooser.getTabButton();
|
final ImageButton tabButton = chooser.getTabButton();
|
||||||
if (tabButton != null) {
|
if (tabButton != null) {
|
||||||
tabButton.setVisibility(enabled ? View.VISIBLE : View.GONE);
|
tabButton.setVisibility(View.GONE);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -443,7 +591,12 @@ public class MediaPicker extends Fragment implements DraftMessageSubscriptionDat
|
|||||||
public void dismiss(final boolean animate) {
|
public void dismiss(final boolean animate) {
|
||||||
mOpen = false;
|
mOpen = false;
|
||||||
if (mMediaPickerPanel != null) {
|
if (mMediaPickerPanel != null) {
|
||||||
|
mMediaPickerPanel.setViewPagerSuppressed(true);
|
||||||
mMediaPickerPanel.setExpanded(false, animate, MediaPickerPanel.PAGE_NOT_SET);
|
mMediaPickerPanel.setExpanded(false, animate, MediaPickerPanel.PAGE_NOT_SET);
|
||||||
|
if (!animate) {
|
||||||
|
mMediaPickerPanel.setVisibility(View.GONE);
|
||||||
|
setHostContainerVisible(false);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
mSelectedChooser = null;
|
mSelectedChooser = null;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -58,6 +58,9 @@ public class MediaPickerPanel extends ViewGroup {
|
|||||||
*/
|
*/
|
||||||
private boolean mExpanded;
|
private boolean mExpanded;
|
||||||
|
|
||||||
|
/** When true, only the attachment grid is shown (Google Messages menu sheet). */
|
||||||
|
private boolean mViewPagerSuppressed;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The current desired height of the MediaPicker. This property may be animated and the
|
* The current desired height of the MediaPicker. This property may be animated and the
|
||||||
* measure pass uses it to determine what size the components are.
|
* measure pass uses it to determine what size the components are.
|
||||||
@@ -154,13 +157,32 @@ public class MediaPickerPanel extends ViewGroup {
|
|||||||
// take the max here as on some devices the mDefaultViewPagerHeight may be too big in
|
// take the max here as on some devices the mDefaultViewPagerHeight may be too big in
|
||||||
// landscape mode after animation.
|
// landscape mode after animation.
|
||||||
final int tabAdjustedDesiredHeight = desiredHeight - tabStripHeight;
|
final int tabAdjustedDesiredHeight = desiredHeight - tabStripHeight;
|
||||||
final int viewPagerHeight =
|
final int viewPagerHeight;
|
||||||
|
if (mViewPagerSuppressed || !mExpanded) {
|
||||||
|
viewPagerHeight = 0;
|
||||||
|
mViewPager.setVisibility(GONE);
|
||||||
|
} else {
|
||||||
|
viewPagerHeight =
|
||||||
tabAdjustedDesiredHeight <= 1 ? mDefaultViewPagerHeight : tabAdjustedDesiredHeight;
|
tabAdjustedDesiredHeight <= 1 ? mDefaultViewPagerHeight : tabAdjustedDesiredHeight;
|
||||||
|
mViewPager.setVisibility(VISIBLE);
|
||||||
|
}
|
||||||
int viewPagerHeightMeasureSpec = MeasureSpec.makeMeasureSpec(
|
int viewPagerHeightMeasureSpec = MeasureSpec.makeMeasureSpec(
|
||||||
viewPagerHeight, MeasureSpec.EXACTLY);
|
viewPagerHeight, MeasureSpec.EXACTLY);
|
||||||
measureChild(mViewPager, widthMeasureSpec, viewPagerHeightMeasureSpec);
|
measureChild(mViewPager, widthMeasureSpec, viewPagerHeightMeasureSpec);
|
||||||
setMeasuredDimension(mViewPager.getMeasuredWidth(), desiredHeight);
|
final int totalHeight = (mViewPagerSuppressed || !mExpanded) ? 0 : desiredHeight;
|
||||||
|
setMeasuredDimension(Math.max(mViewPager.getMeasuredWidth(), getSuggestedMinimumWidth()),
|
||||||
|
totalHeight);
|
||||||
|
}
|
||||||
|
|
||||||
|
void setViewPagerSuppressed(final boolean suppressed) {
|
||||||
|
if (mViewPagerSuppressed == suppressed) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
mViewPagerSuppressed = suppressed;
|
||||||
|
if (suppressed) {
|
||||||
|
mViewPager.setVisibility(GONE);
|
||||||
|
}
|
||||||
|
requestLayout();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -224,6 +246,11 @@ public class MediaPickerPanel extends ViewGroup {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void setupViewPager(final int startingPage) {
|
private void setupViewPager(final int startingPage) {
|
||||||
|
if (mViewPagerSuppressed) {
|
||||||
|
mViewPager.setVisibility(GONE);
|
||||||
|
mViewPager.setAdapter(null);
|
||||||
|
return;
|
||||||
|
}
|
||||||
mViewPager.setVisibility(View.VISIBLE);
|
mViewPager.setVisibility(View.VISIBLE);
|
||||||
if (startingPage >= 0 && startingPage < mMediaPicker.getPagerAdapter().getCount()) {
|
if (startingPage >= 0 && startingPage < mMediaPicker.getPagerAdapter().getCount()) {
|
||||||
mViewPager.setAdapter(mMediaPicker.getPagerAdapter());
|
mViewPager.setAdapter(mMediaPicker.getPagerAdapter());
|
||||||
@@ -250,13 +277,16 @@ public class MediaPickerPanel extends ViewGroup {
|
|||||||
}
|
}
|
||||||
mFullScreen = false;
|
mFullScreen = false;
|
||||||
mExpanded = expanded;
|
mExpanded = expanded;
|
||||||
mHandler.post(() -> setDesiredHeight(getDesiredHeight(), animate));
|
|
||||||
if (expanded) {
|
if (expanded) {
|
||||||
|
setVisibility(VISIBLE);
|
||||||
setupViewPager(startingPage);
|
setupViewPager(startingPage);
|
||||||
mMediaPicker.dispatchOpened();
|
mMediaPicker.dispatchOpened();
|
||||||
} else {
|
} else {
|
||||||
|
mViewPager.setAdapter(null);
|
||||||
|
mViewPager.setVisibility(GONE);
|
||||||
mMediaPicker.dispatchDismissed();
|
mMediaPicker.dispatchDismissed();
|
||||||
}
|
}
|
||||||
|
mHandler.post(() -> setDesiredHeight(getDesiredHeight(), animate));
|
||||||
|
|
||||||
// Call setFullScreenView() when we are in landscape mode so it can go full screen as
|
// Call setFullScreenView() when we are in landscape mode so it can go full screen as
|
||||||
// soon as it is expanded.
|
// soon as it is expanded.
|
||||||
@@ -292,9 +322,31 @@ public class MediaPickerPanel extends ViewGroup {
|
|||||||
};
|
};
|
||||||
animation.setDuration(UiUtils.MEDIAPICKER_TRANSITION_DURATION);
|
animation.setDuration(UiUtils.MEDIAPICKER_TRANSITION_DURATION);
|
||||||
animation.setInterpolator(UiUtils.EASE_OUT_INTERPOLATOR);
|
animation.setInterpolator(UiUtils.EASE_OUT_INTERPOLATOR);
|
||||||
|
animation.setAnimationListener(new Animation.AnimationListener() {
|
||||||
|
@Override
|
||||||
|
public void onAnimationStart(final Animation a) {
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onAnimationRepeat(final Animation a) {
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onAnimationEnd(final Animation a) {
|
||||||
|
if (!mExpanded) {
|
||||||
|
setVisibility(GONE);
|
||||||
|
if (getParent() instanceof View) {
|
||||||
|
((View) getParent()).setVisibility(GONE);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
startAnimation(animation);
|
startAnimation(animation);
|
||||||
} else {
|
} else {
|
||||||
mCurrentDesiredHeight = height;
|
mCurrentDesiredHeight = height;
|
||||||
|
if (!mExpanded) {
|
||||||
|
setVisibility(GONE);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
requestLayout();
|
requestLayout();
|
||||||
}
|
}
|
||||||
@@ -305,6 +357,9 @@ public class MediaPickerPanel extends ViewGroup {
|
|||||||
private int measureHeight() {
|
private int measureHeight() {
|
||||||
final int measureSpec = MeasureSpec.makeMeasureSpec(Integer.MAX_VALUE, MeasureSpec.AT_MOST);
|
final int measureSpec = MeasureSpec.makeMeasureSpec(Integer.MAX_VALUE, MeasureSpec.AT_MOST);
|
||||||
measureChild(mTabStrip, measureSpec, measureSpec);
|
measureChild(mTabStrip, measureSpec, measureSpec);
|
||||||
|
if (mViewPagerSuppressed) {
|
||||||
|
return mTabStrip.getMeasuredHeight();
|
||||||
|
}
|
||||||
return mDefaultViewPagerHeight + mTabStrip.getMeasuredHeight();
|
return mDefaultViewPagerHeight + mTabStrip.getMeasuredHeight();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -31,6 +31,8 @@ import android.provider.ContactsContract.DisplayNameSources;
|
|||||||
import android.provider.ContactsContract.PhoneLookup;
|
import android.provider.ContactsContract.PhoneLookup;
|
||||||
import android.provider.ContactsContract.Profile;
|
import android.provider.ContactsContract.Profile;
|
||||||
import android.text.TextUtils;
|
import android.text.TextUtils;
|
||||||
|
import android.content.ActivityNotFoundException;
|
||||||
|
import android.content.Intent;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
|
|
||||||
import com.android.ex.chips.RecipientEntry;
|
import com.android.ex.chips.RecipientEntry;
|
||||||
@@ -189,6 +191,122 @@ public class ContactUtil {
|
|||||||
* @param avatarUri Uri to the avatar image if available
|
* @param avatarUri Uri to the avatar image if available
|
||||||
* @param normalizedDestination The normalized phone number or email
|
* @param normalizedDestination The normalized phone number or email
|
||||||
*/
|
*/
|
||||||
|
/**
|
||||||
|
* Opens the platform contact detail screen (Google Contacts full-page UI when available).
|
||||||
|
*
|
||||||
|
* @return true if an activity was started
|
||||||
|
*/
|
||||||
|
public static boolean openContactDetail(final Context context, final long contactId,
|
||||||
|
final String contactLookupKey) {
|
||||||
|
return openContactDetail(context, contactId, contactLookupKey, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Opens contact detail for a 1:1 participant, including numbers not yet linked in our DB.
|
||||||
|
* Resolves address-book matches via {@link #lookupDestination} when lookup key is missing.
|
||||||
|
* For numbers not in the address book, delegates to the contacts app tel/mailto detail UI.
|
||||||
|
*
|
||||||
|
* @return true if an activity was started
|
||||||
|
*/
|
||||||
|
public static boolean openContactDetail(final Context context, final long contactId,
|
||||||
|
final String contactLookupKey, final String normalizedDestination) {
|
||||||
|
if (context == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
Uri lookupUri = null;
|
||||||
|
if (contactId > ParticipantData.PARTICIPANT_CONTACT_ID_NOT_RESOLVED
|
||||||
|
&& !TextUtils.isEmpty(contactLookupKey)) {
|
||||||
|
lookupUri = Contacts.getLookupUri(contactId, contactLookupKey);
|
||||||
|
}
|
||||||
|
if (lookupUri == null && !TextUtils.isEmpty(normalizedDestination)) {
|
||||||
|
lookupUri = resolveLookupUriFromDestination(context, normalizedDestination);
|
||||||
|
}
|
||||||
|
if (lookupUri != null) {
|
||||||
|
return startContactDetailActivity(context, lookupUri);
|
||||||
|
}
|
||||||
|
if (!TextUtils.isEmpty(normalizedDestination)
|
||||||
|
&& !TextUtils.equals(normalizedDestination,
|
||||||
|
ParticipantData.getUnknownSenderDestination())) {
|
||||||
|
return openContactDetailForUnsavedDestination(context, normalizedDestination);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Whether the participant is stored in the device address book (saved contact). */
|
||||||
|
public static boolean isParticipantInAddressBook(final Context context, final long contactId,
|
||||||
|
final String contactLookupKey, final String normalizedDestination) {
|
||||||
|
if (contactId > ParticipantData.PARTICIPANT_CONTACT_ID_NOT_RESOLVED
|
||||||
|
&& !TextUtils.isEmpty(contactLookupKey)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (context == null || TextUtils.isEmpty(normalizedDestination)
|
||||||
|
|| !hasReadContactsPermission()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
Cursor cursor = null;
|
||||||
|
try {
|
||||||
|
cursor = lookupDestination(context, normalizedDestination).performSynchronousQuery();
|
||||||
|
return cursor != null && cursor.getCount() > 0;
|
||||||
|
} finally {
|
||||||
|
if (cursor != null) {
|
||||||
|
cursor.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Uri resolveLookupUriFromDestination(final Context context,
|
||||||
|
final String normalizedDestination) {
|
||||||
|
if (!hasReadContactsPermission()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
Cursor cursor = null;
|
||||||
|
try {
|
||||||
|
cursor = lookupDestination(context, normalizedDestination).performSynchronousQuery();
|
||||||
|
if (cursor == null || !cursor.moveToFirst()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
final long resolvedContactId = cursor.getLong(INDEX_CONTACT_ID);
|
||||||
|
final String resolvedLookupKey = cursor.getString(INDEX_LOOKUP_KEY);
|
||||||
|
if (resolvedContactId <= ParticipantData.PARTICIPANT_CONTACT_ID_NOT_RESOLVED
|
||||||
|
|| TextUtils.isEmpty(resolvedLookupKey)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return Contacts.getLookupUri(resolvedContactId, resolvedLookupKey);
|
||||||
|
} finally {
|
||||||
|
if (cursor != null) {
|
||||||
|
cursor.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean startContactDetailActivity(final Context context, final Uri lookupUri) {
|
||||||
|
final Intent intent = new Intent(Intent.ACTION_VIEW, lookupUri);
|
||||||
|
try {
|
||||||
|
context.startActivity(intent);
|
||||||
|
return true;
|
||||||
|
} catch (final ActivityNotFoundException e) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean openContactDetailForUnsavedDestination(final Context context,
|
||||||
|
final String normalizedDestination) {
|
||||||
|
final Intent intent = new Intent(Intent.ACTION_VIEW);
|
||||||
|
if (MmsSmsUtils.isEmailAddress(normalizedDestination)) {
|
||||||
|
intent.setDataAndType(Uri.parse("mailto:" + normalizedDestination),
|
||||||
|
Contacts.CONTENT_ITEM_TYPE);
|
||||||
|
} else {
|
||||||
|
intent.setDataAndType(Uri.fromParts("tel", normalizedDestination, null),
|
||||||
|
Contacts.CONTENT_ITEM_TYPE);
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
context.startActivity(intent);
|
||||||
|
return true;
|
||||||
|
} catch (final ActivityNotFoundException e) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public static void showOrAddContact(final View view, final long contactId,
|
public static void showOrAddContact(final View view, final long contactId,
|
||||||
final String contactLookupKey, final Uri avatarUri,
|
final String contactLookupKey, final Uri avatarUri,
|
||||||
final String normalizedDestination) {
|
final String normalizedDestination) {
|
||||||
|
|||||||
@@ -123,6 +123,18 @@ public class OsUtil {
|
|||||||
return OsUtil.hasPermission(Manifest.permission.RECORD_AUDIO);
|
return OsUtil.hasPermission(Manifest.permission.RECORD_AUDIO);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static String[] getMediaPermissions() {
|
||||||
|
return new String[] {
|
||||||
|
Manifest.permission.READ_MEDIA_IMAGES,
|
||||||
|
Manifest.permission.READ_MEDIA_VIDEO,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public static boolean hasMediaPermissions() {
|
||||||
|
return hasPermission(Manifest.permission.READ_MEDIA_IMAGES)
|
||||||
|
|| hasPermission(Manifest.permission.READ_MEDIA_VIDEO);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns array with the set of permissions that have not been granted from the given set.
|
* Returns array with the set of permissions that have not been granted from the given set.
|
||||||
* The array will be empty if the app has all of the specified permissions. Note that calling
|
* The array will be empty if the app has all of the specified permissions. Note that calling
|
||||||
|
|||||||
@@ -0,0 +1,14 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<set xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:duration="260"
|
||||||
|
android:interpolator="@android:interpolator/fast_out_slow_in"
|
||||||
|
android:shareInterpolator="true">
|
||||||
|
|
||||||
|
<alpha
|
||||||
|
android:fromAlpha="1"
|
||||||
|
android:toAlpha="0" />
|
||||||
|
|
||||||
|
<translate
|
||||||
|
android:fromYDelta="0"
|
||||||
|
android:toYDelta="8%" />
|
||||||
|
</set>
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<alpha xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:duration="300"
|
||||||
|
android:fromAlpha="1"
|
||||||
|
android:toAlpha="1" />
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<set xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:duration="300"
|
||||||
|
android:interpolator="@android:interpolator/fast_out_slow_in"
|
||||||
|
android:shareInterpolator="true">
|
||||||
|
|
||||||
|
<alpha
|
||||||
|
android:fromAlpha="0"
|
||||||
|
android:toAlpha="1" />
|
||||||
|
|
||||||
|
<translate
|
||||||
|
android:fromYDelta="12%"
|
||||||
|
android:toYDelta="0" />
|
||||||
|
</set>
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<translate xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:duration="260"
|
||||||
|
android:fromYDelta="0"
|
||||||
|
android:interpolator="@android:interpolator/fast_out_linear_in"
|
||||||
|
android:toYDelta="100%p" />
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<translate xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:duration="300"
|
||||||
|
android:fromYDelta="100%p"
|
||||||
|
android:interpolator="@android:interpolator/fast_out_slow_in"
|
||||||
|
android:toYDelta="0" />
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:shape="rectangle">
|
||||||
|
<solid android:color="@color/home_surface" />
|
||||||
|
<corners android:radius="@dimen/looply_radius_lg" />
|
||||||
|
</shape>
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:shape="rectangle">
|
||||||
|
<solid android:color="@color/home_bg" />
|
||||||
|
<corners
|
||||||
|
android:topLeftRadius="@dimen/looply_radius_lg"
|
||||||
|
android:topRightRadius="@dimen/looply_radius_lg" />
|
||||||
|
</shape>
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:shape="rectangle">
|
||||||
|
<solid android:color="@color/home_mediapicker_sheet_bg" />
|
||||||
|
<corners
|
||||||
|
android:topLeftRadius="@dimen/looply_radius_xl"
|
||||||
|
android:topRightRadius="@dimen/looply_radius_xl" />
|
||||||
|
</shape>
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:shape="oval">
|
||||||
|
<solid android:color="@color/home_compose_attach_circle_bg" />
|
||||||
|
</shape>
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:shape="rectangle">
|
||||||
|
<solid android:color="@color/home_chip_bg" />
|
||||||
|
<corners android:radius="@dimen/looply_radius_pill" />
|
||||||
|
</shape>
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:shape="rectangle">
|
||||||
|
<solid android:color="@color/home_bg" />
|
||||||
|
<corners
|
||||||
|
android:topLeftRadius="@dimen/looply_radius_xl"
|
||||||
|
android:topRightRadius="@dimen/looply_radius_xl" />
|
||||||
|
</shape>
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:shape="rectangle">
|
||||||
|
<corners android:radius="@dimen/looply_radius_pill" />
|
||||||
|
<solid android:color="@color/home_compose_pill_bg" />
|
||||||
|
</shape>
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<ripple xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:color="@color/home_surface_elevated">
|
||||||
|
<item>
|
||||||
|
<shape android:shape="oval">
|
||||||
|
<solid android:color="@color/home_accent" />
|
||||||
|
</shape>
|
||||||
|
</item>
|
||||||
|
</ripple>
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<ripple xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:color="@color/home_compose_voice_ripple">
|
||||||
|
<item>
|
||||||
|
<shape android:shape="oval">
|
||||||
|
<solid android:color="@color/home_compose_voice_bg" />
|
||||||
|
</shape>
|
||||||
|
</item>
|
||||||
|
</ripple>
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<item>
|
||||||
|
<shape android:shape="oval">
|
||||||
|
<solid android:color="@color/home_accent" />
|
||||||
|
</shape>
|
||||||
|
</item>
|
||||||
|
<item
|
||||||
|
android:drawable="@drawable/ic_home_check_on_accent"
|
||||||
|
android:gravity="center"
|
||||||
|
android:width="@dimen/home_contact_selected_check_size"
|
||||||
|
android:height="@dimen/home_contact_selected_check_size" />
|
||||||
|
</layer-list>
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:shape="rectangle">
|
||||||
|
<solid android:color="@color/home_bg" />
|
||||||
|
<corners
|
||||||
|
android:topLeftRadius="@dimen/looply_radius_xl"
|
||||||
|
android:topRightRadius="@dimen/looply_radius_xl" />
|
||||||
|
</shape>
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:shape="oval">
|
||||||
|
<solid android:color="#1AFF3B30" />
|
||||||
|
</shape>
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:shape="rectangle">
|
||||||
|
<solid android:color="@color/home_text_tertiary" />
|
||||||
|
<corners android:radius="@dimen/looply_radius_pill" />
|
||||||
|
</shape>
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:shape="rectangle">
|
||||||
|
<corners android:radius="@dimen/looply_radius_md" />
|
||||||
|
<solid android:color="@color/home_search_field_bg" />
|
||||||
|
</shape>
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:shape="rectangle">
|
||||||
|
<corners android:radius="@dimen/looply_radius_pill" />
|
||||||
|
<solid android:color="@color/home_bg" />
|
||||||
|
</shape>
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:shape="rectangle">
|
||||||
|
<corners android:radius="@dimen/looply_radius_xl" />
|
||||||
|
<solid android:color="@color/home_surface" />
|
||||||
|
</shape>
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:shape="rectangle">
|
||||||
|
<corners
|
||||||
|
android:topLeftRadius="@dimen/looply_radius_xl"
|
||||||
|
android:topRightRadius="@dimen/looply_radius_xl"
|
||||||
|
android:bottomLeftRadius="0dp"
|
||||||
|
android:bottomRightRadius="0dp" />
|
||||||
|
<solid android:color="@color/home_mediapicker_sheet_bg" />
|
||||||
|
</shape>
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:shape="rectangle">
|
||||||
|
<corners android:radius="@dimen/looply_radius_pill" />
|
||||||
|
<solid android:color="@color/home_mediapicker_tile_bg" />
|
||||||
|
</shape>
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<ripple xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:color="@color/home_surface_elevated">
|
||||||
|
<item>
|
||||||
|
<shape android:shape="rectangle">
|
||||||
|
<solid android:color="@color/home_bg" />
|
||||||
|
<stroke
|
||||||
|
android:width="1dp"
|
||||||
|
android:color="@color/home_button_stroke" />
|
||||||
|
<corners android:radius="@dimen/looply_radius_pill" />
|
||||||
|
</shape>
|
||||||
|
</item>
|
||||||
|
</ripple>
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<ripple xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:color="@color/home_surface_elevated">
|
||||||
|
<item>
|
||||||
|
<shape android:shape="rectangle">
|
||||||
|
<solid android:color="@color/home_accent" />
|
||||||
|
<corners android:radius="@dimen/looply_radius_pill" />
|
||||||
|
</shape>
|
||||||
|
</item>
|
||||||
|
</ripple>
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:shape="rectangle">
|
||||||
|
<corners android:radius="@dimen/looply_radius_lg" />
|
||||||
|
<solid android:color="@color/home_surface" />
|
||||||
|
</shape>
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<ripple xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:color="@color/home_surface_elevated">
|
||||||
|
<item>
|
||||||
|
<shape android:shape="rectangle">
|
||||||
|
<corners android:radius="@dimen/looply_radius_md" />
|
||||||
|
<solid android:color="@color/home_bg" />
|
||||||
|
</shape>
|
||||||
|
</item>
|
||||||
|
</ripple>
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<ripple xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:color="@color/home_surface_elevated">
|
||||||
|
<item>
|
||||||
|
<shape android:shape="rectangle">
|
||||||
|
<corners android:radius="@dimen/looply_radius_md" />
|
||||||
|
<solid android:color="@color/home_surface" />
|
||||||
|
</shape>
|
||||||
|
</item>
|
||||||
|
</ripple>
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:shape="rectangle">
|
||||||
|
<corners android:radius="@dimen/looply_radius_pill" />
|
||||||
|
<solid android:color="@color/home_surface_elevated" />
|
||||||
|
</shape>
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:shape="rectangle">
|
||||||
|
<corners android:radius="@dimen/looply_radius_xl" />
|
||||||
|
<solid android:color="@color/home_unknown_sender_banner_bg" />
|
||||||
|
</shape>
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:shape="oval">
|
||||||
|
<solid android:color="@color/home_unknown_sender_accent" />
|
||||||
|
</shape>
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="20dp"
|
||||||
|
android:height="20dp"
|
||||||
|
android:tint="@color/looply_text_on_primary"
|
||||||
|
android:viewportWidth="24"
|
||||||
|
android:viewportHeight="24">
|
||||||
|
<path
|
||||||
|
android:fillColor="#FFFFFF"
|
||||||
|
android:pathData="M12,4l-1.41,1.41L16.17,11H4v2h12.17l-5.58,5.59L12,20l8,-8z" />
|
||||||
|
</vector>
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="24dp" android:height="24dp" android:viewportWidth="24" android:viewportHeight="24">
|
||||||
|
<!-- notifications outline -->
|
||||||
|
<path android:fillColor="#000000"
|
||||||
|
android:pathData="M12,22c1.1,0 2,-0.9 2,-2h-4c0,1.1 0.9,2 2,2zM18,16v-5c0,-3.07 -1.64,-5.64 -4.5,-6.32V4c0,-0.83 -0.67,-1.5 -1.5,-1.5S10.5,3.17 10.5,4v0.68C7.63,5.36 6,7.92 6,11v5l-2,2v1h16v-1l-2,-2zM16,17H8v-6c0,-2.48 1.51,-4.5 4,-4.5s4,2.02 4,4.5v6z"/>
|
||||||
|
</vector>
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="24dp"
|
||||||
|
android:height="24dp"
|
||||||
|
android:viewportWidth="24"
|
||||||
|
android:viewportHeight="24">
|
||||||
|
<path
|
||||||
|
android:fillColor="@android:color/transparent"
|
||||||
|
android:pathData="M4,8h3l1.5,-2h7L17,8h3a1,1 0,0 1,1 1v10a1,1 0,0 1,-1 1H4a1,1 0,0 1,-1 -1V9a1,1 0,0 1,1 -1z"
|
||||||
|
android:strokeWidth="1.5"
|
||||||
|
android:strokeColor="#000000"
|
||||||
|
android:strokeLineJoin="round" />
|
||||||
|
<path
|
||||||
|
android:fillColor="@android:color/transparent"
|
||||||
|
android:pathData="M12,17a3.5,3.5 0,1 0,0 -7a3.5,3.5 0,0 0,0 7z"
|
||||||
|
android:strokeWidth="1.5"
|
||||||
|
android:strokeColor="#000000" />
|
||||||
|
</vector>
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="14dp"
|
||||||
|
android:height="14dp"
|
||||||
|
android:tint="@color/looply_text_on_primary"
|
||||||
|
android:viewportWidth="24"
|
||||||
|
android:viewportHeight="24">
|
||||||
|
<path
|
||||||
|
android:fillColor="#FFFFFF"
|
||||||
|
android:pathData="M9,16.17L4.83,12l-1.42,1.41L9,19 21,7l-1.41,-1.41L9,16.17z" />
|
||||||
|
</vector>
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="24dp"
|
||||||
|
android:height="24dp"
|
||||||
|
android:viewportWidth="24"
|
||||||
|
android:viewportHeight="24">
|
||||||
|
<path
|
||||||
|
android:fillColor="#8E8E93"
|
||||||
|
android:pathData="M7.41,15.41L12,10.83l4.59,4.58L18,14l-6,-6 -6,6 1.41,1.41z" />
|
||||||
|
</vector>
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="24dp"
|
||||||
|
android:height="24dp"
|
||||||
|
android:viewportWidth="24"
|
||||||
|
android:viewportHeight="24">
|
||||||
|
<path
|
||||||
|
android:fillColor="@color/home_text_secondary"
|
||||||
|
android:pathData="M19,6.41L17.59,5L12,10.59L6.41,5L5,6.41L10.59,12L5,17.59L6.41,19L12,13.41L17.59,19L19,17.59L13.41,12L19,6.41Z" />
|
||||||
|
</vector>
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="24dp"
|
||||||
|
android:height="24dp"
|
||||||
|
android:viewportWidth="24"
|
||||||
|
android:viewportHeight="24">
|
||||||
|
<path
|
||||||
|
android:fillColor="@android:color/transparent"
|
||||||
|
android:pathData="M12,5v14M5,12h14"
|
||||||
|
android:strokeWidth="1.8"
|
||||||
|
android:strokeColor="#000000"
|
||||||
|
android:strokeLineCap="round" />
|
||||||
|
</vector>
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="24dp" android:height="24dp" android:viewportWidth="24" android:viewportHeight="24">
|
||||||
|
<!-- pencil outline -->
|
||||||
|
<path android:fillColor="#000000"
|
||||||
|
android:pathData="M3,17.25V21h3.75L17.81,9.94l-3.75,-3.75L3,17.25zM5.92,19H5v-0.92l9.06,-9.06 0.92,0.92L5.92,19zM20.71,5.63l-2.34,-2.34c-0.39,-0.39 -1.02,-0.39 -1.41,0l-1.83,1.83 3.75,3.75 1.83,-1.83c0.39,-0.39 0.39,-1.02 0,-1.41z"/>
|
||||||
|
</vector>
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="24dp" android:height="24dp" android:viewportWidth="24" android:viewportHeight="24">
|
||||||
|
<path android:fillColor="#000000"
|
||||||
|
android:pathData="M11.99,2C6.47,2 2,6.48 2,12s4.47,10 9.99,10C17.52,22 22,17.52 22,12S17.52,2 11.99,2zM12,20c-4.42,0 -8,-3.58 -8,-8s3.58,-8 8,-8 8,3.58 8,8 -3.58,8 -8,8zM15.5,11c0.83,0 1.5,-0.67 1.5,-1.5S16.33,8 15.5,8 14,8.67 14,9.5s0.67,1.5 1.5,1.5zM8.5,11c0.83,0 1.5,-0.67 1.5,-1.5S9.33,8 8.5,8 7,8.67 7,9.5 7.67,11 8.5,11zM12,17.5c2.33,0 4.31,-1.46 5.11,-3.5H6.89c0.8,2.04 2.78,3.5 5.11,3.5z"/>
|
||||||
|
</vector>
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="24dp" android:height="24dp" android:viewportWidth="24" android:viewportHeight="24">
|
||||||
|
<!-- group add outline -->
|
||||||
|
<path android:fillColor="#000000"
|
||||||
|
android:pathData="M8,10H5V7H3v3H0v2h3v3h2v-3h3v-2z"/>
|
||||||
|
<path android:fillColor="#000000"
|
||||||
|
android:pathData="M18,11c1.66,0 2.99,-1.34 2.99,-3S19.66,5 18,5c-0.32,0 -0.63,0.05 -0.91,0.14 0.57,0.81 0.9,1.79 0.9,2.86s-0.34,2.04 -0.9,2.86c0.28,0.09 0.59,0.14 0.91,0.14zM12.5,11c1.66,0 2.99,-1.34 2.99,-3S14.16,5 12.5,5C10.84,5 9.5,6.34 9.5,8s1.34,3 3,3zM12.5,7c0.55,0 1,0.45 1,1s-0.45,1 -1,1 -1,-0.45 -1,-1 0.45,-1 1,-1z"/>
|
||||||
|
<path android:fillColor="#000000"
|
||||||
|
android:pathData="M12.5,12.5c-2,0 -6,1 -6,3V18h12v-2.5c0,-2 -4,-3 -6,-3zM8.5,16c0.22,-0.35 1.74,-1.5 4,-1.5s3.78,1.15 4,1.5h-8zM19.62,12.68c0.98,0.69 1.88,1.56 1.88,2.82V18H24v-2.5c0,-1.54 -2.51,-2.48 -4.38,-2.82z"/>
|
||||||
|
</vector>
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="24dp" android:height="24dp" android:viewportWidth="24" android:viewportHeight="24">
|
||||||
|
<!-- image outline -->
|
||||||
|
<path android:fillColor="#000000"
|
||||||
|
android:pathData="M19,5v14H5V5h14m0,-2H5C3.9,3 3,3.9 3,5v14c0,1.1 0.9,2 2,2h14c1.1,0 2,-0.9 2,-2V5c0,-1.1 -0.9,-2 -2,-2z"/>
|
||||||
|
<path android:fillColor="#000000"
|
||||||
|
android:pathData="M14.14,11.86l-3,3.87L9,13.14 6,17h12l-3.86,-5.14z"/>
|
||||||
|
</vector>
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="24dp" android:height="24dp" android:viewportWidth="24" android:viewportHeight="24">
|
||||||
|
<!-- link outline -->
|
||||||
|
<path android:fillColor="#000000"
|
||||||
|
android:pathData="M3.9,12c0,-1.71 1.39,-3.1 3.1,-3.1h4V7H7c-2.76,0 -5,2.24 -5,5s2.24,5 5,5h4v-1.9H7c-1.71,0 -3.1,-1.39 -3.1,-3.1zM8,13h8v-2H8v2zM17,7h-4v1.9h4c1.71,0 3.1,1.39 3.1,3.1s-1.39,3.1 -3.1,3.1h-4V17h4c2.76,0 5,-2.24 5,-5s-2.24,-5 -5,-5z"/>
|
||||||
|
</vector>
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="24dp"
|
||||||
|
android:height="24dp"
|
||||||
|
android:viewportWidth="24"
|
||||||
|
android:viewportHeight="24">
|
||||||
|
<path
|
||||||
|
android:fillColor="#000000"
|
||||||
|
android:pathData="M12,8c-2.21,0 -4,1.79 -4,4s1.79,4 4,4 4,-1.79 4,-4 -1.79,-4 -4,-4zM20.94,11c-0.46,-4.17 -3.77,-7.48 -7.94,-7.94V1h-2v2.06C6.83,3.52 3.52,6.83 3.06,11H1v2h2.06c0.46,4.17 3.77,7.48 7.94,7.94V23h2v-2.06c4.17,-0.46 7.48,-3.77 7.94,-7.94H23v-2h-2.06zM12,19c-3.87,0 -7,-3.13 -7,-7s3.13,-7 7,-7 7,3.13 7,7 -3.13,7 -7,7z" />
|
||||||
|
</vector>
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="24dp" android:height="24dp" android:viewportWidth="24" android:viewportHeight="24">
|
||||||
|
<!-- place outline -->
|
||||||
|
<path android:fillColor="#000000"
|
||||||
|
android:pathData="M12,2C8.13,2 5,5.13 5,9c0,5.25 7,13 7,13s7,-7.75 7,-13c0,-3.87 -3.13,-7 -7,-7zM7,9c0,-2.76 2.24,-5 5,-5s5,2.24 5,5c0,2.88 -2.88,7.19 -5,9.88C9.92,16.21 7,11.85 7,9z"/>
|
||||||
|
<path android:fillColor="#000000"
|
||||||
|
android:pathData="M12,11.5c1.38,0 2.5,-1.12 2.5,-2.5S13.38,6.5 12,6.5 9.5,7.62 9.5,9s1.12,2.5 2.5,2.5z"/>
|
||||||
|
</vector>
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="24dp" android:height="24dp" android:viewportWidth="24" android:viewportHeight="24">
|
||||||
|
<!-- starred outline -->
|
||||||
|
<path android:fillColor="#000000"
|
||||||
|
android:pathData="M22,9.24l-7.19,-0.62L12,2 9.19,8.63 2,9.24l5.46,4.73L5.82,21 12,17.27 18.18,21l-1.63,-7.03L22,9.24zM12,15.4l-3.76,2.27 1,-4.28 -3.32,-2.88 4.38,-0.38L12,6.1l1.71,4.04 4.38,0.38 -3.32,2.88 1,4.28L12,15.4z"/>
|
||||||
|
</vector>
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="24dp" android:height="24dp" android:viewportWidth="24" android:viewportHeight="24">
|
||||||
|
<!-- movie outline -->
|
||||||
|
<path android:fillColor="#000000"
|
||||||
|
android:pathData="M18,4l2,4h-3l-2,-4h-2l2,4h-3l-2,-4H8l2,4H7L5,4H4C2.9,4 2.01,4.9 2.01,6L2,18c0,1.1 0.9,2 2,2h16c1.1,0 2,-0.9 2,-2V4h-4zM4,18V6h0.15l1,2H20v10H4z"/>
|
||||||
|
</vector>
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="24dp"
|
||||||
|
android:height="24dp"
|
||||||
|
android:viewportWidth="56"
|
||||||
|
android:viewportHeight="56">
|
||||||
|
<path
|
||||||
|
android:fillColor="#000000"
|
||||||
|
android:pathData="M24.5898,49.5742C25.5508,49.5742 26.2539,48.8477 26.2539,47.9336L26.2539,8.0664C26.2539,7.1524 25.5508,6.4258 24.5898,6.4258C23.6524,6.4258 22.9492,7.1524 22.9492,8.0664L22.9492,47.9336C22.9492,48.8477 23.6524,49.5742 24.5898,49.5742Z" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#000000"
|
||||||
|
android:pathData="M38.2305,44.8867C39.168,44.8867 39.8711,44.1367 39.8711,43.2227L39.8711,12.7773C39.8711,11.8633 39.168,11.1133 38.2305,11.1133C37.293,11.1133 36.5664,11.8633 36.5664,12.7773L36.5664,43.2227C36.5664,44.1367 37.293,44.8867 38.2305,44.8867Z" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#000000"
|
||||||
|
android:pathData="M17.793,41.0898C18.7305,41.0898 19.4336,40.3633 19.4336,39.4492L19.4336,16.5508C19.4336,15.6367 18.7305,14.9102 17.793,14.9102C16.832,14.9102 16.1289,15.6367 16.1289,16.5508L16.1289,39.4492C16.1289,40.3633 16.832,41.0898 17.793,41.0898Z" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#000000"
|
||||||
|
android:pathData="M31.4102,38.5586C32.3476,38.5586 33.0742,37.832 33.0742,36.918L33.0742,19.082C33.0742,18.168 32.3476,17.4414 31.4102,17.4414C30.4727,17.4414 29.7695,18.168 29.7695,19.082L29.7695,36.918C29.7695,37.832 30.4727,38.5586 31.4102,38.5586Z" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#000000"
|
||||||
|
android:pathData="M45.0508,34.3633C45.9883,34.3633 46.6914,33.6133 46.6914,32.6992L46.6914,23.3008C46.6914,22.3867 45.9883,21.6367 45.0508,21.6367C44.0898,21.6367 43.3867,22.3867 43.3867,23.3008L43.3867,32.6992C43.3867,33.6133 44.0898,34.3633 45.0508,34.3633Z" />
|
||||||
|
<path
|
||||||
|
android:fillColor="#000000"
|
||||||
|
android:pathData="M10.9727,32.5117C11.9102,32.5117 12.6133,31.7851 12.6133,30.8711L12.6133,25.1289C12.6133,24.2149 11.9102,23.4883 10.9727,23.4883C10.0117,23.4883 9.3086,24.2149 9.3086,25.1289L9.3086,30.8711C9.3086,31.7851 10.0117,32.5117 10.9727,32.5117Z" />
|
||||||
|
</vector>
|
||||||
@@ -1,31 +1,41 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<!--
|
<!-- Tappable avatar + title opens conversation details (Google Messages style). -->
|
||||||
Copyright (C) 2015 The Android Open Source Project
|
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
|
||||||
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.
|
|
||||||
-->
|
|
||||||
<RelativeLayout
|
|
||||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
android:id="@+id/conversation_title_container"
|
android:id="@+id/conversation_title_container"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_gravity="fill_horizontal" >
|
android:layout_gravity="fill_horizontal|center_vertical"
|
||||||
|
android:gravity="center_vertical"
|
||||||
|
android:orientation="horizontal">
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:id="@+id/conversation_toolbar_identity"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:background="?android:attr/selectableItemBackgroundBorderless"
|
||||||
|
android:gravity="center_vertical"
|
||||||
|
android:orientation="horizontal"
|
||||||
|
android:paddingTop="4dp"
|
||||||
|
android:paddingBottom="4dp">
|
||||||
|
|
||||||
|
<com.android.messaging.ui.ContactIconView
|
||||||
|
android:id="@+id/conversation_toolbar_avatar"
|
||||||
|
android:layout_width="@dimen/home_conversation_toolbar_avatar_size"
|
||||||
|
android:layout_height="@dimen/home_conversation_toolbar_avatar_size"
|
||||||
|
android:contentDescription="@null"
|
||||||
|
app:contactIconSize="small" />
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
android:id="@+id/conversation_title"
|
android:id="@+id/conversation_title"
|
||||||
style="@style/ConversationActionBarTitleTextStyle"
|
android:layout_width="wrap_content"
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_centerVertical="true" />
|
android:layout_marginStart="@dimen/looply_space_md"
|
||||||
</RelativeLayout>
|
android:ellipsize="end"
|
||||||
|
android:includeFontPadding="false"
|
||||||
|
android:maxLines="1"
|
||||||
|
android:textColor="@color/home_text_primary"
|
||||||
|
android:textSize="17sp"
|
||||||
|
android:textStyle="bold" />
|
||||||
|
</LinearLayout>
|
||||||
|
</LinearLayout>
|
||||||
|
|||||||
@@ -1,192 +1,569 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
|
||||||
<!--
|
<!--
|
||||||
|
|
||||||
Copyright (C) 2015 The Android Open Source Project
|
Copyright (C) 2015 The Android Open Source Project
|
||||||
|
|
||||||
Copyright (C) 2024 The LineageOS Project
|
Copyright (C) 2024 The LineageOS Project
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
|
||||||
you may not use this file except in compliance with the License.
|
you may not use this file except in compliance with the License.
|
||||||
|
|
||||||
You may obtain a copy of the License at
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
http://www.apache.org/licenses/LICENSE-2.0
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
Unless required by applicable law or agreed to in writing, software
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
|
||||||
distributed under the License is distributed on an "AS IS" BASIS,
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
|
||||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
|
||||||
See the License for the specific language governing permissions and
|
See the License for the specific language governing permissions and
|
||||||
|
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
|
|
||||||
-->
|
-->
|
||||||
|
|
||||||
<com.android.messaging.ui.conversation.ComposeMessageView
|
<com.android.messaging.ui.conversation.ComposeMessageView
|
||||||
|
|
||||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
|
||||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
|
||||||
xmlns:tools="http://schemas.android.com/tools"
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
|
||||||
android:id="@+id/message_compose_view_container"
|
android:id="@+id/message_compose_view_container"
|
||||||
|
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
|
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
|
|
||||||
android:orientation="vertical" >
|
android:orientation="vertical" >
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<!-- Note it may seem off to have the layout_weight="1" on the compose bar and attachment view
|
<!-- Note it may seem off to have the layout_weight="1" on the compose bar and attachment view
|
||||||
|
|
||||||
when it's the media picker that expands, but this is correct because weight also works when
|
when it's the media picker that expands, but this is correct because weight also works when
|
||||||
|
|
||||||
there's not enough space. Any views with positive weights will be shrunk to make space.
|
there's not enough space. Any views with positive weights will be shrunk to make space.
|
||||||
|
|
||||||
When the media picker goes full screen, it fills the parent, leaving no room for the compose
|
When the media picker goes full screen, it fills the parent, leaving no room for the compose
|
||||||
|
|
||||||
bar -->
|
bar -->
|
||||||
|
|
||||||
<LinearLayout
|
<LinearLayout
|
||||||
|
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
|
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
|
|
||||||
android:layout_weight="1"
|
android:layout_weight="1"
|
||||||
|
|
||||||
android:clipChildren="false"
|
android:clipChildren="false"
|
||||||
|
|
||||||
android:clipToPadding="false"
|
android:clipToPadding="false"
|
||||||
|
|
||||||
android:orientation="vertical">
|
android:orientation="vertical">
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<View
|
<View
|
||||||
|
|
||||||
android:id="@+id/top_margin"
|
android:id="@+id/top_margin"
|
||||||
|
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="16dp" />
|
|
||||||
|
android:layout_height="0dp" />
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<include layout="@layout/attachment_preview"
|
<include layout="@layout/attachment_preview"
|
||||||
|
|
||||||
android:id="@+id/attachment_draft_view"
|
android:id="@+id/attachment_draft_view"
|
||||||
|
|
||||||
style="@style/ComposeMessageViewDraftViewStyle"
|
style="@style/ComposeMessageViewDraftViewStyle"
|
||||||
|
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
|
|
||||||
android:layout_height="0dp"
|
android:layout_height="0dp"
|
||||||
|
|
||||||
android:layout_marginBottom="4dp"
|
android:layout_marginBottom="4dp"
|
||||||
|
|
||||||
android:layout_weight="1"
|
android:layout_weight="1"
|
||||||
|
|
||||||
android:visibility="gone" />
|
android:visibility="gone" />
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<RelativeLayout
|
<RelativeLayout
|
||||||
|
|
||||||
android:id="@+id/subject_view"
|
android:id="@+id/subject_view"
|
||||||
|
|
||||||
style="@style/SubjectViewStyle"
|
style="@style/SubjectViewStyle"
|
||||||
|
|
||||||
android:paddingTop="@dimen/compose_message_subject_top_padding"
|
android:paddingTop="@dimen/compose_message_subject_top_padding"
|
||||||
|
|
||||||
android:paddingBottom="@dimen/compose_message_subject_bottom_padding"
|
android:paddingBottom="@dimen/compose_message_subject_bottom_padding"
|
||||||
|
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
|
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
|
|
||||||
android:visibility="gone"
|
android:visibility="gone"
|
||||||
|
|
||||||
android:clipChildren="false"
|
android:clipChildren="false"
|
||||||
|
|
||||||
android:clipToPadding="false"
|
android:clipToPadding="false"
|
||||||
|
|
||||||
android:orientation="horizontal">
|
android:orientation="horizontal">
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<com.android.messaging.ui.PlainTextEditText
|
<com.android.messaging.ui.PlainTextEditText
|
||||||
|
|
||||||
android:background="@drawable/subject_editor_bubble"
|
android:background="@drawable/subject_editor_bubble"
|
||||||
|
|
||||||
android:id="@+id/compose_subject_text"
|
android:id="@+id/compose_subject_text"
|
||||||
|
|
||||||
style="@style/ConversationComposeSubjectText"
|
style="@style/ConversationComposeSubjectText"
|
||||||
|
|
||||||
android:hint="@string/compose_message_view_subject_hint_text"
|
android:hint="@string/compose_message_view_subject_hint_text"
|
||||||
|
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
|
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
|
|
||||||
android:imeOptions="flagNoExtractUi" />
|
android:imeOptions="flagNoExtractUi" />
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<ImageButton
|
<ImageButton
|
||||||
|
|
||||||
android:id="@+id/delete_subject_button"
|
android:id="@+id/delete_subject_button"
|
||||||
|
|
||||||
style="@style/AttachmentPreviewCloseButtonStyle"
|
style="@style/AttachmentPreviewCloseButtonStyle"
|
||||||
|
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
|
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
|
|
||||||
android:background="@null"
|
android:background="@null"
|
||||||
|
|
||||||
android:src="@drawable/ic_remove_light"
|
android:src="@drawable/ic_remove_light"
|
||||||
|
|
||||||
android:translationY="@dimen/compose_message_subject_cancel_top_offset"
|
android:translationY="@dimen/compose_message_subject_cancel_top_offset"
|
||||||
|
|
||||||
android:contentDescription="@string/delete_subject_content_description" />
|
android:contentDescription="@string/delete_subject_content_description" />
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
</RelativeLayout>
|
</RelativeLayout>
|
||||||
|
|
||||||
<!-- Holds all views that create the actual compose box -->
|
|
||||||
<LinearLayout
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:orientation="horizontal"
|
|
||||||
android:baselineAligned="false">
|
|
||||||
|
|
||||||
<!-- Contains compose message bubble and character counter for SMS or attachments size
|
|
||||||
for MMS which should be left aligned -->
|
<!-- Compose bar stays above the icon grid; the grid grows below and lifts the input. -->
|
||||||
|
|
||||||
<LinearLayout
|
<LinearLayout
|
||||||
android:layout_width="0dp"
|
|
||||||
android:layout_weight="1"
|
android:id="@+id/home_compose_bottom_stack"
|
||||||
|
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
|
|
||||||
|
android:clipChildren="false"
|
||||||
|
|
||||||
|
android:clipToOutline="false"
|
||||||
|
|
||||||
android:orientation="vertical">
|
android:orientation="vertical">
|
||||||
|
|
||||||
<!-- Contains media button and compose message bubble whose centers should be
|
|
||||||
vertically aligned -->
|
|
||||||
|
<!-- Google Messages–style compose row: pill (+ | text | emoji | gallery) + voice/send -->
|
||||||
|
|
||||||
<LinearLayout
|
<LinearLayout
|
||||||
|
|
||||||
|
android:id="@+id/home_conversation_compose_bar"
|
||||||
|
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:orientation="horizontal">
|
|
||||||
|
|
||||||
<ImageButton
|
|
||||||
android:id="@+id/attach_media_button"
|
|
||||||
style="@style/ComposeMessageViewAttachMediaButtonStyle"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_gravity="center_vertical"
|
|
||||||
android:src="@drawable/ic_attachment_dark"
|
|
||||||
android:minHeight="48dp"
|
|
||||||
android:minWidth="48dp"
|
|
||||||
android:paddingTop="@dimen/compose_message_attachment_padding_topBottom"
|
|
||||||
android:paddingBottom="@dimen/compose_message_attachment_padding_topBottom"
|
|
||||||
android:contentDescription="@string/attachMediaButtonContentDescription" />
|
|
||||||
|
|
||||||
<com.android.messaging.ui.PlainTextEditText
|
android:background="@color/home_bg"
|
||||||
android:id="@+id/compose_message_text"
|
|
||||||
style="@style/ConversationComposeSendText"
|
android:baselineAligned="false"
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_width="0dp"
|
android:clipChildren="false"
|
||||||
android:layout_weight="1"
|
|
||||||
android:layout_gravity="center"
|
android:clipToPadding="false"
|
||||||
android:background="@drawable/msg_bubble_input_rounded"
|
|
||||||
android:hint="@string/compose_message_view_hint_text"
|
android:gravity="bottom"
|
||||||
android:imeOptions="actionSend|flagNoEnterAction|flagNoExtractUi" />
|
|
||||||
|
android:orientation="horizontal"
|
||||||
|
|
||||||
|
android:paddingStart="@dimen/looply_space_md"
|
||||||
|
|
||||||
|
android:paddingTop="@dimen/looply_space_sm"
|
||||||
|
|
||||||
|
android:paddingEnd="@dimen/looply_space_md"
|
||||||
|
|
||||||
|
android:paddingBottom="@dimen/looply_space_md">
|
||||||
|
|
||||||
</LinearLayout>
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:id="@+id/message_body_size"
|
|
||||||
style="@style/ComposeMessageViewMessageBodySizeStyle"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:singleLine="true"
|
|
||||||
android:maxLines="1"
|
|
||||||
android:visibility="invisible" />
|
|
||||||
</LinearLayout>
|
|
||||||
|
|
||||||
<LinearLayout
|
<LinearLayout
|
||||||
android:orientation="vertical"
|
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="0dp"
|
||||||
android:layout_height="wrap_content">
|
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
|
||||||
|
android:layout_gravity="bottom"
|
||||||
|
|
||||||
|
android:layout_weight="1"
|
||||||
|
|
||||||
|
android:orientation="vertical">
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
|
||||||
|
android:layout_height="@dimen/home_conversation_compose_pill_height"
|
||||||
|
|
||||||
|
android:background="@drawable/home_bg_compose_pill"
|
||||||
|
|
||||||
|
android:gravity="center_vertical"
|
||||||
|
|
||||||
|
android:orientation="horizontal"
|
||||||
|
|
||||||
|
android:paddingStart="@dimen/looply_space_xs"
|
||||||
|
|
||||||
|
android:paddingEnd="@dimen/looply_space_xs">
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<FrameLayout
|
<FrameLayout
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
android:layout_width="@dimen/home_compose_attach_size"
|
||||||
style="@style/ComposeMessageViewFrameLayoutStyle"
|
|
||||||
android:layout_gravity="top" >
|
android:layout_height="@dimen/home_compose_attach_size"
|
||||||
<com.android.messaging.ui.conversation.SimIconView
|
|
||||||
android:id="@+id/self_send_icon"
|
android:layout_marginStart="@dimen/looply_space_xs">
|
||||||
android:layout_width="@dimen/conversation_message_contact_icon_size"
|
|
||||||
android:layout_height="@dimen/conversation_message_contact_icon_size"
|
|
||||||
android:importantForAccessibility="no"
|
|
||||||
app:contactIconSize="normal"
|
|
||||||
app:reveal="true"
|
|
||||||
android:clickable="true" />
|
|
||||||
<ImageButton
|
<ImageButton
|
||||||
android:id="@+id/send_message_button"
|
|
||||||
android:layout_width="@dimen/conversation_message_contact_icon_size"
|
android:id="@+id/attach_media_button"
|
||||||
android:layout_height="@dimen/conversation_message_contact_icon_size"
|
|
||||||
android:src="@drawable/ic_send_light"
|
android:layout_width="@dimen/home_compose_attach_size"
|
||||||
android:background="@drawable/send_arrow_background"
|
|
||||||
android:contentDescription="@string/sendButtonContentDescription"
|
android:layout_height="@dimen/home_compose_attach_size"
|
||||||
android:visibility="gone" />
|
|
||||||
|
android:background="@drawable/home_bg_compose_attach_circle"
|
||||||
|
|
||||||
|
android:contentDescription="@string/attachMediaButtonContentDescription"
|
||||||
|
|
||||||
|
android:padding="@dimen/home_compose_attach_icon_padding"
|
||||||
|
|
||||||
|
android:scaleType="centerInside"
|
||||||
|
|
||||||
|
app:srcCompat="@drawable/ic_home_compose_plus"
|
||||||
|
|
||||||
|
app:tint="@color/home_accent" />
|
||||||
|
|
||||||
</FrameLayout>
|
</FrameLayout>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<com.android.messaging.ui.PlainTextEditText
|
||||||
|
|
||||||
|
android:id="@+id/compose_message_text"
|
||||||
|
|
||||||
|
android:layout_width="0dp"
|
||||||
|
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
|
||||||
|
android:layout_weight="1"
|
||||||
|
|
||||||
|
android:background="@android:color/transparent"
|
||||||
|
|
||||||
|
android:gravity="center_vertical"
|
||||||
|
|
||||||
|
android:hint="@string/home_conversation_compose_hint"
|
||||||
|
|
||||||
|
android:imeOptions="actionSend|flagNoEnterAction|flagNoExtractUi"
|
||||||
|
|
||||||
|
android:includeFontPadding="false"
|
||||||
|
|
||||||
|
android:inputType="textShortMessage|textAutoCorrect|textCapSentences|textMultiLine"
|
||||||
|
|
||||||
|
android:maxLines="4"
|
||||||
|
|
||||||
|
android:minHeight="0dp"
|
||||||
|
|
||||||
|
android:paddingStart="@dimen/looply_space_xs"
|
||||||
|
|
||||||
|
android:paddingEnd="@dimen/looply_space_xs"
|
||||||
|
|
||||||
|
android:textColor="@color/home_text_primary"
|
||||||
|
|
||||||
|
android:textColorHint="@color/home_text_secondary"
|
||||||
|
|
||||||
|
android:textSize="16sp" />
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<ImageButton
|
||||||
|
|
||||||
|
android:id="@+id/home_compose_emoji_button"
|
||||||
|
|
||||||
|
android:layout_width="36dp"
|
||||||
|
|
||||||
|
android:layout_height="36dp"
|
||||||
|
|
||||||
|
android:background="@android:color/transparent"
|
||||||
|
|
||||||
|
android:contentDescription="@string/home_compose_emoji_content_description"
|
||||||
|
|
||||||
|
android:padding="6dp"
|
||||||
|
|
||||||
|
android:scaleType="centerInside"
|
||||||
|
|
||||||
|
app:srcCompat="@drawable/ic_home_emoji"
|
||||||
|
|
||||||
|
app:tint="@color/home_text_primary" />
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<ImageButton
|
||||||
|
|
||||||
|
android:id="@+id/home_compose_gallery_button"
|
||||||
|
|
||||||
|
android:layout_width="36dp"
|
||||||
|
|
||||||
|
android:layout_height="36dp"
|
||||||
|
|
||||||
|
android:layout_marginEnd="@dimen/looply_space_xs"
|
||||||
|
|
||||||
|
android:background="@android:color/transparent"
|
||||||
|
|
||||||
|
android:contentDescription="@string/home_compose_gallery_content_description"
|
||||||
|
|
||||||
|
android:padding="6dp"
|
||||||
|
|
||||||
|
android:scaleType="centerInside"
|
||||||
|
|
||||||
|
app:srcCompat="@drawable/ic_home_image_line"
|
||||||
|
|
||||||
|
app:tint="@color/home_text_primary" />
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
android:id="@+id/mms_indicator"
|
|
||||||
style="@style/MmsIndicatorStyle"
|
android:id="@+id/message_body_size"
|
||||||
|
|
||||||
|
style="@style/ComposeMessageViewMessageBodySizeStyle"
|
||||||
|
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
|
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:singleLine="true"
|
|
||||||
android:maxLines="1"
|
android:maxLines="1"
|
||||||
android:visibility="invisible"
|
|
||||||
android:text="@string/mms_text" />
|
android:singleLine="true"
|
||||||
</LinearLayout>
|
|
||||||
</LinearLayout>
|
android:visibility="gone" />
|
||||||
|
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<FrameLayout
|
<FrameLayout
|
||||||
android:id="@+id/mediapicker_container"
|
|
||||||
|
android:layout_width="@dimen/home_conversation_compose_voice_size"
|
||||||
|
|
||||||
|
android:layout_height="@dimen/home_conversation_compose_voice_size"
|
||||||
|
|
||||||
|
android:layout_gravity="bottom"
|
||||||
|
|
||||||
|
android:layout_marginStart="@dimen/looply_space_sm">
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<ImageButton
|
||||||
|
|
||||||
|
android:id="@+id/home_conversation_voice_button"
|
||||||
|
|
||||||
|
android:layout_width="@dimen/home_conversation_compose_voice_size"
|
||||||
|
|
||||||
|
android:layout_height="@dimen/home_conversation_compose_voice_size"
|
||||||
|
|
||||||
|
android:layout_gravity="center"
|
||||||
|
|
||||||
|
android:background="@drawable/home_bg_compose_voice_circle"
|
||||||
|
|
||||||
|
android:contentDescription="@string/home_conversation_voice_content_description"
|
||||||
|
|
||||||
|
android:padding="12dp"
|
||||||
|
|
||||||
|
android:scaleType="centerInside"
|
||||||
|
|
||||||
|
app:srcCompat="@drawable/ic_home_voice_wave"
|
||||||
|
|
||||||
|
app:tint="@color/home_accent" />
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<FrameLayout
|
||||||
|
|
||||||
|
android:id="@+id/home_conversation_send_column"
|
||||||
|
|
||||||
|
android:layout_width="@dimen/home_conversation_compose_voice_size"
|
||||||
|
|
||||||
|
android:layout_height="@dimen/home_conversation_compose_voice_size"
|
||||||
|
|
||||||
|
android:layout_gravity="center"
|
||||||
|
|
||||||
|
android:visibility="gone">
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<FrameLayout
|
||||||
|
|
||||||
|
android:id="@+id/home_conversation_send_button_container"
|
||||||
|
|
||||||
|
android:layout_width="@dimen/home_conversation_compose_send_size"
|
||||||
|
|
||||||
|
android:layout_height="@dimen/home_conversation_compose_send_size"
|
||||||
|
|
||||||
|
android:layout_gravity="center">
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<com.android.messaging.ui.conversation.SimIconView
|
||||||
|
|
||||||
|
android:id="@+id/self_send_icon"
|
||||||
|
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
|
||||||
|
android:clickable="true"
|
||||||
|
|
||||||
|
android:importantForAccessibility="no"
|
||||||
|
|
||||||
|
app:contactIconSize="small"
|
||||||
|
|
||||||
|
app:reveal="true" />
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<ImageButton
|
||||||
|
|
||||||
|
android:id="@+id/send_message_button"
|
||||||
|
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
|
||||||
|
android:background="@drawable/home_bg_compose_send_circle"
|
||||||
|
|
||||||
|
android:contentDescription="@string/sendButtonContentDescription"
|
||||||
|
|
||||||
|
android:padding="10dp"
|
||||||
|
|
||||||
|
android:scaleType="centerInside"
|
||||||
|
|
||||||
|
android:src="@drawable/ic_send_light"
|
||||||
|
|
||||||
|
android:visibility="gone"
|
||||||
|
|
||||||
|
app:tint="@android:color/white" />
|
||||||
|
|
||||||
|
</FrameLayout>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
|
||||||
|
android:id="@+id/mms_indicator"
|
||||||
|
|
||||||
|
style="@style/MmsIndicatorStyle"
|
||||||
|
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
|
||||||
|
android:layout_gravity="bottom|center_horizontal"
|
||||||
|
|
||||||
|
android:maxLines="1"
|
||||||
|
|
||||||
|
android:singleLine="true"
|
||||||
|
|
||||||
|
android:text="@string/mms_text"
|
||||||
|
|
||||||
|
android:visibility="gone" />
|
||||||
|
|
||||||
|
</FrameLayout>
|
||||||
|
|
||||||
|
</FrameLayout>
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<FrameLayout
|
||||||
|
|
||||||
|
android:id="@+id/home_compose_attachment_sheet_container"
|
||||||
|
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
|
||||||
|
android:layout_height="0dp"
|
||||||
|
|
||||||
|
android:clipChildren="true"
|
||||||
|
|
||||||
|
android:visibility="gone">
|
||||||
|
|
||||||
|
<include
|
||||||
|
layout="@layout/home_mediapicker_sheet"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_weight="0" />
|
android:layout_gravity="top" />
|
||||||
|
</FrameLayout>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<FrameLayout
|
||||||
|
|
||||||
|
android:id="@+id/mediapicker_container"
|
||||||
|
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
|
||||||
|
android:clipChildren="true"
|
||||||
|
|
||||||
|
android:visibility="gone" />
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
</com.android.messaging.ui.conversation.ComposeMessageView>
|
</com.android.messaging.ui.conversation.ComposeMessageView>
|
||||||
|
|
||||||
|
|||||||
@@ -15,54 +15,95 @@
|
|||||||
See the License for the specific language governing permissions and
|
See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
-->
|
-->
|
||||||
<LinearLayout
|
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
|
android:background="@color/home_conversation_chrome_bg"
|
||||||
android:orientation="vertical"
|
android:orientation="vertical"
|
||||||
android:weightSum="1.0">
|
android:weightSum="1.0">
|
||||||
|
|
||||||
<LinearLayout
|
<LinearLayout
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
|
android:clipChildren="false"
|
||||||
|
android:clipToPadding="false"
|
||||||
android:orientation="vertical">
|
android:orientation="vertical">
|
||||||
<!-- This FrameLayout will take all available height (excluding media picker)
|
|
||||||
but setting height to 0dp causes it not to shrink properly in KK, so a height of match_parent
|
<include layout="@layout/home_conversation_header" />
|
||||||
works equally well -->
|
|
||||||
|
<include layout="@layout/home_conversation_message_selection_header" />
|
||||||
|
|
||||||
<FrameLayout
|
<FrameLayout
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="0dp"
|
android:layout_height="0dp"
|
||||||
android:layout_weight="1" >
|
android:layout_weight="1">
|
||||||
<!-- Message list -->
|
|
||||||
|
<LinearLayout
|
||||||
|
android:id="@+id/conversation_message_sheet"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:background="@drawable/home_bg_conversation_sheet"
|
||||||
|
android:clipChildren="false"
|
||||||
|
android:clipToPadding="false"
|
||||||
|
android:orientation="vertical">
|
||||||
|
|
||||||
|
<include
|
||||||
|
layout="@layout/home_conversation_unknown_sender_banner"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginStart="@dimen/looply_space_md"
|
||||||
|
android:layout_marginTop="@dimen/looply_space_md"
|
||||||
|
android:layout_marginEnd="@dimen/looply_space_md" />
|
||||||
|
|
||||||
|
<FrameLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="0dp"
|
||||||
|
android:layout_weight="1">
|
||||||
|
|
||||||
<androidx.recyclerview.widget.RecyclerView
|
<androidx.recyclerview.widget.RecyclerView
|
||||||
android:id="@android:id/list"
|
android:id="@android:id/list"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
|
android:clipToPadding="false"
|
||||||
android:listSelector="@android:color/transparent"
|
android:listSelector="@android:color/transparent"
|
||||||
android:paddingTop="@dimen/action_bar_height"
|
android:paddingTop="0dp" />
|
||||||
android:clipToPadding="false" />
|
|
||||||
|
<include layout="@layout/home_conversation_empty" />
|
||||||
|
</FrameLayout>
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
<View
|
<View
|
||||||
android:id="@+id/conversation_compose_divider"
|
android:id="@+id/conversation_compose_divider"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="4dp"
|
android:layout_height="4dp"
|
||||||
android:layout_gravity="bottom"
|
android:layout_gravity="bottom"
|
||||||
android:background="@drawable/conversation_compose_divider_gradient"
|
|
||||||
android:alpha="0"
|
android:alpha="0"
|
||||||
|
android:background="@drawable/conversation_compose_divider_gradient"
|
||||||
android:importantForAccessibility="no" />
|
android:importantForAccessibility="no" />
|
||||||
|
|
||||||
<include layout="@layout/sim_selector_view"
|
<include
|
||||||
android:id="@+id/sim_selector"
|
android:id="@+id/sim_selector"
|
||||||
|
layout="@layout/sim_selector_view"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
android:visibility="gone" />
|
android:visibility="gone" />
|
||||||
</FrameLayout>
|
</FrameLayout>
|
||||||
|
|
||||||
<!-- Attachments to send, compose message view, media picker. -->
|
<FrameLayout
|
||||||
<include layout="@layout/compose_message_view"
|
|
||||||
android:id="@+id/message_compose_view_container"
|
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"/>
|
android:layout_height="wrap_content"
|
||||||
|
android:background="@color/home_bg"
|
||||||
|
android:clipChildren="false"
|
||||||
|
android:clipToPadding="false">
|
||||||
|
|
||||||
|
<include
|
||||||
|
android:id="@+id/message_compose_view_container"
|
||||||
|
layout="@layout/compose_message_view"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content" />
|
||||||
|
|
||||||
|
<include layout="@layout/home_conversation_message_action_bar" />
|
||||||
|
</FrameLayout>
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
|||||||
@@ -33,25 +33,46 @@
|
|||||||
android:background="@color/home_bg"
|
android:background="@color/home_bg"
|
||||||
android:clipChildren="false"
|
android:clipChildren="false"
|
||||||
android:clipToPadding="false"
|
android:clipToPadding="false"
|
||||||
android:elevation="8dp"
|
android:orientation="vertical">
|
||||||
android:orientation="vertical"
|
|
||||||
android:outlineAmbientShadowColor="@color/home_pill_shadow_ambient"
|
|
||||||
android:outlineSpotShadowColor="@color/home_pill_shadow_spot">
|
|
||||||
|
|
||||||
<!-- Header with pill actions (Telegram proportions) -->
|
<!-- Header with pill actions (Telegram proportions). The title is centered on the header
|
||||||
<LinearLayout
|
rather than on the space left by the side buttons, so it does not shift when the
|
||||||
|
selection header swaps in with differently sized buttons. -->
|
||||||
|
<FrameLayout
|
||||||
android:id="@+id/home_header"
|
android:id="@+id/home_header"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:clipChildren="false"
|
android:clipChildren="false"
|
||||||
android:clipToPadding="false"
|
android:clipToPadding="false"
|
||||||
android:gravity="center_vertical"
|
android:minHeight="@dimen/home_header_height"
|
||||||
android:minHeight="52dp"
|
|
||||||
android:orientation="horizontal"
|
|
||||||
android:paddingStart="12dp"
|
android:paddingStart="12dp"
|
||||||
android:paddingTop="6dp"
|
android:paddingTop="6dp"
|
||||||
android:paddingEnd="12dp"
|
android:paddingEnd="12dp"
|
||||||
android:paddingBottom="6dp">
|
android:paddingBottom="5dp">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/home_title"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_gravity="start|center_vertical"
|
||||||
|
android:layout_marginEnd="@dimen/home_header_title_inset"
|
||||||
|
android:ellipsize="end"
|
||||||
|
android:gravity="start|center_vertical"
|
||||||
|
android:includeFontPadding="false"
|
||||||
|
android:maxLines="1"
|
||||||
|
android:text="@string/home_title"
|
||||||
|
android:textColor="@color/home_text_primary"
|
||||||
|
android:textSize="17sp"
|
||||||
|
android:textStyle="bold" />
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_gravity="end|center_vertical"
|
||||||
|
android:clipChildren="false"
|
||||||
|
android:clipToPadding="false"
|
||||||
|
android:gravity="center_vertical"
|
||||||
|
android:orientation="horizontal">
|
||||||
|
|
||||||
<FrameLayout
|
<FrameLayout
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
@@ -91,21 +112,11 @@
|
|||||||
app:srcCompat="@drawable/ic_checkmark_circle_primary" />
|
app:srcCompat="@drawable/ic_checkmark_circle_primary" />
|
||||||
</FrameLayout>
|
</FrameLayout>
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:id="@+id/home_title"
|
|
||||||
android:layout_width="0dp"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_weight="1"
|
|
||||||
android:gravity="center"
|
|
||||||
android:text="@string/home_title"
|
|
||||||
android:textColor="@color/home_text_primary"
|
|
||||||
android:textSize="17sp"
|
|
||||||
android:textStyle="bold" />
|
|
||||||
|
|
||||||
<ImageButton
|
<ImageButton
|
||||||
android:id="@+id/home_filter_button"
|
android:id="@+id/home_filter_button"
|
||||||
android:layout_width="40dp"
|
android:layout_width="40dp"
|
||||||
android:layout_height="45dp"
|
android:layout_height="45dp"
|
||||||
|
android:layout_marginStart="@dimen/looply_space_sm"
|
||||||
android:background="@drawable/home_bg_circle"
|
android:background="@drawable/home_bg_circle"
|
||||||
android:clipChildren="false"
|
android:clipChildren="false"
|
||||||
android:clipToPadding="false"
|
android:clipToPadding="false"
|
||||||
@@ -118,6 +129,7 @@
|
|||||||
android:scaleType="fitCenter"
|
android:scaleType="fitCenter"
|
||||||
android:src="@drawable/ic_home_menu_lines"
|
android:src="@drawable/ic_home_menu_lines"
|
||||||
android:tint="@color/home_text_primary" />
|
android:tint="@color/home_text_primary" />
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
<ImageButton
|
<ImageButton
|
||||||
android:id="@+id/start_new_conversation_button"
|
android:id="@+id/start_new_conversation_button"
|
||||||
@@ -125,47 +137,44 @@
|
|||||||
android:layout_height="0dp"
|
android:layout_height="0dp"
|
||||||
android:visibility="gone"
|
android:visibility="gone"
|
||||||
android:contentDescription="@string/home_compose_content_description" />
|
android:contentDescription="@string/home_compose_content_description" />
|
||||||
</LinearLayout>
|
</FrameLayout>
|
||||||
|
|
||||||
<LinearLayout
|
<FrameLayout
|
||||||
android:id="@+id/home_selection_header"
|
android:id="@+id/home_selection_header"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:gravity="center_vertical"
|
android:minHeight="@dimen/home_header_height"
|
||||||
android:minHeight="52dp"
|
|
||||||
android:orientation="horizontal"
|
|
||||||
android:paddingStart="12dp"
|
android:paddingStart="12dp"
|
||||||
android:paddingTop="6dp"
|
android:paddingTop="6dp"
|
||||||
android:paddingEnd="12dp"
|
android:paddingEnd="12dp"
|
||||||
android:paddingBottom="6dp"
|
android:paddingBottom="5dp"
|
||||||
android:visibility="gone">
|
android:visibility="gone">
|
||||||
|
|
||||||
<ImageButton
|
|
||||||
android:id="@+id/home_selection_done"
|
|
||||||
android:layout_width="36dp"
|
|
||||||
android:layout_height="36dp"
|
|
||||||
android:background="@android:color/transparent"
|
|
||||||
android:contentDescription="@string/home_selection_done"
|
|
||||||
android:padding="0dp"
|
|
||||||
android:scaleType="fitCenter"
|
|
||||||
app:srcCompat="@drawable/ic_checkmark_circle_primary" />
|
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
android:layout_width="0dp"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_weight="1"
|
android:layout_gravity="start|center_vertical"
|
||||||
android:gravity="center"
|
android:layout_marginEnd="@dimen/home_header_title_inset"
|
||||||
|
android:ellipsize="end"
|
||||||
|
android:gravity="start|center_vertical"
|
||||||
android:includeFontPadding="false"
|
android:includeFontPadding="false"
|
||||||
|
android:maxLines="1"
|
||||||
android:text="@string/home_title"
|
android:text="@string/home_title"
|
||||||
android:textColor="@color/home_text_primary"
|
android:textColor="@color/home_text_primary"
|
||||||
android:textSize="17sp"
|
android:textSize="17sp"
|
||||||
android:textStyle="bold" />
|
android:textStyle="bold" />
|
||||||
|
|
||||||
<View
|
<ImageButton
|
||||||
|
android:id="@+id/home_selection_done"
|
||||||
android:layout_width="36dp"
|
android:layout_width="36dp"
|
||||||
android:layout_height="36dp"
|
android:layout_height="36dp"
|
||||||
android:importantForAccessibility="no" />
|
android:layout_gravity="end|center_vertical"
|
||||||
</LinearLayout>
|
android:background="@android:color/transparent"
|
||||||
|
android:contentDescription="@string/home_selection_done"
|
||||||
|
android:padding="0dp"
|
||||||
|
android:scaleType="fitCenter"
|
||||||
|
app:srcCompat="@drawable/ic_checkmark_circle_primary" />
|
||||||
|
</FrameLayout>
|
||||||
|
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
|
||||||
@@ -177,12 +186,6 @@
|
|||||||
android:clipChildren="true"
|
android:clipChildren="true"
|
||||||
android:clipToPadding="false">
|
android:clipToPadding="false">
|
||||||
|
|
||||||
<FrameLayout
|
|
||||||
android:id="@+id/home_contacts_host"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="match_parent"
|
|
||||||
android:visibility="gone" />
|
|
||||||
|
|
||||||
<androidx.recyclerview.widget.RecyclerView
|
<androidx.recyclerview.widget.RecyclerView
|
||||||
android:id="@android:id/list"
|
android:id="@android:id/list"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
@@ -201,6 +204,8 @@
|
|||||||
android:layout_marginBottom="@dimen/conversation_list_empty_text_bottom_margin"
|
android:layout_marginBottom="@dimen/conversation_list_empty_text_bottom_margin"
|
||||||
android:visibility="gone" />
|
android:visibility="gone" />
|
||||||
|
|
||||||
|
<include layout="@layout/home_list_profile_popup" />
|
||||||
|
|
||||||
<LinearLayout
|
<LinearLayout
|
||||||
android:id="@+id/home_search_bar_container"
|
android:id="@+id/home_search_bar_container"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
@@ -347,19 +352,6 @@
|
|||||||
android:layout_gravity="center" />
|
android:layout_gravity="center" />
|
||||||
</FrameLayout>
|
</FrameLayout>
|
||||||
|
|
||||||
<FrameLayout
|
|
||||||
android:layout_width="0dp"
|
|
||||||
android:layout_height="match_parent"
|
|
||||||
android:layout_weight="1">
|
|
||||||
|
|
||||||
<include
|
|
||||||
android:id="@+id/home_nav_contacts"
|
|
||||||
layout="@layout/home_bottom_nav_tab_item"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_gravity="center" />
|
|
||||||
</FrameLayout>
|
|
||||||
|
|
||||||
<FrameLayout
|
<FrameLayout
|
||||||
android:layout_width="0dp"
|
android:layout_width="0dp"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user