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:
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+110
-13
@@ -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,
|
||||
|
||||
+109
-115
@@ -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();
|
||||
}
|
||||
|
||||
+179
-135
@@ -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;
|
||||
}
|
||||
|
||||
+38
@@ -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);
|
||||
}
|
||||
}
|
||||
+672
-47
@@ -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) {
|
||||
|
||||
+22
-1
@@ -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;
|
||||
}
|
||||
|
||||
+38
-7
@@ -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;
|
||||
|
||||
|
||||
+13
-5
@@ -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(
|
||||
|
||||
+35
-17
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
+4
-3
@@ -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() {
|
||||
|
||||
+677
-45
@@ -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);
|
||||
}
|
||||
|
||||
+221
-43
@@ -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
|
||||
|
||||
+66
-4
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
+21
-23
@@ -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;
|
||||
|
||||
+2
-3
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user