Messaging: Add scheduled send, location share, app lock, and settings polish

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-09-27 08:13:21 +09:00
co-authored by Cursor
parent 42148360e0
commit 74fb80f783
114 changed files with 6230 additions and 606 deletions
+5 -3
View File
@@ -87,11 +87,13 @@ android_app {
"1.0.001",
"--version-code",
"10001040",
"--rename-manifest-package",
"com.filabcorp.msgq",
],
required: [
"libframesequence",
"libgiftranscode",
"com.bnkode.notie.allowlist",
"com.filabcorp.msgq.allowlist",
],
optimize: {
obfuscate: true,
@@ -105,9 +107,9 @@ android_app {
}
prebuilt_etc {
name: "com.bnkode.notie.allowlist",
name: "com.filabcorp.msgq.allowlist",
product_specific: true,
sub_dir: "sysconfig",
src: "com.bnkode.notie.allowlist.xml",
src: "com.filabcorp.msgq.allowlist.xml",
filename_from_src: true,
}
+4 -3
View File
@@ -13,13 +13,13 @@ android {
compileSdk 35
defaultConfig {
applicationId 'com.bnkode.notie'
applicationId 'com.filabcorp.msgq'
minSdk 33
targetSdk 34
versionCode 10001040
versionName '1.0.001'
vectorDrawables.useSupportLibrary true
resourceConfigurations += ['en']
resourceConfigurations += ['en', 'ko']
}
compileOptions {
@@ -78,6 +78,7 @@ dependencies {
implementation 'androidx.core:core:1.15.0'
implementation 'androidx.exifinterface:exifinterface:1.3.7'
implementation 'androidx.fragment:fragment:1.8.5'
implementation 'androidx.lifecycle:lifecycle-process:2.8.7'
implementation 'androidx.media:media:1.7.0'
implementation 'androidx.palette:palette:1.0.0'
implementation 'androidx.preference:preference:1.2.1'
@@ -104,5 +105,5 @@ tasks.register('launchMessaging', Exec) {
description = "Launch Messaging on ${messagingAvdSerial}"
dependsOn 'installMessaging'
commandLine adbExecutable.absolutePath, '-s', messagingAvdSerial, 'shell', 'am', 'start',
'-n', 'com.bnkode.notie/com.android.messaging.ui.conversationlist.ConversationListActivity'
'-n', 'com.filabcorp.msgq/com.android.messaging.ui.conversationlist.ConversationListActivity'
}
+55 -9
View File
@@ -46,6 +46,10 @@
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES" />
<uses-permission android:name="android.permission.READ_MEDIA_VIDEO" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.SCHEDULE_EXACT_ALARM" />
<uses-permission android:name="android.permission.USE_EXACT_ALARM" />
<!-- Optional features -->
<uses-feature
@@ -66,6 +70,12 @@
<uses-feature
android:name="android.hardware.telephony"
android:required="false" />
<uses-feature
android:name="android.hardware.location"
android:required="false" />
<uses-feature
android:name="android.hardware.location.gps"
android:required="false" />
<queries>
<intent>
@@ -82,6 +92,7 @@
android:appCategory="social"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:localeConfig="@xml/locales_config"
android:requestLegacyExternalStorage="true"
android:supportsRtl="true"
android:theme="@style/BugleTheme">
@@ -206,6 +217,32 @@
android:theme="@style/Home.BottomSheet"
android:windowSoftInputMode="adjustResize|stateHidden" />
<activity
android:name=".ui.home.HomeLocationActivity"
android:configChanges="orientation|screenSize|keyboardHidden"
android:label="@string/home_location_title"
android:parentActivityName="com.android.messaging.ui.conversationlist.ConversationListActivity"
android:screenOrientation="user"
android:theme="@style/Home.SubScreen" />
<activity
android:name=".ui.home.HomeSecuritySettingsActivity"
android:configChanges="orientation|screenSize|keyboardHidden"
android:label="@string/home_security_title"
android:parentActivityName="com.android.messaging.ui.appsettings.ApplicationSettingsActivity"
android:screenOrientation="user"
android:theme="@style/Home.SubScreen" />
<activity
android:name=".ui.home.HomeAppLockActivity"
android:configChanges="orientation|screenSize|keyboardHidden"
android:excludeFromRecents="true"
android:label="@string/home_settings_app_lock"
android:launchMode="singleTop"
android:screenOrientation="user"
android:theme="@style/Home.SubScreen"
android:windowSoftInputMode="adjustResize" />
<!-- Full-screen photo viewer -->
<activity
android:name=".ui.photoviewer.BuglePhotoViewActivity"
@@ -320,7 +357,8 @@
android:configChanges="orientation|screenSize|keyboardHidden"
android:exported="false"
android:screenOrientation="user"
android:theme="@style/Invisible">
android:theme="@style/Home.TransparentSheetHost"
android:windowSoftInputMode="adjustResize">
</activity>
<!-- VCard details -->
@@ -343,20 +381,20 @@
<provider
android:name=".datamodel.MessagingContentProvider"
android:authorities="com.bnkode.notie.datamodel.MessagingContentProvider"
android:authorities="com.filabcorp.msgq.datamodel.MessagingContentProvider"
android:exported="false"
android:label="@string/app_name">
</provider>
<provider
android:name=".datamodel.MmsFileProvider"
android:authorities="com.bnkode.notie.datamodel.MmsFileProvider"
android:authorities="com.filabcorp.msgq.datamodel.MmsFileProvider"
android:exported="false"
android:grantUriPermissions="true" />
<provider
android:name=".datamodel.MediaScratchFileProvider"
android:authorities="com.bnkode.notie.datamodel.MediaScratchFileProvider"
android:authorities="com.filabcorp.msgq.datamodel.MediaScratchFileProvider"
android:exported="false"
android:grantUriPermissions="true" />
@@ -412,18 +450,18 @@
android:name=".receiver.SendStatusReceiver"
android:exported="false">
<intent-filter>
<action android:name="com.bnkode.notie.receiver.SendStatusReceiver.MESSAGE_SENT" />
<action android:name="com.filabcorp.msgq.receiver.SendStatusReceiver.MESSAGE_SENT" />
<data android:scheme="content" />
</intent-filter>
<intent-filter>
<action android:name="com.bnkode.notie.receiver.SendStatusReceiver.MESSAGE_DELIVERED" />
<action android:name="com.filabcorp.msgq.receiver.SendStatusReceiver.MESSAGE_DELIVERED" />
</intent-filter>
<intent-filter>
<action android:name="com.bnkode.notie.receiver.SendStatusReceiver.MMS_SENT" />
<action android:name="com.filabcorp.msgq.receiver.SendStatusReceiver.MMS_SENT" />
<data android:scheme="content" />
</intent-filter>
<intent-filter>
<action android:name="com.bnkode.notie.receiver.SendStatusReceiver.MMS_DOWNLOADED" />
<action android:name="com.filabcorp.msgq.receiver.SendStatusReceiver.MMS_DOWNLOADED" />
<data android:scheme="content" />
</intent-filter>
</receiver>
@@ -462,6 +500,14 @@
</intent-filter>
</receiver>
<receiver
android:name=".receiver.ScheduledMessageReceiver"
android:exported="false">
<intent-filter>
<action android:name="com.filabcorp.msgq.SEND_SCHEDULED_MESSAGE" />
</intent-filter>
</receiver>
<!-- Broadcast receiver that will be notified to reset notifications -->
<receiver
android:name=".receiver.NotificationReceiver"
@@ -473,7 +519,7 @@
android:name=".datamodel.action.ActionServiceImpl$PendingActionReceiver"
android:exported="false">
<intent-filter>
<action android:name="com.bnkode.notie.datamodel.PENDING_ACTION" />
<action android:name="com.filabcorp.msgq.datamodel.PENDING_ACTION" />
</intent-filter>
</receiver>
@@ -0,0 +1,34 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no">
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css"/>
<style>
html, body, #map { height: 100%; margin: 0; padding: 0; background: #f4f1fb; }
.leaflet-control-attribution { font-size: 10px; }
</style>
</head>
<body>
<div id="map"></div>
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
<script>
var map = L.map('map', { zoomControl: false }).setView([37.5665, 126.9780], 16);
L.control.zoom({ position: 'bottomright' }).addTo(map);
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
maxZoom: 19,
attribution: '&copy; OpenStreetMap'
}).addTo(map);
function notify() {
var c = map.getCenter();
if (window.LocationBridge) {
LocationBridge.onMapMoved(c.lat, c.lng);
}
}
map.on('moveend', notify);
function setCenter(lat, lng) {
map.setView([lat, lng], Math.max(map.getZoom(), 16));
}
</script>
</body>
</html>
@@ -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);
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);
}
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);
}
} 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();
// 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;
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);
if (TextUtils.isEmpty(text) && messageLineInfo.mAttachmentUri != null) {
text = BugleNotifications.formatAttachmentTag(
convInfo.mIsGroup ? messageLineInfo.mAuthorFirstName : null,
messageLineInfo.mAttachmentType);
}
buf.append(text);
if (i > 0) {
buf.append('\n');
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);
}
// Show a single notification -- big style with the text of all the messages
notifStyle = new NotificationCompat.BigTextStyle(builder).bigText(buf);
}
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);
}
@@ -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));
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,
bindRow(R.id.home_settings_blocked, R.drawable.ic_home_menu_spam,
R.string.home_settings_blocked, R.string.home_settings_blocked_subtitle,
v -> {
startActivity(UIIntents.get().getAdvancedSettingsIntent(this));
overridePendingTransition(R.anim.home_activity_open,
R.anim.home_activity_hold);
},
null);
} else {
advancedCard.setVisibility(View.GONE);
}
UIIntents.get().launchHomeSpamActivity(this);
overridePendingTransition(R.anim.home_activity_open, R.anim.home_activity_hold);
});
HomeSettingsRow.bind(findViewById(R.id.home_settings_licenses),
R.drawable.ic_info_light,
getString(R.string.home_settings_licenses),
null, true, false, false,
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);
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());
@@ -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);
}
/**
@@ -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);
}
@@ -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) {
private int getChromeSlotHeight() {
int height = 0;
if (mHomeCategoryStrip != null && mHomeCategoryStrip.getHeight() > 0) {
height = Math.max(height, mHomeCategoryStrip.getHeight());
mCategoryNaturalHeight = mHomeCategoryStrip.getHeight();
}
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);
}
final int height = mSearchBar.getHeight();
return height > 0 ? height : 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();
positionListProfilePopup(anchorView, () -> {
ensureListProfileWindow();
mHomeListProfilePopup.setVisibility(View.VISIBLE);
showListProfileWindow();
positionListProfilePopup(anchorView, () -> {
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,8 +1940,6 @@ 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];
@@ -1802,9 +1953,13 @@ public class ConversationListFragment extends Fragment implements ConversationLi
} else {
windowRight = getResources().getDisplayMetrics().widthPixels;
}
xOff = windowRight - endInset - content.getMeasuredWidth() - anchorLoc[0];
} else {
xOff = 0;
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() {
@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<ripple xmlns:android="http://schemas.android.com/apk/res/android"
android:color="@color/home_surface_elevated">
<item>
<shape android:shape="rectangle">
<corners android:radius="@dimen/looply_radius_pill" />
<solid android:color="@color/home_chip_bg" />
</shape>
</item>
</ripple>
@@ -0,0 +1,13 @@
<?xml version="1.0" encoding="utf-8"?>
<ripple xmlns:android="http://schemas.android.com/apk/res/android"
android:color="@color/home_accent_soft">
<item>
<shape android:shape="rectangle">
<corners android:radius="@dimen/looply_radius_md" />
<solid android:color="@color/home_search_field_bg" />
<stroke
android:width="1.5dp"
android:color="@color/home_divider" />
</shape>
</item>
</ripple>
@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<ripple xmlns:android="http://schemas.android.com/apk/res/android"
android:color="@color/home_accent">
<item>
<shape android:shape="rectangle">
<solid android:color="@color/home_accent_soft" />
<corners android:radius="@dimen/looply_radius_pill" />
</shape>
</item>
</ripple>
@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<ripple xmlns:android="http://schemas.android.com/apk/res/android"
android:color="@color/home_surface_elevated">
<item>
<shape android:shape="rectangle">
<solid android:color="@color/home_row_bg" />
<corners
android:bottomLeftRadius="@dimen/looply_radius_md"
android:bottomRightRadius="@dimen/looply_radius_md" />
</shape>
</item>
</ripple>
@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<ripple xmlns:android="http://schemas.android.com/apk/res/android"
android:color="@color/home_surface_elevated">
<item>
<shape android:shape="rectangle">
<solid android:color="@color/home_row_bg" />
</shape>
</item>
</ripple>
@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<ripple xmlns:android="http://schemas.android.com/apk/res/android"
android:color="@color/home_surface_elevated">
<item>
<shape android:shape="rectangle">
<solid android:color="@color/home_row_bg" />
<corners android:radius="@dimen/looply_radius_md" />
</shape>
</item>
</ripple>
@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<ripple xmlns:android="http://schemas.android.com/apk/res/android"
android:color="@color/home_surface_elevated">
<item>
<shape android:shape="rectangle">
<solid android:color="@color/home_row_bg" />
<corners
android:topLeftRadius="@dimen/looply_radius_md"
android:topRightRadius="@dimen/looply_radius_md" />
</shape>
</item>
</ripple>
@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<gradient
android:angle="270"
android:endColor="#99F4F1FB"
android:startColor="#00F4F1FB"
android:type="linear" />
</shape>
@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<gradient
android:angle="270"
android:centerColor="#4DF4F1FB"
android:endColor="#00F4F1FB"
android:startColor="@color/home_bg"
android:type="linear" />
</shape>
@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<solid android:color="@color/home_accent" />
<corners android:radius="26dp" />
</shape>
@@ -0,0 +1,21 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:state_selected="true">
<shape android:shape="rectangle">
<solid android:color="@color/home_search_field_bg" />
<corners android:radius="@dimen/looply_radius_md" />
<stroke
android:width="2dp"
android:color="@color/home_accent" />
</shape>
</item>
<item>
<shape android:shape="rectangle">
<solid android:color="@color/home_search_field_bg" />
<corners android:radius="@dimen/looply_radius_md" />
<stroke
android:width="1.5dp"
android:color="@color/home_divider" />
</shape>
</item>
</selector>
@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<ripple xmlns:android="http://schemas.android.com/apk/res/android"
android:color="@color/home_surface_elevated">
<item>
<shape android:shape="oval">
<solid android:color="@color/home_row_bg" />
<stroke
android:width="1dp"
android:color="@color/home_button_stroke" />
</shape>
</item>
</ripple>
@@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:state_selected="true">
<shape android:shape="oval">
<solid android:color="@color/home_accent" />
<size android:width="14dp" android:height="14dp" />
</shape>
</item>
<item>
<shape android:shape="oval">
<solid android:color="@android:color/transparent" />
<stroke
android:width="2dp"
android:color="@color/home_divider" />
<size android:width="14dp" android:height="14dp" />
</shape>
</item>
</selector>
@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<ripple xmlns:android="http://schemas.android.com/apk/res/android"
android:color="@color/home_accent">
<item>
<shape android:shape="rectangle">
<solid android:color="@color/home_unknown_sender_banner_bg" />
<corners android:radius="@dimen/looply_radius_pill" />
</shape>
</item>
</ripple>
@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="@drawable/ic_home_switch_on" android:state_selected="true" />
<item android:drawable="@drawable/ic_home_switch_off" />
</selector>
@@ -0,0 +1,19 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="@android:color/transparent"
android:pathData="M8.5,6h11A2.5,2.5 0,0 1,22 8.5v7A2.5,2.5 0,0 1,19.5 18h-11L3,12z"
android:strokeWidth="1.5"
android:strokeColor="#000000"
android:strokeLineJoin="round" />
<path
android:fillColor="@android:color/transparent"
android:pathData="M11,9.5l5,5M16,9.5l-5,5"
android:strokeWidth="1.5"
android:strokeColor="#000000"
android:strokeLineCap="round" />
</vector>
@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="16dp"
android:height="16dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#8E8E93"
android:pathData="M7.41,8.59L12,13.17l4.59,-4.58L18,10l-6,6 -6,-6 1.41,-1.41z" />
</vector>
@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#000000"
android:pathData="M12,2C6.48,2 2,6.48 2,12s4.48,10 10,10 10,-4.48 10,-10S17.52,2 12,2zM4,12c0,-0.61 0.08,-1.21 0.21,-1.78L8,14.5V16c0,1.1 0.9,2 2,2v1.93C6.47,19.16 4,15.91 4,12zM16.9,18.42V17c0,-0.55 -0.45,-1 -1,-1h-1v-3c0,-0.55 -0.45,-1 -1,-1h-3v-2h2c0.55,0 1,-0.45 1,-1V8h2c1.1,0 2,-0.9 2,-2V5.59C18.87,6.88 20,9.3 20,12c0,3.35 -2.08,6.22 -5.03,7.42l1.93,-1z" />
</vector>
@@ -0,0 +1,19 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="@android:color/transparent"
android:pathData="M7,11V8a5,5 0,0 1,10 0v3"
android:strokeWidth="1.5"
android:strokeColor="#000000"
android:strokeLineCap="round" />
<path
android:fillColor="@android:color/transparent"
android:pathData="M6,11h12a2,2 0,0 1,2 2v7a2,2 0,0 1,-2 2H6a2,2 0,0 1,-2 -2v-7a2,2 0,0 1,2 -2z"
android:strokeWidth="1.5"
android:strokeColor="#000000"
android:strokeLineJoin="round" />
</vector>
@@ -0,0 +1,19 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="@android:color/transparent"
android:pathData="M5,11h14a2,2 0,0 1,2 2v6a2,2 0,0 1,-2 2H5a2,2 0,0 1,-2 -2v-6a2,2 0,0 1,2 -2z"
android:strokeWidth="1.5"
android:strokeColor="#000000"
android:strokeLineJoin="round" />
<path
android:fillColor="@android:color/transparent"
android:pathData="M8,11V8a4,4 0,0 1,8 0v3"
android:strokeWidth="1.5"
android:strokeColor="#000000"
android:strokeLineCap="round" />
</vector>
@@ -0,0 +1,34 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#000000"
android:pathData="M6,6m-1.6,0a1.6,1.6 0,1 1,3.2 0a1.6,1.6 0,1 1,-3.2 0" />
<path
android:fillColor="#000000"
android:pathData="M12,6m-1.6,0a1.6,1.6 0,1 1,3.2 0a1.6,1.6 0,1 1,-3.2 0" />
<path
android:fillColor="#000000"
android:pathData="M18,6m-1.6,0a1.6,1.6 0,1 1,3.2 0a1.6,1.6 0,1 1,-3.2 0" />
<path
android:fillColor="#000000"
android:pathData="M6,12m-1.6,0a1.6,1.6 0,1 1,3.2 0a1.6,1.6 0,1 1,-3.2 0" />
<path
android:fillColor="#000000"
android:pathData="M12,12m-1.6,0a1.6,1.6 0,1 1,3.2 0a1.6,1.6 0,1 1,-3.2 0" />
<path
android:fillColor="#000000"
android:pathData="M18,12m-1.6,0a1.6,1.6 0,1 1,3.2 0a1.6,1.6 0,1 1,-3.2 0" />
<path
android:fillColor="#000000"
android:pathData="M6,18m-1.6,0a1.6,1.6 0,1 1,3.2 0a1.6,1.6 0,1 1,-3.2 0" />
<path
android:fillColor="#000000"
android:pathData="M12,18m-1.6,0a1.6,1.6 0,1 1,3.2 0a1.6,1.6 0,1 1,-3.2 0" />
<path
android:fillColor="#000000"
android:pathData="M18,18m-1.6,0a1.6,1.6 0,1 1,3.2 0a1.6,1.6 0,1 1,-3.2 0" />
</vector>
+13
View File
@@ -0,0 +1,13 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="@android:color/transparent"
android:pathData="M4,8h16M4,12h16M4,16h10"
android:strokeWidth="1.5"
android:strokeColor="#000000"
android:strokeLineCap="round" />
</vector>
@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#000000"
android:pathData="M17.65,6.35C16.2,4.9 14.21,4 12,4c-4.42,0 -7.99,3.58 -7.99,8s3.57,8 7.99,8c3.73,0 6.84,-2.55 7.73,-6h-2.08c-0.82,2.33 -3.04,4 -5.65,4 -3.31,0 -6,-2.69 -6,-6s2.69,-6 6,-6c1.66,0 3.14,0.69 4.22,1.78L13,11h7V4l-2.35,2.35z" />
</vector>
@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="14dp"
android:height="14dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#000000"
android:pathData="M17.65,6.35C16.2,4.9 14.21,4 12,4c-4.42,0 -7.99,3.58 -7.99,8s3.57,8 7.99,8c3.73,0 6.84,-2.55 7.73,-6h-2.08c-0.82,2.33 -3.04,4 -5.65,4 -3.31,0 -6,-2.69 -6,-6s2.69,-6 6,-6c1.66,0 3.14,0.69 4.22,1.78L13,11h7V4l-2.35,2.35z" />
</vector>
@@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="52dp"
android:height="32dp"
android:viewportWidth="52"
android:viewportHeight="32">
<path
android:fillColor="@color/home_switch_off"
android:fillType="evenOdd"
android:pathData="M16,0H36A16,16 0,0 1,36 32H16A16,16 0,0 1,16 0ZM16,16m-13,0a13,13 0,1 1,26 0a13,13 0,1 1,-26 0" />
</vector>
@@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="52dp"
android:height="32dp"
android:viewportWidth="52"
android:viewportHeight="32">
<path
android:fillColor="@color/home_switch_on"
android:fillType="evenOdd"
android:pathData="M16,0H36A16,16 0,0 1,36 32H16A16,16 0,0 1,16 0ZM36,16m-13,0a13,13 0,1 1,26 0a13,13 0,1 1,-26 0" />
</vector>
@@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="@android:color/transparent"
android:pathData="M2.5,12S6,6.5 12,6.5 21.5,12 21.5,12 18,17.5 12,17.5 2.5,12 2.5,12z"
android:strokeWidth="1.5"
android:strokeColor="#000000"
android:strokeLineJoin="round" />
<path
android:fillColor="@android:color/transparent"
android:pathData="M12,12m-2.5,0a2.5,2.5 0,1 1,5 0a2.5,2.5 0,1 1,-5 0"
android:strokeWidth="1.5"
android:strokeColor="#000000" />
</vector>
@@ -0,0 +1,26 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="@android:color/transparent"
android:pathData="M3,4.5L20.5,22"
android:strokeWidth="1.5"
android:strokeColor="#000000"
android:strokeLineCap="round" />
<path
android:fillColor="@android:color/transparent"
android:pathData="M9.2,9.4C8.2,10.1 7.5,11.1 7.5,12.4a4.5,4.5 0,0 0,7.1 3.7"
android:strokeWidth="1.5"
android:strokeColor="#000000"
android:strokeLineCap="round" />
<path
android:fillColor="@android:color/transparent"
android:pathData="M4,12s2.6,-4.2 8,-4.2c1.1,0 2.1,0.2 3,0.5M20,12s-2.6,4.2 -8,4.2c-0.8,0 -1.5,-0.1 -2.2,-0.3"
android:strokeWidth="1.5"
android:strokeColor="#000000"
android:strokeLineCap="round"
android:strokeLineJoin="round" />
</vector>
@@ -188,6 +188,49 @@
<LinearLayout
android:id="@+id/home_schedule_chip_row"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="start"
android:orientation="horizontal"
android:paddingStart="@dimen/looply_space_md"
android:paddingTop="@dimen/looply_space_sm"
android:paddingEnd="@dimen/looply_space_md"
android:visibility="gone">
<LinearLayout
android:id="@+id/home_schedule_chip"
android:layout_width="wrap_content"
android:layout_height="40dp"
android:background="@drawable/home_bg_schedule_chip"
android:clickable="true"
android:focusable="true"
android:gravity="center_vertical"
android:orientation="horizontal"
android:paddingStart="16dp"
android:paddingEnd="4dp">
<TextView
android:id="@+id/home_schedule_chip_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:includeFontPadding="false"
android:textColor="@color/home_unknown_sender_accent"
android:textSize="14sp" />
<ImageButton
android:id="@+id/home_schedule_chip_clear"
android:layout_width="36dp"
android:layout_height="36dp"
android:background="?attr/selectableItemBackgroundBorderless"
android:contentDescription="@string/home_schedule_clear"
android:padding="8dp"
android:src="@drawable/ic_home_close"
app:tint="@color/home_unknown_sender_accent" />
</LinearLayout>
</LinearLayout>
<!-- Google Messages–style compose row: pill (+ | text | emoji | gallery) + voice/send -->
<LinearLayout
@@ -15,9 +15,16 @@
See the License for the specific language governing permissions and
limitations under the License.
-->
<LinearLayout
<FrameLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/home_bg"
android:clipChildren="false"
android:clipToPadding="false">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/home_bg"
@@ -197,14 +204,23 @@
app:srcCompat="@drawable/ic_checkmark_circle_primary" />
</FrameLayout>
<FrameLayout
android:id="@+id/home_category_collapse"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@color/home_bg"
android:clipChildren="true"
android:clipToPadding="true">
<com.android.messaging.ui.home.HomeCategoryStrip
android:id="@+id/home_category_strip"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="top"
android:paddingStart="@dimen/looply_screen_padding"
android:paddingTop="@dimen/looply_space_md"
android:paddingTop="@dimen/looply_space_sm"
android:paddingEnd="12dp"
android:paddingBottom="@dimen/looply_space_md">
android:paddingBottom="2dp">
<FrameLayout
android:id="@+id/home_category_all"
@@ -263,56 +279,20 @@
android:layout_height="32dp" />
</com.android.messaging.ui.home.HomeCategoryStrip>
</LinearLayout>
<FrameLayout
android:id="@+id/home_messages_host"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
android:clipChildren="true"
android:clipToPadding="false">
<androidx.recyclerview.widget.RecyclerView
android:id="@android:id/list"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/home_bg"
android:clipToPadding="false"
android:dividerHeight="0px"
android:listSelector="@android:color/transparent"
android:paddingBottom="104dp" />
<include
android:id="@+id/no_conversations_view"
layout="@layout/home_empty_state"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:visibility="gone" />
<include
android:id="@+id/home_automation_pane"
layout="@layout/home_automation_pane"
android:layout_width="match_parent"
android:layout_height="match_parent" />
<include layout="@layout/home_list_profile_popup" />
<LinearLayout
android:id="@+id/home_search_bar_container"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="top"
android:background="@drawable/home_bg_search_bar_outer"
android:background="@color/home_bg"
android:clipChildren="false"
android:clipToPadding="false"
android:elevation="0dp"
android:orientation="vertical"
android:paddingStart="12dp"
android:paddingTop="6dp"
android:paddingTop="4dp"
android:paddingEnd="12dp"
android:paddingBottom="10dp"
android:visibility="gone">
android:paddingBottom="4dp"
android:visibility="invisible">
<LinearLayout
android:id="@+id/home_search_bar"
@@ -355,6 +335,64 @@
</LinearLayout>
</LinearLayout>
</FrameLayout>
</LinearLayout>
<FrameLayout
android:id="@+id/home_messages_host"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
android:clipChildren="false"
android:clipToPadding="false">
<androidx.recyclerview.widget.RecyclerView
android:id="@android:id/list"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/home_bg"
android:clipToPadding="false"
android:dividerHeight="0px"
android:listSelector="@android:color/transparent"
android:paddingBottom="104dp" />
<com.android.messaging.ui.home.HomePassThroughView
android:id="@+id/home_list_top_fade"
android:layout_width="match_parent"
android:layout_height="@dimen/home_list_top_fade_height"
android:layout_gravity="top"
android:background="@drawable/home_bg_list_top_fade"
android:clickable="false"
android:focusable="false"
android:importantForAccessibility="no"
android:visibility="gone" />
<com.android.messaging.ui.home.HomePassThroughView
android:id="@+id/home_list_bottom_fade"
android:layout_width="match_parent"
android:layout_height="@dimen/home_list_bottom_fade_height"
android:layout_gravity="bottom"
android:layout_marginBottom="@dimen/home_list_bottom_fade_margin"
android:background="@drawable/home_bg_list_bottom_fade"
android:clickable="false"
android:focusable="false"
android:importantForAccessibility="no"
android:visibility="gone" />
<include
android:id="@+id/no_conversations_view"
layout="@layout/home_empty_state"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:visibility="gone" />
<include
android:id="@+id/home_automation_pane"
layout="@layout/home_automation_pane"
android:layout_width="match_parent"
android:layout_height="match_parent" />
<LinearLayout
android:id="@+id/home_selection_bar"
android:layout_width="match_parent"
@@ -525,3 +563,9 @@
</FrameLayout>
</LinearLayout>
<include
layout="@layout/home_list_profile_popup"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</FrameLayout>
@@ -107,7 +107,7 @@
android:minHeight="72dp"
android:paddingStart="@dimen/looply_space_md"
android:paddingEnd="@dimen/looply_space_lg"
android:paddingTop="@dimen/looply_space_md"
android:paddingTop="@dimen/looply_space_sm"
android:paddingBottom="@dimen/looply_space_md"
android:baselineAligned="false">
@@ -127,73 +127,80 @@
android:visibility="gone"
android:importantForAccessibility="no" />
</com.android.messaging.ui.conversation.MessageBubbleBackground>
<com.android.messaging.ui.LineWrapLayout
android:id="@+id/message_metadata"
android:orientation="horizontal"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center_vertical"
android:importantForAccessibility="noHideDescendants" >
android:importantForAccessibility="noHideDescendants"
android:orientation="horizontal">
<TextView
android:id="@+id/message_sender_name"
style="@style/ConversationMessageStatus"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginRight="4dp"
style="@style/ConversationMessageStatus"
android:layout_marginEnd="4dp"
android:includeFontPadding="false"
android:visibility="gone" />
<TextView
android:id="@+id/message_status"
style="@style/ConversationMessageStatus"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
style="@style/ConversationMessageStatus"
android:includeFontPadding="false"
android:visibility="gone" />
<TextView
android:id="@+id/message_status_action"
style="@style/ConversationMessageStatus"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="?android:attr/selectableItemBackground"
android:clickable="true"
android:focusable="true"
android:includeFontPadding="false"
android:paddingStart="2dp"
android:paddingTop="1dp"
android:paddingEnd="2dp"
android:paddingBottom="1dp"
android:text="@string/home_message_status_check_options"
android:visibility="gone" />
<ImageView
android:id="@+id/message_status_icon"
android:layout_width="14dp"
android:layout_height="14dp"
android:layout_gravity="center_vertical"
android:layout_marginStart="4dp"
android:clickable="true"
android:contentDescription="@null"
android:focusable="true"
android:importantForAccessibility="no"
android:scaleType="centerInside"
android:src="@drawable/ic_home_status_retry"
android:visibility="gone" />
<TextView
android:id="@+id/sim_name"
style="@style/MessageSimIndicator"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_weight="1"
android:visibility="gone" />
<ImageView
android:id="@+id/smsDeliveredBadge"
style="@style/SmsDeliverdBadge"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
style="@style/SmsDeliverdBadge"
android:src="@drawable/ic_sms_delivery_ok"
android:visibility="gone" />
</com.android.messaging.ui.LineWrapLayout>
</com.android.messaging.ui.conversation.MessageBubbleBackground>
<LinearLayout
android:id="@+id/message_send_error"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="6dp"
android:gravity="center_vertical"
android:orientation="horizontal"
android:visibility="gone">
<ImageView
android:layout_width="14dp"
android:layout_height="14dp"
android:contentDescription="@null"
android:importantForAccessibility="no"
android:scaleType="centerInside"
android:src="@drawable/ic_message_send_error" />
<TextView
android:id="@+id/message_send_error_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="4dp"
android:includeFontPadding="false"
android:textColor="@color/home_destructive"
android:textSize="12sp" />
</LinearLayout>
</com.android.messaging.ui.conversation.ConversationMessageBubbleView>
</com.android.messaging.ui.conversation.ConversationMessageView>
@@ -0,0 +1,71 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@drawable/home_bg_compose_group"
android:clickable="true"
android:focusable="true"
android:gravity="center_vertical"
android:minHeight="@dimen/looply_button_height"
android:orientation="horizontal"
android:paddingStart="@dimen/looply_space_lg"
android:paddingEnd="@dimen/looply_space_lg">
<ImageView
android:id="@+id/home_compose_account_icon"
android:layout_width="22dp"
android:layout_height="22dp"
android:contentDescription="@null"
android:importantForAccessibility="no"
android:src="@drawable/ic_home_menu_person"
android:tint="@color/home_text_primary" />
<LinearLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/looply_space_md"
android:layout_weight="1"
android:orientation="vertical">
<TextView
android:id="@+id/home_compose_account_title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:ellipsize="end"
android:includeFontPadding="false"
android:maxLines="1"
android:textColor="@color/home_text_primary"
android:textSize="16sp" />
<TextView
android:id="@+id/home_compose_account_subtitle"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="2dp"
android:ellipsize="end"
android:includeFontPadding="false"
android:maxLines="1"
android:textColor="@color/home_text_secondary"
android:textSize="13sp"
android:visibility="gone" />
</LinearLayout>
<TextView
android:id="@+id/home_compose_account_count"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/looply_space_sm"
android:includeFontPadding="false"
android:textColor="@color/home_text_secondary"
android:textSize="14sp" />
<ImageView
android:id="@+id/home_compose_account_check"
android:layout_width="20dp"
android:layout_height="20dp"
android:layout_marginStart="@dimen/looply_space_sm"
android:contentDescription="@null"
android:src="@drawable/ic_home_check"
android:tint="@color/home_accent"
android:visibility="gone" />
</LinearLayout>
@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/home_compose_account_list"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical" />
@@ -3,7 +3,9 @@
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/home_sheet_scrim"
android:fitsSystemWindows="true">
android:fitsSystemWindows="true"
android:focusable="true"
android:focusableInTouchMode="true">
<View
android:id="@+id/home_compose_scrim"
@@ -94,46 +96,84 @@
</LinearLayout>
<LinearLayout
android:id="@+id/home_compose_create_group"
android:layout_width="match_parent"
android:layout_height="@dimen/home_compose_group_button_height"
android:layout_marginStart="@dimen/looply_space_md"
android:layout_marginTop="@dimen/looply_space_sm"
android:layout_marginEnd="@dimen/looply_space_md"
android:background="@drawable/home_bg_sheet_action"
android:layout_height="wrap_content"
android:gravity="center_vertical"
android:orientation="horizontal"
android:paddingStart="@dimen/looply_screen_padding"
android:paddingTop="@dimen/looply_space_md"
android:paddingEnd="@dimen/looply_screen_padding"
android:paddingBottom="@dimen/looply_space_xs">
<LinearLayout
android:id="@+id/home_compose_account_chip"
android:layout_width="wrap_content"
android:layout_height="36dp"
android:background="@drawable/home_bg_compose_account_chip"
android:clickable="true"
android:focusable="true"
android:gravity="center"
android:orientation="horizontal">
android:gravity="center_vertical"
android:paddingStart="12dp"
android:paddingEnd="10dp">
<ImageView
android:layout_width="22dp"
android:layout_height="22dp"
android:layout_width="18dp"
android:layout_height="18dp"
android:contentDescription="@null"
android:scaleType="fitCenter"
android:src="@drawable/ic_home_group_add"
android:src="@drawable/ic_home_menu_person"
android:tint="@color/home_text_primary" />
<TextView
android:id="@+id/home_compose_account_label"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:includeFontPadding="false"
android:text="@string/home_compose_all_contacts"
android:textColor="@color/home_text_primary"
android:textSize="14sp" />
<ImageView
android:layout_width="16dp"
android:layout_height="16dp"
android:layout_marginStart="4dp"
android:contentDescription="@null"
android:src="@drawable/ic_home_expand" />
</LinearLayout>
<View
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_weight="1" />
<LinearLayout
android:id="@+id/home_compose_create_group"
android:layout_width="wrap_content"
android:layout_height="36dp"
android:background="@drawable/home_bg_compose_group_pill"
android:clickable="true"
android:focusable="true"
android:gravity="center_vertical"
android:paddingStart="14dp"
android:paddingEnd="14dp">
<ImageView
android:layout_width="18dp"
android:layout_height="18dp"
android:contentDescription="@null"
android:src="@drawable/ic_home_group_add"
android:tint="@color/home_accent" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/looply_space_sm"
android:layout_marginStart="6dp"
android:includeFontPadding="false"
android:text="@string/home_compose_create_group"
android:textColor="@color/home_text_primary"
android:textSize="15sp" />
android:textColor="@color/home_accent"
android:textSize="14sp" />
</LinearLayout>
</LinearLayout>
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingStart="@dimen/looply_space_lg"
android:paddingTop="@dimen/looply_space_lg"
android:paddingEnd="@dimen/looply_space_lg"
android:paddingBottom="@dimen/looply_space_xs"
android:text="@string/home_compose_contacts_header"
android:textColor="@color/home_text_secondary"
android:textSize="13sp" />
<FrameLayout
android:layout_width="match_parent"
@@ -24,6 +24,8 @@
android:clipToPadding="false"
android:divider="@null"
android:dividerHeight="0dp"
android:paddingStart="@dimen/looply_screen_padding"
android:paddingEnd="@dimen/looply_screen_padding"
android:paddingBottom="24dp"
android:scrollbarStyle="outsideOverlay" />
@@ -4,7 +4,37 @@
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?android:attr/selectableItemBackground"
android:orientation="vertical">
<View
android:id="@+id/contact_section_gap_top"
android:layout_width="match_parent"
android:layout_height="@dimen/looply_space_sm"
android:visibility="gone" />
<LinearLayout
android:id="@+id/contact_section_card"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<TextView
android:id="@+id/alphabet_header"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:includeFontPadding="false"
android:paddingStart="16dp"
android:paddingTop="14dp"
android:paddingEnd="16dp"
android:paddingBottom="2dp"
android:textColor="@color/home_text_secondary"
android:textSize="14sp"
android:textStyle="bold"
android:visibility="gone" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center_vertical"
android:minHeight="56dp"
android:orientation="horizontal"
@@ -13,12 +43,6 @@
android:paddingEnd="16dp"
android:paddingBottom="8dp">
<TextView
android:id="@+id/alphabet_header"
android:layout_width="0dp"
android:layout_height="0dp"
android:visibility="gone" />
<FrameLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content">
@@ -30,7 +54,6 @@
android:contentDescription="@null"
app:contactIconSize="normal" />
<!-- Covers the avatar while the row is picked for a group -->
<ImageView
android:id="@+id/contact_checkmark"
android:layout_width="@dimen/looply_avatar_size"
@@ -79,4 +102,20 @@
android:layout_height="0dp"
android:visibility="gone" />
</LinearLayout>
</LinearLayout>
<View
android:id="@+id/contact_section_divider"
android:layout_width="match_parent"
android:layout_height="1dp"
android:layout_marginStart="66dp"
android:background="@color/home_divider"
android:visibility="gone" />
</LinearLayout>
<View
android:id="@+id/contact_section_gap_bottom"
android:layout_width="match_parent"
android:layout_height="@dimen/looply_space_xs"
android:visibility="gone" />
</com.android.messaging.ui.contact.ContactListItemView>
@@ -5,6 +5,8 @@
android:layout_height="match_parent"
android:background="@color/home_bg"
android:fitsSystemWindows="true"
android:focusable="true"
android:focusableInTouchMode="true"
android:orientation="vertical">
<include layout="@layout/home_sub_header" />
@@ -0,0 +1,34 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@drawable/home_bg_compose_group"
android:clickable="true"
android:focusable="true"
android:gravity="center_vertical"
android:minHeight="@dimen/looply_button_height"
android:orientation="horizontal"
android:paddingStart="@dimen/looply_space_lg"
android:paddingEnd="@dimen/looply_space_lg">
<TextView
android:id="@+id/home_language_title"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:ellipsize="end"
android:includeFontPadding="false"
android:maxLines="1"
android:textColor="@color/home_text_primary"
android:textSize="16sp" />
<ImageView
android:id="@+id/home_language_check"
android:layout_width="20dp"
android:layout_height="20dp"
android:layout_marginStart="@dimen/looply_space_sm"
android:contentDescription="@null"
android:src="@drawable/ic_home_check"
android:tint="@color/home_accent"
android:visibility="gone" />
</LinearLayout>
@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/home_language_list"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical" />
@@ -6,6 +6,7 @@
android:layout_width="match_parent"
android:layout_height="match_parent"
android:clickable="true"
android:elevation="24dp"
android:focusable="true"
android:visibility="gone">
@@ -21,12 +22,12 @@
android:layout_width="match_parent"
android:layout_height="match_parent"
android:contentDescription="@null"
android:scaleType="centerCrop" />
android:scaleType="fitXY" />
<View
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#4D1B1528"
android:background="#801B1528"
android:importantForAccessibility="no" />
</FrameLayout>
@@ -0,0 +1,91 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/home_bg"
android:fitsSystemWindows="true"
android:orientation="vertical">
<include layout="@layout/home_sub_header" />
<FrameLayout
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1">
<WebView
android:id="@+id/home_location_map"
android:layout_width="match_parent"
android:layout_height="match_parent" />
<ImageView
android:layout_width="40dp"
android:layout_height="40dp"
android:layout_gravity="center"
android:contentDescription="@null"
android:importantForAccessibility="no"
android:translationY="-16dp"
app:srcCompat="@drawable/ic_home_place_line"
app:tint="@color/home_accent" />
<ImageButton
android:id="@+id/home_location_my_location"
android:layout_width="44dp"
android:layout_height="44dp"
android:layout_gravity="bottom|end"
android:layout_marginEnd="@dimen/looply_screen_padding"
android:layout_marginBottom="20dp"
android:background="@drawable/home_bg_map_locate"
android:contentDescription="@string/home_location_my_location"
android:padding="10dp"
android:scaleType="centerInside"
app:srcCompat="@drawable/ic_home_place_line"
app:tint="@color/home_text_primary" />
</FrameLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@color/home_row_bg"
android:gravity="center_vertical"
android:minHeight="72dp"
android:orientation="horizontal"
android:paddingStart="@dimen/looply_screen_padding"
android:paddingTop="@dimen/looply_space_md"
android:paddingEnd="@dimen/looply_screen_padding"
android:paddingBottom="@dimen/looply_space_md">
<ImageView
android:layout_width="22dp"
android:layout_height="22dp"
android:contentDescription="@null"
android:importantForAccessibility="no"
app:srcCompat="@drawable/ic_home_place_line"
app:tint="@color/home_accent" />
<TextView
android:id="@+id/home_location_address"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/looply_space_md"
android:layout_weight="1"
android:ellipsize="end"
android:includeFontPadding="false"
android:maxLines="2"
android:text="@string/home_location_finding"
android:textColor="@color/home_text_primary"
android:textSize="15sp" />
<ImageButton
android:id="@+id/home_location_send"
android:layout_width="48dp"
android:layout_height="48dp"
android:layout_marginStart="@dimen/looply_space_sm"
android:background="@drawable/home_bg_compose_send_circle"
android:contentDescription="@string/home_location_send"
android:padding="6dp"
android:scaleType="fitCenter"
android:src="@drawable/ic_home_send_up" />
</LinearLayout>
</LinearLayout>
@@ -0,0 +1,156 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<LinearLayout
android:id="@+id/home_lock_method_pin"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@drawable/home_bg_lock_method"
android:gravity="center_vertical"
android:minHeight="@dimen/looply_button_height"
android:orientation="horizontal"
android:paddingStart="@dimen/looply_space_lg"
android:paddingEnd="@dimen/looply_space_lg">
<ImageView
android:layout_width="24dp"
android:layout_height="24dp"
android:contentDescription="@null"
android:src="@drawable/ic_home_pin"
android:tint="@color/home_text_primary" />
<LinearLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/looply_space_md"
android:layout_weight="1"
android:orientation="vertical">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:includeFontPadding="false"
android:text="@string/home_lock_method_pin"
android:textColor="@color/home_text_primary"
android:textSize="16sp"
android:textStyle="bold" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="2dp"
android:includeFontPadding="false"
android:text="@string/home_lock_method_pin_subtitle"
android:textColor="@color/home_text_secondary"
android:textSize="13sp" />
</LinearLayout>
</LinearLayout>
<LinearLayout
android:id="@+id/home_lock_method_pattern"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/looply_space_sm"
android:background="@drawable/home_bg_lock_method"
android:gravity="center_vertical"
android:minHeight="@dimen/looply_button_height"
android:orientation="horizontal"
android:paddingStart="@dimen/looply_space_lg"
android:paddingEnd="@dimen/looply_space_lg">
<ImageView
android:layout_width="24dp"
android:layout_height="24dp"
android:contentDescription="@null"
android:src="@drawable/ic_home_pattern"
android:tint="@color/home_text_primary" />
<LinearLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/looply_space_md"
android:layout_weight="1"
android:orientation="vertical">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:includeFontPadding="false"
android:text="@string/home_lock_method_pattern"
android:textColor="@color/home_text_primary"
android:textSize="16sp"
android:textStyle="bold" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="2dp"
android:includeFontPadding="false"
android:text="@string/home_lock_method_pattern_subtitle"
android:textColor="@color/home_text_secondary"
android:textSize="13sp" />
</LinearLayout>
</LinearLayout>
<LinearLayout
android:id="@+id/home_lock_method_password"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/looply_space_sm"
android:background="@drawable/home_bg_lock_method"
android:gravity="center_vertical"
android:minHeight="@dimen/looply_button_height"
android:orientation="horizontal"
android:paddingStart="@dimen/looply_space_lg"
android:paddingEnd="@dimen/looply_space_lg">
<ImageView
android:layout_width="24dp"
android:layout_height="24dp"
android:contentDescription="@null"
android:src="@drawable/ic_home_password"
android:tint="@color/home_text_primary" />
<LinearLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/looply_space_md"
android:layout_weight="1"
android:orientation="vertical">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:includeFontPadding="false"
android:text="@string/home_lock_method_password"
android:textColor="@color/home_text_primary"
android:textSize="16sp"
android:textStyle="bold" />
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="2dp"
android:includeFontPadding="false"
android:text="@string/home_lock_method_password_subtitle"
android:textColor="@color/home_text_secondary"
android:textSize="13sp" />
</LinearLayout>
</LinearLayout>
<TextView
android:id="@+id/home_lock_method_continue"
android:layout_width="match_parent"
android:layout_height="52dp"
android:layout_marginTop="@dimen/looply_space_lg"
android:background="@drawable/home_bg_lock_continue"
android:gravity="center"
android:includeFontPadding="false"
android:text="@string/home_lock_continue"
android:textColor="@android:color/white"
android:textSize="16sp"
android:textStyle="bold" />
</LinearLayout>
@@ -0,0 +1,126 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<TextView
android:id="@+id/home_lock_password_label"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:includeFontPadding="false"
android:text="@string/home_lock_password_hint"
android:textColor="@color/home_text_secondary"
android:textSize="13sp"
android:textStyle="bold" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:addStatesFromChildren="true"
android:background="@drawable/home_bg_modal_input"
android:gravity="center_vertical"
android:minHeight="@dimen/looply_input_height"
android:orientation="horizontal"
android:paddingStart="@dimen/looply_space_lg"
android:paddingEnd="@dimen/looply_space_sm">
<EditText
android:id="@+id/home_lock_password"
style="@style/Home.EditText"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:background="@android:color/transparent"
android:imeOptions="actionDone"
android:importantForAutofill="no"
android:includeFontPadding="false"
android:inputType="textPassword"
android:singleLine="true"
android:textColor="@color/home_text_primary"
android:textColorHint="@color/home_text_secondary"
android:textSize="16sp" />
<ImageView
android:id="@+id/home_lock_password_toggle"
android:layout_width="40dp"
android:layout_height="40dp"
android:background="?attr/selectableItemBackgroundBorderless"
android:contentDescription="@string/home_lock_password_toggle"
android:padding="8dp"
android:scaleType="fitCenter"
app:srcCompat="@drawable/ic_home_visibility"
app:tint="@color/home_text_secondary" />
</LinearLayout>
<LinearLayout
android:id="@+id/home_lock_password_rules"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="20dp"
android:orientation="vertical"
android:visibility="gone">
<TextView
android:id="@+id/home_lock_rule_length"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:drawablePadding="10dp"
android:gravity="center_vertical"
android:includeFontPadding="false"
android:paddingTop="4dp"
android:paddingBottom="4dp"
android:text="@string/home_lock_rule_length"
android:textColor="@color/home_text_secondary"
android:textSize="16sp"
app:drawableStartCompat="@drawable/ic_home_check"
app:drawableTint="@color/home_text_tertiary" />
<TextView
android:id="@+id/home_lock_rule_number"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:drawablePadding="10dp"
android:gravity="center_vertical"
android:includeFontPadding="false"
android:paddingTop="4dp"
android:paddingBottom="4dp"
android:text="@string/home_lock_rule_number"
android:textColor="@color/home_text_secondary"
android:textSize="16sp"
app:drawableStartCompat="@drawable/ic_home_check"
app:drawableTint="@color/home_text_tertiary" />
<TextView
android:id="@+id/home_lock_rule_symbol"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:drawablePadding="10dp"
android:gravity="center_vertical"
android:includeFontPadding="false"
android:paddingTop="4dp"
android:paddingBottom="4dp"
android:text="@string/home_lock_rule_symbol"
android:textColor="@color/home_text_secondary"
android:textSize="16sp"
app:drawableStartCompat="@drawable/ic_home_check"
app:drawableTint="@color/home_text_tertiary" />
<TextView
android:id="@+id/home_lock_rule_upper"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:drawablePadding="10dp"
android:gravity="center_vertical"
android:includeFontPadding="false"
android:paddingTop="4dp"
android:paddingBottom="4dp"
android:text="@string/home_lock_rule_upper"
android:textColor="@color/home_text_secondary"
android:textSize="16sp"
app:drawableStartCompat="@drawable/ic_home_check"
app:drawableTint="@color/home_text_tertiary" />
</LinearLayout>
</LinearLayout>
@@ -0,0 +1,47 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center_horizontal"
android:orientation="vertical">
<FrameLayout
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1">
<com.android.messaging.ui.home.HomePatternLockView
android:id="@+id/home_lock_pattern"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_gravity="center" />
</FrameLayout>
<TextView
android:id="@+id/home_lock_pattern_recorded"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/looply_space_md"
android:background="@drawable/home_bg_schedule_chip"
android:includeFontPadding="false"
android:paddingStart="14dp"
android:paddingTop="8dp"
android:paddingEnd="14dp"
android:paddingBottom="8dp"
android:text="@string/home_lock_pattern_recorded"
android:textColor="@color/home_text_primary"
android:textSize="13sp"
android:visibility="gone" />
<TextView
android:id="@+id/home_lock_pattern_clear"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/looply_space_sm"
android:background="?attr/selectableItemBackgroundBorderless"
android:includeFontPadding="false"
android:padding="@dimen/looply_space_sm"
android:text="@string/home_lock_clear_pattern"
android:textColor="@color/home_accent"
android:textSize="16sp" />
</LinearLayout>
@@ -0,0 +1,21 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center_horizontal"
android:orientation="vertical">
<include
android:id="@+id/home_lock_pin_dots"
layout="@layout/home_lock_pin_dots"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
<include
android:id="@+id/home_lock_pin_keypad"
layout="@layout/home_lock_pin_keypad"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_marginTop="@dimen/looply_space_xl"
android:layout_weight="1" />
</LinearLayout>
@@ -0,0 +1,43 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/home_lock_pin_dots"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center"
android:orientation="horizontal">
<View
android:layout_width="20dp"
android:layout_height="20dp"
android:background="@drawable/home_bg_pin_dot" />
<View
android:layout_width="20dp"
android:layout_height="20dp"
android:layout_marginStart="16dp"
android:background="@drawable/home_bg_pin_dot" />
<View
android:layout_width="20dp"
android:layout_height="20dp"
android:layout_marginStart="16dp"
android:background="@drawable/home_bg_pin_dot" />
<View
android:layout_width="20dp"
android:layout_height="20dp"
android:layout_marginStart="16dp"
android:background="@drawable/home_bg_pin_dot" />
<View
android:layout_width="20dp"
android:layout_height="20dp"
android:layout_marginStart="16dp"
android:background="@drawable/home_bg_pin_dot" />
<View
android:layout_width="20dp"
android:layout_height="20dp"
android:layout_marginStart="16dp"
android:background="@drawable/home_bg_pin_dot" />
</LinearLayout>
@@ -0,0 +1,68 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/home_lock_pin_keypad"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:orientation="vertical"
android:paddingTop="@dimen/looply_space_md"
android:paddingBottom="@dimen/looply_space_md">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:orientation="horizontal">
<TextView style="@style/Home.LockKey" android:text="1" />
<TextView style="@style/Home.LockKey" android:text="2" />
<TextView style="@style/Home.LockKey" android:text="3" />
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:orientation="horizontal">
<TextView style="@style/Home.LockKey" android:text="4" />
<TextView style="@style/Home.LockKey" android:text="5" />
<TextView style="@style/Home.LockKey" android:text="6" />
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:orientation="horizontal">
<TextView style="@style/Home.LockKey" android:text="7" />
<TextView style="@style/Home.LockKey" android:text="8" />
<TextView style="@style/Home.LockKey" android:text="9" />
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:orientation="horizontal">
<View
android:layout_width="96dp"
android:layout_height="96dp" />
<TextView style="@style/Home.LockKey" android:text="0" />
<ImageView
android:id="@+id/home_lock_pin_backspace"
android:layout_width="96dp"
android:layout_height="96dp"
android:background="?attr/selectableItemBackgroundBorderless"
android:contentDescription="@string/home_lock_backspace"
android:padding="28dp"
android:scaleType="fitCenter"
app:srcCompat="@drawable/ic_home_backspace"
app:tint="@color/home_text_primary" />
</LinearLayout>
</LinearLayout>
@@ -0,0 +1,48 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/home_bg"
android:fitsSystemWindows="true"
android:orientation="vertical">
<include layout="@layout/home_sub_header" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
android:gravity="center_horizontal"
android:orientation="vertical"
android:paddingStart="24dp"
android:paddingEnd="24dp">
<TextView
android:id="@+id/home_lock_subtitle"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/looply_space_md"
android:gravity="start"
android:includeFontPadding="false"
android:lineSpacingExtra="3dp"
android:textColor="@color/home_text_secondary"
android:textSize="16sp" />
<TextView
android:id="@+id/home_lock_error"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:includeFontPadding="false"
android:textColor="@color/home_destructive"
android:textSize="13sp"
android:visibility="gone" />
<FrameLayout
android:id="@+id/home_lock_body"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_marginTop="28dp"
android:layout_weight="1" />
</LinearLayout>
</LinearLayout>
@@ -0,0 +1,77 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/home_bg"
android:fitsSystemWindows="true"
android:orientation="vertical">
<TextView
android:id="@+id/home_lock_app_title"
android:layout_width="match_parent"
android:layout_height="@dimen/home_header_height"
android:gravity="center_vertical"
android:includeFontPadding="false"
android:paddingStart="@dimen/looply_screen_padding"
android:paddingEnd="@dimen/looply_screen_padding"
android:text="@string/home_title"
android:textColor="@color/home_text_primary"
android:textSize="20sp"
android:textStyle="bold" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
android:gravity="center_horizontal"
android:orientation="vertical"
android:paddingStart="24dp"
android:paddingEnd="24dp">
<ImageView
android:layout_width="48dp"
android:layout_height="48dp"
android:layout_marginTop="32dp"
android:contentDescription="@null"
app:srcCompat="@drawable/ic_home_lock"
app:tint="@color/home_accent" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:includeFontPadding="false"
android:text="@string/home_lock_enter_title"
android:textColor="@color/home_text_primary"
android:textSize="20sp"
android:textStyle="bold" />
<TextView
android:id="@+id/home_lock_subtitle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="6dp"
android:gravity="center"
android:includeFontPadding="false"
android:textColor="@color/home_text_secondary"
android:textSize="13sp" />
<TextView
android:id="@+id/home_lock_error"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:includeFontPadding="false"
android:textColor="@color/home_destructive"
android:textSize="13sp"
android:visibility="gone" />
<FrameLayout
android:id="@+id/home_lock_body"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_marginTop="24dp"
android:layout_weight="1" />
</LinearLayout>
</LinearLayout>
@@ -0,0 +1,68 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<LinearLayout
android:id="@+id/home_message_status_resend"
android:layout_width="match_parent"
android:layout_height="@dimen/looply_button_height"
android:background="@drawable/home_bg_sheet_action"
android:clickable="true"
android:focusable="true"
android:gravity="center_vertical"
android:orientation="horizontal"
android:paddingStart="@dimen/looply_space_lg"
android:paddingEnd="@dimen/looply_space_lg">
<ImageView
android:layout_width="22dp"
android:layout_height="22dp"
android:contentDescription="@null"
android:importantForAccessibility="no"
app:srcCompat="@drawable/ic_home_resend"
app:tint="@color/home_text_primary" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/looply_space_md"
android:includeFontPadding="false"
android:text="@string/home_message_action_send"
android:textColor="@color/home_text_primary"
android:textSize="16sp" />
</LinearLayout>
<LinearLayout
android:id="@+id/home_message_status_delete"
android:layout_width="match_parent"
android:layout_height="@dimen/looply_button_height"
android:layout_marginTop="@dimen/looply_space_sm"
android:background="@drawable/home_bg_sheet_action"
android:clickable="true"
android:focusable="true"
android:gravity="center_vertical"
android:orientation="horizontal"
android:paddingStart="@dimen/looply_space_lg"
android:paddingEnd="@dimen/looply_space_lg">
<ImageView
android:layout_width="22dp"
android:layout_height="22dp"
android:contentDescription="@null"
android:importantForAccessibility="no"
app:srcCompat="@drawable/ic_home_menu_trash"
app:tint="@color/home_destructive" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/looply_space_md"
android:includeFontPadding="false"
android:text="@string/home_message_action_delete"
android:textColor="@color/home_destructive"
android:textSize="16sp" />
</LinearLayout>
</LinearLayout>
@@ -0,0 +1,52 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center_vertical"
android:orientation="horizontal">
<LinearLayout
android:layout_width="0dp"
android:layout_height="@dimen/home_conversation_compose_pill_height"
android:layout_weight="1"
android:addStatesFromChildren="true"
android:background="@drawable/home_bg_compose_pill"
android:gravity="center_vertical"
android:orientation="horizontal"
android:paddingStart="@dimen/looply_space_xs"
android:paddingEnd="@dimen/looply_space_xs">
<EditText
android:id="@+id/home_notification_reply_input"
style="@style/Home.EditText"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1"
android:background="@android:color/transparent"
android:gravity="center_vertical"
android:hint="@string/home_notification_reply_hint"
android:imeOptions="actionSend|flagNoEnterAction|flagNoExtractUi"
android:importantForAutofill="no"
android:includeFontPadding="false"
android:inputType="textCapSentences|textMultiLine"
android:maxLines="3"
android:paddingStart="@dimen/looply_space_md"
android:paddingEnd="@dimen/looply_space_md"
android:textColor="@color/home_text_primary"
android:textColorHint="@color/home_text_secondary"
android:textSize="16sp" />
</LinearLayout>
<ImageButton
android:id="@+id/home_notification_reply_send"
android:layout_width="@dimen/home_conversation_compose_send_size"
android:layout_height="@dimen/home_conversation_compose_send_size"
android:layout_marginStart="@dimen/looply_space_sm"
android:background="@drawable/home_bg_compose_send_circle"
android:contentDescription="@string/sendButtonContentDescription"
android:padding="4dp"
android:scaleType="fitCenter"
android:src="@drawable/ic_home_send_up"
app:tint="@color/home_accent_soft" />
</LinearLayout>
@@ -221,6 +221,33 @@
android:layout_marginBottom="4dp"
android:background="@color/home_divider" />
<LinearLayout
android:id="@+id/popover_share_app"
style="@style/Home.PopoverItem"
android:layout_width="match_parent"
android:layout_height="44dp">
<View
android:layout_width="18dp"
android:layout_height="18dp"
android:layout_marginEnd="8dp" />
<ImageView
android:layout_width="22dp"
android:layout_height="22dp"
android:contentDescription="@null"
android:src="@drawable/ic_share_light"
android:tint="@color/home_text_primary" />
<TextView
style="@style/Home.PopoverLabel"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="12dp"
android:layout_weight="1"
android:text="@string/home_popover_share_app" />
</LinearLayout>
<LinearLayout
android:id="@+id/popover_settings"
style="@style/Home.PopoverItem"
@@ -0,0 +1,47 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:paddingStart="@dimen/looply_space_sm"
android:paddingEnd="@dimen/looply_space_sm">
<DatePicker
android:id="@+id/home_schedule_date"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:calendarViewShown="true"
android:datePickerMode="calendar"
android:spinnersShown="false"
android:theme="@style/Home.Picker" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="end"
android:orientation="horizontal"
android:paddingTop="@dimen/looply_space_sm"
android:paddingBottom="@dimen/looply_space_sm">
<TextView
android:id="@+id/home_schedule_cancel"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="?attr/selectableItemBackgroundBorderless"
android:padding="@dimen/looply_space_md"
android:text="@string/home_schedule_cancel"
android:textColor="@color/home_accent"
android:textSize="16sp" />
<TextView
android:id="@+id/home_schedule_next"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="?attr/selectableItemBackgroundBorderless"
android:padding="@dimen/looply_space_md"
android:text="@string/home_schedule_next"
android:textColor="@color/home_accent"
android:textSize="16sp"
android:textStyle="bold" />
</LinearLayout>
</LinearLayout>
@@ -0,0 +1,45 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:paddingStart="@dimen/looply_space_sm"
android:paddingEnd="@dimen/looply_space_sm">
<TimePicker
android:id="@+id/home_schedule_time"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:theme="@style/Home.Picker"
android:timePickerMode="clock" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="end"
android:orientation="horizontal"
android:paddingTop="@dimen/looply_space_sm"
android:paddingBottom="@dimen/looply_space_sm">
<TextView
android:id="@+id/home_schedule_cancel"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="?attr/selectableItemBackgroundBorderless"
android:padding="@dimen/looply_space_md"
android:text="@string/home_schedule_back"
android:textColor="@color/home_accent"
android:textSize="16sp" />
<TextView
android:id="@+id/home_schedule_next"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="?attr/selectableItemBackgroundBorderless"
android:padding="@dimen/looply_space_md"
android:text="@string/home_schedule_done"
android:textColor="@color/home_accent"
android:textSize="16sp"
android:textStyle="bold" />
</LinearLayout>
</LinearLayout>
@@ -5,6 +5,8 @@
android:layout_height="match_parent"
android:background="@color/home_bg"
android:fitsSystemWindows="true"
android:focusable="true"
android:focusableInTouchMode="true"
android:orientation="vertical">
<include layout="@layout/home_sub_header" />
@@ -0,0 +1,70 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/home_bg"
android:fitsSystemWindows="true"
android:orientation="vertical">
<include layout="@layout/home_sub_header" />
<ScrollView
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
android:clipToPadding="false"
android:paddingStart="@dimen/looply_space_lg"
android:paddingTop="@dimen/looply_space_md"
android:paddingEnd="@dimen/looply_space_lg"
android:paddingBottom="@dimen/looply_screen_padding"
android:scrollbars="none">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@drawable/home_bg_profile_card"
android:orientation="vertical"
android:padding="@dimen/looply_space_sm">
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:includeFontPadding="false"
android:paddingStart="@dimen/looply_space_md"
android:paddingTop="@dimen/looply_space_xs"
android:paddingEnd="@dimen/looply_space_md"
android:paddingBottom="@dimen/looply_space_sm"
android:text="@string/home_settings_app_lock"
android:textColor="@color/home_accent"
android:textSize="14sp"
android:textStyle="bold" />
<include
android:id="@+id/home_security_app_lock"
layout="@layout/home_settings_row"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
<include
android:id="@+id/home_security_set_pin"
layout="@layout/home_settings_row"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/looply_space_xs" />
<include
android:id="@+id/home_security_set_pattern"
layout="@layout/home_settings_row"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/looply_space_xs" />
<include
android:id="@+id/home_security_set_password"
layout="@layout/home_settings_row"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="@dimen/looply_space_xs" />
</LinearLayout>
</ScrollView>
</LinearLayout>

Some files were not shown because too many files have changed in this diff Show More