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", "1.0.001",
"--version-code", "--version-code",
"10001040", "10001040",
"--rename-manifest-package",
"com.filabcorp.msgq",
], ],
required: [ required: [
"libframesequence", "libframesequence",
"libgiftranscode", "libgiftranscode",
"com.bnkode.notie.allowlist", "com.filabcorp.msgq.allowlist",
], ],
optimize: { optimize: {
obfuscate: true, obfuscate: true,
@@ -105,9 +107,9 @@ android_app {
} }
prebuilt_etc { prebuilt_etc {
name: "com.bnkode.notie.allowlist", name: "com.filabcorp.msgq.allowlist",
product_specific: true, product_specific: true,
sub_dir: "sysconfig", sub_dir: "sysconfig",
src: "com.bnkode.notie.allowlist.xml", src: "com.filabcorp.msgq.allowlist.xml",
filename_from_src: true, filename_from_src: true,
} }
+4 -3
View File
@@ -13,13 +13,13 @@ android {
compileSdk 35 compileSdk 35
defaultConfig { defaultConfig {
applicationId 'com.bnkode.notie' applicationId 'com.filabcorp.msgq'
minSdk 33 minSdk 33
targetSdk 34 targetSdk 34
versionCode 10001040 versionCode 10001040
versionName '1.0.001' versionName '1.0.001'
vectorDrawables.useSupportLibrary true vectorDrawables.useSupportLibrary true
resourceConfigurations += ['en'] resourceConfigurations += ['en', 'ko']
} }
compileOptions { compileOptions {
@@ -78,6 +78,7 @@ dependencies {
implementation 'androidx.core:core:1.15.0' implementation 'androidx.core:core:1.15.0'
implementation 'androidx.exifinterface:exifinterface:1.3.7' implementation 'androidx.exifinterface:exifinterface:1.3.7'
implementation 'androidx.fragment:fragment:1.8.5' implementation 'androidx.fragment:fragment:1.8.5'
implementation 'androidx.lifecycle:lifecycle-process:2.8.7'
implementation 'androidx.media:media:1.7.0' implementation 'androidx.media:media:1.7.0'
implementation 'androidx.palette:palette:1.0.0' implementation 'androidx.palette:palette:1.0.0'
implementation 'androidx.preference:preference:1.2.1' implementation 'androidx.preference:preference:1.2.1'
@@ -104,5 +105,5 @@ tasks.register('launchMessaging', Exec) {
description = "Launch Messaging on ${messagingAvdSerial}" description = "Launch Messaging on ${messagingAvdSerial}"
dependsOn 'installMessaging' dependsOn 'installMessaging'
commandLine adbExecutable.absolutePath, '-s', messagingAvdSerial, 'shell', 'am', 'start', 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'
} }
+58 -12
View File
@@ -46,6 +46,10 @@
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" /> <uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES" /> <uses-permission android:name="android.permission.READ_MEDIA_IMAGES" />
<uses-permission android:name="android.permission.READ_MEDIA_VIDEO" /> <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 --> <!-- Optional features -->
<uses-feature <uses-feature
@@ -63,9 +67,15 @@
<uses-feature <uses-feature
android:name="android.hardware.screen.portrait" android:name="android.hardware.screen.portrait"
android:required="false" /> android:required="false" />
<uses-feature <uses-feature
android:name="android.hardware.telephony" android:name="android.hardware.telephony"
android:required="false" /> 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> <queries>
<intent> <intent>
@@ -82,6 +92,7 @@
android:appCategory="social" android:appCategory="social"
android:icon="@mipmap/ic_launcher" android:icon="@mipmap/ic_launcher"
android:label="@string/app_name" android:label="@string/app_name"
android:localeConfig="@xml/locales_config"
android:requestLegacyExternalStorage="true" android:requestLegacyExternalStorage="true"
android:supportsRtl="true" android:supportsRtl="true"
android:theme="@style/BugleTheme"> android:theme="@style/BugleTheme">
@@ -206,6 +217,32 @@
android:theme="@style/Home.BottomSheet" android:theme="@style/Home.BottomSheet"
android:windowSoftInputMode="adjustResize|stateHidden" /> 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 --> <!-- Full-screen photo viewer -->
<activity <activity
android:name=".ui.photoviewer.BuglePhotoViewActivity" android:name=".ui.photoviewer.BuglePhotoViewActivity"
@@ -320,7 +357,8 @@
android:configChanges="orientation|screenSize|keyboardHidden" android:configChanges="orientation|screenSize|keyboardHidden"
android:exported="false" android:exported="false"
android:screenOrientation="user" android:screenOrientation="user"
android:theme="@style/Invisible"> android:theme="@style/Home.TransparentSheetHost"
android:windowSoftInputMode="adjustResize">
</activity> </activity>
<!-- VCard details --> <!-- VCard details -->
@@ -343,20 +381,20 @@
<provider <provider
android:name=".datamodel.MessagingContentProvider" android:name=".datamodel.MessagingContentProvider"
android:authorities="com.bnkode.notie.datamodel.MessagingContentProvider" android:authorities="com.filabcorp.msgq.datamodel.MessagingContentProvider"
android:exported="false" android:exported="false"
android:label="@string/app_name"> android:label="@string/app_name">
</provider> </provider>
<provider <provider
android:name=".datamodel.MmsFileProvider" android:name=".datamodel.MmsFileProvider"
android:authorities="com.bnkode.notie.datamodel.MmsFileProvider" android:authorities="com.filabcorp.msgq.datamodel.MmsFileProvider"
android:exported="false" android:exported="false"
android:grantUriPermissions="true" /> android:grantUriPermissions="true" />
<provider <provider
android:name=".datamodel.MediaScratchFileProvider" android:name=".datamodel.MediaScratchFileProvider"
android:authorities="com.bnkode.notie.datamodel.MediaScratchFileProvider" android:authorities="com.filabcorp.msgq.datamodel.MediaScratchFileProvider"
android:exported="false" android:exported="false"
android:grantUriPermissions="true" /> android:grantUriPermissions="true" />
@@ -412,18 +450,18 @@
android:name=".receiver.SendStatusReceiver" android:name=".receiver.SendStatusReceiver"
android:exported="false"> android:exported="false">
<intent-filter> <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" /> <data android:scheme="content" />
</intent-filter> </intent-filter>
<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>
<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" /> <data android:scheme="content" />
</intent-filter> </intent-filter>
<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" /> <data android:scheme="content" />
</intent-filter> </intent-filter>
</receiver> </receiver>
@@ -462,6 +500,14 @@
</intent-filter> </intent-filter>
</receiver> </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 --> <!-- Broadcast receiver that will be notified to reset notifications -->
<receiver <receiver
android:name=".receiver.NotificationReceiver" android:name=".receiver.NotificationReceiver"
@@ -473,7 +519,7 @@
android:name=".datamodel.action.ActionServiceImpl$PendingActionReceiver" android:name=".datamodel.action.ActionServiceImpl$PendingActionReceiver"
android:exported="false"> android:exported="false">
<intent-filter> <intent-filter>
<action android:name="com.bnkode.notie.datamodel.PENDING_ACTION" /> <action android:name="com.filabcorp.msgq.datamodel.PENDING_ACTION" />
</intent-filter> </intent-filter>
</receiver> </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.BugleUserAgentInfoLoader;
import com.android.messaging.sms.MmsConfig; import com.android.messaging.sms.MmsConfig;
import com.android.messaging.ui.ConversationDrawables; import com.android.messaging.ui.ConversationDrawables;
import com.android.messaging.ui.home.HomeAppLock;
import com.android.messaging.ui.home.HomeToast; import com.android.messaging.ui.home.HomeToast;
import com.android.messaging.util.BuglePrefsKeys; import com.android.messaging.util.BuglePrefsKeys;
import com.android.messaging.util.LogUtil; import com.android.messaging.util.LogUtil;
@@ -58,6 +59,7 @@ public class BugleApplication extends Application implements UncaughtExceptionHa
super.onCreate(); super.onCreate();
FactoryImpl.register(getApplicationContext(), this); FactoryImpl.register(getApplicationContext(), this);
HomeAppLock.install(this);
HomeToast.install(this); HomeToast.install(this);
sSystemUncaughtExceptionHandler = Thread.getDefaultUncaughtExceptionHandler(); sSystemUncaughtExceptionHandler = Thread.getDefaultUncaughtExceptionHandler();
@@ -722,7 +722,9 @@ public class BugleNotifications {
setWearableGroupOptions(notifBuilder, notificationState); setWearableGroupOptions(notifBuilder, notificationState);
if (notificationState instanceof MultiMessageNotificationState) { 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 // 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 // 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 // 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 int requestCode = multiMessageNotificationState.getReplyIntentRequestCode();
final PendingIntent replyPendingIntent = UIIntents.get() final PendingIntent replyPendingIntent = UIIntents.get()
.getPendingIntentForSendingMessageToConversation(context, .getPendingIntentForSendingMessageToConversation(context,
conversationId, selfId, requiresMms, requestCode); conversationId, selfId, requiresMms,
multiMessageNotificationState.mTitle, requestCode);
final int replyLabelRes = requiresMms ? R.string.notification_reply_via_mms : // Phone: open the Home bottom-sheet composer. Wear keeps RemoteInput.
R.string.notification_reply_via_sms; final NotificationCompat.Action replyAction =
final NotificationCompat.Action.Builder actionBuilder =
new NotificationCompat.Action.Builder(R.drawable.ic_wear_reply, new NotificationCompat.Action.Builder(R.drawable.ic_wear_reply,
context.getString(replyLabelRes), replyPendingIntent); context.getString(R.string.home_notification_reply), replyPendingIntent)
final RemoteInput.Builder remoteInputBuilder = new RemoteInput.Builder(Intent.EXTRA_TEXT); .setSemanticAction(NotificationCompat.Action.SEMANTIC_ACTION_REPLY)
remoteInputBuilder.setLabel(context.getString(R.string.notification_reply_prompt)); .setShowsUserInterface(true)
actionBuilder.addRemoteInput(remoteInputBuilder.build()); .build();
notifBuilder.addAction(actionBuilder.build()); notifBuilder.addAction(replyAction);
// Support the action on a wearable device // 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( final String[] choices = context.getResources().getStringArray(
R.array.notification_reply_choices); R.array.notification_reply_choices);
remoteInputBuilder.setChoices(choices); final RemoteInput wearRemoteInput = new RemoteInput.Builder(Intent.EXTRA_TEXT)
wearActionBuilder.addRemoteInput(remoteInputBuilder.build()); .setLabel(context.getString(R.string.home_notification_reply_hint))
wearableExtender.addAction(wearActionBuilder.build()); .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, private static void addDownloadMmsAction(final NotificationCompat.Builder notifBuilder,
@@ -120,6 +120,8 @@ public class FrequentContactsCursorBuilder {
mAllContactsCursor.getInt(ContactUtil.INDEX_PHONE_EMAIL_TYPE); mAllContactsCursor.getInt(ContactUtil.INDEX_PHONE_EMAIL_TYPE);
row[ContactUtil.INDEX_PHONE_EMAIL_LABEL] = row[ContactUtil.INDEX_PHONE_EMAIL_LABEL] =
mAllContactsCursor.getString(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; boolean numberAlreadyAdded = false;
for (Object[] oldRow : rows) { for (Object[] oldRow : rows) {
@@ -46,7 +46,7 @@ public class MediaScratchFileProvider extends FileProvider {
private static final SimpleArrayMap<Uri, String> sUriToDisplayNameMap = new SimpleArrayMap<>(); private static final SimpleArrayMap<Uri, String> sUriToDisplayNameMap = new SimpleArrayMap<>();
public static final String AUTHORITY = public static final String AUTHORITY =
"com.bnkode.notie.datamodel.MediaScratchFileProvider"; "com.filabcorp.msgq.datamodel.MediaScratchFileProvider";
private static final String MEDIA_SCRATCH_SPACE_DIR = "mediascratchspace"; private static final String MEDIA_SCRATCH_SPACE_DIR = "mediascratchspace";
public static boolean isMediaScratchSpaceUri(final Uri uri) { 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.Builder;
import androidx.core.app.NotificationCompat.WearableExtender; import androidx.core.app.NotificationCompat.WearableExtender;
import androidx.core.app.NotificationManagerCompat; import androidx.core.app.NotificationManagerCompat;
import androidx.core.app.Person;
import com.android.messaging.Factory; import com.android.messaging.Factory;
import com.android.messaging.R; import com.android.messaging.R;
@@ -494,85 +495,62 @@ public abstract class MessageNotificationState extends NotificationState {
@Override @Override
protected NotificationCompat.Style build(final Builder builder) { protected NotificationCompat.Style build(final Builder builder) {
builder.setContentTitle(mTitle) 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 ConversationLineInfo convInfo = mConvList.mConvInfos.get(0);
final List<NotificationLineInfo> lineInfos = convInfo.mLineInfos; final List<NotificationLineInfo> lineInfos = convInfo.mLineInfos;
final int messageCount = lineInfos.size(); final Person user = new Person.Builder()
// At this point, all the messages come from the same conversation. We need to load .setName(context.getString(R.string.home_notification_you))
// the sender's avatar and then finish building the notification on a callback. .build();
final NotificationCompat.MessagingStyle style =
builder.setContentText(mContent); // for collapsed state new NotificationCompat.MessagingStyle(user);
if (convInfo.mIsGroup) {
if (messageCount == 1) { style.setGroupConversation(true);
final boolean shouldShowImage = ContentType.isImageType(mAttachmentType) if (!TextUtils.isEmpty(convInfo.mGroupConversationName)) {
|| (ContentType.isVideoType(mAttachmentType)); style.setConversationTitle(convInfo.mGroupConversationName);
if (mAttachmentUri != null && shouldShowImage) {
// Show "Picture" as the content
final MessageLineInfo messageLineInfo = (MessageLineInfo) lineInfos.get(0);
String authorFirstName = messageLineInfo.mAuthorFirstName;
// For the collapsed state, just show "picture" unless this is a
// group conversation. If it's a group, show the sender name and
// "picture".
final CharSequence tickerTag =
BugleNotifications.formatAttachmentTag(authorFirstName,
mAttachmentType);
// For 1:1 notifications don't show first name in the notification, but
// do show it in the ticker text
CharSequence pictureTag = tickerTag;
if (!convInfo.mIsGroup) {
authorFirstName = null;
pictureTag = BugleNotifications.formatAttachmentTag(authorFirstName,
mAttachmentType);
}
builder.setContentText(pictureTag);
builder.setTicker(tickerTag);
notifStyle = new NotificationCompat.BigPictureStyle(builder)
.setSummaryText(BugleNotifications.formatInboxMessage(
authorFirstName,
null, null,
null)); // expanded state, just show sender
} else {
notifStyle = new NotificationCompat.BigTextStyle(builder)
.bigText(mContent);
} }
} else {
// We've got multiple messages for the same sender.
// Starting with the oldest new message, display the full text of each message.
// Begin a line for each subsequent message.
final SpannableStringBuilder buf = new SpannableStringBuilder();
for (int i = lineInfos.size() - 1; i >= 0; --i) {
final NotificationLineInfo info = lineInfos.get(i);
final MessageLineInfo messageLineInfo = (MessageLineInfo) info;
mAttachmentUri = messageLineInfo.mAttachmentUri;
mAttachmentType = messageLineInfo.mAttachmentType;
CharSequence text = messageLineInfo.mText;
if (!TextUtils.isEmpty(text) || mAttachmentUri != null) {
if (convInfo.mIsGroup) {
// append the bold name to the front of the message
text = BugleNotifications.buildSpaceSeparatedMessage(
messageLineInfo.mAuthorFullName, text, mAttachmentUri,
mAttachmentType);
} else {
text = BugleNotifications.buildSpaceSeparatedMessage(
null, text, mAttachmentUri, mAttachmentType);
}
buf.append(text);
if (i > 0) {
buf.append('\n');
}
}
}
// Show a single notification -- big style with the text of all the messages
notifStyle = new NotificationCompat.BigTextStyle(builder).bigText(buf);
} }
// MessagingStyle wants chronological order (oldest first). Latest is at index 0.
for (int i = lineInfos.size() - 1; i >= 0; --i) {
final NotificationLineInfo info = lineInfos.get(i);
if (!(info instanceof MessageLineInfo)) {
continue;
}
final MessageLineInfo messageLineInfo = (MessageLineInfo) info;
CharSequence text = messageLineInfo.mText;
if (TextUtils.isEmpty(text) && messageLineInfo.mAttachmentUri != null) {
text = BugleNotifications.formatAttachmentTag(
convInfo.mIsGroup ? messageLineInfo.mAuthorFirstName : null,
messageLineInfo.mAttachmentType);
}
if (TextUtils.isEmpty(text)) {
continue;
}
final Person.Builder personBuilder = new Person.Builder()
.setName(!TextUtils.isEmpty(messageLineInfo.mAuthorFullName)
? messageLineInfo.mAuthorFullName : mTitle);
if (convInfo.mContactUri != null) {
personBuilder.setUri(convInfo.mContactUri.toString());
}
final long timestamp = Math.max(0, convInfo.mReceivedTimestamp - i);
final NotificationCompat.MessagingStyle.Message message =
new NotificationCompat.MessagingStyle.Message(
text, timestamp, personBuilder.build());
if (messageLineInfo.mAttachmentUri != null
&& !TextUtils.isEmpty(messageLineInfo.mAttachmentType)
&& (ContentType.isImageType(messageLineInfo.mAttachmentType)
|| ContentType.isVideoType(messageLineInfo.mAttachmentType))) {
message.setData(messageLineInfo.mAttachmentType,
messageLineInfo.mAttachmentUri);
}
style.addMessage(message);
}
builder.setWhen(convInfo.mReceivedTimestamp); 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; private static final String TAG = LogUtil.BUGLE_TAG;
public static final String AUTHORITY = public static final String AUTHORITY =
"com.bnkode.notie.datamodel.MessagingContentProvider"; "com.filabcorp.msgq.datamodel.MessagingContentProvider";
private static final String CONTENT_AUTHORITY = "content://" + AUTHORITY + '/'; private static final String CONTENT_AUTHORITY = "content://" + AUTHORITY + '/';
// Conversations query // Conversations query
@@ -32,7 +32,7 @@ import java.io.IOException;
public class MmsFileProvider extends FileProvider { public class MmsFileProvider extends FileProvider {
private static final String TAG = LogUtil.BUGLE_TAG; 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"; 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. * Broadcast receiver for alarms scheduled through ActionService.
*/ */
public static class PendingActionReceiver extends BroadcastReceiver { 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. * Allocate an intent with a specific opcode and alarm action.
@@ -62,6 +62,7 @@ public class ContactPickerData extends BindableData implements
private ContactPickerDataListener mListener; private ContactPickerDataListener mListener;
private final FrequentContactsCursorBuilder mFrequentContactsCursorBuilder; private final FrequentContactsCursorBuilder mFrequentContactsCursorBuilder;
private String mSearchText; private String mSearchText;
private ContactUtil.AccountFilter mAccountFilter;
public ContactPickerData(final Context context, final ContactPickerDataListener listener) { public ContactPickerData(final Context context, final ContactPickerDataListener listener) {
mListener = listener; mListener = listener;
@@ -82,7 +83,7 @@ public class ContactPickerData extends BindableData implements
if (isBound(bindingId)) { if (isBound(bindingId)) {
switch (id) { switch (id) {
case ALL_CONTACTS_LOADER: case ALL_CONTACTS_LOADER:
return ContactUtil.getPhones(mContext) return ContactUtil.getPhones(mContext, mAccountFilter)
.createBoundCursorLoader(bindingId); .createBoundCursorLoader(bindingId);
case FREQUENT_CONTACTS_LOADER: case FREQUENT_CONTACTS_LOADER:
return ContactUtil.getFrequentContacts(mContext) return ContactUtil.getFrequentContacts(mContext)
@@ -92,7 +93,7 @@ public class ContactPickerData extends BindableData implements
MessagingContentProvider.PARTICIPANTS_URI, MessagingContentProvider.PARTICIPANTS_URI,
ParticipantData.ParticipantsQuery.PROJECTION, null, null, null); ParticipantData.ParticipantsQuery.PROJECTION, null, null, null);
case FILTERED_CONTACTS_LOADER: case FILTERED_CONTACTS_LOADER:
return ContactUtil.filterDestination(mContext, mSearchText) return ContactUtil.filterDestination(mContext, mSearchText, mAccountFilter)
.createBoundCursorLoader(bindingId); .createBoundCursorLoader(bindingId);
default: default:
Assert.fail("Unknown loader id for contact picker!"); 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); 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 @Override
protected void unregisterListeners() { protected void unregisterListeners() {
mListener = null; mListener = null;
@@ -22,6 +22,7 @@ import android.content.Intent;
import com.android.messaging.BugleApplication; import com.android.messaging.BugleApplication;
import com.android.messaging.Factory; import com.android.messaging.Factory;
import com.android.messaging.datamodel.ScheduledMessageStore;
import com.android.messaging.datamodel.action.UpdateMessageNotificationAction; import com.android.messaging.datamodel.action.UpdateMessageNotificationAction;
import com.android.messaging.util.BuglePrefsKeys; import com.android.messaging.util.BuglePrefsKeys;
import com.android.messaging.util.LogUtil; import com.android.messaging.util.LogUtil;
@@ -40,6 +41,7 @@ public class BootAndPackageReplacedReceiver extends BroadcastReceiver {
UpdateMessageNotificationAction.updateMessageNotification(); UpdateMessageNotificationAction.updateMessageNotification();
BugleApplication.updateAppConfig(context); BugleApplication.updateAppConfig(context);
ScheduledMessageStore.rescheduleAll(context);
} else { } else {
LogUtil.i(LogUtil.BUGLE_TAG, "BootAndPackageReplacedReceiver got unexpected action: " LogUtil.i(LogUtil.BUGLE_TAG, "BootAndPackageReplacedReceiver got unexpected action: "
+ intent.getAction()); + 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 class SendStatusReceiver extends BroadcastReceiver {
public static final String MESSAGE_SENT_ACTION = 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 = 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 = 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 = 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. // Defined by platform, but no constant provided. See docs for SmsManager.sendTextMessage.
public static final String EXTRA_ERROR_CODE = "errorCode"; public static final String EXTRA_ERROR_CODE = "errorCode";
@@ -18,9 +18,23 @@ package com.android.messaging.ui;
import android.content.Intent; import android.content.Intent;
import android.os.Bundle; import android.os.Bundle;
import android.telephony.TelephonyManager; 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.datamodel.NoConfirmationSmsSendService;
import com.android.messaging.ui.home.HomeBottomSheet;
import com.android.messaging.util.LogUtil; import com.android.messaging.util.LogUtil;
import com.google.android.material.bottomsheet.BottomSheetDialog;
public class RemoteInputEntrypointActivity extends BaseBugleActivity { public class RemoteInputEntrypointActivity extends BaseBugleActivity {
private static final String TAG = LogUtil.BUGLE_TAG; private static final String TAG = LogUtil.BUGLE_TAG;
@@ -37,22 +51,111 @@ public class RemoteInputEntrypointActivity extends BaseBugleActivity {
return; return;
} }
// Perform some action depending on the intent
String action = intent.getAction(); String action = intent.getAction();
if (Intent.ACTION_SENDTO.equals(action)) { 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 {
LogUtil.w(TAG, "Unrecognized intent action: " + action); LogUtil.w(TAG, "Unrecognized intent action: " + action);
setResult(RESULT_CANCELED); 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(); 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.net.Uri;
import android.os.Bundle; import android.os.Bundle;
import androidx.activity.result.ActivityResultLauncher;
import androidx.fragment.app.Fragment; import androidx.fragment.app.Fragment;
import com.android.messaging.Factory; 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 UI_INTENT_EXTRA_WITH_CUSTOM_TRANSITION = "with_custom_transition";
public static final String ACTION_RESET_NOTIFICATIONS = public static final String ACTION_RESET_NOTIFICATIONS =
"com.bnkode.notie.reset_notifications"; "com.filabcorp.msgq.reset_notifications";
// Sending VCard uri to VCard detail activity // Sending VCard uri to VCard detail activity
public static final String UI_INTENT_EXTRA_VCARD_URI = "vcard_uri"; 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 // Sending attachment content type from widget
public static final String UI_INTENT_EXTRA_ATTACHMENT_TYPE = "attachment_type"; 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 = 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_REQUIRES_MMS = "requires_mms";
public static final String UI_INTENT_EXTRA_SELF_ID = "self_id"; 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. // Message position to scroll to.
public static final String UI_INTENT_EXTRA_MESSAGE_POSITION = "message_position"; 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); 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 * Launch an activity to show a class zero message
*/ */
@@ -315,7 +327,8 @@ public abstract class UIIntents {
*/ */
public abstract PendingIntent getPendingIntentForSendingMessageToConversation( public abstract PendingIntent getPendingIntentForSendingMessageToConversation(
final Context context, final String conversationId, final String selfId, 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. * Get a PendingIntent for clearing notifications.
@@ -35,6 +35,7 @@ import android.provider.MediaStore;
import android.support.v7.mms.pdu.ContentType; import android.support.v7.mms.pdu.ContentType;
import android.text.TextUtils; import android.text.TextUtils;
import androidx.activity.result.ActivityResultLauncher;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import androidx.core.app.TaskStackBuilder; import androidx.core.app.TaskStackBuilder;
import androidx.localbroadcastmanager.content.LocalBroadcastManager; import androidx.localbroadcastmanager.content.LocalBroadcastManager;
@@ -248,6 +249,19 @@ public class UIIntentsImpl extends UIIntents {
context.startActivity(intent); 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 @Override
public void launchHomeProfileActivity(final Activity activity, final String conversationId) { public void launchHomeProfileActivity(final Activity activity, final String conversationId) {
final Intent intent = new Intent(activity, final Intent intent = new Intent(activity,
@@ -396,7 +410,7 @@ public class UIIntentsImpl extends UIIntents {
@Override @Override
public PendingIntent getPendingIntentForSendingMessageToConversation(final Context context, public PendingIntent getPendingIntentForSendingMessageToConversation(final Context context,
final String conversationId, final String selfId, final boolean requiresMms, 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); final Intent intent = new Intent(context, RemoteInputEntrypointActivity.class);
intent.setAction(Intent.ACTION_SENDTO); intent.setAction(Intent.ACTION_SENDTO);
// Ensure that the platform doesn't reuse PendingIntents across conversations // 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_CONVERSATION_ID, conversationId);
intent.putExtra(UIIntents.UI_INTENT_EXTRA_SELF_ID, selfId); intent.putExtra(UIIntents.UI_INTENT_EXTRA_SELF_ID, selfId);
intent.putExtra(UIIntents.UI_INTENT_EXTRA_REQUIRES_MMS, requiresMms); 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); intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
return getPendingIntentWithParentStack(context, intent, requestCode); return getPendingIntentWithParentStack(context, intent, requestCode);
} }
@@ -24,24 +24,37 @@ import android.content.Intent;
import android.os.Bundle; import android.os.Bundle;
import android.provider.Settings; import android.provider.Settings;
import android.text.TextUtils; import android.text.TextUtils;
import android.view.LayoutInflater;
import android.view.View; import android.view.View;
import android.widget.LinearLayout;
import android.widget.TextView;
import androidx.activity.result.ActivityResultLauncher; import androidx.activity.result.ActivityResultLauncher;
import androidx.activity.result.contract.ActivityResultContracts; import androidx.activity.result.contract.ActivityResultContracts;
import com.android.messaging.R; import com.android.messaging.R;
import com.android.messaging.ui.BugleActionBarActivity; import com.android.messaging.ui.BugleActionBarActivity;
import com.android.messaging.ui.LicenseActivity; import com.android.messaging.ui.LicenseActivity;
import com.android.messaging.ui.UIIntents; 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.HomeSettingsRow;
import com.android.messaging.ui.home.HomeSubHeader; 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.BuglePrefs;
import com.android.messaging.util.ChangeDefaultSmsAppHelper; import com.android.messaging.util.ChangeDefaultSmsAppHelper;
import com.android.messaging.util.PhoneUtils; import com.android.messaging.util.PhoneUtils;
import com.google.android.material.bottomsheet.BottomSheetDialog;
public class ApplicationSettingsActivity extends BugleActionBarActivity { 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 mSmsRow;
private View mLanguageRow;
private String mLanguageTag = LANGUAGE_SYSTEM;
private ChangeDefaultSmsAppHelper mChangeDefaultSmsAppHelper; private ChangeDefaultSmsAppHelper mChangeDefaultSmsAppHelper;
private final ActivityResultLauncher<Intent> mLauncher = registerForActivityResult( private final ActivityResultLauncher<Intent> mLauncher = registerForActivityResult(
new ActivityResultContracts.StartActivityForResult(), result -> { new ActivityResultContracts.StartActivityForResult(), result -> {
@@ -64,6 +77,9 @@ public class ApplicationSettingsActivity extends BugleActionBarActivity {
HomeSubHeader.bind(this, R.string.home_settings_title, v -> finish()); HomeSubHeader.bind(this, R.string.home_settings_title, v -> finish());
mSmsRow = findViewById(R.id.home_settings_sms); 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(); bindStaticRows();
updateSmsRow(); updateSmsRow();
} }
@@ -72,6 +88,7 @@ public class ApplicationSettingsActivity extends BugleActionBarActivity {
protected void onResume() { protected void onResume() {
super.onResume(); super.onResume();
updateSmsRow(); updateSmsRow();
bindAppLockRow();
} }
@Override @Override
@@ -82,8 +99,9 @@ public class ApplicationSettingsActivity extends BugleActionBarActivity {
private void bindStaticRows() { private void bindStaticRows() {
final BuglePrefs prefs = BuglePrefs.getApplicationPrefs(); 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), HomeSettingsRow.bind(findViewById(R.id.home_settings_notifications),
R.drawable.ic_home_bell, R.drawable.ic_home_bell,
@@ -96,6 +114,36 @@ public class ApplicationSettingsActivity extends BugleActionBarActivity {
startActivity(intent); startActivity(intent);
}, null); }, 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 String sendKey = getString(R.string.send_sound_pref_key);
final boolean sendSound = prefs.getBoolean(sendKey, final boolean sendSound = prefs.getBoolean(sendKey,
getResources().getBoolean(R.bool.send_sound_pref_default)); getResources().getBoolean(R.bool.send_sound_pref_default));
@@ -105,41 +153,119 @@ public class ApplicationSettingsActivity extends BugleActionBarActivity {
null, false, true, sendSound, null, false, true, sendSound,
null, (button, checked) -> prefs.putBoolean(sendKey, checked)); null, (button, checked) -> prefs.putBoolean(sendKey, checked));
final String swipeKey = getString(R.string.swipe_right_deletes_conversation_key); bindRow(R.id.home_settings_blocked, R.drawable.ic_home_menu_spam,
final boolean swipeDelete = prefs.getBoolean(swipeKey, false); R.string.home_settings_blocked, R.string.home_settings_blocked_subtitle,
HomeSettingsRow.bind(findViewById(R.id.home_settings_swipe_delete), v -> {
R.drawable.ic_home_menu_trash, UIIntents.get().launchHomeSpamActivity(this);
getString(R.string.home_settings_swipe_delete), overridePendingTransition(R.anim.home_activity_open, R.anim.home_activity_hold);
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); bindComingSoon(R.id.home_settings_privacy, R.drawable.ic_home_menu_person,
if (topLevel) { R.string.home_settings_privacy, R.string.home_settings_privacy_subtitle);
advancedCard.setVisibility(View.VISIBLE); bindComingSoon(R.id.home_settings_backup, R.drawable.ic_home_archive_line,
HomeSettingsRow.bind(findViewById(R.id.home_settings_advanced), R.string.home_settings_backup, R.string.home_settings_backup_subtitle);
R.drawable.ic_home_nav_settings, bindComingSoon(R.id.home_settings_data, R.drawable.ic_home_file_line,
getString(R.string.home_settings_advanced), R.string.home_settings_data, R.string.home_settings_data_subtitle);
null, true, false, false, bindComingSoon(R.id.home_settings_help, R.drawable.ic_home_info,
v -> { R.string.home_settings_help, R.string.home_settings_help_subtitle);
startActivity(UIIntents.get().getAdvancedSettingsIntent(this));
overridePendingTransition(R.anim.home_activity_open,
R.anim.home_activity_hold);
},
null);
} else {
advancedCard.setVisibility(View.GONE);
}
HomeSettingsRow.bind(findViewById(R.id.home_settings_licenses), bindRow(R.id.home_settings_licenses, R.drawable.ic_info_light,
R.drawable.ic_info_light, R.string.home_settings_licenses, 0,
getString(R.string.home_settings_licenses),
null, true, false, false,
v -> { v -> {
startActivity(new Intent(this, LicenseActivity.class)); startActivity(new Intent(this, LicenseActivity.class));
overridePendingTransition(R.anim.home_activity_open, R.anim.home_activity_hold); 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() { private void updateSmsRow() {
@@ -20,6 +20,7 @@ import android.content.Context;
import android.database.Cursor; import android.database.Cursor;
import android.net.Uri; import android.net.Uri;
import android.provider.ContactsContract.CommonDataKinds.Phone; import android.provider.ContactsContract.CommonDataKinds.Phone;
import android.text.TextUtils;
import android.util.AttributeSet; import android.util.AttributeSet;
import android.view.View; import android.view.View;
import android.view.View.OnClickListener; import android.view.View.OnClickListener;
@@ -53,9 +54,19 @@ public class ContactListItemView extends LinearLayout implements OnClickListener
private ContactIconView mContactIconView; private ContactIconView mContactIconView;
private ImageView mContactCheckmarkView; private ImageView mContactCheckmarkView;
private ImageView mWorkProfileIcon; private ImageView mWorkProfileIcon;
private View mSectionCard;
private View mSectionDivider;
private View mSectionGapTop;
private View mSectionGapBottom;
private HostInterface mHostInterface; private HostInterface mHostInterface;
private boolean mShouldShowAlphabetHeader; 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) { public ContactListItemView(final Context context, final AttributeSet attrs) {
super(context, attrs); super(context, attrs);
mData = DataModel.get().createContactListItemData(); mData = DataModel.get().createContactListItemData();
@@ -71,6 +82,55 @@ public class ContactListItemView extends LinearLayout implements OnClickListener
mContactIconView = findViewById(R.id.contact_icon); mContactIconView = findViewById(R.id.contact_icon);
mContactCheckmarkView = findViewById(R.id.contact_checkmark); mContactCheckmarkView = findViewById(R.id.contact_checkmark);
mWorkProfileIcon = findViewById(R.id.work_profile_icon); 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 { } else {
mWorkProfileIcon.setVisibility(GONE); mWorkProfileIcon.setVisibility(GONE);
} }
if (mShouldShowAlphabetHeader) { final String headerLabel = mData.getAlphabetHeader();
if (mShouldShowAlphabetHeader && !TextUtils.isEmpty(headerLabel)
&& !TextUtils.equals(headerLabel, " ")) {
mAlphabetHeaderTextView.setVisibility(VISIBLE); mAlphabetHeaderTextView.setVisibility(VISIBLE);
mAlphabetHeaderTextView.setText(mData.getAlphabetHeader()); mAlphabetHeaderTextView.setText(headerLabel);
} else { } else {
mAlphabetHeaderTextView.setVisibility(GONE); mAlphabetHeaderTextView.setVisibility(GONE);
} }
@@ -118,7 +118,7 @@ public class ContactSectionIndexer implements SectionIndexer {
if (TextUtils.isEmpty(mSections[i])) { if (TextUtils.isEmpty(mSections[i])) {
mSections[i] = BLANK_HEADER_STRING; mSections[i] = BLANK_HEADER_STRING;
} else if (!mSections[i].equals(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); 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 // 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 // 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. // a slightly more sophisticated heuristic, but as a fallback this is good enough.
final String sortKey = cursor.getString(ContactUtil.INDEX_SORT_KEY); String sortKey = cursor.getString(ContactUtil.INDEX_SORT_KEY);
final String section = TextUtils.isEmpty(sortKey) ? BLANK_HEADER_STRING : if (TextUtils.isEmpty(sortKey)) {
sortKey.substring(0, 1).toUpperCase(); sortKey = cursor.getString(ContactUtil.INDEX_DISPLAY_NAME);
}
final String section = sectionLabelFor(sortKey);
final int lastIndex = sections.size() - 1; final int lastIndex = sections.size() - 1;
final String currentSection = lastIndex >= 0 ? sections.get(lastIndex) : null; final String currentSection = lastIndex >= 0 ? sections.get(lastIndex) : null;
@@ -158,6 +160,94 @@ public class ContactSectionIndexer implements SectionIndexer {
sections.toArray(mSections); 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) { private void buildIndexer(final Cursor cursor) {
// First check if we get indexer label extras from the contact provider; if not, fall back // First check if we get indexer label extras from the contact provider; if not, fall back
// to building from display names. // to building from display names.
@@ -18,6 +18,7 @@ package com.android.messaging.ui.conversation;
import android.app.Activity; import android.app.Activity;
import android.content.Context; import android.content.Context;
import android.content.Intent;
import android.content.res.Resources; import android.content.res.Resources;
import android.graphics.Rect; import android.graphics.Rect;
import android.net.Uri; import android.net.Uri;
@@ -53,6 +54,7 @@ import androidx.core.view.WindowInsetsControllerCompat;
import com.android.messaging.Factory; import com.android.messaging.Factory;
import com.android.messaging.R; import com.android.messaging.R;
import com.android.messaging.datamodel.ScheduledMessageStore;
import com.android.messaging.datamodel.binding.Binding; import com.android.messaging.datamodel.binding.Binding;
import com.android.messaging.datamodel.binding.BindingBase; import com.android.messaging.datamodel.binding.BindingBase;
import com.android.messaging.datamodel.binding.ImmutableBindingRef; import com.android.messaging.datamodel.binding.ImmutableBindingRef;
@@ -72,7 +74,9 @@ import com.android.messaging.ui.AttachmentPreview;
import com.android.messaging.ui.BugleActionBarActivity; import com.android.messaging.ui.BugleActionBarActivity;
import com.android.messaging.ui.PlainTextEditText; import com.android.messaging.ui.PlainTextEditText;
import com.android.messaging.ui.conversation.ConversationInputManager.ConversationInputSink; import com.android.messaging.ui.conversation.ConversationInputManager.ConversationInputSink;
import com.android.messaging.ui.UIIntents;
import com.android.messaging.ui.home.HomeComposeAttachPanel; 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.HomeToast;
import com.android.messaging.ui.home.HomeUiMotion; import com.android.messaging.ui.home.HomeUiMotion;
import com.android.messaging.util.AccessibilityUtil; import com.android.messaging.util.AccessibilityUtil;
@@ -116,6 +120,8 @@ public class ComposeMessageView extends LinearLayout
void showAttachmentChooser(); void showAttachmentChooser();
void launchSystemGalleryPicker(); void launchSystemGalleryPicker();
void requestMediaPermission(Runnable onGranted); void requestMediaPermission(Runnable onGranted);
void requestLocationPermission(Runnable onGranted);
void launchLocationShare();
boolean shouldShowSubjectEditor(); boolean shouldShowSubjectEditor();
boolean shouldHideAttachmentsWhenSimSelectorShown(); boolean shouldHideAttachmentsWhenSimSelectorShown();
Uri getSelfSendButtonIconUri(); Uri getSelfSendButtonIconUri();
@@ -124,6 +130,7 @@ public class ComposeMessageView extends LinearLayout
} }
public static final int CODEPOINTS_REMAINING_BEFORE_COUNTER_SHOWN = 10; 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 // There is no draft and there is no need for the SIM selector
private static final int SEND_WIDGET_MODE_SELF_AVATAR = 1; private static final int SEND_WIDGET_MODE_SELF_AVATAR = 1;
@@ -150,6 +157,9 @@ public class ComposeMessageView extends LinearLayout
private int mComposeBarPaddingBottom; private int mComposeBarPaddingBottom;
private ImageButton mVoiceButton; private ImageButton mVoiceButton;
private View mSendColumn; private View mSendColumn;
private View mScheduleChipRow;
private TextView mScheduleChipText;
private long mScheduledSendAt;
private final Binding<DraftMessageData> mBinding; private final Binding<DraftMessageData> mBinding;
private IComposeMessageViewHost mHost; private IComposeMessageViewHost mHost;
@@ -322,6 +332,16 @@ public class ComposeMessageView extends LinearLayout
mComposeBarPaddingBottom = mComposeBar.getPaddingBottom(); mComposeBarPaddingBottom = mComposeBar.getPaddingBottom();
} }
mAttachHost = createComposeAttachHost(); 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.setOnClickListener(clickView -> toggleComposeAttachmentSheet());
mAttachMediaButton.setSelected(mComposeEditText.hasFocus()); mAttachMediaButton.setSelected(mComposeEditText.hasFocus());
@@ -433,6 +453,9 @@ public class ComposeMessageView extends LinearLayout
mBinding.ensureBound(data); mBinding.ensureBound(data);
switch (result) { switch (result) {
case CheckDraftForSendTask.RESULT_PASSED: case CheckDraftForSendTask.RESULT_PASSED:
if (queueScheduledMessageIfNeeded()) {
break;
}
// Continue sending after check succeeded. // Continue sending after check succeeded.
final MessageData message = mBinding.getData() final MessageData message = mBinding.getData()
.prepareMessageForSending(mBinding); .prepareMessageForSending(mBinding);
@@ -1040,6 +1063,35 @@ public class ComposeMessageView extends LinearLayout
public void saveInputState(final Bundle outState) { public void saveInputState(final Bundle outState) {
mInputManager.onSaveInputState(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() { public void resetMediaPickerState() {
@@ -1135,6 +1187,18 @@ public class ComposeMessageView extends LinearLayout
openMediaFromAttachmentSheet(ConversationInputManager.MEDIA_PICKER_TARGET_CONTACT); openMediaFromAttachmentSheet(ConversationInputManager.MEDIA_PICKER_TARGET_CONTACT);
} }
@Override
public void onShareLocation() {
if (mHost != null) {
mHost.requestLocationPermission(() -> mHost.launchLocationShare());
}
}
@Override
public void onScheduleMessage() {
showSchedulePicker();
}
@Override @Override
public void onRequestMediaPermission() { public void onRequestMediaPermission() {
if (mHost != null) { 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() { private Activity resolveActivity() {
if (mOriginalContext instanceof Activity) { if (mOriginalContext instanceof Activity) {
return (Activity) mOriginalContext; return (Activity) mOriginalContext;
@@ -111,6 +111,7 @@ import com.android.messaging.ui.ConversationDrawables;
import com.android.messaging.ui.SnackBar; import com.android.messaging.ui.SnackBar;
import com.android.messaging.ui.UIIntents; import com.android.messaging.ui.UIIntents;
import com.android.messaging.ui.animation.PopupTransitionAnimation; import com.android.messaging.ui.animation.PopupTransitionAnimation;
import com.android.messaging.ui.home.HomeBottomSheet;
import com.android.messaging.ui.home.HomeComposeAttachPanel; import com.android.messaging.ui.home.HomeComposeAttachPanel;
import com.android.messaging.ui.home.HomeEmptyState; import com.android.messaging.ui.home.HomeEmptyState;
import com.android.messaging.ui.home.HomeGallerySheet; 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.ContactUtil;
import com.android.messaging.util.ImeUtil; import com.android.messaging.util.ImeUtil;
import com.android.messaging.util.LogUtil; import com.android.messaging.util.LogUtil;
import com.android.messaging.util.OsUtil;
import com.android.messaging.util.PhoneUtils; import com.android.messaging.util.PhoneUtils;
import com.android.messaging.util.TextUtil; import com.android.messaging.util.TextUtil;
import com.android.messaging.util.UiUtils; import com.android.messaging.util.UiUtils;
import com.android.messaging.util.UriUtil; import com.android.messaging.util.UriUtil;
import com.google.android.material.bottomsheet.BottomSheetDialog;
import java.io.File; import java.io.File;
import java.util.ArrayList; 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() { public ConversationFragment getConversationFragment() {
return (ConversationFragment) getParentFragmentManager().findFragmentByTag( return (ConversationFragment) getParentFragmentManager().findFragmentByTag(
ConversationFragment.FRAGMENT_TAG); ConversationFragment.FRAGMENT_TAG);
@@ -544,6 +567,7 @@ public class ConversationFragment extends Fragment implements ConversationDataLi
// Bind the compose message view to the DraftMessageData // Bind the compose message view to the DraftMessageData
mComposeMessageView.bind(DataModel.get().createDraftMessageData( mComposeMessageView.bind(DataModel.get().createDraftMessageData(
mBinding.getData().getConversationId()), this); mBinding.getData().getConversationId()), this);
mComposeMessageView.restoreScheduledSendAt(savedInstanceState);
return view; return view;
} }
@@ -1508,13 +1532,9 @@ public class ConversationFragment extends Fragment implements ConversationDataLi
if (messageView != mSelectedMessage) { if (messageView != mSelectedMessage) {
final ConversationMessageData data = messageView.getData(); final ConversationMessageData data = messageView.getData();
final boolean isReadyToSend = isReadyForAction(); final boolean isReadyToSend = isReadyForAction();
if (data.getOneClickResendMessage()) { if (data.getOneClickResendMessage() || data.getShowResendMessage()) {
// Directly resend the message on tap if it's failed // Resend / delete is offered from the status row under the bubble.
retrySend(data.getMessageId());
selectMessage(null); selectMessage(null);
} else if (data.getShowResendMessage() && isReadyToSend) {
// Select the message to show the resend/download/delete options
selectMessage(messageView);
} else if (data.getShowDownloadMessage() && isReadyToSend) { } else if (data.getShowDownloadMessage() && isReadyToSend) {
// Directly download the message on tap // Directly download the message on tap
retryDownload(data.getMessageId()); 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 { private static class AttachmentToSave {
public final Uri uri; public final Uri uri;
public final String contentType; 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 @Override
public void showAttachmentChooser() { public void showAttachmentChooser() {
showAttachmentChooser(mConversationId, getActivity()); showAttachmentChooser(mConversationId, getActivity());
@@ -19,6 +19,7 @@ package com.android.messaging.ui.conversation;
import android.content.Context; import android.content.Context;
import android.content.res.Resources; import android.content.res.Resources;
import android.database.Cursor; import android.database.Cursor;
import android.graphics.Paint;
import android.graphics.Rect; import android.graphics.Rect;
import android.graphics.drawable.Drawable; import android.graphics.drawable.Drawable;
import android.net.Uri; import android.net.Uri;
@@ -37,6 +38,7 @@ import android.view.View;
import android.view.ViewGroup; import android.view.ViewGroup;
import android.view.WindowManager; import android.view.WindowManager;
import android.widget.FrameLayout; import android.widget.FrameLayout;
import android.widget.ImageView;
import android.widget.ImageView.ScaleType; import android.widget.ImageView.ScaleType;
import android.widget.LinearLayout; import android.widget.LinearLayout;
import android.widget.TextView; import android.widget.TextView;
@@ -87,6 +89,7 @@ public class ConversationMessageView extends FrameLayout implements View.OnClick
Rect imageBounds, boolean longPress); Rect imageBounds, boolean longPress);
SubscriptionListEntry getSubscriptionEntryForSelfParticipant(String selfParticipantId, SubscriptionListEntry getSubscriptionEntryForSelfParticipant(String selfParticipantId,
boolean excludeDefault); boolean excludeDefault);
void onMessageSendOptionsClick(ConversationMessageView view);
} }
private final ConversationMessageData mData; private final ConversationMessageData mData;
@@ -98,6 +101,8 @@ public class ConversationMessageView extends FrameLayout implements View.OnClick
private boolean mMessageTextHasLinks; private boolean mMessageTextHasLinks;
private boolean mMessageHasYouTubeLink; private boolean mMessageHasYouTubeLink;
private TextView mStatusTextView; private TextView mStatusTextView;
private TextView mStatusActionView;
private ImageView mStatusIconView;
private TextView mTitleTextView; private TextView mTitleTextView;
private TextView mMmsInfoTextView; private TextView mMmsInfoTextView;
private LinearLayout mMessageTitleLayout; private LinearLayout mMessageTitleLayout;
@@ -144,6 +149,16 @@ public class ConversationMessageView extends FrameLayout implements View.OnClick
IgnoreLinkLongClickHelper.ignoreLinkLongClick(mMessageTextView, this); IgnoreLinkLongClickHelper.ignoreLinkLongClick(mMessageTextView, this);
mStatusTextView = findViewById(R.id.message_status); 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); mTitleTextView = findViewById(R.id.message_title);
mMmsInfoTextView = findViewById(R.id.mms_info); mMmsInfoTextView = findViewById(R.id.mms_info);
mMessageTitleLayout = findViewById(R.id.message_title_layout); 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; || 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 * Returns whether we should show simplified visual style for the message view (i.e. hide the
* avatar and bubble arrow, reduce padding). * 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_YET_TO_SEND:
case MessageData.BUGLE_STATUS_OUTGOING_SENDING: case MessageData.BUGLE_STATUS_OUTGOING_SENDING:
case MessageData.BUGLE_STATUS_OUTGOING_RESENDING: case MessageData.BUGLE_STATUS_OUTGOING_RESENDING:
case MessageData.BUGLE_STATUS_OUTGOING_AWAITING_RETRY:
statusResId = R.string.message_status_sending; statusResId = R.string.message_status_sending;
break; 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: case MessageData.BUGLE_STATUS_OUTGOING_FAILED_EMERGENCY_NUMBER:
statusResId = R.string.message_status_send_failed_emergency_number; statusResId = R.string.message_status_send_failed_emergency_number;
break; break;
@@ -382,12 +425,7 @@ public class ConversationMessageView extends FrameLayout implements View.OnClick
break; break;
} }
if (PhoneUtils.getDefault().isDefaultSmsApp()) { if (PhoneUtils.getDefault().isDefaultSmsApp()) {
if (isSelected()) { statusText = getResources().getString(R.string.home_message_status_not_sent);
statusResId = R.string.message_status_resend;
} else {
statusResId = MmsUtils.mapRawStatusToErrorResourceId(
mData.getStatus(), mData.getRawTelephonyStatus());
}
break; break;
} }
// FALL THROUGH HERE // 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 // We set the text even if the view will be GONE for accessibility
mStatusTextView.setText(statusText); mStatusTextView.setText(statusText);
final boolean statusVisible = !TextUtils.isEmpty(statusText); final boolean statusVisible = !TextUtils.isEmpty(statusText);
if (statusVisible) { mStatusTextView.setVisibility(statusVisible ? View.VISIBLE : View.GONE);
mStatusTextView.setVisibility(View.VISIBLE);
} else { final boolean sendOptionsVisible = shouldShowSendOptions();
mStatusTextView.setVisibility(View.GONE); if (mStatusActionView != null) {
mStatusActionView.setVisibility(sendOptionsVisible ? View.VISIBLE : View.GONE);
}
if (mStatusIconView != null) {
mStatusIconView.setVisibility(
isOutgoingWaitingStatus() ? View.VISIBLE : View.GONE);
} }
final boolean deliveredBadgeVisible = final boolean deliveredBadgeVisible =
@@ -477,11 +520,11 @@ public class ConversationMessageView extends FrameLayout implements View.OnClick
} }
final boolean metadataVisible = senderNameVisible || statusVisible final boolean metadataVisible = senderNameVisible || statusVisible
|| deliveredBadgeVisible || simNameVisible; || sendOptionsVisible || deliveredBadgeVisible || simNameVisible;
mMessageMetadataView.setVisibility(metadataVisible ? View.VISIBLE : View.GONE); mMessageMetadataView.setVisibility(metadataVisible ? View.VISIBLE : View.GONE);
final boolean messageTextAndOrInfoVisible = titleVisible || subjectVisible final boolean messageTextAndOrInfoVisible = titleVisible || subjectVisible
|| mData.hasText() || metadataVisible; || mData.hasText();
mMessageTextAndInfoView.setVisibility( mMessageTextAndInfoView.setVisibility(
messageTextAndOrInfoVisible ? View.VISIBLE : View.GONE); messageTextAndOrInfoVisible ? View.VISIBLE : View.GONE);
@@ -787,6 +830,9 @@ public class ConversationMessageView extends FrameLayout implements View.OnClick
updateMessageAttachmentsAppearance(gravity); updateMessageAttachmentsAppearance(gravity);
mMessageMetadataView.setPadding(0, metadataTopPadding, 0, 0); mMessageMetadataView.setPadding(0, metadataTopPadding, 0, 0);
if (mMessageMetadataView.getVisibility() == View.VISIBLE) {
setPadding(getPaddingStart(), getPaddingTop(), getPaddingEnd(), metadataTopPadding);
}
updateTextAppearance(); updateTextAppearance();
@@ -844,6 +890,10 @@ public class ConversationMessageView extends FrameLayout implements View.OnClick
description.append(separator); description.append(separator);
description.append(mStatusTextView.getText()); description.append(mStatusTextView.getText());
} }
if (mStatusActionView != null && mStatusActionView.getVisibility() == View.VISIBLE) {
description.append(separator);
description.append(mStatusActionView.getText());
}
if (mSimNameView.getVisibility() == View.VISIBLE) { if (mSimNameView.getVisibility() == View.VISIBLE) {
description.append(separator); description.append(separator);
@@ -995,10 +1045,30 @@ public class ConversationMessageView extends FrameLayout implements View.OnClick
mData.hasAttachments() && !shouldShowMessageTextBubble()) { mData.hasAttachments() && !shouldShowMessageTextBubble()) {
timestampColorResId = R.color.timestamp_text_outgoing; 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)); 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) { if (actionBar == null) {
return; return;
} }
// Custom Message+ chrome replaces the ActionBar on the home screen. // Custom Message Q chrome replaces the ActionBar on the home screen.
actionBar.hide(); actionBar.hide();
super.updateActionBar(actionBar); super.updateActionBar(actionBar);
} }
@@ -19,6 +19,7 @@ package com.android.messaging.ui.conversationlist;
import android.app.Activity; import android.app.Activity;
import android.app.AlertDialog; import android.app.AlertDialog;
import android.content.Context; import android.content.Context;
import android.content.Intent;
import android.database.Cursor; import android.database.Cursor;
import android.graphics.Bitmap; import android.graphics.Bitmap;
import android.graphics.Canvas; import android.graphics.Canvas;
@@ -136,6 +137,7 @@ public class ConversationListFragment extends Fragment implements ConversationLi
private TextView mHomeReadAllButton; private TextView mHomeReadAllButton;
private int mHomeSelectionCount; private int mHomeSelectionCount;
private View mHomeMessagesTop; private View mHomeMessagesTop;
private View mHomeCategoryCollapse;
private View mHomeCategoryStrip; private View mHomeCategoryStrip;
private ViewGroup mHomeCategoryAll; private ViewGroup mHomeCategoryAll;
private View mHomeCategoryScroll; private View mHomeCategoryScroll;
@@ -162,6 +164,7 @@ public class ConversationListFragment extends Fragment implements ConversationLi
private int mHomeNavAutomateNaturalWidth; private int mHomeNavAutomateNaturalWidth;
private int mHomeTab = HOME_TAB_MESSAGES; private int mHomeTab = HOME_TAB_MESSAGES;
private View mHomeListProfilePopup; private View mHomeListProfilePopup;
private PopupWindow mHomeListProfileWindow;
private View mHomeListProfileScrim; private View mHomeListProfileScrim;
private ImageView mHomeListProfileBlur; private ImageView mHomeListProfileBlur;
private Bitmap mHomeListProfileBlurBitmap; 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_MESSAGES = 0;
private static final int HOME_TAB_AUTOMATE = 1; private static final int HOME_TAB_AUTOMATE = 1;
private static final int SEARCH_NEWEST_EDGE_SLOP_DP = 48; private static final int SEARCH_NEWEST_EDGE_SLOP_DP = 48;
private static final int SEARCH_BAR_ANIM_MS = 280; private static final int SEARCH_BAR_ANIM_MS = 220;
private static final int SEARCH_BAR_SLIDE_FALLBACK_DP = 72; private static final int SEARCH_BAR_SLIDE_FALLBACK_DP = 72;
private static final int CHROME_SCROLL_THRESHOLD_DP = 10;
private Parcelable mListState; private Parcelable mListState;
private View mSearchBar; private View mSearchBar;
private View mListTopFade;
private View mListBottomFade;
private boolean mSearchBarShown; private boolean mSearchBarShown;
private boolean mCategoryStripShown = true;
private int mCategoryNaturalHeight;
private int mChromeScrollAccum;
final Binding<ConversationListData> mListBinding = BindingBase.createBinding(this); final Binding<ConversationListData> mListBinding = BindingBase.createBinding(this);
private final ConversationListItemData mPinnedRowProbe = new ConversationListItemData(); private final ConversationListItemData mPinnedRowProbe = new ConversationListItemData();
@@ -259,6 +268,7 @@ public class ConversationListFragment extends Fragment implements ConversationLi
setScrolledToNewestConversationIfNeeded(); setScrolledToNewestConversationIfNeeded();
applySearchBarVisibility(); applySearchBarVisibility();
resetSearchBarScrollTracking(); resetSearchBarScrollTracking();
updateListTopFade();
updateHomeMainNavVisibility(); updateHomeMainNavVisibility();
refreshPinnedStrip(); refreshPinnedStrip();
updateUi(); 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() { private boolean isScrolledToFirstConversation() {
int firstItemPosition = ((LinearLayoutManager) mRecyclerView.getLayoutManager()) int firstItemPosition = ((LinearLayoutManager) mRecyclerView.getLayoutManager())
.findFirstCompletelyVisibleItemPosition(); .findFirstCompletelyVisibleItemPosition();
@@ -287,6 +341,7 @@ public class ConversationListFragment extends Fragment implements ConversationLi
*/ */
@Override @Override
public void onDestroy() { public void onDestroy() {
dismissListProfileWindowImmediate();
super.onDestroy(); super.onDestroy();
mListBinding.unbind(); mListBinding.unbind();
mHost = null; mHost = null;
@@ -301,6 +356,8 @@ public class ConversationListFragment extends Fragment implements ConversationLi
final ViewGroup rootView = (ViewGroup) inflater.inflate(R.layout.conversation_list_fragment, final ViewGroup rootView = (ViewGroup) inflater.inflate(R.layout.conversation_list_fragment,
container, false); container, false);
mRecyclerView = rootView.findViewById(android.R.id.list); 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); mEmptyListMessageView = rootView.findViewById(R.id.no_conversations_view);
// The default behavior for default layout param generation by LinearLayoutManager is to // 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 // 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) { if (!mArchiveMode && !mForwardMessageMode) {
handleSearchBarByScrollOffset(dy); handleSearchBarByScrollOffset(dy);
} }
updateListTopFade();
} }
@Override @Override
@@ -398,6 +456,7 @@ public class ConversationListFragment extends Fragment implements ConversationLi
} }
setupHomeChrome(rootView); setupHomeChrome(rootView);
mHomeMessagesTop = rootView.findViewById(R.id.home_messages_top); mHomeMessagesTop = rootView.findViewById(R.id.home_messages_top);
mHomeCategoryCollapse = rootView.findViewById(R.id.home_category_collapse);
mHomeCategoryStrip = rootView.findViewById(R.id.home_category_strip); mHomeCategoryStrip = rootView.findViewById(R.id.home_category_strip);
mHomeCategoryAll = rootView.findViewById(R.id.home_category_all); mHomeCategoryAll = rootView.findViewById(R.id.home_category_all);
mHomeCategoryScroll = rootView.findViewById(R.id.home_category_scroll); 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) { 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(() -> { searchBar.post(() -> {
updateListPaddingForSearchBar();
applySearchBarVisibility(); applySearchBarVisibility();
resetSearchBarScrollTracking(); 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. // 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) { private void handleSearchBarByScrollOffset(final int dy) {
if (mHomeTab != HOME_TAB_MESSAGES || isSelectionMode() || mHomePinnedEditMode if (mHomeTab != HOME_TAB_MESSAGES || isSelectionMode() || mHomePinnedEditMode
|| mSearchBar == null || mRecyclerView == null || dy == 0) { || mSearchBar == null || mRecyclerView == null || dy == 0) {
return; 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(); showSearchBar();
} else { } else if (mChromeScrollAccum < -threshold) {
mChromeScrollAccum = 0;
hideSearchBar(); hideSearchBar();
} }
} }
@@ -621,73 +684,122 @@ public class ConversationListFragment extends Fragment implements ConversationLi
if (mSearchBar != null) { if (mSearchBar != null) {
mSearchBar.animate().cancel(); mSearchBar.animate().cancel();
} }
if (mHomeCategoryStrip != null) {
mHomeCategoryStrip.animate().cancel();
}
} }
private float getSearchBarSlideDistancePx() { private int getChromeSlotHeight() {
if (mSearchBar == null) { int height = 0;
return dpToPx(SEARCH_BAR_SLIDE_FALLBACK_DP); if (mHomeCategoryStrip != null && mHomeCategoryStrip.getHeight() > 0) {
height = Math.max(height, mHomeCategoryStrip.getHeight());
mCategoryNaturalHeight = mHomeCategoryStrip.getHeight();
} }
final int height = mSearchBar.getHeight(); if (mSearchBar != null && mSearchBar.getHeight() > 0) {
return height > 0 ? height : dpToPx(SEARCH_BAR_SLIDE_FALLBACK_DP); height = Math.max(height, mSearchBar.getHeight());
}
if (height > 0) {
return height;
}
if (mCategoryNaturalHeight > 0) {
return mCategoryNaturalHeight;
}
return dpToPx(SEARCH_BAR_SLIDE_FALLBACK_DP);
}
private void setChromeInteractive(final View view, final boolean interactive) {
if (view == null) {
return;
}
view.setClickable(interactive);
view.setImportantForAccessibility(interactive
? View.IMPORTANT_FOR_ACCESSIBILITY_YES
: View.IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS);
if (view instanceof ViewGroup) {
((ViewGroup) view).setDescendantFocusability(interactive
? ViewGroup.FOCUS_AFTER_DESCENDANTS
: ViewGroup.FOCUS_BLOCK_DESCENDANTS);
}
view.setOnTouchListener(interactive ? null : (v, event) -> true);
}
private void applyChromeSlot(final boolean searchShown, final boolean animate) {
if (mHomeCategoryCollapse == null) {
return;
}
if (mHomeTab != HOME_TAB_MESSAGES) {
cancelSearchBarAnimation();
mSearchBarShown = false;
mCategoryStripShown = false;
mHomeCategoryCollapse.setVisibility(View.GONE);
return;
}
mHomeCategoryCollapse.setVisibility(View.VISIBLE);
mSearchBarShown = searchShown;
mCategoryStripShown = !searchShown;
if (mSearchBar == null || mHomeCategoryStrip == null) {
return;
}
final float slot = getChromeSlotHeight();
mHomeCategoryCollapse.setMinimumHeight((int) slot);
final float searchY = searchShown ? 0f : -slot;
cancelSearchBarAnimation();
mSearchBar.setAlpha(1f);
mHomeCategoryStrip.setAlpha(1f);
mHomeCategoryStrip.setTranslationY(0f);
if (!animate) {
mSearchBar.setTranslationY(searchY);
mSearchBar.setVisibility(searchShown ? View.VISIBLE : View.INVISIBLE);
mHomeCategoryStrip.setVisibility(searchShown ? View.INVISIBLE : View.VISIBLE);
setChromeInteractive(mSearchBar, searchShown);
setChromeInteractive(mHomeCategoryStrip, !searchShown);
return;
}
mHomeCategoryStrip.setVisibility(View.VISIBLE);
mSearchBar.setVisibility(View.VISIBLE);
setChromeInteractive(mSearchBar, searchShown);
setChromeInteractive(mHomeCategoryStrip, !searchShown);
mSearchBar.animate()
.translationY(searchY)
.setDuration(SEARCH_BAR_ANIM_MS)
.setInterpolator(UiUtils.DEFAULT_INTERPOLATOR)
.withEndAction(() -> {
if (mSearchBar == null || mHomeCategoryStrip == null) {
return;
}
if (mSearchBarShown) {
mHomeCategoryStrip.setVisibility(View.INVISIBLE);
} else {
mSearchBar.setVisibility(View.INVISIBLE);
}
});
}
private void setCategoryStripShown(final boolean shown, final boolean animate) {
applyChromeSlot(!shown, animate);
} }
private void updateListPaddingForSearchBar() { private void updateListPaddingForSearchBar() {
if (mRecyclerView == null) { if (mRecyclerView == null) {
return; 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.setPadding(
mRecyclerView.getPaddingLeft(), mRecyclerView.getPaddingLeft(),
top, 0,
mRecyclerView.getPaddingRight(), mRecyclerView.getPaddingRight(),
mRecyclerView.getPaddingBottom()); mRecyclerView.getPaddingBottom());
} }
private void applySearchBarVisibility() { private void applySearchBarVisibility() {
if (mSearchBar == null || mHomeTab != HOME_TAB_MESSAGES) { if (mHomeTab != HOME_TAB_MESSAGES) {
applyChromeSlot(false, false);
return; return;
} }
if (mHomePinnedEditMode) { if (mHomePinnedEditMode) {
cancelSearchBarAnimation(); applyChromeSlot(false, false);
mSearchBar.setVisibility(View.GONE);
mSearchBar.setAlpha(1f);
mSearchBar.setTranslationY(0f);
updateListPaddingForSearchBar();
return; return;
} }
cancelSearchBarAnimation(); applyChromeSlot(mSearchBarShown, false);
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);
} }
private void showSearchBar() { private void showSearchBar() {
@@ -695,40 +807,14 @@ public class ConversationListFragment extends Fragment implements ConversationLi
|| mSearchBar == null || mSearchBarShown) { || mSearchBar == null || mSearchBarShown) {
return; return;
} }
mSearchBarShown = true; applyChromeSlot(true, true);
cancelSearchBarAnimation();
mSearchBar.setVisibility(View.VISIBLE);
updateListPaddingForSearchBar();
if (mSearchBar.getHeight() == 0) {
mSearchBar.post(() -> {
updateListPaddingForSearchBar();
runSearchBarShowAnimation();
});
} else {
runSearchBarShowAnimation();
}
} }
private void hideSearchBar() { private void hideSearchBar() {
if (mSearchBar == null || !mSearchBarShown) { if (mSearchBar == null || !mSearchBarShown) {
return; return;
} }
mSearchBarShown = false; applyChromeSlot(false, true);
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();
}
});
} }
private void setupHomeChrome(final View rootView) { private void setupHomeChrome(final View rootView) {
@@ -838,7 +924,7 @@ public class ConversationListFragment extends Fragment implements ConversationLi
? mHomeListProfileData.getConversationId() : null; ? mHomeListProfileData.getConversationId() : null;
final boolean sameProfile = data.getConversationId() != null final boolean sameProfile = data.getConversationId() != null
&& data.getConversationId().equals(previousProfileId) && data.getConversationId().equals(previousProfileId)
&& mHomeListProfilePopup.getVisibility() == View.VISIBLE; && isListProfilePopupShowing();
if (sameProfile) { if (sameProfile) {
return; return;
} }
@@ -874,13 +960,81 @@ public class ConversationListFragment extends Fragment implements ConversationLi
mHomeListProfileContent.setAlpha(0f); mHomeListProfileContent.setAlpha(0f);
mHomeListProfilePopup.setVisibility(View.INVISIBLE); mHomeListProfilePopup.setVisibility(View.INVISIBLE);
bindListProfileBlur(); bindListProfileBlur();
ensureListProfileWindow();
mHomeListProfilePopup.setVisibility(View.VISIBLE);
showListProfileWindow();
positionListProfilePopup(anchorView, () -> { positionListProfilePopup(anchorView, () -> {
mHomeListProfilePopup.setVisibility(View.VISIBLE);
HomeUiMotion.listProfilePopupEnter(mHomeListProfileScrim, mHomeListProfileContent, HomeUiMotion.listProfilePopupEnter(mHomeListProfileScrim, mHomeListProfileContent,
anchorView); 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() { private void updateListProfileActionsPillWidth() {
if (mHomeListProfileActions == null) { if (mHomeListProfileActions == null) {
return; return;
@@ -1003,17 +1157,19 @@ public class ConversationListFragment extends Fragment implements ConversationLi
} }
private void dismissHomeListProfilePopup(final boolean animate) { private void dismissHomeListProfilePopup(final boolean animate) {
if (mHomeListProfilePopup == null if (!isListProfilePopupShowing()) {
|| mHomeListProfilePopup.getVisibility() != View.VISIBLE) {
recycleListProfileBlur(); recycleListProfileBlur();
mHomeListProfileData = null; mHomeListProfileData = null;
if (mHomeListProfilePopup != null) {
mHomeListProfilePopup.setVisibility(View.GONE);
}
return; return;
} }
final Runnable finish = () -> { final Runnable finish = () -> {
mHomeListProfilePopup.setVisibility(View.GONE);
resetListProfilePopupPosition(); resetListProfilePopupPosition();
recycleListProfileBlur(); if (mHomeListProfileWindow != null && mHomeListProfileWindow.isShowing()) {
mHomeListProfileData = null; mHomeListProfileWindow.dismiss();
}
}; };
if (animate) { if (animate) {
HomeUiMotion.listProfilePopupExit(mHomeListProfileScrim, mHomeListProfileContent, HomeUiMotion.listProfilePopupExit(mHomeListProfileScrim, mHomeListProfileContent,
@@ -1079,14 +1235,7 @@ public class ConversationListFragment extends Fragment implements ConversationLi
} }
if (enabled) { if (enabled) {
setPinnedEditMode(false, false); setPinnedEditMode(false, false);
mSearchBarShown = false; applyChromeSlot(false, false);
cancelSearchBarAnimation();
if (mSearchBar != null) {
mSearchBar.setVisibility(View.GONE);
mSearchBar.setAlpha(1f);
mSearchBar.setTranslationY(0f);
}
updateListPaddingForSearchBar();
} }
updateHomeSelectionBar(0); updateHomeSelectionBar(0);
updateHomeComposeButtonVisibility(); updateHomeComposeButtonVisibility();
@@ -1129,10 +1278,10 @@ public class ConversationListFragment extends Fragment implements ConversationLi
if (mHomeMessagesTop != null) { if (mHomeMessagesTop != null) {
mHomeMessagesTop.setVisibility(View.VISIBLE); mHomeMessagesTop.setVisibility(View.VISIBLE);
} }
if (mHomeCategoryStrip != null) { if (showMessages) {
mHomeCategoryStrip.setVisibility(showMessages ? View.VISIBLE : View.GONE); setCategoryStripShown(!mSearchBarShown, false);
} else if (mHomeCategoryScroll != null) { } else {
mHomeCategoryScroll.setVisibility(showMessages ? View.VISIBLE : View.GONE); setCategoryStripShown(false, false);
} }
final CharSequence title = getString(showMessages final CharSequence title = getString(showMessages
? R.string.home_title : R.string.home_automation_title); ? R.string.home_title : R.string.home_automation_title);
@@ -1156,14 +1305,7 @@ public class ConversationListFragment extends Fragment implements ConversationLi
} }
} }
if (!showMessages) { if (!showMessages) {
mSearchBarShown = false; applyChromeSlot(false, false);
cancelSearchBarAnimation();
if (mSearchBar != null) {
mSearchBar.setVisibility(View.GONE);
mSearchBar.setAlpha(1f);
mSearchBar.setTranslationY(0f);
}
updateListPaddingForSearchBar();
if (mEmptyListMessageView != null) { if (mEmptyListMessageView != null) {
mEmptyListMessageView.setVisibility(View.GONE); mEmptyListMessageView.setVisibility(View.GONE);
} }
@@ -1175,10 +1317,15 @@ public class ConversationListFragment extends Fragment implements ConversationLi
updateHomeNavHighlight(animate); updateHomeNavHighlight(animate);
updateHomeMainNavVisibility(); updateHomeMainNavVisibility();
refreshPinnedStrip(); refreshPinnedStrip();
updateListTopFade();
mHomeTabReady = true; mHomeTabReady = true;
} }
public boolean onHomeBackPressed() { public boolean onHomeBackPressed() {
if (isListProfilePopupShowing()) {
dismissHomeListProfilePopup(true);
return true;
}
if (mHomeTab == HOME_TAB_AUTOMATE) { if (mHomeTab == HOME_TAB_AUTOMATE) {
selectHomeTab(HOME_TAB_MESSAGES); selectHomeTab(HOME_TAB_MESSAGES);
return true; return true;
@@ -1415,12 +1562,15 @@ public class ConversationListFragment extends Fragment implements ConversationLi
return; return;
} }
mHomeListProfileBlurBitmap = captureDecorSnapshot(); mHomeListProfileBlurBitmap = captureDecorSnapshot();
if (mHomeListProfileBlurBitmap == null) {
mHomeListProfileBlurBitmap = captureViewSnapshot(getView());
}
if (mHomeListProfileBlurBitmap == null) { if (mHomeListProfileBlurBitmap == null) {
return; return;
} }
mHomeListProfileBlur.setImageBitmap(mHomeListProfileBlurBitmap); mHomeListProfileBlur.setImageBitmap(mHomeListProfileBlurBitmap);
mHomeListProfileBlur.setRenderEffect(RenderEffect.createBlurEffect( mHomeListProfileBlur.setRenderEffect(RenderEffect.createBlurEffect(
36f, 36f, Shader.TileMode.CLAMP)); 64f, 64f, Shader.TileMode.CLAMP));
} }
private void recycleListProfileBlur() { private void recycleListProfileBlur() {
@@ -1700,8 +1850,11 @@ public class ConversationListFragment extends Fragment implements ConversationLi
if (activity == null) { if (activity == null) {
return null; return null;
} }
final View root = activity.getWindow().getDecorView(); return captureViewSnapshot(activity.getWindow().getDecorView());
if (root.getWidth() <= 0 || root.getHeight() <= 0) { }
private Bitmap captureViewSnapshot(final View root) {
if (root == null || root.getWidth() <= 0 || root.getHeight() <= 0) {
return null; return null;
} }
final Bitmap bitmap = Bitmap.createBitmap( final Bitmap bitmap = Bitmap.createBitmap(
@@ -1787,24 +1940,26 @@ public class ConversationListFragment extends Fragment implements ConversationLi
mHomePopover = null; mHomePopover = null;
} }
}); });
final int xOff; final int endInset = getResources().getDimensionPixelSize(
if (alignEnd) { R.dimen.looply_screen_padding);
final int endInset = getResources().getDimensionPixelSize( final int[] anchorLoc = new int[2];
R.dimen.looply_screen_padding); anchor.getLocationOnScreen(anchorLoc);
final int[] anchorLoc = new int[2]; final View root = getView();
anchor.getLocationOnScreen(anchorLoc); final int windowRight;
final View root = getView(); if (root != null) {
final int windowRight; final int[] rootLoc = new int[2];
if (root != null) { root.getLocationOnScreen(rootLoc);
final int[] rootLoc = new int[2]; windowRight = rootLoc[0] + root.getWidth();
root.getLocationOnScreen(rootLoc);
windowRight = rootLoc[0] + root.getWidth();
} else {
windowRight = getResources().getDisplayMetrics().widthPixels;
}
xOff = windowRight - endInset - content.getMeasuredWidth() - anchorLoc[0];
} else { } else {
xOff = 0; windowRight = getResources().getDisplayMetrics().widthPixels;
}
int xOff = alignEnd
? windowRight - endInset - content.getMeasuredWidth() - anchorLoc[0]
: 0;
final int overflow = (anchorLoc[0] + xOff + content.getMeasuredWidth())
- (windowRight - endInset);
if (overflow > 0) {
xOff -= overflow;
} }
popup.showAsDropDown(anchor, xOff, 8); popup.showAsDropDown(anchor, xOff, 8);
HomeUiMotion.popoverEnter(content, alignEnd); HomeUiMotion.popoverEnter(content, alignEnd);
@@ -1840,6 +1995,10 @@ public class ConversationListFragment extends Fragment implements ConversationLi
selectHomeChip(ConversationListAdapter.FILTER_ALL); selectHomeChip(ConversationListAdapter.FILTER_ALL);
dismissHomePopover(); dismissHomePopover();
}); });
content.findViewById(R.id.popover_share_app).setOnClickListener(v -> {
dismissHomePopover();
shareThisApp();
});
content.findViewById(R.id.popover_settings).setOnClickListener(v -> { content.findViewById(R.id.popover_settings).setOnClickListener(v -> {
dismissHomePopover(); dismissHomePopover();
launchHomeSubScreen(() -> { 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) { 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(); final View content = popup.getContentView();
content.findViewById(R.id.popover_select_messages).setOnClickListener(v -> { content.findViewById(R.id.popover_select_messages).setOnClickListener(v -> {
dismissHomePopoverImmediate(); dismissHomePopoverImmediate();
@@ -2107,6 +2279,7 @@ public class ConversationListFragment extends Fragment implements ConversationLi
@Override @Override
public void onPause() { public void onPause() {
super.onPause(); super.onPause();
dismissHomeListProfilePopup(false);
dismissHomePopover(); dismissHomePopover();
if (mHomeBottomNav != null && !mArchiveMode && !mForwardMessageMode) { if (mHomeBottomNav != null && !mArchiveMode && !mForwardMessageMode) {
mHomeBottomNav.setVisibility(View.GONE); mHomeBottomNav.setVisibility(View.GONE);
@@ -2139,6 +2312,9 @@ public class ConversationListFragment extends Fragment implements ConversationLi
public void updateUi() { public void updateUi() {
mAdapter.notifyDataSetChanged(); mAdapter.notifyDataSetChanged();
if (mRecyclerView != null) {
mRecyclerView.post(this::updateListTopFade);
}
} }
@Override @Override
@@ -2382,13 +2558,8 @@ public class ConversationListFragment extends Fragment implements ConversationLi
if (mSearchBar == null) { if (mSearchBar == null) {
return; return;
} }
cancelSearchBarAnimation();
if (mHomePinnedEditMode) { if (mHomePinnedEditMode) {
mSearchBarShown = false; applyChromeSlot(false, false);
mSearchBar.setVisibility(View.GONE);
mSearchBar.setAlpha(1f);
mSearchBar.setTranslationY(0f);
updateListPaddingForSearchBar();
} else { } else {
applySearchBarVisibility(); 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.content.Intent;
import android.os.Bundle; import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
import android.text.Editable; import android.text.Editable;
import android.text.TextUtils; import android.text.TextUtils;
import android.text.TextWatcher; import android.text.TextWatcher;
@@ -36,10 +38,14 @@ import com.android.messaging.ui.BugleActionBarActivity;
import com.android.messaging.ui.ContactIconView; import com.android.messaging.ui.ContactIconView;
import com.android.messaging.util.AvatarUriUtil; import com.android.messaging.util.AvatarUriUtil;
import com.android.messaging.util.ContactRecipientEntryUtils; import com.android.messaging.util.ContactRecipientEntryUtils;
import com.android.messaging.util.ContactUtil;
import com.android.messaging.util.ImeUtil; import com.android.messaging.util.ImeUtil;
import com.google.android.material.bottomsheet.BottomSheetDialog;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; 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. */ /** Home-style "new chat" screen: recipient field, group entry point and contact list. */
public class HomeComposeActivity extends BugleActionBarActivity public class HomeComposeActivity extends BugleActionBarActivity
@@ -57,7 +63,11 @@ public class HomeComposeActivity extends BugleActionBarActivity
private LinearLayout mChipContainer; private LinearLayout mChipContainer;
private TextView mChipHint; private TextView mChipHint;
private TextView mNextButton; private TextView mNextButton;
private TextView mAccountLabel;
private boolean mGroupMode; private boolean mGroupMode;
private ContactUtil.AccountFilter mAccountFilter;
private final ExecutorService mAccountExecutor = Executors.newSingleThreadExecutor();
private final Handler mMainHandler = new Handler(Looper.getMainLooper());
@Override @Override
protected void onCreate(final Bundle savedInstanceState) { protected void onCreate(final Bundle savedInstanceState) {
@@ -125,6 +135,10 @@ public class HomeComposeActivity extends BugleActionBarActivity
mCreateGroupButton = findViewById(R.id.home_compose_create_group); mCreateGroupButton = findViewById(R.id.home_compose_create_group);
mCreateGroupButton.setOnClickListener(v -> enterGroupMode()); 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() { private HomeContactsFragment getContactsFragment() {
@@ -132,6 +146,93 @@ public class HomeComposeActivity extends BugleActionBarActivity
.findFragmentById(R.id.home_compose_fragment_host); .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() { private void enterGroupMode() {
mGroupMode = true; mGroupMode = true;
ImeUtil.get().hideImeKeyboard(this, mRecipientField); ImeUtil.get().hideImeKeyboard(this, mRecipientField);
@@ -226,6 +327,12 @@ public class HomeComposeActivity extends BugleActionBarActivity
super.onBackPressed(); super.onBackPressed();
} }
@Override
protected void onDestroy() {
mAccountExecutor.shutdownNow();
super.onDestroy();
}
@Override @Override
public void finish() { public void finish() {
if (mRecipientField != null && !TextUtils.isEmpty(mRecipientField.getText())) { if (mRecipientField != null && !TextUtils.isEmpty(mRecipientField.getText())) {
@@ -20,6 +20,7 @@ import android.content.ContentUris;
import android.content.Context; import android.content.Context;
import android.database.Cursor; import android.database.Cursor;
import android.graphics.Color; import android.graphics.Color;
import android.graphics.Rect;
import android.graphics.drawable.ColorDrawable; import android.graphics.drawable.ColorDrawable;
import android.net.Uri; import android.net.Uri;
import android.os.Handler; import android.os.Handler;
@@ -44,6 +45,7 @@ import androidx.annotation.NonNull;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import androidx.core.content.ContextCompat; import androidx.core.content.ContextCompat;
import androidx.core.view.ViewCompat; import androidx.core.view.ViewCompat;
import androidx.core.view.WindowInsetsAnimationCompat;
import androidx.core.view.WindowInsetsCompat; import androidx.core.view.WindowInsetsCompat;
import androidx.recyclerview.widget.GridLayoutManager; import androidx.recyclerview.widget.GridLayoutManager;
import androidx.recyclerview.widget.RecyclerView; import androidx.recyclerview.widget.RecyclerView;
@@ -77,6 +79,8 @@ public final class HomeComposeAttachPanel {
void onTakePhoto(); void onTakePhoto();
void onRecordVideo(); void onRecordVideo();
void onOpenContact(); void onOpenContact();
void onShareLocation();
void onScheduleMessage();
void onRequestMediaPermission(); void onRequestMediaPermission();
void onComingSoon(); void onComingSoon();
void onDismiss(); void onDismiss();
@@ -247,6 +251,7 @@ public final class HomeComposeAttachPanel {
float peekY; float peekY;
int statusBarTop; int statusBarTop;
int navigationBarBottom; int navigationBarBottom;
int imeBottom;
final int dockBaseBottomPadding; final int dockBaseBottomPadding;
final int originalStatusBarColor; final int originalStatusBarColor;
boolean expanded; boolean expanded;
@@ -328,6 +333,12 @@ public final class HomeComposeAttachPanel {
if (send != null) { if (send != null) {
send.setOnClickListener(v -> sendSelection()); send.setOnClickListener(v -> sendSelection());
} }
overlay.getViewTreeObserver().addOnGlobalLayoutListener(() -> {
if (closing) {
return;
}
applyImeLift(inferredImeHeight());
});
if (caption != null) { if (caption != null) {
caption.setOnEditorActionListener((v, actionId, event) -> { caption.setOnEditorActionListener((v, actionId, event) -> {
if (actionId == android.view.inputmethod.EditorInfo.IME_ACTION_SEND) { if (actionId == android.view.inputmethod.EditorInfo.IME_ACTION_SEND) {
@@ -367,12 +378,21 @@ public final class HomeComposeAttachPanel {
overlay.setLayoutParams(new FrameLayout.LayoutParams( overlay.setLayoutParams(new FrameLayout.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)); ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT));
ViewCompat.setOnApplyWindowInsetsListener(overlay, (v, insets) -> { ViewCompat.setOnApplyWindowInsetsListener(overlay, (v, insets) -> {
statusBarTop = insets.getInsets(WindowInsetsCompat.Type.statusBars()).top; applyWindowInsets(insets);
navigationBarBottom = insets.getInsets(WindowInsetsCompat.Type.navigationBars()).bottom;
applyDockInsets();
applyExpandHeaderInsets(expanded);
return 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); ViewCompat.requestApplyInsets(overlay);
bindHandleDrag(handleArea, this); bindHandleDrag(handleArea, this);
bindHandleDrag(expandHeader, 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() { void applyDockInsets() {
if (dock != null) { if (dock != null) {
final int bottom = dockBaseBottomPadding + navigationBarBottom; final int bottom = dockBaseBottomPadding
+ (imeBottom > 0 ? 0 : navigationBarBottom);
if (dock.getPaddingBottom() != bottom) { if (dock.getPaddingBottom() != bottom) {
dock.setPaddingRelative( dock.setPaddingRelative(
dock.getPaddingStart(), 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() { void applyGridDockPadding() {
if (grid == null) { if (grid == null) {
return; return;
@@ -982,6 +1046,12 @@ public final class HomeComposeAttachPanel {
} else if (index == TAB_CONTACT) { } else if (index == TAB_CONTACT) {
host.onOpenContact(); host.onOpenContact();
dismissAnimated.run(); 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) { } else if (index != TAB_GALLERY) {
host.onComingSoon(); host.onComingSoon();
} }
@@ -6,24 +6,74 @@ import android.view.LayoutInflater;
import android.view.View; import android.view.View;
import android.view.ViewGroup; import android.view.ViewGroup;
import android.widget.CursorAdapter; import android.widget.CursorAdapter;
import android.widget.SectionIndexer;
import com.android.messaging.R; import com.android.messaging.R;
import com.android.messaging.ui.contact.ContactListItemView; import com.android.messaging.ui.contact.ContactListItemView;
import com.android.messaging.ui.contact.ContactSectionIndexer;
import com.android.messaging.util.Assert; import com.android.messaging.util.Assert;
public class HomeContactListAdapter extends CursorAdapter { public class HomeContactListAdapter extends CursorAdapter implements SectionIndexer {
private final ContactListItemView.HostInterface mHost; private final ContactListItemView.HostInterface mHost;
private ContactSectionIndexer mSectionIndexer;
private boolean mNeedAlphabetHeader = true;
public HomeContactListAdapter(final Context context, final Cursor cursor, public HomeContactListAdapter(final Context context, final Cursor cursor,
final ContactListItemView.HostInterface host) { final ContactListItemView.HostInterface host) {
super(context, cursor, 0); super(context, cursor, 0);
mHost = host; mHost = host;
mSectionIndexer = new ContactSectionIndexer(cursor);
}
public void setNeedAlphabetHeader(final boolean needAlphabetHeader) {
if (mNeedAlphabetHeader == needAlphabetHeader) {
return;
}
mNeedAlphabetHeader = needAlphabetHeader;
notifyDataSetChanged();
} }
@Override @Override
public void bindView(final View view, final Context context, final Cursor cursor) { public void bindView(final View view, final Context context, final Cursor cursor) {
Assert.isTrue(view instanceof ContactListItemView); 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 @Override
@@ -31,4 +81,25 @@ public class HomeContactListAdapter extends CursorAdapter {
return LayoutInflater.from(context).inflate(R.layout.home_contacts_list_item, parent, return LayoutInflater.from(context).inflate(R.layout.home_contacts_list_item, parent,
false); 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. */ /** Narrows the list as the host screen's recipient field is typed into. */
public void setFilterText(final String filterText) { public void setFilterText(final String filterText) {
mFilterText = filterText; mFilterText = filterText;
if (mAdapter != null) {
mAdapter.setNeedAlphabetHeader(TextUtils.isEmpty(filterText));
}
if (mBinding.isBound()) { if (mBinding.isBound()) {
mBinding.getData().setSearchText(mBinding, filterText); mBinding.getData().setSearchText(mBinding, filterText);
} }
updateResultViews(); 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. * Starts a conversation with the raw text currently typed, if it can be messaged.
* *
@@ -1,6 +1,5 @@
package com.android.messaging.ui.home; package com.android.messaging.ui.home;
import android.graphics.Rect;
import android.view.MotionEvent; import android.view.MotionEvent;
import android.view.View; import android.view.View;
import android.view.ViewGroup; import android.view.ViewGroup;
@@ -43,14 +42,20 @@ public final class HomeInputFocus {
if (isInside(box, ev)) { if (isInside(box, ev)) {
return false; return false;
} }
root.setFocusable(true); final View sink = focusSink(root);
root.setFocusableInTouchMode(true); sink.setFocusable(true);
sink.setFocusableInTouchMode(true);
sink.requestFocus();
focused.clearFocus(); focused.clearFocus();
root.requestFocus();
ImeUtil.get().hideImeKeyboard(focused.getContext(), focused); ImeUtil.get().hideImeKeyboard(focused.getContext(), focused);
return true; 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) { private static View findBox(final View focused) {
ViewParent parent = focused.getParent(); ViewParent parent = focused.getParent();
while (parent instanceof ViewGroup) { while (parent instanceof ViewGroup) {
@@ -64,11 +69,13 @@ public final class HomeInputFocus {
} }
private static boolean isInside(final View view, final MotionEvent ev) { private static boolean isInside(final View view, final MotionEvent ev) {
final Rect rect = new Rect(); final int[] loc = new int[2];
if (!view.getGlobalVisibleRect(rect)) { view.getLocationOnScreen(loc);
return false; final int x = Math.round(ev.getRawX());
} final int y = Math.round(ev.getRawY());
return rect.contains(Math.round(ev.getRawX()), 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 { 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.DrawableRes;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import androidx.appcompat.widget.SwitchCompat;
import com.android.messaging.R; 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 ImageView icon = row.findViewById(R.id.home_settings_row_icon);
final TextView titleView = row.findViewById(R.id.home_settings_row_title); final TextView titleView = row.findViewById(R.id.home_settings_row_title);
final TextView subtitleView = row.findViewById(R.id.home_settings_row_subtitle); 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); final View chevron = row.findViewById(R.id.home_settings_row_chevron);
icon.setImageResource(iconRes); icon.setImageResource(iconRes);
@@ -48,14 +47,16 @@ public final class HomeSettingsRow {
chevron.setVisibility(showChevron ? View.VISIBLE : View.GONE); chevron.setVisibility(showChevron ? View.VISIBLE : View.GONE);
} }
if (toggle != null) { if (toggle != null) {
toggle.setOnCheckedChangeListener(null);
toggle.setVisibility(showSwitch ? View.VISIBLE : View.GONE); toggle.setVisibility(showSwitch ? View.VISIBLE : View.GONE);
toggle.setChecked(switchOn); toggle.setSelected(switchOn);
toggle.setOnCheckedChangeListener(switchListener);
} }
row.setOnClickListener(v -> { row.setOnClickListener(v -> {
if (showSwitch && toggle != null) { if (showSwitch && toggle != null) {
toggle.toggle(); final boolean checked = !toggle.isSelected();
toggle.setSelected(checked);
if (switchListener != null) {
switchListener.onCheckedChanged(null, checked);
}
} }
if (clickListener != null) { if (clickListener != null) {
clickListener.onClick(v); clickListener.onClick(v);
@@ -98,7 +99,7 @@ public final class HomeSettingsRow {
public static void setEnabled(@NonNull final View row, final boolean enabled) { public static void setEnabled(@NonNull final View row, final boolean enabled) {
row.setEnabled(enabled); row.setEnabled(enabled);
row.setAlpha(enabled ? 1f : 0.4f); 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) { if (toggle != null) {
toggle.setEnabled(enabled); toggle.setEnabled(enabled);
} }
@@ -18,6 +18,8 @@
package com.android.messaging.util; package com.android.messaging.util;
import android.Manifest; import android.Manifest;
import android.accounts.AccountManager;
import android.accounts.AuthenticatorDescription;
import android.app.Activity; import android.app.Activity;
import android.content.ContentUris; import android.content.ContentUris;
import android.content.Context; import android.content.Context;
@@ -33,11 +35,18 @@ import android.provider.ContactsContract.Directory;
import android.provider.ContactsContract.DisplayNameSources; import android.provider.ContactsContract.DisplayNameSources;
import android.provider.ContactsContract.PhoneLookup; import android.provider.ContactsContract.PhoneLookup;
import android.provider.ContactsContract.Profile; import android.provider.ContactsContract.Profile;
import android.provider.ContactsContract.RawContacts;
import android.text.TextUtils; import android.text.TextUtils;
import android.content.ActivityNotFoundException; import android.content.ActivityNotFoundException;
import android.content.Intent; import android.content.Intent;
import android.view.View; 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.ex.chips.RecipientEntry;
import com.android.messaging.datamodel.CursorQueryData; import com.android.messaging.datamodel.CursorQueryData;
import com.android.messaging.datamodel.FrequentContactsCursorQueryData; 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. * In that case, each phone will be returned as a separate record in the result cursor.
*/ */
public static CursorQueryData getPhones(final Context context) { 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()) { if (!ContactUtil.hasReadContactsPermission()) {
return CursorQueryData.getEmptyQueryData(); return CursorQueryData.getEmptyQueryData();
} }
@@ -373,8 +389,173 @@ public class ContactUtil {
.appendQueryParameter(Contacts.EXTRA_ADDRESS_BOOK_INDEX, "true") .appendQueryParameter(Contacts.EXTRA_ADDRESS_BOOK_INDEX, "true")
.build(); .build();
return new CursorQueryData(context, uri, PhoneQuery.PROJECTION, null, null, return new CursorQueryData(context, uri, PhoneQuery.PROJECTION,
PhoneQuery.SORT_KEY); 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, public static CursorQueryData filterDestination(final Context context,
final String destination) { final String destination) {
return filterDestination(context, destination, null);
}
public static CursorQueryData filterDestination(final Context context,
final String destination, final AccountFilter filter) {
if (shouldFilterForEmail(destination)) { if (shouldFilterForEmail(destination)) {
return ContactUtil.filterEmails(context, destination); return ContactUtil.filterEmails(context, destination, filter);
} else { } 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. * we support email addresses as well.
*/ */
public static CursorQueryData filterPhones(final Context context, final String query) { 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, public static CursorQueryData filterPhonesEnterprise(final Context context,
final String query) { final String query) {
return filterPhonesInternal(context, Phone.ENTERPRISE_CONTENT_FILTER_URI, query, return filterPhonesInternal(context, Phone.ENTERPRISE_CONTENT_FILTER_URI, query,
Directory.ENTERPRISE_DEFAULT); Directory.ENTERPRISE_DEFAULT, null);
} }
private static CursorQueryData filterPhonesInternal(final Context context, 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()) { if (!ContactUtil.hasReadContactsPermission()) {
return CursorQueryData.getEmptyQueryData(); return CursorQueryData.getEmptyQueryData();
} }
Uri phoneFilterUri = buildDirectorySearchUri(phoneFilterBaseUri, query, directoryId); Uri phoneFilterUri = buildDirectorySearchUri(phoneFilterBaseUri, query, directoryId);
return new CursorQueryData(context, return new CursorQueryData(context,
phoneFilterUri, phoneFilterUri,
PhoneQuery.PROJECTION, null, null, PhoneQuery.PROJECTION, accountSelection(filter), accountSelectionArgs(filter),
PhoneQuery.SORT_KEY); PhoneQuery.SORT_KEY);
} }
/** /**
@@ -492,7 +685,13 @@ public class ContactUtil {
* we support email addresses as well. * we support email addresses as well.
*/ */
public static CursorQueryData filterEmails(final Context context, final String query) { 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, public static CursorQueryData filterEmailsEnterprise(final Context context,
final String query) { final String query) {
return filterEmailsInternal(context, Email.ENTERPRISE_CONTENT_FILTER_URI, query, return filterEmailsInternal(context, Email.ENTERPRISE_CONTENT_FILTER_URI, query,
Directory.ENTERPRISE_DEFAULT); Directory.ENTERPRISE_DEFAULT, null);
} }
private static CursorQueryData filterEmailsInternal(final Context context, 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()) { if (!ContactUtil.hasReadContactsPermission()) {
return CursorQueryData.getEmptyQueryData(); return CursorQueryData.getEmptyQueryData();
} }
@@ -513,7 +713,7 @@ public class ContactUtil {
directoryId); directoryId);
return new CursorQueryData(context, return new CursorQueryData(context,
filterEmailsUri, filterEmailsUri,
PhoneQuery.PROJECTION, null, null, PhoneQuery.PROJECTION, accountSelection(filter), accountSelectionArgs(filter),
PhoneQuery.SORT_KEY); PhoneQuery.SORT_KEY);
} }
@@ -116,7 +116,15 @@ public class OsUtil {
} }
public static boolean hasLocationPermission() { 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() { 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 --> <!-- Google Messages–style compose row: pill (+ | text | emoji | gallery) + voice/send -->
<LinearLayout <LinearLayout
@@ -15,9 +15,16 @@
See the License for the specific language governing permissions and See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
--> -->
<LinearLayout <FrameLayout
xmlns:android="http://schemas.android.com/apk/res/android" xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:app="http://schemas.android.com/apk/res-auto"
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_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
android:background="@color/home_bg" android:background="@color/home_bg"
@@ -197,14 +204,23 @@
app:srcCompat="@drawable/ic_checkmark_circle_primary" /> app:srcCompat="@drawable/ic_checkmark_circle_primary" />
</FrameLayout> </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 <com.android.messaging.ui.home.HomeCategoryStrip
android:id="@+id/home_category_strip" android:id="@+id/home_category_strip"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_gravity="top"
android:paddingStart="@dimen/looply_screen_padding" android:paddingStart="@dimen/looply_screen_padding"
android:paddingTop="@dimen/looply_space_md" android:paddingTop="@dimen/looply_space_sm"
android:paddingEnd="12dp" android:paddingEnd="12dp"
android:paddingBottom="@dimen/looply_space_md"> android:paddingBottom="2dp">
<FrameLayout <FrameLayout
android:id="@+id/home_category_all" android:id="@+id/home_category_all"
@@ -263,56 +279,20 @@
android:layout_height="32dp" /> android:layout_height="32dp" />
</com.android.messaging.ui.home.HomeCategoryStrip> </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 <LinearLayout
android:id="@+id/home_search_bar_container" android:id="@+id/home_search_bar_container"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_gravity="top" android:layout_gravity="top"
android:background="@drawable/home_bg_search_bar_outer" android:background="@color/home_bg"
android:clipChildren="false" android:clipChildren="false"
android:clipToPadding="false" android:clipToPadding="false"
android:elevation="0dp"
android:orientation="vertical" android:orientation="vertical"
android:paddingStart="12dp" android:paddingStart="12dp"
android:paddingTop="6dp" android:paddingTop="4dp"
android:paddingEnd="12dp" android:paddingEnd="12dp"
android:paddingBottom="10dp" android:paddingBottom="4dp"
android:visibility="gone"> android:visibility="invisible">
<LinearLayout <LinearLayout
android:id="@+id/home_search_bar" android:id="@+id/home_search_bar"
@@ -355,6 +335,64 @@
</LinearLayout> </LinearLayout>
</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 <LinearLayout
android:id="@+id/home_selection_bar" android:id="@+id/home_selection_bar"
android:layout_width="match_parent" android:layout_width="match_parent"
@@ -525,3 +563,9 @@
</FrameLayout> </FrameLayout>
</LinearLayout> </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:minHeight="72dp"
android:paddingStart="@dimen/looply_space_md" android:paddingStart="@dimen/looply_space_md"
android:paddingEnd="@dimen/looply_space_lg" 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:paddingBottom="@dimen/looply_space_md"
android:baselineAligned="false"> android:baselineAligned="false">
@@ -127,72 +127,79 @@
android:visibility="gone" android:visibility="gone"
android:importantForAccessibility="no" /> android:importantForAccessibility="no" />
<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" >
<TextView
android:id="@+id/message_sender_name"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginRight="4dp"
style="@style/ConversationMessageStatus"
android:visibility="gone" />
<TextView
android:id="@+id/message_status"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
style="@style/ConversationMessageStatus"
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"
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> </com.android.messaging.ui.conversation.MessageBubbleBackground>
<LinearLayout <com.android.messaging.ui.LineWrapLayout
android:id="@+id/message_send_error" android:id="@+id/message_metadata"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginTop="6dp"
android:gravity="center_vertical" android:gravity="center_vertical"
android:orientation="horizontal" android:importantForAccessibility="noHideDescendants"
android:visibility="gone"> android:orientation="horizontal">
<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 <TextView
android:id="@+id/message_send_error_text" android:id="@+id/message_sender_name"
style="@style/ConversationMessageStatus"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginStart="4dp" android:layout_marginEnd="4dp"
android:includeFontPadding="false" android:includeFontPadding="false"
android:textColor="@color/home_destructive" android:visibility="gone" />
android:textSize="12sp" />
</LinearLayout> <TextView
android:id="@+id/message_status"
style="@style/ConversationMessageStatus"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
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:visibility="gone" />
<ImageView
android:id="@+id/smsDeliveredBadge"
style="@style/SmsDeliverdBadge"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:src="@drawable/ic_sms_delivery_ok"
android:visibility="gone" />
</com.android.messaging.ui.LineWrapLayout>
</com.android.messaging.ui.conversation.ConversationMessageBubbleView> </com.android.messaging.ui.conversation.ConversationMessageBubbleView>
@@ -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_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
android:background="@color/home_sheet_scrim" android:background="@color/home_sheet_scrim"
android:fitsSystemWindows="true"> android:fitsSystemWindows="true"
android:focusable="true"
android:focusableInTouchMode="true">
<View <View
android:id="@+id/home_compose_scrim" android:id="@+id/home_compose_scrim"
@@ -94,46 +96,84 @@
</LinearLayout> </LinearLayout>
<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:clickable="true"
android:focusable="true"
android:gravity="center"
android:orientation="horizontal">
<ImageView
android:layout_width="22dp"
android:layout_height="22dp"
android:contentDescription="@null"
android:scaleType="fitCenter"
android:src="@drawable/ic_home_group_add"
android:tint="@color/home_text_primary" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/looply_space_sm"
android:includeFontPadding="false"
android:text="@string/home_compose_create_group"
android:textColor="@color/home_text_primary"
android:textSize="15sp" />
</LinearLayout>
<TextView
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:paddingStart="@dimen/looply_space_lg" android:gravity="center_vertical"
android:paddingTop="@dimen/looply_space_lg" android:orientation="horizontal"
android:paddingEnd="@dimen/looply_space_lg" android:paddingStart="@dimen/looply_screen_padding"
android:paddingBottom="@dimen/looply_space_xs" android:paddingTop="@dimen/looply_space_md"
android:text="@string/home_compose_contacts_header" android:paddingEnd="@dimen/looply_screen_padding"
android:textColor="@color/home_text_secondary" android:paddingBottom="@dimen/looply_space_xs">
android:textSize="13sp" />
<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_vertical"
android:paddingStart="12dp"
android:paddingEnd="10dp">
<ImageView
android:layout_width="18dp"
android:layout_height="18dp"
android:contentDescription="@null"
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="6dp"
android:includeFontPadding="false"
android:text="@string/home_compose_create_group"
android:textColor="@color/home_accent"
android:textSize="14sp" />
</LinearLayout>
</LinearLayout>
<FrameLayout <FrameLayout
android:layout_width="match_parent" android:layout_width="match_parent"
@@ -24,6 +24,8 @@
android:clipToPadding="false" android:clipToPadding="false"
android:divider="@null" android:divider="@null"
android:dividerHeight="0dp" android:dividerHeight="0dp"
android:paddingStart="@dimen/looply_screen_padding"
android:paddingEnd="@dimen/looply_screen_padding"
android:paddingBottom="24dp" android:paddingBottom="24dp"
android:scrollbarStyle="outsideOverlay" /> android:scrollbarStyle="outsideOverlay" />
@@ -4,79 +4,118 @@
xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:background="?android:attr/selectableItemBackground" android:orientation="vertical">
android:gravity="center_vertical"
android:minHeight="56dp"
android:orientation="horizontal"
android:paddingStart="16dp"
android:paddingTop="8dp"
android:paddingEnd="16dp"
android:paddingBottom="8dp">
<TextView <View
android:id="@+id/alphabet_header" android:id="@+id/contact_section_gap_top"
android:layout_width="0dp" android:layout_width="match_parent"
android:layout_height="0dp" android:layout_height="@dimen/looply_space_sm"
android:visibility="gone" />
<FrameLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content">
<com.android.messaging.ui.ContactIconView
android:id="@+id/contact_icon"
android:layout_width="@dimen/looply_avatar_size"
android:layout_height="@dimen/looply_avatar_size"
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"
android:layout_height="@dimen/looply_avatar_size"
android:contentDescription="@null"
android:src="@drawable/home_bg_contact_selected"
android:visibility="gone" />
</FrameLayout>
<ImageView
android:id="@+id/work_profile_icon"
android:layout_width="0dp"
android:layout_height="0dp"
android:visibility="gone" /> android:visibility="gone" />
<LinearLayout <LinearLayout
android:layout_width="0dp" android:id="@+id/contact_section_card"
android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginStart="12dp"
android:layout_weight="1"
android:orientation="vertical"> android:orientation="vertical">
<TextView <TextView
android:id="@+id/contact_name" android:id="@+id/alphabet_header"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:ellipsize="end" android:includeFontPadding="false"
android:maxLines="1" android:paddingStart="16dp"
android:textColor="@color/home_text_primary" android:paddingTop="14dp"
android:textSize="16sp" android:paddingEnd="16dp"
android:textStyle="bold" /> android:paddingBottom="2dp"
<TextView
android:id="@+id/contact_details"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="2dp"
android:ellipsize="end"
android:maxLines="1"
android:textColor="@color/home_text_secondary" android:textColor="@color/home_text_secondary"
android:textSize="14sp" /> android:textSize="14sp"
android:textStyle="bold"
android:visibility="gone" />
<TextView <LinearLayout
android:id="@+id/contact_detail_type" android:layout_width="match_parent"
android:layout_width="0dp" android:layout_height="wrap_content"
android:layout_height="0dp" android:gravity="center_vertical"
android:minHeight="56dp"
android:orientation="horizontal"
android:paddingStart="16dp"
android:paddingTop="8dp"
android:paddingEnd="16dp"
android:paddingBottom="8dp">
<FrameLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content">
<com.android.messaging.ui.ContactIconView
android:id="@+id/contact_icon"
android:layout_width="@dimen/looply_avatar_size"
android:layout_height="@dimen/looply_avatar_size"
android:contentDescription="@null"
app:contactIconSize="normal" />
<ImageView
android:id="@+id/contact_checkmark"
android:layout_width="@dimen/looply_avatar_size"
android:layout_height="@dimen/looply_avatar_size"
android:contentDescription="@null"
android:src="@drawable/home_bg_contact_selected"
android:visibility="gone" />
</FrameLayout>
<ImageView
android:id="@+id/work_profile_icon"
android:layout_width="0dp"
android:layout_height="0dp"
android:visibility="gone" />
<LinearLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="12dp"
android:layout_weight="1"
android:orientation="vertical">
<TextView
android:id="@+id/contact_name"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:ellipsize="end"
android:maxLines="1"
android:textColor="@color/home_text_primary"
android:textSize="16sp"
android:textStyle="bold" />
<TextView
android:id="@+id/contact_details"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="2dp"
android:ellipsize="end"
android:maxLines="1"
android:textColor="@color/home_text_secondary"
android:textSize="14sp" />
<TextView
android:id="@+id/contact_detail_type"
android:layout_width="0dp"
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" /> android:visibility="gone" />
</LinearLayout> </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> </com.android.messaging.ui.contact.ContactListItemView>
@@ -5,6 +5,8 @@
android:layout_height="match_parent" android:layout_height="match_parent"
android:background="@color/home_bg" android:background="@color/home_bg"
android:fitsSystemWindows="true" android:fitsSystemWindows="true"
android:focusable="true"
android:focusableInTouchMode="true"
android:orientation="vertical"> android:orientation="vertical">
<include layout="@layout/home_sub_header" /> <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_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
android:clickable="true" android:clickable="true"
android:elevation="24dp"
android:focusable="true" android:focusable="true"
android:visibility="gone"> android:visibility="gone">
@@ -21,12 +22,12 @@
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
android:contentDescription="@null" android:contentDescription="@null"
android:scaleType="centerCrop" /> android:scaleType="fitXY" />
<View <View
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
android:background="#4D1B1528" android:background="#801B1528"
android:importantForAccessibility="no" /> android:importantForAccessibility="no" />
</FrameLayout> </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:layout_marginBottom="4dp"
android:background="@color/home_divider" /> 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 <LinearLayout
android:id="@+id/popover_settings" android:id="@+id/popover_settings"
style="@style/Home.PopoverItem" 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:layout_height="match_parent"
android:background="@color/home_bg" android:background="@color/home_bg"
android:fitsSystemWindows="true" android:fitsSystemWindows="true"
android:focusable="true"
android:focusableInTouchMode="true"
android:orientation="vertical"> android:orientation="vertical">
<include layout="@layout/home_sub_header" /> <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