Messaging: Add scheduled send, location share, app lock, and settings polish
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.HomeAppLock;
|
||||
import com.android.messaging.ui.home.HomeToast;
|
||||
import com.android.messaging.util.BuglePrefsKeys;
|
||||
import com.android.messaging.util.LogUtil;
|
||||
@@ -58,6 +59,7 @@ public class BugleApplication extends Application implements UncaughtExceptionHa
|
||||
super.onCreate();
|
||||
|
||||
FactoryImpl.register(getApplicationContext(), this);
|
||||
HomeAppLock.install(this);
|
||||
HomeToast.install(this);
|
||||
|
||||
sSystemUncaughtExceptionHandler = Thread.getDefaultUncaughtExceptionHandler();
|
||||
|
||||
@@ -722,7 +722,9 @@ public class BugleNotifications {
|
||||
setWearableGroupOptions(notifBuilder, notificationState);
|
||||
|
||||
if (notificationState instanceof MultiMessageNotificationState) {
|
||||
if (attachmentBitmap != null) {
|
||||
if (attachmentBitmap != null
|
||||
&& notificationState.mNotificationStyle
|
||||
instanceof NotificationCompat.BigPictureStyle) {
|
||||
// When we've got a picture attachment, we do some switcheroo trickery. When
|
||||
// the notification is expanded, we show the picture as a bigPicture. The small
|
||||
// icon shows the sender's avatar. When that same notification is collapsed, the
|
||||
@@ -787,28 +789,34 @@ public class BugleNotifications {
|
||||
final int requestCode = multiMessageNotificationState.getReplyIntentRequestCode();
|
||||
final PendingIntent replyPendingIntent = UIIntents.get()
|
||||
.getPendingIntentForSendingMessageToConversation(context,
|
||||
conversationId, selfId, requiresMms, requestCode);
|
||||
conversationId, selfId, requiresMms,
|
||||
multiMessageNotificationState.mTitle, requestCode);
|
||||
|
||||
final int replyLabelRes = requiresMms ? R.string.notification_reply_via_mms :
|
||||
R.string.notification_reply_via_sms;
|
||||
|
||||
final NotificationCompat.Action.Builder actionBuilder =
|
||||
// Phone: open the Home bottom-sheet composer. Wear keeps RemoteInput.
|
||||
final NotificationCompat.Action replyAction =
|
||||
new NotificationCompat.Action.Builder(R.drawable.ic_wear_reply,
|
||||
context.getString(replyLabelRes), replyPendingIntent);
|
||||
final RemoteInput.Builder remoteInputBuilder = new RemoteInput.Builder(Intent.EXTRA_TEXT);
|
||||
remoteInputBuilder.setLabel(context.getString(R.string.notification_reply_prompt));
|
||||
actionBuilder.addRemoteInput(remoteInputBuilder.build());
|
||||
notifBuilder.addAction(actionBuilder.build());
|
||||
context.getString(R.string.home_notification_reply), replyPendingIntent)
|
||||
.setSemanticAction(NotificationCompat.Action.SEMANTIC_ACTION_REPLY)
|
||||
.setShowsUserInterface(true)
|
||||
.build();
|
||||
notifBuilder.addAction(replyAction);
|
||||
|
||||
// Support the action on a wearable device
|
||||
final NotificationCompat.Action.Builder wearActionBuilder =
|
||||
new NotificationCompat.Action.Builder(R.drawable.ic_wear_reply,
|
||||
context.getString(replyLabelRes), replyPendingIntent);
|
||||
final String[] choices = context.getResources().getStringArray(
|
||||
R.array.notification_reply_choices);
|
||||
remoteInputBuilder.setChoices(choices);
|
||||
wearActionBuilder.addRemoteInput(remoteInputBuilder.build());
|
||||
wearableExtender.addAction(wearActionBuilder.build());
|
||||
final RemoteInput wearRemoteInput = new RemoteInput.Builder(Intent.EXTRA_TEXT)
|
||||
.setLabel(context.getString(R.string.home_notification_reply_hint))
|
||||
.setAllowFreeFormInput(true)
|
||||
.setChoices(choices)
|
||||
.build();
|
||||
final int wearLabelRes = requiresMms ? R.string.notification_reply_via_mms :
|
||||
R.string.notification_reply_via_sms;
|
||||
wearableExtender.addAction(new NotificationCompat.Action.Builder(R.drawable.ic_wear_reply,
|
||||
context.getString(wearLabelRes), replyPendingIntent)
|
||||
.addRemoteInput(wearRemoteInput)
|
||||
.setSemanticAction(NotificationCompat.Action.SEMANTIC_ACTION_REPLY)
|
||||
.setShowsUserInterface(false)
|
||||
.build());
|
||||
}
|
||||
|
||||
private static void addDownloadMmsAction(final NotificationCompat.Builder notifBuilder,
|
||||
|
||||
@@ -120,6 +120,8 @@ public class FrequentContactsCursorBuilder {
|
||||
mAllContactsCursor.getInt(ContactUtil.INDEX_PHONE_EMAIL_TYPE);
|
||||
row[ContactUtil.INDEX_PHONE_EMAIL_LABEL] =
|
||||
mAllContactsCursor.getString(ContactUtil.INDEX_PHONE_EMAIL_LABEL);
|
||||
row[ContactUtil.INDEX_SORT_KEY] =
|
||||
mAllContactsCursor.getString(ContactUtil.INDEX_SORT_KEY);
|
||||
|
||||
boolean numberAlreadyAdded = false;
|
||||
for (Object[] oldRow : rows) {
|
||||
|
||||
@@ -46,7 +46,7 @@ public class MediaScratchFileProvider extends FileProvider {
|
||||
private static final SimpleArrayMap<Uri, String> sUriToDisplayNameMap = new SimpleArrayMap<>();
|
||||
|
||||
public static final String AUTHORITY =
|
||||
"com.bnkode.notie.datamodel.MediaScratchFileProvider";
|
||||
"com.filabcorp.msgq.datamodel.MediaScratchFileProvider";
|
||||
private static final String MEDIA_SCRATCH_SPACE_DIR = "mediascratchspace";
|
||||
|
||||
public static boolean isMediaScratchSpaceUri(final Uri uri) {
|
||||
|
||||
@@ -39,6 +39,7 @@ import androidx.core.app.NotificationCompat;
|
||||
import androidx.core.app.NotificationCompat.Builder;
|
||||
import androidx.core.app.NotificationCompat.WearableExtender;
|
||||
import androidx.core.app.NotificationManagerCompat;
|
||||
import androidx.core.app.Person;
|
||||
|
||||
import com.android.messaging.Factory;
|
||||
import com.android.messaging.R;
|
||||
@@ -494,85 +495,62 @@ public abstract class MessageNotificationState extends NotificationState {
|
||||
@Override
|
||||
protected NotificationCompat.Style build(final Builder builder) {
|
||||
builder.setContentTitle(mTitle)
|
||||
.setTicker(getTicker());
|
||||
.setTicker(getTicker())
|
||||
.setContentText(mContent);
|
||||
|
||||
NotificationCompat.Style notifStyle;
|
||||
final Context context = Factory.get().getApplicationContext();
|
||||
final ConversationLineInfo convInfo = mConvList.mConvInfos.get(0);
|
||||
final List<NotificationLineInfo> lineInfos = convInfo.mLineInfos;
|
||||
final int messageCount = lineInfos.size();
|
||||
// At this point, all the messages come from the same conversation. We need to load
|
||||
// the sender's avatar and then finish building the notification on a callback.
|
||||
|
||||
builder.setContentText(mContent); // for collapsed state
|
||||
|
||||
if (messageCount == 1) {
|
||||
final boolean shouldShowImage = ContentType.isImageType(mAttachmentType)
|
||||
|| (ContentType.isVideoType(mAttachmentType));
|
||||
if (mAttachmentUri != null && shouldShowImage) {
|
||||
// Show "Picture" as the content
|
||||
final MessageLineInfo messageLineInfo = (MessageLineInfo) lineInfos.get(0);
|
||||
String authorFirstName = messageLineInfo.mAuthorFirstName;
|
||||
|
||||
// For the collapsed state, just show "picture" unless this is a
|
||||
// group conversation. If it's a group, show the sender name and
|
||||
// "picture".
|
||||
final CharSequence tickerTag =
|
||||
BugleNotifications.formatAttachmentTag(authorFirstName,
|
||||
mAttachmentType);
|
||||
// For 1:1 notifications don't show first name in the notification, but
|
||||
// do show it in the ticker text
|
||||
CharSequence pictureTag = tickerTag;
|
||||
if (!convInfo.mIsGroup) {
|
||||
authorFirstName = null;
|
||||
pictureTag = BugleNotifications.formatAttachmentTag(authorFirstName,
|
||||
mAttachmentType);
|
||||
}
|
||||
builder.setContentText(pictureTag);
|
||||
builder.setTicker(tickerTag);
|
||||
|
||||
notifStyle = new NotificationCompat.BigPictureStyle(builder)
|
||||
.setSummaryText(BugleNotifications.formatInboxMessage(
|
||||
authorFirstName,
|
||||
null, null,
|
||||
null)); // expanded state, just show sender
|
||||
} else {
|
||||
notifStyle = new NotificationCompat.BigTextStyle(builder)
|
||||
.bigText(mContent);
|
||||
final Person user = new Person.Builder()
|
||||
.setName(context.getString(R.string.home_notification_you))
|
||||
.build();
|
||||
final NotificationCompat.MessagingStyle style =
|
||||
new NotificationCompat.MessagingStyle(user);
|
||||
if (convInfo.mIsGroup) {
|
||||
style.setGroupConversation(true);
|
||||
if (!TextUtils.isEmpty(convInfo.mGroupConversationName)) {
|
||||
style.setConversationTitle(convInfo.mGroupConversationName);
|
||||
}
|
||||
} else {
|
||||
// We've got multiple messages for the same sender.
|
||||
// Starting with the oldest new message, display the full text of each message.
|
||||
// Begin a line for each subsequent message.
|
||||
final SpannableStringBuilder buf = new SpannableStringBuilder();
|
||||
|
||||
for (int i = lineInfos.size() - 1; i >= 0; --i) {
|
||||
final NotificationLineInfo info = lineInfos.get(i);
|
||||
final MessageLineInfo messageLineInfo = (MessageLineInfo) info;
|
||||
mAttachmentUri = messageLineInfo.mAttachmentUri;
|
||||
mAttachmentType = messageLineInfo.mAttachmentType;
|
||||
CharSequence text = messageLineInfo.mText;
|
||||
if (!TextUtils.isEmpty(text) || mAttachmentUri != null) {
|
||||
if (convInfo.mIsGroup) {
|
||||
// append the bold name to the front of the message
|
||||
text = BugleNotifications.buildSpaceSeparatedMessage(
|
||||
messageLineInfo.mAuthorFullName, text, mAttachmentUri,
|
||||
mAttachmentType);
|
||||
} else {
|
||||
text = BugleNotifications.buildSpaceSeparatedMessage(
|
||||
null, text, mAttachmentUri, mAttachmentType);
|
||||
}
|
||||
buf.append(text);
|
||||
if (i > 0) {
|
||||
buf.append('\n');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Show a single notification -- big style with the text of all the messages
|
||||
notifStyle = new NotificationCompat.BigTextStyle(builder).bigText(buf);
|
||||
}
|
||||
|
||||
// MessagingStyle wants chronological order (oldest first). Latest is at index 0.
|
||||
for (int i = lineInfos.size() - 1; i >= 0; --i) {
|
||||
final NotificationLineInfo info = lineInfos.get(i);
|
||||
if (!(info instanceof MessageLineInfo)) {
|
||||
continue;
|
||||
}
|
||||
final MessageLineInfo messageLineInfo = (MessageLineInfo) info;
|
||||
CharSequence text = messageLineInfo.mText;
|
||||
if (TextUtils.isEmpty(text) && messageLineInfo.mAttachmentUri != null) {
|
||||
text = BugleNotifications.formatAttachmentTag(
|
||||
convInfo.mIsGroup ? messageLineInfo.mAuthorFirstName : null,
|
||||
messageLineInfo.mAttachmentType);
|
||||
}
|
||||
if (TextUtils.isEmpty(text)) {
|
||||
continue;
|
||||
}
|
||||
final Person.Builder personBuilder = new Person.Builder()
|
||||
.setName(!TextUtils.isEmpty(messageLineInfo.mAuthorFullName)
|
||||
? messageLineInfo.mAuthorFullName : mTitle);
|
||||
if (convInfo.mContactUri != null) {
|
||||
personBuilder.setUri(convInfo.mContactUri.toString());
|
||||
}
|
||||
final long timestamp = Math.max(0, convInfo.mReceivedTimestamp - i);
|
||||
final NotificationCompat.MessagingStyle.Message message =
|
||||
new NotificationCompat.MessagingStyle.Message(
|
||||
text, timestamp, personBuilder.build());
|
||||
if (messageLineInfo.mAttachmentUri != null
|
||||
&& !TextUtils.isEmpty(messageLineInfo.mAttachmentType)
|
||||
&& (ContentType.isImageType(messageLineInfo.mAttachmentType)
|
||||
|| ContentType.isVideoType(messageLineInfo.mAttachmentType))) {
|
||||
message.setData(messageLineInfo.mAttachmentType,
|
||||
messageLineInfo.mAttachmentUri);
|
||||
}
|
||||
style.addMessage(message);
|
||||
}
|
||||
|
||||
builder.setWhen(convInfo.mReceivedTimestamp);
|
||||
return notifStyle;
|
||||
return style;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -53,7 +53,7 @@ public class MessagingContentProvider extends ContentProvider {
|
||||
private static final String TAG = LogUtil.BUGLE_TAG;
|
||||
|
||||
public static final String AUTHORITY =
|
||||
"com.bnkode.notie.datamodel.MessagingContentProvider";
|
||||
"com.filabcorp.msgq.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.bnkode.notie.datamodel.MmsFileProvider";
|
||||
static final String AUTHORITY = "com.filabcorp.msgq.datamodel.MmsFileProvider";
|
||||
private static final String RAW_MMS_DIR = "rawmms";
|
||||
|
||||
/**
|
||||
|
||||
@@ -0,0 +1,236 @@
|
||||
package com.android.messaging.datamodel;
|
||||
|
||||
import android.app.AlarmManager;
|
||||
import android.app.PendingIntent;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.SharedPreferences;
|
||||
import android.net.Uri;
|
||||
import android.os.Build;
|
||||
import android.os.Parcel;
|
||||
import android.text.TextUtils;
|
||||
import android.util.Base64;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.WorkerThread;
|
||||
|
||||
import com.android.messaging.Factory;
|
||||
import com.android.messaging.datamodel.action.InsertNewMessageAction;
|
||||
import com.android.messaging.datamodel.data.MessageData;
|
||||
import com.android.messaging.receiver.ScheduledMessageReceiver;
|
||||
import com.android.messaging.util.LogUtil;
|
||||
|
||||
import org.json.JSONArray;
|
||||
import org.json.JSONObject;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
/** Persists compose-time scheduled sends and arms AlarmManager to deliver them. */
|
||||
public final class ScheduledMessageStore {
|
||||
private static final String PREFS = "home_scheduled_messages";
|
||||
private static final String KEY_ENTRIES = "entries";
|
||||
private static final String JSON_ID = "id";
|
||||
private static final String JSON_SEND_AT = "sendAt";
|
||||
private static final String JSON_PAYLOAD = "payload";
|
||||
|
||||
private ScheduledMessageStore() {
|
||||
}
|
||||
|
||||
public static void schedule(@NonNull final Context context, @NonNull final MessageData message,
|
||||
final long sendAt) {
|
||||
final Entry entry = new Entry();
|
||||
entry.id = UUID.randomUUID().toString();
|
||||
entry.sendAt = sendAt;
|
||||
entry.payload = encode(message);
|
||||
if (TextUtils.isEmpty(entry.payload)) {
|
||||
LogUtil.e(LogUtil.BUGLE_TAG, "Scheduled message encode failed");
|
||||
return;
|
||||
}
|
||||
final List<Entry> entries = loadLocked(context);
|
||||
entries.add(entry);
|
||||
saveLocked(context, entries);
|
||||
setAlarm(context, entry);
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
public static void sendNow(@NonNull final Context context, @Nullable final String id) {
|
||||
if (TextUtils.isEmpty(id)) {
|
||||
return;
|
||||
}
|
||||
Entry match = null;
|
||||
synchronized (ScheduledMessageStore.class) {
|
||||
final List<Entry> entries = loadLocked(context);
|
||||
for (int i = 0; i < entries.size(); i++) {
|
||||
if (id.equals(entries.get(i).id)) {
|
||||
match = entries.remove(i);
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (match != null) {
|
||||
saveLocked(context, entries);
|
||||
}
|
||||
}
|
||||
if (match == null) {
|
||||
return;
|
||||
}
|
||||
cancelAlarm(context, match.id);
|
||||
final MessageData message = decode(match.payload);
|
||||
if (message == null || !message.hasContent()) {
|
||||
LogUtil.e(LogUtil.BUGLE_TAG, "Scheduled message payload missing for " + id);
|
||||
return;
|
||||
}
|
||||
InsertNewMessageAction.insertNewMessage(message);
|
||||
}
|
||||
|
||||
public static void rescheduleAll(@NonNull final Context context) {
|
||||
final List<Entry> entries = loadLocked(context);
|
||||
final long now = System.currentTimeMillis();
|
||||
final List<Entry> due = new ArrayList<>();
|
||||
synchronized (ScheduledMessageStore.class) {
|
||||
final List<Entry> remaining = new ArrayList<>();
|
||||
for (final Entry entry : entries) {
|
||||
if (entry.sendAt <= now) {
|
||||
due.add(entry);
|
||||
} else {
|
||||
remaining.add(entry);
|
||||
setAlarm(context, entry);
|
||||
}
|
||||
}
|
||||
if (due.size() != entries.size()) {
|
||||
saveLocked(context, remaining);
|
||||
} else if (!due.isEmpty()) {
|
||||
saveLocked(context, new ArrayList<>());
|
||||
}
|
||||
}
|
||||
for (final Entry entry : due) {
|
||||
final MessageData message = decode(entry.payload);
|
||||
if (message != null && message.hasContent()) {
|
||||
InsertNewMessageAction.insertNewMessage(message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void setAlarm(@NonNull final Context context, @NonNull final Entry entry) {
|
||||
final AlarmManager alarms = context.getSystemService(AlarmManager.class);
|
||||
if (alarms == null) {
|
||||
return;
|
||||
}
|
||||
final PendingIntent pending = pendingIntent(context, entry.id, true);
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S && !alarms.canScheduleExactAlarms()) {
|
||||
alarms.setAndAllowWhileIdle(AlarmManager.RTC_WAKEUP, entry.sendAt, pending);
|
||||
return;
|
||||
}
|
||||
alarms.setExactAndAllowWhileIdle(AlarmManager.RTC_WAKEUP, entry.sendAt, pending);
|
||||
}
|
||||
|
||||
private static void cancelAlarm(@NonNull final Context context, @NonNull final String id) {
|
||||
final AlarmManager alarms = context.getSystemService(AlarmManager.class);
|
||||
if (alarms == null) {
|
||||
return;
|
||||
}
|
||||
final PendingIntent pending = pendingIntent(context, id, false);
|
||||
if (pending != null) {
|
||||
alarms.cancel(pending);
|
||||
}
|
||||
}
|
||||
|
||||
private static PendingIntent pendingIntent(@NonNull final Context context,
|
||||
@NonNull final String id, final boolean update) {
|
||||
final Intent intent = new Intent(context, ScheduledMessageReceiver.class);
|
||||
intent.setAction(ScheduledMessageReceiver.ACTION_SEND);
|
||||
intent.setData(Uri.parse("msgq-schedule:" + id));
|
||||
intent.putExtra(ScheduledMessageReceiver.EXTRA_ID, id);
|
||||
final int flags = PendingIntent.FLAG_IMMUTABLE
|
||||
| (update ? PendingIntent.FLAG_UPDATE_CURRENT : PendingIntent.FLAG_NO_CREATE);
|
||||
return PendingIntent.getBroadcast(context.getApplicationContext(), id.hashCode(),
|
||||
intent, flags);
|
||||
}
|
||||
|
||||
private static synchronized List<Entry> loadLocked(@NonNull final Context context) {
|
||||
final SharedPreferences prefs = prefs(context);
|
||||
final String raw = prefs.getString(KEY_ENTRIES, "[]");
|
||||
final List<Entry> entries = new ArrayList<>();
|
||||
try {
|
||||
final JSONArray array = new JSONArray(raw);
|
||||
for (int i = 0; i < array.length(); i++) {
|
||||
final JSONObject object = array.getJSONObject(i);
|
||||
final Entry entry = new Entry();
|
||||
entry.id = object.optString(JSON_ID);
|
||||
entry.sendAt = object.optLong(JSON_SEND_AT);
|
||||
entry.payload = object.optString(JSON_PAYLOAD);
|
||||
if (!TextUtils.isEmpty(entry.id) && !TextUtils.isEmpty(entry.payload)) {
|
||||
entries.add(entry);
|
||||
}
|
||||
}
|
||||
} catch (final Exception exception) {
|
||||
LogUtil.e(LogUtil.BUGLE_TAG, "Failed to load scheduled messages", exception);
|
||||
}
|
||||
return entries;
|
||||
}
|
||||
|
||||
private static synchronized void saveLocked(@NonNull final Context context,
|
||||
@NonNull final List<Entry> entries) {
|
||||
final JSONArray array = new JSONArray();
|
||||
try {
|
||||
for (final Entry entry : entries) {
|
||||
final JSONObject object = new JSONObject();
|
||||
object.put(JSON_ID, entry.id);
|
||||
object.put(JSON_SEND_AT, entry.sendAt);
|
||||
object.put(JSON_PAYLOAD, entry.payload);
|
||||
array.put(object);
|
||||
}
|
||||
prefs(context).edit().putString(KEY_ENTRIES, array.toString()).apply();
|
||||
} catch (final Exception exception) {
|
||||
LogUtil.e(LogUtil.BUGLE_TAG, "Failed to save scheduled messages", exception);
|
||||
}
|
||||
}
|
||||
|
||||
private static SharedPreferences prefs(@NonNull final Context context) {
|
||||
final Context app = context.getApplicationContext() != null
|
||||
? context.getApplicationContext()
|
||||
: Factory.get().getApplicationContext();
|
||||
return app.getSharedPreferences(PREFS, Context.MODE_PRIVATE);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private static String encode(@NonNull final MessageData message) {
|
||||
final Parcel parcel = Parcel.obtain();
|
||||
try {
|
||||
message.writeToParcel(parcel, 0);
|
||||
return Base64.encodeToString(parcel.marshall(), Base64.NO_WRAP);
|
||||
} catch (final Exception exception) {
|
||||
LogUtil.e(LogUtil.BUGLE_TAG, "Failed to encode scheduled message", exception);
|
||||
return null;
|
||||
} finally {
|
||||
parcel.recycle();
|
||||
}
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private static MessageData decode(@Nullable final String payload) {
|
||||
if (TextUtils.isEmpty(payload)) {
|
||||
return null;
|
||||
}
|
||||
final Parcel parcel = Parcel.obtain();
|
||||
try {
|
||||
final byte[] bytes = Base64.decode(payload, Base64.NO_WRAP);
|
||||
parcel.unmarshall(bytes, 0, bytes.length);
|
||||
parcel.setDataPosition(0);
|
||||
return MessageData.CREATOR.createFromParcel(parcel);
|
||||
} catch (final Exception exception) {
|
||||
LogUtil.e(LogUtil.BUGLE_TAG, "Failed to decode scheduled message", exception);
|
||||
return null;
|
||||
} finally {
|
||||
parcel.recycle();
|
||||
}
|
||||
}
|
||||
|
||||
private static final class Entry {
|
||||
String id;
|
||||
long sendAt;
|
||||
String payload;
|
||||
}
|
||||
}
|
||||
@@ -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.bnkode.notie.datamodel.PENDING_ACTION";
|
||||
static final String ACTION = "com.filabcorp.msgq.datamodel.PENDING_ACTION";
|
||||
|
||||
/**
|
||||
* Allocate an intent with a specific opcode and alarm action.
|
||||
|
||||
@@ -62,6 +62,7 @@ public class ContactPickerData extends BindableData implements
|
||||
private ContactPickerDataListener mListener;
|
||||
private final FrequentContactsCursorBuilder mFrequentContactsCursorBuilder;
|
||||
private String mSearchText;
|
||||
private ContactUtil.AccountFilter mAccountFilter;
|
||||
|
||||
public ContactPickerData(final Context context, final ContactPickerDataListener listener) {
|
||||
mListener = listener;
|
||||
@@ -82,7 +83,7 @@ public class ContactPickerData extends BindableData implements
|
||||
if (isBound(bindingId)) {
|
||||
switch (id) {
|
||||
case ALL_CONTACTS_LOADER:
|
||||
return ContactUtil.getPhones(mContext)
|
||||
return ContactUtil.getPhones(mContext, mAccountFilter)
|
||||
.createBoundCursorLoader(bindingId);
|
||||
case FREQUENT_CONTACTS_LOADER:
|
||||
return ContactUtil.getFrequentContacts(mContext)
|
||||
@@ -92,7 +93,7 @@ public class ContactPickerData extends BindableData implements
|
||||
MessagingContentProvider.PARTICIPANTS_URI,
|
||||
ParticipantData.ParticipantsQuery.PROJECTION, null, null, null);
|
||||
case FILTERED_CONTACTS_LOADER:
|
||||
return ContactUtil.filterDestination(mContext, mSearchText)
|
||||
return ContactUtil.filterDestination(mContext, mSearchText, mAccountFilter)
|
||||
.createBoundCursorLoader(bindingId);
|
||||
default:
|
||||
Assert.fail("Unknown loader id for contact picker!");
|
||||
@@ -217,6 +218,29 @@ public class ContactPickerData extends BindableData implements
|
||||
mLoaderManager.restartLoader(FILTERED_CONTACTS_LOADER, args, this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reloads the list for one contacts account, or every account when {@code filter} is null.
|
||||
*/
|
||||
public void setAccountFilter(final BindingBase<ContactPickerData> binding,
|
||||
final ContactUtil.AccountFilter filter) {
|
||||
if (mLoaderManager == null) {
|
||||
mAccountFilter = filter;
|
||||
return;
|
||||
}
|
||||
final boolean same = (filter == null && mAccountFilter == null)
|
||||
|| (filter != null && filter.matches(mAccountFilter));
|
||||
if (same) {
|
||||
return;
|
||||
}
|
||||
mAccountFilter = filter;
|
||||
final Bundle args = new Bundle();
|
||||
args.putString(BINDING_ID, binding.getBindingId());
|
||||
mLoaderManager.restartLoader(ALL_CONTACTS_LOADER, args, this);
|
||||
if (mSearchText != null) {
|
||||
mLoaderManager.restartLoader(FILTERED_CONTACTS_LOADER, args, this);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void unregisterListeners() {
|
||||
mListener = null;
|
||||
|
||||
@@ -22,6 +22,7 @@ import android.content.Intent;
|
||||
|
||||
import com.android.messaging.BugleApplication;
|
||||
import com.android.messaging.Factory;
|
||||
import com.android.messaging.datamodel.ScheduledMessageStore;
|
||||
import com.android.messaging.datamodel.action.UpdateMessageNotificationAction;
|
||||
import com.android.messaging.util.BuglePrefsKeys;
|
||||
import com.android.messaging.util.LogUtil;
|
||||
@@ -40,6 +41,7 @@ public class BootAndPackageReplacedReceiver extends BroadcastReceiver {
|
||||
UpdateMessageNotificationAction.updateMessageNotification();
|
||||
|
||||
BugleApplication.updateAppConfig(context);
|
||||
ScheduledMessageStore.rescheduleAll(context);
|
||||
} else {
|
||||
LogUtil.i(LogUtil.BUGLE_TAG, "BootAndPackageReplacedReceiver got unexpected action: "
|
||||
+ intent.getAction());
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
package com.android.messaging.receiver;
|
||||
|
||||
import android.content.BroadcastReceiver;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.text.TextUtils;
|
||||
|
||||
import com.android.messaging.datamodel.ScheduledMessageStore;
|
||||
|
||||
import java.util.concurrent.Executors;
|
||||
|
||||
/** Fires a previously scheduled compose message when its AlarmManager time is reached. */
|
||||
public class ScheduledMessageReceiver extends BroadcastReceiver {
|
||||
public static final String ACTION_SEND = "com.filabcorp.msgq.SEND_SCHEDULED_MESSAGE";
|
||||
public static final String EXTRA_ID = "scheduled_id";
|
||||
|
||||
@Override
|
||||
public void onReceive(final Context context, final Intent intent) {
|
||||
if (intent == null || !ACTION_SEND.equals(intent.getAction())) {
|
||||
return;
|
||||
}
|
||||
final String id = intent.getStringExtra(EXTRA_ID);
|
||||
if (TextUtils.isEmpty(id)) {
|
||||
return;
|
||||
}
|
||||
final PendingResult pending = goAsync();
|
||||
Executors.newSingleThreadExecutor().execute(() -> {
|
||||
try {
|
||||
ScheduledMessageStore.sendNow(context.getApplicationContext(), id);
|
||||
} finally {
|
||||
pending.finish();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -38,13 +38,13 @@ import com.android.messaging.util.LogUtil;
|
||||
*/
|
||||
public class SendStatusReceiver extends BroadcastReceiver {
|
||||
public static final String MESSAGE_SENT_ACTION =
|
||||
"com.bnkode.notie.receiver.SendStatusReceiver.MESSAGE_SENT";
|
||||
"com.filabcorp.msgq.receiver.SendStatusReceiver.MESSAGE_SENT";
|
||||
public static final String MESSAGE_DELIVERED_ACTION =
|
||||
"com.bnkode.notie.receiver.SendStatusReceiver.MESSAGE_DELIVERED";
|
||||
"com.filabcorp.msgq.receiver.SendStatusReceiver.MESSAGE_DELIVERED";
|
||||
public static final String MMS_SENT_ACTION =
|
||||
"com.bnkode.notie.receiver.SendStatusReceiver.MMS_SENT";
|
||||
"com.filabcorp.msgq.receiver.SendStatusReceiver.MMS_SENT";
|
||||
public static final String MMS_DOWNLOADED_ACTION =
|
||||
"com.bnkode.notie.receiver.SendStatusReceiver.MMS_DOWNLOADED";
|
||||
"com.filabcorp.msgq.receiver.SendStatusReceiver.MMS_DOWNLOADED";
|
||||
|
||||
// Defined by platform, but no constant provided. See docs for SmsManager.sendTextMessage.
|
||||
public static final String EXTRA_ERROR_CODE = "errorCode";
|
||||
|
||||
@@ -18,9 +18,23 @@ package com.android.messaging.ui;
|
||||
import android.content.Intent;
|
||||
import android.os.Bundle;
|
||||
import android.telephony.TelephonyManager;
|
||||
import android.text.Editable;
|
||||
import android.text.TextUtils;
|
||||
import android.text.TextWatcher;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.inputmethod.EditorInfo;
|
||||
import android.view.inputmethod.InputMethodManager;
|
||||
import android.widget.EditText;
|
||||
import android.widget.ImageButton;
|
||||
|
||||
import androidx.core.app.RemoteInput;
|
||||
|
||||
import com.android.messaging.R;
|
||||
import com.android.messaging.datamodel.NoConfirmationSmsSendService;
|
||||
import com.android.messaging.ui.home.HomeBottomSheet;
|
||||
import com.android.messaging.util.LogUtil;
|
||||
import com.google.android.material.bottomsheet.BottomSheetDialog;
|
||||
|
||||
public class RemoteInputEntrypointActivity extends BaseBugleActivity {
|
||||
private static final String TAG = LogUtil.BUGLE_TAG;
|
||||
@@ -37,22 +51,111 @@ public class RemoteInputEntrypointActivity extends BaseBugleActivity {
|
||||
return;
|
||||
}
|
||||
|
||||
// Perform some action depending on the intent
|
||||
String action = intent.getAction();
|
||||
if (Intent.ACTION_SENDTO.equals(action)) {
|
||||
// Build and send the intent
|
||||
final Intent sendIntent = new Intent(this, NoConfirmationSmsSendService.class);
|
||||
sendIntent.setAction(TelephonyManager.ACTION_RESPOND_VIA_MESSAGE);
|
||||
sendIntent.putExtras(intent);
|
||||
// Wear apparently passes all of its extras via the clip data. Must pass it along.
|
||||
sendIntent.setClipData(intent.getClipData());
|
||||
startService(sendIntent);
|
||||
setResult(RESULT_OK);
|
||||
} else {
|
||||
if (!Intent.ACTION_SENDTO.equals(action)) {
|
||||
LogUtil.w(TAG, "Unrecognized intent action: " + action);
|
||||
setResult(RESULT_CANCELED);
|
||||
finish();
|
||||
return;
|
||||
}
|
||||
// This activity should never stick around after processing the intent
|
||||
|
||||
final CharSequence remoteText = readRemoteInputText(intent);
|
||||
if (!TextUtils.isEmpty(remoteText)) {
|
||||
sendReply(intent, remoteText.toString());
|
||||
setResult(RESULT_OK);
|
||||
finish();
|
||||
return;
|
||||
}
|
||||
|
||||
showReplySheet(intent);
|
||||
}
|
||||
|
||||
private static CharSequence readRemoteInputText(final Intent intent) {
|
||||
final Bundle remoteInput = RemoteInput.getResultsFromIntent(intent);
|
||||
if (remoteInput != null) {
|
||||
return remoteInput.getCharSequence(Intent.EXTRA_TEXT);
|
||||
}
|
||||
return intent.getCharSequenceExtra(Intent.EXTRA_TEXT);
|
||||
}
|
||||
|
||||
private void showReplySheet(final Intent sourceIntent) {
|
||||
final View custom = LayoutInflater.from(this)
|
||||
.inflate(R.layout.home_notification_reply_sheet, null, false);
|
||||
final EditText input = custom.findViewById(R.id.home_notification_reply_input);
|
||||
final ImageButton send = custom.findViewById(R.id.home_notification_reply_send);
|
||||
if (send != null) {
|
||||
send.setEnabled(false);
|
||||
send.setAlpha(0.4f);
|
||||
}
|
||||
if (input != null && send != null) {
|
||||
input.addTextChangedListener(new TextWatcher() {
|
||||
@Override
|
||||
public void beforeTextChanged(final CharSequence s, final int start,
|
||||
final int count, final int after) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onTextChanged(final CharSequence s, final int start, final int before,
|
||||
final int count) {
|
||||
final boolean hasText = !TextUtils.isEmpty(s.toString().trim());
|
||||
send.setEnabled(hasText);
|
||||
send.setAlpha(hasText ? 1f : 0.4f);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void afterTextChanged(final Editable s) {
|
||||
}
|
||||
});
|
||||
input.setOnEditorActionListener((v, actionId, event) -> {
|
||||
if (actionId == EditorInfo.IME_ACTION_SEND) {
|
||||
submitReply(sourceIntent, input);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
send.setOnClickListener(v -> submitReply(sourceIntent, input));
|
||||
}
|
||||
|
||||
CharSequence title = sourceIntent.getStringExtra(UIIntents.UI_INTENT_EXTRA_CONVERSATION_NAME);
|
||||
if (TextUtils.isEmpty(title)) {
|
||||
title = getString(R.string.home_notification_reply);
|
||||
}
|
||||
final BottomSheetDialog dialog = HomeBottomSheet.builder(this)
|
||||
.title(title)
|
||||
.custom(custom)
|
||||
.show();
|
||||
dialog.setOnDismissListener(d -> finish());
|
||||
if (input != null) {
|
||||
input.post(() -> {
|
||||
input.requestFocus();
|
||||
final InputMethodManager imm =
|
||||
(InputMethodManager) getSystemService(INPUT_METHOD_SERVICE);
|
||||
if (imm != null) {
|
||||
imm.showSoftInput(input, InputMethodManager.SHOW_IMPLICIT);
|
||||
}
|
||||
});
|
||||
}
|
||||
setResult(RESULT_OK);
|
||||
}
|
||||
|
||||
private void submitReply(final Intent sourceIntent, final EditText input) {
|
||||
if (input == null) {
|
||||
return;
|
||||
}
|
||||
final String message = input.getText() == null ? "" : input.getText().toString().trim();
|
||||
if (TextUtils.isEmpty(message)) {
|
||||
return;
|
||||
}
|
||||
sendReply(sourceIntent, message);
|
||||
finish();
|
||||
}
|
||||
|
||||
private void sendReply(final Intent sourceIntent, final String message) {
|
||||
final Intent sendIntent = new Intent(this, NoConfirmationSmsSendService.class);
|
||||
sendIntent.setAction(TelephonyManager.ACTION_RESPOND_VIA_MESSAGE);
|
||||
sendIntent.putExtras(sourceIntent);
|
||||
sendIntent.putExtra(Intent.EXTRA_TEXT, message);
|
||||
sendIntent.setClipData(sourceIntent.getClipData());
|
||||
startService(sendIntent);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,6 +26,7 @@ import android.graphics.Rect;
|
||||
import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
|
||||
import androidx.activity.result.ActivityResultLauncher;
|
||||
import androidx.fragment.app.Fragment;
|
||||
|
||||
import com.android.messaging.Factory;
|
||||
@@ -69,7 +70,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.bnkode.notie.reset_notifications";
|
||||
"com.filabcorp.msgq.reset_notifications";
|
||||
|
||||
// Sending VCard uri to VCard detail activity
|
||||
public static final String UI_INTENT_EXTRA_VCARD_URI = "vcard_uri";
|
||||
@@ -102,13 +103,18 @@ public abstract class UIIntents {
|
||||
// Sending attachment content type from widget
|
||||
public static final String UI_INTENT_EXTRA_ATTACHMENT_TYPE = "attachment_type";
|
||||
|
||||
public static final String UI_INTENT_EXTRA_LOCATION_TEXT = "location_text";
|
||||
public static final String UI_INTENT_EXTRA_LOCATION_IMAGE_URI = "location_image_uri";
|
||||
|
||||
public static final String ACTION_WIDGET_CONVERSATION =
|
||||
"com.bnkode.notie.widget_conversation:";
|
||||
"com.filabcorp.msgq.widget_conversation:";
|
||||
|
||||
public static final String UI_INTENT_EXTRA_REQUIRES_MMS = "requires_mms";
|
||||
|
||||
public static final String UI_INTENT_EXTRA_SELF_ID = "self_id";
|
||||
|
||||
public static final String UI_INTENT_EXTRA_CONVERSATION_NAME = "conversation_name";
|
||||
|
||||
// Message position to scroll to.
|
||||
public static final String UI_INTENT_EXTRA_MESSAGE_POSITION = "message_position";
|
||||
|
||||
@@ -220,6 +226,12 @@ public abstract class UIIntents {
|
||||
*/
|
||||
public abstract void launchHomeAutomationCreateActivity(final Context context);
|
||||
|
||||
/**
|
||||
* Launch home UI to pick a location to share in compose.
|
||||
*/
|
||||
public abstract void launchHomeLocationActivity(final Activity activity,
|
||||
final ActivityResultLauncher<Intent> launcher);
|
||||
|
||||
/**
|
||||
* Launch an activity to show a class zero message
|
||||
*/
|
||||
@@ -315,7 +327,8 @@ public abstract class UIIntents {
|
||||
*/
|
||||
public abstract PendingIntent getPendingIntentForSendingMessageToConversation(
|
||||
final Context context, final String conversationId, final String selfId,
|
||||
final boolean requiresMms, final int requestCode);
|
||||
final boolean requiresMms, final CharSequence conversationName,
|
||||
final int requestCode);
|
||||
|
||||
/**
|
||||
* Get a PendingIntent for clearing notifications.
|
||||
|
||||
@@ -35,6 +35,7 @@ import android.provider.MediaStore;
|
||||
import android.support.v7.mms.pdu.ContentType;
|
||||
import android.text.TextUtils;
|
||||
|
||||
import androidx.activity.result.ActivityResultLauncher;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.core.app.TaskStackBuilder;
|
||||
import androidx.localbroadcastmanager.content.LocalBroadcastManager;
|
||||
@@ -248,6 +249,19 @@ public class UIIntentsImpl extends UIIntents {
|
||||
context.startActivity(intent);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void launchHomeLocationActivity(final Activity activity,
|
||||
final ActivityResultLauncher<Intent> launcher) {
|
||||
final Intent intent = new Intent(activity,
|
||||
com.android.messaging.ui.home.HomeLocationActivity.class);
|
||||
if (launcher != null) {
|
||||
launcher.launch(intent);
|
||||
} else {
|
||||
activity.startActivity(intent);
|
||||
}
|
||||
activity.overridePendingTransition(R.anim.home_activity_open, R.anim.home_activity_hold);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void launchHomeProfileActivity(final Activity activity, final String conversationId) {
|
||||
final Intent intent = new Intent(activity,
|
||||
@@ -396,7 +410,7 @@ public class UIIntentsImpl extends UIIntents {
|
||||
@Override
|
||||
public PendingIntent getPendingIntentForSendingMessageToConversation(final Context context,
|
||||
final String conversationId, final String selfId, final boolean requiresMms,
|
||||
final int requestCode) {
|
||||
final CharSequence conversationName, final int requestCode) {
|
||||
final Intent intent = new Intent(context, RemoteInputEntrypointActivity.class);
|
||||
intent.setAction(Intent.ACTION_SENDTO);
|
||||
// Ensure that the platform doesn't reuse PendingIntents across conversations
|
||||
@@ -404,6 +418,10 @@ public class UIIntentsImpl extends UIIntents {
|
||||
intent.putExtra(UIIntents.UI_INTENT_EXTRA_CONVERSATION_ID, conversationId);
|
||||
intent.putExtra(UIIntents.UI_INTENT_EXTRA_SELF_ID, selfId);
|
||||
intent.putExtra(UIIntents.UI_INTENT_EXTRA_REQUIRES_MMS, requiresMms);
|
||||
if (!TextUtils.isEmpty(conversationName)) {
|
||||
intent.putExtra(UIIntents.UI_INTENT_EXTRA_CONVERSATION_NAME,
|
||||
conversationName.toString());
|
||||
}
|
||||
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
|
||||
return getPendingIntentWithParentStack(context, intent, requestCode);
|
||||
}
|
||||
|
||||
+159
-33
@@ -24,24 +24,37 @@ import android.content.Intent;
|
||||
import android.os.Bundle;
|
||||
import android.provider.Settings;
|
||||
import android.text.TextUtils;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.widget.LinearLayout;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.activity.result.ActivityResultLauncher;
|
||||
import androidx.activity.result.contract.ActivityResultContracts;
|
||||
|
||||
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.HomeAppLock;
|
||||
import com.android.messaging.ui.home.HomeBottomSheet;
|
||||
import com.android.messaging.ui.home.HomeSecuritySettingsActivity;
|
||||
import com.android.messaging.ui.home.HomeSettingsRow;
|
||||
import com.android.messaging.ui.home.HomeSubHeader;
|
||||
import com.android.messaging.ui.home.HomeToast;
|
||||
import com.android.messaging.util.BuglePrefs;
|
||||
import com.android.messaging.util.ChangeDefaultSmsAppHelper;
|
||||
import com.android.messaging.util.PhoneUtils;
|
||||
import com.google.android.material.bottomsheet.BottomSheetDialog;
|
||||
|
||||
public class ApplicationSettingsActivity extends BugleActionBarActivity {
|
||||
|
||||
private static final String LANGUAGE_SYSTEM = "system";
|
||||
private static final String LANGUAGE_EN = "en";
|
||||
private static final String LANGUAGE_KO = "ko";
|
||||
|
||||
private View mSmsRow;
|
||||
private View mLanguageRow;
|
||||
private String mLanguageTag = LANGUAGE_SYSTEM;
|
||||
private ChangeDefaultSmsAppHelper mChangeDefaultSmsAppHelper;
|
||||
private final ActivityResultLauncher<Intent> mLauncher = registerForActivityResult(
|
||||
new ActivityResultContracts.StartActivityForResult(), result -> {
|
||||
@@ -64,6 +77,9 @@ public class ApplicationSettingsActivity extends BugleActionBarActivity {
|
||||
HomeSubHeader.bind(this, R.string.home_settings_title, v -> finish());
|
||||
|
||||
mSmsRow = findViewById(R.id.home_settings_sms);
|
||||
mLanguageRow = findViewById(R.id.home_settings_language);
|
||||
mLanguageTag = BuglePrefs.getApplicationPrefs().getString(
|
||||
getString(R.string.language_pref_key), LANGUAGE_SYSTEM);
|
||||
bindStaticRows();
|
||||
updateSmsRow();
|
||||
}
|
||||
@@ -72,6 +88,7 @@ public class ApplicationSettingsActivity extends BugleActionBarActivity {
|
||||
protected void onResume() {
|
||||
super.onResume();
|
||||
updateSmsRow();
|
||||
bindAppLockRow();
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -82,8 +99,9 @@ public class ApplicationSettingsActivity extends BugleActionBarActivity {
|
||||
|
||||
private void bindStaticRows() {
|
||||
final BuglePrefs prefs = BuglePrefs.getApplicationPrefs();
|
||||
final boolean topLevel = getIntent().getBooleanExtra(
|
||||
UIIntents.UI_INTENT_EXTRA_TOP_LEVEL_SETTINGS, false);
|
||||
|
||||
bindComingSoon(R.id.home_settings_premium, R.drawable.ic_home_star_line,
|
||||
R.string.home_settings_premium, R.string.home_settings_premium_subtitle);
|
||||
|
||||
HomeSettingsRow.bind(findViewById(R.id.home_settings_notifications),
|
||||
R.drawable.ic_home_bell,
|
||||
@@ -96,6 +114,36 @@ public class ApplicationSettingsActivity extends BugleActionBarActivity {
|
||||
startActivity(intent);
|
||||
}, null);
|
||||
|
||||
bindRow(R.id.home_settings_sms_mms, R.drawable.ic_home_nav_messages,
|
||||
R.string.home_settings_sms_mms, R.string.home_settings_sms_mms_subtitle,
|
||||
v -> {
|
||||
startActivity(UIIntents.get().getAdvancedSettingsIntent(this));
|
||||
overridePendingTransition(R.anim.home_activity_open, R.anim.home_activity_hold);
|
||||
});
|
||||
|
||||
bindComingSoon(R.id.home_settings_appearance, R.drawable.ic_home_edit,
|
||||
R.string.home_settings_appearance, R.string.home_settings_appearance_subtitle);
|
||||
|
||||
bindLanguageRow();
|
||||
bindAppLockRow();
|
||||
|
||||
final String automationKey = getString(R.string.automation_only_pref_key);
|
||||
final boolean automationOnly = prefs.getBoolean(automationKey,
|
||||
getResources().getBoolean(R.bool.automation_only_pref_default));
|
||||
HomeSettingsRow.bind(findViewById(R.id.home_settings_automation_only),
|
||||
R.drawable.ic_home_nav_automate,
|
||||
getString(R.string.home_settings_automation_only),
|
||||
getString(R.string.home_settings_automation_only_subtitle),
|
||||
false, true, automationOnly,
|
||||
null, (button, checked) -> prefs.putBoolean(automationKey, checked));
|
||||
|
||||
bindRow(R.id.home_settings_automations, R.drawable.ic_home_checklist,
|
||||
R.string.home_settings_automations, R.string.home_settings_automations_subtitle,
|
||||
v -> {
|
||||
UIIntents.get().launchHomeAutomationActivity(this);
|
||||
overridePendingTransition(R.anim.home_activity_open, R.anim.home_activity_hold);
|
||||
});
|
||||
|
||||
final String sendKey = getString(R.string.send_sound_pref_key);
|
||||
final boolean sendSound = prefs.getBoolean(sendKey,
|
||||
getResources().getBoolean(R.bool.send_sound_pref_default));
|
||||
@@ -105,41 +153,119 @@ public class ApplicationSettingsActivity extends BugleActionBarActivity {
|
||||
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));
|
||||
bindRow(R.id.home_settings_blocked, R.drawable.ic_home_menu_spam,
|
||||
R.string.home_settings_blocked, R.string.home_settings_blocked_subtitle,
|
||||
v -> {
|
||||
UIIntents.get().launchHomeSpamActivity(this);
|
||||
overridePendingTransition(R.anim.home_activity_open, R.anim.home_activity_hold);
|
||||
});
|
||||
|
||||
final View advancedCard = findViewById(R.id.home_settings_advanced_card);
|
||||
if (topLevel) {
|
||||
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);
|
||||
}
|
||||
bindComingSoon(R.id.home_settings_privacy, R.drawable.ic_home_menu_person,
|
||||
R.string.home_settings_privacy, R.string.home_settings_privacy_subtitle);
|
||||
bindComingSoon(R.id.home_settings_backup, R.drawable.ic_home_archive_line,
|
||||
R.string.home_settings_backup, R.string.home_settings_backup_subtitle);
|
||||
bindComingSoon(R.id.home_settings_data, R.drawable.ic_home_file_line,
|
||||
R.string.home_settings_data, R.string.home_settings_data_subtitle);
|
||||
bindComingSoon(R.id.home_settings_help, R.drawable.ic_home_info,
|
||||
R.string.home_settings_help, R.string.home_settings_help_subtitle);
|
||||
|
||||
HomeSettingsRow.bind(findViewById(R.id.home_settings_licenses),
|
||||
R.drawable.ic_info_light,
|
||||
getString(R.string.home_settings_licenses),
|
||||
null, true, false, false,
|
||||
bindRow(R.id.home_settings_licenses, R.drawable.ic_info_light,
|
||||
R.string.home_settings_licenses, 0,
|
||||
v -> {
|
||||
startActivity(new Intent(this, LicenseActivity.class));
|
||||
overridePendingTransition(R.anim.home_activity_open, R.anim.home_activity_hold);
|
||||
},
|
||||
null);
|
||||
});
|
||||
}
|
||||
|
||||
private void bindLanguageRow() {
|
||||
if (mLanguageRow == null) {
|
||||
return;
|
||||
}
|
||||
HomeSettingsRow.bind(mLanguageRow, R.drawable.ic_home_language,
|
||||
getString(R.string.home_settings_language), null, true, false, false,
|
||||
v -> showLanguageSheet(), null);
|
||||
HomeSettingsRow.setValue(mLanguageRow, languageLabel(mLanguageTag));
|
||||
}
|
||||
|
||||
private void bindAppLockRow() {
|
||||
final View row = findViewById(R.id.home_settings_app_lock);
|
||||
if (row == null) {
|
||||
return;
|
||||
}
|
||||
HomeSettingsRow.bind(row, R.drawable.ic_home_lock,
|
||||
getString(R.string.home_settings_app_lock),
|
||||
getString(R.string.home_settings_app_lock_subtitle),
|
||||
true, false, false,
|
||||
v -> {
|
||||
startActivity(new Intent(this, HomeSecuritySettingsActivity.class));
|
||||
overridePendingTransition(R.anim.home_activity_open, R.anim.home_activity_hold);
|
||||
}, null);
|
||||
HomeSettingsRow.setValue(row, getString(HomeAppLock.isEnabled()
|
||||
? R.string.home_lock_on : R.string.home_lock_off));
|
||||
}
|
||||
|
||||
private String languageLabel(final String tag) {
|
||||
if (LANGUAGE_EN.equals(tag)) {
|
||||
return getString(R.string.home_language_english);
|
||||
}
|
||||
if (LANGUAGE_KO.equals(tag)) {
|
||||
return getString(R.string.home_language_korean);
|
||||
}
|
||||
return getString(R.string.home_language_system);
|
||||
}
|
||||
|
||||
private void showLanguageSheet() {
|
||||
final LayoutInflater inflater = LayoutInflater.from(this);
|
||||
final LinearLayout list = (LinearLayout) inflater.inflate(
|
||||
R.layout.home_language_sheet, null, false);
|
||||
final BottomSheetDialog[] dialog = new BottomSheetDialog[1];
|
||||
addLanguageOption(inflater, list, LANGUAGE_SYSTEM, R.string.home_language_system, dialog);
|
||||
addLanguageOption(inflater, list, LANGUAGE_EN, R.string.home_language_english, dialog);
|
||||
addLanguageOption(inflater, list, LANGUAGE_KO, R.string.home_language_korean, dialog);
|
||||
dialog[0] = HomeBottomSheet.builder(this)
|
||||
.title(R.string.home_settings_language)
|
||||
.custom(list)
|
||||
.show();
|
||||
}
|
||||
|
||||
private void addLanguageOption(final LayoutInflater inflater, final LinearLayout parent,
|
||||
final String tag, final int titleRes, final BottomSheetDialog[] dialog) {
|
||||
final View row = inflater.inflate(R.layout.home_language_row, parent, false);
|
||||
((TextView) row.findViewById(R.id.home_language_title)).setText(titleRes);
|
||||
row.findViewById(R.id.home_language_check)
|
||||
.setVisibility(tag.equals(mLanguageTag) ? View.VISIBLE : View.GONE);
|
||||
row.setOnClickListener(v -> {
|
||||
mLanguageTag = tag;
|
||||
BuglePrefs.getApplicationPrefs().putString(
|
||||
getString(R.string.language_pref_key), tag);
|
||||
bindLanguageRow();
|
||||
if (dialog[0] != null) {
|
||||
dialog[0].dismiss();
|
||||
}
|
||||
});
|
||||
final LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(
|
||||
LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.WRAP_CONTENT);
|
||||
params.topMargin = parent.getChildCount() == 0
|
||||
? 0 : getResources().getDimensionPixelSize(R.dimen.looply_space_sm);
|
||||
row.setLayoutParams(params);
|
||||
parent.addView(row);
|
||||
}
|
||||
|
||||
private void bindComingSoon(final int rowId, final int iconRes, final int titleRes,
|
||||
final int subtitleRes) {
|
||||
bindRow(rowId, iconRes, titleRes, subtitleRes,
|
||||
v -> HomeToast.show(this, R.string.home_popover_coming_soon));
|
||||
}
|
||||
|
||||
private void bindRow(final int rowId, final int iconRes, final int titleRes,
|
||||
final int subtitleRes, final View.OnClickListener clickListener) {
|
||||
final View row = findViewById(rowId);
|
||||
if (row == null) {
|
||||
return;
|
||||
}
|
||||
HomeSettingsRow.bind(row, iconRes, getString(titleRes),
|
||||
subtitleRes == 0 ? null : getString(subtitleRes),
|
||||
true, false, false, clickListener, null);
|
||||
}
|
||||
|
||||
private void updateSmsRow() {
|
||||
|
||||
@@ -20,6 +20,7 @@ import android.content.Context;
|
||||
import android.database.Cursor;
|
||||
import android.net.Uri;
|
||||
import android.provider.ContactsContract.CommonDataKinds.Phone;
|
||||
import android.text.TextUtils;
|
||||
import android.util.AttributeSet;
|
||||
import android.view.View;
|
||||
import android.view.View.OnClickListener;
|
||||
@@ -53,9 +54,19 @@ public class ContactListItemView extends LinearLayout implements OnClickListener
|
||||
private ContactIconView mContactIconView;
|
||||
private ImageView mContactCheckmarkView;
|
||||
private ImageView mWorkProfileIcon;
|
||||
private View mSectionCard;
|
||||
private View mSectionDivider;
|
||||
private View mSectionGapTop;
|
||||
private View mSectionGapBottom;
|
||||
private HostInterface mHostInterface;
|
||||
private boolean mShouldShowAlphabetHeader;
|
||||
|
||||
public static final int SECTION_NONE = 0;
|
||||
public static final int SECTION_SINGLE = 1;
|
||||
public static final int SECTION_FIRST = 2;
|
||||
public static final int SECTION_MIDDLE = 3;
|
||||
public static final int SECTION_LAST = 4;
|
||||
|
||||
public ContactListItemView(final Context context, final AttributeSet attrs) {
|
||||
super(context, attrs);
|
||||
mData = DataModel.get().createContactListItemData();
|
||||
@@ -71,6 +82,55 @@ public class ContactListItemView extends LinearLayout implements OnClickListener
|
||||
mContactIconView = findViewById(R.id.contact_icon);
|
||||
mContactCheckmarkView = findViewById(R.id.contact_checkmark);
|
||||
mWorkProfileIcon = findViewById(R.id.work_profile_icon);
|
||||
mSectionCard = findViewById(R.id.contact_section_card);
|
||||
mSectionDivider = findViewById(R.id.contact_section_divider);
|
||||
mSectionGapTop = findViewById(R.id.contact_section_gap_top);
|
||||
mSectionGapBottom = findViewById(R.id.contact_section_gap_bottom);
|
||||
}
|
||||
|
||||
/** Draws this row as part of a letter-grouped card, or clears the chrome. */
|
||||
public void setSectionBox(final int role) {
|
||||
if (mSectionCard == null) {
|
||||
return;
|
||||
}
|
||||
if (role == SECTION_NONE) {
|
||||
mSectionCard.setBackground(null);
|
||||
mSectionCard.setClipToOutline(false);
|
||||
if (mSectionDivider != null) {
|
||||
mSectionDivider.setVisibility(GONE);
|
||||
}
|
||||
if (mSectionGapTop != null) {
|
||||
mSectionGapTop.setVisibility(GONE);
|
||||
}
|
||||
if (mSectionGapBottom != null) {
|
||||
mSectionGapBottom.setVisibility(GONE);
|
||||
}
|
||||
return;
|
||||
}
|
||||
final int background;
|
||||
if (role == SECTION_SINGLE) {
|
||||
background = R.drawable.home_bg_contact_section_single;
|
||||
} else if (role == SECTION_FIRST) {
|
||||
background = R.drawable.home_bg_contact_section_top;
|
||||
} else if (role == SECTION_LAST) {
|
||||
background = R.drawable.home_bg_contact_section_bottom;
|
||||
} else {
|
||||
background = R.drawable.home_bg_contact_section_middle;
|
||||
}
|
||||
mSectionCard.setBackgroundResource(background);
|
||||
mSectionCard.setClipToOutline(true);
|
||||
if (mSectionDivider != null) {
|
||||
mSectionDivider.setVisibility(
|
||||
role == SECTION_FIRST || role == SECTION_MIDDLE ? VISIBLE : GONE);
|
||||
}
|
||||
if (mSectionGapTop != null) {
|
||||
mSectionGapTop.setVisibility(
|
||||
role == SECTION_FIRST || role == SECTION_SINGLE ? VISIBLE : GONE);
|
||||
}
|
||||
if (mSectionGapBottom != null) {
|
||||
mSectionGapBottom.setVisibility(
|
||||
role == SECTION_LAST || role == SECTION_SINGLE ? VISIBLE : GONE);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -160,9 +220,11 @@ public class ContactListItemView extends LinearLayout implements OnClickListener
|
||||
} else {
|
||||
mWorkProfileIcon.setVisibility(GONE);
|
||||
}
|
||||
if (mShouldShowAlphabetHeader) {
|
||||
final String headerLabel = mData.getAlphabetHeader();
|
||||
if (mShouldShowAlphabetHeader && !TextUtils.isEmpty(headerLabel)
|
||||
&& !TextUtils.equals(headerLabel, " ")) {
|
||||
mAlphabetHeaderTextView.setVisibility(VISIBLE);
|
||||
mAlphabetHeaderTextView.setText(mData.getAlphabetHeader());
|
||||
mAlphabetHeaderTextView.setText(headerLabel);
|
||||
} else {
|
||||
mAlphabetHeaderTextView.setVisibility(GONE);
|
||||
}
|
||||
|
||||
@@ -118,7 +118,7 @@ public class ContactSectionIndexer implements SectionIndexer {
|
||||
if (TextUtils.isEmpty(mSections[i])) {
|
||||
mSections[i] = BLANK_HEADER_STRING;
|
||||
} else if (!mSections[i].equals(BLANK_HEADER_STRING)) {
|
||||
mSections[i] = mSections[i].trim();
|
||||
mSections[i] = normalizeProviderSection(mSections[i].trim());
|
||||
}
|
||||
|
||||
mSectionStartingPositions.add(position);
|
||||
@@ -141,9 +141,11 @@ public class ContactSectionIndexer implements SectionIndexer {
|
||||
// The sort key is typically the contact's display name, so for example, a contact
|
||||
// named "Bob" will go into section "B". The Contacts provider generally uses a
|
||||
// a slightly more sophisticated heuristic, but as a fallback this is good enough.
|
||||
final String sortKey = cursor.getString(ContactUtil.INDEX_SORT_KEY);
|
||||
final String section = TextUtils.isEmpty(sortKey) ? BLANK_HEADER_STRING :
|
||||
sortKey.substring(0, 1).toUpperCase();
|
||||
String sortKey = cursor.getString(ContactUtil.INDEX_SORT_KEY);
|
||||
if (TextUtils.isEmpty(sortKey)) {
|
||||
sortKey = cursor.getString(ContactUtil.INDEX_DISPLAY_NAME);
|
||||
}
|
||||
final String section = sectionLabelFor(sortKey);
|
||||
|
||||
final int lastIndex = sections.size() - 1;
|
||||
final String currentSection = lastIndex >= 0 ? sections.get(lastIndex) : null;
|
||||
@@ -158,6 +160,94 @@ public class ContactSectionIndexer implements SectionIndexer {
|
||||
sections.toArray(mSections);
|
||||
}
|
||||
|
||||
/** Maps a Hangul syllable section from the provider to its choseong; leaves other labels. */
|
||||
private static String normalizeProviderSection(final String title) {
|
||||
if (title.codePointCount(0, title.length()) != 1) {
|
||||
return title;
|
||||
}
|
||||
final int codePoint = title.codePointAt(0);
|
||||
if (codePoint >= 0xAC00 && codePoint <= 0xD7A3) {
|
||||
return sectionLabelFor(title);
|
||||
}
|
||||
return title;
|
||||
}
|
||||
|
||||
/**
|
||||
* Locale-aware bucket for a sort key: Latin A–Z, Hangul choseong (ㄱ…ㅎ), Japanese
|
||||
* gojuon rows, other letters uppercased, and # for digits or symbols.
|
||||
*/
|
||||
static String sectionLabelFor(final String sortKey) {
|
||||
if (TextUtils.isEmpty(sortKey)) {
|
||||
return BLANK_HEADER_STRING;
|
||||
}
|
||||
int index = 0;
|
||||
while (index < sortKey.length() && Character.isWhitespace(sortKey.charAt(index))) {
|
||||
index++;
|
||||
}
|
||||
if (index >= sortKey.length()) {
|
||||
return BLANK_HEADER_STRING;
|
||||
}
|
||||
final int codePoint = sortKey.codePointAt(index);
|
||||
if (codePoint >= 0xAC00 && codePoint <= 0xD7A3) {
|
||||
final int choseongIndex = (codePoint - 0xAC00) / (21 * 28);
|
||||
return String.valueOf(HANGUL_CHOSEONG[choseongIndex]);
|
||||
}
|
||||
if ((codePoint >= 0x3131 && codePoint <= 0x314E)
|
||||
|| (codePoint >= 0x1100 && codePoint <= 0x1112)) {
|
||||
return new String(Character.toChars(codePoint));
|
||||
}
|
||||
final int kana = toHiragana(codePoint);
|
||||
if (kana >= 0x3041 && kana <= 0x3096) {
|
||||
return String.valueOf(japaneseRow(kana));
|
||||
}
|
||||
if (Character.isDigit(codePoint) || isCjkIdeograph(codePoint)
|
||||
|| !Character.isLetter(codePoint)) {
|
||||
return "#";
|
||||
}
|
||||
return new String(Character.toChars(Character.toUpperCase(codePoint)));
|
||||
}
|
||||
|
||||
private static final char[] HANGUL_CHOSEONG = {
|
||||
'ㄱ', 'ㄲ', 'ㄴ', 'ㄷ', 'ㄸ', 'ㄹ', 'ㅁ', 'ㅂ', 'ㅃ', 'ㅅ',
|
||||
'ㅆ', 'ㅇ', 'ㅈ', 'ㅉ', 'ㅊ', 'ㅋ', 'ㅌ', 'ㅍ', 'ㅎ'
|
||||
};
|
||||
|
||||
private static int toHiragana(final int codePoint) {
|
||||
if (codePoint >= 0x30A1 && codePoint <= 0x30F6) {
|
||||
return codePoint - 0x60;
|
||||
}
|
||||
return codePoint;
|
||||
}
|
||||
|
||||
private static char japaneseRow(final int hiragana) {
|
||||
if (hiragana <= 0x304A) {
|
||||
return 'あ';
|
||||
} else if (hiragana <= 0x3054) {
|
||||
return 'か';
|
||||
} else if (hiragana <= 0x305E) {
|
||||
return 'さ';
|
||||
} else if (hiragana <= 0x3069) {
|
||||
return 'た';
|
||||
} else if (hiragana <= 0x306E) {
|
||||
return 'な';
|
||||
} else if (hiragana <= 0x307D) {
|
||||
return 'は';
|
||||
} else if (hiragana <= 0x3082) {
|
||||
return 'ま';
|
||||
} else if (hiragana <= 0x3088) {
|
||||
return 'や';
|
||||
} else if (hiragana <= 0x308D) {
|
||||
return 'ら';
|
||||
}
|
||||
return 'わ';
|
||||
}
|
||||
|
||||
private static boolean isCjkIdeograph(final int codePoint) {
|
||||
return (codePoint >= 0x4E00 && codePoint <= 0x9FFF)
|
||||
|| (codePoint >= 0x3400 && codePoint <= 0x4DBF)
|
||||
|| (codePoint >= 0x20000 && codePoint <= 0x2A6DF);
|
||||
}
|
||||
|
||||
private void buildIndexer(final Cursor cursor) {
|
||||
// First check if we get indexer label extras from the contact provider; if not, fall back
|
||||
// to building from display names.
|
||||
|
||||
@@ -18,6 +18,7 @@ package com.android.messaging.ui.conversation;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.res.Resources;
|
||||
import android.graphics.Rect;
|
||||
import android.net.Uri;
|
||||
@@ -53,6 +54,7 @@ import androidx.core.view.WindowInsetsControllerCompat;
|
||||
|
||||
import com.android.messaging.Factory;
|
||||
import com.android.messaging.R;
|
||||
import com.android.messaging.datamodel.ScheduledMessageStore;
|
||||
import com.android.messaging.datamodel.binding.Binding;
|
||||
import com.android.messaging.datamodel.binding.BindingBase;
|
||||
import com.android.messaging.datamodel.binding.ImmutableBindingRef;
|
||||
@@ -72,7 +74,9 @@ 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.UIIntents;
|
||||
import com.android.messaging.ui.home.HomeComposeAttachPanel;
|
||||
import com.android.messaging.ui.home.HomeSchedulePicker;
|
||||
import com.android.messaging.ui.home.HomeToast;
|
||||
import com.android.messaging.ui.home.HomeUiMotion;
|
||||
import com.android.messaging.util.AccessibilityUtil;
|
||||
@@ -116,6 +120,8 @@ public class ComposeMessageView extends LinearLayout
|
||||
void showAttachmentChooser();
|
||||
void launchSystemGalleryPicker();
|
||||
void requestMediaPermission(Runnable onGranted);
|
||||
void requestLocationPermission(Runnable onGranted);
|
||||
void launchLocationShare();
|
||||
boolean shouldShowSubjectEditor();
|
||||
boolean shouldHideAttachmentsWhenSimSelectorShown();
|
||||
Uri getSelfSendButtonIconUri();
|
||||
@@ -124,6 +130,7 @@ public class ComposeMessageView extends LinearLayout
|
||||
}
|
||||
|
||||
public static final int CODEPOINTS_REMAINING_BEFORE_COUNTER_SHOWN = 10;
|
||||
private static final String KEY_SCHEDULED_SEND_AT = "home_scheduled_send_at";
|
||||
|
||||
// There is no draft and there is no need for the SIM selector
|
||||
private static final int SEND_WIDGET_MODE_SELF_AVATAR = 1;
|
||||
@@ -150,6 +157,9 @@ public class ComposeMessageView extends LinearLayout
|
||||
private int mComposeBarPaddingBottom;
|
||||
private ImageButton mVoiceButton;
|
||||
private View mSendColumn;
|
||||
private View mScheduleChipRow;
|
||||
private TextView mScheduleChipText;
|
||||
private long mScheduledSendAt;
|
||||
|
||||
private final Binding<DraftMessageData> mBinding;
|
||||
private IComposeMessageViewHost mHost;
|
||||
@@ -322,6 +332,16 @@ public class ComposeMessageView extends LinearLayout
|
||||
mComposeBarPaddingBottom = mComposeBar.getPaddingBottom();
|
||||
}
|
||||
mAttachHost = createComposeAttachHost();
|
||||
mScheduleChipRow = findViewById(R.id.home_schedule_chip_row);
|
||||
mScheduleChipText = findViewById(R.id.home_schedule_chip_text);
|
||||
final View scheduleChip = findViewById(R.id.home_schedule_chip);
|
||||
if (scheduleChip != null) {
|
||||
scheduleChip.setOnClickListener(v -> showSchedulePicker());
|
||||
}
|
||||
final View scheduleClear = findViewById(R.id.home_schedule_chip_clear);
|
||||
if (scheduleClear != null) {
|
||||
scheduleClear.setOnClickListener(v -> setScheduledSendAt(0L));
|
||||
}
|
||||
mAttachMediaButton.setOnClickListener(clickView -> toggleComposeAttachmentSheet());
|
||||
mAttachMediaButton.setSelected(mComposeEditText.hasFocus());
|
||||
|
||||
@@ -433,6 +453,9 @@ public class ComposeMessageView extends LinearLayout
|
||||
mBinding.ensureBound(data);
|
||||
switch (result) {
|
||||
case CheckDraftForSendTask.RESULT_PASSED:
|
||||
if (queueScheduledMessageIfNeeded()) {
|
||||
break;
|
||||
}
|
||||
// Continue sending after check succeeded.
|
||||
final MessageData message = mBinding.getData()
|
||||
.prepareMessageForSending(mBinding);
|
||||
@@ -1040,6 +1063,35 @@ public class ComposeMessageView extends LinearLayout
|
||||
|
||||
public void saveInputState(final Bundle outState) {
|
||||
mInputManager.onSaveInputState(outState);
|
||||
outState.putLong(KEY_SCHEDULED_SEND_AT, mScheduledSendAt);
|
||||
}
|
||||
|
||||
public void restoreScheduledSendAt(final Bundle state) {
|
||||
if (state == null) {
|
||||
return;
|
||||
}
|
||||
setScheduledSendAt(state.getLong(KEY_SCHEDULED_SEND_AT, 0L));
|
||||
}
|
||||
|
||||
public void applySharedLocation(final Intent data) {
|
||||
if (data == null) {
|
||||
return;
|
||||
}
|
||||
final String text = data.getStringExtra(UIIntents.UI_INTENT_EXTRA_LOCATION_TEXT);
|
||||
if (!TextUtils.isEmpty(text)) {
|
||||
mComposeEditText.setText(text);
|
||||
mComposeEditText.setSelection(text.length());
|
||||
if (mBinding.isBound()) {
|
||||
mBinding.getData().setMessageText(text);
|
||||
}
|
||||
}
|
||||
final Uri image = data.getParcelableExtra(
|
||||
UIIntents.UI_INTENT_EXTRA_LOCATION_IMAGE_URI, Uri.class);
|
||||
if (image != null) {
|
||||
onPendingAttachmentAdded(PendingAttachmentData.createPendingAttachmentData(
|
||||
"image/jpeg", image));
|
||||
}
|
||||
resumeComposeMessage();
|
||||
}
|
||||
|
||||
public void resetMediaPickerState() {
|
||||
@@ -1135,6 +1187,18 @@ public class ComposeMessageView extends LinearLayout
|
||||
openMediaFromAttachmentSheet(ConversationInputManager.MEDIA_PICKER_TARGET_CONTACT);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onShareLocation() {
|
||||
if (mHost != null) {
|
||||
mHost.requestLocationPermission(() -> mHost.launchLocationShare());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onScheduleMessage() {
|
||||
showSchedulePicker();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRequestMediaPermission() {
|
||||
if (mHost != null) {
|
||||
@@ -1245,6 +1309,58 @@ public class ComposeMessageView extends LinearLayout
|
||||
}
|
||||
}
|
||||
|
||||
private boolean queueScheduledMessageIfNeeded() {
|
||||
if (mScheduledSendAt <= System.currentTimeMillis()) {
|
||||
if (mScheduledSendAt > 0L) {
|
||||
setScheduledSendAt(0L);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
final DraftMessageData draft = mBinding.getData();
|
||||
final boolean empty = TextUtils.isEmpty(draft.getMessageText())
|
||||
&& TextUtils.isEmpty(draft.getMessageSubject())
|
||||
&& draft.getReadOnlyAttachments().isEmpty();
|
||||
if (empty) {
|
||||
HomeToast.show(getContext(), R.string.home_schedule_need_message);
|
||||
return true;
|
||||
}
|
||||
final MessageData message = draft.prepareMessageForSending(mBinding);
|
||||
if (message == null || !message.hasContent()) {
|
||||
HomeToast.show(getContext(), R.string.home_schedule_need_message);
|
||||
return true;
|
||||
}
|
||||
ScheduledMessageStore.schedule(getContext(), message, mScheduledSendAt);
|
||||
hideSubjectEditor();
|
||||
HomeToast.show(getContext(), getResources().getString(
|
||||
R.string.home_schedule_queued, HomeSchedulePicker.formatChip(
|
||||
getContext(), mScheduledSendAt)));
|
||||
setScheduledSendAt(0L);
|
||||
return true;
|
||||
}
|
||||
|
||||
private void showSchedulePicker() {
|
||||
final Activity activity = resolveActivity();
|
||||
if (activity == null) {
|
||||
return;
|
||||
}
|
||||
HomeSchedulePicker.show(activity, mScheduledSendAt, this::setScheduledSendAt);
|
||||
}
|
||||
|
||||
private void setScheduledSendAt(final long sendAt) {
|
||||
mScheduledSendAt = sendAt > System.currentTimeMillis() ? sendAt : 0L;
|
||||
if (mScheduleChipRow == null) {
|
||||
return;
|
||||
}
|
||||
if (mScheduledSendAt <= 0L) {
|
||||
mScheduleChipRow.setVisibility(GONE);
|
||||
return;
|
||||
}
|
||||
if (mScheduleChipText != null) {
|
||||
mScheduleChipText.setText(HomeSchedulePicker.formatChip(getContext(), mScheduledSendAt));
|
||||
}
|
||||
mScheduleChipRow.setVisibility(VISIBLE);
|
||||
}
|
||||
|
||||
private Activity resolveActivity() {
|
||||
if (mOriginalContext instanceof Activity) {
|
||||
return (Activity) mOriginalContext;
|
||||
|
||||
@@ -111,6 +111,7 @@ 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.HomeBottomSheet;
|
||||
import com.android.messaging.ui.home.HomeComposeAttachPanel;
|
||||
import com.android.messaging.ui.home.HomeEmptyState;
|
||||
import com.android.messaging.ui.home.HomeGallerySheet;
|
||||
@@ -131,10 +132,12 @@ import com.android.messaging.util.ChangeDefaultSmsAppHelper;
|
||||
import com.android.messaging.util.ContactUtil;
|
||||
import com.android.messaging.util.ImeUtil;
|
||||
import com.android.messaging.util.LogUtil;
|
||||
import com.android.messaging.util.OsUtil;
|
||||
import com.android.messaging.util.PhoneUtils;
|
||||
import com.android.messaging.util.TextUtil;
|
||||
import com.android.messaging.util.UiUtils;
|
||||
import com.android.messaging.util.UriUtil;
|
||||
import com.google.android.material.bottomsheet.BottomSheetDialog;
|
||||
|
||||
import java.io.File;
|
||||
import java.util.ArrayList;
|
||||
@@ -336,6 +339,26 @@ public class ConversationFragment extends Fragment implements ConversationDataLi
|
||||
}
|
||||
});
|
||||
|
||||
private Runnable mOnLocationPermissionGranted;
|
||||
private final ActivityResultLauncher<String[]> mLocationPermissionLauncher =
|
||||
registerForActivityResult(new ActivityResultContracts.RequestMultiplePermissions(),
|
||||
result -> {
|
||||
final Runnable next = mOnLocationPermissionGranted;
|
||||
mOnLocationPermissionGranted = null;
|
||||
if (next != null) {
|
||||
next.run();
|
||||
}
|
||||
});
|
||||
private final ActivityResultLauncher<Intent> mLocationShareLauncher =
|
||||
registerForActivityResult(new ActivityResultContracts.StartActivityForResult(),
|
||||
result -> {
|
||||
if (result.getResultCode() == Activity.RESULT_OK
|
||||
&& result.getData() != null
|
||||
&& mComposeMessageView != null) {
|
||||
mComposeMessageView.applySharedLocation(result.getData());
|
||||
}
|
||||
});
|
||||
|
||||
public ConversationFragment getConversationFragment() {
|
||||
return (ConversationFragment) getParentFragmentManager().findFragmentByTag(
|
||||
ConversationFragment.FRAGMENT_TAG);
|
||||
@@ -544,6 +567,7 @@ public class ConversationFragment extends Fragment implements ConversationDataLi
|
||||
// Bind the compose message view to the DraftMessageData
|
||||
mComposeMessageView.bind(DataModel.get().createDraftMessageData(
|
||||
mBinding.getData().getConversationId()), this);
|
||||
mComposeMessageView.restoreScheduledSendAt(savedInstanceState);
|
||||
|
||||
return view;
|
||||
}
|
||||
@@ -1508,13 +1532,9 @@ public class ConversationFragment extends Fragment implements ConversationDataLi
|
||||
if (messageView != mSelectedMessage) {
|
||||
final ConversationMessageData data = messageView.getData();
|
||||
final boolean isReadyToSend = isReadyForAction();
|
||||
if (data.getOneClickResendMessage()) {
|
||||
// Directly resend the message on tap if it's failed
|
||||
retrySend(data.getMessageId());
|
||||
if (data.getOneClickResendMessage() || data.getShowResendMessage()) {
|
||||
// Resend / delete is offered from the status row under the bubble.
|
||||
selectMessage(null);
|
||||
} else if (data.getShowResendMessage() && isReadyToSend) {
|
||||
// Select the message to show the resend/download/delete options
|
||||
selectMessage(messageView);
|
||||
} else if (data.getShowDownloadMessage() && isReadyToSend) {
|
||||
// Directly download the message on tap
|
||||
retryDownload(data.getMessageId());
|
||||
@@ -1530,6 +1550,43 @@ public class ConversationFragment extends Fragment implements ConversationDataLi
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onMessageSendOptionsClick(final ConversationMessageView messageView) {
|
||||
if (getActivity() == null || messageView == null) {
|
||||
return;
|
||||
}
|
||||
final ConversationMessageData data = messageView.getData();
|
||||
final String messageId = data.getMessageId();
|
||||
if (TextUtils.isEmpty(messageId)) {
|
||||
return;
|
||||
}
|
||||
final View custom = LayoutInflater.from(requireContext())
|
||||
.inflate(R.layout.home_message_status_sheet, null, false);
|
||||
final BottomSheetDialog[] dialog = { null };
|
||||
final View resend = custom.findViewById(R.id.home_message_status_resend);
|
||||
final View delete = custom.findViewById(R.id.home_message_status_delete);
|
||||
if (resend != null) {
|
||||
resend.setOnClickListener(v -> {
|
||||
if (dialog[0] != null) {
|
||||
dialog[0].dismiss();
|
||||
}
|
||||
retrySend(messageId);
|
||||
});
|
||||
}
|
||||
if (delete != null) {
|
||||
delete.setOnClickListener(v -> {
|
||||
if (dialog[0] != null) {
|
||||
dialog[0].dismiss();
|
||||
}
|
||||
deleteMessage(messageId);
|
||||
});
|
||||
}
|
||||
dialog[0] = HomeBottomSheet.builder(requireContext())
|
||||
.title(R.string.home_message_status_options_title)
|
||||
.custom(custom)
|
||||
.show();
|
||||
}
|
||||
|
||||
private static class AttachmentToSave {
|
||||
public final Uri uri;
|
||||
public final String contentType;
|
||||
@@ -1762,6 +1819,23 @@ public class ConversationFragment extends Fragment implements ConversationDataLi
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void requestLocationPermission(final Runnable onGranted) {
|
||||
if (OsUtil.hasLocationPermission()) {
|
||||
if (onGranted != null) {
|
||||
onGranted.run();
|
||||
}
|
||||
return;
|
||||
}
|
||||
mOnLocationPermissionGranted = onGranted;
|
||||
mLocationPermissionLauncher.launch(OsUtil.getLocationPermissions());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void launchLocationShare() {
|
||||
UIIntents.get().launchHomeLocationActivity(requireActivity(), mLocationShareLauncher);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void showAttachmentChooser() {
|
||||
showAttachmentChooser(mConversationId, getActivity());
|
||||
|
||||
+85
-15
@@ -19,6 +19,7 @@ package com.android.messaging.ui.conversation;
|
||||
import android.content.Context;
|
||||
import android.content.res.Resources;
|
||||
import android.database.Cursor;
|
||||
import android.graphics.Paint;
|
||||
import android.graphics.Rect;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.net.Uri;
|
||||
@@ -37,6 +38,7 @@ import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.view.WindowManager;
|
||||
import android.widget.FrameLayout;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.ImageView.ScaleType;
|
||||
import android.widget.LinearLayout;
|
||||
import android.widget.TextView;
|
||||
@@ -87,6 +89,7 @@ public class ConversationMessageView extends FrameLayout implements View.OnClick
|
||||
Rect imageBounds, boolean longPress);
|
||||
SubscriptionListEntry getSubscriptionEntryForSelfParticipant(String selfParticipantId,
|
||||
boolean excludeDefault);
|
||||
void onMessageSendOptionsClick(ConversationMessageView view);
|
||||
}
|
||||
|
||||
private final ConversationMessageData mData;
|
||||
@@ -98,6 +101,8 @@ public class ConversationMessageView extends FrameLayout implements View.OnClick
|
||||
private boolean mMessageTextHasLinks;
|
||||
private boolean mMessageHasYouTubeLink;
|
||||
private TextView mStatusTextView;
|
||||
private TextView mStatusActionView;
|
||||
private ImageView mStatusIconView;
|
||||
private TextView mTitleTextView;
|
||||
private TextView mMmsInfoTextView;
|
||||
private LinearLayout mMessageTitleLayout;
|
||||
@@ -144,6 +149,16 @@ public class ConversationMessageView extends FrameLayout implements View.OnClick
|
||||
IgnoreLinkLongClickHelper.ignoreLinkLongClick(mMessageTextView, this);
|
||||
|
||||
mStatusTextView = findViewById(R.id.message_status);
|
||||
mStatusActionView = findViewById(R.id.message_status_action);
|
||||
mStatusIconView = findViewById(R.id.message_status_icon);
|
||||
if (mStatusActionView != null) {
|
||||
mStatusActionView.setPaintFlags(
|
||||
mStatusActionView.getPaintFlags() | Paint.UNDERLINE_TEXT_FLAG);
|
||||
mStatusActionView.setOnClickListener(v -> notifySendOptionsClick());
|
||||
}
|
||||
if (mStatusIconView != null) {
|
||||
mStatusIconView.setOnClickListener(v -> notifySendOptionsClick());
|
||||
}
|
||||
mTitleTextView = findViewById(R.id.message_title);
|
||||
mMmsInfoTextView = findViewById(R.id.mms_info);
|
||||
mMessageTitleLayout = findViewById(R.id.message_title_layout);
|
||||
@@ -298,6 +313,31 @@ public class ConversationMessageView extends FrameLayout implements View.OnClick
|
||||
|| status == MessageData.BUGLE_STATUS_OUTGOING_FAILED_EMERGENCY_NUMBER;
|
||||
}
|
||||
|
||||
private boolean isOutgoingWaitingStatus() {
|
||||
if (mData == null || mData.getIsIncoming() || isOptimisticSending()) {
|
||||
return false;
|
||||
}
|
||||
final int status = mData.getStatus();
|
||||
return status == MessageData.BUGLE_STATUS_OUTGOING_AWAITING_RETRY;
|
||||
}
|
||||
|
||||
private boolean shouldShowSendOptions() {
|
||||
if (mData == null || mData.getIsIncoming() || isOptimisticSending()) {
|
||||
return false;
|
||||
}
|
||||
if (!PhoneUtils.getDefault().isDefaultSmsApp()) {
|
||||
return false;
|
||||
}
|
||||
return isOutgoingWaitingStatus()
|
||||
|| mData.getStatus() == MessageData.BUGLE_STATUS_OUTGOING_FAILED;
|
||||
}
|
||||
|
||||
private void notifySendOptionsClick() {
|
||||
if (mHost != null && shouldShowSendOptions()) {
|
||||
mHost.onMessageSendOptionsClick(this);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether we should show simplified visual style for the message view (i.e. hide the
|
||||
* avatar and bubble arrow, reduce padding).
|
||||
@@ -367,10 +407,13 @@ public class ConversationMessageView extends FrameLayout implements View.OnClick
|
||||
case MessageData.BUGLE_STATUS_OUTGOING_YET_TO_SEND:
|
||||
case MessageData.BUGLE_STATUS_OUTGOING_SENDING:
|
||||
case MessageData.BUGLE_STATUS_OUTGOING_RESENDING:
|
||||
case MessageData.BUGLE_STATUS_OUTGOING_AWAITING_RETRY:
|
||||
statusResId = R.string.message_status_sending;
|
||||
break;
|
||||
|
||||
case MessageData.BUGLE_STATUS_OUTGOING_AWAITING_RETRY:
|
||||
statusText = getResources().getString(R.string.home_message_status_waiting);
|
||||
break;
|
||||
|
||||
case MessageData.BUGLE_STATUS_OUTGOING_FAILED_EMERGENCY_NUMBER:
|
||||
statusResId = R.string.message_status_send_failed_emergency_number;
|
||||
break;
|
||||
@@ -382,12 +425,7 @@ public class ConversationMessageView extends FrameLayout implements View.OnClick
|
||||
break;
|
||||
}
|
||||
if (PhoneUtils.getDefault().isDefaultSmsApp()) {
|
||||
if (isSelected()) {
|
||||
statusResId = R.string.message_status_resend;
|
||||
} else {
|
||||
statusResId = MmsUtils.mapRawStatusToErrorResourceId(
|
||||
mData.getStatus(), mData.getRawTelephonyStatus());
|
||||
}
|
||||
statusText = getResources().getString(R.string.home_message_status_not_sent);
|
||||
break;
|
||||
}
|
||||
// FALL THROUGH HERE
|
||||
@@ -443,10 +481,15 @@ public class ConversationMessageView extends FrameLayout implements View.OnClick
|
||||
// We set the text even if the view will be GONE for accessibility
|
||||
mStatusTextView.setText(statusText);
|
||||
final boolean statusVisible = !TextUtils.isEmpty(statusText);
|
||||
if (statusVisible) {
|
||||
mStatusTextView.setVisibility(View.VISIBLE);
|
||||
} else {
|
||||
mStatusTextView.setVisibility(View.GONE);
|
||||
mStatusTextView.setVisibility(statusVisible ? View.VISIBLE : View.GONE);
|
||||
|
||||
final boolean sendOptionsVisible = shouldShowSendOptions();
|
||||
if (mStatusActionView != null) {
|
||||
mStatusActionView.setVisibility(sendOptionsVisible ? View.VISIBLE : View.GONE);
|
||||
}
|
||||
if (mStatusIconView != null) {
|
||||
mStatusIconView.setVisibility(
|
||||
isOutgoingWaitingStatus() ? View.VISIBLE : View.GONE);
|
||||
}
|
||||
|
||||
final boolean deliveredBadgeVisible =
|
||||
@@ -477,11 +520,11 @@ public class ConversationMessageView extends FrameLayout implements View.OnClick
|
||||
}
|
||||
|
||||
final boolean metadataVisible = senderNameVisible || statusVisible
|
||||
|| deliveredBadgeVisible || simNameVisible;
|
||||
|| sendOptionsVisible || deliveredBadgeVisible || simNameVisible;
|
||||
mMessageMetadataView.setVisibility(metadataVisible ? View.VISIBLE : View.GONE);
|
||||
|
||||
final boolean messageTextAndOrInfoVisible = titleVisible || subjectVisible
|
||||
|| mData.hasText() || metadataVisible;
|
||||
|| mData.hasText();
|
||||
mMessageTextAndInfoView.setVisibility(
|
||||
messageTextAndOrInfoVisible ? View.VISIBLE : View.GONE);
|
||||
|
||||
@@ -787,6 +830,9 @@ public class ConversationMessageView extends FrameLayout implements View.OnClick
|
||||
updateMessageAttachmentsAppearance(gravity);
|
||||
|
||||
mMessageMetadataView.setPadding(0, metadataTopPadding, 0, 0);
|
||||
if (mMessageMetadataView.getVisibility() == View.VISIBLE) {
|
||||
setPadding(getPaddingStart(), getPaddingTop(), getPaddingEnd(), metadataTopPadding);
|
||||
}
|
||||
|
||||
updateTextAppearance();
|
||||
|
||||
@@ -844,6 +890,10 @@ public class ConversationMessageView extends FrameLayout implements View.OnClick
|
||||
description.append(separator);
|
||||
description.append(mStatusTextView.getText());
|
||||
}
|
||||
if (mStatusActionView != null && mStatusActionView.getVisibility() == View.VISIBLE) {
|
||||
description.append(separator);
|
||||
description.append(mStatusActionView.getText());
|
||||
}
|
||||
|
||||
if (mSimNameView.getVisibility() == View.VISIBLE) {
|
||||
description.append(separator);
|
||||
@@ -995,10 +1045,30 @@ public class ConversationMessageView extends FrameLayout implements View.OnClick
|
||||
mData.hasAttachments() && !shouldShowMessageTextBubble()) {
|
||||
timestampColorResId = R.color.timestamp_text_outgoing;
|
||||
}
|
||||
mStatusTextView.setTextColor(getResources().getColor(timestampColorResId, theme));
|
||||
// Status captions sit outside the bubble, so keep them readable against the canvas.
|
||||
final int captionColorResId;
|
||||
if (shouldShowSendOptions() && !isOutgoingWaitingStatus()) {
|
||||
captionColorResId = R.color.home_destructive;
|
||||
} else if (isOutgoingWaitingStatus() || (!mData.getIsIncoming()
|
||||
&& (mData.getStatus() == MessageData.BUGLE_STATUS_OUTGOING_SENDING
|
||||
|| mData.getStatus() == MessageData.BUGLE_STATUS_OUTGOING_RESENDING
|
||||
|| isOptimisticSending()))) {
|
||||
captionColorResId = R.color.home_text_secondary;
|
||||
} else {
|
||||
captionColorResId = mData.getIsIncoming()
|
||||
? R.color.timestamp_text_incoming : R.color.timestamp_text_outgoing;
|
||||
}
|
||||
final int captionColor = getResources().getColor(captionColorResId, theme);
|
||||
mStatusTextView.setTextColor(captionColor);
|
||||
if (mStatusActionView != null) {
|
||||
mStatusActionView.setTextColor(captionColor);
|
||||
}
|
||||
if (mStatusIconView != null) {
|
||||
mStatusIconView.setColorFilter(captionColor);
|
||||
}
|
||||
|
||||
mSubjectLabel.setTextColor(getResources().getColor(subjectLabelColorResId, theme));
|
||||
mSenderNameTextView.setTextColor(getResources().getColor(timestampColorResId, theme));
|
||||
mSenderNameTextView.setTextColor(captionColor);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
+1
-1
@@ -63,7 +63,7 @@ public class ConversationListActivity extends AbstractConversationListActivity {
|
||||
if (actionBar == null) {
|
||||
return;
|
||||
}
|
||||
// Custom Message+ chrome replaces the ActionBar on the home screen.
|
||||
// Custom Message Q chrome replaces the ActionBar on the home screen.
|
||||
actionBar.hide();
|
||||
super.updateActionBar(actionBar);
|
||||
}
|
||||
|
||||
+309
-138
@@ -19,6 +19,7 @@ package com.android.messaging.ui.conversationlist;
|
||||
import android.app.Activity;
|
||||
import android.app.AlertDialog;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.database.Cursor;
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.Canvas;
|
||||
@@ -136,6 +137,7 @@ public class ConversationListFragment extends Fragment implements ConversationLi
|
||||
private TextView mHomeReadAllButton;
|
||||
private int mHomeSelectionCount;
|
||||
private View mHomeMessagesTop;
|
||||
private View mHomeCategoryCollapse;
|
||||
private View mHomeCategoryStrip;
|
||||
private ViewGroup mHomeCategoryAll;
|
||||
private View mHomeCategoryScroll;
|
||||
@@ -162,6 +164,7 @@ public class ConversationListFragment extends Fragment implements ConversationLi
|
||||
private int mHomeNavAutomateNaturalWidth;
|
||||
private int mHomeTab = HOME_TAB_MESSAGES;
|
||||
private View mHomeListProfilePopup;
|
||||
private PopupWindow mHomeListProfileWindow;
|
||||
private View mHomeListProfileScrim;
|
||||
private ImageView mHomeListProfileBlur;
|
||||
private Bitmap mHomeListProfileBlurBitmap;
|
||||
@@ -189,12 +192,18 @@ public class ConversationListFragment extends Fragment implements ConversationLi
|
||||
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_ANIM_MS = 220;
|
||||
private static final int SEARCH_BAR_SLIDE_FALLBACK_DP = 72;
|
||||
private static final int CHROME_SCROLL_THRESHOLD_DP = 10;
|
||||
|
||||
private Parcelable mListState;
|
||||
private View mSearchBar;
|
||||
private View mListTopFade;
|
||||
private View mListBottomFade;
|
||||
private boolean mSearchBarShown;
|
||||
private boolean mCategoryStripShown = true;
|
||||
private int mCategoryNaturalHeight;
|
||||
private int mChromeScrollAccum;
|
||||
|
||||
final Binding<ConversationListData> mListBinding = BindingBase.createBinding(this);
|
||||
private final ConversationListItemData mPinnedRowProbe = new ConversationListItemData();
|
||||
@@ -259,6 +268,7 @@ public class ConversationListFragment extends Fragment implements ConversationLi
|
||||
setScrolledToNewestConversationIfNeeded();
|
||||
applySearchBarVisibility();
|
||||
resetSearchBarScrollTracking();
|
||||
updateListTopFade();
|
||||
updateHomeMainNavVisibility();
|
||||
refreshPinnedStrip();
|
||||
updateUi();
|
||||
@@ -273,6 +283,50 @@ public class ConversationListFragment extends Fragment implements ConversationLi
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isListFlushAtTop() {
|
||||
if (mRecyclerView == null || mAdapter == null || mAdapter.getItemCount() == 0) {
|
||||
return true;
|
||||
}
|
||||
final LinearLayoutManager manager =
|
||||
(LinearLayoutManager) mRecyclerView.getLayoutManager();
|
||||
if (manager == null) {
|
||||
return true;
|
||||
}
|
||||
if (manager.findFirstVisibleItemPosition() > 0) {
|
||||
return false;
|
||||
}
|
||||
final View first = manager.findViewByPosition(0);
|
||||
if (first == null) {
|
||||
return !mRecyclerView.canScrollVertically(-1);
|
||||
}
|
||||
return first.getTop() >= mRecyclerView.getPaddingTop() - 1;
|
||||
}
|
||||
|
||||
private boolean isListFlushAtBottom() {
|
||||
if (mRecyclerView == null || mAdapter == null || mAdapter.getItemCount() == 0) {
|
||||
return true;
|
||||
}
|
||||
return !mRecyclerView.canScrollVertically(1);
|
||||
}
|
||||
|
||||
private void setFadeVisible(final View fade, final boolean show) {
|
||||
if (fade == null) {
|
||||
return;
|
||||
}
|
||||
final int visibility = show ? View.VISIBLE : View.GONE;
|
||||
if (fade.getVisibility() != visibility) {
|
||||
fade.setVisibility(visibility);
|
||||
}
|
||||
}
|
||||
|
||||
private void updateListTopFade() {
|
||||
final boolean listActive = mHomeTab == HOME_TAB_MESSAGES
|
||||
&& !mArchiveMode
|
||||
&& !mForwardMessageMode;
|
||||
setFadeVisible(mListTopFade, listActive && !isListFlushAtTop());
|
||||
setFadeVisible(mListBottomFade, listActive && !isListFlushAtBottom());
|
||||
}
|
||||
|
||||
private boolean isScrolledToFirstConversation() {
|
||||
int firstItemPosition = ((LinearLayoutManager) mRecyclerView.getLayoutManager())
|
||||
.findFirstCompletelyVisibleItemPosition();
|
||||
@@ -287,6 +341,7 @@ public class ConversationListFragment extends Fragment implements ConversationLi
|
||||
*/
|
||||
@Override
|
||||
public void onDestroy() {
|
||||
dismissListProfileWindowImmediate();
|
||||
super.onDestroy();
|
||||
mListBinding.unbind();
|
||||
mHost = null;
|
||||
@@ -301,6 +356,8 @@ public class ConversationListFragment extends Fragment implements ConversationLi
|
||||
final ViewGroup rootView = (ViewGroup) inflater.inflate(R.layout.conversation_list_fragment,
|
||||
container, false);
|
||||
mRecyclerView = rootView.findViewById(android.R.id.list);
|
||||
mListTopFade = rootView.findViewById(R.id.home_list_top_fade);
|
||||
mListBottomFade = rootView.findViewById(R.id.home_list_bottom_fade);
|
||||
mEmptyListMessageView = rootView.findViewById(R.id.no_conversations_view);
|
||||
// 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
|
||||
@@ -343,6 +400,7 @@ public class ConversationListFragment extends Fragment implements ConversationLi
|
||||
if (!mArchiveMode && !mForwardMessageMode) {
|
||||
handleSearchBarByScrollOffset(dy);
|
||||
}
|
||||
updateListTopFade();
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -398,6 +456,7 @@ public class ConversationListFragment extends Fragment implements ConversationLi
|
||||
}
|
||||
setupHomeChrome(rootView);
|
||||
mHomeMessagesTop = rootView.findViewById(R.id.home_messages_top);
|
||||
mHomeCategoryCollapse = rootView.findViewById(R.id.home_category_collapse);
|
||||
mHomeCategoryStrip = rootView.findViewById(R.id.home_category_strip);
|
||||
mHomeCategoryAll = rootView.findViewById(R.id.home_category_all);
|
||||
mHomeCategoryScroll = rootView.findViewById(R.id.home_category_scroll);
|
||||
@@ -588,13 +647,8 @@ public class ConversationListFragment extends Fragment implements ConversationLi
|
||||
}
|
||||
|
||||
private void setupSearchBarScroll(final View searchBar) {
|
||||
searchBar.addOnLayoutChangeListener(
|
||||
(v, left, top, right, bottom, oldLeft, oldTop, oldRight, oldBottom) -> {
|
||||
if (bottom - top != oldBottom - oldTop && mSearchBarShown) {
|
||||
updateListPaddingForSearchBar();
|
||||
}
|
||||
});
|
||||
searchBar.post(() -> {
|
||||
updateListPaddingForSearchBar();
|
||||
applySearchBarVisibility();
|
||||
resetSearchBarScrollTracking();
|
||||
});
|
||||
@@ -604,15 +658,24 @@ public class ConversationListFragment extends Fragment implements ConversationLi
|
||||
// No scroll state is kept; visibility is derived from the live scroll position.
|
||||
}
|
||||
|
||||
/** Toward older rows → keep search visible. Toward recent rows → hide. */
|
||||
/** Toward older rows → search. Toward recent rows → categories. */
|
||||
private void handleSearchBarByScrollOffset(final int dy) {
|
||||
if (mHomeTab != HOME_TAB_MESSAGES || isSelectionMode() || mHomePinnedEditMode
|
||||
|| mSearchBar == null || mRecyclerView == null || dy == 0) {
|
||||
return;
|
||||
}
|
||||
if (dy > 0) {
|
||||
if (isScrolledToFirstConversation() && dy < 0) {
|
||||
mChromeScrollAccum = 0;
|
||||
hideSearchBar();
|
||||
return;
|
||||
}
|
||||
mChromeScrollAccum += dy;
|
||||
final int threshold = dpToPx(CHROME_SCROLL_THRESHOLD_DP);
|
||||
if (mChromeScrollAccum > threshold) {
|
||||
mChromeScrollAccum = 0;
|
||||
showSearchBar();
|
||||
} else {
|
||||
} else if (mChromeScrollAccum < -threshold) {
|
||||
mChromeScrollAccum = 0;
|
||||
hideSearchBar();
|
||||
}
|
||||
}
|
||||
@@ -621,73 +684,122 @@ public class ConversationListFragment extends Fragment implements ConversationLi
|
||||
if (mSearchBar != null) {
|
||||
mSearchBar.animate().cancel();
|
||||
}
|
||||
if (mHomeCategoryStrip != null) {
|
||||
mHomeCategoryStrip.animate().cancel();
|
||||
}
|
||||
}
|
||||
|
||||
private float getSearchBarSlideDistancePx() {
|
||||
if (mSearchBar == null) {
|
||||
return dpToPx(SEARCH_BAR_SLIDE_FALLBACK_DP);
|
||||
private int getChromeSlotHeight() {
|
||||
int height = 0;
|
||||
if (mHomeCategoryStrip != null && mHomeCategoryStrip.getHeight() > 0) {
|
||||
height = Math.max(height, mHomeCategoryStrip.getHeight());
|
||||
mCategoryNaturalHeight = mHomeCategoryStrip.getHeight();
|
||||
}
|
||||
final int height = mSearchBar.getHeight();
|
||||
return height > 0 ? height : dpToPx(SEARCH_BAR_SLIDE_FALLBACK_DP);
|
||||
if (mSearchBar != null && mSearchBar.getHeight() > 0) {
|
||||
height = Math.max(height, mSearchBar.getHeight());
|
||||
}
|
||||
if (height > 0) {
|
||||
return height;
|
||||
}
|
||||
if (mCategoryNaturalHeight > 0) {
|
||||
return mCategoryNaturalHeight;
|
||||
}
|
||||
return dpToPx(SEARCH_BAR_SLIDE_FALLBACK_DP);
|
||||
}
|
||||
|
||||
private void setChromeInteractive(final View view, final boolean interactive) {
|
||||
if (view == null) {
|
||||
return;
|
||||
}
|
||||
view.setClickable(interactive);
|
||||
view.setImportantForAccessibility(interactive
|
||||
? View.IMPORTANT_FOR_ACCESSIBILITY_YES
|
||||
: View.IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS);
|
||||
if (view instanceof ViewGroup) {
|
||||
((ViewGroup) view).setDescendantFocusability(interactive
|
||||
? ViewGroup.FOCUS_AFTER_DESCENDANTS
|
||||
: ViewGroup.FOCUS_BLOCK_DESCENDANTS);
|
||||
}
|
||||
view.setOnTouchListener(interactive ? null : (v, event) -> true);
|
||||
}
|
||||
|
||||
private void applyChromeSlot(final boolean searchShown, final boolean animate) {
|
||||
if (mHomeCategoryCollapse == null) {
|
||||
return;
|
||||
}
|
||||
if (mHomeTab != HOME_TAB_MESSAGES) {
|
||||
cancelSearchBarAnimation();
|
||||
mSearchBarShown = false;
|
||||
mCategoryStripShown = false;
|
||||
mHomeCategoryCollapse.setVisibility(View.GONE);
|
||||
return;
|
||||
}
|
||||
mHomeCategoryCollapse.setVisibility(View.VISIBLE);
|
||||
mSearchBarShown = searchShown;
|
||||
mCategoryStripShown = !searchShown;
|
||||
if (mSearchBar == null || mHomeCategoryStrip == null) {
|
||||
return;
|
||||
}
|
||||
final float slot = getChromeSlotHeight();
|
||||
mHomeCategoryCollapse.setMinimumHeight((int) slot);
|
||||
final float searchY = searchShown ? 0f : -slot;
|
||||
cancelSearchBarAnimation();
|
||||
mSearchBar.setAlpha(1f);
|
||||
mHomeCategoryStrip.setAlpha(1f);
|
||||
mHomeCategoryStrip.setTranslationY(0f);
|
||||
if (!animate) {
|
||||
mSearchBar.setTranslationY(searchY);
|
||||
mSearchBar.setVisibility(searchShown ? View.VISIBLE : View.INVISIBLE);
|
||||
mHomeCategoryStrip.setVisibility(searchShown ? View.INVISIBLE : View.VISIBLE);
|
||||
setChromeInteractive(mSearchBar, searchShown);
|
||||
setChromeInteractive(mHomeCategoryStrip, !searchShown);
|
||||
return;
|
||||
}
|
||||
mHomeCategoryStrip.setVisibility(View.VISIBLE);
|
||||
mSearchBar.setVisibility(View.VISIBLE);
|
||||
setChromeInteractive(mSearchBar, searchShown);
|
||||
setChromeInteractive(mHomeCategoryStrip, !searchShown);
|
||||
mSearchBar.animate()
|
||||
.translationY(searchY)
|
||||
.setDuration(SEARCH_BAR_ANIM_MS)
|
||||
.setInterpolator(UiUtils.DEFAULT_INTERPOLATOR)
|
||||
.withEndAction(() -> {
|
||||
if (mSearchBar == null || mHomeCategoryStrip == null) {
|
||||
return;
|
||||
}
|
||||
if (mSearchBarShown) {
|
||||
mHomeCategoryStrip.setVisibility(View.INVISIBLE);
|
||||
} else {
|
||||
mSearchBar.setVisibility(View.INVISIBLE);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void setCategoryStripShown(final boolean shown, final boolean animate) {
|
||||
applyChromeSlot(!shown, animate);
|
||||
}
|
||||
|
||||
private void updateListPaddingForSearchBar() {
|
||||
if (mRecyclerView == null) {
|
||||
return;
|
||||
}
|
||||
int top = 0;
|
||||
if (mSearchBarShown && mSearchBar != null
|
||||
&& mSearchBar.getVisibility() != View.GONE) {
|
||||
top = mSearchBar.getHeight();
|
||||
if (top == 0) {
|
||||
top = dpToPx(SEARCH_BAR_SLIDE_FALLBACK_DP);
|
||||
}
|
||||
}
|
||||
mRecyclerView.setPadding(
|
||||
mRecyclerView.getPaddingLeft(),
|
||||
top,
|
||||
0,
|
||||
mRecyclerView.getPaddingRight(),
|
||||
mRecyclerView.getPaddingBottom());
|
||||
}
|
||||
|
||||
private void applySearchBarVisibility() {
|
||||
if (mSearchBar == null || mHomeTab != HOME_TAB_MESSAGES) {
|
||||
if (mHomeTab != HOME_TAB_MESSAGES) {
|
||||
applyChromeSlot(false, false);
|
||||
return;
|
||||
}
|
||||
if (mHomePinnedEditMode) {
|
||||
cancelSearchBarAnimation();
|
||||
mSearchBar.setVisibility(View.GONE);
|
||||
mSearchBar.setAlpha(1f);
|
||||
mSearchBar.setTranslationY(0f);
|
||||
updateListPaddingForSearchBar();
|
||||
applyChromeSlot(false, false);
|
||||
return;
|
||||
}
|
||||
cancelSearchBarAnimation();
|
||||
if (mSearchBarShown) {
|
||||
mSearchBar.setVisibility(View.VISIBLE);
|
||||
mSearchBar.setAlpha(1f);
|
||||
mSearchBar.setTranslationY(0f);
|
||||
updateListPaddingForSearchBar();
|
||||
mSearchBar.post(this::updateListPaddingForSearchBar);
|
||||
} else {
|
||||
mSearchBar.setVisibility(View.GONE);
|
||||
mSearchBar.setAlpha(1f);
|
||||
mSearchBar.setTranslationY(0f);
|
||||
updateListPaddingForSearchBar();
|
||||
}
|
||||
}
|
||||
|
||||
private void runSearchBarShowAnimation() {
|
||||
updateListPaddingForSearchBar();
|
||||
final float slide = getSearchBarSlideDistancePx();
|
||||
mSearchBar.setTranslationY(-slide);
|
||||
mSearchBar.setAlpha(0f);
|
||||
mSearchBar.animate()
|
||||
.translationY(0f)
|
||||
.alpha(1f)
|
||||
.setDuration(SEARCH_BAR_ANIM_MS)
|
||||
.setInterpolator(UiUtils.DEFAULT_INTERPOLATOR)
|
||||
.withEndAction(this::updateListPaddingForSearchBar);
|
||||
applyChromeSlot(mSearchBarShown, false);
|
||||
}
|
||||
|
||||
private void showSearchBar() {
|
||||
@@ -695,40 +807,14 @@ public class ConversationListFragment extends Fragment implements ConversationLi
|
||||
|| mSearchBar == null || mSearchBarShown) {
|
||||
return;
|
||||
}
|
||||
mSearchBarShown = true;
|
||||
cancelSearchBarAnimation();
|
||||
mSearchBar.setVisibility(View.VISIBLE);
|
||||
updateListPaddingForSearchBar();
|
||||
if (mSearchBar.getHeight() == 0) {
|
||||
mSearchBar.post(() -> {
|
||||
updateListPaddingForSearchBar();
|
||||
runSearchBarShowAnimation();
|
||||
});
|
||||
} else {
|
||||
runSearchBarShowAnimation();
|
||||
}
|
||||
applyChromeSlot(true, true);
|
||||
}
|
||||
|
||||
private void hideSearchBar() {
|
||||
if (mSearchBar == null || !mSearchBarShown) {
|
||||
return;
|
||||
}
|
||||
mSearchBarShown = false;
|
||||
cancelSearchBarAnimation();
|
||||
final float slide = getSearchBarSlideDistancePx();
|
||||
mSearchBar.animate()
|
||||
.translationY(-slide)
|
||||
.alpha(0f)
|
||||
.setDuration(SEARCH_BAR_ANIM_MS)
|
||||
.setInterpolator(UiUtils.DEFAULT_INTERPOLATOR)
|
||||
.withEndAction(() -> {
|
||||
if (!mSearchBarShown && mSearchBar != null) {
|
||||
mSearchBar.setVisibility(View.GONE);
|
||||
mSearchBar.setTranslationY(0f);
|
||||
mSearchBar.setAlpha(1f);
|
||||
updateListPaddingForSearchBar();
|
||||
}
|
||||
});
|
||||
applyChromeSlot(false, true);
|
||||
}
|
||||
|
||||
private void setupHomeChrome(final View rootView) {
|
||||
@@ -838,7 +924,7 @@ public class ConversationListFragment extends Fragment implements ConversationLi
|
||||
? mHomeListProfileData.getConversationId() : null;
|
||||
final boolean sameProfile = data.getConversationId() != null
|
||||
&& data.getConversationId().equals(previousProfileId)
|
||||
&& mHomeListProfilePopup.getVisibility() == View.VISIBLE;
|
||||
&& isListProfilePopupShowing();
|
||||
if (sameProfile) {
|
||||
return;
|
||||
}
|
||||
@@ -874,13 +960,81 @@ public class ConversationListFragment extends Fragment implements ConversationLi
|
||||
mHomeListProfileContent.setAlpha(0f);
|
||||
mHomeListProfilePopup.setVisibility(View.INVISIBLE);
|
||||
bindListProfileBlur();
|
||||
ensureListProfileWindow();
|
||||
mHomeListProfilePopup.setVisibility(View.VISIBLE);
|
||||
showListProfileWindow();
|
||||
positionListProfilePopup(anchorView, () -> {
|
||||
mHomeListProfilePopup.setVisibility(View.VISIBLE);
|
||||
HomeUiMotion.listProfilePopupEnter(mHomeListProfileScrim, mHomeListProfileContent,
|
||||
anchorView);
|
||||
});
|
||||
}
|
||||
|
||||
private boolean isListProfilePopupShowing() {
|
||||
return mHomeListProfileWindow != null && mHomeListProfileWindow.isShowing();
|
||||
}
|
||||
|
||||
private void ensureListProfileWindow() {
|
||||
if (mHomeListProfilePopup == null || mHomeListProfileWindow != null) {
|
||||
return;
|
||||
}
|
||||
if (mHomeListProfilePopup.getParent() instanceof ViewGroup) {
|
||||
((ViewGroup) mHomeListProfilePopup.getParent()).removeView(mHomeListProfilePopup);
|
||||
}
|
||||
mHomeListProfileWindow = new PopupWindow(mHomeListProfilePopup,
|
||||
ViewGroup.LayoutParams.MATCH_PARENT,
|
||||
ViewGroup.LayoutParams.MATCH_PARENT,
|
||||
true);
|
||||
mHomeListProfileWindow.setOutsideTouchable(true);
|
||||
mHomeListProfileWindow.setFocusable(true);
|
||||
mHomeListProfileWindow.setClippingEnabled(false);
|
||||
mHomeListProfileWindow.setElevation(32f);
|
||||
mHomeListProfileWindow.setBackgroundDrawable(
|
||||
new android.graphics.drawable.ColorDrawable(android.graphics.Color.TRANSPARENT));
|
||||
mHomeListProfileWindow.setOnDismissListener(() -> {
|
||||
if (mHomeListProfilePopup != null) {
|
||||
mHomeListProfilePopup.setVisibility(View.GONE);
|
||||
}
|
||||
recycleListProfileBlur();
|
||||
mHomeListProfileData = null;
|
||||
});
|
||||
}
|
||||
|
||||
private void showListProfileWindow() {
|
||||
final Activity activity = getActivity();
|
||||
if (activity == null || mHomeListProfileWindow == null) {
|
||||
return;
|
||||
}
|
||||
final View decor = activity.getWindow().getDecorView();
|
||||
if (decor.getWidth() > 0 && decor.getHeight() > 0) {
|
||||
mHomeListProfileWindow.setWidth(decor.getWidth());
|
||||
mHomeListProfileWindow.setHeight(decor.getHeight());
|
||||
}
|
||||
if (!mHomeListProfileWindow.isShowing()) {
|
||||
mHomeListProfileWindow.showAtLocation(decor, Gravity.NO_GRAVITY, 0, 0);
|
||||
}
|
||||
}
|
||||
|
||||
private void dismissListProfileWindowImmediate() {
|
||||
if (mHomeListProfileWindow == null) {
|
||||
recycleListProfileBlur();
|
||||
mHomeListProfileData = null;
|
||||
if (mHomeListProfilePopup != null) {
|
||||
mHomeListProfilePopup.setVisibility(View.GONE);
|
||||
}
|
||||
return;
|
||||
}
|
||||
mHomeListProfileWindow.setOnDismissListener(null);
|
||||
if (mHomeListProfileWindow.isShowing()) {
|
||||
mHomeListProfileWindow.dismiss();
|
||||
}
|
||||
mHomeListProfileWindow = null;
|
||||
if (mHomeListProfilePopup != null) {
|
||||
mHomeListProfilePopup.setVisibility(View.GONE);
|
||||
}
|
||||
recycleListProfileBlur();
|
||||
mHomeListProfileData = null;
|
||||
}
|
||||
|
||||
private void updateListProfileActionsPillWidth() {
|
||||
if (mHomeListProfileActions == null) {
|
||||
return;
|
||||
@@ -1003,17 +1157,19 @@ public class ConversationListFragment extends Fragment implements ConversationLi
|
||||
}
|
||||
|
||||
private void dismissHomeListProfilePopup(final boolean animate) {
|
||||
if (mHomeListProfilePopup == null
|
||||
|| mHomeListProfilePopup.getVisibility() != View.VISIBLE) {
|
||||
if (!isListProfilePopupShowing()) {
|
||||
recycleListProfileBlur();
|
||||
mHomeListProfileData = null;
|
||||
if (mHomeListProfilePopup != null) {
|
||||
mHomeListProfilePopup.setVisibility(View.GONE);
|
||||
}
|
||||
return;
|
||||
}
|
||||
final Runnable finish = () -> {
|
||||
mHomeListProfilePopup.setVisibility(View.GONE);
|
||||
resetListProfilePopupPosition();
|
||||
recycleListProfileBlur();
|
||||
mHomeListProfileData = null;
|
||||
if (mHomeListProfileWindow != null && mHomeListProfileWindow.isShowing()) {
|
||||
mHomeListProfileWindow.dismiss();
|
||||
}
|
||||
};
|
||||
if (animate) {
|
||||
HomeUiMotion.listProfilePopupExit(mHomeListProfileScrim, mHomeListProfileContent,
|
||||
@@ -1079,14 +1235,7 @@ public class ConversationListFragment extends Fragment implements ConversationLi
|
||||
}
|
||||
if (enabled) {
|
||||
setPinnedEditMode(false, false);
|
||||
mSearchBarShown = false;
|
||||
cancelSearchBarAnimation();
|
||||
if (mSearchBar != null) {
|
||||
mSearchBar.setVisibility(View.GONE);
|
||||
mSearchBar.setAlpha(1f);
|
||||
mSearchBar.setTranslationY(0f);
|
||||
}
|
||||
updateListPaddingForSearchBar();
|
||||
applyChromeSlot(false, false);
|
||||
}
|
||||
updateHomeSelectionBar(0);
|
||||
updateHomeComposeButtonVisibility();
|
||||
@@ -1129,10 +1278,10 @@ public class ConversationListFragment extends Fragment implements ConversationLi
|
||||
if (mHomeMessagesTop != null) {
|
||||
mHomeMessagesTop.setVisibility(View.VISIBLE);
|
||||
}
|
||||
if (mHomeCategoryStrip != null) {
|
||||
mHomeCategoryStrip.setVisibility(showMessages ? View.VISIBLE : View.GONE);
|
||||
} else if (mHomeCategoryScroll != null) {
|
||||
mHomeCategoryScroll.setVisibility(showMessages ? View.VISIBLE : View.GONE);
|
||||
if (showMessages) {
|
||||
setCategoryStripShown(!mSearchBarShown, false);
|
||||
} else {
|
||||
setCategoryStripShown(false, false);
|
||||
}
|
||||
final CharSequence title = getString(showMessages
|
||||
? R.string.home_title : R.string.home_automation_title);
|
||||
@@ -1156,14 +1305,7 @@ public class ConversationListFragment extends Fragment implements ConversationLi
|
||||
}
|
||||
}
|
||||
if (!showMessages) {
|
||||
mSearchBarShown = false;
|
||||
cancelSearchBarAnimation();
|
||||
if (mSearchBar != null) {
|
||||
mSearchBar.setVisibility(View.GONE);
|
||||
mSearchBar.setAlpha(1f);
|
||||
mSearchBar.setTranslationY(0f);
|
||||
}
|
||||
updateListPaddingForSearchBar();
|
||||
applyChromeSlot(false, false);
|
||||
if (mEmptyListMessageView != null) {
|
||||
mEmptyListMessageView.setVisibility(View.GONE);
|
||||
}
|
||||
@@ -1175,10 +1317,15 @@ public class ConversationListFragment extends Fragment implements ConversationLi
|
||||
updateHomeNavHighlight(animate);
|
||||
updateHomeMainNavVisibility();
|
||||
refreshPinnedStrip();
|
||||
updateListTopFade();
|
||||
mHomeTabReady = true;
|
||||
}
|
||||
|
||||
public boolean onHomeBackPressed() {
|
||||
if (isListProfilePopupShowing()) {
|
||||
dismissHomeListProfilePopup(true);
|
||||
return true;
|
||||
}
|
||||
if (mHomeTab == HOME_TAB_AUTOMATE) {
|
||||
selectHomeTab(HOME_TAB_MESSAGES);
|
||||
return true;
|
||||
@@ -1415,12 +1562,15 @@ public class ConversationListFragment extends Fragment implements ConversationLi
|
||||
return;
|
||||
}
|
||||
mHomeListProfileBlurBitmap = captureDecorSnapshot();
|
||||
if (mHomeListProfileBlurBitmap == null) {
|
||||
mHomeListProfileBlurBitmap = captureViewSnapshot(getView());
|
||||
}
|
||||
if (mHomeListProfileBlurBitmap == null) {
|
||||
return;
|
||||
}
|
||||
mHomeListProfileBlur.setImageBitmap(mHomeListProfileBlurBitmap);
|
||||
mHomeListProfileBlur.setRenderEffect(RenderEffect.createBlurEffect(
|
||||
36f, 36f, Shader.TileMode.CLAMP));
|
||||
64f, 64f, Shader.TileMode.CLAMP));
|
||||
}
|
||||
|
||||
private void recycleListProfileBlur() {
|
||||
@@ -1700,8 +1850,11 @@ public class ConversationListFragment extends Fragment implements ConversationLi
|
||||
if (activity == null) {
|
||||
return null;
|
||||
}
|
||||
final View root = activity.getWindow().getDecorView();
|
||||
if (root.getWidth() <= 0 || root.getHeight() <= 0) {
|
||||
return captureViewSnapshot(activity.getWindow().getDecorView());
|
||||
}
|
||||
|
||||
private Bitmap captureViewSnapshot(final View root) {
|
||||
if (root == null || root.getWidth() <= 0 || root.getHeight() <= 0) {
|
||||
return null;
|
||||
}
|
||||
final Bitmap bitmap = Bitmap.createBitmap(
|
||||
@@ -1787,24 +1940,26 @@ public class ConversationListFragment extends Fragment implements ConversationLi
|
||||
mHomePopover = null;
|
||||
}
|
||||
});
|
||||
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];
|
||||
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 {
|
||||
xOff = 0;
|
||||
windowRight = getResources().getDisplayMetrics().widthPixels;
|
||||
}
|
||||
int xOff = alignEnd
|
||||
? windowRight - endInset - content.getMeasuredWidth() - anchorLoc[0]
|
||||
: 0;
|
||||
final int overflow = (anchorLoc[0] + xOff + content.getMeasuredWidth())
|
||||
- (windowRight - endInset);
|
||||
if (overflow > 0) {
|
||||
xOff -= overflow;
|
||||
}
|
||||
popup.showAsDropDown(anchor, xOff, 8);
|
||||
HomeUiMotion.popoverEnter(content, alignEnd);
|
||||
@@ -1840,6 +1995,10 @@ public class ConversationListFragment extends Fragment implements ConversationLi
|
||||
selectHomeChip(ConversationListAdapter.FILTER_ALL);
|
||||
dismissHomePopover();
|
||||
});
|
||||
content.findViewById(R.id.popover_share_app).setOnClickListener(v -> {
|
||||
dismissHomePopover();
|
||||
shareThisApp();
|
||||
});
|
||||
content.findViewById(R.id.popover_settings).setOnClickListener(v -> {
|
||||
dismissHomePopover();
|
||||
launchHomeSubScreen(() -> {
|
||||
@@ -1850,8 +2009,21 @@ public class ConversationListFragment extends Fragment implements ConversationLi
|
||||
});
|
||||
}
|
||||
|
||||
private void shareThisApp() {
|
||||
final Context context = getContext();
|
||||
if (context == null) {
|
||||
return;
|
||||
}
|
||||
final String storeUrl = "https://play.google.com/store/apps/details?id="
|
||||
+ context.getPackageName();
|
||||
final Intent share = new Intent(Intent.ACTION_SEND);
|
||||
share.setType("text/plain");
|
||||
share.putExtra(Intent.EXTRA_TEXT, getString(R.string.home_share_app_text, storeUrl));
|
||||
startActivity(Intent.createChooser(share, getString(R.string.home_popover_share_app)));
|
||||
}
|
||||
|
||||
private void showHomeEditPopover(final View anchor) {
|
||||
final PopupWindow popup = showHomePopover(anchor, R.layout.home_popover_edit, false);
|
||||
final PopupWindow popup = showHomePopover(anchor, R.layout.home_popover_edit, true);
|
||||
final View content = popup.getContentView();
|
||||
content.findViewById(R.id.popover_select_messages).setOnClickListener(v -> {
|
||||
dismissHomePopoverImmediate();
|
||||
@@ -2107,6 +2279,7 @@ public class ConversationListFragment extends Fragment implements ConversationLi
|
||||
@Override
|
||||
public void onPause() {
|
||||
super.onPause();
|
||||
dismissHomeListProfilePopup(false);
|
||||
dismissHomePopover();
|
||||
if (mHomeBottomNav != null && !mArchiveMode && !mForwardMessageMode) {
|
||||
mHomeBottomNav.setVisibility(View.GONE);
|
||||
@@ -2139,6 +2312,9 @@ public class ConversationListFragment extends Fragment implements ConversationLi
|
||||
|
||||
public void updateUi() {
|
||||
mAdapter.notifyDataSetChanged();
|
||||
if (mRecyclerView != null) {
|
||||
mRecyclerView.post(this::updateListTopFade);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -2382,13 +2558,8 @@ public class ConversationListFragment extends Fragment implements ConversationLi
|
||||
if (mSearchBar == null) {
|
||||
return;
|
||||
}
|
||||
cancelSearchBarAnimation();
|
||||
if (mHomePinnedEditMode) {
|
||||
mSearchBarShown = false;
|
||||
mSearchBar.setVisibility(View.GONE);
|
||||
mSearchBar.setAlpha(1f);
|
||||
mSearchBar.setTranslationY(0f);
|
||||
updateListPaddingForSearchBar();
|
||||
applyChromeSlot(false, false);
|
||||
} else {
|
||||
applySearchBarVisibility();
|
||||
}
|
||||
|
||||
@@ -0,0 +1,294 @@
|
||||
package com.android.messaging.ui.home;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.app.Application;
|
||||
import android.content.Intent;
|
||||
import android.os.Bundle;
|
||||
import android.text.TextUtils;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.lifecycle.Lifecycle;
|
||||
import androidx.lifecycle.LifecycleEventObserver;
|
||||
import androidx.lifecycle.ProcessLifecycleOwner;
|
||||
|
||||
import com.android.messaging.util.BuglePrefs;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.security.MessageDigest;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
|
||||
/** App lock credentials, session, and process-level unlock gate (File Plus behavior). */
|
||||
public final class HomeAppLock {
|
||||
|
||||
public enum Method {
|
||||
NONE, PIN, PATTERN, PASSWORD;
|
||||
|
||||
static Method fromStored(@Nullable final String value) {
|
||||
if (TextUtils.isEmpty(value)) {
|
||||
return NONE;
|
||||
}
|
||||
try {
|
||||
return Method.valueOf(value);
|
||||
} catch (final IllegalArgumentException ignored) {
|
||||
return NONE;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static final String EXTRA_MODE = "home_app_lock_mode";
|
||||
public static final int MODE_UNLOCK = 0;
|
||||
public static final int MODE_SET_PIN = 1;
|
||||
public static final int MODE_SET_PATTERN = 2;
|
||||
public static final int MODE_SET_PASSWORD = 3;
|
||||
|
||||
private static final String KEY_ENABLED = "home_app_lock_enabled";
|
||||
private static final String KEY_METHOD = "home_app_lock_method";
|
||||
private static final String KEY_HASH = "home_app_lock_hash";
|
||||
private static final String HASH_SALT = "Messaging.Security.v1";
|
||||
|
||||
private static boolean sUnlocked;
|
||||
private static boolean sShowingUnlock;
|
||||
|
||||
private HomeAppLock() {
|
||||
}
|
||||
|
||||
public static void install(@NonNull final Application application) {
|
||||
ProcessLifecycleOwner.get().getLifecycle().addObserver(
|
||||
(LifecycleEventObserver) (source, event) -> {
|
||||
if (event == Lifecycle.Event.ON_STOP && requiresUnlock()) {
|
||||
sUnlocked = false;
|
||||
}
|
||||
});
|
||||
application.registerActivityLifecycleCallbacks(new Application.ActivityLifecycleCallbacks() {
|
||||
@Override
|
||||
public void onActivityCreated(@NonNull final Activity activity,
|
||||
@Nullable final Bundle savedInstanceState) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onActivityStarted(@NonNull final Activity activity) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onActivityResumed(@NonNull final Activity activity) {
|
||||
maybeLock(activity);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onActivityPaused(@NonNull final Activity activity) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onActivityStopped(@NonNull final Activity activity) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onActivitySaveInstanceState(@NonNull final Activity activity,
|
||||
@NonNull final Bundle outState) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onActivityDestroyed(@NonNull final Activity activity) {
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public static boolean isEnabled() {
|
||||
return prefs().getBoolean(KEY_ENABLED, false);
|
||||
}
|
||||
|
||||
public static void setEnabled(final boolean enabled) {
|
||||
prefs().putBoolean(KEY_ENABLED, enabled);
|
||||
if (enabled) {
|
||||
markUnlocked();
|
||||
}
|
||||
}
|
||||
|
||||
public static Method getMethod() {
|
||||
return Method.fromStored(prefs().getString(KEY_METHOD, Method.NONE.name()));
|
||||
}
|
||||
|
||||
public static boolean hasLockConfigured() {
|
||||
return getMethod() != Method.NONE && !TextUtils.isEmpty(prefs().getString(KEY_HASH, null));
|
||||
}
|
||||
|
||||
public static boolean requiresUnlock() {
|
||||
return isEnabled() && hasLockConfigured();
|
||||
}
|
||||
|
||||
public static void setPin(@NonNull final String pin) {
|
||||
if (pin.length() != 6 || !pin.matches("\\d{6}")) {
|
||||
throw new IllegalArgumentException("PIN must be 6 digits");
|
||||
}
|
||||
store(Method.PIN, pin);
|
||||
}
|
||||
|
||||
public static void setPattern(@NonNull final List<Integer> cells) {
|
||||
if (cells.size() < 4) {
|
||||
throw new IllegalArgumentException("Pattern too short");
|
||||
}
|
||||
store(Method.PATTERN, joinCells(cells));
|
||||
}
|
||||
|
||||
public static void setPassword(@NonNull final String password) {
|
||||
if (password.length() < 8) {
|
||||
throw new IllegalArgumentException("Password too short");
|
||||
}
|
||||
store(Method.PASSWORD, password);
|
||||
}
|
||||
|
||||
public static boolean verifyPin(@NonNull final String pin) {
|
||||
return verify(Method.PIN, pin);
|
||||
}
|
||||
|
||||
public static boolean verifyPattern(@NonNull final List<Integer> cells) {
|
||||
return verify(Method.PATTERN, joinCells(cells));
|
||||
}
|
||||
|
||||
public static boolean verifyPassword(@NonNull final String password) {
|
||||
return verify(Method.PASSWORD, password);
|
||||
}
|
||||
|
||||
public static boolean isUnlockedThisSession() {
|
||||
return sUnlocked;
|
||||
}
|
||||
|
||||
public static void markUnlocked() {
|
||||
sUnlocked = true;
|
||||
}
|
||||
|
||||
public static void clearUnlocked() {
|
||||
sUnlocked = false;
|
||||
}
|
||||
|
||||
static void markUnlockShowing(final boolean showing) {
|
||||
sShowingUnlock = showing;
|
||||
}
|
||||
|
||||
public static Intent unlockIntent(@NonNull final Activity activity) {
|
||||
return createIntent(activity, MODE_UNLOCK);
|
||||
}
|
||||
|
||||
public static Intent createIntent(@NonNull final Activity activity, final int mode) {
|
||||
final Intent intent = new Intent(activity, HomeAppLockActivity.class);
|
||||
intent.putExtra(EXTRA_MODE, mode);
|
||||
return intent;
|
||||
}
|
||||
|
||||
static List<Integer> appendPatternCells(@NonNull final List<Integer> current, final int next) {
|
||||
if (current.isEmpty()) {
|
||||
final List<Integer> start = new ArrayList<>();
|
||||
start.add(next);
|
||||
return start;
|
||||
}
|
||||
final int last = current.get(current.size() - 1);
|
||||
if (next == last) {
|
||||
return current;
|
||||
}
|
||||
final Integer bridge = bridgedCellIndex(last, next);
|
||||
final List<Integer> out = new ArrayList<>(current);
|
||||
if (bridge != null && !current.contains(bridge)) {
|
||||
out.add(bridge);
|
||||
}
|
||||
out.add(next);
|
||||
return out;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
static Integer bridgedCellIndex(final int from, final int to) {
|
||||
final int fromRow = from / 3;
|
||||
final int fromCol = from % 3;
|
||||
final int toRow = to / 3;
|
||||
final int toCol = to % 3;
|
||||
final int dRow = toRow - fromRow;
|
||||
final int dCol = toCol - fromCol;
|
||||
if (dRow == 0 && dCol == 0) {
|
||||
return null;
|
||||
}
|
||||
if (dRow != 0 && dCol != 0 && Math.abs(dRow) != Math.abs(dCol)) {
|
||||
return null;
|
||||
}
|
||||
if (dRow != 0 && dRow % 2 != 0 && Math.abs(dRow) > 1) {
|
||||
return null;
|
||||
}
|
||||
if (dCol != 0 && dCol % 2 != 0 && Math.abs(dCol) > 1) {
|
||||
return null;
|
||||
}
|
||||
final int midRow = fromRow + dRow / 2;
|
||||
final int midCol = fromCol + dCol / 2;
|
||||
if (midRow == fromRow && midCol == fromCol) {
|
||||
return null;
|
||||
}
|
||||
if (midRow == toRow && midCol == toCol) {
|
||||
return null;
|
||||
}
|
||||
return midRow * 3 + midCol;
|
||||
}
|
||||
|
||||
private static void maybeLock(@NonNull final Activity activity) {
|
||||
if (!requiresUnlock() || sUnlocked || sShowingUnlock) {
|
||||
return;
|
||||
}
|
||||
if (activity instanceof HomeAppLockActivity) {
|
||||
return;
|
||||
}
|
||||
activity.getWindow().getDecorView().post(() -> {
|
||||
if (activity.isFinishing() || activity.isDestroyed()
|
||||
|| !requiresUnlock() || sUnlocked || sShowingUnlock) {
|
||||
return;
|
||||
}
|
||||
sShowingUnlock = true;
|
||||
activity.startActivity(unlockIntent(activity));
|
||||
activity.overridePendingTransition(0, 0);
|
||||
});
|
||||
}
|
||||
|
||||
private static void store(@NonNull final Method method, @NonNull final String secret) {
|
||||
final BuglePrefs prefs = prefs();
|
||||
prefs.putString(KEY_HASH, hash(secret));
|
||||
prefs.putString(KEY_METHOD, method.name());
|
||||
prefs.putBoolean(KEY_ENABLED, true);
|
||||
markUnlocked();
|
||||
}
|
||||
|
||||
private static boolean verify(@NonNull final Method method, @NonNull final String secret) {
|
||||
if (getMethod() != method) {
|
||||
return false;
|
||||
}
|
||||
final String stored = prefs().getString(KEY_HASH, null);
|
||||
return !TextUtils.isEmpty(stored) && stored.equals(hash(secret));
|
||||
}
|
||||
|
||||
private static String joinCells(@NonNull final List<Integer> cells) {
|
||||
final StringBuilder builder = new StringBuilder();
|
||||
for (int i = 0; i < cells.size(); i++) {
|
||||
if (i > 0) {
|
||||
builder.append(',');
|
||||
}
|
||||
builder.append(cells.get(i));
|
||||
}
|
||||
return builder.toString();
|
||||
}
|
||||
|
||||
private static String hash(@NonNull final String value) {
|
||||
try {
|
||||
final MessageDigest digest = MessageDigest.getInstance("SHA-256");
|
||||
final byte[] bytes = digest.digest(
|
||||
(HASH_SALT + "|" + value).getBytes(StandardCharsets.UTF_8));
|
||||
final StringBuilder hex = new StringBuilder(bytes.length * 2);
|
||||
for (final byte b : bytes) {
|
||||
hex.append(String.format(Locale.US, "%02x", b));
|
||||
}
|
||||
return hex.toString();
|
||||
} catch (final Exception e) {
|
||||
throw new IllegalStateException("Unable to hash lock secret", e);
|
||||
}
|
||||
}
|
||||
|
||||
private static BuglePrefs prefs() {
|
||||
return BuglePrefs.getApplicationPrefs();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,483 @@
|
||||
package com.android.messaging.ui.home;
|
||||
|
||||
import android.os.Bundle;
|
||||
import android.text.Editable;
|
||||
import android.text.TextUtils;
|
||||
import android.text.TextWatcher;
|
||||
import android.text.method.PasswordTransformationMethod;
|
||||
import android.view.KeyEvent;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.view.inputmethod.EditorInfo;
|
||||
import android.widget.EditText;
|
||||
import android.widget.FrameLayout;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.activity.OnBackPressedCallback;
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
import androidx.core.content.ContextCompat;
|
||||
|
||||
import com.android.messaging.R;
|
||||
import com.android.messaging.ui.BugleActionBarActivity;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/** Unlock overlay and PIN / pattern / password setup screens. */
|
||||
public class HomeAppLockActivity extends BugleActionBarActivity {
|
||||
|
||||
private int mMode;
|
||||
private int mStep;
|
||||
private String mFirstSecret = "";
|
||||
private final List<Integer> mFirstPattern = new ArrayList<>();
|
||||
private String mPin = "";
|
||||
private TextView mSubtitle;
|
||||
private TextView mError;
|
||||
private ViewGroup mPinDots;
|
||||
private HomePatternLockView mPatternView;
|
||||
private View mPatternRecorded;
|
||||
private EditText mPassword;
|
||||
private ImageView mPasswordToggle;
|
||||
private View mPasswordRules;
|
||||
private TextView mPasswordLabel;
|
||||
private boolean mPasswordVisible;
|
||||
|
||||
@Override
|
||||
protected void onCreate(final Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
if (getSupportActionBar() != null) {
|
||||
getSupportActionBar().hide();
|
||||
}
|
||||
mMode = getIntent().getIntExtra(HomeAppLock.EXTRA_MODE, HomeAppLock.MODE_UNLOCK);
|
||||
if (mMode == HomeAppLock.MODE_UNLOCK) {
|
||||
HomeAppLock.markUnlockShowing(true);
|
||||
if (!HomeAppLock.requiresUnlock()) {
|
||||
HomeAppLock.markUnlocked();
|
||||
finish();
|
||||
return;
|
||||
}
|
||||
setContentView(R.layout.home_lock_unlock);
|
||||
} else {
|
||||
setContentView(R.layout.home_lock_setup);
|
||||
HomeSubHeader.bind(this, headerTitle(), v -> finish());
|
||||
}
|
||||
mSubtitle = findViewById(R.id.home_lock_subtitle);
|
||||
mError = findViewById(R.id.home_lock_error);
|
||||
final FrameLayout body = findViewById(R.id.home_lock_body);
|
||||
final LayoutInflater inflater = LayoutInflater.from(this);
|
||||
if (isPinMode()) {
|
||||
inflater.inflate(R.layout.home_lock_pin_body, body, true);
|
||||
bindPinPad(body);
|
||||
} else if (isPatternMode()) {
|
||||
inflater.inflate(R.layout.home_lock_pattern_body, body, true);
|
||||
bindPattern(body);
|
||||
} else {
|
||||
inflater.inflate(R.layout.home_lock_password_body, body, true);
|
||||
bindPassword(body);
|
||||
}
|
||||
refreshCopy();
|
||||
getOnBackPressedDispatcher().addCallback(this, new OnBackPressedCallback(true) {
|
||||
@Override
|
||||
public void handleOnBackPressed() {
|
||||
if (mMode == HomeAppLock.MODE_UNLOCK) {
|
||||
moveTaskToBack(true);
|
||||
} else {
|
||||
finish();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDestroy() {
|
||||
if (mMode == HomeAppLock.MODE_UNLOCK && isFinishing()) {
|
||||
HomeAppLock.markUnlockShowing(false);
|
||||
}
|
||||
super.onDestroy();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void finish() {
|
||||
super.finish();
|
||||
if (mMode != HomeAppLock.MODE_UNLOCK) {
|
||||
overridePendingTransition(R.anim.home_activity_hold, R.anim.home_activity_close);
|
||||
} else {
|
||||
overridePendingTransition(0, 0);
|
||||
}
|
||||
}
|
||||
|
||||
private void bindPinPad(final View host) {
|
||||
mPinDots = host.findViewById(R.id.home_lock_pin_dots);
|
||||
final ViewGroup keypad = host.findViewById(R.id.home_lock_pin_keypad);
|
||||
bindDigitKeys(keypad);
|
||||
final View backspace = host.findViewById(R.id.home_lock_pin_backspace);
|
||||
if (backspace != null) {
|
||||
backspace.setOnClickListener(v -> {
|
||||
if (!mPin.isEmpty()) {
|
||||
mPin = mPin.substring(0, mPin.length() - 1);
|
||||
updatePinDots();
|
||||
hideError();
|
||||
}
|
||||
});
|
||||
}
|
||||
updatePinDots();
|
||||
}
|
||||
|
||||
private void bindDigitKeys(final ViewGroup parent) {
|
||||
for (int i = 0; i < parent.getChildCount(); i++) {
|
||||
final View child = parent.getChildAt(i);
|
||||
if (child instanceof ViewGroup) {
|
||||
bindDigitKeys((ViewGroup) child);
|
||||
} else if (child instanceof TextView) {
|
||||
final String text = ((TextView) child).getText().toString();
|
||||
if (text.length() == 1 && Character.isDigit(text.charAt(0))) {
|
||||
child.setOnClickListener(v -> appendPin(text));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void appendPin(final String digit) {
|
||||
if (mPin.length() >= 6) {
|
||||
return;
|
||||
}
|
||||
mPin += digit;
|
||||
updatePinDots();
|
||||
hideError();
|
||||
if (mPin.length() < 6) {
|
||||
return;
|
||||
}
|
||||
if (mMode == HomeAppLock.MODE_UNLOCK) {
|
||||
if (HomeAppLock.verifyPin(mPin)) {
|
||||
unlockAndFinish();
|
||||
} else {
|
||||
showError(R.string.home_lock_incorrect_pin);
|
||||
mPin = "";
|
||||
updatePinDots();
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (mStep == 0) {
|
||||
mFirstSecret = mPin;
|
||||
mPin = "";
|
||||
mStep = 1;
|
||||
updatePinDots();
|
||||
refreshCopy();
|
||||
} else if (mFirstSecret.equals(mPin)) {
|
||||
HomeAppLock.setPin(mPin);
|
||||
finish();
|
||||
} else {
|
||||
showMismatch();
|
||||
}
|
||||
}
|
||||
|
||||
private void updatePinDots() {
|
||||
if (mPinDots == null) {
|
||||
return;
|
||||
}
|
||||
for (int i = 0; i < mPinDots.getChildCount(); i++) {
|
||||
mPinDots.getChildAt(i).setSelected(i < mPin.length());
|
||||
}
|
||||
}
|
||||
|
||||
private void bindPattern(final View host) {
|
||||
mPatternView = host.findViewById(R.id.home_lock_pattern);
|
||||
mPatternRecorded = host.findViewById(R.id.home_lock_pattern_recorded);
|
||||
host.findViewById(R.id.home_lock_pattern_clear).setOnClickListener(v -> {
|
||||
if (mPatternView != null) {
|
||||
mPatternView.clear();
|
||||
}
|
||||
if (mPatternRecorded != null) {
|
||||
mPatternRecorded.setVisibility(View.GONE);
|
||||
}
|
||||
hideError();
|
||||
});
|
||||
mPatternView.setListener(new HomePatternLockView.Listener() {
|
||||
@Override
|
||||
public void onPatternChanged(final List<Integer> cells) {
|
||||
hideError();
|
||||
if (mPatternRecorded != null) {
|
||||
mPatternRecorded.setVisibility(
|
||||
cells.size() >= 4 ? View.VISIBLE : View.GONE);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPatternComplete(final List<Integer> cells) {
|
||||
if (mMode == HomeAppLock.MODE_UNLOCK) {
|
||||
if (HomeAppLock.verifyPattern(cells)) {
|
||||
unlockAndFinish();
|
||||
} else {
|
||||
showError(R.string.home_lock_incorrect_pattern);
|
||||
mPatternView.clear();
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (cells.size() < 4) {
|
||||
mPatternView.clear();
|
||||
return;
|
||||
}
|
||||
if (mStep == 0) {
|
||||
mFirstPattern.clear();
|
||||
mFirstPattern.addAll(cells);
|
||||
mPatternView.clear();
|
||||
mStep = 1;
|
||||
if (mPatternRecorded != null) {
|
||||
mPatternRecorded.setVisibility(View.GONE);
|
||||
}
|
||||
refreshCopy();
|
||||
} else if (mFirstPattern.equals(cells)) {
|
||||
HomeAppLock.setPattern(cells);
|
||||
finish();
|
||||
} else {
|
||||
showMismatch();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void bindPassword(final View host) {
|
||||
mPassword = host.findViewById(R.id.home_lock_password);
|
||||
mPasswordToggle = host.findViewById(R.id.home_lock_password_toggle);
|
||||
mPasswordRules = host.findViewById(R.id.home_lock_password_rules);
|
||||
mPasswordLabel = host.findViewById(R.id.home_lock_password_label);
|
||||
if (mMode != HomeAppLock.MODE_UNLOCK) {
|
||||
mPasswordRules.setVisibility(View.VISIBLE);
|
||||
}
|
||||
mPasswordToggle.setOnClickListener(v -> {
|
||||
mPasswordVisible = !mPasswordVisible;
|
||||
final int start = mPassword.getSelectionStart();
|
||||
mPassword.setTransformationMethod(mPasswordVisible
|
||||
? null : PasswordTransformationMethod.getInstance());
|
||||
mPasswordToggle.setImageResource(mPasswordVisible
|
||||
? R.drawable.ic_home_visibility_off : R.drawable.ic_home_visibility);
|
||||
if (start >= 0) {
|
||||
mPassword.setSelection(start);
|
||||
}
|
||||
});
|
||||
mPassword.setOnEditorActionListener((v, actionId, event) -> {
|
||||
if (actionId == EditorInfo.IME_ACTION_DONE
|
||||
|| (event != null && event.getKeyCode() == KeyEvent.KEYCODE_ENTER)) {
|
||||
attemptPassword();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
if (mMode != HomeAppLock.MODE_UNLOCK) {
|
||||
mPassword.requestFocus();
|
||||
}
|
||||
mPassword.addTextChangedListener(new TextWatcher() {
|
||||
@Override
|
||||
public void beforeTextChanged(final CharSequence s, final int start, final int count,
|
||||
final int after) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onTextChanged(final CharSequence s, final int start, final int before,
|
||||
final int count) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void afterTextChanged(final Editable s) {
|
||||
hideError();
|
||||
final String value = s.toString();
|
||||
if (mMode == HomeAppLock.MODE_UNLOCK) {
|
||||
if (value.length() >= 8 && HomeAppLock.verifyPassword(value)) {
|
||||
unlockAndFinish();
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (mStep == 0) {
|
||||
updatePasswordRules(value);
|
||||
if (isPasswordValid(value)) {
|
||||
mFirstSecret = value;
|
||||
mStep = 1;
|
||||
mPassword.setText("");
|
||||
refreshCopy();
|
||||
}
|
||||
} else if (value.length() >= mFirstSecret.length()) {
|
||||
if (mFirstSecret.equals(value)) {
|
||||
HomeAppLock.setPassword(value);
|
||||
finish();
|
||||
} else {
|
||||
showMismatch();
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void attemptPassword() {
|
||||
final String value = mPassword.getText() == null ? "" : mPassword.getText().toString();
|
||||
if (HomeAppLock.verifyPassword(value)) {
|
||||
unlockAndFinish();
|
||||
} else if (!TextUtils.isEmpty(value)) {
|
||||
showError(R.string.home_lock_incorrect_password);
|
||||
mPassword.setText("");
|
||||
}
|
||||
}
|
||||
|
||||
private void updatePasswordRules(final String password) {
|
||||
setRule(R.id.home_lock_rule_length, password.length() >= 8);
|
||||
setRule(R.id.home_lock_rule_number, containsDigit(password));
|
||||
setRule(R.id.home_lock_rule_symbol, containsSymbol(password));
|
||||
setRule(R.id.home_lock_rule_upper, containsUpper(password));
|
||||
}
|
||||
|
||||
private void setRule(final int id, final boolean ok) {
|
||||
final TextView view = findViewById(id);
|
||||
if (view == null) {
|
||||
return;
|
||||
}
|
||||
view.setTextColor(getColor(ok ? R.color.home_text_primary : R.color.home_text_secondary));
|
||||
view.setCompoundDrawableTintList(ContextCompat.getColorStateList(this,
|
||||
ok ? R.color.home_accent : R.color.home_text_tertiary));
|
||||
}
|
||||
|
||||
private static boolean isPasswordValid(final String password) {
|
||||
return password.length() >= 8 && containsDigit(password)
|
||||
&& containsSymbol(password) && containsUpper(password);
|
||||
}
|
||||
|
||||
private static boolean containsDigit(final String value) {
|
||||
for (int i = 0; i < value.length(); i++) {
|
||||
if (Character.isDigit(value.charAt(i))) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private static boolean containsSymbol(final String value) {
|
||||
for (int i = 0; i < value.length(); i++) {
|
||||
if (!Character.isLetterOrDigit(value.charAt(i))) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private static boolean containsUpper(final String value) {
|
||||
for (int i = 0; i < value.length(); i++) {
|
||||
if (Character.isUpperCase(value.charAt(i))) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private void refreshCopy() {
|
||||
if (mSubtitle != null) {
|
||||
mSubtitle.setText(subtitle());
|
||||
}
|
||||
if (mPasswordLabel != null) {
|
||||
mPasswordLabel.setText(mStep == 0
|
||||
? R.string.home_lock_password_hint
|
||||
: R.string.home_lock_password_confirm_hint);
|
||||
}
|
||||
if (mPasswordRules != null) {
|
||||
mPasswordRules.setVisibility(mMode != HomeAppLock.MODE_UNLOCK && mStep == 0
|
||||
? View.VISIBLE : View.GONE);
|
||||
}
|
||||
}
|
||||
|
||||
private int headerTitle() {
|
||||
switch (mMode) {
|
||||
case HomeAppLock.MODE_SET_PIN:
|
||||
return R.string.home_lock_set_pin;
|
||||
case HomeAppLock.MODE_SET_PATTERN:
|
||||
return R.string.home_lock_set_pattern;
|
||||
case HomeAppLock.MODE_SET_PASSWORD:
|
||||
return R.string.home_lock_set_password;
|
||||
default:
|
||||
return R.string.home_title;
|
||||
}
|
||||
}
|
||||
|
||||
private int subtitle() {
|
||||
if (mMode == HomeAppLock.MODE_UNLOCK) {
|
||||
switch (HomeAppLock.getMethod()) {
|
||||
case PIN:
|
||||
return R.string.home_lock_enter_pin;
|
||||
case PATTERN:
|
||||
return R.string.home_lock_enter_pattern;
|
||||
case PASSWORD:
|
||||
return R.string.home_lock_enter_password;
|
||||
default:
|
||||
return R.string.home_lock_enter_title;
|
||||
}
|
||||
}
|
||||
if (mMode == HomeAppLock.MODE_SET_PIN) {
|
||||
return mStep == 0 ? R.string.home_lock_create_pin_subtitle
|
||||
: R.string.home_lock_confirm_pin;
|
||||
}
|
||||
if (mMode == HomeAppLock.MODE_SET_PATTERN) {
|
||||
return mStep == 0 ? R.string.home_lock_create_pattern_subtitle
|
||||
: R.string.home_lock_confirm_pattern;
|
||||
}
|
||||
return mStep == 0 ? R.string.home_lock_create_password_subtitle
|
||||
: R.string.home_lock_confirm_password;
|
||||
}
|
||||
|
||||
private boolean isPinMode() {
|
||||
return mMode == HomeAppLock.MODE_SET_PIN
|
||||
|| (mMode == HomeAppLock.MODE_UNLOCK
|
||||
&& HomeAppLock.getMethod() == HomeAppLock.Method.PIN);
|
||||
}
|
||||
|
||||
private boolean isPatternMode() {
|
||||
return mMode == HomeAppLock.MODE_SET_PATTERN
|
||||
|| (mMode == HomeAppLock.MODE_UNLOCK
|
||||
&& HomeAppLock.getMethod() == HomeAppLock.Method.PATTERN);
|
||||
}
|
||||
|
||||
private void showError(final int messageRes) {
|
||||
if (mError == null) {
|
||||
return;
|
||||
}
|
||||
mError.setText(messageRes);
|
||||
mError.setVisibility(View.VISIBLE);
|
||||
}
|
||||
|
||||
private void hideError() {
|
||||
if (mError != null) {
|
||||
mError.setVisibility(View.GONE);
|
||||
}
|
||||
}
|
||||
|
||||
private void showMismatch() {
|
||||
new AlertDialog.Builder(this, R.style.Home_AlertDialog)
|
||||
.setTitle(R.string.home_lock_mismatch_title)
|
||||
.setMessage(R.string.home_lock_mismatch_message)
|
||||
.setPositiveButton(android.R.string.ok, (dialog, which) -> resetSetup())
|
||||
.setOnDismissListener(dialog -> resetSetup())
|
||||
.show();
|
||||
}
|
||||
|
||||
private void resetSetup() {
|
||||
mStep = 0;
|
||||
mPin = "";
|
||||
mFirstSecret = "";
|
||||
mFirstPattern.clear();
|
||||
updatePinDots();
|
||||
if (mPatternView != null) {
|
||||
mPatternView.clear();
|
||||
}
|
||||
if (mPatternRecorded != null) {
|
||||
mPatternRecorded.setVisibility(View.GONE);
|
||||
}
|
||||
if (mPassword != null) {
|
||||
mPassword.setText("");
|
||||
}
|
||||
refreshCopy();
|
||||
hideError();
|
||||
}
|
||||
|
||||
private void unlockAndFinish() {
|
||||
HomeAppLock.markUnlocked();
|
||||
HomeAppLock.markUnlockShowing(false);
|
||||
finish();
|
||||
}
|
||||
}
|
||||
@@ -18,6 +18,8 @@ package com.android.messaging.ui.home;
|
||||
|
||||
import android.content.Intent;
|
||||
import android.os.Bundle;
|
||||
import android.os.Handler;
|
||||
import android.os.Looper;
|
||||
import android.text.Editable;
|
||||
import android.text.TextUtils;
|
||||
import android.text.TextWatcher;
|
||||
@@ -36,10 +38,14 @@ import com.android.messaging.ui.BugleActionBarActivity;
|
||||
import com.android.messaging.ui.ContactIconView;
|
||||
import com.android.messaging.util.AvatarUriUtil;
|
||||
import com.android.messaging.util.ContactRecipientEntryUtils;
|
||||
import com.android.messaging.util.ContactUtil;
|
||||
import com.android.messaging.util.ImeUtil;
|
||||
import com.google.android.material.bottomsheet.BottomSheetDialog;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
|
||||
/** Home-style "new chat" screen: recipient field, group entry point and contact list. */
|
||||
public class HomeComposeActivity extends BugleActionBarActivity
|
||||
@@ -57,7 +63,11 @@ public class HomeComposeActivity extends BugleActionBarActivity
|
||||
private LinearLayout mChipContainer;
|
||||
private TextView mChipHint;
|
||||
private TextView mNextButton;
|
||||
private TextView mAccountLabel;
|
||||
private boolean mGroupMode;
|
||||
private ContactUtil.AccountFilter mAccountFilter;
|
||||
private final ExecutorService mAccountExecutor = Executors.newSingleThreadExecutor();
|
||||
private final Handler mMainHandler = new Handler(Looper.getMainLooper());
|
||||
|
||||
@Override
|
||||
protected void onCreate(final Bundle savedInstanceState) {
|
||||
@@ -125,6 +135,10 @@ public class HomeComposeActivity extends BugleActionBarActivity
|
||||
|
||||
mCreateGroupButton = findViewById(R.id.home_compose_create_group);
|
||||
mCreateGroupButton.setOnClickListener(v -> enterGroupMode());
|
||||
|
||||
mAccountLabel = findViewById(R.id.home_compose_account_label);
|
||||
final View accountChip = findViewById(R.id.home_compose_account_chip);
|
||||
accountChip.setOnClickListener(v -> showAccountSheet());
|
||||
}
|
||||
|
||||
private HomeContactsFragment getContactsFragment() {
|
||||
@@ -132,6 +146,93 @@ public class HomeComposeActivity extends BugleActionBarActivity
|
||||
.findFragmentById(R.id.home_compose_fragment_host);
|
||||
}
|
||||
|
||||
private void showAccountSheet() {
|
||||
mAccountExecutor.execute(() -> {
|
||||
final List<ContactUtil.ContactAccount> accounts =
|
||||
ContactUtil.getContactAccounts(this);
|
||||
mMainHandler.post(() -> presentAccountSheet(accounts));
|
||||
});
|
||||
}
|
||||
|
||||
private void presentAccountSheet(final List<ContactUtil.ContactAccount> accounts) {
|
||||
if (isFinishing() || isDestroyed()) {
|
||||
return;
|
||||
}
|
||||
final LayoutInflater inflater = LayoutInflater.from(this);
|
||||
final LinearLayout list = (LinearLayout) inflater.inflate(
|
||||
R.layout.home_compose_account_sheet, null, false);
|
||||
final BottomSheetDialog[] dialog = new BottomSheetDialog[1];
|
||||
|
||||
int allCount = 0;
|
||||
for (final ContactUtil.ContactAccount account : accounts) {
|
||||
allCount += account.contactCount;
|
||||
}
|
||||
list.addView(createAccountRow(inflater, list,
|
||||
getString(R.string.home_compose_all_contacts), null, allCount,
|
||||
mAccountFilter == null, () -> {
|
||||
applyAccountFilter(null, getString(R.string.home_compose_all_contacts));
|
||||
if (dialog[0] != null) {
|
||||
dialog[0].dismiss();
|
||||
}
|
||||
}));
|
||||
|
||||
for (final ContactUtil.ContactAccount account : accounts) {
|
||||
final String title = account.deviceOnly
|
||||
? getString(R.string.home_compose_device_contacts)
|
||||
: (!TextUtils.isEmpty(account.title) ? account.title
|
||||
: getString(R.string.home_compose_device_contacts));
|
||||
final String subtitle = account.deviceOnly ? null : account.subtitle;
|
||||
final boolean selected = mAccountFilter != null
|
||||
&& account.toFilter().matches(mAccountFilter);
|
||||
list.addView(createAccountRow(inflater, list, title, subtitle, account.contactCount,
|
||||
selected, () -> {
|
||||
applyAccountFilter(account.toFilter(), title);
|
||||
if (dialog[0] != null) {
|
||||
dialog[0].dismiss();
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
dialog[0] = HomeBottomSheet.builder(this)
|
||||
.title(R.string.home_compose_contacts_source_title)
|
||||
.custom(list)
|
||||
.show();
|
||||
}
|
||||
|
||||
private View createAccountRow(final LayoutInflater inflater, final LinearLayout parent,
|
||||
final String title, final String subtitle, final int count, final boolean selected,
|
||||
final Runnable onClick) {
|
||||
final View row = inflater.inflate(R.layout.home_compose_account_row, parent, false);
|
||||
((TextView) row.findViewById(R.id.home_compose_account_title)).setText(title);
|
||||
final TextView subtitleView = row.findViewById(R.id.home_compose_account_subtitle);
|
||||
if (TextUtils.isEmpty(subtitle)) {
|
||||
subtitleView.setVisibility(View.GONE);
|
||||
} else {
|
||||
subtitleView.setVisibility(View.VISIBLE);
|
||||
subtitleView.setText(subtitle);
|
||||
}
|
||||
((TextView) row.findViewById(R.id.home_compose_account_count))
|
||||
.setText(String.valueOf(count));
|
||||
row.findViewById(R.id.home_compose_account_check)
|
||||
.setVisibility(selected ? View.VISIBLE : View.GONE);
|
||||
row.setOnClickListener(v -> onClick.run());
|
||||
final LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(
|
||||
LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.WRAP_CONTENT);
|
||||
params.topMargin = parent.getChildCount() == 0
|
||||
? 0 : getResources().getDimensionPixelSize(R.dimen.looply_space_sm);
|
||||
row.setLayoutParams(params);
|
||||
return row;
|
||||
}
|
||||
|
||||
private void applyAccountFilter(final ContactUtil.AccountFilter filter, final String label) {
|
||||
mAccountFilter = filter;
|
||||
mAccountLabel.setText(label);
|
||||
final HomeContactsFragment fragment = getContactsFragment();
|
||||
if (fragment != null) {
|
||||
fragment.setAccountFilter(filter);
|
||||
}
|
||||
}
|
||||
|
||||
private void enterGroupMode() {
|
||||
mGroupMode = true;
|
||||
ImeUtil.get().hideImeKeyboard(this, mRecipientField);
|
||||
@@ -226,6 +327,12 @@ public class HomeComposeActivity extends BugleActionBarActivity
|
||||
super.onBackPressed();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDestroy() {
|
||||
mAccountExecutor.shutdownNow();
|
||||
super.onDestroy();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void finish() {
|
||||
if (mRecipientField != null && !TextUtils.isEmpty(mRecipientField.getText())) {
|
||||
|
||||
@@ -20,6 +20,7 @@ import android.content.ContentUris;
|
||||
import android.content.Context;
|
||||
import android.database.Cursor;
|
||||
import android.graphics.Color;
|
||||
import android.graphics.Rect;
|
||||
import android.graphics.drawable.ColorDrawable;
|
||||
import android.net.Uri;
|
||||
import android.os.Handler;
|
||||
@@ -44,6 +45,7 @@ import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.core.content.ContextCompat;
|
||||
import androidx.core.view.ViewCompat;
|
||||
import androidx.core.view.WindowInsetsAnimationCompat;
|
||||
import androidx.core.view.WindowInsetsCompat;
|
||||
import androidx.recyclerview.widget.GridLayoutManager;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
@@ -77,6 +79,8 @@ public final class HomeComposeAttachPanel {
|
||||
void onTakePhoto();
|
||||
void onRecordVideo();
|
||||
void onOpenContact();
|
||||
void onShareLocation();
|
||||
void onScheduleMessage();
|
||||
void onRequestMediaPermission();
|
||||
void onComingSoon();
|
||||
void onDismiss();
|
||||
@@ -247,6 +251,7 @@ public final class HomeComposeAttachPanel {
|
||||
float peekY;
|
||||
int statusBarTop;
|
||||
int navigationBarBottom;
|
||||
int imeBottom;
|
||||
final int dockBaseBottomPadding;
|
||||
final int originalStatusBarColor;
|
||||
boolean expanded;
|
||||
@@ -328,6 +333,12 @@ public final class HomeComposeAttachPanel {
|
||||
if (send != null) {
|
||||
send.setOnClickListener(v -> sendSelection());
|
||||
}
|
||||
overlay.getViewTreeObserver().addOnGlobalLayoutListener(() -> {
|
||||
if (closing) {
|
||||
return;
|
||||
}
|
||||
applyImeLift(inferredImeHeight());
|
||||
});
|
||||
if (caption != null) {
|
||||
caption.setOnEditorActionListener((v, actionId, event) -> {
|
||||
if (actionId == android.view.inputmethod.EditorInfo.IME_ACTION_SEND) {
|
||||
@@ -367,12 +378,21 @@ public final class HomeComposeAttachPanel {
|
||||
overlay.setLayoutParams(new FrameLayout.LayoutParams(
|
||||
ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT));
|
||||
ViewCompat.setOnApplyWindowInsetsListener(overlay, (v, insets) -> {
|
||||
statusBarTop = insets.getInsets(WindowInsetsCompat.Type.statusBars()).top;
|
||||
navigationBarBottom = insets.getInsets(WindowInsetsCompat.Type.navigationBars()).bottom;
|
||||
applyDockInsets();
|
||||
applyExpandHeaderInsets(expanded);
|
||||
applyWindowInsets(insets);
|
||||
return insets;
|
||||
});
|
||||
ViewCompat.setWindowInsetsAnimationCallback(overlay,
|
||||
new WindowInsetsAnimationCompat.Callback(
|
||||
WindowInsetsAnimationCompat.Callback.DISPATCH_MODE_CONTINUE_ON_SUBTREE) {
|
||||
@NonNull
|
||||
@Override
|
||||
public WindowInsetsCompat onProgress(
|
||||
@NonNull final WindowInsetsCompat insets,
|
||||
@NonNull final java.util.List<WindowInsetsAnimationCompat> running) {
|
||||
applyWindowInsets(insets);
|
||||
return insets;
|
||||
}
|
||||
});
|
||||
ViewCompat.requestApplyInsets(overlay);
|
||||
bindHandleDrag(handleArea, this);
|
||||
bindHandleDrag(expandHeader, this);
|
||||
@@ -568,9 +588,41 @@ public final class HomeComposeAttachPanel {
|
||||
}
|
||||
}
|
||||
|
||||
void applyWindowInsets(final WindowInsetsCompat insets) {
|
||||
statusBarTop = insets.getInsets(WindowInsetsCompat.Type.statusBars()).top;
|
||||
navigationBarBottom = insets.getInsets(WindowInsetsCompat.Type.navigationBars()).bottom;
|
||||
applyImeLift(Math.max(
|
||||
insets.getInsets(WindowInsetsCompat.Type.ime()).bottom,
|
||||
inferredImeHeight()));
|
||||
}
|
||||
|
||||
int inferredImeHeight() {
|
||||
final View decor = activity.getWindow() == null
|
||||
? null : activity.getWindow().getDecorView();
|
||||
if (decor == null) {
|
||||
return 0;
|
||||
}
|
||||
final Rect visible = new Rect();
|
||||
decor.getWindowVisibleDisplayFrame(visible);
|
||||
final int covered = decor.getHeight() - visible.bottom;
|
||||
return covered > 80 ? covered : 0;
|
||||
}
|
||||
|
||||
void applyImeLift(final int ime) {
|
||||
if (imeBottom == ime) {
|
||||
return;
|
||||
}
|
||||
imeBottom = ime;
|
||||
applyBottomMargin(sheet, imeBottom);
|
||||
applyBottomMargin(dock, imeBottom);
|
||||
applyDockInsets();
|
||||
applyExpandHeaderInsets(expanded);
|
||||
}
|
||||
|
||||
void applyDockInsets() {
|
||||
if (dock != null) {
|
||||
final int bottom = dockBaseBottomPadding + navigationBarBottom;
|
||||
final int bottom = dockBaseBottomPadding
|
||||
+ (imeBottom > 0 ? 0 : navigationBarBottom);
|
||||
if (dock.getPaddingBottom() != bottom) {
|
||||
dock.setPaddingRelative(
|
||||
dock.getPaddingStart(),
|
||||
@@ -590,6 +642,18 @@ public final class HomeComposeAttachPanel {
|
||||
}
|
||||
}
|
||||
|
||||
void applyBottomMargin(final View view, final int bottom) {
|
||||
if (view == null || !(view.getLayoutParams() instanceof ViewGroup.MarginLayoutParams)) {
|
||||
return;
|
||||
}
|
||||
final ViewGroup.MarginLayoutParams lp =
|
||||
(ViewGroup.MarginLayoutParams) view.getLayoutParams();
|
||||
if (lp.bottomMargin != bottom) {
|
||||
lp.bottomMargin = bottom;
|
||||
view.setLayoutParams(lp);
|
||||
}
|
||||
}
|
||||
|
||||
void applyGridDockPadding() {
|
||||
if (grid == null) {
|
||||
return;
|
||||
@@ -982,6 +1046,12 @@ public final class HomeComposeAttachPanel {
|
||||
} else if (index == TAB_CONTACT) {
|
||||
host.onOpenContact();
|
||||
dismissAnimated.run();
|
||||
} else if (index == TAB_LOCATION) {
|
||||
host.onShareLocation();
|
||||
dismissAnimated.run();
|
||||
} else if (index == TAB_SCHEDULE) {
|
||||
host.onScheduleMessage();
|
||||
dismissAnimated.run();
|
||||
} else if (index != TAB_GALLERY) {
|
||||
host.onComingSoon();
|
||||
}
|
||||
|
||||
@@ -6,24 +6,74 @@ import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.CursorAdapter;
|
||||
import android.widget.SectionIndexer;
|
||||
|
||||
import com.android.messaging.R;
|
||||
import com.android.messaging.ui.contact.ContactListItemView;
|
||||
import com.android.messaging.ui.contact.ContactSectionIndexer;
|
||||
import com.android.messaging.util.Assert;
|
||||
|
||||
public class HomeContactListAdapter extends CursorAdapter {
|
||||
public class HomeContactListAdapter extends CursorAdapter implements SectionIndexer {
|
||||
private final ContactListItemView.HostInterface mHost;
|
||||
private ContactSectionIndexer mSectionIndexer;
|
||||
private boolean mNeedAlphabetHeader = true;
|
||||
|
||||
public HomeContactListAdapter(final Context context, final Cursor cursor,
|
||||
final ContactListItemView.HostInterface host) {
|
||||
super(context, cursor, 0);
|
||||
mHost = host;
|
||||
mSectionIndexer = new ContactSectionIndexer(cursor);
|
||||
}
|
||||
|
||||
public void setNeedAlphabetHeader(final boolean needAlphabetHeader) {
|
||||
if (mNeedAlphabetHeader == needAlphabetHeader) {
|
||||
return;
|
||||
}
|
||||
mNeedAlphabetHeader = needAlphabetHeader;
|
||||
notifyDataSetChanged();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void bindView(final View view, final Context context, final Cursor cursor) {
|
||||
Assert.isTrue(view instanceof ContactListItemView);
|
||||
((ContactListItemView) view).bind(cursor, mHost, false, null);
|
||||
String alphabetHeader = null;
|
||||
if (mNeedAlphabetHeader && mSectionIndexer.getSections().length > 0) {
|
||||
final int position = cursor.getPosition();
|
||||
final int section = mSectionIndexer.getSectionForPosition(position);
|
||||
if (section >= 0 && mSectionIndexer.getPositionForSection(section) == position) {
|
||||
alphabetHeader = (String) mSectionIndexer.getSections()[section];
|
||||
}
|
||||
}
|
||||
final ContactListItemView itemView = (ContactListItemView) view;
|
||||
itemView.bind(cursor, mHost, mNeedAlphabetHeader, alphabetHeader);
|
||||
itemView.setSectionBox(sectionRole(cursor.getPosition()));
|
||||
}
|
||||
|
||||
private int sectionRole(final int position) {
|
||||
if (!mNeedAlphabetHeader || mSectionIndexer.getSections().length == 0) {
|
||||
return ContactListItemView.SECTION_SINGLE;
|
||||
}
|
||||
final int section = mSectionIndexer.getSectionForPosition(position);
|
||||
if (section < 0) {
|
||||
return ContactListItemView.SECTION_SINGLE;
|
||||
}
|
||||
final int first = mSectionIndexer.getPositionForSection(section);
|
||||
final int last;
|
||||
if (section + 1 < mSectionIndexer.getSections().length) {
|
||||
last = mSectionIndexer.getPositionForSection(section + 1) - 1;
|
||||
} else {
|
||||
last = getCount() - 1;
|
||||
}
|
||||
if (position == first && position == last) {
|
||||
return ContactListItemView.SECTION_SINGLE;
|
||||
}
|
||||
if (position == first) {
|
||||
return ContactListItemView.SECTION_FIRST;
|
||||
}
|
||||
if (position == last) {
|
||||
return ContactListItemView.SECTION_LAST;
|
||||
}
|
||||
return ContactListItemView.SECTION_MIDDLE;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -31,4 +81,25 @@ public class HomeContactListAdapter extends CursorAdapter {
|
||||
return LayoutInflater.from(context).inflate(R.layout.home_contacts_list_item, parent,
|
||||
false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Cursor swapCursor(final Cursor newCursor) {
|
||||
mSectionIndexer = new ContactSectionIndexer(newCursor);
|
||||
return super.swapCursor(newCursor);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object[] getSections() {
|
||||
return mSectionIndexer.getSections();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getPositionForSection(final int sectionIndex) {
|
||||
return mSectionIndexer.getPositionForSection(sectionIndex);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getSectionForPosition(final int position) {
|
||||
return mSectionIndexer.getSectionForPosition(position);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -114,12 +114,22 @@ public class HomeContactsFragment extends Fragment implements ContactPickerDataL
|
||||
/** Narrows the list as the host screen's recipient field is typed into. */
|
||||
public void setFilterText(final String filterText) {
|
||||
mFilterText = filterText;
|
||||
if (mAdapter != null) {
|
||||
mAdapter.setNeedAlphabetHeader(TextUtils.isEmpty(filterText));
|
||||
}
|
||||
if (mBinding.isBound()) {
|
||||
mBinding.getData().setSearchText(mBinding, filterText);
|
||||
}
|
||||
updateResultViews();
|
||||
}
|
||||
|
||||
/** Limits the list to one contacts account, or every account when {@code filter} is null. */
|
||||
public void setAccountFilter(final ContactUtil.AccountFilter filter) {
|
||||
if (mBinding.isBound()) {
|
||||
mBinding.getData().setAccountFilter(mBinding, filter);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Starts a conversation with the raw text currently typed, if it can be messaged.
|
||||
*
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
package com.android.messaging.ui.home;
|
||||
|
||||
import android.graphics.Rect;
|
||||
import android.view.MotionEvent;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
@@ -43,14 +42,20 @@ public final class HomeInputFocus {
|
||||
if (isInside(box, ev)) {
|
||||
return false;
|
||||
}
|
||||
root.setFocusable(true);
|
||||
root.setFocusableInTouchMode(true);
|
||||
final View sink = focusSink(root);
|
||||
sink.setFocusable(true);
|
||||
sink.setFocusableInTouchMode(true);
|
||||
sink.requestFocus();
|
||||
focused.clearFocus();
|
||||
root.requestFocus();
|
||||
ImeUtil.get().hideImeKeyboard(focused.getContext(), focused);
|
||||
return true;
|
||||
}
|
||||
|
||||
private static View focusSink(@NonNull final View root) {
|
||||
final View content = root.findViewById(android.R.id.content);
|
||||
return content != null ? content : root;
|
||||
}
|
||||
|
||||
private static View findBox(final View focused) {
|
||||
ViewParent parent = focused.getParent();
|
||||
while (parent instanceof ViewGroup) {
|
||||
@@ -64,11 +69,13 @@ public final class HomeInputFocus {
|
||||
}
|
||||
|
||||
private static boolean isInside(final View view, final MotionEvent ev) {
|
||||
final Rect rect = new Rect();
|
||||
if (!view.getGlobalVisibleRect(rect)) {
|
||||
return false;
|
||||
}
|
||||
return rect.contains(Math.round(ev.getRawX()), Math.round(ev.getRawY()));
|
||||
final int[] loc = new int[2];
|
||||
view.getLocationOnScreen(loc);
|
||||
final int x = Math.round(ev.getRawX());
|
||||
final int y = Math.round(ev.getRawY());
|
||||
return x >= loc[0] && y >= loc[1]
|
||||
&& x < loc[0] + view.getWidth()
|
||||
&& y < loc[1] + view.getHeight();
|
||||
}
|
||||
|
||||
private static final class Installed extends WindowCallbackWrapper {
|
||||
|
||||
@@ -0,0 +1,278 @@
|
||||
package com.android.messaging.ui.home;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.content.Intent;
|
||||
import android.location.Address;
|
||||
import android.location.Geocoder;
|
||||
import android.location.Location;
|
||||
import android.location.LocationManager;
|
||||
import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
import android.os.Handler;
|
||||
import android.os.Looper;
|
||||
import android.text.TextUtils;
|
||||
import android.webkit.JavascriptInterface;
|
||||
import android.webkit.WebSettings;
|
||||
import android.webkit.WebView;
|
||||
import android.webkit.WebViewClient;
|
||||
import android.widget.ImageButton;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import com.android.messaging.R;
|
||||
import com.android.messaging.datamodel.MediaScratchFileProvider;
|
||||
import com.android.messaging.ui.BugleActionBarActivity;
|
||||
import com.android.messaging.ui.UIIntents;
|
||||
import com.android.messaging.util.LogUtil;
|
||||
import com.android.messaging.util.OsUtil;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.InputStream;
|
||||
import java.net.HttpURLConnection;
|
||||
import java.net.URL;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
|
||||
/** Full-screen map picker that returns a maps URL and optional preview image. */
|
||||
public class HomeLocationActivity extends BugleActionBarActivity {
|
||||
private static final double DEFAULT_LAT = 37.5665;
|
||||
private static final double DEFAULT_LNG = 126.9780;
|
||||
private static final int STATIC_MAP_TIMEOUT_MS = 8000;
|
||||
|
||||
private WebView mMap;
|
||||
private TextView mAddressView;
|
||||
private ImageButton mSendButton;
|
||||
private final Handler mMain = new Handler(Looper.getMainLooper());
|
||||
private final ExecutorService mIo = Executors.newSingleThreadExecutor();
|
||||
private double mLat = DEFAULT_LAT;
|
||||
private double mLng = DEFAULT_LNG;
|
||||
private String mAddress = "";
|
||||
private boolean mMapReady;
|
||||
private boolean mSending;
|
||||
|
||||
@Override
|
||||
protected void onCreate(final Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
setContentView(R.layout.home_location_activity);
|
||||
if (getSupportActionBar() != null) {
|
||||
getSupportActionBar().hide();
|
||||
}
|
||||
HomeSubHeader.bind(this, R.string.home_location_title, v -> finish());
|
||||
|
||||
mAddressView = findViewById(R.id.home_location_address);
|
||||
mSendButton = findViewById(R.id.home_location_send);
|
||||
mMap = findViewById(R.id.home_location_map);
|
||||
mSendButton.setOnClickListener(v -> sendCurrentLocation());
|
||||
findViewById(R.id.home_location_my_location).setOnClickListener(
|
||||
v -> moveToMyLocation(true));
|
||||
setupMap();
|
||||
moveToMyLocation(false);
|
||||
}
|
||||
|
||||
@SuppressLint({"SetJavaScriptEnabled", "AddJavascriptInterface"})
|
||||
private void setupMap() {
|
||||
final WebSettings settings = mMap.getSettings();
|
||||
settings.setJavaScriptEnabled(true);
|
||||
settings.setDomStorageEnabled(true);
|
||||
settings.setAllowFileAccess(true);
|
||||
mMap.addJavascriptInterface(new MapBridge(), "LocationBridge");
|
||||
mMap.setWebViewClient(new WebViewClient() {
|
||||
@Override
|
||||
public void onPageFinished(final WebView view, final String url) {
|
||||
mMapReady = true;
|
||||
applyMapCenter(mLat, mLng);
|
||||
}
|
||||
});
|
||||
mMap.loadUrl("file:///android_asset/home_location_map.html");
|
||||
}
|
||||
|
||||
private void moveToMyLocation(final boolean userRequested) {
|
||||
if (!OsUtil.hasLocationPermission()) {
|
||||
if (userRequested) {
|
||||
HomeToast.show(this, R.string.home_location_permission_denied);
|
||||
}
|
||||
return;
|
||||
}
|
||||
final LocationManager manager = getSystemService(LocationManager.class);
|
||||
if (manager == null) {
|
||||
return;
|
||||
}
|
||||
Location best = null;
|
||||
try {
|
||||
best = manager.getLastKnownLocation(LocationManager.GPS_PROVIDER);
|
||||
if (best == null) {
|
||||
best = manager.getLastKnownLocation(LocationManager.NETWORK_PROVIDER);
|
||||
}
|
||||
if (best == null) {
|
||||
best = manager.getLastKnownLocation(LocationManager.PASSIVE_PROVIDER);
|
||||
}
|
||||
} catch (final SecurityException exception) {
|
||||
LogUtil.w(LogUtil.BUGLE_TAG, "Location lookup failed", exception);
|
||||
}
|
||||
if (best != null) {
|
||||
mLat = best.getLatitude();
|
||||
mLng = best.getLongitude();
|
||||
applyMapCenter(mLat, mLng);
|
||||
reverseGeocode(mLat, mLng);
|
||||
return;
|
||||
}
|
||||
if (userRequested) {
|
||||
HomeToast.show(this, R.string.home_location_unavailable);
|
||||
}
|
||||
}
|
||||
|
||||
private void applyMapCenter(final double lat, final double lng) {
|
||||
if (!mMapReady) {
|
||||
return;
|
||||
}
|
||||
mMap.evaluateJavascript("setCenter(" + lat + "," + lng + ")", null);
|
||||
}
|
||||
|
||||
private Runnable mPendingGeocode;
|
||||
|
||||
private void reverseGeocode(final double lat, final double lng) {
|
||||
if (mPendingGeocode != null) {
|
||||
mMain.removeCallbacks(mPendingGeocode);
|
||||
}
|
||||
mPendingGeocode = () -> reverseGeocodeNow(lat, lng);
|
||||
mMain.postDelayed(mPendingGeocode, 350);
|
||||
}
|
||||
|
||||
private void reverseGeocodeNow(final double lat, final double lng) {
|
||||
mAddressView.setText(R.string.home_location_finding);
|
||||
mIo.execute(() -> {
|
||||
String address = formatLatLng(lat, lng);
|
||||
try {
|
||||
if (Geocoder.isPresent()) {
|
||||
final List<Address> results = new Geocoder(this, Locale.getDefault())
|
||||
.getFromLocation(lat, lng, 1);
|
||||
if (results != null && !results.isEmpty()) {
|
||||
final String line = results.get(0).getAddressLine(0);
|
||||
if (!TextUtils.isEmpty(line)) {
|
||||
address = line;
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (final Exception exception) {
|
||||
LogUtil.w(LogUtil.BUGLE_TAG, "Geocoder failed", exception);
|
||||
}
|
||||
final String resolved = address;
|
||||
mMain.post(() -> {
|
||||
mAddress = resolved;
|
||||
mAddressView.setText(resolved);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private void sendCurrentLocation() {
|
||||
if (mSending) {
|
||||
return;
|
||||
}
|
||||
mSending = true;
|
||||
mSendButton.setEnabled(false);
|
||||
final double lat = mLat;
|
||||
final double lng = mLng;
|
||||
final String address = TextUtils.isEmpty(mAddress) ? formatLatLng(lat, lng) : mAddress;
|
||||
final String mapsUrl = "https://maps.google.com/maps?q=" + lat + "," + lng;
|
||||
final String text = address + "\n" + mapsUrl;
|
||||
mIo.execute(() -> {
|
||||
final Uri image = downloadStaticMap(lat, lng);
|
||||
mMain.post(() -> {
|
||||
final Intent data = new Intent();
|
||||
data.putExtra(UIIntents.UI_INTENT_EXTRA_LOCATION_TEXT, text);
|
||||
if (image != null) {
|
||||
data.putExtra(UIIntents.UI_INTENT_EXTRA_LOCATION_IMAGE_URI, image);
|
||||
}
|
||||
setResult(RESULT_OK, data);
|
||||
finish();
|
||||
overridePendingTransition(R.anim.home_activity_hold, R.anim.home_activity_close);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private Uri downloadStaticMap(final double lat, final double lng) {
|
||||
HttpURLConnection connection = null;
|
||||
try {
|
||||
final String url = "https://staticmap.openstreetmap.de/staticmap.php?center="
|
||||
+ lat + "," + lng + "&zoom=16&size=640x360&markers=" + lat + "," + lng
|
||||
+ ",red-pushpin";
|
||||
connection = (HttpURLConnection) new URL(url).openConnection();
|
||||
connection.setConnectTimeout(STATIC_MAP_TIMEOUT_MS);
|
||||
connection.setReadTimeout(STATIC_MAP_TIMEOUT_MS);
|
||||
connection.connect();
|
||||
if (connection.getResponseCode() != HttpURLConnection.HTTP_OK) {
|
||||
return null;
|
||||
}
|
||||
final String contentType = connection.getContentType();
|
||||
if (contentType == null || !contentType.contains("image")) {
|
||||
return null;
|
||||
}
|
||||
final Uri uri = MediaScratchFileProvider.buildMediaScratchSpaceUri("jpg");
|
||||
final File file = MediaScratchFileProvider.getFileFromUri(uri);
|
||||
if (file == null) {
|
||||
return null;
|
||||
}
|
||||
try (InputStream in = connection.getInputStream();
|
||||
FileOutputStream out = new FileOutputStream(file)) {
|
||||
final byte[] buffer = new byte[8192];
|
||||
int read;
|
||||
while ((read = in.read(buffer)) != -1) {
|
||||
out.write(buffer, 0, read);
|
||||
}
|
||||
}
|
||||
return uri;
|
||||
} catch (final Exception exception) {
|
||||
LogUtil.w(LogUtil.BUGLE_TAG, "Static map download failed", exception);
|
||||
return null;
|
||||
} finally {
|
||||
if (connection != null) {
|
||||
connection.disconnect();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static String formatLatLng(final double lat, final double lng) {
|
||||
return String.format(Locale.US, "%.5f, %.5f", lat, lng);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onResume() {
|
||||
super.onResume();
|
||||
if (mMap != null) {
|
||||
mMap.onResume();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPause() {
|
||||
if (mMap != null) {
|
||||
mMap.onPause();
|
||||
}
|
||||
super.onPause();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDestroy() {
|
||||
if (mMap != null) {
|
||||
mMap.destroy();
|
||||
}
|
||||
mIo.shutdownNow();
|
||||
super.onDestroy();
|
||||
}
|
||||
|
||||
public final class MapBridge {
|
||||
@JavascriptInterface
|
||||
public void onMapMoved(final double lat, final double lng) {
|
||||
mMain.post(() -> {
|
||||
mLat = lat;
|
||||
mLng = lng;
|
||||
reverseGeocode(lat, lng);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
package com.android.messaging.ui.home;
|
||||
|
||||
import android.content.Context;
|
||||
import android.util.AttributeSet;
|
||||
import android.view.MotionEvent;
|
||||
import android.view.View;
|
||||
|
||||
/** Overlay that draws but never takes touches. */
|
||||
public class HomePassThroughView extends View {
|
||||
public HomePassThroughView(final Context context) {
|
||||
super(context);
|
||||
}
|
||||
|
||||
public HomePassThroughView(final Context context, final AttributeSet attrs) {
|
||||
super(context, attrs);
|
||||
}
|
||||
|
||||
public HomePassThroughView(final Context context, final AttributeSet attrs,
|
||||
final int defStyleAttr) {
|
||||
super(context, attrs, defStyleAttr);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean dispatchTouchEvent(final MotionEvent event) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,226 @@
|
||||
package com.android.messaging.ui.home;
|
||||
|
||||
import android.content.Context;
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.Paint;
|
||||
import android.graphics.Path;
|
||||
import android.util.AttributeSet;
|
||||
import android.util.TypedValue;
|
||||
import android.view.MotionEvent;
|
||||
import android.view.View;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import com.android.messaging.R;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/** 3x3 pattern lock grid with mid-cell bridging (File Plus behavior). */
|
||||
public class HomePatternLockView extends View {
|
||||
|
||||
public interface Listener {
|
||||
void onPatternChanged(@NonNull List<Integer> cells);
|
||||
|
||||
void onPatternComplete(@NonNull List<Integer> cells);
|
||||
}
|
||||
|
||||
private final Paint mLinePaint = new Paint(Paint.ANTI_ALIAS_FLAG);
|
||||
private final Paint mDragPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
|
||||
private final Paint mHaloPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
|
||||
private final Paint mDotPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
|
||||
private final Paint mInnerPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
|
||||
private final Path mPath = new Path();
|
||||
private final float[] mCentersX = new float[9];
|
||||
private final float[] mCentersY = new float[9];
|
||||
private final List<Integer> mCells = new ArrayList<>();
|
||||
|
||||
private Listener mListener;
|
||||
private boolean mDragging;
|
||||
private float mDragX;
|
||||
private float mDragY;
|
||||
private float mHitRadius;
|
||||
private float mCoreRadius;
|
||||
private float mSelectedRadius;
|
||||
private float mInnerRadius;
|
||||
private float mHaloGap;
|
||||
private int mAccent;
|
||||
private int mIdle;
|
||||
|
||||
public HomePatternLockView(final Context context) {
|
||||
super(context);
|
||||
init();
|
||||
}
|
||||
|
||||
public HomePatternLockView(final Context context, @Nullable final AttributeSet attrs) {
|
||||
super(context, attrs);
|
||||
init();
|
||||
}
|
||||
|
||||
public HomePatternLockView(final Context context, @Nullable final AttributeSet attrs,
|
||||
final int defStyleAttr) {
|
||||
super(context, attrs, defStyleAttr);
|
||||
init();
|
||||
}
|
||||
|
||||
private void init() {
|
||||
mAccent = getContext().getColor(R.color.home_accent);
|
||||
mIdle = getContext().getColor(R.color.home_divider);
|
||||
mLinePaint.setStyle(Paint.Style.STROKE);
|
||||
mLinePaint.setStrokeCap(Paint.Cap.ROUND);
|
||||
mLinePaint.setStrokeJoin(Paint.Join.ROUND);
|
||||
mLinePaint.setColor(mAccent);
|
||||
mLinePaint.setStrokeWidth(dp(4));
|
||||
mDragPaint.set(mLinePaint);
|
||||
mDragPaint.setAlpha(217);
|
||||
mHaloPaint.setStyle(Paint.Style.STROKE);
|
||||
mHaloPaint.setStrokeWidth(dp(3));
|
||||
mHaloPaint.setColor(mAccent);
|
||||
mDotPaint.setStyle(Paint.Style.FILL);
|
||||
mInnerPaint.setStyle(Paint.Style.FILL);
|
||||
mInnerPaint.setColor(0xFFFFFFFF);
|
||||
mHitRadius = dp(32);
|
||||
mCoreRadius = dp(6);
|
||||
mSelectedRadius = dp(10);
|
||||
mInnerRadius = dp(3.5f);
|
||||
mHaloGap = dp(5);
|
||||
setClickable(true);
|
||||
}
|
||||
|
||||
public void setListener(@Nullable final Listener listener) {
|
||||
mListener = listener;
|
||||
}
|
||||
|
||||
public void clear() {
|
||||
mCells.clear();
|
||||
mDragging = false;
|
||||
invalidate();
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public List<Integer> getCells() {
|
||||
return new ArrayList<>(mCells);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onMeasure(final int widthMeasureSpec, final int heightMeasureSpec) {
|
||||
final int width = MeasureSpec.getSize(widthMeasureSpec);
|
||||
final int height = MeasureSpec.getSize(heightMeasureSpec);
|
||||
final int size;
|
||||
if (MeasureSpec.getMode(heightMeasureSpec) == MeasureSpec.UNSPECIFIED) {
|
||||
size = width;
|
||||
} else {
|
||||
size = Math.min(width, height);
|
||||
}
|
||||
setMeasuredDimension(size, size);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onSizeChanged(final int w, final int h, final int oldw, final int oldh) {
|
||||
super.onSizeChanged(w, h, oldw, oldh);
|
||||
final float spacingX = w / 4f;
|
||||
final float spacingY = h / 4f;
|
||||
final float cell = Math.min(spacingX, spacingY);
|
||||
mHitRadius = cell * 0.42f;
|
||||
mCoreRadius = cell * 0.12f;
|
||||
mSelectedRadius = cell * 0.18f;
|
||||
mInnerRadius = cell * 0.07f;
|
||||
mHaloGap = cell * 0.08f;
|
||||
mLinePaint.setStrokeWidth(Math.max(dp(4), cell * 0.08f));
|
||||
mDragPaint.setStrokeWidth(mLinePaint.getStrokeWidth());
|
||||
for (int i = 0; i < 9; i++) {
|
||||
mCentersX[i] = spacingX * ((i % 3) + 1);
|
||||
mCentersY[i] = spacingY * ((i / 3) + 1);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDraw(@NonNull final Canvas canvas) {
|
||||
super.onDraw(canvas);
|
||||
if (mCells.size() >= 2) {
|
||||
mPath.reset();
|
||||
mPath.moveTo(mCentersX[mCells.get(0)], mCentersY[mCells.get(0)]);
|
||||
for (int i = 1; i < mCells.size(); i++) {
|
||||
final int cell = mCells.get(i);
|
||||
mPath.lineTo(mCentersX[cell], mCentersY[cell]);
|
||||
}
|
||||
canvas.drawPath(mPath, mLinePaint);
|
||||
}
|
||||
if (mDragging && !mCells.isEmpty()) {
|
||||
final int last = mCells.get(mCells.size() - 1);
|
||||
canvas.drawLine(mCentersX[last], mCentersY[last], mDragX, mDragY, mDragPaint);
|
||||
}
|
||||
for (int i = 0; i < 9; i++) {
|
||||
final boolean selected = mCells.contains(i);
|
||||
final float core = selected ? mSelectedRadius : mCoreRadius;
|
||||
mHaloPaint.setAlpha(selected ? 97 : 51);
|
||||
canvas.drawCircle(mCentersX[i], mCentersY[i], core + mHaloGap, mHaloPaint);
|
||||
mDotPaint.setColor(selected ? mAccent : mIdle);
|
||||
canvas.drawCircle(mCentersX[i], mCentersY[i], core, mDotPaint);
|
||||
if (selected) {
|
||||
canvas.drawCircle(mCentersX[i], mCentersY[i], mInnerRadius, mInnerPaint);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onTouchEvent(final MotionEvent event) {
|
||||
switch (event.getActionMasked()) {
|
||||
case MotionEvent.ACTION_DOWN:
|
||||
mDragging = true;
|
||||
mDragX = event.getX();
|
||||
mDragY = event.getY();
|
||||
mCells.clear();
|
||||
addCellAt(mDragX, mDragY);
|
||||
invalidate();
|
||||
return true;
|
||||
case MotionEvent.ACTION_MOVE:
|
||||
mDragX = event.getX();
|
||||
mDragY = event.getY();
|
||||
addCellAt(mDragX, mDragY);
|
||||
invalidate();
|
||||
return true;
|
||||
case MotionEvent.ACTION_UP:
|
||||
case MotionEvent.ACTION_CANCEL:
|
||||
mDragging = false;
|
||||
invalidate();
|
||||
if (mListener != null && !mCells.isEmpty()) {
|
||||
mListener.onPatternComplete(getCells());
|
||||
}
|
||||
return true;
|
||||
default:
|
||||
return super.onTouchEvent(event);
|
||||
}
|
||||
}
|
||||
|
||||
private void addCellAt(final float x, final float y) {
|
||||
final Integer hit = cellAt(x, y);
|
||||
if (hit == null || mCells.contains(hit)) {
|
||||
return;
|
||||
}
|
||||
final List<Integer> next = HomeAppLock.appendPatternCells(mCells, hit);
|
||||
mCells.clear();
|
||||
mCells.addAll(next);
|
||||
if (mListener != null) {
|
||||
mListener.onPatternChanged(getCells());
|
||||
}
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private Integer cellAt(final float x, final float y) {
|
||||
for (int i = 0; i < 9; i++) {
|
||||
final float dx = x - mCentersX[i];
|
||||
final float dy = y - mCentersY[i];
|
||||
if (dx * dx + dy * dy <= mHitRadius * mHitRadius) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private float dp(final float value) {
|
||||
return TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, value,
|
||||
getResources().getDisplayMetrics());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,137 @@
|
||||
package com.android.messaging.ui.home;
|
||||
|
||||
import android.content.Context;
|
||||
import android.text.format.DateFormat;
|
||||
import android.text.format.DateUtils;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.widget.DatePicker;
|
||||
import android.widget.TextView;
|
||||
import android.widget.TimePicker;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import com.android.messaging.R;
|
||||
import com.google.android.material.bottomsheet.BottomSheetDialog;
|
||||
|
||||
import java.util.Calendar;
|
||||
|
||||
/** Home date then time sheets used by Schedule in the compose attach panel. */
|
||||
public final class HomeSchedulePicker {
|
||||
|
||||
public interface Callback {
|
||||
void onScheduled(long sendAtMillis);
|
||||
}
|
||||
|
||||
private HomeSchedulePicker() {
|
||||
}
|
||||
|
||||
public static void show(@NonNull final Context context, final long existingSendAt,
|
||||
@NonNull final Callback callback) {
|
||||
final Calendar calendar = Calendar.getInstance();
|
||||
if (existingSendAt > System.currentTimeMillis()) {
|
||||
calendar.setTimeInMillis(existingSendAt);
|
||||
} else {
|
||||
calendar.add(Calendar.HOUR_OF_DAY, 1);
|
||||
calendar.set(Calendar.MINUTE, 0);
|
||||
calendar.set(Calendar.SECOND, 0);
|
||||
calendar.set(Calendar.MILLISECOND, 0);
|
||||
}
|
||||
showDate(context, calendar, callback);
|
||||
}
|
||||
|
||||
public static String formatChip(@NonNull final Context context, final long sendAtMillis) {
|
||||
final String time = DateFormat.getTimeFormat(context).format(sendAtMillis);
|
||||
if (DateUtils.isToday(sendAtMillis)) {
|
||||
return context.getString(R.string.home_schedule_today, time);
|
||||
}
|
||||
final Calendar tomorrow = Calendar.getInstance();
|
||||
tomorrow.add(Calendar.DAY_OF_YEAR, 1);
|
||||
final Calendar target = Calendar.getInstance();
|
||||
target.setTimeInMillis(sendAtMillis);
|
||||
if (tomorrow.get(Calendar.YEAR) == target.get(Calendar.YEAR)
|
||||
&& tomorrow.get(Calendar.DAY_OF_YEAR) == target.get(Calendar.DAY_OF_YEAR)) {
|
||||
return context.getString(R.string.home_schedule_tomorrow, time);
|
||||
}
|
||||
final String date = DateUtils.formatDateTime(context, sendAtMillis,
|
||||
DateUtils.FORMAT_SHOW_DATE | DateUtils.FORMAT_ABBREV_MONTH
|
||||
| DateUtils.FORMAT_NO_YEAR);
|
||||
return date + " " + time;
|
||||
}
|
||||
|
||||
private static void showDate(@NonNull final Context context, @NonNull final Calendar calendar,
|
||||
@NonNull final Callback callback) {
|
||||
final View body = LayoutInflater.from(context).inflate(
|
||||
R.layout.home_schedule_date_sheet, null, false);
|
||||
final DatePicker picker = body.findViewById(R.id.home_schedule_date);
|
||||
picker.setMinDate(System.currentTimeMillis() - 1000L);
|
||||
picker.updateDate(
|
||||
calendar.get(Calendar.YEAR),
|
||||
calendar.get(Calendar.MONTH),
|
||||
calendar.get(Calendar.DAY_OF_MONTH));
|
||||
final BottomSheetDialog[] dialogHolder = new BottomSheetDialog[1];
|
||||
dialogHolder[0] = HomeBottomSheet.builder(context)
|
||||
.title(R.string.home_schedule_select_date)
|
||||
.custom(body)
|
||||
.show();
|
||||
bindActions(body,
|
||||
v -> dialogHolder[0].dismiss(),
|
||||
v -> {
|
||||
calendar.set(Calendar.YEAR, picker.getYear());
|
||||
calendar.set(Calendar.MONTH, picker.getMonth());
|
||||
calendar.set(Calendar.DAY_OF_MONTH, picker.getDayOfMonth());
|
||||
dialogHolder[0].dismiss();
|
||||
showTime(context, calendar, callback);
|
||||
},
|
||||
R.string.home_schedule_next);
|
||||
}
|
||||
|
||||
private static void showTime(@NonNull final Context context, @NonNull final Calendar calendar,
|
||||
@NonNull final Callback callback) {
|
||||
final View body = LayoutInflater.from(context).inflate(
|
||||
R.layout.home_schedule_time_sheet, null, false);
|
||||
final TimePicker picker = body.findViewById(R.id.home_schedule_time);
|
||||
picker.setIs24HourView(DateFormat.is24HourFormat(context));
|
||||
picker.setHour(calendar.get(Calendar.HOUR_OF_DAY));
|
||||
picker.setMinute(calendar.get(Calendar.MINUTE));
|
||||
final BottomSheetDialog[] dialogHolder = new BottomSheetDialog[1];
|
||||
dialogHolder[0] = HomeBottomSheet.builder(context)
|
||||
.title(R.string.home_schedule_select_time)
|
||||
.custom(body)
|
||||
.show();
|
||||
bindActions(body,
|
||||
v -> {
|
||||
dialogHolder[0].dismiss();
|
||||
showDate(context, calendar, callback);
|
||||
},
|
||||
v -> {
|
||||
calendar.set(Calendar.HOUR_OF_DAY, picker.getHour());
|
||||
calendar.set(Calendar.MINUTE, picker.getMinute());
|
||||
calendar.set(Calendar.SECOND, 0);
|
||||
calendar.set(Calendar.MILLISECOND, 0);
|
||||
final long sendAt = calendar.getTimeInMillis();
|
||||
if (sendAt <= System.currentTimeMillis()) {
|
||||
HomeToast.show(context, R.string.home_schedule_past);
|
||||
return;
|
||||
}
|
||||
dialogHolder[0].dismiss();
|
||||
callback.onScheduled(sendAt);
|
||||
},
|
||||
R.string.home_schedule_done);
|
||||
}
|
||||
|
||||
private static void bindActions(@NonNull final View body,
|
||||
@NonNull final View.OnClickListener onCancel,
|
||||
@NonNull final View.OnClickListener onPositive,
|
||||
final int positiveLabel) {
|
||||
final TextView cancel = body.findViewById(R.id.home_schedule_cancel);
|
||||
final TextView next = body.findViewById(R.id.home_schedule_next);
|
||||
if (cancel != null) {
|
||||
cancel.setOnClickListener(onCancel);
|
||||
}
|
||||
if (next != null) {
|
||||
next.setText(positiveLabel);
|
||||
next.setOnClickListener(onPositive);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,127 @@
|
||||
package com.android.messaging.ui.home;
|
||||
|
||||
import android.os.Bundle;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.widget.LinearLayout;
|
||||
|
||||
import com.android.messaging.R;
|
||||
import com.android.messaging.ui.BugleActionBarActivity;
|
||||
import com.google.android.material.bottomsheet.BottomSheetDialog;
|
||||
|
||||
/** File Plus-style security settings: App Lock toggle plus PIN / pattern / password. */
|
||||
public class HomeSecuritySettingsActivity extends BugleActionBarActivity {
|
||||
|
||||
private View mLockRow;
|
||||
private View mPinRow;
|
||||
private View mPatternRow;
|
||||
private View mPasswordRow;
|
||||
|
||||
@Override
|
||||
protected void onCreate(final Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
setContentView(R.layout.home_security_settings_activity);
|
||||
if (getSupportActionBar() != null) {
|
||||
getSupportActionBar().hide();
|
||||
}
|
||||
HomeSubHeader.bind(this, R.string.home_security_title, v -> finish());
|
||||
mLockRow = findViewById(R.id.home_security_app_lock);
|
||||
mPinRow = findViewById(R.id.home_security_set_pin);
|
||||
mPatternRow = findViewById(R.id.home_security_set_pattern);
|
||||
mPasswordRow = findViewById(R.id.home_security_set_password);
|
||||
bindRows();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onResume() {
|
||||
super.onResume();
|
||||
bindRows();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void finish() {
|
||||
super.finish();
|
||||
overridePendingTransition(R.anim.home_activity_hold, R.anim.home_activity_close);
|
||||
}
|
||||
|
||||
private void bindRows() {
|
||||
final boolean enabled = HomeAppLock.isEnabled();
|
||||
final HomeAppLock.Method method = HomeAppLock.getMethod();
|
||||
HomeSettingsRow.bind(mLockRow, R.drawable.ic_home_lock,
|
||||
getString(R.string.home_settings_app_lock),
|
||||
getString(R.string.home_settings_app_lock_subtitle),
|
||||
false, true, enabled, null, (button, checked) -> onLockToggled(checked));
|
||||
|
||||
bindMethodRow(mPinRow, R.drawable.ic_home_pin, R.string.home_lock_set_pin,
|
||||
method == HomeAppLock.Method.PIN, enabled, HomeAppLock.MODE_SET_PIN);
|
||||
bindMethodRow(mPatternRow, R.drawable.ic_home_pattern, R.string.home_lock_set_pattern,
|
||||
method == HomeAppLock.Method.PATTERN, enabled, HomeAppLock.MODE_SET_PATTERN);
|
||||
bindMethodRow(mPasswordRow, R.drawable.ic_home_password, R.string.home_lock_set_password,
|
||||
method == HomeAppLock.Method.PASSWORD, enabled, HomeAppLock.MODE_SET_PASSWORD);
|
||||
}
|
||||
|
||||
private void bindMethodRow(final View row, final int iconRes, final int titleRes,
|
||||
final boolean current, final boolean enabled, final int mode) {
|
||||
HomeSettingsRow.bind(row, iconRes, getString(titleRes),
|
||||
current ? getString(R.string.home_lock_current_method) : null,
|
||||
true, false, false,
|
||||
v -> startActivity(HomeAppLock.createIntent(this, mode)), null);
|
||||
HomeSettingsRow.setEnabled(row, enabled);
|
||||
}
|
||||
|
||||
private void onLockToggled(final boolean checked) {
|
||||
if (checked) {
|
||||
if (HomeAppLock.hasLockConfigured()) {
|
||||
HomeAppLock.setEnabled(true);
|
||||
bindRows();
|
||||
} else {
|
||||
bindRows();
|
||||
showChooseMethodSheet();
|
||||
}
|
||||
} else {
|
||||
HomeAppLock.setEnabled(false);
|
||||
bindRows();
|
||||
}
|
||||
}
|
||||
|
||||
private void showChooseMethodSheet() {
|
||||
final LayoutInflater inflater = LayoutInflater.from(this);
|
||||
final LinearLayout content = (LinearLayout) inflater.inflate(
|
||||
R.layout.home_lock_method_sheet, null, false);
|
||||
final View pin = content.findViewById(R.id.home_lock_method_pin);
|
||||
final View pattern = content.findViewById(R.id.home_lock_method_pattern);
|
||||
final View password = content.findViewById(R.id.home_lock_method_password);
|
||||
final int[] selected = { HomeAppLock.MODE_SET_PIN };
|
||||
final BottomSheetDialog[] dialog = new BottomSheetDialog[1];
|
||||
selectMethod(pin, pattern, password, selected[0]);
|
||||
pin.setOnClickListener(v -> {
|
||||
selected[0] = HomeAppLock.MODE_SET_PIN;
|
||||
selectMethod(pin, pattern, password, selected[0]);
|
||||
});
|
||||
pattern.setOnClickListener(v -> {
|
||||
selected[0] = HomeAppLock.MODE_SET_PATTERN;
|
||||
selectMethod(pin, pattern, password, selected[0]);
|
||||
});
|
||||
password.setOnClickListener(v -> {
|
||||
selected[0] = HomeAppLock.MODE_SET_PASSWORD;
|
||||
selectMethod(pin, pattern, password, selected[0]);
|
||||
});
|
||||
content.findViewById(R.id.home_lock_method_continue).setOnClickListener(v -> {
|
||||
if (dialog[0] != null) {
|
||||
dialog[0].dismiss();
|
||||
}
|
||||
startActivity(HomeAppLock.createIntent(this, selected[0]));
|
||||
});
|
||||
dialog[0] = HomeBottomSheet.builder(this)
|
||||
.title(R.string.home_lock_choose_method)
|
||||
.custom(content)
|
||||
.show();
|
||||
}
|
||||
|
||||
private static void selectMethod(final View pin, final View pattern, final View password,
|
||||
final int mode) {
|
||||
pin.setSelected(mode == HomeAppLock.MODE_SET_PIN);
|
||||
pattern.setSelected(mode == HomeAppLock.MODE_SET_PATTERN);
|
||||
password.setSelected(mode == HomeAppLock.MODE_SET_PASSWORD);
|
||||
}
|
||||
}
|
||||
@@ -10,7 +10,6 @@ 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;
|
||||
|
||||
@@ -28,7 +27,7 @@ public final class HomeSettingsRow {
|
||||
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 ImageView toggle = row.findViewById(R.id.home_settings_row_switch);
|
||||
final View chevron = row.findViewById(R.id.home_settings_row_chevron);
|
||||
|
||||
icon.setImageResource(iconRes);
|
||||
@@ -48,14 +47,16 @@ public final class HomeSettingsRow {
|
||||
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);
|
||||
toggle.setSelected(switchOn);
|
||||
}
|
||||
row.setOnClickListener(v -> {
|
||||
if (showSwitch && toggle != null) {
|
||||
toggle.toggle();
|
||||
final boolean checked = !toggle.isSelected();
|
||||
toggle.setSelected(checked);
|
||||
if (switchListener != null) {
|
||||
switchListener.onCheckedChanged(null, checked);
|
||||
}
|
||||
}
|
||||
if (clickListener != null) {
|
||||
clickListener.onClick(v);
|
||||
@@ -98,7 +99,7 @@ public final class HomeSettingsRow {
|
||||
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);
|
||||
final View toggle = row.findViewById(R.id.home_settings_row_switch);
|
||||
if (toggle != null) {
|
||||
toggle.setEnabled(enabled);
|
||||
}
|
||||
|
||||
@@ -18,6 +18,8 @@
|
||||
package com.android.messaging.util;
|
||||
|
||||
import android.Manifest;
|
||||
import android.accounts.AccountManager;
|
||||
import android.accounts.AuthenticatorDescription;
|
||||
import android.app.Activity;
|
||||
import android.content.ContentUris;
|
||||
import android.content.Context;
|
||||
@@ -33,11 +35,18 @@ import android.provider.ContactsContract.Directory;
|
||||
import android.provider.ContactsContract.DisplayNameSources;
|
||||
import android.provider.ContactsContract.PhoneLookup;
|
||||
import android.provider.ContactsContract.Profile;
|
||||
import android.provider.ContactsContract.RawContacts;
|
||||
import android.text.TextUtils;
|
||||
import android.content.ActivityNotFoundException;
|
||||
import android.content.Intent;
|
||||
import android.view.View;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashSet;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import com.android.ex.chips.RecipientEntry;
|
||||
import com.android.messaging.datamodel.CursorQueryData;
|
||||
import com.android.messaging.datamodel.FrequentContactsCursorQueryData;
|
||||
@@ -361,6 +370,13 @@ public class ContactUtil {
|
||||
* In that case, each phone will be returned as a separate record in the result cursor.
|
||||
*/
|
||||
public static CursorQueryData getPhones(final Context context) {
|
||||
return getPhones(context, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Same as {@link #getPhones(Context)} but limited to one account, or device-only contacts.
|
||||
*/
|
||||
public static CursorQueryData getPhones(final Context context, final AccountFilter filter) {
|
||||
if (!ContactUtil.hasReadContactsPermission()) {
|
||||
return CursorQueryData.getEmptyQueryData();
|
||||
}
|
||||
@@ -373,8 +389,173 @@ public class ContactUtil {
|
||||
.appendQueryParameter(Contacts.EXTRA_ADDRESS_BOOK_INDEX, "true")
|
||||
.build();
|
||||
|
||||
return new CursorQueryData(context, uri, PhoneQuery.PROJECTION, null, null,
|
||||
PhoneQuery.SORT_KEY);
|
||||
return new CursorQueryData(context, uri, PhoneQuery.PROJECTION,
|
||||
accountSelection(filter), accountSelectionArgs(filter), PhoneQuery.SORT_KEY);
|
||||
}
|
||||
|
||||
/** Restricts a phone/email query to one contacts account. Null means every account. */
|
||||
public static final class AccountFilter {
|
||||
public final String accountName;
|
||||
public final String accountType;
|
||||
public final boolean deviceOnly;
|
||||
|
||||
private AccountFilter(final String accountName, final String accountType,
|
||||
final boolean deviceOnly) {
|
||||
this.accountName = accountName;
|
||||
this.accountType = accountType;
|
||||
this.deviceOnly = deviceOnly;
|
||||
}
|
||||
|
||||
public static AccountFilter account(final String accountName, final String accountType) {
|
||||
if (TextUtils.isEmpty(accountName) && TextUtils.isEmpty(accountType)) {
|
||||
return device();
|
||||
}
|
||||
return new AccountFilter(accountName, accountType == null ? "" : accountType, false);
|
||||
}
|
||||
|
||||
public static AccountFilter device() {
|
||||
return new AccountFilter(null, null, true);
|
||||
}
|
||||
|
||||
public boolean matches(final AccountFilter other) {
|
||||
if (other == null) {
|
||||
return false;
|
||||
}
|
||||
return deviceOnly == other.deviceOnly
|
||||
&& TextUtils.equals(accountName, other.accountName)
|
||||
&& TextUtils.equals(accountType, other.accountType);
|
||||
}
|
||||
}
|
||||
|
||||
/** One source shown in the compose "All contacts" sheet. */
|
||||
public static final class ContactAccount {
|
||||
public final String accountName;
|
||||
public final String accountType;
|
||||
public final String title;
|
||||
public final String subtitle;
|
||||
public final int contactCount;
|
||||
public final boolean deviceOnly;
|
||||
|
||||
ContactAccount(final String accountName, final String accountType, final String title,
|
||||
final String subtitle, final int contactCount, final boolean deviceOnly) {
|
||||
this.accountName = accountName;
|
||||
this.accountType = accountType;
|
||||
this.title = title;
|
||||
this.subtitle = subtitle;
|
||||
this.contactCount = contactCount;
|
||||
this.deviceOnly = deviceOnly;
|
||||
}
|
||||
|
||||
public AccountFilter toFilter() {
|
||||
return deviceOnly ? AccountFilter.device() : AccountFilter.account(accountName,
|
||||
accountType);
|
||||
}
|
||||
}
|
||||
|
||||
private static String accountSelection(final AccountFilter filter) {
|
||||
if (filter == null) {
|
||||
return null;
|
||||
}
|
||||
if (filter.deviceOnly) {
|
||||
return "(" + RawContacts.ACCOUNT_NAME + " IS NULL OR " + RawContacts.ACCOUNT_NAME
|
||||
+ "='') AND (" + RawContacts.ACCOUNT_TYPE + " IS NULL OR "
|
||||
+ RawContacts.ACCOUNT_TYPE + "='')";
|
||||
}
|
||||
return RawContacts.ACCOUNT_NAME + "=? AND " + RawContacts.ACCOUNT_TYPE + "=?";
|
||||
}
|
||||
|
||||
private static String[] accountSelectionArgs(final AccountFilter filter) {
|
||||
if (filter == null || filter.deviceOnly) {
|
||||
return null;
|
||||
}
|
||||
return new String[] { filter.accountName, filter.accountType };
|
||||
}
|
||||
|
||||
/**
|
||||
* Accounts that currently have at least one raw contact, for the compose source sheet.
|
||||
*/
|
||||
public static List<ContactAccount> getContactAccounts(final Context context) {
|
||||
final List<ContactAccount> accounts = new ArrayList<>();
|
||||
if (!hasReadContactsPermission() || context == null) {
|
||||
return accounts;
|
||||
}
|
||||
final Map<String, AccountBucket> buckets = new LinkedHashMap<>();
|
||||
Cursor cursor = null;
|
||||
try {
|
||||
cursor = context.getContentResolver().query(RawContacts.CONTENT_URI,
|
||||
new String[] {
|
||||
RawContacts.ACCOUNT_NAME,
|
||||
RawContacts.ACCOUNT_TYPE,
|
||||
RawContacts.CONTACT_ID
|
||||
},
|
||||
RawContacts.DELETED + "=0",
|
||||
null,
|
||||
RawContacts.ACCOUNT_NAME);
|
||||
if (cursor != null) {
|
||||
while (cursor.moveToNext()) {
|
||||
final String name = cursor.getString(0);
|
||||
final String type = cursor.getString(1);
|
||||
final long contactId = cursor.getLong(2);
|
||||
final boolean device = TextUtils.isEmpty(name) && TextUtils.isEmpty(type);
|
||||
final String key = device ? "device" : name + "\0" + type;
|
||||
AccountBucket bucket = buckets.get(key);
|
||||
if (bucket == null) {
|
||||
bucket = new AccountBucket(name, type, device);
|
||||
buckets.put(key, bucket);
|
||||
}
|
||||
bucket.contactIds.add(contactId);
|
||||
}
|
||||
}
|
||||
} catch (final SecurityException ignored) {
|
||||
return accounts;
|
||||
} finally {
|
||||
if (cursor != null) {
|
||||
cursor.close();
|
||||
}
|
||||
}
|
||||
for (final AccountBucket bucket : buckets.values()) {
|
||||
accounts.add(new ContactAccount(bucket.accountName, bucket.accountType,
|
||||
bucket.deviceOnly ? null : bucket.accountName,
|
||||
authenticatorLabel(context, bucket.accountType, bucket.deviceOnly),
|
||||
bucket.contactIds.size(), bucket.deviceOnly));
|
||||
}
|
||||
return accounts;
|
||||
}
|
||||
|
||||
private static final class AccountBucket {
|
||||
final String accountName;
|
||||
final String accountType;
|
||||
final boolean deviceOnly;
|
||||
final HashSet<Long> contactIds = new HashSet<>();
|
||||
|
||||
AccountBucket(final String accountName, final String accountType,
|
||||
final boolean deviceOnly) {
|
||||
this.accountName = accountName;
|
||||
this.accountType = accountType;
|
||||
this.deviceOnly = deviceOnly;
|
||||
}
|
||||
}
|
||||
|
||||
private static String authenticatorLabel(final Context context, final String accountType,
|
||||
final boolean deviceOnly) {
|
||||
if (deviceOnly || TextUtils.isEmpty(accountType)) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
final AuthenticatorDescription[] types =
|
||||
AccountManager.get(context).getAuthenticatorTypes();
|
||||
for (final AuthenticatorDescription description : types) {
|
||||
if (TextUtils.equals(description.type, accountType)) {
|
||||
final CharSequence label = context.getPackageManager().getText(
|
||||
description.packageName, description.labelId, null);
|
||||
if (!TextUtils.isEmpty(label)) {
|
||||
return label.toString();
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (final RuntimeException ignored) {
|
||||
}
|
||||
return accountType;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -403,10 +584,15 @@ public class ContactUtil {
|
||||
*/
|
||||
public static CursorQueryData filterDestination(final Context context,
|
||||
final String destination) {
|
||||
return filterDestination(context, destination, null);
|
||||
}
|
||||
|
||||
public static CursorQueryData filterDestination(final Context context,
|
||||
final String destination, final AccountFilter filter) {
|
||||
if (shouldFilterForEmail(destination)) {
|
||||
return ContactUtil.filterEmails(context, destination);
|
||||
return ContactUtil.filterEmails(context, destination, filter);
|
||||
} else {
|
||||
return ContactUtil.filterPhones(context, destination);
|
||||
return ContactUtil.filterPhones(context, destination, filter);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -430,7 +616,13 @@ public class ContactUtil {
|
||||
* we support email addresses as well.
|
||||
*/
|
||||
public static CursorQueryData filterPhones(final Context context, final String query) {
|
||||
return filterPhonesInternal(context, Phone.CONTENT_FILTER_URI, query, Directory.DEFAULT);
|
||||
return filterPhones(context, query, null);
|
||||
}
|
||||
|
||||
public static CursorQueryData filterPhones(final Context context, final String query,
|
||||
final AccountFilter filter) {
|
||||
return filterPhonesInternal(context, Phone.CONTENT_FILTER_URI, query, Directory.DEFAULT,
|
||||
filter);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -439,18 +631,19 @@ public class ContactUtil {
|
||||
public static CursorQueryData filterPhonesEnterprise(final Context context,
|
||||
final String query) {
|
||||
return filterPhonesInternal(context, Phone.ENTERPRISE_CONTENT_FILTER_URI, query,
|
||||
Directory.ENTERPRISE_DEFAULT);
|
||||
Directory.ENTERPRISE_DEFAULT, null);
|
||||
}
|
||||
|
||||
private static CursorQueryData filterPhonesInternal(final Context context,
|
||||
final Uri phoneFilterBaseUri, final String query, final long directoryId) {
|
||||
final Uri phoneFilterBaseUri, final String query, final long directoryId,
|
||||
final AccountFilter filter) {
|
||||
if (!ContactUtil.hasReadContactsPermission()) {
|
||||
return CursorQueryData.getEmptyQueryData();
|
||||
}
|
||||
Uri phoneFilterUri = buildDirectorySearchUri(phoneFilterBaseUri, query, directoryId);
|
||||
return new CursorQueryData(context,
|
||||
phoneFilterUri,
|
||||
PhoneQuery.PROJECTION, null, null,
|
||||
PhoneQuery.PROJECTION, accountSelection(filter), accountSelectionArgs(filter),
|
||||
PhoneQuery.SORT_KEY);
|
||||
}
|
||||
/**
|
||||
@@ -492,7 +685,13 @@ public class ContactUtil {
|
||||
* we support email addresses as well.
|
||||
*/
|
||||
public static CursorQueryData filterEmails(final Context context, final String query) {
|
||||
return filterEmailsInternal(context, Email.CONTENT_FILTER_URI, query, Directory.DEFAULT);
|
||||
return filterEmails(context, query, null);
|
||||
}
|
||||
|
||||
public static CursorQueryData filterEmails(final Context context, final String query,
|
||||
final AccountFilter filter) {
|
||||
return filterEmailsInternal(context, Email.CONTENT_FILTER_URI, query, Directory.DEFAULT,
|
||||
filter);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -501,11 +700,12 @@ public class ContactUtil {
|
||||
public static CursorQueryData filterEmailsEnterprise(final Context context,
|
||||
final String query) {
|
||||
return filterEmailsInternal(context, Email.ENTERPRISE_CONTENT_FILTER_URI, query,
|
||||
Directory.ENTERPRISE_DEFAULT);
|
||||
Directory.ENTERPRISE_DEFAULT, null);
|
||||
}
|
||||
|
||||
private static CursorQueryData filterEmailsInternal(final Context context,
|
||||
final Uri filterEmailsBaseUri, final String query, final long directoryId) {
|
||||
final Uri filterEmailsBaseUri, final String query, final long directoryId,
|
||||
final AccountFilter filter) {
|
||||
if (!ContactUtil.hasReadContactsPermission()) {
|
||||
return CursorQueryData.getEmptyQueryData();
|
||||
}
|
||||
@@ -513,7 +713,7 @@ public class ContactUtil {
|
||||
directoryId);
|
||||
return new CursorQueryData(context,
|
||||
filterEmailsUri,
|
||||
PhoneQuery.PROJECTION, null, null,
|
||||
PhoneQuery.PROJECTION, accountSelection(filter), accountSelectionArgs(filter),
|
||||
PhoneQuery.SORT_KEY);
|
||||
}
|
||||
|
||||
|
||||
@@ -116,7 +116,15 @@ public class OsUtil {
|
||||
}
|
||||
|
||||
public static boolean hasLocationPermission() {
|
||||
return OsUtil.hasPermission(Manifest.permission.ACCESS_FINE_LOCATION);
|
||||
return hasPermission(Manifest.permission.ACCESS_FINE_LOCATION)
|
||||
|| hasPermission(Manifest.permission.ACCESS_COARSE_LOCATION);
|
||||
}
|
||||
|
||||
public static String[] getLocationPermissions() {
|
||||
return new String[] {
|
||||
Manifest.permission.ACCESS_FINE_LOCATION,
|
||||
Manifest.permission.ACCESS_COARSE_LOCATION,
|
||||
};
|
||||
}
|
||||
|
||||
public static boolean hasRecordAudioPermission() {
|
||||
|
||||
Reference in New Issue
Block a user