Messaging: Looply empty states, popovers, and conversation polish

Keep English-only UI, align home menus to the screen guideline, and
match message-action icon weight across conversation and list popovers.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-09-26 05:03:33 +09:00
co-authored by Cursor
parent 8e5d88183c
commit 88529c3634
398 changed files with 8661 additions and 37386 deletions
@@ -36,6 +36,7 @@ import com.android.messaging.sms.BugleApnSettingsLoader;
import com.android.messaging.sms.BugleUserAgentInfoLoader;
import com.android.messaging.sms.MmsConfig;
import com.android.messaging.ui.ConversationDrawables;
import com.android.messaging.ui.home.HomeToast;
import com.android.messaging.util.BuglePrefsKeys;
import com.android.messaging.util.LogUtil;
import com.android.messaging.util.PhoneUtils;
@@ -57,6 +58,7 @@ public class BugleApplication extends Application implements UncaughtExceptionHa
super.onCreate();
FactoryImpl.register(getApplicationContext(), this);
HomeToast.install(this);
sSystemUncaughtExceptionHandler = Thread.getDefaultUncaughtExceptionHandler();
Thread.setDefaultUncaughtExceptionHandler(this);
@@ -66,6 +66,7 @@ import com.android.messaging.datamodel.media.UriImageRequestDescriptor;
import com.android.messaging.sms.MmsSmsUtils;
import com.android.messaging.sms.MmsUtils;
import com.android.messaging.ui.UIIntents;
import com.android.messaging.ui.home.MutedConversationStore;
import com.android.messaging.util.Assert;
import com.android.messaging.util.AvatarUriUtil;
import com.android.messaging.util.BugleGservicesKeys;
@@ -354,6 +355,9 @@ public class BugleNotifications {
final Context context = Factory.get().getApplicationContext();
// TODO: Need to fix this for multi conversation notifications to rate limit dings.
final String conversationId = state.mConversationIds.first();
if (MutedConversationStore.isMuted(context, conversationId)) {
return;
}
String id = NotificationsUtil.DEFAULT_CHANNEL_ID;
if (NotificationsUtil.getNotificationChannel(context, conversationId) != null) {
id = conversationId;
@@ -103,7 +103,7 @@ public abstract class FileProvider extends ContentProvider {
final String fileId = uri.getPath();
if (isValidFileId(fileId)) {
final File file = getFile(fileId, getExtensionFromUri(uri));
return file.delete() ? 1 : 0;
return file != null && file.delete() ? 1 : 0;
}
return 0;
}
@@ -114,6 +114,9 @@ public abstract class FileProvider extends ContentProvider {
final String fileId = uri.getPath();
if (isValidFileId(fileId)) {
final File file = getFile(fileId, getExtensionFromUri(uri));
if (file == null) {
throw new FileNotFoundException(String.valueOf(uri));
}
final int mode =
(TextUtils.equals(fileMode, "r") ? ParcelFileDescriptor.MODE_READ_ONLY :
ParcelFileDescriptor.MODE_WRITE_ONLY | ParcelFileDescriptor.MODE_TRUNCATE);
@@ -46,7 +46,7 @@ public class MediaScratchFileProvider extends FileProvider {
private static final SimpleArrayMap<Uri, String> sUriToDisplayNameMap = new SimpleArrayMap<>();
public static final String AUTHORITY =
"com.android.messaging.datamodel.MediaScratchFileProvider";
"com.bnkode.notie.datamodel.MediaScratchFileProvider";
private static final String MEDIA_SCRATCH_SPACE_DIR = "mediascratchspace";
public static boolean isMediaScratchSpaceUri(final Uri uri) {
@@ -68,8 +68,8 @@ public class MediaScratchFileProvider extends FileProvider {
public static Uri buildMediaScratchSpaceUri(final String extension) {
final Uri uri = FileProvider.buildFileUri(AUTHORITY, extension);
final File file = getFileWithExtension(uri.getPath(), extension);
if (!ensureFileExists(file)) {
LogUtil.e(TAG, "Failed to create temp file " + file.getAbsolutePath());
if (file == null || !ensureFileExists(file)) {
LogUtil.e(TAG, "Failed to create temp file for " + uri);
}
return uri;
}
@@ -90,8 +90,12 @@ public class MediaScratchFileProvider extends FileProvider {
private static File getFileWithExtension(final String path, final String extension) {
final Context context = Factory.get().getApplicationContext();
String name = path;
if (name != null && name.startsWith("/")) {
name = name.substring(1);
}
final File filePath = new File(getDirectory(context),
TextUtils.isEmpty(extension) ? path : path + "." + extension);
TextUtils.isEmpty(extension) ? name : name + "." + extension);
try {
if (!filePath.getCanonicalPath()
@@ -53,7 +53,7 @@ public class MessagingContentProvider extends ContentProvider {
private static final String TAG = LogUtil.BUGLE_TAG;
public static final String AUTHORITY =
"com.android.messaging.datamodel.MessagingContentProvider";
"com.bnkode.notie.datamodel.MessagingContentProvider";
private static final String CONTENT_AUTHORITY = "content://" + AUTHORITY + '/';
// Conversations query
@@ -32,7 +32,7 @@ import java.io.IOException;
public class MmsFileProvider extends FileProvider {
private static final String TAG = LogUtil.BUGLE_TAG;
static final String AUTHORITY = "com.android.messaging.datamodel.MmsFileProvider";
static final String AUTHORITY = "com.bnkode.notie.datamodel.MmsFileProvider";
private static final String RAW_MMS_DIR = "rawmms";
/**
@@ -137,7 +137,7 @@ public class ActionServiceImpl extends JobIntentService {
* Broadcast receiver for alarms scheduled through ActionService.
*/
public static class PendingActionReceiver extends BroadcastReceiver {
static final String ACTION = "com.android.messaging.datamodel.PENDING_ACTION";
static final String ACTION = "com.bnkode.notie.datamodel.PENDING_ACTION";
/**
* Allocate an intent with a specific opcode and alarm action.
@@ -22,6 +22,7 @@ import android.widget.Toast;
import com.android.messaging.Factory;
import com.android.messaging.R;
import com.android.messaging.ui.home.HomeToast;
import com.android.messaging.datamodel.DataModel;
import com.android.messaging.datamodel.data.MessageData;
import com.android.messaging.datamodel.data.ParticipantData;
@@ -115,13 +116,13 @@ public class BugleActionToasts {
}
private static void showToast(final int messageResId) {
ThreadUtil.getMainThreadHandler().post(() -> Toast.makeText(getApplicationContext(),
getApplicationContext().getString(messageResId), Toast.LENGTH_LONG).show());
ThreadUtil.getMainThreadHandler().post(() -> HomeToast.show(getApplicationContext(),
messageResId, Toast.LENGTH_LONG));
}
private static void showToast(final String message) {
ThreadUtil.getMainThreadHandler().post(() ->
Toast.makeText(getApplicationContext(), message, Toast.LENGTH_LONG).show());
HomeToast.show(getApplicationContext(), message, Toast.LENGTH_LONG));
}
private static Context getApplicationContext() {
@@ -143,12 +144,11 @@ public class BugleActionToasts {
final boolean block,
final String destination) {
if (success) {
Toast.makeText(mContext,
HomeToast.show(mContext,
block
? R.string.update_destination_blocked
: R.string.update_destination_unblocked,
Toast.LENGTH_LONG
).show();
Toast.LENGTH_LONG);
}
}
}
@@ -367,27 +367,25 @@ public class ConversationMessageData {
* @return the text that is a concatenation of all text parts
*/
public String getText() {
// This is optimized for single text part case, which is the majority
return stripDisplayText(getRawText());
}
// For single text part, we just return the part without creating the StringBuilder
public String getRawText() {
// Optimized for the common single text part case.
String firstTextPart = null;
boolean foundText = false;
// For multiple text parts, we need the StringBuilder and the separator for concatenation
StringBuilder sb = null;
String separator = null;
for (final MessagePartData part : mParts) {
if (part.isText()) {
if (!foundText) {
// First text part
firstTextPart = part.getText();
foundText = true;
} else {
// Second and beyond
if (sb == null) {
// Need the StringBuilder and the separator starting from 2nd text part
sb = new StringBuilder();
if (!TextUtils.isEmpty(firstTextPart)) {
sb.append(firstTextPart);
sb.append(firstTextPart);
}
separator = BugleGservicesKeys.MMS_TEXT_CONCAT_SEPARATOR_DEFAULT;
}
@@ -401,13 +399,112 @@ public class ConversationMessageData {
}
}
}
if (sb == null) {
// Only one text part
return firstTextPart;
} else {
// More than one
return sb.toString();
return sb == null ? firstTextPart : sb.toString();
}
private static String stripDisplayText(final String text) {
return sanitizeDisplayText(text);
}
public static String sanitizeDisplayText(final String text) {
if (text == null) {
return null;
}
final StringBuilder out = new StringBuilder(text.length());
boolean lineStart = true;
for (int i = 0; i < text.length(); ) {
final int cp = text.codePointAt(i);
if (lineStart && isDisplayPadding(cp)) {
i += Character.charCount(cp);
continue;
}
lineStart = cp == '\n' || cp == '\r';
out.appendCodePoint(cp);
i += Character.charCount(cp);
}
int end = out.length();
while (end > 0 && isDisplayPadding(out.charAt(end - 1))) {
end--;
}
return end <= 0 ? null : out.substring(0, end);
}
/**
* Keep original spaces but flatten body line breaks so list / peek preview
* wraps by width only.
*/
public static String flattenPreviewText(final String text) {
if (text == null) {
return null;
}
final StringBuilder out = new StringBuilder(text.length());
boolean pendingBreak = false;
for (int i = 0; i < text.length(); ) {
final int cp = text.codePointAt(i);
if (cp == '\n' || cp == '\r') {
pendingBreak = true;
} else {
if (pendingBreak && out.length() > 0) {
out.append(' ');
}
pendingBreak = false;
out.appendCodePoint(cp);
}
i += Character.charCount(cp);
}
return out.length() == 0 ? null : out.toString();
}
/**
* Drop spaces next to CJK so wrapped lines do not start with a leftover space.
*/
public static String collapseCjkWrapSpaces(final String text) {
if (text == null || text.isEmpty()) {
return text;
}
final StringBuilder out = new StringBuilder(text.length());
for (int i = 0; i < text.length(); ) {
final int cp = text.codePointAt(i);
final int n = Character.charCount(cp);
if (cp == ' ' || cp == 0x00A0 || cp == 0x3000) {
final int prev = out.length() > 0 ? out.codePointBefore(out.length()) : 0;
final int next = i + n < text.length() ? text.codePointAt(i + n) : 0;
if (isCjk(prev) || isCjk(next)) {
i += n;
continue;
}
}
out.appendCodePoint(cp);
i += n;
}
return out.toString();
}
private static boolean isCjk(final int cp) {
return Character.UnicodeBlock.of(cp) == Character.UnicodeBlock.HANGUL_SYLLABLES
|| Character.UnicodeBlock.of(cp) == Character.UnicodeBlock.HANGUL_JAMO
|| Character.UnicodeBlock.of(cp) == Character.UnicodeBlock.HANGUL_COMPATIBILITY_JAMO
|| Character.UnicodeBlock.of(cp) == Character.UnicodeBlock.CJK_UNIFIED_IDEOGRAPHS
|| Character.UnicodeBlock.of(cp) == Character.UnicodeBlock.CJK_SYMBOLS_AND_PUNCTUATION;
}
private static boolean isDisplayPadding(final int cp) {
final int type = Character.getType(cp);
return Character.isWhitespace(cp)
|| Character.isSpaceChar(cp)
|| Character.isISOControl(cp)
|| type == Character.FORMAT
|| type == Character.CONTROL
|| cp == 0x00A0
|| cp == 0x1680
|| cp == 0x180E
|| cp == 0x200B
|| cp == 0x200C
|| cp == 0x200D
|| cp == 0x2060
|| cp == 0x3000
|| cp == 0x3164
|| cp == 0xFEFF;
}
public boolean hasAttachments() {
@@ -282,6 +282,12 @@ public class ParticipantData implements Parcelable {
return pd;
}
public static String getCachedParticipantId(final int subId) {
synchronized (sSubIdtoParticipantIdCache) {
return sSubIdtoParticipantIdCache.get(subId);
}
}
public static String getParticipantId(final DatabaseWrapper db, final int subId) {
String id;
synchronized (sSubIdtoParticipantIdCache) {
@@ -127,6 +127,14 @@ public class AvatarRequest extends UriImageRequest<AvatarRequestDescriptor> {
mContext.getResources(), R.drawable.ic_person_light, mContext.getTheme());
float dstWidth = Math.min(defaultPerson.getIntrinsicWidth() * SCALING_FACTOR, width);
float dstHeight = Math.min(defaultPerson.getIntrinsicHeight() * SCALING_FACTOR, height);
final float dest = Math.min(width, height);
// List-size tiles keep the 24dp * 1.33 silhouette. Profile-popup tiles
// (240dp) would otherwise leave a tiny icon in the center of the card.
if (dest > dstWidth * 2f) {
final float large = dest * 0.56f;
dstWidth = large;
dstHeight = large;
}
canvas.translate((width - dstWidth) / 2, (height - dstHeight) / 2);
defaultPerson.setBounds(0, 0, (int)dstWidth, (int)dstHeight);
@@ -38,13 +38,13 @@ import com.android.messaging.util.LogUtil;
*/
public class SendStatusReceiver extends BroadcastReceiver {
public static final String MESSAGE_SENT_ACTION =
"com.android.messaging.receiver.SendStatusReceiver.MESSAGE_SENT";
"com.bnkode.notie.receiver.SendStatusReceiver.MESSAGE_SENT";
public static final String MESSAGE_DELIVERED_ACTION =
"com.android.messaging.receiver.SendStatusReceiver.MESSAGE_DELIVERED";
"com.bnkode.notie.receiver.SendStatusReceiver.MESSAGE_DELIVERED";
public static final String MMS_SENT_ACTION =
"com.android.messaging.receiver.SendStatusReceiver.MMS_SENT";
"com.bnkode.notie.receiver.SendStatusReceiver.MMS_SENT";
public static final String MMS_DOWNLOADED_ACTION =
"com.android.messaging.receiver.SendStatusReceiver.MMS_DOWNLOADED";
"com.bnkode.notie.receiver.SendStatusReceiver.MMS_DOWNLOADED";
// Defined by platform, but no constant provided. See docs for SmsManager.sendTextMessage.
public static final String EXTRA_ERROR_CODE = "errorCode";
@@ -70,9 +70,6 @@ public class BugleActionBarActivity extends AppCompatActivity implements ImeUtil
mLastScreenHeight = getResources().getDisplayMetrics().heightPixels;
LogUtil.v(LogUtil.BUGLE_TAG, this.getLocalClassName() + ".onCreate");
getWindow().setStatusBarColor(getResources().getColor(R.color.action_bar_background_color,
getTheme()));
}
@Override
@@ -42,6 +42,7 @@ public class ContactIconView extends AsyncImageView {
private static final int LARGE_ICON_SIZE_ID = 1;
private static final int SMALL_ICON_SIZE_ID = 2;
private static final int PROFILE_POPUP_ICON_SIZE_ID = 3;
private static final int HERO_ICON_SIZE_ID = 4;
protected final int mIconSize;
private final boolean mCropToCircle;
@@ -79,6 +80,10 @@ public class ContactIconView extends AsyncImageView {
R.dimen.home_list_profile_avatar_size);
cropToCircle = false;
break;
case HERO_ICON_SIZE_ID:
mIconSize = (int) resources.getDimension(
R.dimen.home_profile_avatar_size);
break;
default:
// For the compiler, something has to be set even with the assert.
mIconSize = 0;
@@ -17,20 +17,31 @@
package com.android.messaging.ui;
import android.app.Activity;
import android.os.Bundle;
import android.webkit.WebView;
import com.android.messaging.R;
import com.android.messaging.ui.home.HomeSubHeader;
public class LicenseActivity extends Activity {
private final String LICENSE_URL = "file:///android_asset/licenses.html";
public class LicenseActivity extends BugleActionBarActivity {
private static final String LICENSE_URL = "file:///android_asset/licenses.html";
@Override
public void onCreate(final Bundle bundle) {
super.onCreate(bundle);
setContentView(R.layout.license_activity);
protected void onCreate(final Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.home_license_activity);
if (getSupportActionBar() != null) {
getSupportActionBar().hide();
}
HomeSubHeader.bind(this, R.string.home_settings_licenses, v -> finish());
final WebView webView = findViewById(R.id.content);
webView.setBackgroundColor(getColor(R.color.home_bg));
webView.loadUrl(LICENSE_URL);
}
@Override
public void finish() {
super.finish();
overridePendingTransition(R.anim.home_activity_hold, R.anim.home_activity_close);
}
}
@@ -22,7 +22,6 @@ import androidx.annotation.Nullable;
import android.text.TextUtils;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup.MarginLayoutParams;
import android.widget.FrameLayout;
import android.widget.TextView;
@@ -272,18 +271,12 @@ public class SnackBar {
private void setUpButton() {
if (mAction == null || mAction.getActionRunnable() == null) {
mActionTextView.setVisibility(View.GONE);
// In the XML layout we add left/right padding to the button to add space between
// the message text and the button and on the right side we add padding to put space
// between the button and the edge of the snack bar. This is so the button can use the
// padding area as part of it's click target. Since we have no button, we need to put
// some margin on the right side. While the left margin is already set on the wrapper,
// we're setting it again to not have to make a special case for RTL.
final MarginLayoutParams lp = (MarginLayoutParams) mMessageWrapper.getLayoutParams();
final int leftRightMargin = mContext.getResources().getDimensionPixelSize(
R.dimen.snack_bar_left_right_margin);
lp.leftMargin = leftRightMargin;
lp.rightMargin = leftRightMargin;
mMessageWrapper.setLayoutParams(lp);
final int endPad = mSnackBarView.getPaddingStart();
mSnackBarView.setPaddingRelative(
mSnackBarView.getPaddingStart(),
mSnackBarView.getPaddingTop(),
endPad,
mSnackBarView.getPaddingBottom());
} else {
mActionTextView.setVisibility(View.VISIBLE);
mActionTextView.setText(mAction.getActionLabel());
@@ -69,7 +69,7 @@ public abstract class UIIntents {
public static final String UI_INTENT_EXTRA_WITH_CUSTOM_TRANSITION = "with_custom_transition";
public static final String ACTION_RESET_NOTIFICATIONS =
"com.android.messaging.reset_notifications";
"com.bnkode.notie.reset_notifications";
// Sending VCard uri to VCard detail activity
public static final String UI_INTENT_EXTRA_VCARD_URI = "vcard_uri";
@@ -103,7 +103,7 @@ public abstract class UIIntents {
public static final String UI_INTENT_EXTRA_ATTACHMENT_TYPE = "attachment_type";
public static final String ACTION_WIDGET_CONVERSATION =
"com.android.messaging.widget_conversation:";
"com.bnkode.notie.widget_conversation:";
public static final String UI_INTENT_EXTRA_REQUIRES_MMS = "requires_mms";
@@ -210,6 +210,16 @@ public abstract class UIIntents {
*/
public abstract void launchHomeComposeActivity(final Context context);
/**
* Launch home UI for message automations.
*/
public abstract void launchHomeAutomationActivity(final Context context);
/**
* Launch home UI to create an automation task.
*/
public abstract void launchHomeAutomationCreateActivity(final Context context);
/**
* Launch an activity to show a class zero message
*/
@@ -234,6 +234,20 @@ public class UIIntentsImpl extends UIIntents {
context.startActivity(intent);
}
@Override
public void launchHomeAutomationActivity(final Context context) {
final Intent intent = new Intent(context,
com.android.messaging.ui.home.HomeAutomationActivity.class);
context.startActivity(intent);
}
@Override
public void launchHomeAutomationCreateActivity(final Context context) {
final Intent intent = new Intent(context,
com.android.messaging.ui.home.HomeAutomationCreateActivity.class);
context.startActivity(intent);
}
@Override
public void launchHomeProfileActivity(final Activity activity, final String conversationId) {
final Intent intent = new Intent(activity,
@@ -24,148 +24,142 @@ import android.content.Intent;
import android.os.Bundle;
import android.provider.Settings;
import android.text.TextUtils;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import androidx.activity.result.ActivityResultLauncher;
import androidx.activity.result.contract.ActivityResultContracts;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.core.app.NavUtils;
import androidx.fragment.app.FragmentTransaction;
import androidx.preference.Preference;
import androidx.preference.PreferenceFragmentCompat;
import androidx.preference.PreferenceScreen;
import com.android.messaging.R;
import com.android.messaging.ui.BugleActionBarActivity;
import com.android.messaging.ui.LicenseActivity;
import com.android.messaging.ui.UIIntents;
import com.android.messaging.ui.home.HomeSettingsRow;
import com.android.messaging.ui.home.HomeSubHeader;
import com.android.messaging.util.BuglePrefs;
import com.android.messaging.util.ChangeDefaultSmsAppHelper;
import com.android.messaging.util.PhoneUtils;
public class ApplicationSettingsActivity extends BugleActionBarActivity {
private View mSmsRow;
private ChangeDefaultSmsAppHelper mChangeDefaultSmsAppHelper;
private final ActivityResultLauncher<Intent> mLauncher = registerForActivityResult(
new ActivityResultContracts.StartActivityForResult(), result -> {
if (result.getResultCode() == Activity.RESULT_OK) {
if (mChangeDefaultSmsAppHelper == null) {
mChangeDefaultSmsAppHelper = new ChangeDefaultSmsAppHelper();
}
mChangeDefaultSmsAppHelper.handleChangeDefaultSmsResult(
REQUEST_SET_DEFAULT_SMS_APP, result.getResultCode(), null);
}
});
@Override
protected void onCreate(final Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.home_settings_activity);
if (getSupportActionBar() != null) {
getSupportActionBar().hide();
}
HomeSubHeader.bind(this, R.string.home_settings_title, v -> finish());
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
mSmsRow = findViewById(R.id.home_settings_sms);
bindStaticRows();
updateSmsRow();
}
@Override
protected void onResume() {
super.onResume();
updateSmsRow();
}
@Override
public void finish() {
super.finish();
overridePendingTransition(R.anim.home_activity_hold, R.anim.home_activity_close);
}
private void bindStaticRows() {
final BuglePrefs prefs = BuglePrefs.getApplicationPrefs();
final boolean topLevel = getIntent().getBooleanExtra(
UIIntents.UI_INTENT_EXTRA_TOP_LEVEL_SETTINGS, false);
HomeSettingsRow.bind(findViewById(R.id.home_settings_notifications),
R.drawable.ic_home_bell,
getString(R.string.home_settings_notifications),
getString(R.string.home_settings_notifications_subtitle),
true, false, false,
v -> {
final Intent intent = new Intent(Settings.ACTION_APP_NOTIFICATION_SETTINGS);
intent.putExtra(Settings.EXTRA_APP_PACKAGE, getPackageName());
startActivity(intent);
}, null);
final String sendKey = getString(R.string.send_sound_pref_key);
final boolean sendSound = prefs.getBoolean(sendKey,
getResources().getBoolean(R.bool.send_sound_pref_default));
HomeSettingsRow.bind(findViewById(R.id.home_settings_send_sound),
R.drawable.ic_home_sound,
getString(R.string.home_settings_send_sound),
null, false, true, sendSound,
null, (button, checked) -> prefs.putBoolean(sendKey, checked));
final String swipeKey = getString(R.string.swipe_right_deletes_conversation_key);
final boolean swipeDelete = prefs.getBoolean(swipeKey, false);
HomeSettingsRow.bind(findViewById(R.id.home_settings_swipe_delete),
R.drawable.ic_home_menu_trash,
getString(R.string.home_settings_swipe_delete),
getString(R.string.home_settings_swipe_delete_subtitle),
false, true, swipeDelete,
null, (button, checked) -> prefs.putBoolean(swipeKey, checked));
final View advancedCard = findViewById(R.id.home_settings_advanced_card);
if (topLevel) {
getSupportActionBar().setTitle(getString(R.string.settings_activity_title));
advancedCard.setVisibility(View.VISIBLE);
HomeSettingsRow.bind(findViewById(R.id.home_settings_advanced),
R.drawable.ic_home_nav_settings,
getString(R.string.home_settings_advanced),
null, true, false, false,
v -> {
startActivity(UIIntents.get().getAdvancedSettingsIntent(this));
overridePendingTransition(R.anim.home_activity_open,
R.anim.home_activity_hold);
},
null);
} else {
advancedCard.setVisibility(View.GONE);
}
FragmentTransaction ft = getSupportFragmentManager().beginTransaction();
ft.replace(android.R.id.content, new ApplicationSettingsFragment());
ft.commit();
HomeSettingsRow.bind(findViewById(R.id.home_settings_licenses),
R.drawable.ic_info_light,
getString(R.string.home_settings_licenses),
null, true, false, false,
v -> {
startActivity(new Intent(this, LicenseActivity.class));
overridePendingTransition(R.anim.home_activity_open, R.anim.home_activity_hold);
},
null);
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
if (super.onCreateOptionsMenu(menu)) {
return true;
private void updateSmsRow() {
if (mSmsRow == null) {
return;
}
getMenuInflater().inflate(R.menu.settings_menu, menu);
return true;
}
@Override
public boolean onOptionsItemSelected(@NonNull final MenuItem item) {
int itemId = item.getItemId();
if (itemId == android.R.id.home) {
NavUtils.navigateUpFromSameTask(this);
return true;
} else if (itemId == R.id.action_license) {
final Intent intent = new Intent(this, LicenseActivity.class);
startActivity(intent);
return true;
String summary = getString(R.string.default_sms_app,
PhoneUtils.getDefault().getDefaultSmsAppLabel());
if (TextUtils.isEmpty(PhoneUtils.getDefault().getDefaultSmsAppLabel())) {
summary = getString(R.string.app_name);
}
return super.onOptionsItemSelected(item);
}
public static class ApplicationSettingsFragment extends PreferenceFragmentCompat {
private String mNotificationsPreferenceKey;
private String mSmsEnabledPrefKey;
private Preference mSmsEnabledPreference;
private ChangeDefaultSmsAppHelper mChangeDefaultSmsAppHelper;
private final ActivityResultLauncher<Intent> mLauncher = registerForActivityResult(
new ActivityResultContracts.StartActivityForResult(), result -> {
if (result.getResultCode() == Activity.RESULT_OK) {
if (mChangeDefaultSmsAppHelper == null) {
mChangeDefaultSmsAppHelper = new ChangeDefaultSmsAppHelper();
}
mChangeDefaultSmsAppHelper.handleChangeDefaultSmsResult(
REQUEST_SET_DEFAULT_SMS_APP, result.getResultCode(), null);
HomeSettingsRow.bind(mSmsRow,
R.drawable.ic_home_nav_messages,
getString(R.string.home_settings_default_sms),
summary, true, false, false,
v -> {
if (!PhoneUtils.getDefault().isDefaultSmsApp()) {
final Intent intent = UIIntents.get().getChangeDefaultSmsAppIntent(this);
mLauncher.launch(intent);
}
});
public ApplicationSettingsFragment() {
// Required empty constructor
}
@Override
public void onCreatePreferences(@Nullable Bundle savedInstanceState,
@Nullable String rootKey) {
getPreferenceManager().setSharedPreferencesName(BuglePrefs.SHARED_PREFERENCES_NAME);
addPreferencesFromResource(R.xml.preferences_application);
mNotificationsPreferenceKey =
getString(R.string.notifications_pref_key);
mSmsEnabledPrefKey = getString(R.string.sms_enabled_pref_key);
mSmsEnabledPreference = findPreference(mSmsEnabledPrefKey);
final PreferenceScreen advancedScreen = findPreference(
getString(R.string.advanced_pref_key));
final boolean topLevel = getActivity().getIntent().getBooleanExtra(
UIIntents.UI_INTENT_EXTRA_TOP_LEVEL_SETTINGS, false);
if (topLevel) {
advancedScreen.setIntent(UIIntents.get()
.getAdvancedSettingsIntent(getPreferenceScreen().getContext()));
} else {
// Hide the Advanced settings screen if this is not top-level; these are shown at
// the parent SettingsActivity.
getPreferenceScreen().removePreference(advancedScreen);
}
}
@Override
public boolean onPreferenceTreeClick(@NonNull Preference preference) {
if (preference.getKey().equals(mNotificationsPreferenceKey)) {
Intent intent = new Intent(Settings.ACTION_APP_NOTIFICATION_SETTINGS);
intent.putExtra(Settings.EXTRA_APP_PACKAGE, getContext().getPackageName());
startActivity(intent);
}
return super.onPreferenceTreeClick(preference);
}
private void updateSmsEnabledPreferences() {
String defaultSmsAppLabel = getString(R.string.default_sms_app,
PhoneUtils.getDefault().getDefaultSmsAppLabel());
if (!PhoneUtils.getDefault().isDefaultSmsApp()) {
mSmsEnabledPreference.setOnPreferenceClickListener(preference -> {
final Intent intent =
UIIntents.get().getChangeDefaultSmsAppIntent(getActivity());
mLauncher.launch(intent);
return true;
});
} else {
mSmsEnabledPreference.setOnPreferenceClickListener(null);
// Fallback since we don't always get our own name
if (TextUtils.isEmpty(defaultSmsAppLabel)) {
defaultSmsAppLabel = getString(R.string.app_name);
}
}
mSmsEnabledPreference.setSummary(defaultSmsAppLabel);
}
@Override
public void onResume() {
super.onResume();
updateSmsEnabledPreferences();
}
}, null);
}
}
@@ -16,12 +16,13 @@
*/
package com.android.messaging.ui.appsettings;
import android.app.AlertDialog;
import android.content.Context;
import android.view.LayoutInflater;
import android.view.View;
import android.widget.RadioButton;
import androidx.appcompat.app.AlertDialog;
import com.android.messaging.R;
import com.android.messaging.util.Assert;
import com.android.messaging.util.BuglePrefs;
@@ -48,9 +49,9 @@ public class GroupMmsSettingDialog {
private void show() {
Assert.isNull(mDialog);
mDialog = new AlertDialog.Builder(mContext, R.style.AlertDialogTheme)
mDialog = new AlertDialog.Builder(mContext, R.style.Home_Dialog)
.setView(createView())
.setTitle(R.string.group_mms_pref_title)
.setTitle(R.string.home_settings_group_mms)
.setNegativeButton(android.R.string.cancel, null)
.show();
}
@@ -17,174 +17,218 @@
package com.android.messaging.ui.appsettings;
import android.content.Intent;
import android.content.SharedPreferences;
import android.content.SharedPreferences.OnSharedPreferenceChangeListener;
import android.os.Bundle;
import android.text.TextUtils;
import android.view.MenuItem;
import android.view.LayoutInflater;
import android.view.View;
import android.widget.EditText;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.core.app.NavUtils;
import androidx.fragment.app.FragmentTransaction;
import androidx.preference.Preference;
import androidx.preference.PreferenceCategory;
import androidx.preference.PreferenceFragmentCompat;
import androidx.appcompat.app.AlertDialog;
import com.android.messaging.Factory;
import com.android.messaging.R;
import com.android.messaging.datamodel.ParticipantRefresh;
import com.android.messaging.datamodel.data.ParticipantData;
import com.android.messaging.sms.MmsConfig;
import com.android.messaging.ui.BugleActionBarActivity;
import com.android.messaging.ui.UIIntents;
import com.android.messaging.ui.home.HomeSettingsRow;
import com.android.messaging.ui.home.HomeSubHeader;
import com.android.messaging.util.BuglePrefs;
import com.android.messaging.util.PhoneUtils;
public class PerSubscriptionSettingsActivity extends BugleActionBarActivity {
private int mSubId;
private BuglePrefs mPrefs;
private String mDefaultPhoneNumber;
private View mGroupRow;
private View mPhoneRow;
private View mRoamingRow;
@Override
protected void onCreate(final Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
final String title = getIntent().getStringExtra(
UIIntents.UI_INTENT_EXTRA_PER_SUBSCRIPTION_SETTING_TITLE);
if (!TextUtils.isEmpty(title)) {
getSupportActionBar().setTitle(title);
} else {
// This will fall back to the default title, i.e. "Messaging settings," so No-op.
setContentView(R.layout.home_settings_advanced_activity);
if (getSupportActionBar() != null) {
getSupportActionBar().hide();
}
final FragmentTransaction ft = getSupportFragmentManager().beginTransaction();
final PerSubscriptionSettingsFragment fragment = new PerSubscriptionSettingsFragment();
ft.replace(android.R.id.content, fragment);
ft.commit();
mSubId = getIntent().getIntExtra(UIIntents.UI_INTENT_EXTRA_SUB_ID,
ParticipantData.DEFAULT_SELF_SUB_ID);
mPrefs = BuglePrefs.getSubscriptionPrefs(mSubId);
mDefaultPhoneNumber = PhoneUtils.get(mSubId).getCanonicalForSelf(false /* allowOverride */);
final String title = getIntent().getStringExtra(
UIIntents.UI_INTENT_EXTRA_PER_SUBSCRIPTION_SETTING_TITLE);
HomeSubHeader.bind(this,
TextUtils.isEmpty(title) ? getString(R.string.home_settings_advanced) : title,
v -> finish());
mGroupRow = findViewById(R.id.home_settings_group_mms);
mPhoneRow = findViewById(R.id.home_settings_phone_number);
mRoamingRow = findViewById(R.id.home_settings_roaming_retrieve);
bindRows();
}
@Override
public boolean onOptionsItemSelected(@NonNull final MenuItem item) {
if (item.getItemId() == android.R.id.home) {
NavUtils.navigateUpFromSameTask(this);
return true;
}
return super.onOptionsItemSelected(item);
protected void onResume() {
super.onResume();
updateGroupRow();
updatePhoneRow();
}
public static class PerSubscriptionSettingsFragment extends PreferenceFragmentCompat
implements OnSharedPreferenceChangeListener {
private PhoneNumberPreference mPhoneNumberPreference;
private Preference mGroupMmsPreference;
private String mGroupMmsPrefKey;
private String mPhoneNumberKey;
private int mSubId;
@Override
public void finish() {
super.finish();
overridePendingTransition(R.anim.home_activity_hold, R.anim.home_activity_close);
}
public PerSubscriptionSettingsFragment() {
// Required empty constructor
private void bindRows() {
final boolean defaultSms = PhoneUtils.getDefault().isDefaultSmsApp();
final boolean groupEnabled = MmsConfig.get(mSubId).getGroupMmsEnabled();
final boolean deliveryEnabled = MmsConfig.get(mSubId).getSMSDeliveryReportsEnabled();
if (groupEnabled) {
mGroupRow.setVisibility(View.VISIBLE);
updateGroupRow();
HomeSettingsRow.setEnabled(mGroupRow, defaultSms);
} else {
mGroupRow.setVisibility(View.GONE);
}
@Override
public void onCreatePreferences(@Nullable Bundle savedInstanceState, String rootKey) {
// Get sub id from launch intent
final Intent intent = requireActivity().getIntent();
mSubId = (intent != null) ? intent.getIntExtra(UIIntents.UI_INTENT_EXTRA_SUB_ID,
ParticipantData.DEFAULT_SELF_SUB_ID) : ParticipantData.DEFAULT_SELF_SUB_ID;
updatePhoneRow();
final BuglePrefs subPrefs = Factory.get().getSubscriptionPrefs(mSubId);
getPreferenceManager().setSharedPreferencesName(subPrefs.getSharedPreferencesName());
addPreferencesFromResource(R.xml.preferences_per_subscription);
mPhoneNumberKey = getString(R.string.mms_phone_number_pref_key);
mPhoneNumberPreference = findPreference(mPhoneNumberKey);
final PreferenceCategory advancedCategory =
findPreference(getString(R.string.advanced_category_pref_key));
final PreferenceCategory mmsCategory =
findPreference(getString(R.string.mms_messaging_category_pref_key));
mPhoneNumberPreference.setDefaultPhoneNumber(
PhoneUtils.get(mSubId).getCanonicalForSelf(false/*allowOverride*/), mSubId);
mGroupMmsPrefKey = getString(R.string.group_mms_pref_key);
mGroupMmsPreference = findPreference(mGroupMmsPrefKey);
if (!MmsConfig.get(mSubId).getGroupMmsEnabled()) {
// Always show group messaging setting even if the SIM has no number
// If broadcast sms is selected, the SIM number is not needed
// If group mms is selected, the phone number dialog will popup when message
// is being sent, making sure we will have a self number for group mms.
mmsCategory.removePreference(mGroupMmsPreference);
} else {
mGroupMmsPreference.setOnPreferenceClickListener(pref -> {
GroupMmsSettingDialog.showDialog(getActivity(), mSubId);
return true;
final String autoKey = getString(R.string.auto_retrieve_mms_pref_key);
final boolean autoOn = mPrefs.getBoolean(autoKey,
getResources().getBoolean(R.bool.auto_retrieve_mms_pref_default));
HomeSettingsRow.bind(findViewById(R.id.home_settings_auto_retrieve),
R.drawable.ic_home_file_line,
getString(R.string.home_settings_auto_retrieve),
getString(R.string.home_settings_auto_retrieve_subtitle),
false, true, autoOn, null,
(button, checked) -> {
mPrefs.putBoolean(autoKey, checked);
updateRoamingEnabled(checked && defaultSms);
});
updateGroupMmsPrefSummary();
}
HomeSettingsRow.setEnabled(findViewById(R.id.home_settings_auto_retrieve), defaultSms);
if (!MmsConfig.get(mSubId).getSMSDeliveryReportsEnabled()) {
final Preference deliveryReportsPref = findPreference(
getString(R.string.delivery_reports_pref_key));
advancedCategory.removePreference(deliveryReportsPref);
}
final String roamingKey = getString(R.string.auto_retrieve_mms_when_roaming_pref_key);
final boolean roamingOn = mPrefs.getBoolean(roamingKey,
getResources().getBoolean(R.bool.auto_retrieve_mms_when_roaming_pref_default));
HomeSettingsRow.bind(mRoamingRow,
R.drawable.ic_home_place_line,
getString(R.string.home_settings_roaming_retrieve),
getString(R.string.home_settings_roaming_retrieve_subtitle),
false, true, roamingOn, null,
(button, checked) -> mPrefs.putBoolean(roamingKey, checked));
updateRoamingEnabled(autoOn && defaultSms);
// We want to disable preferences if we are not the default app, but we do all of the
// above first so that the user sees the correct information on the screen
if (!PhoneUtils.getDefault().isDefaultSmsApp()) {
mGroupMmsPreference.setEnabled(false);
final Preference autoRetrieveMmsPreference =
findPreference(getString(R.string.auto_retrieve_mms_pref_key));
autoRetrieveMmsPreference.setEnabled(false);
final Preference deliveryReportsPreference =
findPreference(getString(R.string.delivery_reports_pref_key));
if (deliveryReportsPreference != null) {
deliveryReportsPreference.setEnabled(false);
}
}
if (deliveryEnabled) {
final String deliveryKey = getString(R.string.delivery_reports_pref_key);
final boolean deliveryOn = mPrefs.getBoolean(deliveryKey,
getResources().getBoolean(R.bool.delivery_reports_pref_default));
HomeSettingsRow.bind(findViewById(R.id.home_settings_delivery_reports),
R.drawable.ic_home_checklist,
getString(R.string.home_settings_delivery_reports),
getString(R.string.home_settings_delivery_reports_subtitle),
false, true, deliveryOn, null,
(button, checked) -> mPrefs.putBoolean(deliveryKey, checked));
HomeSettingsRow.setEnabled(findViewById(R.id.home_settings_delivery_reports),
defaultSms);
} else {
findViewById(R.id.home_settings_delivery_label).setVisibility(View.GONE);
findViewById(R.id.home_settings_delivery_card).setVisibility(View.GONE);
}
}
if (advancedCategory.getPreferenceCount() == 0) {
getPreferenceScreen().removePreference(advancedCategory);
private void updateGroupRow() {
if (mGroupRow == null || mGroupRow.getVisibility() != View.VISIBLE) {
return;
}
final boolean groupOn = mPrefs.getBoolean(getString(R.string.group_mms_pref_key),
getResources().getBoolean(R.bool.group_mms_pref_default));
HomeSettingsRow.bind(mGroupRow,
R.drawable.ic_home_nav_messages,
getString(R.string.home_settings_group_mms),
getString(groupOn
? R.string.home_settings_group_mms_on
: R.string.home_settings_group_mms_off),
true, false, false,
v -> GroupMmsSettingDialog.showDialog(this, mSubId),
null);
HomeSettingsRow.setEnabled(mGroupRow, PhoneUtils.getDefault().isDefaultSmsApp());
}
private void updatePhoneRow() {
if (mPhoneRow == null) {
return;
}
HomeSettingsRow.bind(mPhoneRow,
R.drawable.ic_home_menu_person,
getString(R.string.home_settings_phone_number),
formatPhoneSummary(),
true, false, false,
v -> showPhoneNumberDialog(),
null);
}
private void updateRoamingEnabled(final boolean enabled) {
if (mRoamingRow != null) {
HomeSettingsRow.setEnabled(mRoamingRow, enabled);
}
}
private String formatPhoneSummary() {
String value = mPrefs.getString(getString(R.string.mms_phone_number_pref_key), "");
if (TextUtils.isEmpty(value)) {
value = mDefaultPhoneNumber;
}
if (TextUtils.isEmpty(value)) {
return getString(R.string.home_settings_phone_unknown);
}
return PhoneUtils.get(mSubId).formatForDisplay(value);
}
private void showPhoneNumberDialog() {
final View content = LayoutInflater.from(this)
.inflate(R.layout.home_settings_phone_dialog, null);
final EditText field = content.findViewById(R.id.home_settings_phone_field);
String value = mPrefs.getString(getString(R.string.mms_phone_number_pref_key), "");
if (TextUtils.isEmpty(value)) {
value = mDefaultPhoneNumber;
}
if (value == null) {
value = "";
}
field.setText(value);
if (!TextUtils.isEmpty(value)) {
field.setSelection(value.length());
}
new AlertDialog.Builder(this, R.style.Home_Dialog)
.setTitle(R.string.home_settings_phone_number)
.setView(content)
.setNegativeButton(android.R.string.cancel, null)
.setPositiveButton(android.R.string.ok, (dialog, which) -> {
savePhoneNumber(field.getText().toString().trim());
updatePhoneRow();
})
.show();
}
private void savePhoneNumber(final String text) {
final String key = getString(R.string.mms_phone_number_pref_key);
if (TextUtils.isEmpty(text)) {
mPrefs.remove(key);
} else {
final PhoneUtils phoneUtils = PhoneUtils.get(mSubId);
final String canonical = phoneUtils.getCanonicalBySystemLocale(text);
if (!TextUtils.isEmpty(mDefaultPhoneNumber)
&& TextUtils.equals(canonical,
phoneUtils.getCanonicalBySystemLocale(mDefaultPhoneNumber))) {
mPrefs.remove(key);
} else {
mPrefs.putString(key, text);
}
}
private void updateGroupMmsPrefSummary() {
final boolean groupMmsEnabled = getPreferenceScreen().getSharedPreferences().getBoolean(
mGroupMmsPrefKey, getResources().getBoolean(R.bool.group_mms_pref_default));
mGroupMmsPreference.setSummary(groupMmsEnabled ?
R.string.enable_group_mms : R.string.disable_group_mms);
}
@Override
public void onResume() {
super.onResume();
getPreferenceScreen().getSharedPreferences()
.registerOnSharedPreferenceChangeListener(this);
}
@Override
public void onSharedPreferenceChanged(final SharedPreferences sharedPreferences,
final String key) {
if (key.equals(mGroupMmsPrefKey)) {
updateGroupMmsPrefSummary();
} else if (key.equals(mPhoneNumberKey)) {
// Save the changed phone number in preferences specific to the sub id
final String newPhoneNumber = mPhoneNumberPreference.getText();
final BuglePrefs subPrefs = BuglePrefs.getSubscriptionPrefs(mSubId);
if (TextUtils.isEmpty(newPhoneNumber)) {
subPrefs.remove(mPhoneNumberKey);
} else {
subPrefs.putString(getString(R.string.mms_phone_number_pref_key),
newPhoneNumber);
}
// Update the self participants so the new phone number will be reflected
// everywhere in the UI.
ParticipantRefresh.refreshSelfParticipants();
}
}
@Override
public void onPause() {
super.onPause();
getPreferenceScreen().getSharedPreferences()
.unregisterOnSharedPreferenceChangeListener(this);
}
ParticipantRefresh.refreshSelfParticipants();
}
}
@@ -17,20 +17,11 @@
package com.android.messaging.ui.appsettings;
import android.content.Context;
import android.os.Bundle;
import android.text.TextUtils;
import android.view.LayoutInflater;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ArrayAdapter;
import android.widget.ListView;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.core.app.NavUtils;
import androidx.fragment.app.Fragment;
import androidx.loader.app.LoaderManager;
import com.android.messaging.R;
@@ -42,137 +33,98 @@ import com.android.messaging.datamodel.data.SettingsData.SettingsDataListener;
import com.android.messaging.datamodel.data.SettingsData.SettingsItem;
import com.android.messaging.ui.BugleActionBarActivity;
import com.android.messaging.ui.UIIntents;
import com.android.messaging.ui.home.HomeSettingsRow;
import com.android.messaging.ui.home.HomeSubHeader;
import com.android.messaging.util.Assert;
import com.android.messaging.util.PhoneUtils;
import java.util.ArrayList;
import java.util.List;
/**
* Shows the "primary" settings activity that contains two parts, one for application-wide settings
* (dubbed "General settings"), and one or more for per-subscription settings (dubbed "Messaging
* settings" for single-SIM, and the actual SIM name for multi-SIM). Clicking on either item
* (e.g. "General settings") will open the detail settings activity (ApplicationSettingsActivity
* in this case).
* Multi-SIM settings hub. Single-SIM devices skip this and open ApplicationSettingsActivity.
*/
public class SettingsActivity extends BugleActionBarActivity {
public class SettingsActivity extends BugleActionBarActivity implements SettingsDataListener {
private final Binding<SettingsData> mBinding = BindingBase.createBinding(this);
private ViewGroup mList;
@Override
protected void onCreate(final Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
// Directly open the detailed settings page as the top-level settings activity if this is
// not a multi-SIM device.
if (PhoneUtils.getDefault().getActiveSubscriptionCount() <= 1) {
UIIntents.get().launchApplicationSettingsActivity(this, true /* topLevel */);
finish();
} else {
getSupportFragmentManager().beginTransaction()
.replace(android.R.id.content, new SettingsFragment())
.commit();
overridePendingTransition(0, 0);
return;
}
setContentView(R.layout.home_settings_hub_activity);
if (getSupportActionBar() != null) {
getSupportActionBar().hide();
}
HomeSubHeader.bind(this, R.string.home_settings_title, v -> finish());
mList = findViewById(R.id.home_settings_hub_list);
mBinding.bind(DataModel.get().createSettingsData(this, this));
mBinding.getData().init(LoaderManager.getInstance(this), mBinding);
}
@Override
protected void onDestroy() {
super.onDestroy();
if (mBinding.isBound()) {
mBinding.unbind();
}
}
@Override
public boolean onOptionsItemSelected(@NonNull final MenuItem item) {
if (item.getItemId() == android.R.id.home) {
NavUtils.navigateUpFromSameTask(this);
return true;
}
return super.onOptionsItemSelected(item);
public void finish() {
super.finish();
overridePendingTransition(R.anim.home_activity_hold, R.anim.home_activity_close);
}
public static class SettingsFragment extends Fragment implements SettingsDataListener {
private ListView mListView;
private SettingsListAdapter mAdapter;
private final Binding<SettingsData> mBinding = BindingBase.createBinding(this);
@Override
public void onCreate(final Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
mBinding.bind(DataModel.get().createSettingsData(getActivity(), this));
mBinding.getData().init(LoaderManager.getInstance(this), mBinding);
@Override
public void onSelfParticipantDataLoaded(final SettingsData data) {
mBinding.ensureBound(data);
if (mList == null) {
return;
}
@Override
public View onCreateView(final LayoutInflater inflater, final ViewGroup container,
final Bundle savedInstanceState) {
final View view = inflater.inflate(R.layout.settings_fragment, container, false);
mListView = view.findViewById(android.R.id.list);
mAdapter = new SettingsListAdapter(getActivity());
mListView.setAdapter(mAdapter);
return view;
}
@Override
public void onDestroy() {
super.onDestroy();
mBinding.unbind();
}
@Override
public void onSelfParticipantDataLoaded(SettingsData data) {
mBinding.ensureBound(data);
mAdapter.setSettingsItems(data.getSettingsItems());
}
/**
* An adapter that displays a list of SettingsItem.
*/
private class SettingsListAdapter extends ArrayAdapter<SettingsItem> {
public SettingsListAdapter(final Context context) {
super(context, R.layout.settings_item_view, new ArrayList<>());
mList.removeAllViews();
final LayoutInflater inflater = LayoutInflater.from(this);
boolean first = true;
for (final SettingsItem item : data.getSettingsItems()) {
final View row = inflater.inflate(R.layout.home_settings_row, mList, false);
if (!first) {
final ViewGroup.MarginLayoutParams lp =
(ViewGroup.MarginLayoutParams) row.getLayoutParams();
lp.topMargin = getResources().getDimensionPixelSize(R.dimen.looply_space_xs);
row.setLayoutParams(lp);
}
first = false;
final int icon = item.getType() == SettingsItem.TYPE_GENERAL_SETTINGS
? R.drawable.ic_home_nav_settings
: R.drawable.ic_home_nav_messages;
final CharSequence title = item.getType() == SettingsItem.TYPE_GENERAL_SETTINGS
? getString(R.string.home_settings_general)
: item.getDisplayName();
HomeSettingsRow.bind(row, icon, title, item.getDisplayDetail(),
true, false, false,
v -> openItem(item), null);
mList.addView(row);
}
}
public void setSettingsItems(final List<SettingsItem> newList) {
clear();
addAll(newList);
notifyDataSetChanged();
}
@NonNull
@Override
public View getView(final int position, final View convertView,
@NonNull final ViewGroup parent) {
View itemView;
if (convertView != null) {
itemView = convertView;
} else {
final LayoutInflater inflater = (LayoutInflater) getContext()
.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
itemView = inflater.inflate(
R.layout.settings_item_view, parent, false);
}
final SettingsItem item = getItem(position);
final TextView titleTextView = itemView.findViewById(R.id.title);
final TextView subtitleTextView = itemView.findViewById(R.id.subtitle);
final String summaryText = item.getDisplayDetail();
titleTextView.setText(item.getDisplayName());
if (!TextUtils.isEmpty(summaryText)) {
subtitleTextView.setText(summaryText);
subtitleTextView.setVisibility(View.VISIBLE);
} else {
subtitleTextView.setVisibility(View.GONE);
}
itemView.setOnClickListener(view -> {
switch (item.getType()) {
case SettingsItem.TYPE_GENERAL_SETTINGS:
UIIntents.get().launchApplicationSettingsActivity(getActivity(),
false /* topLevel */);
break;
case SettingsItem.TYPE_PER_SUBSCRIPTION_SETTINGS:
UIIntents.get().launchPerSubscriptionSettingsActivity(getActivity(),
item.getSubId(), item.getActivityTitle());
break;
default:
Assert.fail("unrecognized setting type!");
break;
}
});
return itemView;
}
private void openItem(final SettingsItem item) {
switch (item.getType()) {
case SettingsItem.TYPE_GENERAL_SETTINGS:
UIIntents.get().launchApplicationSettingsActivity(this, false /* topLevel */);
overridePendingTransition(R.anim.home_activity_open, R.anim.home_activity_hold);
break;
case SettingsItem.TYPE_PER_SUBSCRIPTION_SETTINGS:
UIIntents.get().launchPerSubscriptionSettingsActivity(this,
item.getSubId(), item.getActivityTitle());
overridePendingTransition(R.anim.home_activity_open, R.anim.home_activity_hold);
break;
default:
Assert.fail("unrecognized setting type!");
break;
}
}
}
@@ -557,7 +557,7 @@ public class ContactPickerFragment extends Fragment implements ContactPickerData
// etc. will take the spot of the action bar.
actionBar.hide();
UiUtils.setStatusBarColor(getActivity(),
getResources().getColor(R.color.looply_bg, getActivity().getTheme()));
getResources().getColor(R.color.home_bg, getActivity().getTheme()));
}
private GetOrCreateConversationActionMonitor mMonitor;
@@ -16,6 +16,7 @@
*/
package com.android.messaging.ui.conversation;
import android.app.Activity;
import android.content.Context;
import android.content.res.Resources;
import android.graphics.Rect;
@@ -34,18 +35,21 @@ import android.text.format.Formatter;
import android.util.AttributeSet;
import android.view.ContextThemeWrapper;
import android.view.KeyEvent;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
import android.view.WindowManager;
import android.view.accessibility.AccessibilityEvent;
import android.view.inputmethod.EditorInfo;
import android.view.inputmethod.InputMethodManager;
import android.widget.ImageButton;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.TextView;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.ActionBar;
import androidx.core.view.WindowInsetsCompat;
import androidx.core.view.WindowInsetsControllerCompat;
import com.android.messaging.Factory;
import com.android.messaging.R;
@@ -68,8 +72,12 @@ import com.android.messaging.ui.AttachmentPreview;
import com.android.messaging.ui.BugleActionBarActivity;
import com.android.messaging.ui.PlainTextEditText;
import com.android.messaging.ui.conversation.ConversationInputManager.ConversationInputSink;
import com.android.messaging.ui.home.HomeComposeAttachPanel;
import com.android.messaging.ui.home.HomeToast;
import com.android.messaging.ui.home.HomeUiMotion;
import com.android.messaging.util.AccessibilityUtil;
import com.android.messaging.util.ImageUtils;
import com.android.messaging.util.ImeUtil;
import com.android.messaging.util.Assert;
import com.android.messaging.util.AvatarUriUtil;
import com.android.messaging.util.BuglePrefs;
@@ -107,6 +115,7 @@ public class ComposeMessageView extends LinearLayout
void notifyOfAttachmentLoadFailed();
void showAttachmentChooser();
void launchSystemGalleryPicker();
void requestMediaPermission(Runnable onGranted);
boolean shouldShowSubjectEditor();
boolean shouldHideAttachmentsWhenSimSelectorShown();
Uri getSelfSendButtonIconUri();
@@ -137,6 +146,7 @@ public class ComposeMessageView extends LinearLayout
private View mComposeBottomStack;
private View mComposeBar;
private boolean mAttachmentSheetOpen;
private HomeComposeAttachPanel.Host mAttachHost;
private int mComposeBarPaddingBottom;
private ImageButton mVoiceButton;
private View mSendColumn;
@@ -212,13 +222,20 @@ public class ComposeMessageView extends LinearLayout
mComposeEditText.addTextChangedListener(this);
mComposeEditText.setOnFocusChangeListener((v, hasFocus) -> {
if (v == mComposeEditText && hasFocus) {
mHost.onComposeEditTextFocused();
if (mHost != null) {
mHost.onComposeEditTextFocused();
}
showComposeKeyboard();
}
});
mComposeEditText.setOnClickListener(arg0 -> {
if (mHost.shouldHideAttachmentsWhenSimSelectorShown()) {
hideSimSelector();
mComposeEditText.setOnTouchListener((v, event) -> {
if (event.getActionMasked() == MotionEvent.ACTION_DOWN) {
if (mHost != null && mHost.shouldHideAttachmentsWhenSimSelectorShown()) {
hideSimSelector();
}
showComposeKeyboard();
}
return false;
});
// onFinishInflate() is called before self is loaded from db. We set the default text
@@ -301,30 +318,35 @@ public class ComposeMessageView extends LinearLayout
if (mComposeBar != null) {
mComposeBarPaddingBottom = mComposeBar.getPaddingBottom();
}
setupComposeAttachmentSheetTiles();
updateComposeBottomStackBackground();
mAttachHost = createComposeAttachHost();
mAttachMediaButton.setOnClickListener(clickView -> toggleComposeAttachmentSheet());
final ImageButton emojiButton = findViewById(R.id.home_compose_emoji_button);
if (emojiButton != null) {
emojiButton.setOnClickListener(v -> mComposeEditText.requestFocus());
emojiButton.setOnClickListener(v -> showComposeKeyboard());
}
final ImageButton galleryButton = findViewById(R.id.home_compose_gallery_button);
if (galleryButton != null) {
galleryButton.setOnClickListener(v -> {
closeComposeAttachmentSheet(true /* animate */, () -> {
if (mHost != null) {
mHost.launchSystemGalleryPicker();
}
});
if (!mAttachmentSheetOpen) {
toggleComposeAttachmentSheet();
}
});
}
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());
mVoiceButton.setOnClickListener(v -> {
final String messageText = mComposeEditText.getText().toString();
final boolean hasDraft = TextUtils.getTrimmedLength(messageText) > 0
|| (mBinding.isBound() && mBinding.getData().hasAttachments());
if (hasDraft) {
sendMessageInternal(true /* checkMessageSize */);
} else {
HomeToast.show(mOriginalContext, R.string.home_conversation_voice_coming_soon);
}
});
}
mAttachmentPreview = findViewById(R.id.attachment_draft_view);
@@ -576,11 +598,39 @@ public class ComposeMessageView extends LinearLayout
@Override
public void resumeComposeMessage() {
mComposeEditText.requestFocus();
mInputManager.showHideImeKeyboard(true, true);
showComposeKeyboard();
announceAttachmentState();
}
private void showComposeKeyboard() {
if (mComposeEditText == null) {
return;
}
mComposeEditText.setEnabled(true);
mComposeEditText.setFocusable(true);
mComposeEditText.setFocusableInTouchMode(true);
mComposeEditText.setShowSoftInputOnFocus(true);
mComposeEditText.requestFocus();
final Activity activity = resolveActivity();
if (activity != null && activity.getWindow() != null) {
activity.getWindow().setSoftInputMode(
WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE
| WindowManager.LayoutParams.SOFT_INPUT_STATE_VISIBLE);
new WindowInsetsControllerCompat(activity.getWindow(), mComposeEditText)
.show(WindowInsetsCompat.Type.ime());
final InputMethodManager imm = activity.getSystemService(InputMethodManager.class);
if (imm != null) {
imm.restartInput(mComposeEditText);
imm.showSoftInput(mComposeEditText, InputMethodManager.SHOW_FORCED);
}
}
final Context imeContext = activity != null ? activity : mOriginalContext;
ImeUtil.get().showImeKeyboard(imeContext, mComposeEditText);
if (mInputManager != null) {
mInputManager.showHideImeKeyboard(true, false);
}
}
public void clearAttachments() {
mBinding.getData().clearAttachments(mHost.getAttachmentsClearedFlags());
mHost.onAttachmentsCleared();
@@ -757,6 +807,15 @@ public class ComposeMessageView extends LinearLayout
}
} else {
mSelfSendIcon.setImageResourceUri(null);
if (hasWorkingDraft && isDataLoadedForMessageSend()) {
UiUtils.revealOrHideViewWithAnimation(mSendButton, VISIBLE, null);
UiUtils.revealOrHideViewWithAnimation(mSelfSendIcon, GONE, null);
mMmsIndicator.setVisibility(draftMessageData.getIsMms() ? VISIBLE : INVISIBLE);
sendWidgetMode = SEND_WIDGET_MODE_SEND_BUTTON;
} else {
UiUtils.revealOrHideViewWithAnimation(mSendButton, GONE, null);
mMmsIndicator.setVisibility(INVISIBLE);
}
}
if (mSendWidgetMode != sendWidgetMode || sendWidgetMode == SEND_WIDGET_MODE_SIM_SELECTOR) {
@@ -988,9 +1047,8 @@ public class ComposeMessageView extends LinearLayout
}
public boolean onBackPressed() {
if (mAttachmentSheetOpen) {
closeComposeAttachmentSheet(true /* animate */, null);
return true;
if (HomeComposeAttachPanel.isShowing(resolveActivity())) {
return HomeComposeAttachPanel.onBackPressed(resolveActivity());
}
return mInputManager.onBackPressed();
}
@@ -1043,135 +1101,166 @@ public class ComposeMessageView extends LinearLayout
}
}
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 HomeComposeAttachPanel.Host createComposeAttachHost() {
return new HomeComposeAttachPanel.Host() {
@Override
public void onMediaPicked(@NonNull final List<Uri> uris,
@Nullable final String caption) {
if (!TextUtils.isEmpty(caption)) {
mComposeEditText.setText(caption);
mComposeEditText.setSelection(caption.length());
if (mBinding.isBound()) {
mBinding.getData().setMessageText(caption);
}
}
attachPickedUris(uris);
}
@Override
public void onTakePhoto() {
openMediaFromAttachmentSheet(ConversationInputManager.MEDIA_PICKER_TARGET_PHOTO);
}
@Override
public void onRecordVideo() {
openMediaFromAttachmentSheet(ConversationInputManager.MEDIA_PICKER_TARGET_VIDEO);
}
@Override
public void onOpenContact() {
openMediaFromAttachmentSheet(ConversationInputManager.MEDIA_PICKER_TARGET_CONTACT);
}
@Override
public void onRequestMediaPermission() {
if (mHost != null) {
mHost.requestMediaPermission(() ->
HomeComposeAttachPanel.reloadIfShowing(resolveActivity()));
}
}
@Override
public void onComingSoon() {
HomeToast.show(mOriginalContext, R.string.home_popover_coming_soon);
}
@Override
public void onDismiss() {
mAttachmentSheetOpen = false;
updateAttachButtonContentDescription();
updateComposeBottomStackBackground();
}
@Override
public void onAttachSheetExtentChanged(final int overlapPx) {
applyAttachSheetChatPush(overlapPx);
}
@Override
public void onRequestComposeKeyboard() {
showComposeKeyboard();
}
};
}
private void bindComposeAttachmentTile(final int tileRootId, final int iconRes,
final int labelRes, final int mediaPickerTarget) {
final View tile = findViewById(tileRootId);
if (tile == null) {
private void attachPickedUris(final List<Uri> uris) {
if (uris == null || uris.isEmpty()) {
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();
final ExecutorService executor = Executors.newSingleThreadExecutor();
final Handler handler = new Handler(Looper.getMainLooper());
executor.execute(() -> {
final List<PendingAttachmentData> pending = new ArrayList<>();
for (final Uri uri : uris) {
final String contentType = ImageUtils.getContentType(
Factory.get().getApplicationContext().getContentResolver(), uri);
if (contentType != null) {
pending.add(PendingAttachmentData.createPendingAttachmentData(contentType, uri));
}
}
handler.post(() -> {
for (final PendingAttachmentData item : pending) {
onPendingAttachmentAdded(item);
}
executor.shutdown();
});
});
}
private void applyAttachSheetChatPush(final int overlapPx) {
final Activity activity = resolveActivity();
if (activity == null) {
return;
}
final View chatArea = activity.findViewById(R.id.conversation_chat_area);
if (chatArea == null) {
return;
}
final int inset = Math.max(0, overlapPx);
if (chatArea.getPaddingBottom() != inset) {
chatArea.setPaddingRelative(
chatArea.getPaddingStart(),
chatArea.getPaddingTop(),
chatArea.getPaddingEnd(),
inset);
}
}
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);
return;
}
final Activity activity = resolveActivity();
if (activity == null || mAttachHost == null) {
return;
}
if (mInputManager != null) {
mInputManager.showHideImeKeyboard(false, true);
mInputManager.showHideMediaPicker(false, false);
}
mAttachmentSheetOpen = true;
updateAttachButtonContentDescription();
updateComposeBottomStackBackground();
HomeComposeAttachPanel.show(activity, mAttachHost);
}
private void closeComposeAttachmentSheet(final boolean animate, final Runnable endAction) {
if (!mAttachmentSheetOpen || mAttachmentSheetContainer == null) {
if (endAction != null) {
endAction.run();
}
return;
final boolean dismissed = HomeComposeAttachPanel.dismissIfShowing(resolveActivity());
if (!dismissed) {
applyAttachSheetChatPush(0);
}
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();
}
if (endAction != null) {
endAction.run();
}
if (!dismissed && endAction == null) {
return;
}
}
private Activity resolveActivity() {
if (mOriginalContext instanceof Activity) {
return (Activity) mOriginalContext;
}
final Context context = getContext();
if (context instanceof Activity) {
return (Activity) context;
}
return null;
}
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);
}
mComposeBottomStack.setClipToOutline(false);
mComposeBottomStack.setBackgroundColor(android.graphics.Color.TRANSPARENT);
if (mComposeBar != null) {
mComposeBar.setVisibility(View.VISIBLE);
mComposeBar.setBackgroundColor(android.graphics.Color.TRANSPARENT);
}
}
@@ -1181,7 +1270,9 @@ public class ComposeMessageView extends LinearLayout
mHost.launchSystemGalleryPicker();
return;
}
if (mediaPickerTarget == ConversationInputManager.MEDIA_PICKER_TARGET_CONTACT) {
if (mediaPickerTarget == ConversationInputManager.MEDIA_PICKER_TARGET_CONTACT
|| mediaPickerTarget == ConversationInputManager.MEDIA_PICKER_TARGET_PHOTO
|| mediaPickerTarget == ConversationInputManager.MEDIA_PICKER_TARGET_VIDEO) {
if (mInputManager != null) {
mInputManager.showHideMediaPicker(true /* show */, false /* animate */,
mediaPickerTarget);
@@ -150,6 +150,17 @@ public class ConversationActivity extends BugleActionBarActivity
}
}
@Override
@Deprecated
protected void onActivityResult(final int requestCode, final int resultCode,
final Intent data) {
super.onActivityResult(requestCode, resultCode, data);
final ConversationFragment conversationFragment = getConversationFragment();
if (conversationFragment != null) {
conversationFragment.onDefaultSmsAppResult(requestCode, resultCode);
}
}
public void suppressNextResumeTransition() {
mSuppressNextResumeTransition = true;
}
@@ -0,0 +1,38 @@
/*
* 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.conversation;
import android.content.Context;
import android.util.AttributeSet;
import android.widget.FrameLayout;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
/**
* Conversation chrome container. Compose keeps a higher elevation than the
* decorative fades so taps reach the EditText through the normal dispatch path.
*/
public class ConversationChatAreaLayout extends FrameLayout {
public ConversationChatAreaLayout(@NonNull final Context context) {
super(context);
}
public ConversationChatAreaLayout(@NonNull final Context context,
@Nullable final AttributeSet attrs) {
super(context, attrs);
}
}
@@ -28,9 +28,16 @@ import android.content.Intent;
import android.content.IntentFilter;
import android.content.res.Configuration;
import android.database.Cursor;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Point;
import android.graphics.PorterDuff;
import android.graphics.Rect;
import android.graphics.RenderEffect;
import android.graphics.Shader;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.ColorDrawable;
import android.graphics.drawable.Drawable;
import android.net.Uri;
import android.os.Bundle;
import android.os.Environment;
@@ -43,17 +50,22 @@ import android.telephony.PhoneNumberUtils;
import android.text.TextUtils;
import android.view.ActionMode;
import android.view.Display;
import android.view.Gravity;
import android.view.HapticFeedbackConstants;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.PixelCopy;
import android.view.View;
import android.widget.ImageButton;
import android.widget.PopupMenu;
import android.widget.ImageView;
import android.widget.PopupWindow;
import android.widget.Toast;
import android.view.ViewConfiguration;
import android.view.ViewGroup;
import android.view.ViewParent;
import android.widget.FrameLayout;
import android.widget.TextView;
import androidx.activity.result.ActivityResultLauncher;
@@ -99,8 +111,11 @@ import com.android.messaging.ui.ConversationDrawables;
import com.android.messaging.ui.SnackBar;
import com.android.messaging.ui.UIIntents;
import com.android.messaging.ui.animation.PopupTransitionAnimation;
import com.android.messaging.ui.home.HomeComposeAttachPanel;
import com.android.messaging.ui.home.HomeEmptyState;
import com.android.messaging.ui.home.HomeGallerySheet;
import com.android.messaging.ui.home.HomeUiMotion;
import com.android.messaging.ui.home.StarredMessagesStore;
import com.android.messaging.ui.home.UnknownSenderPromptStore;
import com.android.messaging.ui.attachmentchooser.AttachmentChooserActivity;
import com.android.messaging.ui.contact.AddContactsConfirmationDialog;
@@ -112,7 +127,6 @@ import com.android.messaging.ui.mediapicker.MediaPicker;
import com.android.messaging.util.AccessibilityUtil;
import com.android.messaging.util.Assert;
import com.android.messaging.util.AvatarUriUtil;
import com.android.messaging.util.Dates;
import com.android.messaging.util.ChangeDefaultSmsAppHelper;
import com.android.messaging.util.ContactUtil;
import com.android.messaging.util.ImeUtil;
@@ -160,14 +174,14 @@ public class ConversationFragment extends Fragment implements ConversationDataLi
private ConversationMessageAdapter mAdapter;
private ConversationFastScroller mFastScroller;
private View mConversationComposeDivider;
private View mEmptyContainer;
private TextView mEmptyTime;
private TextView mEmptyStatus;
private HomeEmptyState mEmptyContainer;
private View mConversationHeader;
private TextView mConversationHeaderTitle;
private TextView mConversationHeaderSubtitle;
private ContactIconView mConversationHeaderAvatar;
private ImageButton mConversationHeaderOverflow;
private PopupWindow mConversationOverflowPopover;
private ImageButton mConversationHeaderCall;
private boolean mOptionsMenuVisible = true;
private View mUnknownSenderBanner;
private TextView mUnknownSenderTitle;
@@ -175,6 +189,7 @@ public class ConversationFragment extends Fragment implements ConversationDataLi
private View mMessageSelectionHeader;
private TextView mMessageSelectionTitle;
private View mComposeContainer;
private View mComposeStage;
private View mMessageActionBar;
private View mMessageActionCopy;
private View mMessageActionForward;
@@ -184,6 +199,9 @@ public class ConversationFragment extends Fragment implements ConversationDataLi
private View mMessageActionSend;
private View mMessageActionDownload;
private View mMessageActionDelete;
private PopupWindow mMessageActionPopup;
private ViewGroup mMessageBlurOverlay;
private boolean mClearSelectionOnPopupDismiss = true;
private ChangeDefaultSmsAppHelper mChangeDefaultSmsAppHelper;
private String mConversationId;
@@ -255,7 +273,9 @@ public class ConversationFragment extends Fragment implements ConversationDataLi
}
final int totalItemCount = mRecyclerView.getAdapter().getItemCount();
final boolean isAtBottom = (lastVisibleItem + 1 == totalItemCount);
return isAtBottom && lastView.getBottom() <= mRecyclerView.getHeight();
return isAtBottom
&& lastView.getBottom() <= mRecyclerView.getHeight()
- mRecyclerView.getPaddingBottom();
}
private void scrollToBottom(final boolean smoothScroll) {
@@ -271,7 +291,6 @@ public class ConversationFragment extends Fragment implements ConversationDataLi
// hide the media picker & co.
private int mCumulativeScrollDelta;
private boolean mScrollToDismissHandled;
private boolean mWasScrolledToBottom = true;
private int mScrollState = RecyclerView.SCROLL_STATE_IDLE;
@Override
@@ -288,6 +307,9 @@ public class ConversationFragment extends Fragment implements ConversationDataLi
@Override
public void onScrolled(@NonNull final RecyclerView view, final int dx, final int dy) {
if (dy != 0) {
dismissMessageActionPopup();
}
if (mScrollState == RecyclerView.SCROLL_STATE_DRAGGING &&
!mScrollToDismissHandled) {
mCumulativeScrollDelta += dy;
@@ -297,10 +319,6 @@ public class ConversationFragment extends Fragment implements ConversationDataLi
mScrollToDismissHandled = true;
}
}
if (mWasScrolledToBottom != isScrolledToBottom()) {
mConversationComposeDivider.animate().alpha(isScrolledToBottom() ? 0 : 1);
mWasScrolledToBottom = isScrolledToBottom();
}
}
};
@@ -424,11 +442,10 @@ public class ConversationFragment extends Fragment implements ConversationDataLi
final View view = inflater.inflate(R.layout.conversation_fragment, container, false);
mRecyclerView = view.findViewById(android.R.id.list);
mEmptyContainer = view.findViewById(R.id.home_conversation_empty);
mEmptyTime = view.findViewById(R.id.home_conversation_empty_time);
mEmptyStatus = view.findViewById(R.id.home_conversation_empty_status);
setupConversationHeader(view);
setupUnknownSenderBanner(view);
setupMessageSelectionChrome(view);
bindConversationEdgeFade(view);
final LinearLayoutManager manager = new LinearLayoutManager(getActivity());
manager.setStackFromEnd(true);
manager.setReverseLayout(false);
@@ -517,7 +534,6 @@ public class ConversationFragment extends Fragment implements ConversationDataLi
Parcelable.class);
}
mConversationComposeDivider = view.findViewById(R.id.conversation_compose_divider);
mScrollToDismissThreshold = ViewConfiguration.get(getActivity()).getScaledTouchSlop();
mRecyclerView.addOnScrollListener(mListScrollListener);
mFastScroller = ConversationFastScroller.addTo(mRecyclerView,
@@ -591,24 +607,311 @@ public class ConversationFragment extends Fragment implements ConversationDataLi
private void selectMessage(final ConversationMessageView messageView,
final MessagePartData attachment) {
selectMessage(messageView, attachment, true /* showPopup */);
}
private void selectMessage(final ConversationMessageView messageView,
final MessagePartData attachment, final boolean showPopup) {
mSelectedMessage = messageView;
if (mSelectedMessage == null) {
mAdapter.setSelectedMessage(null);
mSelectedAttachment = null;
dismissMessageActionPopup();
updateMessageSelectionChrome(false);
updateUnknownSenderBanner();
return;
}
mSelectedAttachment = attachment;
mAdapter.setSelectedMessage(messageView.getData().getMessageId());
updateMessageSelectionChrome(true);
updateUnknownSenderBanner();
if (showPopup) {
updateMessageSelectionChrome(false);
showMessageActionPopup(messageView);
} else {
dismissMessageActionPopup();
updateMessageSelectionChrome(true);
}
}
private void dismissMessageSelection() {
selectMessage(null);
}
private void showMessageActionPopup(final ConversationMessageView messageView) {
if (getActivity() == null || messageView == null) {
return;
}
dismissMessageActionPopup();
final ConversationMessageData data = messageView.getData();
final View content = LayoutInflater.from(requireContext())
.inflate(R.layout.home_message_action_popup, null, false);
final View forward = content.findViewById(R.id.home_message_menu_forward);
final View copy = content.findViewById(R.id.home_message_menu_copy);
final View star = content.findViewById(R.id.home_message_menu_star);
final TextView starLabel = content.findViewById(R.id.home_message_menu_star_label);
final View delete = content.findViewById(R.id.home_message_menu_delete);
final View selectMore = content.findViewById(R.id.home_message_menu_select_more);
final View details = content.findViewById(R.id.home_message_menu_details);
forward.setVisibility(data.getCanForwardMessage() ? View.VISIBLE : View.GONE);
copy.setVisibility(data.getCanCopyMessageToClipboard() ? View.VISIBLE : View.GONE);
final boolean starred = StarredMessagesStore.isStarred(requireContext(), data.getMessageId());
if (starLabel != null) {
starLabel.setText(starred
? R.string.home_message_menu_unstar
: R.string.home_message_menu_star);
}
forward.setOnClickListener(v -> {
dismissMessageActionPopup();
performSelectedMessageAction(R.id.home_message_action_forward);
});
copy.setOnClickListener(v -> {
dismissMessageActionPopup();
performSelectedMessageAction(R.id.home_message_action_copy);
});
star.setOnClickListener(v -> {
StarredMessagesStore.toggle(requireContext(), data.getMessageId());
dismissMessageActionPopup();
dismissMessageSelection();
});
delete.setOnClickListener(v -> {
dismissMessageActionPopup();
performSelectedMessageAction(R.id.home_message_action_delete);
});
selectMore.setOnClickListener(v -> {
mClearSelectionOnPopupDismiss = false;
dismissMessageActionPopup();
selectMessage(messageView, mSelectedAttachment, false /* showPopup */);
});
details.setOnClickListener(v -> {
dismissMessageActionPopup();
performSelectedMessageAction(R.id.home_message_action_details);
});
content.setLayoutParams(new ViewGroup.LayoutParams(
ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT));
content.measure(
View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED),
View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED));
final int width = Math.round(content.getMeasuredWidth() * 1.1f);
content.measure(
View.MeasureSpec.makeMeasureSpec(width, View.MeasureSpec.EXACTLY),
View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED));
final int height = content.getMeasuredHeight();
final PopupWindow popup = new PopupWindow(content, width, height, true);
popup.setOutsideTouchable(true);
popup.setFocusable(true);
popup.setElevation(16f);
popup.setBackgroundDrawable(new ColorDrawable(android.graphics.Color.TRANSPARENT));
popup.setOnDismissListener(() -> {
if (mMessageActionPopup == popup) {
mMessageActionPopup = null;
}
if (mClearSelectionOnPopupDismiss && mSelectedMessage == messageView
&& (mMessageSelectionHeader == null
|| mMessageSelectionHeader.getVisibility() != View.VISIBLE)) {
dismissMessageSelection();
}
mClearSelectionOnPopupDismiss = true;
});
final View bubble = messageView.findViewById(R.id.message_content);
final View anchor = bubble != null ? bubble : messageView;
final int[] loc = new int[2];
anchor.getLocationOnScreen(loc);
final int screenW = getResources().getDisplayMetrics().widthPixels;
final int screenH = getResources().getDisplayMetrics().heightPixels;
final int margin = getResources().getDimensionPixelSize(R.dimen.looply_space_md);
int headerBottom = margin;
if (mConversationHeader != null && mConversationHeader.getVisibility() == View.VISIBLE) {
final int[] headerLoc = new int[2];
mConversationHeader.getLocationOnScreen(headerLoc);
headerBottom = headerLoc[1] + mConversationHeader.getHeight() + margin;
}
int dockTop = screenH - margin;
if (mComposeStage != null && mComposeStage.getVisibility() == View.VISIBLE) {
final int[] dockLoc = new int[2];
mComposeStage.getLocationOnScreen(dockLoc);
if (mComposeStage.getHeight() > 0) {
dockTop = dockLoc[1] - margin;
}
}
int x = data.getIsIncoming()
? loc[0]
: loc[0] + anchor.getWidth() - width;
x = Math.max(margin, Math.min(x, screenW - width - margin));
final int gap = margin;
final int bubbleTop = loc[1];
final int bubbleBottom = loc[1] + anchor.getHeight();
final int spaceBelow = dockTop - bubbleBottom;
final int spaceAbove = bubbleTop - headerBottom;
boolean placeBelow = spaceBelow >= height + gap;
if (!placeBelow && spaceAbove < height + gap && mRecyclerView != null) {
if (spaceBelow >= spaceAbove) {
mRecyclerView.scrollBy(0, Math.max(0, height + gap - spaceBelow));
placeBelow = true;
} else {
mRecyclerView.scrollBy(0, -Math.max(0, height + gap - spaceAbove));
}
anchor.getLocationOnScreen(loc);
} else if (!placeBelow && spaceAbove < height + gap) {
placeBelow = spaceBelow >= spaceAbove;
}
int y = placeBelow
? loc[1] + anchor.getHeight() + gap
: loc[1] - height - gap;
final int popupX = x;
final int popupY = y;
showMessageBlurOverlay(anchor, () -> {
if (mSelectedMessage != messageView || getActivity() == null) {
return;
}
mMessageActionPopup = popup;
popup.showAtLocation(messageView, Gravity.TOP | Gravity.START, popupX, popupY);
HomeUiMotion.popoverEnter(content, !data.getIsIncoming());
});
}
private void showMessageBlurOverlay(final View keepView, final Runnable onReady) {
final Activity activity = getActivity();
if (activity == null || keepView == null) {
if (onReady != null) {
onReady.run();
}
return;
}
dismissMessageBlurOverlay();
final ViewGroup decor = (ViewGroup) activity.getWindow().getDecorView();
final Bitmap bubble = snapshotView(keepView);
final int[] keepLoc = new int[2];
keepView.getLocationInWindow(keepLoc);
final int copyW = Math.max(1, decor.getWidth());
final int copyH = Math.max(1, decor.getHeight());
final Bitmap screen = Bitmap.createBitmap(copyW, copyH, Bitmap.Config.ARGB_8888);
try {
PixelCopy.request(activity.getWindow(), screen, result -> {
if (getActivity() == null) {
screen.recycle();
if (bubble != null) {
bubble.recycle();
}
return;
}
attachMessageBlurOverlay(activity, decor, bubble, keepLoc,
result == PixelCopy.SUCCESS ? screen : null);
if (result != PixelCopy.SUCCESS) {
screen.recycle();
}
if (onReady != null) {
onReady.run();
}
}, new Handler(Looper.getMainLooper()));
} catch (final IllegalArgumentException ex) {
screen.recycle();
attachMessageBlurOverlay(activity, decor, bubble, keepLoc, null);
if (onReady != null) {
onReady.run();
}
}
}
private void attachMessageBlurOverlay(final Activity activity, final ViewGroup decor,
final Bitmap bubble, final int[] keepLoc, final Bitmap screen) {
dismissMessageBlurOverlay();
final FrameLayout overlay = new FrameLayout(activity);
overlay.setLayoutParams(new FrameLayout.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT));
overlay.setClickable(true);
overlay.setOnClickListener(v -> dismissMessageActionPopup());
final ImageView blur = new ImageView(activity);
blur.setLayoutParams(new FrameLayout.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT));
blur.setScaleType(ImageView.ScaleType.FIT_XY);
if (screen != null) {
blur.setImageBitmap(screen);
blur.setRenderEffect(RenderEffect.createBlurEffect(
28f, 28f, Shader.TileMode.CLAMP));
blur.setColorFilter(0x40000000, PorterDuff.Mode.SRC_ATOP);
} else {
blur.setBackgroundColor(0x73000000);
}
final View dim = new View(activity);
dim.setLayoutParams(new FrameLayout.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT));
dim.setBackgroundColor(0x33000000);
dim.setClickable(false);
overlay.addView(blur);
overlay.addView(dim);
if (bubble != null) {
final ImageView keep = new ImageView(activity);
keep.setClickable(true);
keep.setElevation(8f);
keep.setImageBitmap(bubble);
final FrameLayout.LayoutParams keepLp = new FrameLayout.LayoutParams(
bubble.getWidth(), bubble.getHeight());
keepLp.leftMargin = keepLoc[0];
keepLp.topMargin = keepLoc[1];
keep.setLayoutParams(keepLp);
overlay.addView(keep);
}
decor.addView(overlay);
mMessageBlurOverlay = overlay;
}
private void dismissMessageActionPopup() {
dismissMessageBlurOverlay();
if (mMessageActionPopup != null) {
mMessageActionPopup.dismiss();
mMessageActionPopup = null;
}
}
private void dismissMessageBlurOverlay() {
if (mMessageBlurOverlay == null) {
return;
}
final ViewGroup overlay = mMessageBlurOverlay;
mMessageBlurOverlay = null;
recycleOverlayBitmaps(overlay);
final ViewParent parent = overlay.getParent();
if (parent instanceof ViewGroup) {
((ViewGroup) parent).removeView(overlay);
}
}
private static void recycleOverlayBitmaps(final ViewGroup overlay) {
for (int i = 0; i < overlay.getChildCount(); i++) {
final View child = overlay.getChildAt(i);
if (!(child instanceof ImageView)) {
continue;
}
final Drawable drawable = ((ImageView) child).getDrawable();
((ImageView) child).setImageDrawable(null);
((ImageView) child).setRenderEffect(null);
if (drawable instanceof BitmapDrawable) {
final Bitmap bitmap = ((BitmapDrawable) drawable).getBitmap();
if (bitmap != null && !bitmap.isRecycled()) {
bitmap.recycle();
}
}
}
}
private static Bitmap snapshotView(final View view) {
if (view.getWidth() <= 0 || view.getHeight() <= 0) {
return null;
}
final Bitmap bitmap = Bitmap.createBitmap(view.getWidth(), view.getHeight(),
Bitmap.Config.ARGB_8888);
final Canvas canvas = new Canvas(bitmap);
view.draw(canvas);
return bitmap;
}
@Override
public void onSaveInstanceState(@NonNull final Bundle outState) {
super.onSaveInstanceState(outState);
@@ -643,8 +946,31 @@ public class ConversationFragment extends Fragment implements ConversationDataLi
setConversationFocus();
// On resume, invalidate all message views to show the updated timestamp.
mAdapter.notifyDataSetChanged();
if (mSelectedMessage == null) {
if (mConversationHeader != null) {
mConversationHeader.setVisibility(View.VISIBLE);
}
if (mComposeContainer != null) {
mComposeContainer.setVisibility(View.VISIBLE);
}
}
if (mHost != null) {
mHost.invalidateActionBar();
}
// Camera/gallery return can resume while RecyclerView is still laying out.
if (mRecyclerView != null) {
mRecyclerView.post(() -> {
if (!isAdded() || mAdapter == null) {
return;
}
if (mRecyclerView.isComputingLayout()) {
mRecyclerView.post(mAdapter::notifyDataSetChanged);
return;
}
mAdapter.notifyDataSetChanged();
});
}
LocalBroadcastManager.getInstance(getActivity()).registerReceiver(
mConversationSelfIdChangeReceiver,
@@ -684,10 +1010,8 @@ public class ConversationFragment extends Fragment implements ConversationDataLi
menu.findItem(R.id.action_archive).setVisible(!isArchived);
menu.findItem(R.id.action_unarchive).setVisible(isArchived);
// Conditionally enable the phone call button.
final boolean supportCallAction = (PhoneUtils.getDefault().isVoiceCapable() &&
data.getParticipantPhoneNumber() != null);
menu.findItem(R.id.action_call).setVisible(supportCallAction);
// Call lives on the header pill; keep it out of the overflow.
menu.findItem(R.id.action_call).setVisible(false);
}
@Override
@@ -759,6 +1083,7 @@ public class ConversationFragment extends Fragment implements ConversationDataLi
// Ensure that the action bar is updated with the current data.
invalidateOptionsMenu();
final Cursor oldCursor = mAdapter.swapCursor(cursor);
updateHomeConversationHeader();
if (cursor != null && oldCursor == null) {
if (mListState != null) {
@@ -884,6 +1209,8 @@ public class ConversationFragment extends Fragment implements ConversationDataLi
@Override
public void onPause() {
super.onPause();
dismissMessageActionPopup();
dismissConversationOverflowPopover();
if (mComposeMessageView != null && !mSuppressWriteDraft) {
mComposeMessageView.writeDraftMessage();
}
@@ -891,8 +1218,12 @@ public class ConversationFragment extends Fragment implements ConversationDataLi
mBinding.getData().unsetFocus();
mListState = mRecyclerView.getLayoutManager().onSaveInstanceState();
LocalBroadcastManager.getInstance(getActivity())
.unregisterReceiver(mConversationSelfIdChangeReceiver);
try {
LocalBroadcastManager.getInstance(getActivity())
.unregisterReceiver(mConversationSelfIdChangeReceiver);
} catch (final IllegalArgumentException ignored) {
// onResume may have aborted before the receiver was registered (camera return).
}
}
@Override
@@ -915,6 +1246,14 @@ public class ConversationFragment extends Fragment implements ConversationDataLi
MediaPicker.FRAGMENT_TAG);
}
/** Forwards default-SMS role result from {@link ConversationActivity#onActivityResult}. */
public void onDefaultSmsAppResult(final int requestCode, final int resultCode) {
if (mChangeDefaultSmsAppHelper == null) {
mChangeDefaultSmsAppHelper = new ChangeDefaultSmsAppHelper();
}
mChangeDefaultSmsAppHelper.handleChangeDefaultSmsResult(requestCode, resultCode, null);
}
@Override
public void sendMessage(final MessageData message) {
if (isReadyForAction()) {
@@ -1020,6 +1359,7 @@ public class ConversationFragment extends Fragment implements ConversationDataLi
public void retrySend(final String messageId) {
if (isReadyForAction()) {
if (ensureKnownRecipients()) {
showSendingStatus(messageId);
mBinding.getData().resendMessage(mBinding, messageId);
}
} else {
@@ -1027,6 +1367,22 @@ public class ConversationFragment extends Fragment implements ConversationDataLi
}
}
private void showSendingStatus(final String messageId) {
if (mRecyclerView == null || TextUtils.isEmpty(messageId)) {
return;
}
for (int i = 0; i < mRecyclerView.getChildCount(); i++) {
final View child = mRecyclerView.getChildAt(i);
if (child instanceof ConversationMessageView) {
final ConversationMessageView messageView = (ConversationMessageView) child;
if (TextUtils.equals(messageView.getData().getMessageId(), messageId)) {
messageView.showSendingStatus();
return;
}
}
}
}
void deleteMessage(final String messageId) {
if (isReadyForDeleteAction()) {
final AlertDialog.Builder builder = new AlertDialog.Builder(getActivity())
@@ -1106,6 +1462,9 @@ public class ConversationFragment extends Fragment implements ConversationDataLi
}
public boolean onBackPressed() {
if (HomeComposeAttachPanel.onBackPressed(getActivity())) {
return true;
}
if (HomeGallerySheet.dismissIfShowing(getActivity())) {
return true;
}
@@ -1394,6 +1753,15 @@ public class ConversationFragment extends Fragment implements ConversationDataLi
}
}
@Override
public void requestMediaPermission(final Runnable onGranted) {
if (mGalleryPicker != null) {
mGalleryPicker.requestPermission(onGranted);
} else if (onGranted != null) {
onGranted.run();
}
}
@Override
public void showAttachmentChooser() {
showAttachmentChooser(mConversationId, getActivity());
@@ -1407,10 +1775,11 @@ public class ConversationFragment extends Fragment implements ConversationDataLi
}
private void updateActionAndStatusBarColor(final ActionBar actionBar) {
final int barColor = getResources().getColor(R.color.home_conversation_chrome_bg,
final int barColor = getResources().getColor(R.color.home_conversation_canvas,
getActivity().getTheme());
actionBar.setBackgroundDrawable(new ColorDrawable(barColor));
UiUtils.setStatusBarColor(getActivity(), barColor);
getActivity().getWindow().setNavigationBarColor(barColor);
}
private void updateHomeEmptyState() {
@@ -1422,14 +1791,11 @@ public class ConversationFragment extends Fragment implements ConversationDataLi
if (!empty) {
return;
}
if (mEmptyTime != null) {
mEmptyTime.setText(Dates.getMessageTimeString(System.currentTimeMillis()));
}
if (mEmptyStatus != null) {
final String name = getConversationName();
mEmptyStatus.setText(getString(R.string.home_conversation_empty_status,
TextUtils.isEmpty(name) ? getString(R.string.app_name) : name));
}
final String name = getConversationName();
mEmptyContainer.setContent(R.drawable.ic_home_menu_messages,
getString(R.string.home_empty_conversation_title),
getString(R.string.home_empty_conversation_body,
TextUtils.isEmpty(name) ? getString(R.string.app_name) : name));
}
private void setupUnknownSenderBanner(final View root) {
@@ -1552,6 +1918,7 @@ public class ConversationFragment extends Fragment implements ConversationDataLi
});
}
mConversationHeaderTitle = root.findViewById(R.id.conversation_header_title);
mConversationHeaderSubtitle = root.findViewById(R.id.conversation_header_subtitle);
mConversationHeaderAvatar = root.findViewById(R.id.conversation_header_avatar);
if (mConversationHeaderAvatar != null) {
mConversationHeaderAvatar.setOnClickListener(v -> openConversationProfileDetails());
@@ -1559,24 +1926,136 @@ public class ConversationFragment extends Fragment implements ConversationDataLi
if (mConversationHeaderTitle != null) {
mConversationHeaderTitle.setOnClickListener(v -> openConversationProfileDetails());
}
if (mConversationHeaderSubtitle != null) {
mConversationHeaderSubtitle.setOnClickListener(v -> openConversationProfileDetails());
}
final View identity = root.findViewById(R.id.conversation_header_identity);
if (identity != null) {
identity.setOnClickListener(v -> openConversationProfileDetails());
}
mConversationHeaderOverflow = root.findViewById(R.id.conversation_header_overflow);
if (mConversationHeaderOverflow != null) {
mConversationHeaderOverflow.setOnClickListener(this::showConversationOverflowMenu);
}
mConversationHeaderCall = root.findViewById(R.id.conversation_header_call);
if (mConversationHeaderCall != null) {
mConversationHeaderCall.setOnClickListener(this::placeConversationCall);
}
}
private void bindConversationEdgeFade(final View root) {
if (mRecyclerView == null) {
return;
}
mRecyclerView.setClipToPadding(false);
final View fadeTop = root.findViewById(R.id.conversation_fade_top);
final View fadeBottom = root.findViewById(R.id.conversation_fade_bottom);
final int fadeHeight = getResources().getDimensionPixelSize(
R.dimen.home_conversation_edge_fade);
final View.OnLayoutChangeListener listener = (v, l, t, r, b, ol, ot, or, ob) -> {
if ((b - t) != (ob - ot)) {
applyConversationEdgeFade(fadeTop, fadeBottom, fadeHeight);
}
};
if (mConversationHeader != null) {
mConversationHeader.addOnLayoutChangeListener(listener);
}
if (mMessageSelectionHeader != null) {
mMessageSelectionHeader.addOnLayoutChangeListener(listener);
}
if (mComposeStage != null) {
mComposeStage.addOnLayoutChangeListener(listener);
}
mRecyclerView.addOnLayoutChangeListener(listener);
root.post(() -> applyConversationEdgeFade(fadeTop, fadeBottom, fadeHeight));
}
private void applyConversationEdgeFade(final View fadeTop, final View fadeBottom,
final int fadeHeight) {
if (mRecyclerView == null) {
return;
}
View header = null;
if (mMessageSelectionHeader != null
&& mMessageSelectionHeader.getVisibility() == View.VISIBLE) {
header = mMessageSelectionHeader;
} else if (mConversationHeader != null) {
header = mConversationHeader;
}
final int headerH = header == null ? 0
: Math.max(header.getHeight(), header.getMeasuredHeight());
final int composeH = mComposeStage == null ? fadeHeight
: Math.max(mComposeStage.getHeight(), mComposeStage.getMeasuredHeight());
final int topPad = Math.max(headerH, fadeHeight);
final int bottomPad = Math.max(composeH, fadeHeight);
// Fade from the top of the compose dock to the bottom of the chat area.
final int bottomFadeH = Math.max(composeH, fadeHeight);
if (mRecyclerView.getPaddingTop() != topPad
|| mRecyclerView.getPaddingBottom() != bottomPad) {
mRecyclerView.setPaddingRelative(
mRecyclerView.getPaddingStart(),
topPad,
mRecyclerView.getPaddingEnd(),
bottomPad);
}
if (fadeTop != null && headerH > 0) {
final int topFadeH = headerH + (fadeHeight / 2);
final ViewGroup.LayoutParams lp = fadeTop.getLayoutParams();
if (lp != null && lp.height != topFadeH) {
lp.height = topFadeH;
fadeTop.setLayoutParams(lp);
}
}
if (fadeBottom != null) {
final ViewGroup.LayoutParams lp = fadeBottom.getLayoutParams();
if (lp instanceof FrameLayout.LayoutParams) {
final FrameLayout.LayoutParams flp = (FrameLayout.LayoutParams) lp;
if (flp.height != bottomFadeH || flp.bottomMargin != 0
|| flp.gravity != Gravity.BOTTOM) {
flp.height = bottomFadeH;
flp.gravity = Gravity.BOTTOM;
flp.bottomMargin = 0;
fadeBottom.setLayoutParams(flp);
}
} else if (lp != null && lp.height != bottomFadeH) {
lp.height = bottomFadeH;
fadeBottom.setLayoutParams(lp);
}
}
if (mUnknownSenderBanner != null
&& mUnknownSenderBanner.getLayoutParams() instanceof ViewGroup.MarginLayoutParams) {
final ViewGroup.MarginLayoutParams mlp =
(ViewGroup.MarginLayoutParams) mUnknownSenderBanner.getLayoutParams();
final int top = headerH + getResources().getDimensionPixelSize(
R.dimen.looply_space_md);
if (mlp.topMargin != top) {
mlp.topMargin = top;
mUnknownSenderBanner.setLayoutParams(mlp);
}
}
if (fadeTop != null) {
fadeTop.setClickable(false);
fadeTop.setFocusable(false);
}
if (fadeBottom != null) {
fadeBottom.setClickable(false);
fadeBottom.setFocusable(false);
}
if (mComposeStage != null) {
mComposeStage.bringToFront();
}
if (mMessageSelectionHeader != null
&& mMessageSelectionHeader.getVisibility() == View.VISIBLE) {
mMessageSelectionHeader.bringToFront();
} else if (mConversationHeader != null) {
mConversationHeader.bringToFront();
}
}
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);
}
@@ -1584,6 +2063,7 @@ public class ConversationFragment extends Fragment implements ConversationDataLi
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);
mComposeStage = root.findViewById(R.id.conversation_compose_stage);
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);
@@ -1644,6 +2124,12 @@ public class ConversationFragment extends Fragment implements ConversationDataLi
HomeUiMotion.replaceHeaderInstant(mMessageSelectionHeader, mConversationHeader);
mHost.invalidateActionBar();
}
if (getView() != null) {
getView().post(() -> applyConversationEdgeFade(
getView().findViewById(R.id.conversation_fade_top),
getView().findViewById(R.id.conversation_fade_bottom),
getResources().getDimensionPixelSize(R.dimen.home_conversation_edge_fade)));
}
HomeUiMotion.swapBottomDock(mComposeContainer, mMessageActionBar, active, null);
}
@@ -1762,14 +2248,127 @@ public class ConversationFragment extends Fragment implements ConversationDataLi
}
private void showConversationOverflowMenu(final View anchor) {
if (!mOptionsMenuVisible || getActivity() == null) {
if (!mOptionsMenuVisible || getActivity() == null || !mBinding.isBound()) {
return;
}
final PopupMenu popup = new PopupMenu(requireContext(), anchor);
popup.setOnMenuItemClickListener(this::onOptionsItemSelected);
onCreateOptionsMenu(popup.getMenu(), requireActivity().getMenuInflater());
onPrepareOptionsMenu(popup.getMenu());
popup.show();
dismissConversationOverflowPopover();
final View content = LayoutInflater.from(requireContext())
.inflate(R.layout.home_popover_conversation_overflow, null, false);
final ConversationData data = mBinding.getData();
final ParticipantData participant = data.getOtherParticipant();
final boolean showAddPeople = participant != null
&& !ContactUtil.isParticipantInAddressBook(getActivity(),
participant.getContactId(), participant.getLookupKey(),
participant.getNormalizedDestination());
final View addPeople = content.findViewById(R.id.popover_overflow_add_people);
addPeople.setVisibility(showAddPeople ? View.VISIBLE : View.GONE);
addPeople.setOnClickListener(v -> {
dismissConversationOverflowPopover();
performOverflowAddContact();
});
content.findViewById(R.id.popover_overflow_details).setOnClickListener(v -> {
dismissConversationOverflowPopover();
UIIntents.get().launchHomeProfileActivity(getActivity(), mConversationId);
});
content.findViewById(R.id.popover_overflow_search).setOnClickListener(v -> {
dismissConversationOverflowPopover();
UIIntents.get().launchHomeConversationSearchActivity(getActivity(), mConversationId);
});
final boolean archived = data.getIsArchived();
final TextView archiveLabel = content.findViewById(R.id.popover_overflow_archive_label);
archiveLabel.setText(archived
? R.string.home_profile_option_unarchive
: R.string.home_profile_option_archive);
content.findViewById(R.id.popover_overflow_archive).setOnClickListener(v -> {
dismissConversationOverflowPopover();
if (archived) {
mBinding.getData().unarchiveConversation(mBinding);
} else {
mBinding.getData().archiveConversation(mBinding);
closeConversation(mConversationId);
}
});
content.findViewById(R.id.popover_overflow_delete).setOnClickListener(v -> {
dismissConversationOverflowPopover();
confirmDeleteConversation();
});
content.measure(
View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED),
View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED));
final PopupWindow popup = new PopupWindow(content,
content.getMeasuredWidth(),
ViewGroup.LayoutParams.WRAP_CONTENT,
true);
popup.setOutsideTouchable(true);
popup.setFocusable(true);
popup.setElevation(16f);
popup.setBackgroundDrawable(new ColorDrawable(android.graphics.Color.TRANSPARENT));
popup.setOnDismissListener(() -> {
if (mConversationOverflowPopover == popup) {
mConversationOverflowPopover = null;
}
});
final int endInset = getResources().getDimensionPixelSize(R.dimen.looply_screen_padding);
final int[] anchorLoc = new int[2];
anchor.getLocationOnScreen(anchorLoc);
final View root = getView();
final int windowRight;
if (root != null) {
final int[] rootLoc = new int[2];
root.getLocationOnScreen(rootLoc);
windowRight = rootLoc[0] + root.getWidth();
} else {
windowRight = getResources().getDisplayMetrics().widthPixels;
}
final int desiredLeft = windowRight - endInset - content.getMeasuredWidth();
final int xOff = desiredLeft - anchorLoc[0];
popup.showAsDropDown(anchor, xOff, 8);
HomeUiMotion.popoverEnter(content, true);
mConversationOverflowPopover = popup;
}
private void dismissConversationOverflowPopover() {
if (mConversationOverflowPopover == null) {
return;
}
final PopupWindow popup = mConversationOverflowPopover;
mConversationOverflowPopover = null;
if (popup.isShowing()) {
popup.dismiss();
}
}
private void performOverflowAddContact() {
if (!mBinding.isBound()) {
return;
}
final ParticipantData participant = mBinding.getData().getOtherParticipant();
if (participant == null || getActivity() == null) {
return;
}
final String destination = participant.getNormalizedDestination();
final Uri avatarUri = AvatarUriUtil.createAvatarUri(participant);
new AddContactsConfirmationDialog(getActivity(), avatarUri, destination).show();
}
private void confirmDeleteConversation() {
if (getActivity() == null) {
return;
}
if (isReadyForDeleteAction()) {
new AlertDialog.Builder(getActivity(), R.style.Home_Dialog)
.setTitle(getResources().getQuantityString(
R.plurals.delete_conversations_confirmation_dialog_title, 1))
.setPositiveButton(R.string.delete_conversation_confirmation_button,
(dialog, button) -> deleteConversation())
.setNegativeButton(R.string.delete_conversation_decline_button, null)
.show();
} else {
warnOfMissingActionConditions(false /*sending*/,
null /*commandToRunAfterActionConditionResolved*/);
}
}
private void bindConversationHeaderAvatar(final ContactIconView avatar) {
@@ -1831,10 +2430,36 @@ public class ConversationFragment extends Fragment implements ConversationDataLi
}
}
if (mConversationHeaderCall != null) {
final boolean showCall = mBinding.isBound()
&& PhoneUtils.getDefault().isVoiceCapable()
&& mBinding.getData().getParticipantPhoneNumber() != null;
mConversationHeaderCall.setVisibility(showCall ? View.VISIBLE : View.GONE);
}
bindConversationHeaderSubtitle();
final boolean hideForIme = mHost.isImeOpen() && UiUtils.isLandscapeMode();
mConversationHeader.setVisibility(hideForIme ? View.GONE : View.VISIBLE);
}
private void bindConversationHeaderSubtitle() {
if (mConversationHeaderSubtitle == null) {
return;
}
if (!mBinding.isBound()) {
mConversationHeaderSubtitle.setVisibility(View.GONE);
return;
}
final ConversationData data = mBinding.getData();
if (data.getOtherParticipant() == null && data.getParticipantsLoaded()) {
mConversationHeaderSubtitle.setText(R.string.home_conversation_header_group);
mConversationHeaderSubtitle.setVisibility(View.VISIBLE);
return;
}
mConversationHeaderSubtitle.setVisibility(View.GONE);
}
public void updateActionBar(final ActionBar actionBar) {
if (mComposeMessageView != null && mComposeMessageView.updateActionBar(actionBar)) {
if (mConversationHeader != null) {
@@ -56,6 +56,8 @@ public class ConversationInputManager implements ConversationInput.ConversationI
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;
public static final int MEDIA_PICKER_TARGET_PHOTO = 5;
public static final int MEDIA_PICKER_TARGET_VIDEO = 6;
/**
* The host component where all input components are contained. This is typically the
@@ -222,6 +224,16 @@ public class ConversationInputManager implements ConversationInput.ConversationI
}
public void showHideImeKeyboard(final boolean show, final boolean animate) {
if (show) {
final EditText editText = mSink.getComposeEditText();
if (editText != null) {
ImeUtil.get().showImeKeyboard(mContext, editText);
if (mConversationDataModel.isBound() && !mImeInput.mShowing) {
showHideInternal(mImeInput, true, animate);
}
return;
}
}
showHideInternal(mImeInput, show, animate);
}
@@ -262,6 +274,9 @@ public class ConversationInputManager implements ConversationInput.ConversationI
}
if (target.mShowing == show) {
if (show && target instanceof ConversationImeKeyboard) {
ImeUtil.get().showImeKeyboard(mContext, mSink.getComposeEditText());
}
return false;
}
beginUpdate();
@@ -432,6 +447,12 @@ public class ConversationInputManager implements ConversationInput.ConversationI
mMediaPicker.openContact(animate);
// System picker only — do not keep the in-app panel marked open.
return false;
case MEDIA_PICKER_TARGET_PHOTO:
mMediaPicker.openTakePicture();
return false;
case MEDIA_PICKER_TARGET_VIDEO:
mMediaPicker.openTakeVideo();
return false;
default:
mMediaPicker.open(MediaPicker.MEDIA_TYPE_DEFAULT, animate);
break;
@@ -479,7 +500,7 @@ public class ConversationInputManager implements ConversationInput.ConversationI
mFragmentManager.beginTransaction().replace(
R.id.mediapicker_container,
mediaPicker,
MediaPicker.FRAGMENT_TAG).commit();
MediaPicker.FRAGMENT_TAG).commitNowAllowingStateLoss();
}
return mediaPicker;
}
@@ -114,6 +114,7 @@ public class ConversationMessageView extends FrameLayout implements View.OnClick
private boolean mOneOnOne;
private ConversationMessageViewHost mHost;
private String mOptimisticSendingMessageId;
public ConversationMessageView(final Context context, final AttributeSet attrs) {
super(context, attrs);
@@ -244,6 +245,11 @@ public class ConversationMessageView extends FrameLayout implements View.OnClick
// Update our UI model
mData.bind(cursor);
if (mOptimisticSendingMessageId != null
&& (!TextUtils.equals(mOptimisticSendingMessageId, mData.getMessageId())
|| !isOutgoingFailedStatus(mData.getStatus()))) {
mOptimisticSendingMessageId = null;
}
setSelected(TextUtils.equals(mData.getMessageId(), selectedMessageId));
// Update text and image content for the view.
@@ -272,6 +278,26 @@ public class ConversationMessageView extends FrameLayout implements View.OnClick
return mData;
}
public void showSendingStatus() {
if (mData == null || !isOutgoingFailedStatus(mData.getStatus())) {
return;
}
mOptimisticSendingMessageId = mData.getMessageId();
updateViewContent();
updateViewAppearance();
}
private boolean isOptimisticSending() {
return mOptimisticSendingMessageId != null
&& mData != null
&& TextUtils.equals(mOptimisticSendingMessageId, mData.getMessageId());
}
private static boolean isOutgoingFailedStatus(final int status) {
return status == MessageData.BUGLE_STATUS_OUTGOING_FAILED
|| status == MessageData.BUGLE_STATUS_OUTGOING_FAILED_EMERGENCY_NUMBER;
}
/**
* Returns whether we should show simplified visual style for the message view (i.e. hide the
* avatar and bubble arrow, reduce padding).
@@ -340,12 +366,9 @@ public class ConversationMessageView extends FrameLayout implements View.OnClick
case MessageData.BUGLE_STATUS_OUTGOING_YET_TO_SEND:
case MessageData.BUGLE_STATUS_OUTGOING_SENDING:
statusResId = R.string.message_status_sending;
break;
case MessageData.BUGLE_STATUS_OUTGOING_RESENDING:
case MessageData.BUGLE_STATUS_OUTGOING_AWAITING_RETRY:
statusResId = R.string.message_status_send_retrying;
statusResId = R.string.message_status_sending;
break;
case MessageData.BUGLE_STATUS_OUTGOING_FAILED_EMERGENCY_NUMBER:
@@ -354,6 +377,10 @@ public class ConversationMessageView extends FrameLayout implements View.OnClick
case MessageData.BUGLE_STATUS_OUTGOING_FAILED:
// don't show the error state unless we're the default sms app
if (isOptimisticSending()) {
statusResId = R.string.message_status_sending;
break;
}
if (PhoneUtils.getDefault().isDefaultSmsApp()) {
if (isSelected()) {
statusResId = R.string.message_status_resend;
@@ -697,7 +724,7 @@ public class ConversationMessageView extends FrameLayout implements View.OnClick
textBottomPadding = textBottomPaddingDefault;
textLeftPadding = messageTextLeftRightPadding;
textRightPadding = messageTextLeftRightPadding;
mMessageTextView.setTextIsSelectable(isSelected());
mMessageTextView.setTextIsSelectable(false);
} else {
// Attachment(s) only
contentLeftPadding = incoming ? arrowWidth : 0;
@@ -723,7 +750,7 @@ public class ConversationMessageView extends FrameLayout implements View.OnClick
textTopMargin = 0;
textTopPadding = textTopPaddingDefault;
textBottomPadding = textBottomPaddingDefault;
mMessageTextView.setTextIsSelectable(isSelected());
mMessageTextView.setTextIsSelectable(false);
textLeftPadding = messageTextLeftRightPadding;
textRightPadding = messageTextLeftRightPadding;
}
@@ -908,7 +935,11 @@ public class ConversationMessageView extends FrameLayout implements View.OnClick
case MessageData.BUGLE_STATUS_OUTGOING_FAILED:
case MessageData.BUGLE_STATUS_OUTGOING_FAILED_EMERGENCY_NUMBER:
timestampColorResId = R.color.message_failed_timestamp_text;
if (isOptimisticSending()) {
timestampColorResId = R.color.timestamp_text_outgoing;
} else {
timestampColorResId = R.color.message_failed_timestamp_text;
}
subjectLabelColorResId = R.color.timestamp_text_outgoing;
break;
@@ -26,8 +26,6 @@ import android.content.res.Resources;
import android.net.Uri;
import android.os.Bundle;
import android.view.View;
import android.widget.Toast;
import androidx.activity.result.ActivityResultLauncher;
import androidx.activity.result.contract.ActivityResultContracts;
import androidx.annotation.NonNull;
@@ -47,6 +45,7 @@ import com.android.messaging.datamodel.action.MarkAsUnreadAction;
import com.android.messaging.ui.UIIntents;
import com.android.messaging.ui.contact.AddContactsConfirmationDialog;
import com.android.messaging.ui.home.HomeDeleteAlertDialog;
import com.android.messaging.ui.home.HomeToast;
import com.android.messaging.ui.conversationlist.ConversationListFragment.ConversationListFragmentHost;
import com.android.messaging.ui.conversationlist.MultiSelectActionModeCallback.SelectedConversation;
import com.android.messaging.util.ChangeDefaultSmsAppHelper;
@@ -146,6 +145,14 @@ public abstract class AbstractConversationListActivity extends BugleActionBarAct
}
}
public void confirmDeleteConversation(final ConversationListItemData data) {
if (data == null) {
return;
}
showHomeDeleteAlert(java.util.Collections.singletonList(
new MultiSelectActionModeCallback.SelectedConversation(data)));
}
public void markSelectedConversationsUnread() {
if (!isInConversationListSelectMode()) {
return;
@@ -156,6 +163,7 @@ public abstract class AbstractConversationListActivity extends BugleActionBarAct
: callback.getSelectedConversations()) {
MarkAsUnreadAction.markAsUnread(conversation.conversationId);
}
exitMultiSelectState();
}
protected boolean isInConversationListSelectMode() {
@@ -195,13 +203,13 @@ public abstract class AbstractConversationListActivity extends BugleActionBarAct
final Activity activity = this;
UiUtils.showSnackBarWithCustomAction(this,
getWindow().getDecorView().getRootView(),
getString(R.string.requires_default_sms_app),
getString(R.string.home_requires_default_sms_app),
SnackBar.Action.createCustomAction(() -> {
final Intent intent =
UIIntents.get().getChangeDefaultSmsAppIntent(activity);
mLauncher.launch(intent);
},
getString(R.string.requires_default_sms_change_button)),
getString(R.string.home_requires_default_sms_change)),
null /* interactions */,
null /* placement */);
return false;
@@ -238,7 +246,7 @@ public abstract class AbstractConversationListActivity extends BugleActionBarAct
? getString(R.string.home_delete_toast_one)
: getString(R.string.home_delete_toast_many, count);
}
Toast.makeText(this, message, Toast.LENGTH_SHORT).show();
HomeToast.show(this, message);
}
private void performDeleteAndRegisterSpam(
@@ -16,37 +16,49 @@
*/
package com.android.messaging.ui.conversationlist;
import android.graphics.drawable.ColorDrawable;
import android.os.Bundle;
import android.view.MenuItem;
import androidx.appcompat.app.ActionBar;
import com.android.messaging.R;
import com.android.messaging.ui.home.HomeSubHeader;
public class ArchivedConversationListActivity extends AbstractConversationListActivity {
@Override
protected void onCreate(final Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.home_archived_activity);
if (getSupportActionBar() != null) {
getSupportActionBar().hide();
}
HomeSubHeader.bind(this, R.string.home_recently_deleted_title, v -> onActionBarHome());
mConversationListFragment =
ConversationListFragment.createArchivedConversationListFragment();
getSupportFragmentManager()
.beginTransaction()
.add(android.R.id.content, mConversationListFragment)
.commit();
invalidateActionBar();
if (savedInstanceState == null) {
mConversationListFragment =
ConversationListFragment.createArchivedConversationListFragment();
getSupportFragmentManager()
.beginTransaction()
.replace(R.id.home_archived_list_host, mConversationListFragment)
.commit();
} else {
mConversationListFragment = (ConversationListFragment) getSupportFragmentManager()
.findFragmentById(R.id.home_archived_list_host);
}
}
protected void updateActionBar(ActionBar actionBar) {
actionBar.setTitle(getString(R.string.archived_activity_title));
actionBar.setDisplayShowTitleEnabled(true);
actionBar.setDisplayHomeAsUpEnabled(true);
actionBar.setBackgroundDrawable(new ColorDrawable(
getResources().getColor(
R.color.archived_conversation_action_bar_background_color_dark)));
actionBar.show();
@Override
protected boolean useHomeSelectionChrome() {
return true;
}
@Override
protected void updateActionBar(final ActionBar actionBar) {
if (actionBar == null) {
return;
}
actionBar.hide();
super.updateActionBar(actionBar);
}
@@ -60,7 +72,7 @@ public class ArchivedConversationListActivity extends AbstractConversationListAc
}
@Override
public boolean onOptionsItemSelected(MenuItem menuItem) {
public boolean onOptionsItemSelected(final MenuItem menuItem) {
if (menuItem.getItemId() == android.R.id.home) {
onActionBarHome();
return true;
@@ -77,4 +89,10 @@ public class ArchivedConversationListActivity extends AbstractConversationListAc
public boolean isSwipeAnimatable() {
return false;
}
@Override
public void finish() {
super.finish();
overridePendingTransition(R.anim.home_activity_hold, R.anim.home_activity_close);
}
}
@@ -21,8 +21,6 @@ import android.os.Bundle;
import android.view.ActionMode;
import android.view.Menu;
import android.view.MenuItem;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.appcompat.app.ActionBar;
import androidx.appcompat.widget.PopupMenu;
@@ -80,6 +78,9 @@ public class ConversationListActivity extends AbstractConversationListActivity {
public void onBackPressed() {
if (isInConversationListSelectMode()) {
exitMultiSelectState();
} else if (mConversationListFragment != null
&& mConversationListFragment.onHomeBackPressed()) {
return;
} else {
super.onBackPressed();
}
@@ -146,7 +147,7 @@ public class ConversationListActivity extends AbstractConversationListActivity {
}
public void showAutomateComingSoon() {
Toast.makeText(this, R.string.home_automate_coming_soon, Toast.LENGTH_SHORT).show();
UIIntents.get().launchHomeAutomationActivity(this);
}
public void enterHomeEditMode() {
@@ -20,18 +20,24 @@ import android.app.Activity;
import android.app.AlertDialog;
import android.content.Context;
import android.database.Cursor;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Point;
import android.graphics.Rect;
import android.graphics.RenderEffect;
import android.graphics.Shader;
import android.telephony.PhoneNumberUtils;
import android.net.Uri;
import android.os.Bundle;
import android.os.Parcelable;
import android.text.Layout;
import android.text.TextUtils;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.Gravity;
import android.view.HapticFeedbackConstants;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewGroup.MarginLayoutParams;
@@ -41,10 +47,9 @@ import android.view.accessibility.AccessibilityManager;
import android.widget.AbsListView;
import android.widget.ImageButton;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.PopupWindow;
import android.widget.TextView;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.core.content.ContextCompat;
import androidx.core.view.ViewGroupCompat;
@@ -56,26 +61,33 @@ import androidx.recyclerview.widget.RecyclerView;
import com.android.messaging.R;
import com.android.messaging.datamodel.DataModel;
import com.android.messaging.datamodel.MessagingContentProvider;
import com.android.messaging.datamodel.data.ConversationMessageData;
import com.android.messaging.datamodel.action.MarkAsReadAction;
import com.android.messaging.datamodel.action.MarkAsUnreadAction;
import com.android.messaging.datamodel.binding.Binding;
import com.android.messaging.datamodel.binding.BindingBase;
import com.android.messaging.datamodel.data.ConversationListData;
import com.android.messaging.datamodel.data.ConversationListData.ConversationListDataListener;
import com.android.messaging.datamodel.data.ConversationListItemData;
import com.android.messaging.ui.ContactIconView;
import com.android.messaging.ui.ListEmptyView;
import com.android.messaging.ui.home.HomeEmptyState;
import com.android.messaging.ui.SnackBarInteraction;
import com.android.messaging.ui.UIIntents;
import com.android.messaging.ui.contact.AddContactsConfirmationDialog;
import com.android.messaging.util.AvatarUriUtil;
import com.android.messaging.util.ContactUtil;
import com.android.messaging.util.Dates;
import com.android.messaging.util.PhoneUtils;
import com.android.messaging.ui.home.HomePinnedStrip;
import com.android.messaging.ui.home.MutedConversationStore;
import com.android.messaging.ui.home.HomeToast;
import com.android.messaging.ui.home.HomeUiMotion;
import com.android.messaging.ui.home.PinnedConversationStore;
import com.android.messaging.util.AccessibilityUtil;
import com.android.messaging.util.ImeUtil;
import com.android.messaging.util.LogUtil;
import com.android.messaging.util.ThreadUtil;
import com.android.messaging.util.UiUtils;
import java.util.ArrayList;
@@ -118,16 +130,30 @@ public class ConversationListFragment extends Fragment implements ConversationLi
private TextView mHomeReadAllButton;
private int mHomeSelectionCount;
private View mHomeMessagesTop;
private TextView mHomeTitle;
private View mHomeAutomationPane;
private View mHomeAutomationCreateButton;
private boolean mHomeTabReady;
private View mHomePinnedStrip;
private TextView mHomeEditButton;
private ImageButton mHomePinnedDoneButton;
private boolean mHomePinnedEditMode;
private View mHomeNavMessages;
private View mHomeNavAutomate;
private View mHomeNavTabGap;
private View mHomeNavTabs;
private View mHomeNavIndicator;
private ImageView mHomeNavMessagesIcon;
private TextView mHomeNavMessagesLabel;
private ImageView mHomeNavAutomateIcon;
private TextView mHomeNavAutomateLabel;
private int mHomeNavMessagesNaturalWidth;
private int mHomeNavAutomateNaturalWidth;
private int mHomeTab = HOME_TAB_MESSAGES;
private View mHomeListProfilePopup;
private View mHomeListProfileScrim;
private ImageView mHomeListProfileBlur;
private Bitmap mHomeListProfileBlurBitmap;
private View mHomeListProfileContent;
private ContactIconView mHomeListProfileAvatar;
private View mHomeListProfileActionCall;
@@ -138,7 +164,7 @@ public class ConversationListFragment extends Fragment implements ConversationLi
private RecyclerView mRecyclerView;
private View mHomeComposeContainer;
private ImageButton mStartNewConversationButton;
private ListEmptyView mEmptyListMessageView;
private HomeEmptyState mEmptyListMessageView;
private ConversationListAdapter mAdapter;
// Saved Instance State Data - only for temporal data which is nice to maintain but not
@@ -150,6 +176,7 @@ public class ConversationListFragment extends Fragment implements ConversationLi
private static final String SAVED_INSTANCE_STATE_HOME_TAB = "homeTab";
private static final String SAVED_INSTANCE_STATE_PINNED_EDIT = "homePinnedEdit";
private static final int HOME_TAB_MESSAGES = 0;
private static final int HOME_TAB_AUTOMATE = 1;
private static final int SEARCH_NEWEST_EDGE_SLOP_DP = 48;
private static final int SEARCH_BAR_ANIM_MS = 280;
private static final int SEARCH_BAR_SLIDE_FALLBACK_DP = 72;
@@ -264,7 +291,6 @@ public class ConversationListFragment extends Fragment implements ConversationLi
container, false);
mRecyclerView = rootView.findViewById(android.R.id.list);
mEmptyListMessageView = rootView.findViewById(R.id.no_conversations_view);
mEmptyListMessageView.setImageHint(R.drawable.ic_oobe_conv_list);
// The default behavior for default layout param generation by LinearLayoutManager is to
// provide width and height of WRAP_CONTENT, but this is not desirable for
// ConversationListFragment; the view in each row should be a width of MATCH_PARENT so that
@@ -333,26 +359,51 @@ public class ConversationListFragment extends Fragment implements ConversationLi
mHomeComposeContainer = rootView.findViewById(R.id.home_compose_container);
mStartNewConversationButton = rootView.findViewById(R.id.home_nav_search);
mHomeAutomationCreateButton = rootView.findViewById(R.id.home_nav_automation_create);
if (mArchiveMode || mForwardMessageMode) {
rootView.setFitsSystemWindows(false);
if (mHomeComposeContainer != null) {
mHomeComposeContainer.setVisibility(View.GONE);
}
hideHomeChrome(rootView);
if (mRecyclerView != null) {
mRecyclerView.setPadding(
mRecyclerView.getPaddingLeft(),
mRecyclerView.getPaddingTop(),
mRecyclerView.getPaddingRight(),
getResources().getDimensionPixelSize(R.dimen.looply_space_lg));
}
} else {
if (mStartNewConversationButton != null) {
mStartNewConversationButton.setOnClickListener(v -> launchHomeSubScreen(
() -> UIIntents.get().launchHomeComposeActivity(requireActivity()),
R.anim.home_sheet_open));
}
if (mHomeAutomationCreateButton != null) {
mHomeAutomationCreateButton.setOnClickListener(v -> launchHomeSubScreen(
() -> UIIntents.get().launchHomeAutomationCreateActivity(
requireActivity()),
R.anim.home_sheet_open));
}
setupHomeChrome(rootView);
mHomeMessagesTop = rootView.findViewById(R.id.home_messages_top);
mHomeTitle = rootView.findViewById(R.id.home_title);
mHomeAutomationPane = rootView.findViewById(R.id.home_automation_pane);
mHomeNavMessages = rootView.findViewById(R.id.home_nav_messages);
bindHomeNavTab(mHomeNavMessages, R.drawable.ic_home_nav_messages,
R.string.home_nav_messages);
bindHomeNavTab(rootView.findViewById(R.id.home_nav_automate),
R.drawable.ic_home_nav_automate, R.string.home_nav_automate);
mHomeNavAutomate = rootView.findViewById(R.id.home_nav_automate);
bindHomeNavTab(mHomeNavAutomate, R.drawable.ic_home_nav_automate,
R.string.home_nav_automate);
mHomeNavTabs = rootView.findViewById(R.id.home_nav_tabs);
mHomeNavIndicator = rootView.findViewById(R.id.home_nav_indicator);
bindHomeNavMetrics(rootView.findViewById(R.id.home_nav_tab_gap));
mHomeNavMessagesIcon = mHomeNavMessages.findViewById(R.id.home_nav_tab_icon);
mHomeNavMessagesLabel = mHomeNavMessages.findViewById(R.id.home_nav_tab_label);
if (mHomeNavAutomate != null) {
mHomeNavAutomateIcon = mHomeNavAutomate.findViewById(R.id.home_nav_tab_icon);
mHomeNavAutomateLabel = mHomeNavAutomate.findViewById(R.id.home_nav_tab_label);
}
if (savedInstanceState != null) {
mHomeTab = savedInstanceState.getInt(SAVED_INSTANCE_STATE_HOME_TAB,
HOME_TAB_MESSAGES);
@@ -373,6 +424,96 @@ public class ConversationListFragment extends Fragment implements ConversationLi
return rootView;
}
private void bindHomeNavMetrics(final View gap) {
mHomeNavTabGap = gap;
if (mHomeNavMessages == null) {
return;
}
mHomeNavMessages.addOnLayoutChangeListener((v, l, t, r, b, ol, ot, or, ob) ->
applyHomeNavMetrics());
mHomeNavMessages.post(this::applyHomeNavMetrics);
}
private void applyHomeNavMetrics() {
if (mHomeNavMessagesNaturalWidth <= 0 && mHomeNavMessages != null) {
mHomeNavMessagesNaturalWidth = measureNavTabNaturalWidth(mHomeNavMessages);
}
if (mHomeNavAutomateNaturalWidth <= 0 && mHomeNavAutomate != null) {
mHomeNavAutomateNaturalWidth = measureNavTabNaturalWidth(mHomeNavAutomate);
}
final int gapBase = mHomeNavMessagesNaturalWidth > 0
? mHomeNavMessagesNaturalWidth : mHomeNavAutomateNaturalWidth;
if (gapBase > 0 && mHomeNavTabGap != null) {
final int gapWidth = Math.max(Math.round(gapBase * 0.6f), 1);
final ViewGroup.LayoutParams gapParams = mHomeNavTabGap.getLayoutParams();
if (gapParams != null && gapParams.width != gapWidth) {
gapParams.width = gapWidth;
mHomeNavTabGap.setLayoutParams(gapParams);
}
}
applyHomeNavTabActiveSize(mHomeNavMessages, mHomeNavMessagesNaturalWidth);
applyHomeNavTabActiveSize(mHomeNavAutomate, mHomeNavAutomateNaturalWidth);
}
private void applyHomeNavTabActiveSize(final View tab, final int natural) {
if (tab == null || natural <= 0) {
return;
}
final int basePad = tab.getResources()
.getDimensionPixelSize(R.dimen.home_nav_item_padding);
final int activeWidth = Math.max(Math.round(natural * 1.65f), natural + 1);
final ViewGroup.LayoutParams params = tab.getLayoutParams();
if (params != null && params.width != activeWidth) {
params.width = activeWidth;
tab.setLayoutParams(params);
}
tab.setMinimumWidth(activeWidth);
final int extraPad = Math.max((activeWidth - natural) / 2, 0);
tab.setPadding(basePad + extraPad, basePad, basePad + extraPad, basePad);
}
private void slideHomeNavIndicator(final boolean animate) {
final View tab = mHomeTab == HOME_TAB_AUTOMATE ? mHomeNavAutomate : mHomeNavMessages;
if (tab == null || mHomeNavIndicator == null || mHomeNavTabs == null
|| tab.getWidth() <= 0) {
return;
}
final float x = mHomeNavTabs.getLeft() + tab.getLeft();
final float y = mHomeNavTabs.getTop() + tab.getTop();
HomeUiMotion.slideNavIndicator(mHomeNavIndicator, x, y, tab.getWidth(), tab.getHeight(),
animate);
}
private int measureNavTabNaturalWidth(final View tab) {
final int basePad = tab.getResources()
.getDimensionPixelSize(R.dimen.home_nav_item_padding);
tab.setPadding(basePad, basePad, basePad, basePad);
tab.setMinimumWidth(0);
final ViewGroup.LayoutParams params = tab.getLayoutParams();
final int oldWidth = params != null ? params.width : ViewGroup.LayoutParams.WRAP_CONTENT;
if (params != null) {
params.width = ViewGroup.LayoutParams.WRAP_CONTENT;
}
final TextView label = tab.findViewById(R.id.home_nav_tab_label);
final int restoreStyle = label != null && label.getTypeface() != null
&& label.getTypeface().isBold()
? android.graphics.Typeface.BOLD : android.graphics.Typeface.NORMAL;
if (label != null) {
// Bold is wider than regular; measure the widest state so the last glyph is not clipped.
label.setTypeface(null, android.graphics.Typeface.BOLD);
}
tab.measure(
View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED),
View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED));
if (label != null) {
label.setTypeface(null, restoreStyle);
}
if (params != null) {
params.width = oldWidth;
}
return tab.getMeasuredWidth();
}
private void bindHomeNavTab(final View tabRoot, final int iconRes, final int labelRes) {
if (tabRoot == null) {
return;
@@ -579,9 +720,7 @@ public class ConversationListFragment extends Fragment implements ConversationLi
navMessages.setOnClickListener(v -> selectHomeTab(HOME_TAB_MESSAGES));
}
if (navAutomate != null) {
navAutomate.setOnClickListener(v ->
Toast.makeText(requireContext(), R.string.home_automate_coming_soon,
Toast.LENGTH_SHORT).show());
navAutomate.setOnClickListener(v -> selectHomeTab(HOME_TAB_AUTOMATE));
}
final View selectionDone = rootView.findViewById(R.id.home_selection_done);
@@ -612,6 +751,7 @@ public class ConversationListFragment extends Fragment implements ConversationLi
return;
}
mHomeListProfileScrim = mHomeListProfilePopup.findViewById(R.id.home_list_profile_scrim);
mHomeListProfileBlur = mHomeListProfilePopup.findViewById(R.id.home_list_profile_blur);
mHomeListProfileContent = mHomeListProfilePopup.findViewById(R.id.home_list_profile_content);
mHomeListProfileAvatar = mHomeListProfilePopup.findViewById(R.id.home_list_profile_avatar);
mHomeListProfileActionCall =
@@ -693,6 +833,7 @@ public class ConversationListFragment extends Fragment implements ConversationLi
}
mHomeListProfileContent.setAlpha(0f);
mHomeListProfilePopup.setVisibility(View.INVISIBLE);
bindListProfileBlur();
positionListProfilePopup(anchorView, () -> {
mHomeListProfilePopup.setVisibility(View.VISIBLE);
HomeUiMotion.listProfilePopupEnter(mHomeListProfileScrim, mHomeListProfileContent,
@@ -824,12 +965,14 @@ public class ConversationListFragment extends Fragment implements ConversationLi
private void dismissHomeListProfilePopup(final boolean animate) {
if (mHomeListProfilePopup == null
|| mHomeListProfilePopup.getVisibility() != View.VISIBLE) {
recycleListProfileBlur();
mHomeListProfileData = null;
return;
}
final Runnable finish = () -> {
mHomeListProfilePopup.setVisibility(View.GONE);
resetListProfilePopupPosition();
recycleListProfileBlur();
mHomeListProfileData = null;
};
if (animate) {
@@ -865,12 +1008,15 @@ public class ConversationListFragment extends Fragment implements ConversationLi
if (data == null || getActivity() == null) {
return;
}
if (!data.getIsGroup() && ContactUtil.openContactDetail(requireContext(),
data.getParticipantContactId(), data.getParticipantLookupKey(),
data.getOtherParticipantNormalizedDestination())) {
if (data.getIsGroup()) {
UIIntents.get().launchHomeProfileActivity(getActivity(), data.getConversationId());
return;
}
UIIntents.get().launchHomeProfileActivity(getActivity(), data.getConversationId());
if (!ContactUtil.openContactDetail(getActivity(),
data.getParticipantContactId(), data.getParticipantLookupKey(),
data.getOtherParticipantNormalizedDestination())) {
UIIntents.get().launchHomeProfileActivity(getActivity(), data.getConversationId());
}
}
private void addListProfileContact() {
@@ -932,10 +1078,37 @@ public class ConversationListFragment extends Fragment implements ConversationLi
if (isSelectionMode() && tab != HOME_TAB_MESSAGES) {
return;
}
if (mHomeTabReady && tab == mHomeTab) {
return;
}
final boolean animate = mHomeTabReady;
mHomeTab = tab;
dismissHomePopoverImmediate();
dismissHomeListProfilePopup(false);
final boolean showMessages = tab == HOME_TAB_MESSAGES;
if (mHomeMessagesTop != null) {
mHomeMessagesTop.setVisibility(showMessages ? View.VISIBLE : View.GONE);
mHomeMessagesTop.setVisibility(View.VISIBLE);
}
final CharSequence title = getString(showMessages
? R.string.home_title : R.string.home_automation_title);
if (mHomeTitle != null) {
if (animate) {
HomeUiMotion.fadeSwapText(mHomeTitle, title);
} else {
mHomeTitle.setText(title);
}
}
final View incoming = showMessages ? mRecyclerView : mHomeAutomationPane;
final View outgoing = showMessages ? mHomeAutomationPane : mRecyclerView;
if (animate) {
HomeUiMotion.crossfadeHomePages(outgoing, incoming, !showMessages);
} else {
if (mRecyclerView != null) {
mRecyclerView.setVisibility(showMessages ? View.VISIBLE : View.GONE);
}
if (mHomeAutomationPane != null) {
mHomeAutomationPane.setVisibility(showMessages ? View.GONE : View.VISIBLE);
}
}
if (!showMessages) {
mSearchBarShown = false;
@@ -953,10 +1126,19 @@ public class ConversationListFragment extends Fragment implements ConversationLi
applySearchBarVisibility();
updateEmptyListUi(mAdapter.getConversationItemCount() == 0);
}
updateHomeComposeButtonVisibility();
updateHomeNavHighlight();
updateHomeComposeButtonVisibility(animate);
updateHomeNavHighlight(animate);
updateHomeMainNavVisibility();
refreshPinnedStrip();
mHomeTabReady = true;
}
public boolean onHomeBackPressed() {
if (mHomeTab == HOME_TAB_AUTOMATE) {
selectHomeTab(HOME_TAB_MESSAGES);
return true;
}
return false;
}
/** Main tabs show nav; selection mode and other activities hide it ({@link #onPause}). */
@@ -978,23 +1160,46 @@ public class ConversationListFragment extends Fragment implements ConversationLi
} else {
mHomeBottomNav.setVisibility(isResumed() ? View.VISIBLE : View.GONE);
}
updateHomeComposeButtonVisibility();
updateHomeComposeButtonVisibility(false);
}
private void updateHomeComposeButtonVisibility() {
updateHomeComposeButtonVisibility(false);
}
private void updateHomeComposeButtonVisibility(final boolean animate) {
if (mArchiveMode || mForwardMessageMode) {
return;
}
final boolean showCompose = isResumed() && mHomeTab == HOME_TAB_MESSAGES
&& !isSelectionMode();
final boolean showDock = isResumed() && !isSelectionMode();
if (mHomeComposeContainer != null) {
mHomeComposeContainer.setVisibility(showCompose ? View.VISIBLE : View.GONE);
mHomeComposeContainer.setVisibility(showDock ? View.VISIBLE : View.GONE);
}
if (mStartNewConversationButton != null && showCompose) {
mStartNewConversationButton.animate().cancel();
mStartNewConversationButton.setTranslationX(0f);
mStartNewConversationButton.setVisibility(View.VISIBLE);
mStartNewConversationButton.setEnabled(true);
final boolean showCompose = showDock && mHomeTab == HOME_TAB_MESSAGES;
final boolean showCreate = showDock && mHomeTab == HOME_TAB_AUTOMATE;
final View incoming = showCreate ? mHomeAutomationCreateButton
: (showCompose ? mStartNewConversationButton : null);
final View outgoing = showCreate ? mStartNewConversationButton
: (showCompose ? mHomeAutomationCreateButton : null);
if (incoming != null) {
incoming.setEnabled(true);
incoming.setTranslationX(0f);
}
if (animate && incoming != null) {
HomeUiMotion.fadeSwapViews(outgoing, incoming);
} else {
if (mStartNewConversationButton != null) {
mStartNewConversationButton.animate().cancel();
mStartNewConversationButton.setAlpha(1f);
mStartNewConversationButton.setVisibility(
showCompose ? View.VISIBLE : View.GONE);
}
if (mHomeAutomationCreateButton != null) {
mHomeAutomationCreateButton.animate().cancel();
mHomeAutomationCreateButton.setAlpha(1f);
mHomeAutomationCreateButton.setVisibility(
showCreate ? View.VISIBLE : View.GONE);
}
}
}
@@ -1017,8 +1222,18 @@ public class ConversationListFragment extends Fragment implements ConversationLi
}
private void updateHomeNavHighlight() {
updateHomeNavHighlight(false);
}
private void updateHomeNavHighlight(final boolean animate) {
applyHomeNavTabStyle(mHomeNavMessages, mHomeNavMessagesIcon, mHomeNavMessagesLabel,
mHomeTab == HOME_TAB_MESSAGES);
applyHomeNavTabStyle(mHomeNavAutomate, mHomeNavAutomateIcon, mHomeNavAutomateLabel,
mHomeTab == HOME_TAB_AUTOMATE);
applyHomeNavMetrics();
if (mHomeNavIndicator != null) {
mHomeNavIndicator.post(() -> slideHomeNavIndicator(animate));
}
}
private void applyHomeNavTabStyle(final View tab, final ImageView icon, final TextView label,
@@ -1026,11 +1241,7 @@ public class ConversationListFragment extends Fragment implements ConversationLi
if (tab == null) {
return;
}
if (active) {
tab.setBackgroundResource(R.drawable.home_bg_nav_active_primary);
} else {
tab.setBackgroundResource(android.R.color.transparent);
}
tab.setBackgroundResource(android.R.color.transparent);
final int color = ContextCompat.getColor(requireContext(),
active ? R.color.home_nav_active_content : R.color.home_nav_inactive);
if (icon != null) {
@@ -1077,6 +1288,8 @@ public class ConversationListFragment extends Fragment implements ConversationLi
}
private PopupWindow mHomePopover;
private View mHomePeekCard;
private Bitmap mHomePeekBlur;
private void dismissHomePopover() {
dismissHomePopoverAndThen(null);
@@ -1086,6 +1299,7 @@ public class ConversationListFragment extends Fragment implements ConversationLi
private void dismissHomePopoverImmediate() {
if (mHomePopover == null || !mHomePopover.isShowing()) {
mHomePopover = null;
recyclePeekBlur();
return;
}
final View content = mHomePopover.getContentView();
@@ -1093,6 +1307,10 @@ public class ConversationListFragment extends Fragment implements ConversationLi
content.animate().cancel();
HomeUiMotion.popoverResetInstant(content);
}
if (mHomePeekCard != null) {
mHomePeekCard.animate().cancel();
HomeUiMotion.popoverResetInstant(mHomePeekCard);
}
final PopupWindow popup = mHomePopover;
mHomePopover = null;
popup.dismiss();
@@ -1101,15 +1319,18 @@ public class ConversationListFragment extends Fragment implements ConversationLi
private void dismissHomePopoverAndThen(final Runnable afterDismiss) {
if (mHomePopover == null || !mHomePopover.isShowing()) {
mHomePopover = null;
recyclePeekBlur();
if (afterDismiss != null) {
afterDismiss.run();
}
return;
}
final PopupWindow popup = mHomePopover;
final View content = popup.getContentView();
final View host = popup.getContentView();
final View card = mHomePeekCard != null ? mHomePeekCard : host;
mHomePopover = null;
HomeUiMotion.popoverExit(content, () -> {
fadePeekBackdrop(host, 0f, 160);
HomeUiMotion.popoverExit(card, () -> {
if (popup.isShowing()) {
popup.dismiss();
}
@@ -1119,6 +1340,382 @@ public class ConversationListFragment extends Fragment implements ConversationLi
});
}
private void fadePeekBackdrop(final View host, final float alpha, final int duration) {
if (host == null) {
return;
}
final View blur = host.findViewById(R.id.home_peek_blur);
final View dim = host.findViewById(R.id.home_peek_dim);
if (blur != null) {
blur.animate().cancel();
blur.animate().alpha(alpha).setDuration(duration).start();
}
if (dim != null) {
dim.animate().cancel();
dim.animate().alpha(alpha).setDuration(duration).start();
}
}
private void recyclePeekBlur() {
mHomePeekCard = null;
if (mHomePeekBlur != null && !mHomePeekBlur.isRecycled()) {
mHomePeekBlur.recycle();
}
mHomePeekBlur = null;
}
private void bindListProfileBlur() {
recycleListProfileBlur();
if (mHomeListProfileBlur == null) {
return;
}
mHomeListProfileBlurBitmap = captureDecorSnapshot();
if (mHomeListProfileBlurBitmap == null) {
return;
}
mHomeListProfileBlur.setImageBitmap(mHomeListProfileBlurBitmap);
mHomeListProfileBlur.setRenderEffect(RenderEffect.createBlurEffect(
36f, 36f, Shader.TileMode.CLAMP));
}
private void recycleListProfileBlur() {
if (mHomeListProfileBlur != null) {
mHomeListProfileBlur.setRenderEffect(null);
mHomeListProfileBlur.setImageDrawable(null);
}
if (mHomeListProfileBlurBitmap != null && !mHomeListProfileBlurBitmap.isRecycled()) {
mHomeListProfileBlurBitmap.recycle();
}
mHomeListProfileBlurBitmap = null;
}
private void showConversationActionPopover(final ConversationListItemData data,
final ConversationListItemView conversationView) {
conversationView.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS);
dismissHomePopover();
final View content = LayoutInflater.from(requireContext())
.inflate(R.layout.home_popover_conversation, null, false);
bindConversationPreviewPlaceholder(content, data);
final boolean pinned = PinnedConversationStore.isPinned(requireContext(),
data.getConversationId());
final boolean muted = MutedConversationStore.isMuted(requireContext(),
data.getConversationId());
final boolean read = data.getIsRead();
final TextView pinLabel = content.findViewById(R.id.popover_conversation_pin_label);
if (pinLabel != null) {
pinLabel.setText(pinned
? R.string.home_conversation_unpin
: R.string.home_conversation_pin);
}
final TextView unreadLabel = content.findViewById(R.id.popover_conversation_unread_label);
if (unreadLabel != null) {
unreadLabel.setText(read
? R.string.home_conversation_mark_unread
: R.string.home_conversation_mark_read);
}
final TextView muteLabel = content.findViewById(R.id.popover_conversation_mute_label);
if (muteLabel != null) {
muteLabel.setText(muted
? R.string.home_conversation_show_alerts
: R.string.home_conversation_hide_alerts);
}
final ImageView muteIcon = content.findViewById(R.id.popover_conversation_mute_icon);
if (muteIcon != null) {
muteIcon.setImageResource(muted
? R.drawable.ic_home_bell
: R.drawable.ic_home_bell_off);
}
content.findViewById(R.id.popover_conversation_pin).setOnClickListener(v -> {
dismissHomePopover();
onConversationPinButtonClicked(data.getConversationId());
if (PinnedConversationStore.isPinned(requireContext(), data.getConversationId())
!= pinned) {
HomeToast.show(requireContext(), pinned
? R.string.home_conversation_unpinned_toast
: R.string.home_conversation_pinned_toast);
}
});
content.findViewById(R.id.popover_conversation_unread).setOnClickListener(v -> {
dismissHomePopover();
if (read) {
MarkAsUnreadAction.markAsUnread(data.getConversationId());
HomeToast.show(requireContext(), R.string.home_conversation_marked_unread_toast);
} else {
MarkAsReadAction.markAsRead(data.getConversationId());
HomeToast.show(requireContext(), R.string.home_conversation_marked_read_toast);
}
});
content.findViewById(R.id.popover_conversation_mute).setOnClickListener(v -> {
dismissHomePopover();
toggleConversationMute(data);
});
content.findViewById(R.id.popover_conversation_delete).setOnClickListener(v -> {
dismissHomePopover();
confirmDeleteConversation(data);
});
final PopupWindow popup = showHomePeekPopover(conversationView, content);
loadConversationPreview(data.getConversationId(), conversationView, content, popup);
}
private void toggleConversationMute(final ConversationListItemData data) {
final boolean muted = MutedConversationStore.toggle(requireContext(),
data.getConversationId());
HomeToast.show(requireContext(), muted
? R.string.home_conversation_muted_toast
: R.string.home_conversation_unmuted_toast);
}
private void confirmDeleteConversation(final ConversationListItemData data) {
if (getActivity() instanceof AbstractConversationListActivity) {
((AbstractConversationListActivity) getActivity()).confirmDeleteConversation(data);
}
}
private static final int PREVIEW_MESSAGE_LIMIT = 3;
private static final class PreviewMessage {
final String text;
final boolean incoming;
final long timestamp;
PreviewMessage(final String text, final boolean incoming, final long timestamp) {
this.text = text;
this.incoming = incoming;
this.timestamp = timestamp;
}
}
private void bindConversationPreviewPlaceholder(final View content,
final ConversationListItemData data) {
final String snippet = formatPreviewText(data.getSnippetText());
if (TextUtils.isEmpty(snippet)) {
return;
}
final List<PreviewMessage> placeholder = new ArrayList<>(1);
placeholder.add(new PreviewMessage(snippet, true, 0L));
bindConversationPreview(content, placeholder);
}
private void loadConversationPreview(final String conversationId, final View anchor,
final View content, final PopupWindow popup) {
final Context context = getContext();
if (context == null) {
return;
}
final Context appContext = context.getApplicationContext();
final String attachmentLabel = context.getString(
R.string.home_conversation_preview_attachment);
new Thread(() -> {
final List<PreviewMessage> messages = queryPreviewMessages(
appContext, conversationId, attachmentLabel);
ThreadUtil.getMainThreadHandler().post(() -> {
if (!isAdded() || mHomePopover != popup || !popup.isShowing()) {
return;
}
if (!messages.isEmpty()) {
bindConversationPreview(content, messages);
positionPeekCard(popup.getContentView(), anchor, content);
}
});
}, "home-conv-preview").start();
}
private List<PreviewMessage> queryPreviewMessages(final Context context,
final String conversationId, final String attachmentLabel) {
final List<PreviewMessage> out = new ArrayList<>();
final Uri uri = MessagingContentProvider.buildConversationMessagesUri(conversationId);
try (Cursor cursor = context.getContentResolver().query(
uri, ConversationMessageData.getProjection(), null, null, null)) {
if (cursor == null || !cursor.moveToLast()) {
return out;
}
final ConversationMessageData message = new ConversationMessageData();
int remaining = PREVIEW_MESSAGE_LIMIT;
do {
message.bind(cursor);
String text = formatPreviewText(message.getRawText());
if (TextUtils.isEmpty(text)) {
text = attachmentLabel;
}
out.add(0, new PreviewMessage(text, message.getIsIncoming(),
message.getReceivedTimeStamp()));
remaining--;
} while (remaining > 0 && cursor.moveToPrevious());
} catch (final Exception ignored) {
}
return out;
}
private void bindConversationPreview(final View content,
final List<PreviewMessage> messages) {
final ViewGroup host = content.findViewById(R.id.popover_preview_messages);
if (host == null) {
return;
}
host.removeAllViews();
final LayoutInflater inflater = LayoutInflater.from(content.getContext());
boolean timeShown = false;
for (final PreviewMessage message : messages) {
final View row = inflater.inflate(R.layout.home_popover_preview_bubble, host, false);
final TextView time = row.findViewById(R.id.home_preview_bubble_time);
final TextView bubble = row.findViewById(R.id.home_preview_bubble_text);
if (!timeShown && message.timestamp > 0L) {
time.setVisibility(View.VISIBLE);
time.setText(Dates.getMessageTimeString(message.timestamp));
timeShown = true;
}
final float density = content.getResources().getDisplayMetrics().density;
final int screenW = content.getResources().getDisplayMetrics().widthPixels;
final int cardW = Math.max(0, screenW - Math.round(32f * density));
bubble.setMaxWidth(Math.max(0, cardW - Math.round(40f * density)));
bubble.setBreakStrategy(Layout.BREAK_STRATEGY_SIMPLE);
bubble.setHyphenationFrequency(Layout.HYPHENATION_FREQUENCY_NONE);
bindPreviewBubbleText(bubble, message.text);
if (message.incoming) {
bubble.setBackgroundResource(R.drawable.home_bg_preview_bubble_in);
bubble.setTextColor(content.getContext().getColor(R.color.home_text_primary));
((LinearLayout.LayoutParams) bubble.getLayoutParams()).gravity = Gravity.START;
} else {
bubble.setBackgroundResource(R.drawable.home_bg_preview_bubble_out);
bubble.setTextColor(content.getContext().getColor(R.color.home_text_primary));
((LinearLayout.LayoutParams) bubble.getLayoutParams()).gravity = Gravity.END;
}
host.addView(row);
}
}
private static String formatPreviewText(final String text) {
return ConversationMessageData.flattenPreviewText(text);
}
private static void bindPreviewBubbleText(final TextView bubble, final String raw) {
final String text = formatPreviewText(raw);
bubble.setText(text == null ? "" : text);
}
private PopupWindow showHomePeekPopover(final View anchor, final View content) {
mHomePeekCard = content;
final ViewGroup host = (ViewGroup) LayoutInflater.from(requireContext())
.inflate(R.layout.home_popover_peek_host, null, false);
final ImageView blur = host.findViewById(R.id.home_peek_blur);
final View dim = host.findViewById(R.id.home_peek_dim);
mHomePeekBlur = captureDecorSnapshot();
if (mHomePeekBlur != null && blur != null) {
blur.setImageBitmap(mHomePeekBlur);
blur.setRenderEffect(RenderEffect.createBlurEffect(
36f, 36f, Shader.TileMode.CLAMP));
blur.setAlpha(0f);
}
if (dim != null) {
dim.setAlpha(0f);
dim.setOnClickListener(v -> dismissHomePopover());
}
host.addView(content, new FrameLayout.LayoutParams(
ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT));
final PopupWindow popup = new PopupWindow(host,
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.MATCH_PARENT,
true);
popup.setOutsideTouchable(true);
popup.setFocusable(true);
popup.setClippingEnabled(false);
popup.setBackgroundDrawable(new android.graphics.drawable.ColorDrawable(
android.graphics.Color.TRANSPARENT));
popup.setOnDismissListener(() -> {
if (mHomePopover == popup) {
mHomePopover = null;
}
if (blur != null) {
blur.setRenderEffect(null);
blur.setImageDrawable(null);
}
recyclePeekBlur();
});
popup.showAtLocation(anchor, Gravity.NO_GRAVITY, 0, 0);
host.post(() -> {
if (mHomePopover != popup || !popup.isShowing()) {
return;
}
positionPeekCard(host, anchor, content);
HomeUiMotion.popoverEnter(content, false);
fadePeekBackdrop(host, 1f, 180);
});
mHomePopover = popup;
return popup;
}
private Bitmap captureDecorSnapshot() {
final Activity activity = getActivity();
if (activity == null) {
return null;
}
final View root = activity.getWindow().getDecorView();
if (root.getWidth() <= 0 || root.getHeight() <= 0) {
return null;
}
final Bitmap bitmap = Bitmap.createBitmap(
root.getWidth(), root.getHeight(), Bitmap.Config.ARGB_8888);
final Canvas canvas = new Canvas(bitmap);
root.draw(canvas);
return bitmap;
}
private void positionPeekCard(final View host, final View anchor, final View content) {
if (host == null || anchor == null || content == null) {
return;
}
final int[] hostLoc = new int[2];
host.getLocationOnScreen(hostLoc);
final int[] anchorLoc = new int[2];
anchor.getLocationOnScreen(anchorLoc);
final int hostWidth = host.getWidth() > 0 ? host.getWidth()
: getResources().getDisplayMetrics().widthPixels;
final int hostHeight = host.getHeight() > 0 ? host.getHeight()
: getResources().getDisplayMetrics().heightPixels;
final float density = getResources().getDisplayMetrics().density;
final int margin = Math.round(16f * density);
final int width = Math.max(0, hostWidth - margin * 2);
final ViewGroup.LayoutParams lp = content.getLayoutParams();
if (lp != null) {
lp.width = width;
content.setLayoutParams(lp);
}
final int maxHeight = Math.max(0, hostHeight - margin * 2);
content.measure(
View.MeasureSpec.makeMeasureSpec(width, View.MeasureSpec.EXACTLY),
View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED));
final int naturalHeight = content.getMeasuredHeight();
final boolean needsScroll = naturalHeight > maxHeight;
final int height = needsScroll ? maxHeight : naturalHeight;
if (lp != null) {
lp.height = needsScroll ? maxHeight : ViewGroup.LayoutParams.WRAP_CONTENT;
content.setLayoutParams(lp);
}
content.measure(
View.MeasureSpec.makeMeasureSpec(width, View.MeasureSpec.EXACTLY),
View.MeasureSpec.makeMeasureSpec(height,
needsScroll ? View.MeasureSpec.EXACTLY : View.MeasureSpec.UNSPECIFIED));
final int x = Math.max(0, (hostWidth - width) / 2);
final int yBelow = anchorLoc[1] + anchor.getHeight() + 8 - hostLoc[1];
final int yAbove = anchorLoc[1] - height - 8 - hostLoc[1];
final boolean fitsBelow = yBelow + height <= hostHeight - margin;
final boolean fitsAbove = yAbove >= margin;
final int y;
if (needsScroll) {
y = margin;
} else if (fitsBelow) {
y = yBelow;
} else if (fitsAbove) {
y = yAbove;
} else {
y = Math.max(margin, (hostHeight - height) / 2);
}
content.setX(x);
content.setY(y);
}
private PopupWindow showHomePopover(final View anchor, final int layoutResId,
final boolean alignEnd) {
dismissHomePopover();
@@ -1141,9 +1738,25 @@ public class ConversationListFragment extends Fragment implements ConversationLi
mHomePopover = null;
}
});
final int xOff = alignEnd
? Math.max(0, anchor.getWidth() - content.getMeasuredWidth())
: 0;
final int xOff;
if (alignEnd) {
final int endInset = getResources().getDimensionPixelSize(
R.dimen.looply_screen_padding);
final int[] anchorLoc = new int[2];
anchor.getLocationOnScreen(anchorLoc);
final View root = getView();
final int windowRight;
if (root != null) {
final int[] rootLoc = new int[2];
root.getLocationOnScreen(rootLoc);
windowRight = rootLoc[0] + root.getWidth();
} else {
windowRight = getResources().getDisplayMetrics().widthPixels;
}
xOff = windowRight - endInset - content.getMeasuredWidth() - anchorLoc[0];
} else {
xOff = 0;
}
popup.showAsDropDown(anchor, xOff, 8);
HomeUiMotion.popoverEnter(content, alignEnd);
mHomePopover = popup;
@@ -1172,8 +1785,7 @@ public class ConversationListFragment extends Fragment implements ConversationLi
});
content.findViewById(R.id.popover_work).setOnClickListener(v -> {
dismissHomePopover();
Toast.makeText(requireContext(), R.string.home_popover_coming_soon,
Toast.LENGTH_SHORT).show();
HomeToast.show(requireContext(), R.string.home_popover_coming_soon);
});
content.findViewById(R.id.popover_personal).setOnClickListener(v -> {
selectHomeChip(ConversationListAdapter.FILTER_ALL);
@@ -1316,11 +1928,25 @@ public class ConversationListFragment extends Fragment implements ConversationLi
@Override
public void onConversationClicked(final ConversationListItemData conversationListItemData,
final boolean isLongClick, final ConversationListItemView conversationView) {
if (isLongClick && !mHost.isSelectionMode()) {
showConversationActionPopover(conversationListItemData, conversationView);
return;
}
final ConversationListData listData = mListBinding.getData();
mHost.onConversationClick(listData, conversationListItemData, isLongClick,
conversationView);
}
@Override
public void onConversationSwipeMuteClicked(final ConversationListItemData data) {
toggleConversationMute(data);
}
@Override
public void onConversationSwipeDeleteClicked(final ConversationListItemData data) {
confirmDeleteConversation(data);
}
@Override
public void onConversationAvatarClicked(final ConversationListItemData data,
final View avatarView) {
@@ -1371,19 +1997,25 @@ public class ConversationListFragment extends Fragment implements ConversationLi
// Show and hide empty list UI as needed with appropriate text based on view specifics
private void updateEmptyListUi(final boolean isEmpty) {
if (mEmptyListMessageView == null) {
return;
}
if (isEmpty) {
int emptyListText;
if (!mListBinding.getData().getHasFirstSyncCompleted()) {
emptyListText = R.string.conversation_list_first_sync_text;
mEmptyListMessageView.setContent(R.drawable.ic_home_nav_messages,
R.string.home_empty_messages_sync_title,
R.string.home_empty_messages_sync_body);
} else if (mArchiveMode) {
emptyListText = R.string.archived_conversation_list_empty_text;
mEmptyListMessageView.setContent(R.drawable.ic_home_menu_archive,
R.string.home_empty_archive_title,
R.string.home_empty_archive_body);
} else {
emptyListText = R.string.conversation_list_empty_text;
mEmptyListMessageView.setContent(R.drawable.ic_home_nav_messages,
R.string.home_empty_messages_title,
R.string.home_empty_messages_body);
}
mEmptyListMessageView.setTextHint(emptyListText);
mEmptyListMessageView.setReserveDock(!mArchiveMode && !mForwardMessageMode);
mEmptyListMessageView.setVisibility(View.VISIBLE);
mEmptyListMessageView.setIsImageVisible(true);
mEmptyListMessageView.setIsVerticallyCentered(true);
} else {
mEmptyListMessageView.setVisibility(View.GONE);
}
@@ -22,12 +22,17 @@ import android.database.Cursor;
import android.graphics.Color;
import android.graphics.Rect;
import android.graphics.Typeface;
import android.graphics.drawable.Drawable;
import android.net.Uri;
import android.support.v7.mms.pdu.ContentType;
import android.text.SpannableString;
import android.text.Spanned;
import android.text.StaticLayout;
import android.text.TextPaint;
import android.text.TextUtils;
import android.text.style.LeadingMarginSpan;
import android.telephony.PhoneNumberUtils;
import android.util.AttributeSet;
import android.util.TypedValue;
import android.view.View;
import android.view.View.OnClickListener;
import android.view.View.OnLayoutChangeListener;
@@ -39,7 +44,6 @@ import android.widget.ImageButton;
import android.widget.ImageView;
import android.widget.TextView;
import androidx.core.content.res.ResourcesCompat;
import androidx.core.text.BidiFormatter;
import androidx.core.text.TextDirectionHeuristicsCompat;
@@ -48,6 +52,7 @@ import com.android.messaging.R;
import com.android.messaging.datamodel.MessagingContentProvider;
import com.android.messaging.datamodel.action.UpdateConversationArchiveStatusAction;
import com.android.messaging.datamodel.data.ConversationListItemData;
import com.android.messaging.datamodel.data.ConversationMessageData;
import com.android.messaging.datamodel.data.MessageData;
import com.android.messaging.datamodel.media.UriImageRequestDescriptor;
import com.android.messaging.sms.MmsUtils;
@@ -59,6 +64,7 @@ import com.android.messaging.ui.SnackBarInteraction;
import com.android.messaging.util.Assert;
import com.android.messaging.util.ImageUtils;
import com.android.messaging.util.PhoneUtils;
import com.android.messaging.util.SimNameUtil;
import com.android.messaging.util.Typefaces;
import com.android.messaging.util.UiUtils;
import com.android.messaging.util.UriUtil;
@@ -72,8 +78,9 @@ import java.util.List;
*/
public class ConversationListItemView extends FrameLayout implements OnClickListener,
OnLongClickListener, OnLayoutChangeListener {
static final int UNREAD_SNIPPET_LINE_COUNT = 2;
static final int NO_UNREAD_SNIPPET_LINE_COUNT = 2;
static final int UNREAD_SNIPPET_LINE_COUNT = 3;
static final int NO_UNREAD_SNIPPET_LINE_COUNT = 3;
private static final int SNIPPET_ELLIPSIS_RESERVE_CHARS = 5;
private static final int LIST_DIVIDER_INSET_DEFAULT_DP = 74;
private static final int LIST_DIVIDER_INSET_SELECTION_DP = 104;
private int mListItemReadColor;
@@ -98,6 +105,8 @@ public class ConversationListItemView extends FrameLayout implements OnClickList
boolean shouldShowConversationPinButton();
boolean isConversationPinned(final String conversationId);
void onConversationPinButtonClicked(final String conversationId);
void onConversationSwipeMuteClicked(ConversationListItemData data);
void onConversationSwipeDeleteClicked(ConversationListItemData data);
}
private final OnClickListener fullScreenPreviewClickListener = new OnClickListener() {
@@ -145,7 +154,9 @@ public class ConversationListItemView extends FrameLayout implements OnClickList
private ImageView mContactCheckmarkView;
private ImageView mFailedStatusIconView;
private ImageView mCrossSwipeArchiveLeftImageView;
private ImageView mCrossSwipeArchiveRightImageView;
private View mCrossSwipeRightActions;
private ImageView mCrossSwipeMuteView;
private ImageView mCrossSwipeDeleteView;
private AsyncImageView mImagePreviewView;
private AudioAttachmentView mAudioAttachmentView;
private ImageView mChevronView;
@@ -184,7 +195,25 @@ public class ConversationListItemView extends FrameLayout implements OnClickList
mContactCheckmarkView = findViewById(R.id.conversation_checkmark);
mFailedStatusIconView = findViewById(R.id.conversation_failed_status_icon);
mCrossSwipeArchiveLeftImageView = findViewById(R.id.crossSwipeArchiveIconLeft);
mCrossSwipeArchiveRightImageView = findViewById(R.id.crossSwipeArchiveIconRight);
mCrossSwipeRightActions = findViewById(R.id.cross_swipe_right_actions);
mCrossSwipeMuteView = findViewById(R.id.cross_swipe_mute);
mCrossSwipeDeleteView = findViewById(R.id.cross_swipe_delete);
if (mCrossSwipeMuteView != null) {
mCrossSwipeMuteView.setOnClickListener(v -> {
if (mHostInterface != null) {
mHostInterface.onConversationSwipeMuteClicked(mData);
}
setSwipeTranslationX(0);
});
}
if (mCrossSwipeDeleteView != null) {
mCrossSwipeDeleteView.setOnClickListener(v -> {
if (mHostInterface != null) {
mHostInterface.onConversationSwipeDeleteClicked(mData);
}
setSwipeTranslationX(0);
});
}
mImagePreviewView = findViewById(R.id.conversation_image_preview);
mAudioAttachmentView = findViewById(R.id.audio_attachment_view);
mChevronView = findViewById(R.id.conversation_chevron);
@@ -219,12 +248,20 @@ public class ConversationListItemView extends FrameLayout implements OnClickList
private void setWorkProfileIcon() {
mWorkProfileIconView.setVisibility(mData.isEnterprise() ? View.VISIBLE : View.GONE);
if (mCategoryChipView != null) {
mCategoryChipView.setVisibility(View.VISIBLE);
mCategoryChipView.setText(mData.isEnterprise()
? R.string.home_chip_work
: R.string.home_chip_personal);
setSimBadge();
}
private void setSimBadge() {
if (mCategoryChipView == null) {
return;
}
if (!SimNameUtil.shouldShowSimName(getContext())) {
mCategoryChipView.setText(null);
mCategoryChipView.setVisibility(GONE);
return;
}
mCategoryChipView.setText(SimNameUtil.getSimDisplayName(getContext(), mData.getSelfId()));
mCategoryChipView.setVisibility(VISIBLE);
}
private void setConversationName() {
@@ -244,6 +281,8 @@ public class ConversationListItemView extends FrameLayout implements OnClickList
}
final String conversationName = mData.getName();
mConversationNameView.setTextSize(TypedValue.COMPLEX_UNIT_SP,
looksLikePhoneNumberName(conversationName) ? 18 : 17);
// For group conversations, ellipsize the group members that do not fit
final CharSequence ellipsizedName = UiUtils.commaEllipsize(
@@ -261,6 +300,23 @@ public class ConversationListItemView extends FrameLayout implements OnClickList
mConversationNameView.setText(bidiFormattedName);
}
private static boolean looksLikePhoneNumberName(final String name) {
if (TextUtils.isEmpty(name)) {
return false;
}
final String digits = PhoneNumberUtils.normalizeNumber(name);
if (digits.length() < 6) {
return false;
}
for (int i = 0; i < digits.length(); i++) {
final char c = digits.charAt(i);
if (c != '+' && !Character.isDigit(c)) {
return false;
}
}
return true;
}
private static String getPlusOneString() {
if (sPlusOneString == null) {
sPlusOneString = Factory.get().getApplicationContext().getResources()
@@ -278,20 +334,89 @@ public class ConversationListItemView extends FrameLayout implements OnClickList
}
private void setSubject() {
final String subjectText = mData.getShowDraft() ?
mData.getDraftSubject() :
MmsUtils.cleanseMmsSubject(getContext().getResources(), mData.getSubject());
if (!TextUtils.isEmpty(subjectText)) {
final String subjectPrepend = getResources().getString(R.string.subject_label);
mSubjectTextView.setText(TextUtils.concat(subjectPrepend, subjectText));
mSubjectTextView.setVisibility(VISIBLE);
} else {
if (mSubjectTextView != null) {
mSubjectTextView.setVisibility(GONE);
}
}
private void setSnippet() {
mSnippetTextView.setText(getSnippetText());
final String snippetText = ellipsizeSnippetForMaxLines(getSnippetText());
final int firstLineMargin = getSnippetLeadingMargin();
final CharSequence toSet;
if (firstLineMargin > 0 && !TextUtils.isEmpty(snippetText)) {
final SpannableString spanned = new SpannableString(snippetText);
spanned.setSpan(new LeadingMarginSpan.Standard(firstLineMargin, 0),
0, spanned.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
toSet = spanned;
} else {
toSet = snippetText;
}
if (TextUtils.equals(toSet, mSnippetTextView.getText())) {
return;
}
mSnippetTextView.setText(toSet);
}
private String ellipsizeSnippetForMaxLines(final String source) {
if (TextUtils.isEmpty(source)) {
return source;
}
final int maxLines = mSnippetTextView.getMaxLines();
final int width = mSnippetTextView.getWidth()
- mSnippetTextView.getPaddingLeft() - mSnippetTextView.getPaddingRight();
if (maxLines <= 0 || width <= 0) {
return source;
}
final int firstLineMargin = getSnippetLeadingMargin();
final CharSequence measured;
if (firstLineMargin > 0) {
final SpannableString spanned = new SpannableString(source);
spanned.setSpan(new LeadingMarginSpan.Standard(firstLineMargin, 0),
0, spanned.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
measured = spanned;
} else {
measured = source;
}
final StaticLayout layout = StaticLayout.Builder
.obtain(measured, 0, measured.length(), mSnippetTextView.getPaint(), width)
.setMaxLines(maxLines + 1)
.setEllipsize(null)
.setIncludePad(false)
.setLineSpacing(mSnippetTextView.getLineSpacingExtra(),
mSnippetTextView.getLineSpacingMultiplier())
.build();
if (layout.getLineCount() <= maxLines) {
return source;
}
final int last = maxLines - 1;
final int lineStart = layout.getLineStart(last);
int cut = layout.getLineEnd(last);
while (cut > lineStart && Character.isWhitespace(source.charAt(cut - 1))) {
cut--;
}
int reserved = 0;
while (cut > lineStart && reserved < SNIPPET_ELLIPSIS_RESERVE_CHARS) {
cut -= Character.charCount(Character.codePointBefore(source, cut));
reserved++;
}
while (cut > lineStart && Character.isWhitespace(source.charAt(cut - 1))) {
cut--;
}
if (cut <= 0) {
return "...";
}
return source.substring(0, cut) + "...";
}
private int getSnippetLeadingMargin() {
if (mCategoryChipView == null || mCategoryChipView.getVisibility() != VISIBLE) {
return 0;
}
mCategoryChipView.measure(
MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED),
MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED));
return mCategoryChipView.getMeasuredWidth()
+ Math.round(6f * getResources().getDisplayMetrics().density);
}
private void updateSelectionIndicator(final boolean selectionMode, final boolean isSelected,
@@ -562,16 +687,17 @@ public class ConversationListItemView extends FrameLayout implements OnClickList
color = getResources().getColor(R.color.home_text_snippet, getContext().getTheme());
mSnippetTextView.setMaxLines(maxLines);
mSnippetTextView.setEllipsize(TextUtils.TruncateAt.END);
mSnippetTextView.setTextColor(color);
mSnippetTextView.setTypeface(typeface, typefaceStyle);
mSubjectTextView.setTextColor(color);
mSubjectTextView.setTypeface(typeface, typefaceStyle);
mSnippetTextView.setVisibility(VISIBLE);
setWorkProfileIcon();
setSnippet();
setSubject();
setConversationName();
setWorkProfileIcon();
setContentDescription(buildContentDescription(resources, mData,
mConversationNameView.getPaint()));
@@ -599,11 +725,13 @@ public class ConversationListItemView extends FrameLayout implements OnClickList
mTimestampTextView.setText(resources.getString(
R.string.conversation_list_item_view_draft_message));
} else {
final int timestampColor = (mData.getIsRead() || mData.getShowDraft())
? resources.getColor(R.color.home_text_secondary, getContext().getTheme())
: color;
mTimestampTextView.setTextColor(timestampColor);
mTimestampTextView.setTypeface(mListItemReadTypeface, typefaceStyle);
final boolean unread = !mData.getIsRead();
mTimestampTextView.setTextColor(unread
? color
: resources.getColor(R.color.home_text_secondary, getContext().getTheme()));
mTimestampTextView.setTypeface(
unread ? mListItemUnreadTypeface : mListItemReadTypeface,
unread ? Typeface.BOLD : typefaceStyle);
final String formattedTimestamp = mData.getFormattedTimestamp();
if (mData.getIsSendRequested()) {
mTimestampTextView.setText(R.string.message_status_sending);
@@ -688,15 +816,11 @@ public class ConversationListItemView extends FrameLayout implements OnClickList
mAudioAttachmentView.setOnLongClickListener(this);
mAudioAttachmentView.setVisibility(audioPreviewVisiblity);
Drawable archiveDrawable = ResourcesCompat.getDrawable(
getResources(), R.drawable.ic_archive_small_dark, getContext().getTheme());
if (PrefsUtils.isSwipeRightToDeleteEnabled()) {
mCrossSwipeArchiveLeftImageView.setImageDrawable(ResourcesCompat.getDrawable(
getResources(), R.drawable.ic_delete_small_dark, getContext().getTheme()));
mCrossSwipeArchiveRightImageView.setImageDrawable(archiveDrawable);
} else {
mCrossSwipeArchiveLeftImageView.setImageDrawable(archiveDrawable);
mCrossSwipeArchiveRightImageView.setImageDrawable(archiveDrawable);
if (mCrossSwipeArchiveLeftImageView != null) {
mCrossSwipeArchiveLeftImageView.setImageResource(
PrefsUtils.isSwipeRightToDeleteEnabled()
? R.drawable.ic_delete_small_dark
: R.drawable.ic_archive_small_dark);
}
}
@@ -731,24 +855,72 @@ public class ConversationListItemView extends FrameLayout implements OnClickList
return mSwipeableContainer.getTranslationX();
}
public float getSwipeActionsRevealWidth() {
if (mCrossSwipeRightActions == null) {
return 0f;
}
if (mCrossSwipeRightActions.getWidth() <= 0) {
mCrossSwipeRightActions.measure(
MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED),
MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED));
return mCrossSwipeRightActions.getMeasuredWidth()
+ getResources().getDimensionPixelSize(R.dimen.looply_space_lg);
}
return mCrossSwipeRightActions.getWidth()
+ getResources().getDimensionPixelSize(R.dimen.looply_space_lg);
}
public boolean isSwipeActionsOpen() {
final float reveal = getSwipeActionsRevealWidth();
return reveal > 0 && getSwipeTranslationX() <= -reveal * 0.85f;
}
public boolean isTouchOnSwipeActions(final float parentX, final float parentY) {
if (!isSwipeActionsOpen() || mCrossSwipeRightActions == null
|| mCrossSwipeRightActions.getVisibility() != VISIBLE) {
return false;
}
final int[] parentLoc = new int[2];
getLocationOnScreen(parentLoc);
final int[] actionLoc = new int[2];
mCrossSwipeRightActions.getLocationOnScreen(actionLoc);
final float screenX = parentLoc[0] + parentX;
final float screenY = parentLoc[1] + parentY;
return screenX >= actionLoc[0]
&& screenX <= actionLoc[0] + mCrossSwipeRightActions.getWidth()
&& screenY >= actionLoc[1]
&& screenY <= actionLoc[1] + mCrossSwipeRightActions.getHeight();
}
public void setSwipeTranslationX(final float translationX) {
mSwipeableContainer.setTranslationX(translationX);
if (translationX == 0) {
mCrossSwipeBackground.setVisibility(View.GONE);
mCrossSwipeArchiveLeftImageView.setVisibility(GONE);
mCrossSwipeArchiveRightImageView.setVisibility(GONE);
if (mCrossSwipeArchiveLeftImageView != null) {
mCrossSwipeArchiveLeftImageView.setVisibility(GONE);
}
if (mCrossSwipeRightActions != null) {
mCrossSwipeRightActions.setVisibility(GONE);
}
mSwipeableContainer.setBackgroundColor(Color.TRANSPARENT);
} else {
mCrossSwipeBackground.setVisibility(View.VISIBLE);
if (translationX > 0) {
mCrossSwipeArchiveLeftImageView.setVisibility(VISIBLE);
mCrossSwipeArchiveRightImageView.setVisibility(GONE);
if (mCrossSwipeArchiveLeftImageView != null) {
mCrossSwipeArchiveLeftImageView.setVisibility(VISIBLE);
}
if (mCrossSwipeRightActions != null) {
mCrossSwipeRightActions.setVisibility(GONE);
}
} else {
mCrossSwipeArchiveLeftImageView.setVisibility(GONE);
mCrossSwipeArchiveRightImageView.setVisibility(VISIBLE);
if (mCrossSwipeArchiveLeftImageView != null) {
mCrossSwipeArchiveLeftImageView.setVisibility(GONE);
}
if (mCrossSwipeRightActions != null) {
mCrossSwipeRightActions.setVisibility(VISIBLE);
}
}
mSwipeableContainer.setBackgroundResource(R.color.window_background);
mSwipeableContainer.setBackgroundResource(R.color.home_bg);
}
}
@@ -835,6 +1007,11 @@ public class ConversationListItemView extends FrameLayout implements OnClickList
Assert.isTrue(v == mSwipeableContainer || v == mImagePreviewView);
Assert.notNull(mData.getName());
if (!isLongClick && isSwipeActionsOpen()) {
setSwipeTranslationX(0);
return true;
}
if (mHostInterface != null) {
mHostInterface.onConversationClicked(mData, isLongClick, this);
return true;
@@ -855,6 +1032,7 @@ public class ConversationListItemView extends FrameLayout implements OnClickList
mData.getDraftSnippetText() : mData.getSnippetText();
final String previewContentType = mData.getShowDraft() ?
mData.getDraftPreviewContentType() : mData.getPreviewContentType();
snippetText = ConversationMessageData.flattenPreviewText(snippetText);
if (TextUtils.isEmpty(snippetText)) {
Resources resources = getResources();
// Use the attachment type as a snippet so the preview doesn't look odd
@@ -63,8 +63,10 @@ public class ConversationListSwipeHelper implements OnItemTouchListener {
private VelocityTracker mVelocityTracker;
private float mInitialX;
private float mInitialY;
private float mStartTranslationX;
private boolean mIsSwiping;
private ConversationListItemView mListItemView;
private ConversationListItemView mOpenActionsItem;
public ConversationListSwipeHelper(final RecyclerView recyclerView) {
mRecyclerView = recyclerView;
@@ -106,9 +108,13 @@ public class ConversationListSwipeHelper implements OnItemTouchListener {
final View viewAtPoint = mRecyclerView.findChildViewUnder(mInitialX, mInitialY);
if (viewAtPoint instanceof ConversationListItemView) {
final ConversationListItemView child = (ConversationListItemView) viewAtPoint;
if (child.isSwipeAnimatable()) {
if (child.isTouchOnSwipeActions(mInitialX - child.getLeft(),
mInitialY - child.getTop())) {
mListItemView = null;
} else if (child.isSwipeAnimatable()) {
// Begin detecting swipe on the target for the rest of the gesture.
mListItemView = child;
mStartTranslationX = child.getSwipeTranslationX();
if (mListItemView.isAnimating()) {
mListItemView = null;
}
@@ -189,7 +195,8 @@ public class ConversationListSwipeHelper implements OnItemTouchListener {
case MotionEvent.ACTION_OUTSIDE:
case MotionEvent.ACTION_MOVE:
if (hasValidGestureSwipeTarget()) {
mListItemView.setSwipeTranslationX(event.getX() - mInitialX);
mListItemView.setSwipeTranslationX(clampTranslation(
mListItemView, mStartTranslationX + event.getX() - mInitialX));
}
break;
case MotionEvent.ACTION_UP:
@@ -211,17 +218,27 @@ public class ConversationListSwipeHelper implements OnItemTouchListener {
final boolean fastEnough = isTargetSwipedFastEnough();
final boolean farEnough = isTargetSwipedFarEnough();
final boolean shouldDismiss = (fastEnough || farEnough);
final boolean shouldRevealActions = swipeDirection == SWIPE_DIRECTION_LEFT
&& isTargetSwipedFarEnoughForActions();
final boolean shouldDismiss = swipeDirection == SWIPE_DIRECTION_RIGHT
&& (fastEnough || farEnough);
if (shouldDismiss) {
closeOpenActions(mListItemView);
if (fastEnough) {
animateDismiss(mListItemView, velocityX);
} else {
animateDismiss(mListItemView, swipeDirection);
}
} else if (shouldRevealActions) {
closeOpenActions(mListItemView);
animateRevealActions(mListItemView, velocityX);
mOpenActionsItem = mListItemView;
} else {
animateRestore(mListItemView, velocityX);
if (mOpenActionsItem == mListItemView) {
mOpenActionsItem = null;
}
}
onSwipeGestureEnd(mListItemView,
@@ -269,6 +286,51 @@ public class ConversationListSwipeHelper implements OnItemTouchListener {
mRecyclerView.getParent().requestDisallowInterceptTouchEvent(true);
setHardwareAnimatingLayerType(itemView, ANIMATING);
itemView.setAnimating(true);
closeOpenActions(itemView);
}
private void closeOpenActions(final ConversationListItemView keepOpen) {
if (mOpenActionsItem != null && mOpenActionsItem != keepOpen) {
animateRestore(mOpenActionsItem, 0f);
mOpenActionsItem = null;
}
}
private float clampTranslation(final ConversationListItemView itemView,
final float translationX) {
final float reveal = itemView.getSwipeActionsRevealWidth();
if (translationX < 0) {
return Math.max(translationX, -reveal * 1.2f);
}
return translationX;
}
private boolean isTargetSwipedFarEnoughForActions() {
final float translationX = mListItemView.getSwipeTranslationX();
final float reveal = mListItemView.getSwipeActionsRevealWidth();
return translationX < 0 && reveal > 0 && Math.abs(translationX) > reveal * 0.45f;
}
private void animateRevealActions(final ConversationListItemView itemView,
final float velocityX) {
onSwipeAnimationStart(itemView);
final float animateTo = -itemView.getSwipeActionsRevealWidth();
final long duration;
if (velocityX != 0) {
duration = calculateTranslationDuration(
animateTo - itemView.getSwipeTranslationX(), velocityX);
} else {
duration = mDefaultRestoreAnimationDuration;
}
final ObjectAnimator animator = getSwipeTranslationXAnimator(
itemView, animateTo, duration, UiUtils.DEFAULT_INTERPOLATOR);
animator.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(final Animator animation) {
onSwipeAnimationEnd(itemView);
}
});
animator.start();
}
/**
@@ -0,0 +1,105 @@
/*
* 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.content.Context;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.View;
import android.widget.FrameLayout;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
/**
* Full-screen attach overlay. Sheet/dock keep their own touches. A tap above the
* sheet dismisses it; a tap on the conversation compose field also asks for the IME.
*/
public class HomeAttachOverlayLayout extends FrameLayout {
public interface OutsideTapListener {
void onOutsideTap(boolean onComposeField);
}
@Nullable private View mSheet;
@Nullable private View mDock;
@Nullable private View mComposeField;
@Nullable private OutsideTapListener mOutsideTapListener;
private boolean mDownOutside;
public HomeAttachOverlayLayout(@NonNull final Context context) {
super(context);
}
public HomeAttachOverlayLayout(@NonNull final Context context,
@Nullable final AttributeSet attrs) {
super(context, attrs);
}
public void bind(@Nullable final View sheet, @Nullable final View dock,
@Nullable final View composeField, @Nullable final OutsideTapListener listener) {
mSheet = sheet;
mDock = dock;
mComposeField = composeField;
mOutsideTapListener = listener;
}
@Override
public boolean dispatchTouchEvent(final MotionEvent ev) {
final int action = ev.getActionMasked();
if (action == MotionEvent.ACTION_DOWN) {
mDownOutside = !isOnSheetOrDock(ev);
}
if (mDownOutside) {
if (action == MotionEvent.ACTION_UP && mOutsideTapListener != null) {
mOutsideTapListener.onOutsideTap(isOnComposeField(ev));
}
if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_CANCEL) {
mDownOutside = false;
}
return true;
}
return super.dispatchTouchEvent(ev);
}
private boolean isOnSheetOrDock(final MotionEvent ev) {
final float x = ev.getX();
final float y = ev.getY();
if (containsLocal(mDock, x, y)) {
return true;
}
return mSheet != null && mSheet.getVisibility() == VISIBLE && y >= mSheet.getY();
}
private boolean isOnComposeField(final MotionEvent ev) {
if (mComposeField == null || mComposeField.getVisibility() != VISIBLE) {
return false;
}
final int[] loc = new int[2];
mComposeField.getLocationInWindow(loc);
final float x = ev.getRawX();
final float y = ev.getRawY();
return x >= loc[0] && x < loc[0] + mComposeField.getWidth()
&& y >= loc[1] && y < loc[1] + mComposeField.getHeight();
}
private static boolean containsLocal(@Nullable final View view, final float x, final float y) {
if (view == null || view.getVisibility() != VISIBLE) {
return false;
}
return x >= view.getX() && x < view.getX() + view.getWidth()
&& y >= view.getY() && y < view.getY() + view.getHeight();
}
}
@@ -0,0 +1,41 @@
/*
* 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.os.Bundle;
import com.android.messaging.R;
import com.android.messaging.ui.BugleActionBarActivity;
/** Home Automation screen launched from the bottom nav. */
public class HomeAutomationActivity extends BugleActionBarActivity {
@Override
protected void onCreate(final Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.home_automation_activity);
if (getSupportActionBar() != null) {
getSupportActionBar().hide();
}
HomeSubHeader.bind(this, R.string.home_automation_title, v -> finish());
}
@Override
public void finish() {
super.finish();
overridePendingTransition(R.anim.home_activity_hold, R.anim.home_activity_close);
}
}
@@ -0,0 +1,58 @@
/*
* 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.os.Bundle;
import android.view.View;
import android.widget.ImageButton;
import com.android.messaging.R;
import com.android.messaging.ui.BugleActionBarActivity;
/** Sheet to create a new automation task. */
public class HomeAutomationCreateActivity extends BugleActionBarActivity {
@Override
protected void onCreate(final Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.home_automation_create_activity);
if (getSupportActionBar() != null) {
getSupportActionBar().hide();
}
final ImageButton back = findViewById(R.id.home_sub_header_back);
if (back != null) {
back.setVisibility(View.GONE);
}
final ImageButton close = findViewById(R.id.home_sub_header_close);
if (close != null) {
close.setVisibility(View.VISIBLE);
close.setOnClickListener(v -> finish());
}
HomeSubHeader.bind(this, R.string.home_automation_create_title, v -> finish());
final View scrim = findViewById(R.id.home_automation_create_scrim);
if (scrim != null) {
scrim.setOnClickListener(v -> finish());
}
}
@Override
public void finish() {
super.finish();
overridePendingTransition(R.anim.home_activity_hold, R.anim.home_sheet_close);
}
}
File diff suppressed because it is too large Load Diff
@@ -7,7 +7,6 @@ import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ListView;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
@@ -56,7 +55,7 @@ public class HomeContactsFragment extends Fragment implements ContactPickerDataL
private final LinkedHashMap<String, RecipientEntry> mSelected = new LinkedHashMap<>();
private HomeContactListAdapter mAdapter;
private GetOrCreateConversationActionMonitor mMonitor;
private TextView mEmptyView;
private HomeEmptyState mEmptyView;
private ContactListItemView mSendToView;
private Cursor mAllContactsCursor;
private String mFilterText;
@@ -271,10 +270,19 @@ public class HomeContactsFragment extends Fragment implements ContactPickerDataL
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);
if (filtering) {
mEmptyView.setContent(R.drawable.ic_home_nav_contacts,
R.string.home_empty_contacts_no_results_title,
R.string.home_empty_contacts_no_results_body);
} else if (ContactUtil.hasReadContactsPermission()) {
mEmptyView.setContent(R.drawable.ic_home_nav_contacts,
R.string.home_empty_contacts_title,
R.string.home_empty_contacts_body);
} else {
mEmptyView.setContent(R.drawable.ic_home_nav_contacts,
R.string.home_empty_contacts_permission_title,
R.string.home_empty_contacts_permission_body);
}
mEmptyView.setVisibility(View.VISIBLE);
}
}
@@ -30,7 +30,6 @@ 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;
@@ -61,8 +60,7 @@ public class HomeConversationSearchActivity extends BugleActionBarActivity
private EditText mSearchField;
private ImageButton mClearButton;
private ListView mListView;
private View mIdleContainer;
private TextView mEmptyView;
private HomeEmptyState mEmptyView;
private SearchResultAdapter mAdapter;
@Override
@@ -80,7 +78,6 @@ public class HomeConversationSearchActivity extends BugleActionBarActivity
}
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);
@@ -136,32 +133,25 @@ public class HomeConversationSearchActivity extends BugleActionBarActivity
}
private void updateEmptyView() {
if (mEmptyView == null || mListView == null) {
return;
}
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);
}
mEmptyView.setContent(R.drawable.ic_home_search_line,
R.string.home_empty_conversation_search_idle_title,
R.string.home_empty_conversation_search_idle_body);
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);
if (empty) {
mEmptyView.setContent(R.drawable.ic_home_search_line,
getString(R.string.home_empty_conversation_search_title),
getString(R.string.home_empty_conversation_search_body, searchText));
}
mEmptyView.setVisibility(empty ? View.VISIBLE : View.GONE);
mListView.setVisibility(empty ? View.GONE : View.VISIBLE);
}
@@ -170,6 +160,14 @@ public class HomeConversationSearchActivity extends BugleActionBarActivity
UIIntents.get().launchConversationActivity(this, mConversationId, null, null, false);
}
@Override
protected void onPause() {
if (mSearchField != null) {
ImeUtil.get().hideImeKeyboard(this, mSearchField);
}
super.onPause();
}
@Override
public void finish() {
if (mSearchField != null) {
@@ -0,0 +1,81 @@
/*
* 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.util.AttributeSet;
import android.view.Gravity;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.TextView;
import androidx.annotation.DrawableRes;
import androidx.annotation.StringRes;
import com.android.messaging.R;
/** Centered icon + title + body used when a home list has no records. */
public class HomeEmptyState extends LinearLayout {
private ImageView mIcon;
private TextView mTitle;
private TextView mBody;
public HomeEmptyState(final Context context) {
this(context, null);
}
public HomeEmptyState(final Context context, final AttributeSet attrs) {
this(context, attrs, 0);
}
public HomeEmptyState(final Context context, final AttributeSet attrs,
final int defStyleAttr) {
super(context, attrs, defStyleAttr);
setOrientation(VERTICAL);
setGravity(Gravity.CENTER);
inflate(context, R.layout.home_empty_state_content, this);
mIcon = findViewById(R.id.home_empty_icon);
mTitle = findViewById(R.id.home_empty_title);
mBody = findViewById(R.id.home_empty_body);
}
public void setContent(@DrawableRes final int iconRes, @StringRes final int titleRes,
@StringRes final int bodyRes) {
setContent(iconRes, getResources().getText(titleRes), getResources().getText(bodyRes));
}
public void setContent(@DrawableRes final int iconRes, final CharSequence title,
final CharSequence body) {
if (mIcon != null) {
mIcon.setImageResource(iconRes);
}
if (mTitle != null) {
mTitle.setText(title);
}
if (mBody != null) {
mBody.setText(body);
}
}
public void setReserveDock(final boolean reserve) {
final int bottom = reserve
? getResources().getDimensionPixelSize(R.dimen.home_empty_dock_padding)
: 0;
setPadding(getPaddingLeft(), getPaddingTop(), getPaddingRight(), bottom);
}
}
@@ -22,16 +22,16 @@ 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.VelocityTracker;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewConfiguration;
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;
@@ -63,6 +63,7 @@ public final class HomeGallerySheet {
}
private static final int SPAN_COUNT = 3;
private static final int PEEK_ROWS = 3;
private static final String[] PROJECTION = new String[] {
MediaStore.Files.FileColumns._ID,
MediaStore.Files.FileColumns.MEDIA_TYPE,
@@ -140,7 +141,7 @@ public final class HomeGallerySheet {
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 HomeEmptyState emptyState = content.findViewById(R.id.home_gallery_empty_state);
final View allow = content.findViewById(R.id.home_gallery_allow);
final int screenHeight = activity.getResources().getDisplayMetrics().heightPixels;
@@ -164,7 +165,8 @@ public final class HomeGallerySheet {
content.setTag(dismissAnimated);
scrim.setOnClickListener(v -> dismissAnimated.run());
bindDragToDismiss(header, scrim, sheet, dismissAnimated);
final float[] peekTranslation = new float[] { 0f };
bindHandleDrag(header, scrim, sheet, peekTranslation, dismissAnimated);
final Adapter adapter = new Adapter(uri -> {
dismissAnimated.run();
@@ -190,23 +192,22 @@ public final class HomeGallerySheet {
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);
if (emptyState != null) {
emptyState.setContent(R.drawable.ic_home_image_line,
R.string.home_empty_gallery_permission_title,
R.string.home_empty_gallery_permission_body);
}
allow.setVisibility(View.VISIBLE);
} else {
emptyMessage.setText(R.string.home_gallery_empty);
if (emptyState != null) {
emptyState.setContent(R.drawable.ic_home_image_line,
R.string.home_empty_gallery_title,
R.string.home_empty_gallery_body);
}
allow.setVisibility(View.GONE);
}
} else {
@@ -222,50 +223,65 @@ public final class HomeGallerySheet {
if (content.getViewTreeObserver().isAlive()) {
content.getViewTreeObserver().removeOnPreDrawListener(this);
}
peekTranslation[0] = resolvePeekTranslation(content, header, sheet);
final int height = sheet.getHeight() > 0 ? sheet.getHeight() : screenHeight;
sheet.setTranslationY(height);
if (scrim != null) {
scrim.setAlpha(0f);
}
HomeUiMotion.gallerySheetOpen(scrim, sheet);
HomeUiMotion.gallerySheetOpen(scrim, sheet, peekTranslation[0]);
return false;
}
});
}
private static void bindDragToDismiss(final View header, final View scrim, final View sheet,
final Runnable dismissAnimated) {
private static void bindHandleDrag(final View header, final View scrim, final View sheet,
final float[] peekTranslation, final Runnable dismissAnimated) {
header.setOnTouchListener(new View.OnTouchListener() {
private float mDownY;
private float mDownTranslation;
private boolean mDragging;
private VelocityTracker mTracker;
@Override
public boolean onTouch(final View v, final MotionEvent event) {
if (mTracker == null) {
mTracker = VelocityTracker.obtain();
}
mTracker.addMovement(event);
switch (event.getActionMasked()) {
case MotionEvent.ACTION_DOWN:
mDownY = event.getRawY();
mDownTranslation = sheet.getTranslationY();
mDragging = false;
sheet.animate().cancel();
return true;
case MotionEvent.ACTION_MOVE: {
final float dy = Math.max(0f, event.getRawY() - mDownY);
if (dy > 0f) {
final float dy = event.getRawY() - mDownY;
if (Math.abs(dy) > ViewConfiguration.get(v.getContext())
.getScaledTouchSlop()) {
mDragging = true;
}
sheet.setTranslationY(dy);
final int height = Math.max(sheet.getHeight(), 1);
scrim.setAlpha(1f - Math.min(1f, dy / height));
final float translation = clamp(mDownTranslation + dy, 0f, height);
sheet.setTranslationY(translation);
updateScrimForDrag(scrim, translation, peekTranslation[0], 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();
mTracker.computeCurrentVelocity(1000);
final float velocityY = mTracker.getYVelocity();
mTracker.recycle();
mTracker = null;
if (!mDragging && event.getActionMasked() == MotionEvent.ACTION_CANCEL) {
HomeUiMotion.gallerySheetSnap(sheet, peekTranslation[0]);
updateScrimForDrag(scrim, peekTranslation[0], peekTranslation[0],
Math.max(sheet.getHeight(), 1));
return true;
}
settleHandleDrag(sheet, scrim, peekTranslation[0], velocityY,
dismissAnimated);
return true;
}
default:
@@ -275,6 +291,59 @@ public final class HomeGallerySheet {
});
}
private static void settleHandleDrag(final View sheet, final View scrim,
final float peekY, final float velocityY, final Runnable dismissAnimated) {
final int height = Math.max(sheet.getHeight(), 1);
final float translation = sheet.getTranslationY();
final float dismissDistance = Math.max(height - peekY, 1f);
if (translation > peekY && (velocityY > 1400f
|| translation > peekY + dismissDistance * 0.22f)) {
dismissAnimated.run();
return;
}
final float target;
if (velocityY < -1400f || translation < peekY * 0.45f) {
target = 0f;
} else {
target = peekY;
}
HomeUiMotion.gallerySheetSnap(sheet, target);
if (scrim != null) {
scrim.animate().alpha(1f).setDuration(220).start();
}
}
private static void updateScrimForDrag(final View scrim, final float translation,
final float peekY, final int height) {
if (scrim == null) {
return;
}
if (translation <= peekY) {
scrim.setAlpha(1f);
return;
}
final float span = Math.max(height - peekY, 1f);
scrim.setAlpha(1f - Math.min(1f, (translation - peekY) / span));
}
private static float clamp(final float value, final float min, final float max) {
return Math.max(min, Math.min(max, value));
}
private static float resolvePeekTranslation(final View overlay, final View header,
final View sheet) {
final int sheetHeight = sheet.getHeight() > 0 ? sheet.getHeight() : overlay.getHeight();
final int headerHeight = header != null && header.getHeight() > 0
? header.getHeight() : overlay.getResources()
.getDimensionPixelSize(R.dimen.home_header_height);
final int gap = overlay.getResources().getDimensionPixelSize(R.dimen.home_gallery_grid_gap);
final int width = overlay.getWidth() > 0 ? overlay.getWidth()
: overlay.getResources().getDisplayMetrics().widthPixels;
final int cell = width / SPAN_COUNT;
final int peekHeight = headerHeight + PEEK_ROWS * (cell + gap * 2);
return Math.max(0f, sheetHeight - peekHeight);
}
private static int resolveSheetTop(@NonNull final Activity activity,
@NonNull final View host) {
final View conversationHeader = activity.findViewById(R.id.conversation_header);
@@ -375,7 +444,9 @@ public final class HomeGallerySheet {
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;
final int width = parent.getWidth() > 0 ? parent.getWidth()
: parent.getResources().getDisplayMetrics().widthPixels;
mThumbSize = width / SPAN_COUNT;
}
return new Holder(view);
}
@@ -33,7 +33,6 @@ 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;
@@ -59,6 +58,7 @@ 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.ImeUtil;
import com.android.messaging.util.NotificationsUtil;
import com.android.messaging.util.PhoneUtils;
import com.android.messaging.util.UiUtils;
@@ -78,8 +78,11 @@ public class HomeProfileActivity extends BugleActionBarActivity
private List<ParticipantData> mParticipants = new ArrayList<>();
private TextView mNameView;
private TextView mStatusView;
private LinearLayout mParticipantsContainer;
private TextView mParticipantsHeader;
private TextView mParticipantsSummary;
private boolean mParticipantsLoaded;
@Override
protected void onCreate(final Bundle savedInstanceState) {
@@ -95,22 +98,21 @@ public class HomeProfileActivity extends BugleActionBarActivity
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);
}
bindHeaderChrome();
mNameView = findViewById(R.id.home_profile_name);
mStatusView = findViewById(R.id.home_profile_status);
mParticipantsContainer = findViewById(R.id.home_profile_participants);
mParticipantsHeader = findViewById(R.id.home_profile_participants_header);
mParticipantsSummary = findViewById(R.id.home_profile_participants_summary);
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,
bindQuickAction(R.id.home_profile_action_add_contact, R.drawable.ic_person_add_light,
R.string.home_profile_action_add_contact, v -> onSecondaryQuickAction());
bindQuickAction(R.id.home_profile_action_message, R.drawable.ic_home_nav_messages,
R.string.home_profile_action_message, v -> openConversation());
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));
@@ -119,6 +121,19 @@ public class HomeProfileActivity extends BugleActionBarActivity
mBinding.getData().init(LoaderManager.getInstance(this), mBinding);
}
@Override
protected void onResume() {
super.onResume();
final View root = findViewById(android.R.id.content);
if (root != null) {
ImeUtil.get().hideImeKeyboard(this, root);
}
bindHeaderChrome();
if (mConversation != null) {
updateViews();
}
}
@Override
protected void onDestroy() {
super.onDestroy();
@@ -138,15 +153,37 @@ public class HomeProfileActivity extends BugleActionBarActivity
final List<ParticipantData> participants) {
mBinding.ensureBound(data);
mParticipants = participants;
mParticipantsLoaded = true;
updateViews();
}
private void bindHeaderChrome() {
final ImageButton back = findViewById(R.id.home_sub_header_back);
if (back != null) {
back.setOnClickListener(v -> finish());
}
final TextView headerTitle = findViewById(R.id.home_sub_header_title);
if (headerTitle != null) {
headerTitle.setText(R.string.home_profile_screen_title);
headerTitle.setVisibility(View.VISIBLE);
}
final ImageButton overflow = findViewById(R.id.home_sub_header_overflow);
if (overflow != null) {
overflow.setVisibility(View.VISIBLE);
overflow.setOnClickListener(this::showOverflowMenu);
}
}
private void updateViews() {
if (mConversation == null) {
return;
}
updateAvatars();
updateName();
if (!mParticipantsLoaded) {
return;
}
updateAvatars();
updateStatus();
updateQuickActions();
updateOptions();
updateParticipants();
@@ -191,6 +228,27 @@ public class HomeProfileActivity extends BugleActionBarActivity
mNameView.setText(name);
}
private void updateStatus() {
if (mStatusView == null) {
return;
}
if (mParticipants.size() > 1) {
mStatusView.setVisibility(View.GONE);
return;
}
final ParticipantData other = getOtherParticipant();
final boolean unregistered = other != null
&& !ContactUtil.isParticipantInAddressBook(this,
other.getContactId(), other.getLookupKey(),
other.getNormalizedDestination());
if (unregistered) {
mStatusView.setText(R.string.home_profile_status_unregistered);
mStatusView.setVisibility(View.VISIBLE);
} else {
mStatusView.setVisibility(View.GONE);
}
}
private void updateQuickActions() {
final boolean isGroup = mParticipants.size() > 1;
final View primary = findViewById(R.id.home_profile_action_call);
@@ -200,13 +258,17 @@ public class HomeProfileActivity extends BugleActionBarActivity
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());
bindQuickAction(R.id.home_profile_action_message, R.drawable.ic_home_nav_messages,
R.string.home_profile_action_message, v -> openConversation());
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,
bindQuickAction(R.id.home_profile_action_add_contact, R.drawable.ic_person_add_light,
R.string.home_profile_action_add_contact, v -> onSecondaryQuickAction());
bindQuickAction(R.id.home_profile_action_message, R.drawable.ic_home_nav_messages,
R.string.home_profile_action_message, v -> openConversation());
primary.setVisibility(getCallablePhoneNumber() == null ? View.GONE : View.VISIBLE);
final ParticipantData other = getOtherParticipant();
final boolean canAddContact = other != null
@@ -219,7 +281,7 @@ public class HomeProfileActivity extends BugleActionBarActivity
private void onPrimaryQuickAction() {
if (mParticipants.size() > 1) {
Toast.makeText(this, R.string.home_popover_coming_soon, Toast.LENGTH_SHORT).show();
HomeToast.show(this, R.string.home_popover_coming_soon);
return;
}
placeCall();
@@ -227,21 +289,26 @@ public class HomeProfileActivity extends BugleActionBarActivity
private void onSecondaryQuickAction() {
if (mParticipants.size() > 1) {
Toast.makeText(this, R.string.home_popover_coming_soon, Toast.LENGTH_SHORT).show();
HomeToast.show(this, R.string.home_popover_coming_soon);
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 View notifications = findViewById(R.id.home_profile_option_notifications);
HomeSettingsRow.bind(notifications, R.drawable.ic_home_bell,
getString(R.string.home_profile_option_notifications), null,
true, false, false, v -> openNotificationSettings(), null);
HomeSettingsRow.setValue(notifications,
getString(R.string.home_profile_option_notifications_value));
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));
HomeSettingsRow.bind(findViewById(R.id.home_profile_option_archive),
R.drawable.ic_home_menu_archive,
getString(archived ? R.string.home_profile_option_unarchive
: R.string.home_profile_option_move_archive),
null, true, false, false, v -> toggleArchived(archived), null);
final ParticipantData other = getOtherParticipant();
final View spamRow = findViewById(R.id.home_profile_option_spam);
@@ -250,28 +317,35 @@ public class HomeProfileActivity extends BugleActionBarActivity
} 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));
HomeSettingsRow.bind(spamRow, R.drawable.ic_home_menu_spam,
getString(blocked ? R.string.home_profile_option_unblock
: R.string.home_profile_option_spam),
blocked ? null : getString(R.string.home_profile_option_spam_subtitle),
true, false, false, v -> toggleBlocked(other, blocked), null);
HomeSettingsRow.setDestructive(spamRow, !blocked);
}
bindOption(R.id.home_profile_option_delete, R.drawable.ic_home_menu_trash,
R.string.home_profile_option_delete, true, v -> confirmDelete());
final View deleteRow = findViewById(R.id.home_profile_option_delete);
HomeSettingsRow.bind(deleteRow, R.drawable.ic_home_menu_trash,
getString(R.string.home_profile_option_delete), null,
true, false, false, v -> confirmDelete(), null);
HomeSettingsRow.setDestructive(deleteRow, true);
}
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));
mParticipantsHeader.setText(getString(R.string.home_profile_participants_title,
Math.max(count + 1, 2)));
if (mParticipantsSummary != null) {
mParticipantsSummary.setText(count <= 1
? getString(R.string.home_profile_participants_summary_one)
: getString(R.string.home_profile_participants_summary_many, count));
mParticipantsSummary.setVisibility(View.VISIBLE);
}
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);
addPeople.setVisibility(View.GONE);
}
mParticipantsContainer.removeAllViews();
@@ -286,8 +360,13 @@ public class HomeProfileActivity extends BugleActionBarActivity
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());
final TextView details = row.findViewById(R.id.home_profile_participant_details);
final boolean inBook = ContactUtil.isParticipantInAddressBook(this,
participant.getContactId(), participant.getLookupKey(),
participant.getNormalizedDestination());
details.setText(inBook
? participant.getDisplayDestination()
: getString(R.string.home_profile_status_unregistered));
row.setOnClickListener(v -> ContactUtil.openContactDetail(this,
participant.getContactId(), participant.getLookupKey(),
participant.getNormalizedDestination()));
@@ -308,19 +387,19 @@ public class HomeProfileActivity extends BugleActionBarActivity
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 void showOverflowMenu(final View anchor) {
final PopupMenu menu = new PopupMenu(this, anchor, Gravity.END);
menu.getMenu().add(R.string.home_profile_rename)
.setOnMenuItemClickListener(item -> {
showRenameDialog();
return true;
});
menu.show();
}
private void openConversation() {
UIIntents.get().launchConversationActivity(this, mConversationId, null);
finish();
}
private ParticipantData getOtherParticipant() {
@@ -25,7 +25,6 @@ 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;
@@ -114,9 +113,8 @@ public class HomeSearchActivity extends BugleActionBarActivity {
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());
tile.setOnClickListener(v -> HomeToast.show(this,
getString(R.string.home_search_coming_soon, getString(labelRes))));
}
private void updateClearButton() {
@@ -132,8 +130,8 @@ public class HomeSearchActivity extends BugleActionBarActivity {
return;
}
ImeUtil.get().hideImeKeyboard(this, mSearchField);
Toast.makeText(this, getString(R.string.home_search_coming_soon,
mSearchField.getText().toString()), Toast.LENGTH_SHORT).show();
HomeToast.show(this, getString(R.string.home_search_coming_soon,
mSearchField.getText().toString()));
}
@Override
@@ -0,0 +1,119 @@
package com.android.messaging.ui.home;
import android.content.res.ColorStateList;
import android.text.TextUtils;
import android.view.View;
import android.widget.CompoundButton;
import android.widget.ImageView;
import android.widget.TextView;
import androidx.annotation.DrawableRes;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.widget.SwitchCompat;
import com.android.messaging.R;
/** Binds a home-styled settings row (icon, title, optional subtitle, chevron or switch). */
public final class HomeSettingsRow {
private HomeSettingsRow() {
}
public static void bind(@NonNull final View row, @DrawableRes final int iconRes,
@NonNull final CharSequence title, @Nullable final CharSequence subtitle,
final boolean showChevron, final boolean showSwitch, final boolean switchOn,
@Nullable final View.OnClickListener clickListener,
@Nullable final CompoundButton.OnCheckedChangeListener switchListener) {
final ImageView icon = row.findViewById(R.id.home_settings_row_icon);
final TextView titleView = row.findViewById(R.id.home_settings_row_title);
final TextView subtitleView = row.findViewById(R.id.home_settings_row_subtitle);
final SwitchCompat toggle = row.findViewById(R.id.home_settings_row_switch);
final View chevron = row.findViewById(R.id.home_settings_row_chevron);
icon.setImageResource(iconRes);
icon.setImageTintList(ColorStateList.valueOf(
row.getContext().getColor(R.color.home_text_primary)));
titleView.setText(title);
if (subtitleView != null) {
if (TextUtils.isEmpty(subtitle)) {
subtitleView.setVisibility(View.GONE);
} else {
subtitleView.setVisibility(View.VISIBLE);
subtitleView.setText(subtitle);
}
}
if (chevron != null) {
chevron.setVisibility(showChevron ? View.VISIBLE : View.GONE);
}
if (toggle != null) {
toggle.setOnCheckedChangeListener(null);
toggle.setVisibility(showSwitch ? View.VISIBLE : View.GONE);
toggle.setChecked(switchOn);
toggle.setOnCheckedChangeListener(switchListener);
}
row.setOnClickListener(v -> {
if (showSwitch && toggle != null) {
toggle.toggle();
}
if (clickListener != null) {
clickListener.onClick(v);
}
});
}
public static void setValue(@NonNull final View row, @Nullable final CharSequence value) {
final TextView valueView = row.findViewById(R.id.home_settings_row_value);
if (valueView == null) {
return;
}
if (TextUtils.isEmpty(value)) {
valueView.setVisibility(View.GONE);
} else {
valueView.setVisibility(View.VISIBLE);
valueView.setText(value);
}
}
public static void setDestructive(@NonNull final View row, final boolean destructive) {
final int color = row.getContext().getColor(destructive
? R.color.home_destructive : R.color.home_text_primary);
final ColorStateList tint = ColorStateList.valueOf(color);
final ImageView icon = row.findViewById(R.id.home_settings_row_icon);
if (icon != null) {
icon.setImageTintList(tint);
}
final TextView titleView = row.findViewById(R.id.home_settings_row_title);
if (titleView != null) {
titleView.setTextColor(color);
}
final TextView subtitleView = row.findViewById(R.id.home_settings_row_subtitle);
if (subtitleView != null) {
subtitleView.setTextColor(row.getContext().getColor(destructive
? R.color.home_destructive : R.color.home_text_secondary));
}
}
public static void setEnabled(@NonNull final View row, final boolean enabled) {
row.setEnabled(enabled);
row.setAlpha(enabled ? 1f : 0.4f);
final SwitchCompat toggle = row.findViewById(R.id.home_settings_row_switch);
if (toggle != null) {
toggle.setEnabled(enabled);
}
}
public static void setSubtitle(@NonNull final View row, @Nullable final CharSequence subtitle) {
final TextView subtitleView = row.findViewById(R.id.home_settings_row_subtitle);
if (subtitleView == null) {
return;
}
if (TextUtils.isEmpty(subtitle)) {
subtitleView.setVisibility(View.GONE);
} else {
subtitleView.setVisibility(View.VISIBLE);
subtitleView.setText(subtitle);
}
}
}
@@ -23,7 +23,6 @@ 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;
@@ -41,7 +40,7 @@ import com.android.messaging.util.Assert;
public class HomeSpamFragment extends Fragment implements BlockedParticipantsDataListener {
private ListView mListView;
private TextView mEmptyView;
private HomeEmptyState mEmptyView;
private BlockedParticipantListAdapter mAdapter;
private final Binding<BlockedParticipantsData> mBinding = BindingBase.createBinding(this);
@@ -57,8 +56,11 @@ public class HomeSpamFragment extends Fragment implements BlockedParticipantsDat
mBinding.getData().init(LoaderManager.getInstance(this), mBinding);
final View activityRoot = requireActivity().findViewById(R.id.home_spam_empty);
if (activityRoot instanceof TextView) {
mEmptyView = (TextView) activityRoot;
if (activityRoot instanceof HomeEmptyState) {
mEmptyView = (HomeEmptyState) activityRoot;
mEmptyView.setContent(R.drawable.ic_home_menu_spam,
R.string.home_empty_spam_title,
R.string.home_empty_spam_body);
}
return root;
}
@@ -0,0 +1,136 @@
package com.android.messaging.ui.home;
import android.app.Activity;
import android.app.Application;
import android.content.Context;
import android.os.Bundle;
import android.text.TextUtils;
import android.view.View;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.annotation.StringRes;
import com.android.messaging.Factory;
import com.android.messaging.R;
import com.android.messaging.ui.SnackBar;
import com.android.messaging.ui.SnackBarManager;
import com.android.messaging.util.UiUtils;
import java.lang.ref.WeakReference;
/** Floating rounded toast used for every in-app toast and snackbar. */
public final class HomeToast {
private static final int SHORT_DURATION_MS = 2500;
private static final int LONG_DURATION_MS = 4000;
private static boolean sTracking;
private static WeakReference<Activity> sResumedActivity;
private HomeToast() {
}
public static void install(final Application application) {
if (application == null) {
return;
}
ensureTracking(application);
}
public static void show(final Context context, @StringRes final int messageId) {
if (context == null) {
return;
}
show(context, context.getString(messageId), Toast.LENGTH_SHORT);
}
public static void show(final Context context, @StringRes final int messageId,
final int duration) {
if (context == null) {
return;
}
show(context, context.getString(messageId), duration);
}
public static void show(final Context context, final CharSequence message) {
show(context, message, Toast.LENGTH_SHORT);
}
public static void show(final Context context, final CharSequence message, final int duration) {
if (context == null || TextUtils.isEmpty(message)) {
return;
}
Activity activity = UiUtils.getActivity(context);
if (activity == null) {
activity = getResumedActivity();
}
final View parent = activity != null ? activity.getWindow().getDecorView() : null;
if (parent != null && parent.getWindowToken() != null) {
final SnackBar.Builder builder = SnackBarManager.get()
.newBuilder(parent)
.setText(message.toString())
.setDuration(duration == Toast.LENGTH_LONG
? LONG_DURATION_MS : SHORT_DURATION_MS);
final View compose = activity.findViewById(R.id.message_compose_view_container);
if (compose != null && compose.isShown()) {
builder.withPlacement(SnackBar.Placement.above(compose));
}
builder.show();
return;
}
Toast.makeText(context.getApplicationContext(), message, duration).show();
}
private static Activity getResumedActivity() {
if (!sTracking) {
final Context appContext = Factory.get().getApplicationContext();
if (appContext instanceof Application) {
ensureTracking((Application) appContext);
}
}
return sResumedActivity != null ? sResumedActivity.get() : null;
}
private static void ensureTracking(final Application application) {
if (sTracking) {
return;
}
sTracking = true;
application.registerActivityLifecycleCallbacks(
new Application.ActivityLifecycleCallbacks() {
@Override
public void onActivityCreated(@NonNull Activity activity,
Bundle savedInstanceState) {
}
@Override
public void onActivityStarted(@NonNull Activity activity) {
}
@Override
public void onActivityResumed(@NonNull Activity activity) {
sResumedActivity = new WeakReference<>(activity);
}
@Override
public void onActivityPaused(@NonNull Activity activity) {
if (sResumedActivity != null && sResumedActivity.get() == activity) {
sResumedActivity = null;
}
}
@Override
public void onActivityStopped(@NonNull Activity activity) {
}
@Override
public void onActivitySaveInstanceState(@NonNull Activity activity,
@NonNull Bundle outState) {
}
@Override
public void onActivityDestroyed(@NonNull Activity activity) {
}
});
}
}
@@ -35,6 +35,7 @@ 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 HOME_TAB_MS = 280;
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;
@@ -108,6 +109,177 @@ public final class HomeUiMotion {
.start();
}
/** Slides the home nav active pill to a tab's bounds. */
public static void slideNavIndicator(final View indicator, final float x, final float y,
final int width, final int height, final boolean animate) {
if (indicator == null || width <= 0 || height <= 0) {
return;
}
final Object running = indicator.getTag(R.id.home_nav_indicator);
if (running instanceof ValueAnimator) {
((ValueAnimator) running).cancel();
indicator.setTag(R.id.home_nav_indicator, null);
}
indicator.animate().cancel();
indicator.setVisibility(View.VISIBLE);
if (!enabled() || !animate || indicator.getWidth() <= 0) {
final ViewGroup.LayoutParams params = indicator.getLayoutParams();
if (params != null) {
params.width = width;
params.height = height;
indicator.setLayoutParams(params);
}
indicator.setX(x);
indicator.setY(y);
return;
}
final ViewGroup.LayoutParams params = indicator.getLayoutParams();
final int startW = indicator.getWidth();
final int startH = indicator.getHeight();
final float startX = indicator.getX();
final float startY = indicator.getY();
final ValueAnimator animator = ValueAnimator.ofFloat(0f, 1f);
animator.setDuration(HOME_TAB_MS);
animator.setInterpolator(EASE);
animator.addUpdateListener(animation -> {
final float t = (Float) animation.getAnimatedValue();
if (params != null) {
params.width = Math.round(startW + (width - startW) * t);
params.height = Math.round(startH + (height - startH) * t);
indicator.setLayoutParams(params);
}
indicator.setX(startX + (x - startX) * t);
indicator.setY(startY + (y - startY) * t);
});
animator.addListener(new android.animation.AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(final android.animation.Animator animation) {
if (indicator.getTag(R.id.home_nav_indicator) == animator) {
indicator.setTag(R.id.home_nav_indicator, null);
}
}
});
indicator.setTag(R.id.home_nav_indicator, animator);
animator.start();
}
/** Crossfades two home pages with a short horizontal slide. */
public static void crossfadeHomePages(final View outgoing, final View incoming,
final boolean incomingFromEnd) {
if (incoming != null) {
incoming.animate().cancel();
incoming.setVisibility(View.VISIBLE);
}
if (!enabled()) {
if (incoming != null) {
incoming.setAlpha(1f);
incoming.setTranslationX(0f);
}
if (outgoing != null && outgoing != incoming) {
outgoing.animate().cancel();
outgoing.setVisibility(View.GONE);
outgoing.setAlpha(1f);
outgoing.setTranslationX(0f);
}
return;
}
final int slide = incoming != null
? Math.max(incoming.getWidth() / 10, incoming.getResources()
.getDimensionPixelSize(R.dimen.looply_space_xl))
: 0;
if (incoming != null) {
incoming.setAlpha(0f);
incoming.setTranslationX(incomingFromEnd ? slide : -slide);
beginLayer(incoming);
incoming.animate()
.alpha(1f)
.translationX(0f)
.setDuration(HOME_TAB_MS)
.setInterpolator(EASE)
.withEndAction(() -> endLayer(incoming))
.start();
}
if (outgoing != null && outgoing != incoming) {
outgoing.animate().cancel();
beginLayer(outgoing);
outgoing.animate()
.alpha(0f)
.translationX(incomingFromEnd ? -slide : slide)
.setDuration(HOME_TAB_MS)
.setInterpolator(EASE)
.withEndAction(() -> {
outgoing.setVisibility(View.GONE);
outgoing.setAlpha(1f);
outgoing.setTranslationX(0f);
endLayer(outgoing);
})
.start();
}
}
/** Fades a title, swaps the string, then fades back in. */
public static void fadeSwapText(final android.widget.TextView title, final CharSequence text) {
if (title == null) {
return;
}
if (!enabled() || title.getVisibility() != View.VISIBLE) {
title.setText(text);
return;
}
title.animate().cancel();
title.animate()
.alpha(0f)
.setDuration(HOME_TAB_MS / 2)
.setInterpolator(EASE)
.withEndAction(() -> {
title.setText(text);
title.animate()
.alpha(1f)
.setDuration(HOME_TAB_MS / 2)
.setInterpolator(EASE)
.start();
})
.start();
}
public static void fadeSwapViews(final View outgoing, final View incoming) {
if (incoming != null) {
incoming.animate().cancel();
incoming.setVisibility(View.VISIBLE);
}
if (!enabled()) {
if (incoming != null) {
incoming.setAlpha(1f);
}
if (outgoing != null && outgoing != incoming) {
outgoing.animate().cancel();
outgoing.setVisibility(View.GONE);
outgoing.setAlpha(1f);
}
return;
}
if (incoming != null) {
incoming.setAlpha(0f);
incoming.animate()
.alpha(1f)
.setDuration(HOME_TAB_MS)
.setInterpolator(EASE)
.start();
}
if (outgoing != null && outgoing != incoming) {
outgoing.animate().cancel();
outgoing.animate()
.alpha(0f)
.setDuration(HOME_TAB_MS)
.setInterpolator(EASE)
.withEndAction(() -> {
outgoing.setVisibility(View.GONE);
outgoing.setAlpha(1f);
})
.start();
}
}
/**
* Swaps two headers that share a vertical container, so they are never both laid out.
*/
@@ -751,6 +923,16 @@ public final class HomeUiMotion {
/** Slides the in-conversation gallery sheet up over a fading scrim. */
public static void gallerySheetOpen(final View scrim, final View sheet) {
gallerySheetOpen(scrim, sheet, 0f);
}
public static void gallerySheetOpen(final View scrim, final View sheet,
final float targetTranslationY) {
gallerySheetOpen(scrim, sheet, targetTranslationY, null);
}
public static void gallerySheetOpen(final View scrim, final View sheet,
final float targetTranslationY, final Runnable onUpdate) {
if (sheet == null) {
return;
}
@@ -764,29 +946,86 @@ public final class HomeUiMotion {
if (scrim != null) {
scrim.setAlpha(1f);
}
sheet.setTranslationY(0f);
sheet.setTranslationY(targetTranslationY);
if (onUpdate != null) {
onUpdate.run();
}
return;
}
if (scrim != null) {
scrim.setAlpha(0f);
}
sheet.setTranslationY(height);
if (onUpdate != null) {
onUpdate.run();
}
if (scrim != null) {
scrim.animate().alpha(1f).setDuration(GALLERY_SHEET_MS)
.setInterpolator(EASE_MATERIAL).start();
}
beginLayer(sheet);
sheet.animate()
.translationY(0f)
.translationY(targetTranslationY)
.setDuration(GALLERY_SHEET_MS)
.setInterpolator(EASE_MATERIAL)
.withEndAction(() -> endLayer(sheet))
.setUpdateListener(a -> {
if (onUpdate != null) {
onUpdate.run();
}
})
.withEndAction(() -> {
endLayer(sheet);
if (onUpdate != null) {
onUpdate.run();
}
})
.start();
}
public static void gallerySheetSnap(final View sheet, final float targetTranslationY) {
gallerySheetSnap(sheet, targetTranslationY, null);
}
public static void gallerySheetSnap(final View sheet, final float targetTranslationY,
final Runnable onUpdate) {
if (sheet == null) {
return;
}
sheet.animate().cancel();
if (!enabled()) {
sheet.setTranslationY(targetTranslationY);
if (onUpdate != null) {
onUpdate.run();
}
return;
}
beginLayer(sheet);
sheet.animate()
.translationY(targetTranslationY)
.setDuration(220)
.setInterpolator(EASE_MATERIAL)
.setUpdateListener(a -> {
if (onUpdate != null) {
onUpdate.run();
}
})
.withEndAction(() -> {
endLayer(sheet);
if (onUpdate != null) {
onUpdate.run();
}
})
.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) {
gallerySheetClose(scrim, sheet, endAction, null);
}
public static void gallerySheetClose(final View scrim, final View sheet,
final Runnable endAction, final Runnable onUpdate) {
final Runnable finish = once(endAction);
if (sheet == null) {
if (scrim != null) {
@@ -800,6 +1039,9 @@ public final class HomeUiMotion {
}
sheet.animate().cancel();
if (!enabled()) {
if (onUpdate != null) {
onUpdate.run();
}
finish.run();
return;
}
@@ -814,8 +1056,16 @@ public final class HomeUiMotion {
.translationY(height)
.setDuration(GALLERY_SHEET_MS)
.setInterpolator(EASE_MATERIAL)
.setUpdateListener(a -> {
if (onUpdate != null) {
onUpdate.run();
}
})
.withEndAction(() -> {
endLayer(sheet);
if (onUpdate != null) {
onUpdate.run();
}
finish.run();
})
.start();
@@ -0,0 +1,51 @@
package com.android.messaging.ui.home;
import android.content.Context;
import android.content.SharedPreferences;
import android.text.TextUtils;
import java.util.HashSet;
import java.util.Set;
/** Persists conversation ids whose alerts are hidden. */
public final class MutedConversationStore {
private static final String PREFS_NAME = "home_muted_conversations";
private static final String KEY_IDS = "muted_ids";
private MutedConversationStore() {
}
public static boolean isMuted(final Context context, final String conversationId) {
if (context == null || TextUtils.isEmpty(conversationId)) {
return false;
}
return ids(context).contains(conversationId);
}
public static boolean toggle(final Context context, final String conversationId) {
if (context == null || TextUtils.isEmpty(conversationId)) {
return false;
}
final Set<String> ids = new HashSet<>(ids(context));
final boolean muted;
if (ids.contains(conversationId)) {
ids.remove(conversationId);
muted = false;
} else {
ids.add(conversationId);
muted = true;
}
prefs(context).edit().putStringSet(KEY_IDS, ids).apply();
return muted;
}
private static Set<String> ids(final Context context) {
final Set<String> stored = prefs(context).getStringSet(KEY_IDS, null);
return stored == null ? new HashSet<>() : stored;
}
private static SharedPreferences prefs(final Context context) {
return context.getApplicationContext()
.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE);
}
}
@@ -0,0 +1,68 @@
/*
* 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.content.Context;
import android.content.SharedPreferences;
import android.text.TextUtils;
import java.util.HashSet;
import java.util.Set;
/** Local starred-message flags until a messages-table column exists. */
public final class StarredMessagesStore {
private static final String PREFS_NAME = "home_starred_messages";
private static final String KEY_IDS = "message_ids";
private StarredMessagesStore() {
}
public static boolean isStarred(final Context context, final String messageId) {
if (TextUtils.isEmpty(messageId)) {
return false;
}
return ids(context).contains(messageId);
}
public static boolean toggle(final Context context, final String messageId) {
if (TextUtils.isEmpty(messageId)) {
return false;
}
final Set<String> ids = new HashSet<>(ids(context));
final boolean starred;
if (ids.contains(messageId)) {
ids.remove(messageId);
starred = false;
} else {
ids.add(messageId);
starred = true;
}
prefs(context).edit().putStringSet(KEY_IDS, ids).apply();
return starred;
}
private static Set<String> ids(final Context context) {
final Set<String> stored = prefs(context).getStringSet(KEY_IDS, null);
return stored == null ? new HashSet<>() : stored;
}
private static SharedPreferences prefs(final Context context) {
return context.getApplicationContext().getSharedPreferences(PREFS_NAME,
Context.MODE_PRIVATE);
}
}
@@ -51,6 +51,10 @@ class CameraMediaChooser extends MediaChooser {
private final ActivityResultLauncher<String> mRequestPermissionLauncher;
private final DocumentImagePicker mDocumentImagePicker;
private Uri mOutputUri;
private static final int PENDING_NONE = 0;
private static final int PENDING_PICTURE = 1;
private static final int PENDING_VIDEO = 2;
private int mPendingLaunch = PENDING_NONE;
CameraMediaChooser(final MediaPicker mediaPicker) {
super(mediaPicker);
@@ -61,15 +65,17 @@ class CameraMediaChooser extends MediaChooser {
});
mPictureLauncher = mediaPicker.registerForActivityResult(
new ActivityResultContracts.TakePicture(), result -> {
if (result) {
final Uri output = mOutputUri;
if (result && output != null) {
List<Uri> list = new ArrayList<>();
list.add(mOutputUri);
list.add(output);
mDocumentImagePicker.onDocumentsPicked(list);
}
});
mVideoLauncher = mediaPicker.registerForActivityResult(
new ActivityResultContracts.CaptureVideo(), result -> {
if (result) {
final Uri output = mOutputUri;
if (result && output != null) {
final Rect startRect = new Rect();
// It's possible to throw out the chooser while taking the
// picture/video. In that case, still use the attachment, just
@@ -79,7 +85,7 @@ class CameraMediaChooser extends MediaChooser {
}
mMediaPicker.dispatchItemsSelected(
new MediaPickerMessagePartData(startRect, VIDEO_UNSPECIFIED,
mOutputUri, MessagingContentProvider.UNSPECIFIED_SIZE,
output, MessagingContentProvider.UNSPECIFIED_SIZE,
MessagingContentProvider.UNSPECIFIED_SIZE),
true /* dismissMediaPicker */);
}
@@ -87,6 +93,11 @@ class CameraMediaChooser extends MediaChooser {
mRequestPermissionLauncher = mediaPicker.registerForActivityResult(
new ActivityResultContracts.RequestPermission(), granted -> {
updateForPermissionState(granted);
if (granted) {
tryLaunchPending();
} else {
mPendingLaunch = PENDING_NONE;
}
});
}
@@ -114,19 +125,9 @@ class CameraMediaChooser extends MediaChooser {
mEnabledView = view.findViewById(R.id.mediapicker_enabled);
mMissingPermissionView = view.findViewById(R.id.missing_permission_view);
View takePictureView = view.findViewById(R.id.take_picture);
takePictureView.setOnClickListener(v -> {
mOutputUri = MediaScratchFileProvider.buildMediaScratchSpaceUri(null);
mPictureLauncher.launch(mOutputUri);
});
takePictureView.setOnClickListener(v -> launchTakePicture());
View takeVideoView = view.findViewById(R.id.take_video);
takeVideoView.setOnClickListener(v -> {
if (!OsUtil.hasRecordAudioPermission()) {
requestRecordAudioPermission();
} else {
mOutputUri = MediaScratchFileProvider.buildMediaScratchSpaceUri(null);
mVideoLauncher.launch(mOutputUri);
}
});
takeVideoView.setOnClickListener(v -> launchTakeVideo());
updateForPermissionState(hasCameraPermission());
return view;
}
@@ -151,6 +152,42 @@ class CameraMediaChooser extends MediaChooser {
}
}
void launchTakePicture() {
mPendingLaunch = PENDING_PICTURE;
tryLaunchPending();
}
void launchTakeVideo() {
mPendingLaunch = PENDING_VIDEO;
tryLaunchPending();
}
private void tryLaunchPending() {
if (mPendingLaunch == PENDING_NONE) {
return;
}
if (!hasCameraPermission()) {
requestCameraPermission();
return;
}
if (mPendingLaunch == PENDING_VIDEO && !OsUtil.hasRecordAudioPermission()) {
requestRecordAudioPermission();
return;
}
final int pending = mPendingLaunch;
mPendingLaunch = PENDING_NONE;
mOutputUri = MediaScratchFileProvider.buildMediaScratchSpaceUri(null);
try {
if (pending == PENDING_PICTURE) {
mPictureLauncher.launch(mOutputUri);
} else {
mVideoLauncher.launch(mOutputUri);
}
} catch (final RuntimeException e) {
mOutputUri = null;
}
}
private void requestCameraPermission() {
mRequestPermissionLauncher.launch(Manifest.permission.CAMERA);
}
@@ -65,6 +65,7 @@ public class DocumentImagePicker {
private final SelectionListener mListener;
private final ActivityResultLauncher<String[]> mPermissionLauncher;
private Runnable mAfterPermission;
/**
* Creates a new instance of DocumentImagePicker.
@@ -78,7 +79,17 @@ public class DocumentImagePicker {
mPermissionLauncher = mFragment.registerForActivityResult(
new ActivityResultContracts.RequestMultiplePermissions(),
result -> showGallerySheet());
result -> {
final Runnable after = mAfterPermission;
mAfterPermission = null;
if (after != null) {
after.run();
return;
}
if (OsUtil.hasMediaPermissions()) {
showGallerySheet();
}
});
}
/**
@@ -89,6 +100,7 @@ public class DocumentImagePicker {
if (activity == null) {
return;
}
mAfterPermission = null;
if (!OsUtil.hasMediaPermissions()) {
mPermissionLauncher.launch(OsUtil.getMediaPermissions());
return;
@@ -96,6 +108,17 @@ public class DocumentImagePicker {
showGallerySheet();
}
public void requestPermission(final Runnable onGranted) {
if (OsUtil.hasMediaPermissions()) {
if (onGranted != null) {
onGranted.run();
}
return;
}
mAfterPermission = onGranted;
mPermissionLauncher.launch(OsUtil.getMediaPermissions());
}
public void onDocumentsPicked(List<Uri> uris) {
for (Uri uri: uris) {
prepareDocumentForAttachment(uri);
@@ -186,6 +186,8 @@ public class MediaPicker extends Fragment implements DraftMessageSubscriptionDat
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 static final int OPEN_MODE_TAKE_PICTURE = 5;
private static final int OPEN_MODE_TAKE_VIDEO = 6;
private boolean mIsAttached;
private int mStartingMediaTypeOnAttach = MEDA_TYPE_INVALID;
@@ -203,6 +205,12 @@ public class MediaPicker extends Fragment implements DraftMessageSubscriptionDat
} else if (mDeferredOpenMode == OPEN_MODE_CONTACT) {
launchContactPickerWithoutPanel();
mDeferredOpenMode = OPEN_MODE_INVALID;
} else if (mDeferredOpenMode == OPEN_MODE_TAKE_PICTURE) {
launchCameraCaptureWithoutPanel(true);
mDeferredOpenMode = OPEN_MODE_INVALID;
} else if (mDeferredOpenMode == OPEN_MODE_TAKE_VIDEO) {
launchCameraCaptureWithoutPanel(false);
mDeferredOpenMode = OPEN_MODE_INVALID;
} else if (mDeferredOpenMode != OPEN_MODE_INVALID && mDeferredOpenMode != OPEN_MODE_DEFAULT) {
performOpenMode(mDeferredOpenMode, mAnimateOnAttach);
mDeferredOpenMode = OPEN_MODE_INVALID;
@@ -427,6 +435,38 @@ public class MediaPicker extends Fragment implements DraftMessageSubscriptionDat
}
}
/** Opens the system camera to take a photo without showing the in-app chooser panel. */
public void openTakePicture() {
if (mIsAttached) {
launchCameraCaptureWithoutPanel(true);
} else {
mDeferredOpenMode = OPEN_MODE_TAKE_PICTURE;
}
}
/** Opens the system camera to record video without showing the in-app chooser panel. */
public void openTakeVideo() {
if (mIsAttached) {
launchCameraCaptureWithoutPanel(false);
} else {
mDeferredOpenMode = OPEN_MODE_TAKE_VIDEO;
}
}
private void launchCameraCaptureWithoutPanel(final boolean picture) {
hideInAppPanel();
for (final MediaChooser chooser : mEnabledChoosers) {
if (chooser instanceof CameraMediaChooser) {
if (picture) {
((CameraMediaChooser) chooser).launchTakePicture();
} else {
((CameraMediaChooser) chooser).launchTakeVideo();
}
return;
}
}
}
private void openWithMode(final int openMode, final boolean animate) {
mOpen = true;
mDeferredOpenMode = openMode;
@@ -29,8 +29,6 @@ import android.text.TextUtils;
import android.view.Menu;
import android.view.MenuItem;
import android.widget.ShareActionProvider;
import android.widget.Toast;
import com.android.ex.photo.PhotoViewController;
import com.android.ex.photo.adapters.PhotoPagerAdapter;
import com.android.ex.photo.loaders.PhotoBitmapLoaderInterface.BitmapResult;
@@ -38,6 +36,7 @@ import com.android.messaging.R;
import com.android.messaging.datamodel.ConversationImagePartsView.PhotoViewQuery;
import com.android.messaging.datamodel.MediaScratchFileProvider;
import com.android.messaging.ui.conversation.ConversationFragment;
import com.android.messaging.ui.home.HomeToast;
import com.android.messaging.util.Dates;
import com.android.messaging.util.LogUtil;
@@ -153,7 +152,7 @@ public class BuglePhotoViewController extends PhotoViewController {
final Context context = getActivity().getContext();
final String error = context.getResources().getQuantityString(
R.plurals.attachment_save_error, 1, 1);
Toast.makeText(context, error, Toast.LENGTH_SHORT).show();
HomeToast.show(context, error);
return true;
}
final String photoUri = adapter.getPhotoUri(cursor);
@@ -95,10 +95,10 @@ public class ChangeDefaultSmsAppHelper {
} else {
UiUtils.showSnackBarWithCustomAction(activity,
rootView,
activity.getString(sending ? R.string.requires_default_sms_app_to_send :
R.string.requires_default_sms_app),
activity.getString(sending ? R.string.home_requires_default_sms_app_to_send :
R.string.home_requires_default_sms_app),
SnackBar.Action.createCustomAction(mChangeSmsAppSettingRunnable,
activity.getString(R.string.requires_default_sms_change_button)),
activity.getString(R.string.home_requires_default_sms_change)),
null /* interactions */,
SnackBar.Placement.above(composeView));
}
@@ -18,9 +18,12 @@
package com.android.messaging.util;
import android.Manifest;
import android.app.Activity;
import android.content.ContentUris;
import android.content.Context;
import android.database.Cursor;
import android.net.Uri;
import android.os.Looper;
import android.provider.ContactsContract;
import android.provider.ContactsContract.CommonDataKinds.Email;
import android.provider.ContactsContract.CommonDataKinds.Phone;
@@ -214,9 +217,12 @@ public class ContactUtil {
return false;
}
Uri lookupUri = null;
if (contactId > ParticipantData.PARTICIPANT_CONTACT_ID_NOT_RESOLVED
&& !TextUtils.isEmpty(contactLookupKey)) {
lookupUri = Contacts.getLookupUri(contactId, contactLookupKey);
if (contactId > ParticipantData.PARTICIPANT_CONTACT_ID_NOT_RESOLVED) {
if (!TextUtils.isEmpty(contactLookupKey)) {
lookupUri = Contacts.getLookupUri(contactId, contactLookupKey);
} else {
lookupUri = ContentUris.withAppendedId(Contacts.CONTENT_URI, contactId);
}
}
if (lookupUri == null && !TextUtils.isEmpty(normalizedDestination)) {
lookupUri = resolveLookupUriFromDestination(context, normalizedDestination);
@@ -243,6 +249,11 @@ public class ContactUtil {
|| !hasReadContactsPermission()) {
return false;
}
// Options menus and list binds run on the UI thread. A contacts lookup
// would trip Assert.isNotMainThread() on debug and ANR on release.
if (Looper.myLooper() == Looper.getMainLooper()) {
return false;
}
Cursor cursor = null;
try {
cursor = lookupDestination(context, normalizedDestination).performSynchronousQuery();
@@ -256,7 +267,7 @@ public class ContactUtil {
private static Uri resolveLookupUriFromDestination(final Context context,
final String normalizedDestination) {
if (!hasReadContactsPermission()) {
if (!hasReadContactsPermission() || Looper.myLooper() == Looper.getMainLooper()) {
return null;
}
Cursor cursor = null;
@@ -280,12 +291,25 @@ public class ContactUtil {
}
private static boolean startContactDetailActivity(final Context context, final Uri lookupUri) {
final Intent intent = new Intent(Intent.ACTION_VIEW, lookupUri);
final Intent intent = new Intent(Intent.ACTION_VIEW);
intent.setDataAndType(lookupUri, Contacts.CONTENT_ITEM_TYPE);
if (!(context instanceof Activity)) {
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
}
try {
context.startActivity(intent);
return true;
} catch (final ActivityNotFoundException e) {
return false;
final Intent fallback = new Intent(Intent.ACTION_VIEW, lookupUri);
if (!(context instanceof Activity)) {
fallback.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
}
try {
context.startActivity(fallback);
return true;
} catch (final ActivityNotFoundException ignored) {
return false;
}
}
}
@@ -72,16 +72,7 @@ public class Dates {
return getContext().getResources().getText(R.string.posted_just_now);
}
// Workaround for b/5657035. The platform method {@link DateUtils#getRelativeTimeSpan()}
// passes a null context to other platform methods. However, on some devices, this
// context is dereferenced when it shouldn't be and an NPE is thrown. We catch that
// here and use a slightly less precise time.
try {
return DateUtils.getRelativeTimeSpanString(time, now, DateUtils.MINUTE_IN_MILLIS,
DateUtils.FORMAT_ABBREV_RELATIVE).toString();
} catch (final NullPointerException npe) {
return getShortRelativeTimeSpanString(time);
}
return getShortRelativeTimeSpanString(time);
}
public static CharSequence getConversationTimeString(final long time) {
@@ -108,8 +99,7 @@ public class Dates {
} else {
flags = FORCE_12_HOUR;
}
return getOlderThanAYearTimestamp(time,
context.getResources().getConfiguration().getLocales().get(0),
return getOlderThanAYearTimestamp(time, Locale.US,
false /*abbreviated*/, flags);
}
@@ -123,8 +113,7 @@ public class Dates {
flags = FORCE_12_HOUR;
}
return getTimestamp(time, System.currentTimeMillis(), abbreviated,
context.getResources().getConfiguration().getLocales().get(0), flags,
minPeriodToday);
Locale.US, flags, minPeriodToday);
}
public static CharSequence getTimestamp(final long time, final long now,
@@ -162,17 +151,16 @@ public class Dates {
}
private static CharSequence getTodayTimeStamp(final long time, final int flags) {
return DateUtils.formatDateTime(getContext(), time,
DateUtils.FORMAT_SHOW_TIME | flags);
return getExplicitFormattedTime(time, flags, "HH:mm", "h:mm a");
}
private static CharSequence getExplicitFormattedTime(final long time, final int flags,
final String format24, final String format12) {
SimpleDateFormat formatter;
if ((flags & FORCE_24_HOUR) == FORCE_24_HOUR) {
formatter = new SimpleDateFormat(format24, Locale.getDefault());
formatter = new SimpleDateFormat(format24, Locale.US);
} else {
formatter = new SimpleDateFormat(format12, Locale.getDefault());
formatter = new SimpleDateFormat(format12, Locale.US);
}
return formatter.format(new Date(time));
}
@@ -72,10 +72,21 @@ public class ImeUtil {
final InputMethodManager inputMethodManager =
(InputMethodManager) context.getSystemService(Context.INPUT_METHOD_SERVICE);
if (inputMethodManager != null) {
v.requestFocus();
inputMethodManager.showSoftInput(v, 0 /* flags */);
if (inputMethodManager == null) {
return;
}
v.setFocusable(true);
v.setFocusableInTouchMode(true);
v.requestFocus();
final Runnable showKeyboard = () -> {
if (!v.hasFocus()) {
v.requestFocus();
}
inputMethodManager.restartInput(v);
inputMethodManager.showSoftInput(v, InputMethodManager.SHOW_FORCED);
};
v.post(showKeyboard);
v.postDelayed(showKeyboard, 120);
}
public static void hideSoftInput(@NonNull final Context context, @NonNull final View v) {
@@ -0,0 +1,104 @@
package com.android.messaging.util;
import android.content.Context;
import android.content.pm.ApplicationInfo;
import android.telephony.SubscriptionInfo;
import android.text.TextUtils;
import com.android.messaging.R;
import com.android.messaging.datamodel.data.ParticipantData;
import java.util.List;
public final class SimNameUtil {
private SimNameUtil() {
}
public static boolean shouldShowSimName(final Context context) {
if ((context.getApplicationInfo().flags & ApplicationInfo.FLAG_DEBUGGABLE) != 0) {
return true;
}
try {
return PhoneUtils.getDefault().getActiveSubscriptionCount() >= 2;
} catch (final Exception ignored) {
return false;
}
}
public static String getSimDisplayName(final Context context, final String selfId) {
List<SubscriptionInfo> infos;
try {
infos = PhoneUtils.getDefault().getActiveSubscriptionInfoList();
} catch (final Exception ignored) {
infos = null;
}
if (infos == null || infos.isEmpty()) {
return context.getString(R.string.sim_slot_identifier, "1");
}
SubscriptionInfo match = infos.get(0);
if (infos.size() > 1 && !TextUtils.isEmpty(selfId)) {
for (final SubscriptionInfo info : infos) {
final String participantId =
ParticipantData.getCachedParticipantId(info.getSubscriptionId());
if (TextUtils.equals(participantId, selfId)) {
match = info;
break;
}
}
}
return formatName(context, match);
}
private static final int NAME_SOURCE_USER_INPUT = 2;
private static String formatName(final Context context, final SubscriptionInfo info) {
final CharSequence displayName = info.getDisplayName();
if (isUserAssignedName(info) && !TextUtils.isEmpty(displayName)) {
return displayName.toString();
}
return context.getString(R.string.sim_slot_identifier,
String.valueOf(Math.max(0, info.getSimSlotIndex()) + 1));
}
private static boolean isUserAssignedName(final SubscriptionInfo info) {
try {
final Object source = SubscriptionInfo.class.getMethod("getNameSource").invoke(info);
if (source instanceof Integer) {
return ((Integer) source) == NAME_SOURCE_USER_INPUT;
}
} catch (final Exception ignored) {
}
final CharSequence displayName = info.getDisplayName();
return !TextUtils.isEmpty(displayName)
&& !isCarrierLike(displayName, info.getCarrierName())
&& !isKnownCarrierLabel(displayName);
}
private static boolean isCarrierLike(final CharSequence displayName,
final CharSequence carrierName) {
if (TextUtils.isEmpty(displayName) || TextUtils.isEmpty(carrierName)) {
return false;
}
final String display = normalizeLabel(displayName);
final String carrier = normalizeLabel(carrierName);
return display.equals(carrier) || carrier.contains(display) || display.contains(carrier);
}
private static boolean isKnownCarrierLabel(final CharSequence displayName) {
final String display = normalizeLabel(displayName);
return display.equals("lgu")
|| display.equals("lguplus")
|| display.equals("lgupluslte")
|| display.equals("kt")
|| display.equals("olleh")
|| display.equals("skt")
|| display.equals("sktelecom")
|| display.startsWith("lgu")
|| display.startsWith("skt")
|| display.startsWith("sktelecom");
}
private static String normalizeLabel(final CharSequence value) {
return value.toString().replaceAll("[\\s+\\-_.]", "").toLowerCase();
}
}
@@ -25,7 +25,6 @@ import android.graphics.Color;
import android.graphics.Rect;
import android.text.TextPaint;
import android.text.TextUtils;
import android.view.Gravity;
import android.view.View;
import android.view.View.OnLayoutChangeListener;
import android.view.animation.Animation;
@@ -40,6 +39,7 @@ import androidx.annotation.Nullable;
import com.android.messaging.Factory;
import com.android.messaging.R;
import com.android.messaging.ui.home.HomeToast;
import com.android.messaging.ui.SnackBar;
import com.android.messaging.ui.SnackBar.Placement;
import com.android.messaging.ui.SnackBarInteraction;
@@ -73,31 +73,24 @@ public class UiUtils {
/** Show a simple toast at the bottom */
public static void showToastAtBottom(final int messageId) {
UiUtils.showToastAtBottom(getApplicationContext().getString(messageId));
HomeToast.show(getApplicationContext(), messageId, Toast.LENGTH_LONG);
}
/** Show a simple toast at the bottom */
public static void showToastAtBottom(final String message) {
final Toast toast = Toast.makeText(getApplicationContext(), message, Toast.LENGTH_LONG);
toast.setGravity(Gravity.BOTTOM | Gravity.CENTER_HORIZONTAL, 0, 0);
toast.show();
HomeToast.show(getApplicationContext(), message, Toast.LENGTH_LONG);
}
/** Show a simple toast at the default position */
public static void showToast(final int messageId) {
final Toast toast = Toast.makeText(getApplicationContext(),
getApplicationContext().getString(messageId), Toast.LENGTH_LONG);
toast.setGravity(Gravity.CENTER_HORIZONTAL, 0, 0);
toast.show();
HomeToast.show(getApplicationContext(), messageId, Toast.LENGTH_LONG);
}
/** Show a simple toast at the default position */
public static void showToast(final int pluralsMessageId, final int count) {
final Toast toast = Toast.makeText(getApplicationContext(),
HomeToast.show(getApplicationContext(),
getApplicationContext().getResources().getQuantityString(pluralsMessageId, count),
Toast.LENGTH_LONG);
toast.setGravity(Gravity.CENTER_HORIZONTAL, 0, 0);
toast.show();
}
public static void showSnackBar(final Context context, @NonNull final View parentView,