Compare commits
10
Commits
349eeb7025
..
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0b2f7dfdb5 | ||
|
|
a34cccdf76 | ||
|
|
49789fc4a5 | ||
|
|
a6b23f0f3c | ||
|
|
47d735be2c | ||
|
|
74fb80f783 | ||
|
|
42148360e0 | ||
|
|
a32250fe34 | ||
|
|
88529c3634 | ||
|
|
8e5d88183c |
@@ -2,6 +2,10 @@
|
||||
.cursor/
|
||||
.gradle/
|
||||
/local.properties
|
||||
/keystore.properties
|
||||
/keystore/
|
||||
*.jks
|
||||
*.keystore
|
||||
/.idea/
|
||||
.DS_Store
|
||||
/build/
|
||||
@@ -11,6 +15,7 @@
|
||||
app/build/
|
||||
modules/*/build/
|
||||
modules/*/.git/
|
||||
modules/*/sample/
|
||||
*.apk
|
||||
*.ap_
|
||||
*.dex
|
||||
|
||||
+5
-3
@@ -87,11 +87,13 @@ android_app {
|
||||
"1.0.001",
|
||||
"--version-code",
|
||||
"10001040",
|
||||
"--rename-manifest-package",
|
||||
"com.bnkode.notie",
|
||||
],
|
||||
required: [
|
||||
"libframesequence",
|
||||
"libgiftranscode",
|
||||
"com.android.messaging.allowlist",
|
||||
"com.bnkode.notie.allowlist",
|
||||
],
|
||||
optimize: {
|
||||
obfuscate: true,
|
||||
@@ -105,9 +107,9 @@ android_app {
|
||||
}
|
||||
|
||||
prebuilt_etc {
|
||||
name: "com.android.messaging.allowlist",
|
||||
name: "com.bnkode.notie.allowlist",
|
||||
product_specific: true,
|
||||
sub_dir: "sysconfig",
|
||||
src: "com.android.messaging.allowlist.xml",
|
||||
src: "com.bnkode.notie.allowlist.xml",
|
||||
filename_from_src: true,
|
||||
}
|
||||
|
||||
+23
-2
@@ -2,17 +2,24 @@ plugins {
|
||||
id 'com.android.application'
|
||||
}
|
||||
|
||||
def keystoreProperties = new Properties()
|
||||
def keystorePropertiesFile = rootProject.file('keystore.properties')
|
||||
if (keystorePropertiesFile.exists()) {
|
||||
keystoreProperties.load(new FileInputStream(keystorePropertiesFile))
|
||||
}
|
||||
|
||||
android {
|
||||
namespace 'com.android.messaging'
|
||||
compileSdk 35
|
||||
|
||||
defaultConfig {
|
||||
applicationId 'com.android.messaging'
|
||||
applicationId 'com.bnkode.notie'
|
||||
minSdk 33
|
||||
targetSdk 34
|
||||
versionCode 10001040
|
||||
versionName '1.0.001'
|
||||
vectorDrawables.useSupportLibrary true
|
||||
resourceConfigurations += ['en', 'ko']
|
||||
}
|
||||
|
||||
compileOptions {
|
||||
@@ -20,9 +27,22 @@ android {
|
||||
targetCompatibility JavaVersion.VERSION_17
|
||||
}
|
||||
|
||||
signingConfigs {
|
||||
release {
|
||||
if (keystorePropertiesFile.exists()) {
|
||||
storeFile rootProject.file(keystoreProperties['storeFile'])
|
||||
storePassword keystoreProperties['storePassword']
|
||||
keyAlias keystoreProperties['keyAlias']
|
||||
keyPassword keystoreProperties['keyPassword']
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
release {
|
||||
minifyEnabled true
|
||||
shrinkResources true
|
||||
signingConfig signingConfigs.release
|
||||
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'),
|
||||
rootProject.file('proguard-release.flags'),
|
||||
rootProject.file('proguard.flags')
|
||||
@@ -59,6 +79,7 @@ dependencies {
|
||||
implementation 'androidx.core:core:1.15.0'
|
||||
implementation 'androidx.exifinterface:exifinterface:1.3.7'
|
||||
implementation 'androidx.fragment:fragment:1.8.5'
|
||||
implementation 'androidx.lifecycle:lifecycle-process:2.8.7'
|
||||
implementation 'androidx.media:media:1.7.0'
|
||||
implementation 'androidx.palette:palette:1.0.0'
|
||||
implementation 'androidx.preference:preference:1.2.1'
|
||||
@@ -85,5 +106,5 @@ tasks.register('launchMessaging', Exec) {
|
||||
description = "Launch Messaging on ${messagingAvdSerial}"
|
||||
dependsOn 'installMessaging'
|
||||
commandLine adbExecutable.absolutePath, '-s', messagingAvdSerial, 'shell', 'am', 'start',
|
||||
'-n', 'com.android.messaging/.ui.conversationlist.ConversationListActivity'
|
||||
'-n', 'com.bnkode.notie/com.android.messaging.ui.conversationlist.ConversationListActivity'
|
||||
}
|
||||
|
||||
@@ -44,6 +44,12 @@
|
||||
<uses-permission android:name="android.permission.DOWNLOAD_WITHOUT_NOTIFICATION" />
|
||||
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
|
||||
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
||||
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES" />
|
||||
<uses-permission android:name="android.permission.READ_MEDIA_VIDEO" />
|
||||
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
|
||||
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
|
||||
<uses-permission android:name="android.permission.SCHEDULE_EXACT_ALARM" />
|
||||
<uses-permission android:name="android.permission.USE_EXACT_ALARM" />
|
||||
|
||||
<!-- Optional features -->
|
||||
<uses-feature
|
||||
@@ -61,9 +67,15 @@
|
||||
<uses-feature
|
||||
android:name="android.hardware.screen.portrait"
|
||||
android:required="false" />
|
||||
<uses-feature
|
||||
android:name="android.hardware.telephony"
|
||||
android:required="false" />
|
||||
<uses-feature
|
||||
android:name="android.hardware.telephony"
|
||||
android:required="false" />
|
||||
<uses-feature
|
||||
android:name="android.hardware.location"
|
||||
android:required="false" />
|
||||
<uses-feature
|
||||
android:name="android.hardware.location.gps"
|
||||
android:required="false" />
|
||||
|
||||
<queries>
|
||||
<intent>
|
||||
@@ -79,7 +91,9 @@
|
||||
android:allowBackup="false"
|
||||
android:appCategory="social"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:roundIcon="@mipmap/ic_launcher"
|
||||
android:label="@string/app_name"
|
||||
android:localeConfig="@xml/locales_config"
|
||||
android:requestLegacyExternalStorage="true"
|
||||
android:supportsRtl="true"
|
||||
android:theme="@style/BugleTheme">
|
||||
@@ -132,10 +146,18 @@
|
||||
<activity
|
||||
android:name=".ui.conversationlist.ArchivedConversationListActivity"
|
||||
android:configChanges="orientation|screenSize|keyboardHidden"
|
||||
android:label="@string/archived_activity_title"
|
||||
android:label="@string/home_archived_title"
|
||||
android:parentActivityName="com.android.messaging.ui.conversationlist.ConversationListActivity"
|
||||
android:screenOrientation="user"
|
||||
android:theme="@style/BugleTheme.ArchivedConversationListActivity" />
|
||||
android:theme="@style/Home.SubScreen" />
|
||||
|
||||
<activity
|
||||
android:name=".ui.conversationlist.RecentlyDeletedConversationListActivity"
|
||||
android:configChanges="orientation|screenSize|keyboardHidden"
|
||||
android:label="@string/home_recently_deleted_title"
|
||||
android:parentActivityName="com.android.messaging.ui.conversationlist.ConversationListActivity"
|
||||
android:screenOrientation="user"
|
||||
android:theme="@style/Home.SubScreen" />
|
||||
|
||||
<!-- Displays the contents of a single conversation -->
|
||||
<activity
|
||||
@@ -155,6 +177,81 @@
|
||||
android:screenOrientation="user"
|
||||
android:theme="@style/BugleTheme" />
|
||||
|
||||
<activity
|
||||
android:name=".ui.home.HomeSpamActivity"
|
||||
android:configChanges="orientation|screenSize|keyboardHidden"
|
||||
android:label="@string/home_spam_title"
|
||||
android:parentActivityName="com.android.messaging.ui.conversationlist.ConversationListActivity"
|
||||
android:screenOrientation="user"
|
||||
android:theme="@style/Home.SubScreen" />
|
||||
|
||||
<activity
|
||||
android:name=".ui.home.HomeAutomationActivity"
|
||||
android:configChanges="orientation|screenSize|keyboardHidden"
|
||||
android:label="@string/home_automation_title"
|
||||
android:parentActivityName="com.android.messaging.ui.conversationlist.ConversationListActivity"
|
||||
android:screenOrientation="user"
|
||||
android:theme="@style/Home.SubScreen" />
|
||||
|
||||
<activity
|
||||
android:name=".ui.home.HomeAutomationCreateActivity"
|
||||
android:configChanges="orientation|screenSize|keyboardHidden"
|
||||
android:label="@string/home_automation_create_title"
|
||||
android:parentActivityName="com.android.messaging.ui.conversationlist.ConversationListActivity"
|
||||
android:theme="@style/Home.BottomSheet"
|
||||
android:windowSoftInputMode="adjustResize|stateHidden" />
|
||||
|
||||
<activity
|
||||
android:name=".ui.home.HomeSearchActivity"
|
||||
android:configChanges="orientation|screenSize|keyboardHidden"
|
||||
android:label="@string/home_search_field_hint"
|
||||
android:parentActivityName="com.android.messaging.ui.conversationlist.ConversationListActivity"
|
||||
android:screenOrientation="user"
|
||||
android:theme="@style/Home.SubScreen"
|
||||
android:windowSoftInputMode="adjustResize|stateVisible" />
|
||||
|
||||
<activity
|
||||
android:name=".ui.home.HomeComposeActivity"
|
||||
android:configChanges="orientation|screenSize|keyboardHidden"
|
||||
android:label="@string/home_compose_title"
|
||||
android:parentActivityName="com.android.messaging.ui.conversationlist.ConversationListActivity"
|
||||
android:theme="@style/Home.BottomSheet"
|
||||
android:windowSoftInputMode="adjustResize|stateHidden" />
|
||||
|
||||
<activity
|
||||
android:name=".ui.home.HomeGroupNameActivity"
|
||||
android:configChanges="orientation|screenSize|keyboardHidden"
|
||||
android:label="@string/home_group_name_title"
|
||||
android:parentActivityName="com.android.messaging.ui.conversationlist.ConversationListActivity"
|
||||
android:theme="@style/Home.BottomSheet"
|
||||
android:windowSoftInputMode="adjustResize|stateHidden" />
|
||||
|
||||
<activity
|
||||
android:name=".ui.home.HomeLocationActivity"
|
||||
android:configChanges="orientation|screenSize|keyboardHidden"
|
||||
android:label="@string/home_location_title"
|
||||
android:parentActivityName="com.android.messaging.ui.conversationlist.ConversationListActivity"
|
||||
android:screenOrientation="user"
|
||||
android:theme="@style/Home.SubScreen" />
|
||||
|
||||
<activity
|
||||
android:name=".ui.home.HomeSecuritySettingsActivity"
|
||||
android:configChanges="orientation|screenSize|keyboardHidden"
|
||||
android:label="@string/home_security_title"
|
||||
android:parentActivityName="com.android.messaging.ui.appsettings.ApplicationSettingsActivity"
|
||||
android:screenOrientation="user"
|
||||
android:theme="@style/Home.SubScreen" />
|
||||
|
||||
<activity
|
||||
android:name=".ui.home.HomeAppLockActivity"
|
||||
android:configChanges="orientation|screenSize|keyboardHidden"
|
||||
android:excludeFromRecents="true"
|
||||
android:label="@string/home_settings_app_lock"
|
||||
android:launchMode="singleTop"
|
||||
android:screenOrientation="user"
|
||||
android:theme="@style/Home.SubScreen"
|
||||
android:windowSoftInputMode="adjustResize" />
|
||||
|
||||
<!-- Full-screen photo viewer -->
|
||||
<activity
|
||||
android:name=".ui.photoviewer.BuglePhotoViewActivity"
|
||||
@@ -171,7 +268,7 @@
|
||||
android:label="@string/settings_activity_title"
|
||||
android:parentActivityName="com.android.messaging.ui.conversationlist.ConversationListActivity"
|
||||
android:screenOrientation="user"
|
||||
android:theme="@style/BugleTheme.SettingsActivity" />
|
||||
android:theme="@style/Home.SubScreen" />
|
||||
|
||||
<activity
|
||||
android:name=".ui.appsettings.PerSubscriptionSettingsActivity"
|
||||
@@ -179,7 +276,7 @@
|
||||
android:label="@string/advanced_settings_activity_title"
|
||||
android:parentActivityName="com.android.messaging.ui.appsettings.SettingsActivity"
|
||||
android:screenOrientation="user"
|
||||
android:theme="@style/BugleTheme.SettingsActivity" />
|
||||
android:theme="@style/Home.SubScreen" />
|
||||
|
||||
<activity
|
||||
android:name=".ui.appsettings.ApplicationSettingsActivity"
|
||||
@@ -188,7 +285,7 @@
|
||||
android:label="@string/general_settings_activity_title"
|
||||
android:parentActivityName="com.android.messaging.ui.appsettings.SettingsActivity"
|
||||
android:screenOrientation="user"
|
||||
android:theme="@style/BugleTheme.SettingsActivity">
|
||||
android:theme="@style/Home.SubScreen">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
|
||||
@@ -223,22 +320,40 @@
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<!-- People & Options -->
|
||||
<!-- Conversation profile -->
|
||||
<activity
|
||||
android:name=".ui.conversationsettings.PeopleAndOptionsActivity"
|
||||
android:name=".ui.home.HomeProfileActivity"
|
||||
android:configChanges="orientation|screenSize|keyboardHidden"
|
||||
android:label="@string/people_and_options_activity_title"
|
||||
android:label="@string/home_profile_title"
|
||||
android:parentActivityName="com.android.messaging.ui.conversation.ConversationActivity"
|
||||
android:screenOrientation="user"
|
||||
android:theme="@style/BugleTheme" />
|
||||
android:theme="@style/Home.SubScreen"
|
||||
android:windowSoftInputMode="stateAlwaysHidden|adjustResize" />
|
||||
|
||||
<!-- Conversation search -->
|
||||
<activity
|
||||
android:name=".ui.home.HomeConversationSearchActivity"
|
||||
android:configChanges="orientation|screenSize|keyboardHidden"
|
||||
android:label="@string/home_conversation_search_hint"
|
||||
android:parentActivityName="com.android.messaging.ui.conversation.ConversationActivity"
|
||||
android:screenOrientation="user"
|
||||
android:theme="@style/Home.SubScreen"
|
||||
android:windowSoftInputMode="adjustResize|stateVisible" />
|
||||
|
||||
<activity
|
||||
android:name=".ui.home.HomeMessageDetailsActivity"
|
||||
android:configChanges="orientation|screenSize|keyboardHidden"
|
||||
android:label="@string/home_message_details_title"
|
||||
android:parentActivityName="com.android.messaging.ui.conversation.ConversationActivity"
|
||||
android:screenOrientation="user"
|
||||
android:theme="@style/Home.SubScreen" />
|
||||
|
||||
<!-- License -->
|
||||
<activity
|
||||
android:name=".ui.LicenseActivity"
|
||||
android:exported="true"
|
||||
android:label="@string/menu_license"
|
||||
android:theme="@android:style/Theme.Holo.Light.Dialog">
|
||||
</activity>
|
||||
android:label="@string/home_settings_licenses"
|
||||
android:theme="@style/Home.SubScreen" />
|
||||
|
||||
<!-- Message Forwarding -->
|
||||
<activity
|
||||
@@ -259,7 +374,8 @@
|
||||
android:configChanges="orientation|screenSize|keyboardHidden"
|
||||
android:exported="false"
|
||||
android:screenOrientation="user"
|
||||
android:theme="@style/Invisible">
|
||||
android:theme="@style/Home.TransparentSheetHost"
|
||||
android:windowSoftInputMode="adjustResize">
|
||||
</activity>
|
||||
|
||||
<!-- VCard details -->
|
||||
@@ -282,20 +398,20 @@
|
||||
|
||||
<provider
|
||||
android:name=".datamodel.MessagingContentProvider"
|
||||
android:authorities="com.android.messaging.datamodel.MessagingContentProvider"
|
||||
android:authorities="com.bnkode.notie.datamodel.MessagingContentProvider"
|
||||
android:exported="false"
|
||||
android:label="@string/app_name">
|
||||
</provider>
|
||||
|
||||
<provider
|
||||
android:name=".datamodel.MmsFileProvider"
|
||||
android:authorities="com.android.messaging.datamodel.MmsFileProvider"
|
||||
android:authorities="com.bnkode.notie.datamodel.MmsFileProvider"
|
||||
android:exported="false"
|
||||
android:grantUriPermissions="true" />
|
||||
|
||||
<provider
|
||||
android:name=".datamodel.MediaScratchFileProvider"
|
||||
android:authorities="com.android.messaging.datamodel.MediaScratchFileProvider"
|
||||
android:authorities="com.bnkode.notie.datamodel.MediaScratchFileProvider"
|
||||
android:exported="false"
|
||||
android:grantUriPermissions="true" />
|
||||
|
||||
@@ -351,18 +467,18 @@
|
||||
android:name=".receiver.SendStatusReceiver"
|
||||
android:exported="false">
|
||||
<intent-filter>
|
||||
<action android:name="com.android.messaging.receiver.SendStatusReceiver.MESSAGE_SENT" />
|
||||
<action android:name="com.bnkode.notie.receiver.SendStatusReceiver.MESSAGE_SENT" />
|
||||
<data android:scheme="content" />
|
||||
</intent-filter>
|
||||
<intent-filter>
|
||||
<action android:name="com.android.messaging.receiver.SendStatusReceiver.MESSAGE_DELIVERED" />
|
||||
<action android:name="com.bnkode.notie.receiver.SendStatusReceiver.MESSAGE_DELIVERED" />
|
||||
</intent-filter>
|
||||
<intent-filter>
|
||||
<action android:name="com.android.messaging.receiver.SendStatusReceiver.MMS_SENT" />
|
||||
<action android:name="com.bnkode.notie.receiver.SendStatusReceiver.MMS_SENT" />
|
||||
<data android:scheme="content" />
|
||||
</intent-filter>
|
||||
<intent-filter>
|
||||
<action android:name="com.android.messaging.receiver.SendStatusReceiver.MMS_DOWNLOADED" />
|
||||
<action android:name="com.bnkode.notie.receiver.SendStatusReceiver.MMS_DOWNLOADED" />
|
||||
<data android:scheme="content" />
|
||||
</intent-filter>
|
||||
</receiver>
|
||||
@@ -401,6 +517,14 @@
|
||||
</intent-filter>
|
||||
</receiver>
|
||||
|
||||
<receiver
|
||||
android:name=".receiver.ScheduledMessageReceiver"
|
||||
android:exported="false">
|
||||
<intent-filter>
|
||||
<action android:name="com.bnkode.notie.SEND_SCHEDULED_MESSAGE" />
|
||||
</intent-filter>
|
||||
</receiver>
|
||||
|
||||
<!-- Broadcast receiver that will be notified to reset notifications -->
|
||||
<receiver
|
||||
android:name=".receiver.NotificationReceiver"
|
||||
@@ -412,7 +536,7 @@
|
||||
android:name=".datamodel.action.ActionServiceImpl$PendingActionReceiver"
|
||||
android:exported="false">
|
||||
<intent-filter>
|
||||
<action android:name="com.android.messaging.datamodel.PENDING_ACTION" />
|
||||
<action android:name="com.bnkode.notie.datamodel.PENDING_ACTION" />
|
||||
</intent-filter>
|
||||
</receiver>
|
||||
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no">
|
||||
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css"/>
|
||||
<style>
|
||||
html, body, #map { height: 100%; margin: 0; padding: 0; background: #f4f1fb; }
|
||||
.leaflet-control-attribution { font-size: 10px; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="map"></div>
|
||||
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
|
||||
<script>
|
||||
var map = L.map('map', { zoomControl: false }).setView([37.5665, 126.9780], 16);
|
||||
L.control.zoom({ position: 'bottomright' }).addTo(map);
|
||||
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
|
||||
maxZoom: 19,
|
||||
attribution: '© 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,8 @@ import com.android.messaging.sms.BugleApnSettingsLoader;
|
||||
import com.android.messaging.sms.BugleUserAgentInfoLoader;
|
||||
import com.android.messaging.sms.MmsConfig;
|
||||
import com.android.messaging.ui.ConversationDrawables;
|
||||
import com.android.messaging.ui.home.HomeAppLock;
|
||||
import com.android.messaging.ui.home.HomeToast;
|
||||
import com.android.messaging.util.BuglePrefsKeys;
|
||||
import com.android.messaging.util.LogUtil;
|
||||
import com.android.messaging.util.PhoneUtils;
|
||||
@@ -57,6 +59,8 @@ public class BugleApplication extends Application implements UncaughtExceptionHa
|
||||
super.onCreate();
|
||||
|
||||
FactoryImpl.register(getApplicationContext(), this);
|
||||
HomeAppLock.install(this);
|
||||
HomeToast.install(this);
|
||||
|
||||
sSystemUncaughtExceptionHandler = Thread.getDefaultUncaughtExceptionHandler();
|
||||
Thread.setDefaultUncaughtExceptionHandler(this);
|
||||
@@ -124,6 +128,18 @@ public class BugleApplication extends Application implements UncaughtExceptionHa
|
||||
Trace.endSection();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onTrimMemory(final int level) {
|
||||
super.onTrimMemory(level);
|
||||
if (level < TRIM_MEMORY_UI_HIDDEN) {
|
||||
return;
|
||||
}
|
||||
if (LogUtil.isLoggable(TAG, LogUtil.DEBUG)) {
|
||||
LogUtil.d(TAG, "BugleApplication.onTrimMemory " + level);
|
||||
}
|
||||
reclaimCachedBitmaps();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLowMemory() {
|
||||
super.onLowMemory();
|
||||
@@ -131,7 +147,14 @@ public class BugleApplication extends Application implements UncaughtExceptionHa
|
||||
if (LogUtil.isLoggable(TAG, LogUtil.DEBUG)) {
|
||||
LogUtil.d(TAG, "BugleApplication.onLowMemory");
|
||||
}
|
||||
Factory.get().reclaimMemory();
|
||||
reclaimCachedBitmaps();
|
||||
}
|
||||
|
||||
private static void reclaimCachedBitmaps() {
|
||||
final Factory factory = Factory.get();
|
||||
if (factory != null) {
|
||||
factory.reclaimMemory();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -533,6 +533,7 @@ public class BugleDatabaseOperations {
|
||||
// the sender is blocked, or we have been told to keep it archived.
|
||||
if (!keepArchived) {
|
||||
values.put(ConversationColumns.ARCHIVE_STATUS, 0);
|
||||
values.put(ConversationColumns.TRASH_TIMESTAMP, 0);
|
||||
}
|
||||
|
||||
final MessageData message = readMessage(dbWrapper, messageId);
|
||||
@@ -575,6 +576,20 @@ public class BugleDatabaseOperations {
|
||||
updateConversationRowIfExists(dbWrapper, conversationId, values);
|
||||
}
|
||||
|
||||
@DoesNotRunOnMainThread
|
||||
public static void updateConversationTrashTimestampInTransaction(
|
||||
final DatabaseWrapper dbWrapper, final String conversationId,
|
||||
final long trashTimestamp) {
|
||||
Assert.isNotMainThread();
|
||||
Assert.isTrue(dbWrapper.getDatabase().inTransaction());
|
||||
final ContentValues values = new ContentValues();
|
||||
values.put(ConversationColumns.TRASH_TIMESTAMP, trashTimestamp);
|
||||
if (trashTimestamp > 0) {
|
||||
values.put(ConversationColumns.ARCHIVE_STATUS, 0);
|
||||
}
|
||||
updateConversationRowIfExists(dbWrapper, conversationId, values);
|
||||
}
|
||||
|
||||
static void addSnippetTextAndPreviewToContentValues(final MessageData message,
|
||||
final boolean showDraft, final ContentValues values) {
|
||||
values.put(ConversationColumns.SHOW_DRAFT, showDraft ? 1 : 0);
|
||||
@@ -685,7 +700,16 @@ public class BugleDatabaseOperations {
|
||||
sortTimestamp = cursor.getLong(1);
|
||||
}
|
||||
}
|
||||
|
||||
if (sortTimestamp == 0) {
|
||||
try (Cursor existing = dbWrapper.query(DatabaseHelper.CONVERSATIONS_TABLE,
|
||||
new String[]{ ConversationColumns.SORT_TIMESTAMP },
|
||||
ConversationColumns._ID + "=?",
|
||||
new String[]{ conversationId }, null, null, null)) {
|
||||
if (existing.moveToFirst()) {
|
||||
sortTimestamp = existing.getLong(0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
final ContentValues values = new ContentValues();
|
||||
if (draftMessage == null || !draftMessage.hasContent()) {
|
||||
|
||||
@@ -66,6 +66,7 @@ import com.android.messaging.datamodel.media.UriImageRequestDescriptor;
|
||||
import com.android.messaging.sms.MmsSmsUtils;
|
||||
import com.android.messaging.sms.MmsUtils;
|
||||
import com.android.messaging.ui.UIIntents;
|
||||
import com.android.messaging.ui.home.MutedConversationStore;
|
||||
import com.android.messaging.util.Assert;
|
||||
import com.android.messaging.util.AvatarUriUtil;
|
||||
import com.android.messaging.util.BugleGservicesKeys;
|
||||
@@ -354,6 +355,9 @@ public class BugleNotifications {
|
||||
final Context context = Factory.get().getApplicationContext();
|
||||
// TODO: Need to fix this for multi conversation notifications to rate limit dings.
|
||||
final String conversationId = state.mConversationIds.first();
|
||||
if (MutedConversationStore.isMuted(context, conversationId)) {
|
||||
return;
|
||||
}
|
||||
String id = NotificationsUtil.DEFAULT_CHANNEL_ID;
|
||||
if (NotificationsUtil.getNotificationChannel(context, conversationId) != null) {
|
||||
id = conversationId;
|
||||
@@ -535,7 +539,10 @@ public class BugleNotifications {
|
||||
private static void createMessageNotification(final boolean silent,
|
||||
final String conversationId) {
|
||||
final NotificationState state = MessageNotificationState.getNotificationState();
|
||||
final boolean softSound = DataModel.get().isNewMessageObservable(conversationId);
|
||||
final boolean muted = MutedConversationStore.isMuted(
|
||||
Factory.get().getApplicationContext(), conversationId);
|
||||
final boolean softSound = !muted
|
||||
&& DataModel.get().isNewMessageObservable(conversationId);
|
||||
if (state == null) {
|
||||
cancel(PendingIntentConstants.SMS_NOTIFICATION_ID);
|
||||
if (softSound && !TextUtils.isEmpty(conversationId)) {
|
||||
@@ -718,7 +725,9 @@ public class BugleNotifications {
|
||||
setWearableGroupOptions(notifBuilder, notificationState);
|
||||
|
||||
if (notificationState instanceof MultiMessageNotificationState) {
|
||||
if (attachmentBitmap != null) {
|
||||
if (attachmentBitmap != null
|
||||
&& notificationState.mNotificationStyle
|
||||
instanceof NotificationCompat.BigPictureStyle) {
|
||||
// When we've got a picture attachment, we do some switcheroo trickery. When
|
||||
// the notification is expanded, we show the picture as a bigPicture. The small
|
||||
// icon shows the sender's avatar. When that same notification is collapsed, the
|
||||
@@ -783,28 +792,34 @@ public class BugleNotifications {
|
||||
final int requestCode = multiMessageNotificationState.getReplyIntentRequestCode();
|
||||
final PendingIntent replyPendingIntent = UIIntents.get()
|
||||
.getPendingIntentForSendingMessageToConversation(context,
|
||||
conversationId, selfId, requiresMms, requestCode);
|
||||
conversationId, selfId, requiresMms,
|
||||
multiMessageNotificationState.mTitle, requestCode);
|
||||
|
||||
final int replyLabelRes = requiresMms ? R.string.notification_reply_via_mms :
|
||||
R.string.notification_reply_via_sms;
|
||||
|
||||
final NotificationCompat.Action.Builder actionBuilder =
|
||||
// Phone: open the Home bottom-sheet composer. Wear keeps RemoteInput.
|
||||
final NotificationCompat.Action replyAction =
|
||||
new NotificationCompat.Action.Builder(R.drawable.ic_wear_reply,
|
||||
context.getString(replyLabelRes), replyPendingIntent);
|
||||
final RemoteInput.Builder remoteInputBuilder = new RemoteInput.Builder(Intent.EXTRA_TEXT);
|
||||
remoteInputBuilder.setLabel(context.getString(R.string.notification_reply_prompt));
|
||||
actionBuilder.addRemoteInput(remoteInputBuilder.build());
|
||||
notifBuilder.addAction(actionBuilder.build());
|
||||
context.getString(R.string.home_notification_reply), replyPendingIntent)
|
||||
.setSemanticAction(NotificationCompat.Action.SEMANTIC_ACTION_REPLY)
|
||||
.setShowsUserInterface(true)
|
||||
.build();
|
||||
notifBuilder.addAction(replyAction);
|
||||
|
||||
// Support the action on a wearable device
|
||||
final NotificationCompat.Action.Builder wearActionBuilder =
|
||||
new NotificationCompat.Action.Builder(R.drawable.ic_wear_reply,
|
||||
context.getString(replyLabelRes), replyPendingIntent);
|
||||
final String[] choices = context.getResources().getStringArray(
|
||||
R.array.notification_reply_choices);
|
||||
remoteInputBuilder.setChoices(choices);
|
||||
wearActionBuilder.addRemoteInput(remoteInputBuilder.build());
|
||||
wearableExtender.addAction(wearActionBuilder.build());
|
||||
final RemoteInput wearRemoteInput = new RemoteInput.Builder(Intent.EXTRA_TEXT)
|
||||
.setLabel(context.getString(R.string.home_notification_reply_hint))
|
||||
.setAllowFreeFormInput(true)
|
||||
.setChoices(choices)
|
||||
.build();
|
||||
final int wearLabelRes = requiresMms ? R.string.notification_reply_via_mms :
|
||||
R.string.notification_reply_via_sms;
|
||||
wearableExtender.addAction(new NotificationCompat.Action.Builder(R.drawable.ic_wear_reply,
|
||||
context.getString(wearLabelRes), replyPendingIntent)
|
||||
.addRemoteInput(wearRemoteInput)
|
||||
.setSemanticAction(NotificationCompat.Action.SEMANTIC_ACTION_REPLY)
|
||||
.setShowsUserInterface(false)
|
||||
.build());
|
||||
}
|
||||
|
||||
private static void addDownloadMmsAction(final NotificationCompat.Builder notifBuilder,
|
||||
|
||||
@@ -28,6 +28,8 @@ import com.android.messaging.datamodel.action.ActionService;
|
||||
import com.android.messaging.datamodel.action.BackgroundWorker;
|
||||
import com.android.messaging.datamodel.data.BlockedParticipantsData;
|
||||
import com.android.messaging.datamodel.data.BlockedParticipantsData.BlockedParticipantsDataListener;
|
||||
import com.android.messaging.datamodel.data.ConversationSearchData;
|
||||
import com.android.messaging.datamodel.data.ConversationSearchData.ConversationSearchDataListener;
|
||||
import com.android.messaging.datamodel.data.ContactListItemData;
|
||||
import com.android.messaging.datamodel.data.ContactPickerData;
|
||||
import com.android.messaging.datamodel.data.ContactPickerData.ContactPickerDataListener;
|
||||
@@ -42,9 +44,8 @@ import com.android.messaging.datamodel.data.MediaPickerData;
|
||||
import com.android.messaging.datamodel.data.MessagePartData;
|
||||
import com.android.messaging.datamodel.data.ParticipantData;
|
||||
import com.android.messaging.datamodel.data.ParticipantListItemData;
|
||||
import com.android.messaging.datamodel.data.PeopleAndOptionsData;
|
||||
import com.android.messaging.datamodel.data.PeopleAndOptionsData.PeopleAndOptionsDataListener;
|
||||
import com.android.messaging.datamodel.data.PeopleOptionsItemData;
|
||||
import com.android.messaging.datamodel.data.ConversationProfileData;
|
||||
import com.android.messaging.datamodel.data.ConversationProfileData.ConversationProfileDataListener;
|
||||
import com.android.messaging.datamodel.data.SettingsData;
|
||||
import com.android.messaging.datamodel.data.SettingsData.SettingsDataListener;
|
||||
import com.android.messaging.datamodel.data.SubscriptionListData;
|
||||
@@ -67,8 +68,14 @@ public abstract class DataModel {
|
||||
get().getActionService().scheduleAction(action, code, delayMs);
|
||||
}
|
||||
|
||||
public ConversationListData createConversationListData(final Context context,
|
||||
final ConversationListDataListener listener, final boolean archivedMode) {
|
||||
return createConversationListData(context, listener, archivedMode, false);
|
||||
}
|
||||
|
||||
public abstract ConversationListData createConversationListData(final Context context,
|
||||
final ConversationListDataListener listener, final boolean archivedMode);
|
||||
final ConversationListDataListener listener, final boolean archivedMode,
|
||||
final boolean trashMode);
|
||||
|
||||
public abstract ConversationData createConversationData(final Context context,
|
||||
final ConversationDataListener listener, final String conversationId);
|
||||
@@ -83,10 +90,9 @@ public abstract class DataModel {
|
||||
public abstract LaunchConversationData createLaunchConversationData(
|
||||
LaunchConversationDataListener listener);
|
||||
|
||||
public abstract PeopleOptionsItemData createPeopleOptionsItemData(final Context context);
|
||||
|
||||
public abstract PeopleAndOptionsData createPeopleAndOptionsData(final String conversationId,
|
||||
final Context context, final PeopleAndOptionsDataListener listener);
|
||||
public abstract ConversationProfileData createConversationProfileData(
|
||||
final String conversationId, final Context context,
|
||||
final ConversationProfileDataListener listener);
|
||||
|
||||
public abstract VCardContactItemData createVCardContactItemData(final Context context,
|
||||
final MessagePartData data);
|
||||
@@ -100,6 +106,9 @@ public abstract class DataModel {
|
||||
public abstract BlockedParticipantsData createBlockedParticipantsData(Context context,
|
||||
BlockedParticipantsDataListener listener);
|
||||
|
||||
public abstract ConversationSearchData createConversationSearchData(Context context,
|
||||
String conversationId, ConversationSearchDataListener listener);
|
||||
|
||||
public abstract SubscriptionListData createSubscriptonListData(Context context);
|
||||
|
||||
public abstract SettingsData createSettingsData(Context context, SettingsDataListener listener);
|
||||
|
||||
@@ -26,8 +26,11 @@ import com.android.messaging.datamodel.action.ActionService;
|
||||
import com.android.messaging.datamodel.action.BackgroundWorker;
|
||||
import com.android.messaging.datamodel.action.FixupMessageStatusOnStartupAction;
|
||||
import com.android.messaging.datamodel.action.ProcessPendingMessagesAction;
|
||||
import com.android.messaging.datamodel.action.PurgeExpiredTrashAction;
|
||||
import com.android.messaging.datamodel.data.BlockedParticipantsData;
|
||||
import com.android.messaging.datamodel.data.BlockedParticipantsData.BlockedParticipantsDataListener;
|
||||
import com.android.messaging.datamodel.data.ConversationSearchData;
|
||||
import com.android.messaging.datamodel.data.ConversationSearchData.ConversationSearchDataListener;
|
||||
import com.android.messaging.datamodel.data.ContactListItemData;
|
||||
import com.android.messaging.datamodel.data.ContactPickerData;
|
||||
import com.android.messaging.datamodel.data.ContactPickerData.ContactPickerDataListener;
|
||||
@@ -42,9 +45,8 @@ import com.android.messaging.datamodel.data.MediaPickerData;
|
||||
import com.android.messaging.datamodel.data.MessagePartData;
|
||||
import com.android.messaging.datamodel.data.ParticipantData;
|
||||
import com.android.messaging.datamodel.data.ParticipantListItemData;
|
||||
import com.android.messaging.datamodel.data.PeopleAndOptionsData;
|
||||
import com.android.messaging.datamodel.data.PeopleAndOptionsData.PeopleAndOptionsDataListener;
|
||||
import com.android.messaging.datamodel.data.PeopleOptionsItemData;
|
||||
import com.android.messaging.datamodel.data.ConversationProfileData;
|
||||
import com.android.messaging.datamodel.data.ConversationProfileData.ConversationProfileDataListener;
|
||||
import com.android.messaging.datamodel.data.SettingsData;
|
||||
import com.android.messaging.datamodel.data.SettingsData.SettingsDataListener;
|
||||
import com.android.messaging.datamodel.data.SubscriptionListData;
|
||||
@@ -80,8 +82,9 @@ public class DataModelImpl extends DataModel {
|
||||
|
||||
@Override
|
||||
public ConversationListData createConversationListData(final Context context,
|
||||
final ConversationListDataListener listener, final boolean archivedMode) {
|
||||
return new ConversationListData(context, listener, archivedMode);
|
||||
final ConversationListDataListener listener, final boolean archivedMode,
|
||||
final boolean trashMode) {
|
||||
return new ConversationListData(context, listener, archivedMode, trashMode);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -107,6 +110,12 @@ public class DataModelImpl extends DataModel {
|
||||
return new BlockedParticipantsData(context, listener);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ConversationSearchData createConversationSearchData(final Context context,
|
||||
final String conversationId, final ConversationSearchDataListener listener) {
|
||||
return new ConversationSearchData(context, conversationId, listener);
|
||||
}
|
||||
|
||||
@Override
|
||||
public MediaPickerData createMediaPickerData(final Context context) {
|
||||
return new MediaPickerData(context);
|
||||
@@ -119,14 +128,9 @@ public class DataModelImpl extends DataModel {
|
||||
}
|
||||
|
||||
@Override
|
||||
public PeopleOptionsItemData createPeopleOptionsItemData(final Context context) {
|
||||
return new PeopleOptionsItemData(context);
|
||||
}
|
||||
|
||||
@Override
|
||||
public PeopleAndOptionsData createPeopleAndOptionsData(final String conversationId,
|
||||
final Context context, final PeopleAndOptionsDataListener listener) {
|
||||
return new PeopleAndOptionsData(conversationId, context, listener);
|
||||
public ConversationProfileData createConversationProfileData(final String conversationId,
|
||||
final Context context, final ConversationProfileDataListener listener) {
|
||||
return new ConversationProfileData(conversationId, context, listener);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -209,6 +213,7 @@ public class DataModelImpl extends DataModel {
|
||||
createConnectivityUtilForEachActiveSubscription();
|
||||
|
||||
FixupMessageStatusOnStartupAction.fixupMessageStatus();
|
||||
PurgeExpiredTrashAction.purgeExpired();
|
||||
ProcessPendingMessagesAction.processFirstPendingMessage();
|
||||
SyncManager.immediateSync();
|
||||
|
||||
|
||||
@@ -101,6 +101,9 @@ public class DatabaseHelper extends SQLiteOpenHelper {
|
||||
/* If this conversation is archived */
|
||||
public static final String ARCHIVE_STATUS = "archive_status";
|
||||
|
||||
/* When the conversation was moved to Trash; 0 if not in Trash */
|
||||
public static final String TRASH_TIMESTAMP = "trash_timestamp";
|
||||
|
||||
/* Timestamp for sorting purposes */
|
||||
public static final String SORT_TIMESTAMP = "sort_timestamp";
|
||||
|
||||
@@ -159,6 +162,7 @@ public class DatabaseHelper extends SQLiteOpenHelper {
|
||||
+ ConversationColumns.DRAFT_PREVIEW_URI + " TEXT, "
|
||||
+ ConversationColumns.DRAFT_PREVIEW_CONTENT_TYPE + " TEXT, "
|
||||
+ ConversationColumns.ARCHIVE_STATUS + " INT DEFAULT(0), "
|
||||
+ ConversationColumns.TRASH_TIMESTAMP + " INT DEFAULT(0), "
|
||||
+ ConversationColumns.SORT_TIMESTAMP + " INT DEFAULT(0), "
|
||||
+ ConversationColumns.LAST_READ_TIMESTAMP + " INT DEFAULT(0), "
|
||||
+ ConversationColumns.ICON + " TEXT, "
|
||||
@@ -183,6 +187,11 @@ public class DatabaseHelper extends SQLiteOpenHelper {
|
||||
+ " ON " + CONVERSATIONS_TABLE
|
||||
+ "(" + ConversationColumns.ARCHIVE_STATUS + ")";
|
||||
|
||||
private static final String CONVERSATIONS_TABLE_TRASH_TIMESTAMP_INDEX_SQL =
|
||||
"CREATE INDEX index_" + CONVERSATIONS_TABLE + "_" + ConversationColumns.TRASH_TIMESTAMP
|
||||
+ " ON " + CONVERSATIONS_TABLE
|
||||
+ "(" + ConversationColumns.TRASH_TIMESTAMP + ")";
|
||||
|
||||
private static final String CONVERSATIONS_TABLE_SORT_TIMESTAMP_INDEX_SQL =
|
||||
"CREATE INDEX index_" + CONVERSATIONS_TABLE + "_" + ConversationColumns.SORT_TIMESTAMP
|
||||
+ " ON " + CONVERSATIONS_TABLE
|
||||
@@ -525,6 +534,7 @@ public class DatabaseHelper extends SQLiteOpenHelper {
|
||||
private static final String[] CREATE_INDEX_SQLS = new String[] {
|
||||
CONVERSATIONS_TABLE_SMS_THREAD_ID_INDEX_SQL,
|
||||
CONVERSATIONS_TABLE_ARCHIVE_STATUS_INDEX_SQL,
|
||||
CONVERSATIONS_TABLE_TRASH_TIMESTAMP_INDEX_SQL,
|
||||
CONVERSATIONS_TABLE_SORT_TIMESTAMP_INDEX_SQL,
|
||||
MESSAGES_TABLE_SORT_INDEX_SQL,
|
||||
MESSAGES_TABLE_STATUS_SEEN_INDEX_SQL,
|
||||
|
||||
@@ -48,6 +48,9 @@ public class DatabaseUpgradeHelper {
|
||||
if (currentVersion < 2) {
|
||||
currentVersion = upgradeToVersion2(db);
|
||||
}
|
||||
if (currentVersion < 3) {
|
||||
currentVersion = upgradeToVersion3(db);
|
||||
}
|
||||
// Rebuild all the views
|
||||
final Context context = Factory.get().getApplicationContext();
|
||||
DatabaseHelper.dropAllViews(db);
|
||||
@@ -63,6 +66,17 @@ public class DatabaseUpgradeHelper {
|
||||
return 2;
|
||||
}
|
||||
|
||||
private int upgradeToVersion3(final SQLiteDatabase db) {
|
||||
db.execSQL("ALTER TABLE " + DatabaseHelper.CONVERSATIONS_TABLE + " ADD COLUMN " +
|
||||
DatabaseHelper.ConversationColumns.TRASH_TIMESTAMP + " INT DEFAULT(0)");
|
||||
db.execSQL("CREATE INDEX IF NOT EXISTS index_" + DatabaseHelper.CONVERSATIONS_TABLE
|
||||
+ "_" + DatabaseHelper.ConversationColumns.TRASH_TIMESTAMP
|
||||
+ " ON " + DatabaseHelper.CONVERSATIONS_TABLE
|
||||
+ "(" + DatabaseHelper.ConversationColumns.TRASH_TIMESTAMP + ")");
|
||||
LogUtil.i(TAG, "Upgraded database to version 3");
|
||||
return 3;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks db version correctness at the end of each milestone release. If target database
|
||||
* version lies beyond the version range that the current release may handle, we snap the
|
||||
|
||||
@@ -103,7 +103,7 @@ public abstract class FileProvider extends ContentProvider {
|
||||
final String fileId = uri.getPath();
|
||||
if (isValidFileId(fileId)) {
|
||||
final File file = getFile(fileId, getExtensionFromUri(uri));
|
||||
return file.delete() ? 1 : 0;
|
||||
return file != null && file.delete() ? 1 : 0;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
@@ -114,6 +114,9 @@ public abstract class FileProvider extends ContentProvider {
|
||||
final String fileId = uri.getPath();
|
||||
if (isValidFileId(fileId)) {
|
||||
final File file = getFile(fileId, getExtensionFromUri(uri));
|
||||
if (file == null) {
|
||||
throw new FileNotFoundException(String.valueOf(uri));
|
||||
}
|
||||
final int mode =
|
||||
(TextUtils.equals(fileMode, "r") ? ParcelFileDescriptor.MODE_READ_ONLY :
|
||||
ParcelFileDescriptor.MODE_WRITE_ONLY | ParcelFileDescriptor.MODE_TRUNCATE);
|
||||
|
||||
@@ -120,6 +120,8 @@ public class FrequentContactsCursorBuilder {
|
||||
mAllContactsCursor.getInt(ContactUtil.INDEX_PHONE_EMAIL_TYPE);
|
||||
row[ContactUtil.INDEX_PHONE_EMAIL_LABEL] =
|
||||
mAllContactsCursor.getString(ContactUtil.INDEX_PHONE_EMAIL_LABEL);
|
||||
row[ContactUtil.INDEX_SORT_KEY] =
|
||||
mAllContactsCursor.getString(ContactUtil.INDEX_SORT_KEY);
|
||||
|
||||
boolean numberAlreadyAdded = false;
|
||||
for (Object[] oldRow : rows) {
|
||||
|
||||
@@ -46,7 +46,7 @@ public class MediaScratchFileProvider extends FileProvider {
|
||||
private static final SimpleArrayMap<Uri, String> sUriToDisplayNameMap = new SimpleArrayMap<>();
|
||||
|
||||
public static final String AUTHORITY =
|
||||
"com.android.messaging.datamodel.MediaScratchFileProvider";
|
||||
"com.bnkode.notie.datamodel.MediaScratchFileProvider";
|
||||
private static final String MEDIA_SCRATCH_SPACE_DIR = "mediascratchspace";
|
||||
|
||||
public static boolean isMediaScratchSpaceUri(final Uri uri) {
|
||||
@@ -68,8 +68,8 @@ public class MediaScratchFileProvider extends FileProvider {
|
||||
public static Uri buildMediaScratchSpaceUri(final String extension) {
|
||||
final Uri uri = FileProvider.buildFileUri(AUTHORITY, extension);
|
||||
final File file = getFileWithExtension(uri.getPath(), extension);
|
||||
if (!ensureFileExists(file)) {
|
||||
LogUtil.e(TAG, "Failed to create temp file " + file.getAbsolutePath());
|
||||
if (file == null || !ensureFileExists(file)) {
|
||||
LogUtil.e(TAG, "Failed to create temp file for " + uri);
|
||||
}
|
||||
return uri;
|
||||
}
|
||||
@@ -90,8 +90,12 @@ public class MediaScratchFileProvider extends FileProvider {
|
||||
|
||||
private static File getFileWithExtension(final String path, final String extension) {
|
||||
final Context context = Factory.get().getApplicationContext();
|
||||
String name = path;
|
||||
if (name != null && name.startsWith("/")) {
|
||||
name = name.substring(1);
|
||||
}
|
||||
final File filePath = new File(getDirectory(context),
|
||||
TextUtils.isEmpty(extension) ? path : path + "." + extension);
|
||||
TextUtils.isEmpty(extension) ? name : name + "." + extension);
|
||||
|
||||
try {
|
||||
if (!filePath.getCanonicalPath()
|
||||
|
||||
@@ -39,6 +39,7 @@ import androidx.core.app.NotificationCompat;
|
||||
import androidx.core.app.NotificationCompat.Builder;
|
||||
import androidx.core.app.NotificationCompat.WearableExtender;
|
||||
import androidx.core.app.NotificationManagerCompat;
|
||||
import androidx.core.app.Person;
|
||||
|
||||
import com.android.messaging.Factory;
|
||||
import com.android.messaging.R;
|
||||
@@ -50,6 +51,7 @@ import com.android.messaging.datamodel.data.MessagePartData;
|
||||
import com.android.messaging.datamodel.data.ParticipantData;
|
||||
import com.android.messaging.sms.MmsUtils;
|
||||
import com.android.messaging.ui.UIIntents;
|
||||
import com.android.messaging.ui.home.MutedConversationStore;
|
||||
import com.android.messaging.util.Assert;
|
||||
import com.android.messaging.util.AvatarUriUtil;
|
||||
import com.android.messaging.util.BugleGservicesKeys;
|
||||
@@ -494,85 +496,62 @@ public abstract class MessageNotificationState extends NotificationState {
|
||||
@Override
|
||||
protected NotificationCompat.Style build(final Builder builder) {
|
||||
builder.setContentTitle(mTitle)
|
||||
.setTicker(getTicker());
|
||||
.setTicker(getTicker())
|
||||
.setContentText(mContent);
|
||||
|
||||
NotificationCompat.Style notifStyle;
|
||||
final Context context = Factory.get().getApplicationContext();
|
||||
final ConversationLineInfo convInfo = mConvList.mConvInfos.get(0);
|
||||
final List<NotificationLineInfo> lineInfos = convInfo.mLineInfos;
|
||||
final int messageCount = lineInfos.size();
|
||||
// At this point, all the messages come from the same conversation. We need to load
|
||||
// the sender's avatar and then finish building the notification on a callback.
|
||||
|
||||
builder.setContentText(mContent); // for collapsed state
|
||||
|
||||
if (messageCount == 1) {
|
||||
final boolean shouldShowImage = ContentType.isImageType(mAttachmentType)
|
||||
|| (ContentType.isVideoType(mAttachmentType));
|
||||
if (mAttachmentUri != null && shouldShowImage) {
|
||||
// Show "Picture" as the content
|
||||
final MessageLineInfo messageLineInfo = (MessageLineInfo) lineInfos.get(0);
|
||||
String authorFirstName = messageLineInfo.mAuthorFirstName;
|
||||
|
||||
// For the collapsed state, just show "picture" unless this is a
|
||||
// group conversation. If it's a group, show the sender name and
|
||||
// "picture".
|
||||
final CharSequence tickerTag =
|
||||
BugleNotifications.formatAttachmentTag(authorFirstName,
|
||||
mAttachmentType);
|
||||
// For 1:1 notifications don't show first name in the notification, but
|
||||
// do show it in the ticker text
|
||||
CharSequence pictureTag = tickerTag;
|
||||
if (!convInfo.mIsGroup) {
|
||||
authorFirstName = null;
|
||||
pictureTag = BugleNotifications.formatAttachmentTag(authorFirstName,
|
||||
mAttachmentType);
|
||||
}
|
||||
builder.setContentText(pictureTag);
|
||||
builder.setTicker(tickerTag);
|
||||
|
||||
notifStyle = new NotificationCompat.BigPictureStyle(builder)
|
||||
.setSummaryText(BugleNotifications.formatInboxMessage(
|
||||
authorFirstName,
|
||||
null, null,
|
||||
null)); // expanded state, just show sender
|
||||
} else {
|
||||
notifStyle = new NotificationCompat.BigTextStyle(builder)
|
||||
.bigText(mContent);
|
||||
final Person user = new Person.Builder()
|
||||
.setName(context.getString(R.string.home_notification_you))
|
||||
.build();
|
||||
final NotificationCompat.MessagingStyle style =
|
||||
new NotificationCompat.MessagingStyle(user);
|
||||
if (convInfo.mIsGroup) {
|
||||
style.setGroupConversation(true);
|
||||
if (!TextUtils.isEmpty(convInfo.mGroupConversationName)) {
|
||||
style.setConversationTitle(convInfo.mGroupConversationName);
|
||||
}
|
||||
} else {
|
||||
// We've got multiple messages for the same sender.
|
||||
// Starting with the oldest new message, display the full text of each message.
|
||||
// Begin a line for each subsequent message.
|
||||
final SpannableStringBuilder buf = new SpannableStringBuilder();
|
||||
|
||||
for (int i = lineInfos.size() - 1; i >= 0; --i) {
|
||||
final NotificationLineInfo info = lineInfos.get(i);
|
||||
final MessageLineInfo messageLineInfo = (MessageLineInfo) info;
|
||||
mAttachmentUri = messageLineInfo.mAttachmentUri;
|
||||
mAttachmentType = messageLineInfo.mAttachmentType;
|
||||
CharSequence text = messageLineInfo.mText;
|
||||
if (!TextUtils.isEmpty(text) || mAttachmentUri != null) {
|
||||
if (convInfo.mIsGroup) {
|
||||
// append the bold name to the front of the message
|
||||
text = BugleNotifications.buildSpaceSeparatedMessage(
|
||||
messageLineInfo.mAuthorFullName, text, mAttachmentUri,
|
||||
mAttachmentType);
|
||||
} else {
|
||||
text = BugleNotifications.buildSpaceSeparatedMessage(
|
||||
null, text, mAttachmentUri, mAttachmentType);
|
||||
}
|
||||
buf.append(text);
|
||||
if (i > 0) {
|
||||
buf.append('\n');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Show a single notification -- big style with the text of all the messages
|
||||
notifStyle = new NotificationCompat.BigTextStyle(builder).bigText(buf);
|
||||
}
|
||||
|
||||
// MessagingStyle wants chronological order (oldest first). Latest is at index 0.
|
||||
for (int i = lineInfos.size() - 1; i >= 0; --i) {
|
||||
final NotificationLineInfo info = lineInfos.get(i);
|
||||
if (!(info instanceof MessageLineInfo)) {
|
||||
continue;
|
||||
}
|
||||
final MessageLineInfo messageLineInfo = (MessageLineInfo) info;
|
||||
CharSequence text = messageLineInfo.mText;
|
||||
if (TextUtils.isEmpty(text) && messageLineInfo.mAttachmentUri != null) {
|
||||
text = BugleNotifications.formatAttachmentTag(
|
||||
convInfo.mIsGroup ? messageLineInfo.mAuthorFirstName : null,
|
||||
messageLineInfo.mAttachmentType);
|
||||
}
|
||||
if (TextUtils.isEmpty(text)) {
|
||||
continue;
|
||||
}
|
||||
final Person.Builder personBuilder = new Person.Builder()
|
||||
.setName(!TextUtils.isEmpty(messageLineInfo.mAuthorFullName)
|
||||
? messageLineInfo.mAuthorFullName : mTitle);
|
||||
if (convInfo.mContactUri != null) {
|
||||
personBuilder.setUri(convInfo.mContactUri.toString());
|
||||
}
|
||||
final long timestamp = Math.max(0, convInfo.mReceivedTimestamp - i);
|
||||
final NotificationCompat.MessagingStyle.Message message =
|
||||
new NotificationCompat.MessagingStyle.Message(
|
||||
text, timestamp, personBuilder.build());
|
||||
if (messageLineInfo.mAttachmentUri != null
|
||||
&& !TextUtils.isEmpty(messageLineInfo.mAttachmentType)
|
||||
&& (ContentType.isImageType(messageLineInfo.mAttachmentType)
|
||||
|| ContentType.isVideoType(messageLineInfo.mAttachmentType))) {
|
||||
message.setData(messageLineInfo.mAttachmentType,
|
||||
messageLineInfo.mAttachmentUri);
|
||||
}
|
||||
style.addMessage(message);
|
||||
}
|
||||
|
||||
builder.setWhen(convInfo.mReceivedTimestamp);
|
||||
return notifStyle;
|
||||
return style;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -824,6 +803,9 @@ public abstract class MessageNotificationState extends NotificationState {
|
||||
final String messageText = convMessageData.getText();
|
||||
|
||||
final String convId = convMessageData.getConversationId();
|
||||
if (MutedConversationStore.isMuted(context, convId)) {
|
||||
continue;
|
||||
}
|
||||
final String messageId = convMessageData.getMessageId();
|
||||
|
||||
CharSequence text = messageText;
|
||||
@@ -840,6 +822,9 @@ public abstract class MessageNotificationState extends NotificationState {
|
||||
if (currConvInfo == null) {
|
||||
final ConversationListItemData convData =
|
||||
ConversationListItemData.getExistingConversation(db, convId);
|
||||
if (convData == null || convData.getIsTrashed()) {
|
||||
continue;
|
||||
}
|
||||
final int subId = BugleDatabaseOperations.getSelfSubscriptionId(db,
|
||||
convData.getSelfId());
|
||||
groupConversationName = convData.getName();
|
||||
|
||||
@@ -53,7 +53,7 @@ public class MessagingContentProvider extends ContentProvider {
|
||||
private static final String TAG = LogUtil.BUGLE_TAG;
|
||||
|
||||
public static final String AUTHORITY =
|
||||
"com.android.messaging.datamodel.MessagingContentProvider";
|
||||
"com.bnkode.notie.datamodel.MessagingContentProvider";
|
||||
private static final String CONTENT_AUTHORITY = "content://" + AUTHORITY + '/';
|
||||
|
||||
// Conversations query
|
||||
@@ -70,6 +70,11 @@ public class MessagingContentProvider extends ContentProvider {
|
||||
public static final Uri CONVERSATION_MESSAGES_URI = Uri.parse(CONTENT_AUTHORITY +
|
||||
MESSAGES_QUERY + "/conversation");
|
||||
|
||||
public static final Uri CONVERSATION_MESSAGES_SEARCH_URI = Uri.parse(CONTENT_AUTHORITY +
|
||||
MESSAGES_QUERY + "/search");
|
||||
|
||||
private static final String SEARCH_QUERY_PARAMETER = "q";
|
||||
|
||||
// Conversation participants query
|
||||
private static final String PARTICIPANTS_QUERY = "participants";
|
||||
|
||||
@@ -146,6 +151,7 @@ public class MessagingContentProvider extends ContentProvider {
|
||||
|
||||
private static final int CONVERSATION_QUERY_CODE = 20;
|
||||
private static final int CONVERSATION_MESSAGES_QUERY_CODE = 30;
|
||||
private static final int CONVERSATION_MESSAGES_SEARCH_QUERY_CODE = 35;
|
||||
private static final int CONVERSATION_PARTICIPANTS_QUERY_CODE = 40;
|
||||
private static final int CONVERSATION_IMAGES_QUERY_CODE = 50;
|
||||
private static final int DRAFT_IMAGES_QUERY_CODE = 60;
|
||||
@@ -158,6 +164,8 @@ public class MessagingContentProvider extends ContentProvider {
|
||||
sURIMatcher.addURI(AUTHORITY, CONVERSATIONS_QUERY + "/*", CONVERSATION_QUERY_CODE);
|
||||
sURIMatcher.addURI(AUTHORITY, MESSAGES_QUERY + "/conversation/*",
|
||||
CONVERSATION_MESSAGES_QUERY_CODE);
|
||||
sURIMatcher.addURI(AUTHORITY, MESSAGES_QUERY + "/search/*",
|
||||
CONVERSATION_MESSAGES_SEARCH_QUERY_CODE);
|
||||
sURIMatcher.addURI(AUTHORITY, PARTICIPANTS_QUERY + "/conversation/*",
|
||||
CONVERSATION_PARTICIPANTS_QUERY_CODE);
|
||||
sURIMatcher.addURI(AUTHORITY, PARTICIPANTS_QUERY, PARTICIPANTS_QUERY_CODE);
|
||||
@@ -176,6 +184,17 @@ public class MessagingContentProvider extends ContentProvider {
|
||||
return builder.build();
|
||||
}
|
||||
|
||||
/**
|
||||
* Build a uri for the messages of a conversation whose text matches {@code searchText}.
|
||||
*/
|
||||
public static Uri buildConversationMessagesSearchUri(final String conversationId,
|
||||
final String searchText) {
|
||||
return CONVERSATION_MESSAGES_SEARCH_URI.buildUpon()
|
||||
.appendPath(conversationId)
|
||||
.appendQueryParameter(SEARCH_QUERY_PARAMETER, searchText)
|
||||
.build();
|
||||
}
|
||||
|
||||
public static void notifyMessagesChanged(final String conversationId) {
|
||||
final Uri uri = buildConversationMessagesUri(conversationId);
|
||||
final Context context = Factory.get().getApplicationContext();
|
||||
@@ -319,6 +338,19 @@ public class MessagingContentProvider extends ContentProvider {
|
||||
} else {
|
||||
throw new IllegalArgumentException("Malformed URI " + uri);
|
||||
}
|
||||
case CONVERSATION_MESSAGES_SEARCH_QUERY_CODE:
|
||||
if (uri.getPathSegments().size() == 3) {
|
||||
// Like the query above this is a raw query, so selection/sorting is invalid.
|
||||
if (selection == null && selectionArgs == null && sortOrder == null) {
|
||||
return queryConversationMessagesSearch(uri.getPathSegments().get(2),
|
||||
uri.getQueryParameter(SEARCH_QUERY_PARAMETER));
|
||||
} else {
|
||||
throw new IllegalArgumentException(
|
||||
"Cannot set selection or sort order with this query");
|
||||
}
|
||||
} else {
|
||||
throw new IllegalArgumentException("Malformed URI " + uri);
|
||||
}
|
||||
case CONVERSATION_IMAGES_QUERY_CODE:
|
||||
queryBuilder.setTables(ConversationImagePartsView.getViewName());
|
||||
if (uri.getPathSegments().size() == 2) {
|
||||
@@ -366,6 +398,24 @@ public class MessagingContentProvider extends ContentProvider {
|
||||
return cursor;
|
||||
}
|
||||
|
||||
private Cursor queryConversationMessagesSearch(final String conversationId,
|
||||
final String searchText) {
|
||||
final String[] queryArgs = { conversationId, "%" + escapeLikePattern(searchText) + "%" };
|
||||
final Cursor cursor = getDatabaseWrapper().rawQuery(
|
||||
ConversationMessageData.getConversationMessagesSearchQuerySql(), queryArgs);
|
||||
// Follow the plain message uri so that incoming messages refresh the results as well.
|
||||
cursor.setNotificationUri(getContext().getContentResolver(),
|
||||
buildConversationMessagesUri(conversationId));
|
||||
return cursor;
|
||||
}
|
||||
|
||||
private static String escapeLikePattern(final String text) {
|
||||
if (TextUtils.isEmpty(text)) {
|
||||
return "";
|
||||
}
|
||||
return text.replace("\\", "\\\\").replace("%", "\\%").replace("_", "\\_");
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getType(@NonNull final Uri uri) {
|
||||
final StringBuilder sb = new
|
||||
|
||||
@@ -32,7 +32,7 @@ import java.io.IOException;
|
||||
public class MmsFileProvider extends FileProvider {
|
||||
private static final String TAG = LogUtil.BUGLE_TAG;
|
||||
|
||||
static final String AUTHORITY = "com.android.messaging.datamodel.MmsFileProvider";
|
||||
static final String AUTHORITY = "com.bnkode.notie.datamodel.MmsFileProvider";
|
||||
private static final String RAW_MMS_DIR = "rawmms";
|
||||
|
||||
/**
|
||||
|
||||
@@ -0,0 +1,236 @@
|
||||
package com.android.messaging.datamodel;
|
||||
|
||||
import android.app.AlarmManager;
|
||||
import android.app.PendingIntent;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.SharedPreferences;
|
||||
import android.net.Uri;
|
||||
import android.os.Build;
|
||||
import android.os.Parcel;
|
||||
import android.text.TextUtils;
|
||||
import android.util.Base64;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.WorkerThread;
|
||||
|
||||
import com.android.messaging.Factory;
|
||||
import com.android.messaging.datamodel.action.InsertNewMessageAction;
|
||||
import com.android.messaging.datamodel.data.MessageData;
|
||||
import com.android.messaging.receiver.ScheduledMessageReceiver;
|
||||
import com.android.messaging.util.LogUtil;
|
||||
|
||||
import org.json.JSONArray;
|
||||
import org.json.JSONObject;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
/** Persists compose-time scheduled sends and arms AlarmManager to deliver them. */
|
||||
public final class ScheduledMessageStore {
|
||||
private static final String PREFS = "home_scheduled_messages";
|
||||
private static final String KEY_ENTRIES = "entries";
|
||||
private static final String JSON_ID = "id";
|
||||
private static final String JSON_SEND_AT = "sendAt";
|
||||
private static final String JSON_PAYLOAD = "payload";
|
||||
|
||||
private ScheduledMessageStore() {
|
||||
}
|
||||
|
||||
public static void schedule(@NonNull final Context context, @NonNull final MessageData message,
|
||||
final long sendAt) {
|
||||
final Entry entry = new Entry();
|
||||
entry.id = UUID.randomUUID().toString();
|
||||
entry.sendAt = sendAt;
|
||||
entry.payload = encode(message);
|
||||
if (TextUtils.isEmpty(entry.payload)) {
|
||||
LogUtil.e(LogUtil.BUGLE_TAG, "Scheduled message encode failed");
|
||||
return;
|
||||
}
|
||||
final List<Entry> entries = loadLocked(context);
|
||||
entries.add(entry);
|
||||
saveLocked(context, entries);
|
||||
setAlarm(context, entry);
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
public static void sendNow(@NonNull final Context context, @Nullable final String id) {
|
||||
if (TextUtils.isEmpty(id)) {
|
||||
return;
|
||||
}
|
||||
Entry match = null;
|
||||
synchronized (ScheduledMessageStore.class) {
|
||||
final List<Entry> entries = loadLocked(context);
|
||||
for (int i = 0; i < entries.size(); i++) {
|
||||
if (id.equals(entries.get(i).id)) {
|
||||
match = entries.remove(i);
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (match != null) {
|
||||
saveLocked(context, entries);
|
||||
}
|
||||
}
|
||||
if (match == null) {
|
||||
return;
|
||||
}
|
||||
cancelAlarm(context, match.id);
|
||||
final MessageData message = decode(match.payload);
|
||||
if (message == null || !message.hasContent()) {
|
||||
LogUtil.e(LogUtil.BUGLE_TAG, "Scheduled message payload missing for " + id);
|
||||
return;
|
||||
}
|
||||
InsertNewMessageAction.insertNewMessage(message);
|
||||
}
|
||||
|
||||
public static void rescheduleAll(@NonNull final Context context) {
|
||||
final List<Entry> entries = loadLocked(context);
|
||||
final long now = System.currentTimeMillis();
|
||||
final List<Entry> due = new ArrayList<>();
|
||||
synchronized (ScheduledMessageStore.class) {
|
||||
final List<Entry> remaining = new ArrayList<>();
|
||||
for (final Entry entry : entries) {
|
||||
if (entry.sendAt <= now) {
|
||||
due.add(entry);
|
||||
} else {
|
||||
remaining.add(entry);
|
||||
setAlarm(context, entry);
|
||||
}
|
||||
}
|
||||
if (due.size() != entries.size()) {
|
||||
saveLocked(context, remaining);
|
||||
} else if (!due.isEmpty()) {
|
||||
saveLocked(context, new ArrayList<>());
|
||||
}
|
||||
}
|
||||
for (final Entry entry : due) {
|
||||
final MessageData message = decode(entry.payload);
|
||||
if (message != null && message.hasContent()) {
|
||||
InsertNewMessageAction.insertNewMessage(message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void setAlarm(@NonNull final Context context, @NonNull final Entry entry) {
|
||||
final AlarmManager alarms = context.getSystemService(AlarmManager.class);
|
||||
if (alarms == null) {
|
||||
return;
|
||||
}
|
||||
final PendingIntent pending = pendingIntent(context, entry.id, true);
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S && !alarms.canScheduleExactAlarms()) {
|
||||
alarms.setAndAllowWhileIdle(AlarmManager.RTC_WAKEUP, entry.sendAt, pending);
|
||||
return;
|
||||
}
|
||||
alarms.setExactAndAllowWhileIdle(AlarmManager.RTC_WAKEUP, entry.sendAt, pending);
|
||||
}
|
||||
|
||||
private static void cancelAlarm(@NonNull final Context context, @NonNull final String id) {
|
||||
final AlarmManager alarms = context.getSystemService(AlarmManager.class);
|
||||
if (alarms == null) {
|
||||
return;
|
||||
}
|
||||
final PendingIntent pending = pendingIntent(context, id, false);
|
||||
if (pending != null) {
|
||||
alarms.cancel(pending);
|
||||
}
|
||||
}
|
||||
|
||||
private static PendingIntent pendingIntent(@NonNull final Context context,
|
||||
@NonNull final String id, final boolean update) {
|
||||
final Intent intent = new Intent(context, ScheduledMessageReceiver.class);
|
||||
intent.setAction(ScheduledMessageReceiver.ACTION_SEND);
|
||||
intent.setData(Uri.parse("msgq-schedule:" + id));
|
||||
intent.putExtra(ScheduledMessageReceiver.EXTRA_ID, id);
|
||||
final int flags = PendingIntent.FLAG_IMMUTABLE
|
||||
| (update ? PendingIntent.FLAG_UPDATE_CURRENT : PendingIntent.FLAG_NO_CREATE);
|
||||
return PendingIntent.getBroadcast(context.getApplicationContext(), id.hashCode(),
|
||||
intent, flags);
|
||||
}
|
||||
|
||||
private static synchronized List<Entry> loadLocked(@NonNull final Context context) {
|
||||
final SharedPreferences prefs = prefs(context);
|
||||
final String raw = prefs.getString(KEY_ENTRIES, "[]");
|
||||
final List<Entry> entries = new ArrayList<>();
|
||||
try {
|
||||
final JSONArray array = new JSONArray(raw);
|
||||
for (int i = 0; i < array.length(); i++) {
|
||||
final JSONObject object = array.getJSONObject(i);
|
||||
final Entry entry = new Entry();
|
||||
entry.id = object.optString(JSON_ID);
|
||||
entry.sendAt = object.optLong(JSON_SEND_AT);
|
||||
entry.payload = object.optString(JSON_PAYLOAD);
|
||||
if (!TextUtils.isEmpty(entry.id) && !TextUtils.isEmpty(entry.payload)) {
|
||||
entries.add(entry);
|
||||
}
|
||||
}
|
||||
} catch (final Exception exception) {
|
||||
LogUtil.e(LogUtil.BUGLE_TAG, "Failed to load scheduled messages", exception);
|
||||
}
|
||||
return entries;
|
||||
}
|
||||
|
||||
private static synchronized void saveLocked(@NonNull final Context context,
|
||||
@NonNull final List<Entry> entries) {
|
||||
final JSONArray array = new JSONArray();
|
||||
try {
|
||||
for (final Entry entry : entries) {
|
||||
final JSONObject object = new JSONObject();
|
||||
object.put(JSON_ID, entry.id);
|
||||
object.put(JSON_SEND_AT, entry.sendAt);
|
||||
object.put(JSON_PAYLOAD, entry.payload);
|
||||
array.put(object);
|
||||
}
|
||||
prefs(context).edit().putString(KEY_ENTRIES, array.toString()).apply();
|
||||
} catch (final Exception exception) {
|
||||
LogUtil.e(LogUtil.BUGLE_TAG, "Failed to save scheduled messages", exception);
|
||||
}
|
||||
}
|
||||
|
||||
private static SharedPreferences prefs(@NonNull final Context context) {
|
||||
final Context app = context.getApplicationContext() != null
|
||||
? context.getApplicationContext()
|
||||
: Factory.get().getApplicationContext();
|
||||
return app.getSharedPreferences(PREFS, Context.MODE_PRIVATE);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private static String encode(@NonNull final MessageData message) {
|
||||
final Parcel parcel = Parcel.obtain();
|
||||
try {
|
||||
message.writeToParcel(parcel, 0);
|
||||
return Base64.encodeToString(parcel.marshall(), Base64.NO_WRAP);
|
||||
} catch (final Exception exception) {
|
||||
LogUtil.e(LogUtil.BUGLE_TAG, "Failed to encode scheduled message", exception);
|
||||
return null;
|
||||
} finally {
|
||||
parcel.recycle();
|
||||
}
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private static MessageData decode(@Nullable final String payload) {
|
||||
if (TextUtils.isEmpty(payload)) {
|
||||
return null;
|
||||
}
|
||||
final Parcel parcel = Parcel.obtain();
|
||||
try {
|
||||
final byte[] bytes = Base64.decode(payload, Base64.NO_WRAP);
|
||||
parcel.unmarshall(bytes, 0, bytes.length);
|
||||
parcel.setDataPosition(0);
|
||||
return MessageData.CREATOR.createFromParcel(parcel);
|
||||
} catch (final Exception exception) {
|
||||
LogUtil.e(LogUtil.BUGLE_TAG, "Failed to decode scheduled message", exception);
|
||||
return null;
|
||||
} finally {
|
||||
parcel.recycle();
|
||||
}
|
||||
}
|
||||
|
||||
private static final class Entry {
|
||||
String id;
|
||||
long sendAt;
|
||||
String payload;
|
||||
}
|
||||
}
|
||||
@@ -137,7 +137,7 @@ public class ActionServiceImpl extends JobIntentService {
|
||||
* Broadcast receiver for alarms scheduled through ActionService.
|
||||
*/
|
||||
public static class PendingActionReceiver extends BroadcastReceiver {
|
||||
static final String ACTION = "com.android.messaging.datamodel.PENDING_ACTION";
|
||||
static final String ACTION = "com.bnkode.notie.datamodel.PENDING_ACTION";
|
||||
|
||||
/**
|
||||
* Allocate an intent with a specific opcode and alarm action.
|
||||
|
||||
@@ -22,6 +22,7 @@ import android.widget.Toast;
|
||||
|
||||
import com.android.messaging.Factory;
|
||||
import com.android.messaging.R;
|
||||
import com.android.messaging.ui.home.HomeToast;
|
||||
import com.android.messaging.datamodel.DataModel;
|
||||
import com.android.messaging.datamodel.data.MessageData;
|
||||
import com.android.messaging.datamodel.data.ParticipantData;
|
||||
@@ -115,13 +116,13 @@ public class BugleActionToasts {
|
||||
}
|
||||
|
||||
private static void showToast(final int messageResId) {
|
||||
ThreadUtil.getMainThreadHandler().post(() -> Toast.makeText(getApplicationContext(),
|
||||
getApplicationContext().getString(messageResId), Toast.LENGTH_LONG).show());
|
||||
ThreadUtil.getMainThreadHandler().post(() -> HomeToast.show(getApplicationContext(),
|
||||
messageResId, Toast.LENGTH_LONG));
|
||||
}
|
||||
|
||||
private static void showToast(final String message) {
|
||||
ThreadUtil.getMainThreadHandler().post(() ->
|
||||
Toast.makeText(getApplicationContext(), message, Toast.LENGTH_LONG).show());
|
||||
HomeToast.show(getApplicationContext(), message, Toast.LENGTH_LONG));
|
||||
}
|
||||
|
||||
private static Context getApplicationContext() {
|
||||
@@ -143,12 +144,11 @@ public class BugleActionToasts {
|
||||
final boolean block,
|
||||
final String destination) {
|
||||
if (success) {
|
||||
Toast.makeText(mContext,
|
||||
HomeToast.show(mContext,
|
||||
block
|
||||
? R.string.update_destination_blocked
|
||||
: R.string.update_destination_unblocked,
|
||||
Toast.LENGTH_LONG
|
||||
).show();
|
||||
Toast.LENGTH_LONG);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -132,6 +132,13 @@ public class InsertNewMessageAction extends Action implements Parcelable {
|
||||
final DatabaseWrapper db = DataModel.get().getDatabase();
|
||||
final String conversationId = message.getConversationId();
|
||||
|
||||
final ConversationListItemData conversation =
|
||||
ConversationListItemData.getExistingConversation(db, conversationId);
|
||||
if (conversation != null && conversation.getIsTrashed()) {
|
||||
LogUtil.w(TAG, "InsertNewMessageAction: conversation is in trash");
|
||||
return null;
|
||||
}
|
||||
|
||||
final ParticipantData self = getSelf(db, conversationId, message);
|
||||
if (self == null) {
|
||||
return null;
|
||||
@@ -190,6 +197,7 @@ public class InsertNewMessageAction extends Action implements Parcelable {
|
||||
messageToSend, BugleDatabaseOperations.UPDATE_MODE_CLEAR_DRAFT);
|
||||
}
|
||||
MessagingContentProvider.notifyConversationListChanged();
|
||||
MessagingContentProvider.notifyConversationMetadataChanged(conversationId);
|
||||
ProcessPendingMessagesAction.scheduleProcessPendingMessagesAction(false, this);
|
||||
|
||||
return message;
|
||||
|
||||
@@ -0,0 +1,146 @@
|
||||
/*
|
||||
* Copyright (C) 2015 The Android Open Source Project
|
||||
* Copyright (C) 2024-2025 The LineageOS Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.android.messaging.datamodel.action;
|
||||
|
||||
import android.content.ContentValues;
|
||||
import android.database.Cursor;
|
||||
import android.database.sqlite.SQLiteDoneException;
|
||||
import android.database.sqlite.SQLiteStatement;
|
||||
import android.net.Uri;
|
||||
import android.os.Parcel;
|
||||
import android.os.Parcelable;
|
||||
import android.text.TextUtils;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import com.android.messaging.datamodel.BugleDatabaseOperations;
|
||||
import com.android.messaging.datamodel.DataModel;
|
||||
import com.android.messaging.datamodel.DatabaseHelper;
|
||||
import com.android.messaging.datamodel.DatabaseHelper.MessageColumns;
|
||||
import com.android.messaging.datamodel.DatabaseWrapper;
|
||||
import com.android.messaging.datamodel.MessagingContentProvider;
|
||||
import com.android.messaging.Factory;
|
||||
import com.android.messaging.util.Assert;
|
||||
|
||||
/**
|
||||
* Marks the latest message in a conversation as unread (iOS-style thread unread).
|
||||
*/
|
||||
public class MarkAsUnreadAction extends Action implements Parcelable {
|
||||
private static final String KEY_CONVERSATION_ID = "conversation_id";
|
||||
|
||||
public static void markAsUnread(final String conversationId) {
|
||||
final MarkAsUnreadAction action = new MarkAsUnreadAction(conversationId);
|
||||
action.start();
|
||||
}
|
||||
|
||||
private MarkAsUnreadAction(final String conversationId) {
|
||||
actionParameters.putString(KEY_CONVERSATION_ID, conversationId);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Object executeAction() {
|
||||
final String conversationId = actionParameters.getString(KEY_CONVERSATION_ID);
|
||||
if (TextUtils.isEmpty(conversationId)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
final DatabaseWrapper db = DataModel.get().getDatabase();
|
||||
db.beginTransaction();
|
||||
try {
|
||||
String latestMessageId;
|
||||
try {
|
||||
final SQLiteStatement latestMessageIdSql =
|
||||
BugleDatabaseOperations.getQueryMessagesLatestMessageStatement(
|
||||
db, conversationId);
|
||||
latestMessageId = latestMessageIdSql.simpleQueryForString();
|
||||
} catch (final SQLiteDoneException e) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (TextUtils.isEmpty(latestMessageId)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
final ContentValues values = new ContentValues();
|
||||
values.put(MessageColumns.READ, 0);
|
||||
values.put(MessageColumns.SEEN, 0);
|
||||
|
||||
final int count = db.update(DatabaseHelper.MESSAGES_TABLE, values,
|
||||
MessageColumns._ID + "=? AND (" + MessageColumns.READ + " != 0 OR "
|
||||
+ MessageColumns.SEEN + " != 0)",
|
||||
new String[] { latestMessageId });
|
||||
if (count > 0) {
|
||||
updateTelephonyReadStatus(db, latestMessageId);
|
||||
MessagingContentProvider.notifyMessagesChanged(conversationId);
|
||||
MessagingContentProvider.notifyConversationListChanged();
|
||||
}
|
||||
db.setTransactionSuccessful();
|
||||
} finally {
|
||||
db.endTransaction();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private static void updateTelephonyReadStatus(final DatabaseWrapper db,
|
||||
final String messageId) {
|
||||
Assert.isNotMainThread();
|
||||
try (Cursor cursor = db.query(DatabaseHelper.MESSAGES_TABLE,
|
||||
new String[] { MessageColumns.SMS_MESSAGE_URI },
|
||||
MessageColumns._ID + "=?",
|
||||
new String[] { messageId },
|
||||
null, null, null)) {
|
||||
if (!cursor.moveToFirst()) {
|
||||
return;
|
||||
}
|
||||
final String smsMessageUri = cursor.getString(0);
|
||||
if (TextUtils.isEmpty(smsMessageUri)) {
|
||||
return;
|
||||
}
|
||||
final ContentValues telephonyValues = new ContentValues();
|
||||
telephonyValues.put("read", 0);
|
||||
telephonyValues.put("seen", 0);
|
||||
Factory.get().getApplicationContext().getContentResolver().update(
|
||||
Uri.parse(smsMessageUri),
|
||||
telephonyValues,
|
||||
null,
|
||||
null);
|
||||
}
|
||||
}
|
||||
|
||||
private MarkAsUnreadAction(final Parcel in) {
|
||||
super(in);
|
||||
}
|
||||
|
||||
public static final Parcelable.Creator<MarkAsUnreadAction> CREATOR =
|
||||
new Parcelable.Creator<>() {
|
||||
@Override
|
||||
public MarkAsUnreadAction createFromParcel(final Parcel in) {
|
||||
return new MarkAsUnreadAction(in);
|
||||
}
|
||||
|
||||
@Override
|
||||
public MarkAsUnreadAction[] newArray(final int size) {
|
||||
return new MarkAsUnreadAction[size];
|
||||
}
|
||||
};
|
||||
|
||||
@Override
|
||||
public void writeToParcel(@NonNull final Parcel parcel, final int flags) {
|
||||
writeActionToParcel(parcel, flags);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
package com.android.messaging.datamodel.action;
|
||||
|
||||
import android.database.Cursor;
|
||||
import android.os.Parcel;
|
||||
import android.os.Parcelable;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import com.android.messaging.datamodel.DataModel;
|
||||
import com.android.messaging.datamodel.DatabaseHelper;
|
||||
import com.android.messaging.datamodel.DatabaseHelper.ConversationColumns;
|
||||
import com.android.messaging.datamodel.DatabaseWrapper;
|
||||
import com.android.messaging.util.LogUtil;
|
||||
|
||||
import java.util.ArrayList;
|
||||
|
||||
/** Permanently deletes conversations that have been in Trash longer than 30 days. */
|
||||
public class PurgeExpiredTrashAction extends Action {
|
||||
|
||||
private static final String TAG = LogUtil.BUGLE_DATAMODEL_TAG;
|
||||
|
||||
public static void purgeExpired() {
|
||||
new PurgeExpiredTrashAction().start();
|
||||
}
|
||||
|
||||
private PurgeExpiredTrashAction() {
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Object executeAction() {
|
||||
final long cutoff = System.currentTimeMillis()
|
||||
- UpdateConversationTrashStatusAction.RETENTION_MS;
|
||||
final DatabaseWrapper db = DataModel.get().getDatabase();
|
||||
final ArrayList<String> ids = new ArrayList<>();
|
||||
final ArrayList<Long> timestamps = new ArrayList<>();
|
||||
try (Cursor cursor = db.query(DatabaseHelper.CONVERSATIONS_TABLE,
|
||||
new String[] { ConversationColumns._ID, ConversationColumns.SORT_TIMESTAMP },
|
||||
ConversationColumns.TRASH_TIMESTAMP + " > 0 AND "
|
||||
+ ConversationColumns.TRASH_TIMESTAMP + " < ?",
|
||||
new String[] { Long.toString(cutoff) },
|
||||
null, null, null)) {
|
||||
while (cursor != null && cursor.moveToNext()) {
|
||||
ids.add(cursor.getString(0));
|
||||
timestamps.add(cursor.getLong(1));
|
||||
}
|
||||
}
|
||||
for (int i = 0; i < ids.size(); i++) {
|
||||
DeleteConversationAction.deleteConversation(ids.get(i), timestamps.get(i));
|
||||
}
|
||||
if (!ids.isEmpty()) {
|
||||
LogUtil.i(TAG, "Purged " + ids.size() + " expired trash conversations");
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private PurgeExpiredTrashAction(final Parcel in) {
|
||||
super(in);
|
||||
}
|
||||
|
||||
public static final Parcelable.Creator<PurgeExpiredTrashAction> CREATOR
|
||||
= new Parcelable.Creator<>() {
|
||||
@Override
|
||||
public PurgeExpiredTrashAction createFromParcel(final Parcel in) {
|
||||
return new PurgeExpiredTrashAction(in);
|
||||
}
|
||||
|
||||
@Override
|
||||
public PurgeExpiredTrashAction[] newArray(final int size) {
|
||||
return new PurgeExpiredTrashAction[size];
|
||||
}
|
||||
};
|
||||
|
||||
@Override
|
||||
public void writeToParcel(@NonNull final Parcel parcel, final int flags) {
|
||||
writeActionToParcel(parcel, flags);
|
||||
}
|
||||
}
|
||||
+95
@@ -0,0 +1,95 @@
|
||||
/*
|
||||
* Copyright (C) 2025 The LineageOS Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.android.messaging.datamodel.action;
|
||||
|
||||
import android.content.ContentValues;
|
||||
import android.os.Parcel;
|
||||
import android.os.Parcelable;
|
||||
import android.text.TextUtils;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import com.android.messaging.datamodel.BugleDatabaseOperations;
|
||||
import com.android.messaging.datamodel.DataModel;
|
||||
import com.android.messaging.datamodel.DatabaseHelper.ConversationColumns;
|
||||
import com.android.messaging.datamodel.DatabaseWrapper;
|
||||
import com.android.messaging.datamodel.MessagingContentProvider;
|
||||
|
||||
/**
|
||||
* Gives a conversation a custom name, used when creating a group chat. The name lives only in
|
||||
* the local database, so it is visible to this device only.
|
||||
*/
|
||||
public class RenameConversationAction extends Action implements Parcelable {
|
||||
private static final String KEY_CONVERSATION_ID = "conversation_id";
|
||||
private static final String KEY_NAME = "name";
|
||||
|
||||
public static void renameConversation(final String conversationId, final String name) {
|
||||
final RenameConversationAction action = new RenameConversationAction(conversationId, name);
|
||||
action.start();
|
||||
}
|
||||
|
||||
private RenameConversationAction(final String conversationId, final String name) {
|
||||
actionParameters.putString(KEY_CONVERSATION_ID, conversationId);
|
||||
actionParameters.putString(KEY_NAME, name);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Object executeAction() {
|
||||
final String conversationId = actionParameters.getString(KEY_CONVERSATION_ID);
|
||||
final String name = actionParameters.getString(KEY_NAME);
|
||||
if (TextUtils.isEmpty(conversationId) || TextUtils.isEmpty(name)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
final DatabaseWrapper db = DataModel.get().getDatabase();
|
||||
db.beginTransaction();
|
||||
try {
|
||||
final ContentValues values = new ContentValues();
|
||||
values.put(ConversationColumns.NAME, name);
|
||||
BugleDatabaseOperations.updateConversationRowIfExists(db, conversationId, values);
|
||||
db.setTransactionSuccessful();
|
||||
} finally {
|
||||
db.endTransaction();
|
||||
}
|
||||
|
||||
MessagingContentProvider.notifyConversationMetadataChanged(conversationId);
|
||||
MessagingContentProvider.notifyConversationListChanged();
|
||||
return null;
|
||||
}
|
||||
|
||||
private RenameConversationAction(final Parcel in) {
|
||||
super(in);
|
||||
}
|
||||
|
||||
public static final Parcelable.Creator<RenameConversationAction> CREATOR =
|
||||
new Parcelable.Creator<>() {
|
||||
@Override
|
||||
public RenameConversationAction createFromParcel(final Parcel in) {
|
||||
return new RenameConversationAction(in);
|
||||
}
|
||||
|
||||
@Override
|
||||
public RenameConversationAction[] newArray(final int size) {
|
||||
return new RenameConversationAction[size];
|
||||
}
|
||||
};
|
||||
|
||||
@Override
|
||||
public void writeToParcel(@NonNull final Parcel parcel, final int flags) {
|
||||
writeActionToParcel(parcel, flags);
|
||||
}
|
||||
}
|
||||
@@ -28,6 +28,7 @@ import com.android.messaging.datamodel.DataModel;
|
||||
import com.android.messaging.datamodel.DatabaseHelper.MessageColumns;
|
||||
import com.android.messaging.datamodel.DatabaseWrapper;
|
||||
import com.android.messaging.datamodel.MessagingContentProvider;
|
||||
import com.android.messaging.datamodel.data.ConversationListItemData;
|
||||
import com.android.messaging.datamodel.data.MessageData;
|
||||
import com.android.messaging.util.LogUtil;
|
||||
|
||||
@@ -70,6 +71,12 @@ public class ResendMessageAction extends Action implements Parcelable {
|
||||
final MessageData message = BugleDatabaseOperations.readMessage(db, messageId);
|
||||
// Check message can be resent
|
||||
if (message != null && message.canResendMessage()) {
|
||||
final ConversationListItemData conversation =
|
||||
ConversationListItemData.getExistingConversation(db, message.getConversationId());
|
||||
if (conversation != null && conversation.getIsTrashed()) {
|
||||
LogUtil.w(TAG, "ResendMessageAction: conversation is in trash");
|
||||
return null;
|
||||
}
|
||||
final boolean isMms = message.getIsMms();
|
||||
long timestamp = System.currentTimeMillis();
|
||||
if (isMms) {
|
||||
|
||||
+86
@@ -0,0 +1,86 @@
|
||||
package com.android.messaging.datamodel.action;
|
||||
|
||||
import android.os.Parcel;
|
||||
import android.os.Parcelable;
|
||||
import android.text.TextUtils;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import com.android.messaging.Factory;
|
||||
import com.android.messaging.datamodel.BugleDatabaseOperations;
|
||||
import com.android.messaging.datamodel.DataModel;
|
||||
import com.android.messaging.datamodel.DatabaseWrapper;
|
||||
import com.android.messaging.datamodel.MessagingContentProvider;
|
||||
import com.android.messaging.ui.home.PinnedConversationStore;
|
||||
import com.android.messaging.util.Assert;
|
||||
|
||||
public class UpdateConversationTrashStatusAction extends Action {
|
||||
|
||||
public static final long RETENTION_MS = 30L * 24 * 60 * 60 * 1000;
|
||||
|
||||
public static void moveToTrash(final String conversationId) {
|
||||
final UpdateConversationTrashStatusAction action =
|
||||
new UpdateConversationTrashStatusAction(conversationId, true);
|
||||
action.start();
|
||||
}
|
||||
|
||||
public static void restoreFromTrash(final String conversationId) {
|
||||
final UpdateConversationTrashStatusAction action =
|
||||
new UpdateConversationTrashStatusAction(conversationId, false);
|
||||
action.start();
|
||||
}
|
||||
|
||||
private static final String KEY_CONVERSATION_ID = "conversation_id";
|
||||
private static final String KEY_TO_TRASH = "to_trash";
|
||||
|
||||
private UpdateConversationTrashStatusAction(final String conversationId,
|
||||
final boolean toTrash) {
|
||||
Assert.isTrue(!TextUtils.isEmpty(conversationId));
|
||||
actionParameters.putString(KEY_CONVERSATION_ID, conversationId);
|
||||
actionParameters.putBoolean(KEY_TO_TRASH, toTrash);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Object executeAction() {
|
||||
final String conversationId = actionParameters.getString(KEY_CONVERSATION_ID);
|
||||
final boolean toTrash = actionParameters.getBoolean(KEY_TO_TRASH);
|
||||
final DatabaseWrapper db = DataModel.get().getDatabase();
|
||||
db.beginTransaction();
|
||||
try {
|
||||
BugleDatabaseOperations.updateConversationTrashTimestampInTransaction(
|
||||
db, conversationId, toTrash ? System.currentTimeMillis() : 0L);
|
||||
db.setTransactionSuccessful();
|
||||
} finally {
|
||||
db.endTransaction();
|
||||
}
|
||||
if (toTrash) {
|
||||
PinnedConversationStore.unpin(Factory.get().getApplicationContext(), conversationId);
|
||||
UpdateMessageNotificationAction.updateMessageNotification();
|
||||
}
|
||||
MessagingContentProvider.notifyConversationListChanged();
|
||||
MessagingContentProvider.notifyConversationMetadataChanged(conversationId);
|
||||
return null;
|
||||
}
|
||||
|
||||
private UpdateConversationTrashStatusAction(final Parcel in) {
|
||||
super(in);
|
||||
}
|
||||
|
||||
public static final Parcelable.Creator<UpdateConversationTrashStatusAction> CREATOR
|
||||
= new Parcelable.Creator<>() {
|
||||
@Override
|
||||
public UpdateConversationTrashStatusAction createFromParcel(final Parcel in) {
|
||||
return new UpdateConversationTrashStatusAction(in);
|
||||
}
|
||||
|
||||
@Override
|
||||
public UpdateConversationTrashStatusAction[] newArray(final int size) {
|
||||
return new UpdateConversationTrashStatusAction[size];
|
||||
}
|
||||
};
|
||||
|
||||
@Override
|
||||
public void writeToParcel(@NonNull final Parcel parcel, final int flags) {
|
||||
writeActionToParcel(parcel, flags);
|
||||
}
|
||||
}
|
||||
@@ -20,6 +20,7 @@ package com.android.messaging.datamodel.data;
|
||||
import android.content.Context;
|
||||
import android.database.Cursor;
|
||||
import android.os.Bundle;
|
||||
import android.text.TextUtils;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.loader.app.LoaderManager;
|
||||
@@ -46,6 +47,13 @@ public class ContactPickerData extends BindableData implements
|
||||
void onAllContactsCursorUpdated(Cursor data);
|
||||
void onFrequentContactsCursorUpdated(Cursor data);
|
||||
void onContactCustomColorLoaded(ContactPickerData data);
|
||||
|
||||
/**
|
||||
* Results for the latest {@link ContactPickerData#setSearchText}, or null once the
|
||||
* search is cleared.
|
||||
*/
|
||||
default void onFilteredContactsCursorUpdated(Cursor data) {
|
||||
}
|
||||
}
|
||||
|
||||
private static final String BINDING_ID = "bindingId";
|
||||
@@ -53,6 +61,8 @@ public class ContactPickerData extends BindableData implements
|
||||
private LoaderManager mLoaderManager;
|
||||
private ContactPickerDataListener mListener;
|
||||
private final FrequentContactsCursorBuilder mFrequentContactsCursorBuilder;
|
||||
private String mSearchText;
|
||||
private ContactUtil.AccountFilter mAccountFilter;
|
||||
|
||||
public ContactPickerData(final Context context, final ContactPickerDataListener listener) {
|
||||
mListener = listener;
|
||||
@@ -63,6 +73,7 @@ public class ContactPickerData extends BindableData implements
|
||||
private static final int ALL_CONTACTS_LOADER = 1;
|
||||
private static final int FREQUENT_CONTACTS_LOADER = 2;
|
||||
private static final int PARTICIPANT_LOADER = 3;
|
||||
private static final int FILTERED_CONTACTS_LOADER = 4;
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
@@ -72,7 +83,7 @@ public class ContactPickerData extends BindableData implements
|
||||
if (isBound(bindingId)) {
|
||||
switch (id) {
|
||||
case ALL_CONTACTS_LOADER:
|
||||
return ContactUtil.getPhones(mContext)
|
||||
return ContactUtil.getPhones(mContext, mAccountFilter)
|
||||
.createBoundCursorLoader(bindingId);
|
||||
case FREQUENT_CONTACTS_LOADER:
|
||||
return ContactUtil.getFrequentContacts(mContext)
|
||||
@@ -81,6 +92,9 @@ public class ContactPickerData extends BindableData implements
|
||||
return new BoundCursorLoader(bindingId, mContext,
|
||||
MessagingContentProvider.PARTICIPANTS_URI,
|
||||
ParticipantData.ParticipantsQuery.PROJECTION, null, null, null);
|
||||
case FILTERED_CONTACTS_LOADER:
|
||||
return ContactUtil.filterDestination(mContext, mSearchText, mAccountFilter)
|
||||
.createBoundCursorLoader(bindingId);
|
||||
default:
|
||||
Assert.fail("Unknown loader id for contact picker!");
|
||||
break;
|
||||
@@ -108,12 +122,16 @@ public class ContactPickerData extends BindableData implements
|
||||
case PARTICIPANT_LOADER:
|
||||
mListener.onContactCustomColorLoaded(this);
|
||||
break;
|
||||
case FILTERED_CONTACTS_LOADER:
|
||||
mListener.onFilteredContactsCursorUpdated(data);
|
||||
break;
|
||||
default:
|
||||
Assert.fail("Unknown loader id for contact picker!");
|
||||
break;
|
||||
}
|
||||
|
||||
if (loader.getId() != PARTICIPANT_LOADER) {
|
||||
if (loader.getId() == ALL_CONTACTS_LOADER
|
||||
|| loader.getId() == FREQUENT_CONTACTS_LOADER) {
|
||||
// The frequent contacts cursor to be used in the UI depends on results from both
|
||||
// all contacts and frequent contacts loader, and we don't know which will finish
|
||||
// first. Therefore, try to build the cursor and notify the listener if it's
|
||||
@@ -151,6 +169,9 @@ public class ContactPickerData extends BindableData implements
|
||||
case PARTICIPANT_LOADER:
|
||||
mListener.onContactCustomColorLoaded(this);
|
||||
break;
|
||||
case FILTERED_CONTACTS_LOADER:
|
||||
mListener.onFilteredContactsCursorUpdated(null);
|
||||
break;
|
||||
default:
|
||||
Assert.fail("Unknown loader id for contact picker!");
|
||||
break;
|
||||
@@ -170,6 +191,56 @@ public class ContactPickerData extends BindableData implements
|
||||
mLoaderManager.initLoader(PARTICIPANT_LOADER, args, this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Narrows the contact list down to destinations matching {@code searchText}. Passing empty
|
||||
* text clears the search and reports a null cursor.
|
||||
*/
|
||||
public void setSearchText(final BindingBase<ContactPickerData> binding,
|
||||
final String searchText) {
|
||||
if (mLoaderManager == null) {
|
||||
return;
|
||||
}
|
||||
final String trimmed = searchText == null ? null : searchText.trim();
|
||||
final String normalized = TextUtils.isEmpty(trimmed) ? null : trimmed;
|
||||
if (TextUtils.equals(mSearchText, normalized)) {
|
||||
return;
|
||||
}
|
||||
mSearchText = normalized;
|
||||
if (mSearchText == null) {
|
||||
mLoaderManager.destroyLoader(FILTERED_CONTACTS_LOADER);
|
||||
if (mListener != null) {
|
||||
mListener.onFilteredContactsCursorUpdated(null);
|
||||
}
|
||||
return;
|
||||
}
|
||||
final Bundle args = new Bundle();
|
||||
args.putString(BINDING_ID, binding.getBindingId());
|
||||
mLoaderManager.restartLoader(FILTERED_CONTACTS_LOADER, args, this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reloads the list for one contacts account, or every account when {@code filter} is null.
|
||||
*/
|
||||
public void setAccountFilter(final BindingBase<ContactPickerData> binding,
|
||||
final ContactUtil.AccountFilter filter) {
|
||||
if (mLoaderManager == null) {
|
||||
mAccountFilter = filter;
|
||||
return;
|
||||
}
|
||||
final boolean same = (filter == null && mAccountFilter == null)
|
||||
|| (filter != null && filter.matches(mAccountFilter));
|
||||
if (same) {
|
||||
return;
|
||||
}
|
||||
mAccountFilter = filter;
|
||||
final Bundle args = new Bundle();
|
||||
args.putString(BINDING_ID, binding.getBindingId());
|
||||
mLoaderManager.restartLoader(ALL_CONTACTS_LOADER, args, this);
|
||||
if (mSearchText != null) {
|
||||
mLoaderManager.restartLoader(FILTERED_CONTACTS_LOADER, args, this);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void unregisterListeners() {
|
||||
mListener = null;
|
||||
@@ -180,6 +251,7 @@ public class ContactPickerData extends BindableData implements
|
||||
mLoaderManager.destroyLoader(ALL_CONTACTS_LOADER);
|
||||
mLoaderManager.destroyLoader(FREQUENT_CONTACTS_LOADER);
|
||||
mLoaderManager.destroyLoader(PARTICIPANT_LOADER);
|
||||
mLoaderManager.destroyLoader(FILTERED_CONTACTS_LOADER);
|
||||
mLoaderManager = null;
|
||||
}
|
||||
mFrequentContactsCursorBuilder.resetBuilder();
|
||||
|
||||
@@ -42,6 +42,7 @@ import com.android.messaging.datamodel.action.InsertNewMessageAction;
|
||||
import com.android.messaging.datamodel.action.RedownloadMmsAction;
|
||||
import com.android.messaging.datamodel.action.ResendMessageAction;
|
||||
import com.android.messaging.datamodel.action.UpdateConversationArchiveStatusAction;
|
||||
import com.android.messaging.datamodel.action.UpdateConversationTrashStatusAction;
|
||||
import com.android.messaging.datamodel.binding.BindableData;
|
||||
import com.android.messaging.datamodel.binding.Binding;
|
||||
import com.android.messaging.datamodel.binding.BindingBase;
|
||||
@@ -460,7 +461,11 @@ public class ConversationData extends BindableData {
|
||||
}
|
||||
|
||||
public boolean getIsArchived() {
|
||||
return mConversationMetadata.getIsArchived();
|
||||
return mConversationMetadata != null && mConversationMetadata.getIsArchived();
|
||||
}
|
||||
|
||||
public boolean getIsTrashed() {
|
||||
return mConversationMetadata != null && mConversationMetadata.getIsTrashed();
|
||||
}
|
||||
|
||||
public String getIcon() {
|
||||
@@ -569,6 +574,12 @@ public class ConversationData extends BindableData {
|
||||
final MessageData message) {
|
||||
Assert.isTrue(TextUtils.equals(mConversationId, message.getConversationId()));
|
||||
Assert.isTrue(binding.getData() == this);
|
||||
if (getIsTrashed()) {
|
||||
return;
|
||||
}
|
||||
if (getIsArchived()) {
|
||||
unarchiveConversation(binding);
|
||||
}
|
||||
|
||||
if (message.getSelfId() == null) {
|
||||
InsertNewMessageAction.insertNewMessage(message);
|
||||
@@ -612,6 +623,12 @@ public class ConversationData extends BindableData {
|
||||
public void resendMessage(final BindingBase<ConversationData> binding, final String messageId) {
|
||||
Assert.isTrue(binding.getData() == this);
|
||||
Assert.notNull(messageId);
|
||||
if (getIsTrashed()) {
|
||||
return;
|
||||
}
|
||||
if (getIsArchived()) {
|
||||
unarchiveConversation(binding);
|
||||
}
|
||||
ResendMessageAction.resendMessage(messageId);
|
||||
}
|
||||
|
||||
@@ -642,6 +659,16 @@ public class ConversationData extends BindableData {
|
||||
UpdateConversationArchiveStatusAction.unarchiveConversation(mConversationId);
|
||||
}
|
||||
|
||||
public void moveConversationToTrash(final BindingBase<ConversationData> binding) {
|
||||
Assert.isTrue(binding.getData() == this);
|
||||
UpdateConversationTrashStatusAction.moveToTrash(mConversationId);
|
||||
}
|
||||
|
||||
public void restoreConversationFromTrash(final BindingBase<ConversationData> binding) {
|
||||
Assert.isTrue(binding.getData() == this);
|
||||
UpdateConversationTrashStatusAction.restoreFromTrash(mConversationId);
|
||||
}
|
||||
|
||||
public ConversationParticipantsData getParticipants() {
|
||||
return mParticipantData;
|
||||
}
|
||||
|
||||
@@ -52,6 +52,12 @@ public class ConversationListData extends BindableData
|
||||
"(" + ConversationListViewColumns.ARCHIVE_STATUS + " = 1)";
|
||||
public static final String WHERE_NOT_ARCHIVED =
|
||||
"(" + ConversationListViewColumns.ARCHIVE_STATUS + " = 0)";
|
||||
public static final String WHERE_NOT_TRASHED =
|
||||
"(" + ConversationListViewColumns.TRASH_TIMESTAMP + " = 0)";
|
||||
public static final String WHERE_TRASHED =
|
||||
"(" + ConversationListViewColumns.TRASH_TIMESTAMP + " > 0)";
|
||||
public static final String WHERE_INBOX = WHERE_NOT_ARCHIVED + " AND " + WHERE_NOT_TRASHED;
|
||||
public static final String WHERE_ARCHIVED_VISIBLE = WHERE_ARCHIVED + " AND " + WHERE_NOT_TRASHED;
|
||||
|
||||
public interface ConversationListDataListener {
|
||||
void onConversationListCursorUpdated(ConversationListData data, Cursor cursor);
|
||||
@@ -61,13 +67,20 @@ public class ConversationListData extends BindableData
|
||||
private ConversationListDataListener mListener;
|
||||
private final Context mContext;
|
||||
private final boolean mArchivedMode;
|
||||
private final boolean mTrashMode;
|
||||
private LoaderManager mLoaderManager;
|
||||
|
||||
public ConversationListData(final Context context, final ConversationListDataListener listener,
|
||||
final boolean archivedMode) {
|
||||
this(context, listener, archivedMode, false);
|
||||
}
|
||||
|
||||
public ConversationListData(final Context context, final ConversationListDataListener listener,
|
||||
final boolean archivedMode, final boolean trashMode) {
|
||||
mListener = listener;
|
||||
mContext = context;
|
||||
mArchivedMode = archivedMode;
|
||||
mTrashMode = trashMode;
|
||||
mArchivedMode = archivedMode && !trashMode;
|
||||
}
|
||||
|
||||
private static final int CONVERSATION_LIST_LOADER = 1;
|
||||
@@ -101,7 +114,7 @@ public class ConversationListData extends BindableData
|
||||
loader = new BoundCursorLoader(bindingId, mContext,
|
||||
MessagingContentProvider.CONVERSATIONS_URI,
|
||||
ConversationListItemData.PROJECTION,
|
||||
mArchivedMode ? WHERE_ARCHIVED : WHERE_NOT_ARCHIVED,
|
||||
listSelection(),
|
||||
null, // selection args
|
||||
SORT_ORDER);
|
||||
break;
|
||||
@@ -212,4 +225,18 @@ public class ConversationListData extends BindableData
|
||||
public HashSet<String> getBlockedParticipants() {
|
||||
return mBlockedParticipants;
|
||||
}
|
||||
|
||||
public boolean isTrashMode() {
|
||||
return mTrashMode;
|
||||
}
|
||||
|
||||
private String listSelection() {
|
||||
if (mTrashMode) {
|
||||
return WHERE_TRASHED;
|
||||
}
|
||||
if (mArchivedMode) {
|
||||
return WHERE_ARCHIVED_VISIBLE;
|
||||
}
|
||||
return WHERE_INBOX;
|
||||
}
|
||||
}
|
||||
|
||||
+22
-1
@@ -28,6 +28,7 @@ import com.android.messaging.datamodel.DatabaseHelper.MessageColumns;
|
||||
import com.android.messaging.datamodel.DatabaseHelper.ParticipantColumns;
|
||||
import com.android.messaging.datamodel.DatabaseWrapper;
|
||||
import com.android.messaging.datamodel.action.DeleteConversationAction;
|
||||
import com.android.messaging.datamodel.action.UpdateConversationTrashStatusAction;
|
||||
import com.android.messaging.util.Assert;
|
||||
import com.android.messaging.util.ContactUtil;
|
||||
import com.android.messaging.util.Dates;
|
||||
@@ -61,6 +62,7 @@ public class ConversationListItemData {
|
||||
private String mDraftPreviewContentType;
|
||||
private String mDraftSnippetText;
|
||||
private boolean mIsArchived;
|
||||
private long mTrashTimestamp;
|
||||
private String mSubject;
|
||||
private String mDraftSubject;
|
||||
private String mSnippetSenderFirstName;
|
||||
@@ -110,6 +112,8 @@ public class ConversationListItemData {
|
||||
}
|
||||
|
||||
mIsArchived = cursor.getInt(INDEX_ARCHIVE_STATUS) == 1;
|
||||
final int trashIndex = cursor.getColumnIndex(ConversationListViewColumns.TRASH_TIMESTAMP);
|
||||
mTrashTimestamp = trashIndex >= 0 ? cursor.getLong(trashIndex) : 0;
|
||||
mSubject = cursor.getString(INDEX_SUBJECT_TEXT);
|
||||
mSnippetSenderFirstName = cursor.getString(INDEX_SNIPPET_SENDER_FIRST_NAME);
|
||||
mSnippetSenderDisplayDestination =
|
||||
@@ -240,6 +244,14 @@ public class ConversationListItemData {
|
||||
return mIsArchived;
|
||||
}
|
||||
|
||||
public boolean getIsTrashed() {
|
||||
return mTrashTimestamp > 0;
|
||||
}
|
||||
|
||||
public long getTrashTimestamp() {
|
||||
return mTrashTimestamp;
|
||||
}
|
||||
|
||||
public String getSubject() {
|
||||
return mSubject;
|
||||
}
|
||||
@@ -256,7 +268,11 @@ public class ConversationListItemData {
|
||||
}
|
||||
|
||||
public void deleteConversation() {
|
||||
DeleteConversationAction.deleteConversation(mConversationId, mTimestamp);
|
||||
if (getIsTrashed()) {
|
||||
DeleteConversationAction.deleteConversation(mConversationId, mTimestamp);
|
||||
} else {
|
||||
UpdateConversationTrashStatusAction.moveToTrash(mConversationId);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -281,6 +297,8 @@ public class ConversationListItemData {
|
||||
+ " as " + ConversationListViewColumns.CURRENT_SELF_ID + ", "
|
||||
+ DatabaseHelper.CONVERSATIONS_TABLE + '.' + ConversationColumns.ARCHIVE_STATUS
|
||||
+ " as " + ConversationListViewColumns.ARCHIVE_STATUS + ", "
|
||||
+ DatabaseHelper.CONVERSATIONS_TABLE + '.' + ConversationColumns.TRASH_TIMESTAMP
|
||||
+ " as " + ConversationListViewColumns.TRASH_TIMESTAMP + ", "
|
||||
+ DatabaseHelper.MESSAGES_TABLE + '.' + MessageColumns.READ
|
||||
+ " as " + ConversationListViewColumns.READ + ", "
|
||||
+ DatabaseHelper.CONVERSATIONS_TABLE + '.' + ConversationColumns.ICON
|
||||
@@ -354,6 +372,7 @@ public class ConversationListItemData {
|
||||
public static final String _ID = ConversationColumns._ID;
|
||||
static final String NAME = ConversationColumns.NAME;
|
||||
static final String ARCHIVE_STATUS = ConversationColumns.ARCHIVE_STATUS;
|
||||
static final String TRASH_TIMESTAMP = ConversationColumns.TRASH_TIMESTAMP;
|
||||
static final String READ = MessageColumns.READ;
|
||||
static final String SORT_TIMESTAMP = ConversationColumns.SORT_TIMESTAMP;
|
||||
static final String PREVIEW_URI = ConversationColumns.PREVIEW_URI;
|
||||
@@ -412,6 +431,7 @@ public class ConversationListItemData {
|
||||
ConversationListViewColumns.SNIPPET_SENDER_FIRST_NAME,
|
||||
ConversationListViewColumns.SNIPPET_SENDER_DISPLAY_DESTINATION,
|
||||
ConversationListViewColumns.IS_ENTERPRISE,
|
||||
ConversationListViewColumns.TRASH_TIMESTAMP,
|
||||
};
|
||||
|
||||
private static final int INDEX_ID = 0;
|
||||
@@ -441,6 +461,7 @@ public class ConversationListItemData {
|
||||
private static final int INDEX_SNIPPET_SENDER_FIRST_NAME = 24;
|
||||
private static final int INDEX_SNIPPET_SENDER_DISPLAY_DESTINATION = 25;
|
||||
private static final int INDEX_IS_ENTERPRISE = 26;
|
||||
private static final int INDEX_TRASH_TIMESTAMP = 27;
|
||||
|
||||
private static final String DIVIDER_TEXT = ", ";
|
||||
|
||||
|
||||
+125
-13
@@ -367,27 +367,25 @@ public class ConversationMessageData {
|
||||
* @return the text that is a concatenation of all text parts
|
||||
*/
|
||||
public String getText() {
|
||||
// This is optimized for single text part case, which is the majority
|
||||
return stripDisplayText(getRawText());
|
||||
}
|
||||
|
||||
// For single text part, we just return the part without creating the StringBuilder
|
||||
public String getRawText() {
|
||||
// Optimized for the common single text part case.
|
||||
String firstTextPart = null;
|
||||
boolean foundText = false;
|
||||
// For multiple text parts, we need the StringBuilder and the separator for concatenation
|
||||
StringBuilder sb = null;
|
||||
String separator = null;
|
||||
for (final MessagePartData part : mParts) {
|
||||
if (part.isText()) {
|
||||
if (!foundText) {
|
||||
// First text part
|
||||
firstTextPart = part.getText();
|
||||
foundText = true;
|
||||
} else {
|
||||
// Second and beyond
|
||||
if (sb == null) {
|
||||
// Need the StringBuilder and the separator starting from 2nd text part
|
||||
sb = new StringBuilder();
|
||||
if (!TextUtils.isEmpty(firstTextPart)) {
|
||||
sb.append(firstTextPart);
|
||||
sb.append(firstTextPart);
|
||||
}
|
||||
separator = BugleGservicesKeys.MMS_TEXT_CONCAT_SEPARATOR_DEFAULT;
|
||||
}
|
||||
@@ -401,13 +399,112 @@ public class ConversationMessageData {
|
||||
}
|
||||
}
|
||||
}
|
||||
if (sb == null) {
|
||||
// Only one text part
|
||||
return firstTextPart;
|
||||
} else {
|
||||
// More than one
|
||||
return sb.toString();
|
||||
return sb == null ? firstTextPart : sb.toString();
|
||||
}
|
||||
|
||||
private static String stripDisplayText(final String text) {
|
||||
return sanitizeDisplayText(text);
|
||||
}
|
||||
|
||||
public static String sanitizeDisplayText(final String text) {
|
||||
if (text == null) {
|
||||
return null;
|
||||
}
|
||||
final StringBuilder out = new StringBuilder(text.length());
|
||||
boolean lineStart = true;
|
||||
for (int i = 0; i < text.length(); ) {
|
||||
final int cp = text.codePointAt(i);
|
||||
if (lineStart && isDisplayPadding(cp)) {
|
||||
i += Character.charCount(cp);
|
||||
continue;
|
||||
}
|
||||
lineStart = cp == '\n' || cp == '\r';
|
||||
out.appendCodePoint(cp);
|
||||
i += Character.charCount(cp);
|
||||
}
|
||||
int end = out.length();
|
||||
while (end > 0 && isDisplayPadding(out.charAt(end - 1))) {
|
||||
end--;
|
||||
}
|
||||
return end <= 0 ? null : out.substring(0, end);
|
||||
}
|
||||
|
||||
/**
|
||||
* Keep original spaces but flatten body line breaks so list / peek preview
|
||||
* wraps by width only.
|
||||
*/
|
||||
public static String flattenPreviewText(final String text) {
|
||||
if (text == null) {
|
||||
return null;
|
||||
}
|
||||
final StringBuilder out = new StringBuilder(text.length());
|
||||
boolean pendingBreak = false;
|
||||
for (int i = 0; i < text.length(); ) {
|
||||
final int cp = text.codePointAt(i);
|
||||
if (cp == '\n' || cp == '\r') {
|
||||
pendingBreak = true;
|
||||
} else {
|
||||
if (pendingBreak && out.length() > 0) {
|
||||
out.append(' ');
|
||||
}
|
||||
pendingBreak = false;
|
||||
out.appendCodePoint(cp);
|
||||
}
|
||||
i += Character.charCount(cp);
|
||||
}
|
||||
return out.length() == 0 ? null : out.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Drop spaces next to CJK so wrapped lines do not start with a leftover space.
|
||||
*/
|
||||
public static String collapseCjkWrapSpaces(final String text) {
|
||||
if (text == null || text.isEmpty()) {
|
||||
return text;
|
||||
}
|
||||
final StringBuilder out = new StringBuilder(text.length());
|
||||
for (int i = 0; i < text.length(); ) {
|
||||
final int cp = text.codePointAt(i);
|
||||
final int n = Character.charCount(cp);
|
||||
if (cp == ' ' || cp == 0x00A0 || cp == 0x3000) {
|
||||
final int prev = out.length() > 0 ? out.codePointBefore(out.length()) : 0;
|
||||
final int next = i + n < text.length() ? text.codePointAt(i + n) : 0;
|
||||
if (isCjk(prev) || isCjk(next)) {
|
||||
i += n;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
out.appendCodePoint(cp);
|
||||
i += n;
|
||||
}
|
||||
return out.toString();
|
||||
}
|
||||
|
||||
private static boolean isCjk(final int cp) {
|
||||
return Character.UnicodeBlock.of(cp) == Character.UnicodeBlock.HANGUL_SYLLABLES
|
||||
|| Character.UnicodeBlock.of(cp) == Character.UnicodeBlock.HANGUL_JAMO
|
||||
|| Character.UnicodeBlock.of(cp) == Character.UnicodeBlock.HANGUL_COMPATIBILITY_JAMO
|
||||
|| Character.UnicodeBlock.of(cp) == Character.UnicodeBlock.CJK_UNIFIED_IDEOGRAPHS
|
||||
|| Character.UnicodeBlock.of(cp) == Character.UnicodeBlock.CJK_SYMBOLS_AND_PUNCTUATION;
|
||||
}
|
||||
|
||||
private static boolean isDisplayPadding(final int cp) {
|
||||
final int type = Character.getType(cp);
|
||||
return Character.isWhitespace(cp)
|
||||
|| Character.isSpaceChar(cp)
|
||||
|| Character.isISOControl(cp)
|
||||
|| type == Character.FORMAT
|
||||
|| type == Character.CONTROL
|
||||
|| cp == 0x00A0
|
||||
|| cp == 0x1680
|
||||
|| cp == 0x180E
|
||||
|| cp == 0x200B
|
||||
|| cp == 0x200C
|
||||
|| cp == 0x200D
|
||||
|| cp == 0x2060
|
||||
|| cp == 0x3000
|
||||
|| cp == 0x3164
|
||||
|| cp == 0xFEFF;
|
||||
}
|
||||
|
||||
public boolean hasAttachments() {
|
||||
@@ -619,6 +716,21 @@ public class ConversationMessageData {
|
||||
+ CONVERSATION_MESSAGES_QUERY_SQL_GROUP_BY;
|
||||
}
|
||||
|
||||
/**
|
||||
* Same rows as {@link #getConversationMessagesQuerySql}, narrowed to the messages that have
|
||||
* a part matching the search term. Takes the conversation id then the LIKE pattern.
|
||||
*/
|
||||
public static String getConversationMessagesSearchQuerySql() {
|
||||
return CONVERSATION_MESSAGES_QUERY_SQL
|
||||
+ " AND "
|
||||
+ DatabaseHelper.MESSAGES_TABLE + "." + MessageColumns.CONVERSATION_ID + "=?"
|
||||
+ " AND "
|
||||
+ DatabaseHelper.MESSAGES_TABLE + "." + MessageColumns._ID + " IN (SELECT "
|
||||
+ PartColumns.MESSAGE_ID + " FROM " + DatabaseHelper.PARTS_TABLE
|
||||
+ " WHERE " + PartColumns.TEXT + " LIKE ? ESCAPE '\\'))"
|
||||
+ CONVERSATION_MESSAGES_QUERY_SQL_GROUP_BY;
|
||||
}
|
||||
|
||||
static String getConversationMessageIdsQuerySql() {
|
||||
return CONVERSATION_MESSAGES_IDS_QUERY_SQL
|
||||
+ " AND "
|
||||
|
||||
+82
-81
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
* Copyright (C) 2015 The Android Open Source Project
|
||||
* Copyright (C) 2024 The LineageOS Project
|
||||
* Copyright (C) 2025 The LineageOS Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -38,125 +38,122 @@ import com.android.messaging.util.LogUtil;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Services data needs for PeopleAndOptionsFragment.
|
||||
* Serves the conversation metadata and the participant list needed by the conversation profile
|
||||
* screen.
|
||||
*/
|
||||
public class PeopleAndOptionsData extends BindableData implements
|
||||
public class ConversationProfileData extends BindableData implements
|
||||
LoaderManager.LoaderCallbacks<Cursor> {
|
||||
public interface PeopleAndOptionsDataListener {
|
||||
void onOptionsCursorUpdated(PeopleAndOptionsData data, Cursor cursor);
|
||||
void onParticipantsListLoaded(PeopleAndOptionsData data,
|
||||
public interface ConversationProfileDataListener {
|
||||
void onConversationMetadataUpdated(ConversationProfileData data,
|
||||
ConversationListItemData conversation);
|
||||
void onParticipantsListLoaded(ConversationProfileData data,
|
||||
List<ParticipantData> participants);
|
||||
}
|
||||
|
||||
private static final String BINDING_ID = "bindingId";
|
||||
private static final int CONVERSATION_METADATA_LOADER = 1;
|
||||
private static final int PARTICIPANT_LOADER = 2;
|
||||
|
||||
private final Context mContext;
|
||||
private final String mConversationId;
|
||||
private final ConversationParticipantsData mParticipantData;
|
||||
private LoaderManager mLoaderManager;
|
||||
private PeopleAndOptionsDataListener mListener;
|
||||
private ConversationProfileDataListener mListener;
|
||||
|
||||
public PeopleAndOptionsData(final String conversationId, final Context context,
|
||||
final PeopleAndOptionsDataListener listener) {
|
||||
public ConversationProfileData(final String conversationId, final Context context,
|
||||
final ConversationProfileDataListener listener) {
|
||||
mListener = listener;
|
||||
mContext = context;
|
||||
mConversationId = conversationId;
|
||||
mParticipantData = new ConversationParticipantsData();
|
||||
}
|
||||
|
||||
private static final int CONVERSATION_OPTIONS_LOADER = 1;
|
||||
private static final int PARTICIPANT_LOADER = 2;
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public Loader<Cursor> onCreateLoader(final int id, final Bundle args) {
|
||||
final String bindingId = args.getString(BINDING_ID);
|
||||
// Check if data still bound to the requesting ui element
|
||||
if (isBound(bindingId)) {
|
||||
switch (id) {
|
||||
case CONVERSATION_OPTIONS_LOADER: {
|
||||
final Uri uri =
|
||||
MessagingContentProvider.buildConversationMetadataUri(mConversationId);
|
||||
return new BoundCursorLoader(bindingId, mContext, uri,
|
||||
new String[]{}, null, null, null);
|
||||
}
|
||||
|
||||
case PARTICIPANT_LOADER: {
|
||||
final Uri uri =
|
||||
MessagingContentProvider
|
||||
.buildConversationParticipantsUri(mConversationId);
|
||||
return new BoundCursorLoader(bindingId, mContext, uri,
|
||||
ParticipantData.ParticipantsQuery.PROJECTION, null, null, null);
|
||||
}
|
||||
|
||||
default:
|
||||
Assert.fail("Unknown loader id for PeopleAndOptionsFragment!");
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
LogUtil.w(LogUtil.BUGLE_TAG, "Loader created after unbinding PeopleAndOptionsFragment");
|
||||
if (!isBound(bindingId)) {
|
||||
LogUtil.w(LogUtil.BUGLE_TAG, "Loader created after unbinding conversation profile");
|
||||
return null;
|
||||
}
|
||||
switch (id) {
|
||||
case CONVERSATION_METADATA_LOADER: {
|
||||
final Uri uri =
|
||||
MessagingContentProvider.buildConversationMetadataUri(mConversationId);
|
||||
return new BoundCursorLoader(bindingId, mContext, uri,
|
||||
ConversationListItemData.PROJECTION, null, null, null);
|
||||
}
|
||||
|
||||
case PARTICIPANT_LOADER: {
|
||||
final Uri uri =
|
||||
MessagingContentProvider.buildConversationParticipantsUri(mConversationId);
|
||||
return new BoundCursorLoader(bindingId, mContext, uri,
|
||||
ParticipantData.ParticipantsQuery.PROJECTION, null, null, null);
|
||||
}
|
||||
|
||||
default:
|
||||
Assert.fail("Unknown loader id for the conversation profile!");
|
||||
return null;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
public void onLoadFinished(@NonNull final Loader<Cursor> loader, final Cursor data) {
|
||||
final BoundCursorLoader cursorLoader = (BoundCursorLoader) loader;
|
||||
if (isBound(cursorLoader.getBindingId())) {
|
||||
switch (loader.getId()) {
|
||||
case CONVERSATION_OPTIONS_LOADER:
|
||||
mListener.onOptionsCursorUpdated(this, data);
|
||||
break;
|
||||
|
||||
case PARTICIPANT_LOADER:
|
||||
mParticipantData.bind(data);
|
||||
mListener.onParticipantsListLoaded(this,
|
||||
mParticipantData.getParticipantListExcludingSelf());
|
||||
break;
|
||||
|
||||
default:
|
||||
Assert.fail("Unknown loader id for PeopleAndOptionsFragment!");
|
||||
break;
|
||||
if (!isBound(cursorLoader.getBindingId()) || mListener == null) {
|
||||
LogUtil.w(LogUtil.BUGLE_TAG, "Loader finished after unbinding conversation profile");
|
||||
return;
|
||||
}
|
||||
switch (loader.getId()) {
|
||||
case CONVERSATION_METADATA_LOADER: {
|
||||
ConversationListItemData conversation = null;
|
||||
if (data != null && data.moveToFirst()) {
|
||||
conversation = new ConversationListItemData();
|
||||
conversation.bind(data);
|
||||
}
|
||||
mListener.onConversationMetadataUpdated(this, conversation);
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
LogUtil.w(LogUtil.BUGLE_TAG,
|
||||
"Loader finished after unbinding PeopleAndOptionsFragment");
|
||||
|
||||
case PARTICIPANT_LOADER:
|
||||
mParticipantData.bind(data);
|
||||
mListener.onParticipantsListLoaded(this,
|
||||
mParticipantData.getParticipantListExcludingSelf());
|
||||
break;
|
||||
|
||||
default:
|
||||
Assert.fail("Unknown loader id for the conversation profile!");
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
public void onLoaderReset(@NonNull final Loader<Cursor> loader) {
|
||||
final BoundCursorLoader cursorLoader = (BoundCursorLoader) loader;
|
||||
if (isBound(cursorLoader.getBindingId())) {
|
||||
switch (loader.getId()) {
|
||||
case CONVERSATION_OPTIONS_LOADER:
|
||||
mListener.onOptionsCursorUpdated(this, null);
|
||||
break;
|
||||
if (!isBound(cursorLoader.getBindingId()) || mListener == null) {
|
||||
return;
|
||||
}
|
||||
switch (loader.getId()) {
|
||||
case CONVERSATION_METADATA_LOADER:
|
||||
mListener.onConversationMetadataUpdated(this, null);
|
||||
break;
|
||||
|
||||
case PARTICIPANT_LOADER:
|
||||
mParticipantData.bind(null);
|
||||
break;
|
||||
case PARTICIPANT_LOADER:
|
||||
mParticipantData.bind(null);
|
||||
break;
|
||||
|
||||
default:
|
||||
Assert.fail("Unknown loader id for PeopleAndOptionsFragment!");
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
LogUtil.w(LogUtil.BUGLE_TAG, "Loader reset after unbinding PeopleAndOptionsFragment");
|
||||
default:
|
||||
Assert.fail("Unknown loader id for the conversation profile!");
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
public void init(final LoaderManager loaderManager,
|
||||
final BindingBase<PeopleAndOptionsData> binding) {
|
||||
final BindingBase<ConversationProfileData> binding) {
|
||||
final Bundle args = new Bundle();
|
||||
args.putString(BINDING_ID, binding.getBindingId());
|
||||
mLoaderManager = loaderManager;
|
||||
mLoaderManager.initLoader(CONVERSATION_OPTIONS_LOADER, args, this);
|
||||
mLoaderManager.initLoader(CONVERSATION_METADATA_LOADER, args, this);
|
||||
mLoaderManager.initLoader(PARTICIPANT_LOADER, args, this);
|
||||
}
|
||||
|
||||
@@ -166,17 +163,16 @@ public class PeopleAndOptionsData extends BindableData implements
|
||||
|
||||
// This could be null if we bind but the caller doesn't init the BindableData
|
||||
if (mLoaderManager != null) {
|
||||
mLoaderManager.destroyLoader(CONVERSATION_OPTIONS_LOADER);
|
||||
mLoaderManager.destroyLoader(CONVERSATION_METADATA_LOADER);
|
||||
mLoaderManager.destroyLoader(PARTICIPANT_LOADER);
|
||||
mLoaderManager = null;
|
||||
}
|
||||
}
|
||||
|
||||
public void setDestinationBlocked(final BindingBase<PeopleAndOptionsData> binding,
|
||||
public void setDestinationBlocked(final BindingBase<ConversationProfileData> binding,
|
||||
final boolean blocked) {
|
||||
final String bindingId = binding.getBindingId();
|
||||
final ParticipantData participantData = mParticipantData.getOtherParticipant();
|
||||
if (isBound(bindingId) && participantData != null) {
|
||||
if (isBound(binding.getBindingId()) && participantData != null) {
|
||||
UpdateDestinationBlockedAction.updateDestinationBlocked(
|
||||
participantData.getNormalizedDestination(),
|
||||
blocked, mConversationId,
|
||||
@@ -184,6 +180,11 @@ public class PeopleAndOptionsData extends BindableData implements
|
||||
}
|
||||
}
|
||||
|
||||
/** The single other participant of a one to one conversation, or null for a group. */
|
||||
public ParticipantData getOtherParticipant() {
|
||||
return mParticipantData.getOtherParticipant();
|
||||
}
|
||||
|
||||
public String getConversationId() {
|
||||
return mConversationId;
|
||||
}
|
||||
@@ -0,0 +1,132 @@
|
||||
/*
|
||||
* Copyright (C) 2025 The LineageOS Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.android.messaging.datamodel.data;
|
||||
|
||||
import android.content.Context;
|
||||
import android.database.Cursor;
|
||||
import android.os.Bundle;
|
||||
import android.text.TextUtils;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.loader.app.LoaderManager;
|
||||
import androidx.loader.content.Loader;
|
||||
|
||||
import com.android.messaging.datamodel.BoundCursorLoader;
|
||||
import com.android.messaging.datamodel.MessagingContentProvider;
|
||||
import com.android.messaging.datamodel.binding.BindableData;
|
||||
import com.android.messaging.datamodel.binding.BindingBase;
|
||||
import com.android.messaging.util.Assert;
|
||||
import com.android.messaging.util.LogUtil;
|
||||
|
||||
/**
|
||||
* Searches the messages of a single conversation. No loader runs until
|
||||
* {@link #setSearchText} gets a non empty term.
|
||||
*/
|
||||
public class ConversationSearchData extends BindableData implements
|
||||
LoaderManager.LoaderCallbacks<Cursor> {
|
||||
public interface ConversationSearchDataListener {
|
||||
/** Results for the latest search term, or null once the search is cleared. */
|
||||
void onConversationSearchCursorUpdated(Cursor cursor);
|
||||
}
|
||||
|
||||
private static final String BINDING_ID = "bindingId";
|
||||
private static final int SEARCH_LOADER = 1;
|
||||
|
||||
private final Context mContext;
|
||||
private final String mConversationId;
|
||||
private LoaderManager mLoaderManager;
|
||||
private ConversationSearchDataListener mListener;
|
||||
private String mSearchText;
|
||||
|
||||
public ConversationSearchData(final Context context, final String conversationId,
|
||||
final ConversationSearchDataListener listener) {
|
||||
mContext = context;
|
||||
mConversationId = conversationId;
|
||||
mListener = listener;
|
||||
}
|
||||
|
||||
public void init(final LoaderManager loaderManager) {
|
||||
mLoaderManager = loaderManager;
|
||||
}
|
||||
|
||||
public void setSearchText(final BindingBase<ConversationSearchData> binding,
|
||||
final String searchText) {
|
||||
if (mLoaderManager == null) {
|
||||
return;
|
||||
}
|
||||
final String trimmed = searchText == null ? null : searchText.trim();
|
||||
final String normalized = TextUtils.isEmpty(trimmed) ? null : trimmed;
|
||||
if (TextUtils.equals(mSearchText, normalized)) {
|
||||
return;
|
||||
}
|
||||
mSearchText = normalized;
|
||||
if (mSearchText == null) {
|
||||
mLoaderManager.destroyLoader(SEARCH_LOADER);
|
||||
if (mListener != null) {
|
||||
mListener.onConversationSearchCursorUpdated(null);
|
||||
}
|
||||
return;
|
||||
}
|
||||
final Bundle args = new Bundle();
|
||||
args.putString(BINDING_ID, binding.getBindingId());
|
||||
mLoaderManager.restartLoader(SEARCH_LOADER, args, this);
|
||||
}
|
||||
|
||||
public String getSearchText() {
|
||||
return mSearchText;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public Loader<Cursor> onCreateLoader(final int id, final Bundle args) {
|
||||
Assert.isTrue(id == SEARCH_LOADER);
|
||||
final String bindingId = args.getString(BINDING_ID);
|
||||
if (!isBound(bindingId)) {
|
||||
LogUtil.w(LogUtil.BUGLE_TAG, "Loader created after unbinding conversation search");
|
||||
return null;
|
||||
}
|
||||
return new BoundCursorLoader(bindingId, mContext,
|
||||
MessagingContentProvider.buildConversationMessagesSearchUri(
|
||||
mConversationId, mSearchText),
|
||||
null, null, null, null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLoadFinished(@NonNull final Loader<Cursor> loader, final Cursor cursor) {
|
||||
final BoundCursorLoader cursorLoader = (BoundCursorLoader) loader;
|
||||
if (isBound(cursorLoader.getBindingId()) && mListener != null) {
|
||||
mListener.onConversationSearchCursorUpdated(cursor);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLoaderReset(@NonNull final Loader<Cursor> loader) {
|
||||
final BoundCursorLoader cursorLoader = (BoundCursorLoader) loader;
|
||||
if (isBound(cursorLoader.getBindingId()) && mListener != null) {
|
||||
mListener.onConversationSearchCursorUpdated(null);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void unregisterListeners() {
|
||||
mListener = null;
|
||||
if (mLoaderManager != null) {
|
||||
mLoaderManager.destroyLoader(SEARCH_LOADER);
|
||||
mLoaderManager = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -282,6 +282,12 @@ public class ParticipantData implements Parcelable {
|
||||
return pd;
|
||||
}
|
||||
|
||||
public static String getCachedParticipantId(final int subId) {
|
||||
synchronized (sSubIdtoParticipantIdCache) {
|
||||
return sSubIdtoParticipantIdCache.get(subId);
|
||||
}
|
||||
}
|
||||
|
||||
public static String getParticipantId(final DatabaseWrapper db, final int subId) {
|
||||
String id;
|
||||
synchronized (sSubIdtoParticipantIdCache) {
|
||||
|
||||
@@ -87,6 +87,11 @@ public class ParticipantListItemData extends PersonItemData {
|
||||
return mNormalizedDestination;
|
||||
}
|
||||
|
||||
public void unblockQuietly(final Context context) {
|
||||
UpdateDestinationBlockedAction.updateDestinationBlocked(
|
||||
mNormalizedDestination, false, null, (action, success, block, destination) -> { });
|
||||
}
|
||||
|
||||
public void unblock(final Context context) {
|
||||
UpdateDestinationBlockedAction.updateDestinationBlocked(
|
||||
mNormalizedDestination, false, null,
|
||||
|
||||
@@ -1,79 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2015 The Android Open Source Project
|
||||
* Copyright (C) 2024 The LineageOS Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.android.messaging.datamodel.data;
|
||||
|
||||
import android.content.Context;
|
||||
import android.database.Cursor;
|
||||
|
||||
import com.android.messaging.R;
|
||||
import com.android.messaging.util.Assert;
|
||||
|
||||
public class PeopleOptionsItemData {
|
||||
// Identification for each setting that's surfaced to the UI layer.
|
||||
public static final int SETTING_NOTIFICATION = 0;
|
||||
public static final int SETTING_BLOCKED = 1;
|
||||
public static final int SETTINGS_COUNT = 2;
|
||||
|
||||
private String mTitle;
|
||||
private int mItemId;
|
||||
private ParticipantData mOtherParticipant;
|
||||
|
||||
private final Context mContext;
|
||||
|
||||
public PeopleOptionsItemData(final Context context) {
|
||||
mContext = context;
|
||||
}
|
||||
|
||||
/**
|
||||
* Bind to a specific setting column on conversation metadata cursor. (Note
|
||||
* that it binds to columns because it treats individual columns of the cursor as
|
||||
* separate options to display for the conversation, e.g. notification settings).
|
||||
*/
|
||||
public void bind(
|
||||
final Cursor cursor, final ParticipantData otherParticipant, final int settingType) {
|
||||
mItemId = settingType;
|
||||
mOtherParticipant = otherParticipant;
|
||||
|
||||
switch (settingType) {
|
||||
case SETTING_NOTIFICATION:
|
||||
mTitle = mContext.getString(R.string.notifications_enabled_conversation_pref_title);
|
||||
break;
|
||||
|
||||
case SETTING_BLOCKED:
|
||||
Assert.notNull(otherParticipant);
|
||||
final int resourceId = otherParticipant.isBlocked() ?
|
||||
R.string.unblock_contact_title : R.string.block_contact_title;
|
||||
mTitle = mContext.getString(resourceId, otherParticipant.getDisplayDestination());
|
||||
break;
|
||||
|
||||
default:
|
||||
Assert.fail("Unsupported conversation option type!");
|
||||
}
|
||||
}
|
||||
|
||||
public String getTitle() {
|
||||
return mTitle;
|
||||
}
|
||||
|
||||
public int getItemId() {
|
||||
return mItemId;
|
||||
}
|
||||
|
||||
public ParticipantData getOtherParticipant() {
|
||||
return mOtherParticipant;
|
||||
}
|
||||
}
|
||||
@@ -127,6 +127,14 @@ public class AvatarRequest extends UriImageRequest<AvatarRequestDescriptor> {
|
||||
mContext.getResources(), R.drawable.ic_person_light, mContext.getTheme());
|
||||
float dstWidth = Math.min(defaultPerson.getIntrinsicWidth() * SCALING_FACTOR, width);
|
||||
float dstHeight = Math.min(defaultPerson.getIntrinsicHeight() * SCALING_FACTOR, height);
|
||||
final float dest = Math.min(width, height);
|
||||
// List-size tiles keep the 24dp * 1.33 silhouette. Profile-popup tiles
|
||||
// (240dp) would otherwise leave a tiny icon in the center of the card.
|
||||
if (dest > dstWidth * 2f) {
|
||||
final float large = dest * 0.56f;
|
||||
dstWidth = large;
|
||||
dstHeight = large;
|
||||
}
|
||||
|
||||
canvas.translate((width - dstWidth) / 2, (height - dstHeight) / 2);
|
||||
defaultPerson.setBounds(0, 0, (int)dstWidth, (int)dstHeight);
|
||||
|
||||
@@ -211,7 +211,16 @@ public class DecodedImageResource extends ImageResource {
|
||||
}
|
||||
}
|
||||
byte[] encodedBytes = ImageUtils.bitmapToBytes(bitmap, COMPRESS_QUALITY);
|
||||
return new EncodedImageResource(getKey(), encodedBytes, getOrientation());
|
||||
int desiredWidth = ImageRequest.UNSPECIFIED_SIZE;
|
||||
int desiredHeight = ImageRequest.UNSPECIFIED_SIZE;
|
||||
if (mOriginalImageRequest instanceof ImageRequest<?>) {
|
||||
final ImageRequestDescriptor descriptor =
|
||||
((ImageRequest<?>) mOriginalImageRequest).getDescriptor();
|
||||
desiredWidth = descriptor.desiredWidth;
|
||||
desiredHeight = descriptor.desiredHeight;
|
||||
}
|
||||
return new EncodedImageResource(getKey(), encodedBytes, getOrientation(),
|
||||
desiredWidth, desiredHeight);
|
||||
} catch (Exception ex) {
|
||||
// Something went wrong during bitmap compression, fall back to just using the
|
||||
// original bitmap.
|
||||
|
||||
@@ -17,11 +17,11 @@ package com.android.messaging.datamodel.media;
|
||||
|
||||
import android.content.res.Resources;
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.BitmapFactory;
|
||||
import android.graphics.drawable.Drawable;
|
||||
|
||||
import com.android.messaging.util.Assert;
|
||||
import com.android.messaging.util.Assert.DoesNotRunOnMainThread;
|
||||
import com.android.messaging.util.ImageUtils;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
@@ -41,10 +41,20 @@ import java.util.List;
|
||||
*/
|
||||
public class EncodedImageResource extends ImageResource {
|
||||
private final byte[] mImageBytes;
|
||||
private final int mDesiredWidth;
|
||||
private final int mDesiredHeight;
|
||||
|
||||
public EncodedImageResource(String key, byte[] imageBytes, int orientation) {
|
||||
this(key, imageBytes, orientation, ImageRequest.UNSPECIFIED_SIZE,
|
||||
ImageRequest.UNSPECIFIED_SIZE);
|
||||
}
|
||||
|
||||
public EncodedImageResource(String key, byte[] imageBytes, int orientation,
|
||||
final int desiredWidth, final int desiredHeight) {
|
||||
super(key, orientation);
|
||||
mImageBytes = imageBytes;
|
||||
mDesiredWidth = desiredWidth;
|
||||
mDesiredHeight = desiredHeight;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -54,7 +64,7 @@ public class EncodedImageResource extends ImageResource {
|
||||
try {
|
||||
// This should only be called during the decode request.
|
||||
Assert.isNotMainThread();
|
||||
return BitmapFactory.decodeByteArray(mImageBytes, 0, mImageBytes.length);
|
||||
return decodeSampledBitmap();
|
||||
} finally {
|
||||
releaseLock();
|
||||
}
|
||||
@@ -99,11 +109,15 @@ public class EncodedImageResource extends ImageResource {
|
||||
return true;
|
||||
}
|
||||
|
||||
private Bitmap decodeSampledBitmap() {
|
||||
return ImageUtils.decodeByteArraySampled(mImageBytes, mDesiredWidth, mDesiredHeight);
|
||||
}
|
||||
|
||||
@Override
|
||||
MediaRequest<? extends RefCountedMediaResource> getMediaDecodingRequest(
|
||||
final MediaRequest<? extends RefCountedMediaResource> originalRequest) {
|
||||
Assert.isTrue(isEncoded());
|
||||
return new DecodeImageRequest();
|
||||
return new DecodeImageRequest(originalRequest);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -112,7 +126,22 @@ public class EncodedImageResource extends ImageResource {
|
||||
* properties such as binding.
|
||||
*/
|
||||
private class DecodeImageRequest implements MediaRequest<ImageResource> {
|
||||
public DecodeImageRequest() {
|
||||
private final int mRequestDesiredWidth;
|
||||
private final int mRequestDesiredHeight;
|
||||
|
||||
public DecodeImageRequest(
|
||||
final MediaRequest<? extends RefCountedMediaResource> originalRequest) {
|
||||
int desiredWidth = mDesiredWidth;
|
||||
int desiredHeight = mDesiredHeight;
|
||||
if (originalRequest != null) {
|
||||
final MediaRequestDescriptor<?> descriptor = originalRequest.getDescriptor();
|
||||
if (descriptor instanceof ImageRequestDescriptor) {
|
||||
desiredWidth = ((ImageRequestDescriptor) descriptor).desiredWidth;
|
||||
desiredHeight = ((ImageRequestDescriptor) descriptor).desiredHeight;
|
||||
}
|
||||
}
|
||||
mRequestDesiredWidth = desiredWidth;
|
||||
mRequestDesiredHeight = desiredHeight;
|
||||
// Hold a ref onto the encoded resource before the request finishes.
|
||||
addRef();
|
||||
}
|
||||
@@ -129,8 +158,11 @@ public class EncodedImageResource extends ImageResource {
|
||||
Assert.isNotMainThread();
|
||||
acquireLock();
|
||||
try {
|
||||
final Bitmap decodedBitmap = BitmapFactory.decodeByteArray(mImageBytes, 0,
|
||||
mImageBytes.length);
|
||||
final Bitmap decodedBitmap = ImageUtils.decodeByteArraySampled(mImageBytes,
|
||||
mRequestDesiredWidth, mRequestDesiredHeight);
|
||||
if (decodedBitmap == null) {
|
||||
throw new RuntimeException("failed decoding encoded bitmap");
|
||||
}
|
||||
return new DecodedImageResource(getKey(), decodedBitmap, getOrientation());
|
||||
} finally {
|
||||
releaseLock();
|
||||
|
||||
@@ -18,11 +18,11 @@ package com.android.messaging.datamodel.media;
|
||||
|
||||
import android.content.Context;
|
||||
import android.graphics.Bitmap;
|
||||
import android.graphics.BitmapFactory;
|
||||
import android.support.v7.mms.pdu.ContentType;
|
||||
|
||||
import com.android.messaging.Factory;
|
||||
import com.android.messaging.util.Assert;
|
||||
import com.android.messaging.util.ImageUtils;
|
||||
import com.android.messaging.util.LogUtil;
|
||||
|
||||
import java.io.FileNotFoundException;
|
||||
@@ -91,7 +91,11 @@ public class NetworkUriImageRequest<D extends UriImageRequestDescriptor> extends
|
||||
connection.setDoInput(true);
|
||||
connection.connect();
|
||||
if (connection.getResponseCode() == HttpURLConnection.HTTP_OK) {
|
||||
bitmap = BitmapFactory.decodeStream(connection.getInputStream());
|
||||
try (InputStream inputStream = connection.getInputStream()) {
|
||||
final byte[] bytes = ImageUtils.readStreamToByteArray(inputStream);
|
||||
bitmap = ImageUtils.decodeByteArraySampled(bytes,
|
||||
mDescriptor.desiredWidth, mDescriptor.desiredHeight);
|
||||
}
|
||||
}
|
||||
} catch (MalformedURLException e) {
|
||||
LogUtil.e(LogUtil.BUGLE_TAG,
|
||||
|
||||
@@ -22,6 +22,7 @@ import android.content.Intent;
|
||||
|
||||
import com.android.messaging.BugleApplication;
|
||||
import com.android.messaging.Factory;
|
||||
import com.android.messaging.datamodel.ScheduledMessageStore;
|
||||
import com.android.messaging.datamodel.action.UpdateMessageNotificationAction;
|
||||
import com.android.messaging.util.BuglePrefsKeys;
|
||||
import com.android.messaging.util.LogUtil;
|
||||
@@ -40,6 +41,7 @@ public class BootAndPackageReplacedReceiver extends BroadcastReceiver {
|
||||
UpdateMessageNotificationAction.updateMessageNotification();
|
||||
|
||||
BugleApplication.updateAppConfig(context);
|
||||
ScheduledMessageStore.rescheduleAll(context);
|
||||
} else {
|
||||
LogUtil.i(LogUtil.BUGLE_TAG, "BootAndPackageReplacedReceiver got unexpected action: "
|
||||
+ intent.getAction());
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
package com.android.messaging.receiver;
|
||||
|
||||
import android.content.BroadcastReceiver;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.text.TextUtils;
|
||||
|
||||
import com.android.messaging.datamodel.ScheduledMessageStore;
|
||||
|
||||
import java.util.concurrent.Executors;
|
||||
|
||||
/** Fires a previously scheduled compose message when its AlarmManager time is reached. */
|
||||
public class ScheduledMessageReceiver extends BroadcastReceiver {
|
||||
public static final String ACTION_SEND = "com.bnkode.notie.SEND_SCHEDULED_MESSAGE";
|
||||
public static final String EXTRA_ID = "scheduled_id";
|
||||
|
||||
@Override
|
||||
public void onReceive(final Context context, final Intent intent) {
|
||||
if (intent == null || !ACTION_SEND.equals(intent.getAction())) {
|
||||
return;
|
||||
}
|
||||
final String id = intent.getStringExtra(EXTRA_ID);
|
||||
if (TextUtils.isEmpty(id)) {
|
||||
return;
|
||||
}
|
||||
final PendingResult pending = goAsync();
|
||||
Executors.newSingleThreadExecutor().execute(() -> {
|
||||
try {
|
||||
ScheduledMessageStore.sendNow(context.getApplicationContext(), id);
|
||||
} finally {
|
||||
pending.finish();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -38,13 +38,13 @@ import com.android.messaging.util.LogUtil;
|
||||
*/
|
||||
public class SendStatusReceiver extends BroadcastReceiver {
|
||||
public static final String MESSAGE_SENT_ACTION =
|
||||
"com.android.messaging.receiver.SendStatusReceiver.MESSAGE_SENT";
|
||||
"com.bnkode.notie.receiver.SendStatusReceiver.MESSAGE_SENT";
|
||||
public static final String MESSAGE_DELIVERED_ACTION =
|
||||
"com.android.messaging.receiver.SendStatusReceiver.MESSAGE_DELIVERED";
|
||||
"com.bnkode.notie.receiver.SendStatusReceiver.MESSAGE_DELIVERED";
|
||||
public static final String MMS_SENT_ACTION =
|
||||
"com.android.messaging.receiver.SendStatusReceiver.MMS_SENT";
|
||||
"com.bnkode.notie.receiver.SendStatusReceiver.MMS_SENT";
|
||||
public static final String MMS_DOWNLOADED_ACTION =
|
||||
"com.android.messaging.receiver.SendStatusReceiver.MMS_DOWNLOADED";
|
||||
"com.bnkode.notie.receiver.SendStatusReceiver.MMS_DOWNLOADED";
|
||||
|
||||
// Defined by platform, but no constant provided. See docs for SmsManager.sendTextMessage.
|
||||
public static final String EXTRA_ERROR_CODE = "errorCode";
|
||||
|
||||
@@ -23,13 +23,16 @@ import android.view.ActionMode;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuInflater;
|
||||
import android.view.MenuItem;
|
||||
import android.view.MotionEvent;
|
||||
import android.view.View;
|
||||
import android.view.Window;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.appcompat.app.ActionBar;
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
|
||||
import com.android.messaging.R;
|
||||
import com.android.messaging.ui.home.HomeInputFocus;
|
||||
import com.android.messaging.util.BugleActivityUtil;
|
||||
import com.android.messaging.util.ImeUtil;
|
||||
import com.android.messaging.util.LogUtil;
|
||||
@@ -70,9 +73,6 @@ public class BugleActionBarActivity extends AppCompatActivity implements ImeUtil
|
||||
|
||||
mLastScreenHeight = getResources().getDisplayMetrics().heightPixels;
|
||||
LogUtil.v(LogUtil.BUGLE_TAG, this.getLocalClassName() + ".onCreate");
|
||||
|
||||
getWindow().setStatusBarColor(getResources().getColor(R.color.action_bar_background_color,
|
||||
getTheme()));
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -94,6 +94,13 @@ public class BugleActionBarActivity extends AppCompatActivity implements ImeUtil
|
||||
BugleActivityUtil.onActivityResume(this, BugleActionBarActivity.this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean dispatchTouchEvent(final MotionEvent ev) {
|
||||
final Window window = getWindow();
|
||||
HomeInputFocus.clearIfOutside(window == null ? null : window.getDecorView(), ev);
|
||||
return super.dispatchTouchEvent(ev);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPause() {
|
||||
super.onPause();
|
||||
@@ -339,6 +346,9 @@ public class BugleActionBarActivity extends AppCompatActivity implements ImeUtil
|
||||
}
|
||||
|
||||
public void updateActionBar(final ActionBar actionBar) {
|
||||
if (actionBar == null) {
|
||||
return;
|
||||
}
|
||||
if (!BugleActionBarActivity.this.shouldShowContextualActionBar(mCallback)) {
|
||||
actionBar.hide();
|
||||
return;
|
||||
|
||||
@@ -41,8 +41,11 @@ public class ContactIconView extends AsyncImageView {
|
||||
private static final int NORMAL_ICON_SIZE_ID = 0;
|
||||
private static final int LARGE_ICON_SIZE_ID = 1;
|
||||
private static final int SMALL_ICON_SIZE_ID = 2;
|
||||
private static final int PROFILE_POPUP_ICON_SIZE_ID = 3;
|
||||
private static final int HERO_ICON_SIZE_ID = 4;
|
||||
|
||||
protected final int mIconSize;
|
||||
private final boolean mCropToCircle;
|
||||
private final int mColorPressedId;
|
||||
|
||||
private long mContactId;
|
||||
@@ -58,6 +61,7 @@ public class ContactIconView extends AsyncImageView {
|
||||
final TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.ContactIconView);
|
||||
|
||||
final int iconSizeId = a.getInt(R.styleable.ContactIconView_contactIconSize, 0);
|
||||
boolean cropToCircle = true;
|
||||
switch (iconSizeId) {
|
||||
case NORMAL_ICON_SIZE_ID:
|
||||
mIconSize = (int) resources.getDimension(
|
||||
@@ -71,11 +75,21 @@ public class ContactIconView extends AsyncImageView {
|
||||
mIconSize = (int) resources.getDimension(
|
||||
R.dimen.contact_icon_view_small_size);
|
||||
break;
|
||||
case PROFILE_POPUP_ICON_SIZE_ID:
|
||||
mIconSize = (int) resources.getDimension(
|
||||
R.dimen.home_list_profile_avatar_size);
|
||||
cropToCircle = false;
|
||||
break;
|
||||
case HERO_ICON_SIZE_ID:
|
||||
mIconSize = (int) resources.getDimension(
|
||||
R.dimen.home_profile_avatar_size);
|
||||
break;
|
||||
default:
|
||||
// For the compiler, something has to be set even with the assert.
|
||||
mIconSize = 0;
|
||||
Assert.fail("Unsupported ContactIconView icon size attribute");
|
||||
}
|
||||
mCropToCircle = cropToCircle;
|
||||
mColorPressedId = resources.getColor(R.color.contact_avatar_pressed_color,
|
||||
context.getTheme());
|
||||
|
||||
@@ -118,7 +132,8 @@ public class ContactIconView extends AsyncImageView {
|
||||
if (AvatarUriUtil.TYPE_GROUP_URI.equals(avatarType)) {
|
||||
setImageResourceId(new AvatarGroupRequestDescriptor(uri, mIconSize, mIconSize));
|
||||
} else {
|
||||
setImageResourceId(new AvatarRequestDescriptor(uri, mIconSize, mIconSize));
|
||||
setImageResourceId(new AvatarRequestDescriptor(uri, mIconSize, mIconSize,
|
||||
mCropToCircle));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -17,20 +17,31 @@
|
||||
|
||||
package com.android.messaging.ui;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.os.Bundle;
|
||||
import android.webkit.WebView;
|
||||
|
||||
import com.android.messaging.R;
|
||||
import com.android.messaging.ui.home.HomeSubHeader;
|
||||
|
||||
public class LicenseActivity extends Activity {
|
||||
private final String LICENSE_URL = "file:///android_asset/licenses.html";
|
||||
public class LicenseActivity extends BugleActionBarActivity {
|
||||
private static final String LICENSE_URL = "file:///android_asset/licenses.html";
|
||||
|
||||
@Override
|
||||
public void onCreate(final Bundle bundle) {
|
||||
super.onCreate(bundle);
|
||||
setContentView(R.layout.license_activity);
|
||||
protected void onCreate(final Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
setContentView(R.layout.home_license_activity);
|
||||
if (getSupportActionBar() != null) {
|
||||
getSupportActionBar().hide();
|
||||
}
|
||||
HomeSubHeader.bind(this, R.string.home_settings_licenses, v -> finish());
|
||||
final WebView webView = findViewById(R.id.content);
|
||||
webView.setBackgroundColor(getColor(R.color.home_bg));
|
||||
webView.loadUrl(LICENSE_URL);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void finish() {
|
||||
super.finish();
|
||||
overridePendingTransition(R.anim.home_activity_hold, R.anim.home_activity_close);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,9 +18,23 @@ package com.android.messaging.ui;
|
||||
import android.content.Intent;
|
||||
import android.os.Bundle;
|
||||
import android.telephony.TelephonyManager;
|
||||
import android.text.Editable;
|
||||
import android.text.TextUtils;
|
||||
import android.text.TextWatcher;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.inputmethod.EditorInfo;
|
||||
import android.view.inputmethod.InputMethodManager;
|
||||
import android.widget.EditText;
|
||||
import android.widget.ImageButton;
|
||||
|
||||
import androidx.core.app.RemoteInput;
|
||||
|
||||
import com.android.messaging.R;
|
||||
import com.android.messaging.datamodel.NoConfirmationSmsSendService;
|
||||
import com.android.messaging.ui.home.HomeBottomSheet;
|
||||
import com.android.messaging.util.LogUtil;
|
||||
import com.google.android.material.bottomsheet.BottomSheetDialog;
|
||||
|
||||
public class RemoteInputEntrypointActivity extends BaseBugleActivity {
|
||||
private static final String TAG = LogUtil.BUGLE_TAG;
|
||||
@@ -37,22 +51,111 @@ public class RemoteInputEntrypointActivity extends BaseBugleActivity {
|
||||
return;
|
||||
}
|
||||
|
||||
// Perform some action depending on the intent
|
||||
String action = intent.getAction();
|
||||
if (Intent.ACTION_SENDTO.equals(action)) {
|
||||
// Build and send the intent
|
||||
final Intent sendIntent = new Intent(this, NoConfirmationSmsSendService.class);
|
||||
sendIntent.setAction(TelephonyManager.ACTION_RESPOND_VIA_MESSAGE);
|
||||
sendIntent.putExtras(intent);
|
||||
// Wear apparently passes all of its extras via the clip data. Must pass it along.
|
||||
sendIntent.setClipData(intent.getClipData());
|
||||
startService(sendIntent);
|
||||
setResult(RESULT_OK);
|
||||
} else {
|
||||
if (!Intent.ACTION_SENDTO.equals(action)) {
|
||||
LogUtil.w(TAG, "Unrecognized intent action: " + action);
|
||||
setResult(RESULT_CANCELED);
|
||||
finish();
|
||||
return;
|
||||
}
|
||||
// This activity should never stick around after processing the intent
|
||||
|
||||
final CharSequence remoteText = readRemoteInputText(intent);
|
||||
if (!TextUtils.isEmpty(remoteText)) {
|
||||
sendReply(intent, remoteText.toString());
|
||||
setResult(RESULT_OK);
|
||||
finish();
|
||||
return;
|
||||
}
|
||||
|
||||
showReplySheet(intent);
|
||||
}
|
||||
|
||||
private static CharSequence readRemoteInputText(final Intent intent) {
|
||||
final Bundle remoteInput = RemoteInput.getResultsFromIntent(intent);
|
||||
if (remoteInput != null) {
|
||||
return remoteInput.getCharSequence(Intent.EXTRA_TEXT);
|
||||
}
|
||||
return intent.getCharSequenceExtra(Intent.EXTRA_TEXT);
|
||||
}
|
||||
|
||||
private void showReplySheet(final Intent sourceIntent) {
|
||||
final View custom = LayoutInflater.from(this)
|
||||
.inflate(R.layout.home_notification_reply_sheet, null, false);
|
||||
final EditText input = custom.findViewById(R.id.home_notification_reply_input);
|
||||
final ImageButton send = custom.findViewById(R.id.home_notification_reply_send);
|
||||
if (send != null) {
|
||||
send.setEnabled(false);
|
||||
send.setAlpha(0.4f);
|
||||
}
|
||||
if (input != null && send != null) {
|
||||
input.addTextChangedListener(new TextWatcher() {
|
||||
@Override
|
||||
public void beforeTextChanged(final CharSequence s, final int start,
|
||||
final int count, final int after) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onTextChanged(final CharSequence s, final int start, final int before,
|
||||
final int count) {
|
||||
final boolean hasText = !TextUtils.isEmpty(s.toString().trim());
|
||||
send.setEnabled(hasText);
|
||||
send.setAlpha(hasText ? 1f : 0.4f);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void afterTextChanged(final Editable s) {
|
||||
}
|
||||
});
|
||||
input.setOnEditorActionListener((v, actionId, event) -> {
|
||||
if (actionId == EditorInfo.IME_ACTION_SEND) {
|
||||
submitReply(sourceIntent, input);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
send.setOnClickListener(v -> submitReply(sourceIntent, input));
|
||||
}
|
||||
|
||||
CharSequence title = sourceIntent.getStringExtra(UIIntents.UI_INTENT_EXTRA_CONVERSATION_NAME);
|
||||
if (TextUtils.isEmpty(title)) {
|
||||
title = getString(R.string.home_notification_reply);
|
||||
}
|
||||
final BottomSheetDialog dialog = HomeBottomSheet.builder(this)
|
||||
.title(title)
|
||||
.custom(custom)
|
||||
.show();
|
||||
dialog.setOnDismissListener(d -> finish());
|
||||
if (input != null) {
|
||||
input.post(() -> {
|
||||
input.requestFocus();
|
||||
final InputMethodManager imm =
|
||||
(InputMethodManager) getSystemService(INPUT_METHOD_SERVICE);
|
||||
if (imm != null) {
|
||||
imm.showSoftInput(input, InputMethodManager.SHOW_IMPLICIT);
|
||||
}
|
||||
});
|
||||
}
|
||||
setResult(RESULT_OK);
|
||||
}
|
||||
|
||||
private void submitReply(final Intent sourceIntent, final EditText input) {
|
||||
if (input == null) {
|
||||
return;
|
||||
}
|
||||
final String message = input.getText() == null ? "" : input.getText().toString().trim();
|
||||
if (TextUtils.isEmpty(message)) {
|
||||
return;
|
||||
}
|
||||
sendReply(sourceIntent, message);
|
||||
finish();
|
||||
}
|
||||
|
||||
private void sendReply(final Intent sourceIntent, final String message) {
|
||||
final Intent sendIntent = new Intent(this, NoConfirmationSmsSendService.class);
|
||||
sendIntent.setAction(TelephonyManager.ACTION_RESPOND_VIA_MESSAGE);
|
||||
sendIntent.putExtras(sourceIntent);
|
||||
sendIntent.putExtra(Intent.EXTRA_TEXT, message);
|
||||
sendIntent.setClipData(sourceIntent.getClipData());
|
||||
startService(sendIntent);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,7 +22,6 @@ import androidx.annotation.Nullable;
|
||||
import android.text.TextUtils;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup.MarginLayoutParams;
|
||||
import android.widget.FrameLayout;
|
||||
import android.widget.TextView;
|
||||
|
||||
@@ -272,18 +271,12 @@ public class SnackBar {
|
||||
private void setUpButton() {
|
||||
if (mAction == null || mAction.getActionRunnable() == null) {
|
||||
mActionTextView.setVisibility(View.GONE);
|
||||
// In the XML layout we add left/right padding to the button to add space between
|
||||
// the message text and the button and on the right side we add padding to put space
|
||||
// between the button and the edge of the snack bar. This is so the button can use the
|
||||
// padding area as part of it's click target. Since we have no button, we need to put
|
||||
// some margin on the right side. While the left margin is already set on the wrapper,
|
||||
// we're setting it again to not have to make a special case for RTL.
|
||||
final MarginLayoutParams lp = (MarginLayoutParams) mMessageWrapper.getLayoutParams();
|
||||
final int leftRightMargin = mContext.getResources().getDimensionPixelSize(
|
||||
R.dimen.snack_bar_left_right_margin);
|
||||
lp.leftMargin = leftRightMargin;
|
||||
lp.rightMargin = leftRightMargin;
|
||||
mMessageWrapper.setLayoutParams(lp);
|
||||
final int endPad = mSnackBarView.getPaddingStart();
|
||||
mSnackBarView.setPaddingRelative(
|
||||
mSnackBarView.getPaddingStart(),
|
||||
mSnackBarView.getPaddingTop(),
|
||||
endPad,
|
||||
mSnackBarView.getPaddingBottom());
|
||||
} else {
|
||||
mActionTextView.setVisibility(View.VISIBLE);
|
||||
mActionTextView.setText(mAction.getActionLabel());
|
||||
|
||||
@@ -26,9 +26,11 @@ import android.graphics.Rect;
|
||||
import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
|
||||
import androidx.activity.result.ActivityResultLauncher;
|
||||
import androidx.fragment.app.Fragment;
|
||||
|
||||
import com.android.messaging.Factory;
|
||||
import com.android.messaging.datamodel.data.ConversationMessageData;
|
||||
import com.android.messaging.datamodel.data.MessageData;
|
||||
import com.android.messaging.util.ConversationIdSet;
|
||||
|
||||
@@ -69,7 +71,7 @@ public abstract class UIIntents {
|
||||
public static final String UI_INTENT_EXTRA_WITH_CUSTOM_TRANSITION = "with_custom_transition";
|
||||
|
||||
public static final String ACTION_RESET_NOTIFICATIONS =
|
||||
"com.android.messaging.reset_notifications";
|
||||
"com.bnkode.notie.reset_notifications";
|
||||
|
||||
// Sending VCard uri to VCard detail activity
|
||||
public static final String UI_INTENT_EXTRA_VCARD_URI = "vcard_uri";
|
||||
@@ -102,13 +104,18 @@ public abstract class UIIntents {
|
||||
// Sending attachment content type from widget
|
||||
public static final String UI_INTENT_EXTRA_ATTACHMENT_TYPE = "attachment_type";
|
||||
|
||||
public static final String UI_INTENT_EXTRA_LOCATION_TEXT = "location_text";
|
||||
public static final String UI_INTENT_EXTRA_LOCATION_IMAGE_URI = "location_image_uri";
|
||||
|
||||
public static final String ACTION_WIDGET_CONVERSATION =
|
||||
"com.android.messaging.widget_conversation:";
|
||||
"com.bnkode.notie.widget_conversation:";
|
||||
|
||||
public static final String UI_INTENT_EXTRA_REQUIRES_MMS = "requires_mms";
|
||||
|
||||
public static final String UI_INTENT_EXTRA_SELF_ID = "self_id";
|
||||
|
||||
public static final String UI_INTENT_EXTRA_CONVERSATION_NAME = "conversation_name";
|
||||
|
||||
// Message position to scroll to.
|
||||
public static final String UI_INTENT_EXTRA_MESSAGE_POSITION = "message_position";
|
||||
|
||||
@@ -166,11 +173,23 @@ public abstract class UIIntents {
|
||||
public abstract void launchAddContactActivity(final Context context, final String destination);
|
||||
|
||||
/**
|
||||
* Launch an activity to show people & options for a given conversation.
|
||||
* Launch home UI to show the profile of a given conversation.
|
||||
*/
|
||||
public abstract void launchPeopleAndOptionsActivity(final Activity context,
|
||||
public abstract void launchHomeProfileActivity(final Activity activity,
|
||||
final String conversationId);
|
||||
|
||||
/**
|
||||
* Launch home UI to search the messages of a given conversation.
|
||||
*/
|
||||
public abstract void launchHomeConversationSearchActivity(final Context context,
|
||||
final String conversationId);
|
||||
|
||||
/**
|
||||
* Launch home UI for a single message's details.
|
||||
*/
|
||||
public abstract void launchHomeMessageDetailsActivity(final Context context,
|
||||
final ConversationMessageData data);
|
||||
|
||||
/**
|
||||
* Launch an external activity to handle a phone call
|
||||
* @param phoneNumber the phone number to call
|
||||
@@ -184,11 +203,47 @@ public abstract class UIIntents {
|
||||
*/
|
||||
public abstract void launchArchivedConversationsActivity(final Context context);
|
||||
|
||||
/**
|
||||
* Launch an activity to show conversations in Trash.
|
||||
*/
|
||||
public abstract void launchRecentlyDeletedConversationsActivity(final Context context);
|
||||
|
||||
/**
|
||||
* Launch an activity to show blocked participants.
|
||||
*/
|
||||
public abstract void launchBlockedParticipantsActivity(final Context context);
|
||||
|
||||
/**
|
||||
* Launch home UI to manage spam-registered (blocked) senders.
|
||||
*/
|
||||
public abstract void launchHomeSpamActivity(final Context context);
|
||||
|
||||
/**
|
||||
* Launch home UI to search messages.
|
||||
*/
|
||||
public abstract void launchHomeSearchActivity(final Context context);
|
||||
|
||||
/**
|
||||
* Launch home UI to start a new chat.
|
||||
*/
|
||||
public abstract void launchHomeComposeActivity(final Context context);
|
||||
|
||||
/**
|
||||
* Launch home UI for message automations.
|
||||
*/
|
||||
public abstract void launchHomeAutomationActivity(final Context context);
|
||||
|
||||
/**
|
||||
* Launch home UI to create an automation task.
|
||||
*/
|
||||
public abstract void launchHomeAutomationCreateActivity(final Context context);
|
||||
|
||||
/**
|
||||
* Launch 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
|
||||
*/
|
||||
@@ -284,7 +339,8 @@ public abstract class UIIntents {
|
||||
*/
|
||||
public abstract PendingIntent getPendingIntentForSendingMessageToConversation(
|
||||
final Context context, final String conversationId, final String selfId,
|
||||
final boolean requiresMms, final int requestCode);
|
||||
final boolean requiresMms, final CharSequence conversationName,
|
||||
final int requestCode);
|
||||
|
||||
/**
|
||||
* Get a PendingIntent for clearing notifications.
|
||||
|
||||
@@ -35,6 +35,7 @@ import android.provider.MediaStore;
|
||||
import android.support.v7.mms.pdu.ContentType;
|
||||
import android.text.TextUtils;
|
||||
|
||||
import androidx.activity.result.ActivityResultLauncher;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.core.app.TaskStackBuilder;
|
||||
import androidx.localbroadcastmanager.content.LocalBroadcastManager;
|
||||
@@ -44,6 +45,7 @@ import com.android.messaging.R;
|
||||
import com.android.messaging.datamodel.ConversationImagePartsView;
|
||||
import com.android.messaging.datamodel.MediaScratchFileProvider;
|
||||
import com.android.messaging.datamodel.MessagingContentProvider;
|
||||
import com.android.messaging.datamodel.data.ConversationMessageData;
|
||||
import com.android.messaging.datamodel.data.MessageData;
|
||||
import com.android.messaging.datamodel.data.MessagePartData;
|
||||
import com.android.messaging.datamodel.data.ParticipantData;
|
||||
@@ -55,9 +57,10 @@ import com.android.messaging.ui.appsettings.SettingsActivity;
|
||||
import com.android.messaging.ui.conversation.ConversationActivity;
|
||||
import com.android.messaging.ui.conversation.LaunchConversationActivity;
|
||||
import com.android.messaging.ui.conversationlist.ArchivedConversationListActivity;
|
||||
import com.android.messaging.ui.conversationlist.RecentlyDeletedConversationListActivity;
|
||||
import com.android.messaging.ui.conversationlist.ConversationListActivity;
|
||||
import com.android.messaging.ui.conversationlist.ForwardMessageActivity;
|
||||
import com.android.messaging.ui.conversationsettings.PeopleAndOptionsActivity;
|
||||
import com.android.messaging.ui.home.HomeMessageDetailsActivity;
|
||||
import com.android.messaging.ui.photoviewer.BuglePhotoViewActivity;
|
||||
import com.android.messaging.util.Assert;
|
||||
import com.android.messaging.util.ConversationIdSet;
|
||||
@@ -208,6 +211,12 @@ public class UIIntentsImpl extends UIIntents {
|
||||
context.startActivity(intent);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void launchRecentlyDeletedConversationsActivity(final Context context) {
|
||||
final Intent intent = new Intent(context, RecentlyDeletedConversationListActivity.class);
|
||||
context.startActivity(intent);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void launchBlockedParticipantsActivity(final Context context) {
|
||||
final Intent intent = new Intent(context, BlockedParticipantsActivity.class);
|
||||
@@ -215,11 +224,79 @@ public class UIIntentsImpl extends UIIntents {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void launchPeopleAndOptionsActivity(final Activity activity,
|
||||
final String conversationId) {
|
||||
final Intent intent = new Intent(activity, PeopleAndOptionsActivity.class);
|
||||
public void launchHomeSpamActivity(final Context context) {
|
||||
final Intent intent = new Intent(context,
|
||||
com.android.messaging.ui.home.HomeSpamActivity.class);
|
||||
context.startActivity(intent);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void launchHomeSearchActivity(final Context context) {
|
||||
final Intent intent = new Intent(context,
|
||||
com.android.messaging.ui.home.HomeSearchActivity.class);
|
||||
context.startActivity(intent);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void launchHomeComposeActivity(final Context context) {
|
||||
final Intent intent = new Intent(context,
|
||||
com.android.messaging.ui.home.HomeComposeActivity.class);
|
||||
context.startActivity(intent);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void launchHomeAutomationActivity(final Context context) {
|
||||
final Intent intent = new Intent(context,
|
||||
com.android.messaging.ui.home.HomeAutomationActivity.class);
|
||||
context.startActivity(intent);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void launchHomeAutomationCreateActivity(final Context context) {
|
||||
final Intent intent = new Intent(context,
|
||||
com.android.messaging.ui.home.HomeAutomationCreateActivity.class);
|
||||
context.startActivity(intent);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void launchHomeLocationActivity(final Activity activity,
|
||||
final ActivityResultLauncher<Intent> launcher) {
|
||||
final Intent intent = new Intent(activity,
|
||||
com.android.messaging.ui.home.HomeLocationActivity.class);
|
||||
if (launcher != null) {
|
||||
launcher.launch(intent);
|
||||
} else {
|
||||
activity.startActivity(intent);
|
||||
}
|
||||
activity.overridePendingTransition(R.anim.home_activity_open, R.anim.home_activity_hold);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void launchHomeProfileActivity(final Activity activity, final String conversationId) {
|
||||
final Intent intent = new Intent(activity,
|
||||
com.android.messaging.ui.home.HomeProfileActivity.class);
|
||||
intent.putExtra(UI_INTENT_EXTRA_CONVERSATION_ID, conversationId);
|
||||
activity.startActivityForResult(intent, 0);
|
||||
activity.overridePendingTransition(R.anim.home_activity_open, R.anim.home_activity_hold);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void launchHomeConversationSearchActivity(final Context context,
|
||||
final String conversationId) {
|
||||
final Intent intent = new Intent(context,
|
||||
com.android.messaging.ui.home.HomeConversationSearchActivity.class);
|
||||
intent.putExtra(UI_INTENT_EXTRA_CONVERSATION_ID, conversationId);
|
||||
context.startActivity(intent);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void launchHomeMessageDetailsActivity(final Context context,
|
||||
final ConversationMessageData data) {
|
||||
HomeMessageDetailsActivity.start(context, data);
|
||||
if (context instanceof Activity) {
|
||||
((Activity) context).overridePendingTransition(
|
||||
R.anim.home_activity_open, R.anim.home_activity_hold);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -352,7 +429,7 @@ public class UIIntentsImpl extends UIIntents {
|
||||
@Override
|
||||
public PendingIntent getPendingIntentForSendingMessageToConversation(final Context context,
|
||||
final String conversationId, final String selfId, final boolean requiresMms,
|
||||
final int requestCode) {
|
||||
final CharSequence conversationName, final int requestCode) {
|
||||
final Intent intent = new Intent(context, RemoteInputEntrypointActivity.class);
|
||||
intent.setAction(Intent.ACTION_SENDTO);
|
||||
// Ensure that the platform doesn't reuse PendingIntents across conversations
|
||||
@@ -360,6 +437,10 @@ public class UIIntentsImpl extends UIIntents {
|
||||
intent.putExtra(UIIntents.UI_INTENT_EXTRA_CONVERSATION_ID, conversationId);
|
||||
intent.putExtra(UIIntents.UI_INTENT_EXTRA_SELF_ID, selfId);
|
||||
intent.putExtra(UIIntents.UI_INTENT_EXTRA_REQUIRES_MMS, requiresMms);
|
||||
if (!TextUtils.isEmpty(conversationName)) {
|
||||
intent.putExtra(UIIntents.UI_INTENT_EXTRA_CONVERSATION_NAME,
|
||||
conversationName.toString());
|
||||
}
|
||||
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
|
||||
return getPendingIntentWithParentStack(context, intent, requestCode);
|
||||
}
|
||||
|
||||
+235
-115
@@ -24,148 +24,268 @@ import android.content.Intent;
|
||||
import android.os.Bundle;
|
||||
import android.provider.Settings;
|
||||
import android.text.TextUtils;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuItem;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.widget.LinearLayout;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.activity.result.ActivityResultLauncher;
|
||||
import androidx.activity.result.contract.ActivityResultContracts;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.core.app.NavUtils;
|
||||
import androidx.fragment.app.FragmentTransaction;
|
||||
import androidx.preference.Preference;
|
||||
import androidx.preference.PreferenceFragmentCompat;
|
||||
import androidx.preference.PreferenceScreen;
|
||||
|
||||
import com.android.messaging.R;
|
||||
import com.android.messaging.ui.BugleActionBarActivity;
|
||||
import com.android.messaging.ui.LicenseActivity;
|
||||
import com.android.messaging.ui.UIIntents;
|
||||
import com.android.messaging.ui.home.HomeAppLock;
|
||||
import com.android.messaging.ui.home.HomeBottomSheet;
|
||||
import com.android.messaging.ui.home.HomeSecuritySettingsActivity;
|
||||
import com.android.messaging.ui.home.HomeSettingsRow;
|
||||
import com.android.messaging.ui.home.HomeSubHeader;
|
||||
import com.android.messaging.ui.home.HomeToast;
|
||||
import com.android.messaging.util.BuglePrefs;
|
||||
import com.android.messaging.util.ChangeDefaultSmsAppHelper;
|
||||
import com.android.messaging.util.PhoneUtils;
|
||||
import com.google.android.material.bottomsheet.BottomSheetDialog;
|
||||
|
||||
public class ApplicationSettingsActivity extends BugleActionBarActivity {
|
||||
|
||||
private static final String LANGUAGE_SYSTEM = "system";
|
||||
private static final String LANGUAGE_EN = "en";
|
||||
private static final String LANGUAGE_KO = "ko";
|
||||
|
||||
private View mSmsRow;
|
||||
private View mLanguageRow;
|
||||
private String mLanguageTag = LANGUAGE_SYSTEM;
|
||||
private ChangeDefaultSmsAppHelper mChangeDefaultSmsAppHelper;
|
||||
private final ActivityResultLauncher<Intent> mLauncher = registerForActivityResult(
|
||||
new ActivityResultContracts.StartActivityForResult(), result -> {
|
||||
if (result.getResultCode() == Activity.RESULT_OK) {
|
||||
if (mChangeDefaultSmsAppHelper == null) {
|
||||
mChangeDefaultSmsAppHelper = new ChangeDefaultSmsAppHelper();
|
||||
}
|
||||
mChangeDefaultSmsAppHelper.handleChangeDefaultSmsResult(
|
||||
REQUEST_SET_DEFAULT_SMS_APP, result.getResultCode(), null);
|
||||
}
|
||||
});
|
||||
|
||||
@Override
|
||||
protected void onCreate(final Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
|
||||
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
|
||||
final boolean topLevel = getIntent().getBooleanExtra(
|
||||
UIIntents.UI_INTENT_EXTRA_TOP_LEVEL_SETTINGS, false);
|
||||
if (topLevel) {
|
||||
getSupportActionBar().setTitle(getString(R.string.settings_activity_title));
|
||||
setContentView(R.layout.home_settings_activity);
|
||||
if (getSupportActionBar() != null) {
|
||||
getSupportActionBar().hide();
|
||||
}
|
||||
HomeSubHeader.bind(this, R.string.home_settings_title, v -> finish());
|
||||
|
||||
FragmentTransaction ft = getSupportFragmentManager().beginTransaction();
|
||||
ft.replace(android.R.id.content, new ApplicationSettingsFragment());
|
||||
ft.commit();
|
||||
mSmsRow = findViewById(R.id.home_settings_sms);
|
||||
mLanguageRow = findViewById(R.id.home_settings_language);
|
||||
mLanguageTag = BuglePrefs.getApplicationPrefs().getString(
|
||||
getString(R.string.language_pref_key), LANGUAGE_SYSTEM);
|
||||
bindStaticRows();
|
||||
updateSmsRow();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onCreateOptionsMenu(Menu menu) {
|
||||
if (super.onCreateOptionsMenu(menu)) {
|
||||
return true;
|
||||
}
|
||||
getMenuInflater().inflate(R.menu.settings_menu, menu);
|
||||
return true;
|
||||
protected void onResume() {
|
||||
super.onResume();
|
||||
updateSmsRow();
|
||||
bindAppLockRow();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onOptionsItemSelected(@NonNull final MenuItem item) {
|
||||
int itemId = item.getItemId();
|
||||
if (itemId == android.R.id.home) {
|
||||
NavUtils.navigateUpFromSameTask(this);
|
||||
return true;
|
||||
} else if (itemId == R.id.action_license) {
|
||||
final Intent intent = new Intent(this, LicenseActivity.class);
|
||||
startActivity(intent);
|
||||
return true;
|
||||
}
|
||||
return super.onOptionsItemSelected(item);
|
||||
public void finish() {
|
||||
super.finish();
|
||||
overridePendingTransition(R.anim.home_activity_hold, R.anim.home_activity_close);
|
||||
}
|
||||
|
||||
public static class ApplicationSettingsFragment extends PreferenceFragmentCompat {
|
||||
private void bindStaticRows() {
|
||||
final BuglePrefs prefs = BuglePrefs.getApplicationPrefs();
|
||||
|
||||
private String mNotificationsPreferenceKey;
|
||||
private String mSmsEnabledPrefKey;
|
||||
private Preference mSmsEnabledPreference;
|
||||
private ChangeDefaultSmsAppHelper mChangeDefaultSmsAppHelper;
|
||||
private final ActivityResultLauncher<Intent> mLauncher = registerForActivityResult(
|
||||
new ActivityResultContracts.StartActivityForResult(), result -> {
|
||||
if (result.getResultCode() == Activity.RESULT_OK) {
|
||||
if (mChangeDefaultSmsAppHelper == null) {
|
||||
mChangeDefaultSmsAppHelper = new ChangeDefaultSmsAppHelper();
|
||||
}
|
||||
mChangeDefaultSmsAppHelper.handleChangeDefaultSmsResult(
|
||||
REQUEST_SET_DEFAULT_SMS_APP, result.getResultCode(), null);
|
||||
bindComingSoon(R.id.home_settings_premium, R.drawable.ic_home_star_line,
|
||||
R.string.home_settings_premium, R.string.home_settings_premium_subtitle);
|
||||
|
||||
HomeSettingsRow.bind(findViewById(R.id.home_settings_notifications),
|
||||
R.drawable.ic_home_bell,
|
||||
getString(R.string.home_settings_notifications),
|
||||
getString(R.string.home_settings_notifications_subtitle),
|
||||
true, false, false,
|
||||
v -> {
|
||||
final Intent intent = new Intent(Settings.ACTION_APP_NOTIFICATION_SETTINGS);
|
||||
intent.putExtra(Settings.EXTRA_APP_PACKAGE, getPackageName());
|
||||
startActivity(intent);
|
||||
}, null);
|
||||
|
||||
bindRow(R.id.home_settings_sms_mms, R.drawable.ic_home_nav_messages,
|
||||
R.string.home_settings_sms_mms, R.string.home_settings_sms_mms_subtitle,
|
||||
v -> {
|
||||
startActivity(UIIntents.get().getAdvancedSettingsIntent(this));
|
||||
overridePendingTransition(R.anim.home_activity_open, R.anim.home_activity_hold);
|
||||
});
|
||||
|
||||
bindComingSoon(R.id.home_settings_appearance, R.drawable.ic_home_edit,
|
||||
R.string.home_settings_appearance, R.string.home_settings_appearance_subtitle);
|
||||
|
||||
bindLanguageRow();
|
||||
bindAppLockRow();
|
||||
|
||||
final String automationKey = getString(R.string.automation_only_pref_key);
|
||||
final boolean automationOnly = prefs.getBoolean(automationKey,
|
||||
getResources().getBoolean(R.bool.automation_only_pref_default));
|
||||
HomeSettingsRow.bind(findViewById(R.id.home_settings_automation_only),
|
||||
R.drawable.ic_home_nav_automate,
|
||||
getString(R.string.home_settings_automation_only),
|
||||
getString(R.string.home_settings_automation_only_subtitle),
|
||||
false, true, automationOnly,
|
||||
null, (button, checked) -> prefs.putBoolean(automationKey, checked));
|
||||
|
||||
bindRow(R.id.home_settings_automations, R.drawable.ic_home_checklist,
|
||||
R.string.home_settings_automations, R.string.home_settings_automations_subtitle,
|
||||
v -> {
|
||||
UIIntents.get().launchHomeAutomationActivity(this);
|
||||
overridePendingTransition(R.anim.home_activity_open, R.anim.home_activity_hold);
|
||||
});
|
||||
|
||||
final String sendKey = getString(R.string.send_sound_pref_key);
|
||||
final boolean sendSound = prefs.getBoolean(sendKey,
|
||||
getResources().getBoolean(R.bool.send_sound_pref_default));
|
||||
HomeSettingsRow.bind(findViewById(R.id.home_settings_send_sound),
|
||||
R.drawable.ic_home_sound,
|
||||
getString(R.string.home_settings_send_sound),
|
||||
null, false, true, sendSound,
|
||||
null, (button, checked) -> prefs.putBoolean(sendKey, checked));
|
||||
|
||||
bindRow(R.id.home_settings_blocked, R.drawable.ic_home_menu_spam,
|
||||
R.string.home_settings_blocked, R.string.home_settings_blocked_subtitle,
|
||||
v -> {
|
||||
UIIntents.get().launchHomeSpamActivity(this);
|
||||
overridePendingTransition(R.anim.home_activity_open, R.anim.home_activity_hold);
|
||||
});
|
||||
|
||||
bindComingSoon(R.id.home_settings_privacy, R.drawable.ic_home_menu_person,
|
||||
R.string.home_settings_privacy, R.string.home_settings_privacy_subtitle);
|
||||
bindComingSoon(R.id.home_settings_backup, R.drawable.ic_home_archive_line,
|
||||
R.string.home_settings_backup, R.string.home_settings_backup_subtitle);
|
||||
bindComingSoon(R.id.home_settings_data, R.drawable.ic_home_file_line,
|
||||
R.string.home_settings_data, R.string.home_settings_data_subtitle);
|
||||
bindComingSoon(R.id.home_settings_help, R.drawable.ic_home_info,
|
||||
R.string.home_settings_help, R.string.home_settings_help_subtitle);
|
||||
|
||||
bindRow(R.id.home_settings_licenses, R.drawable.ic_info_light,
|
||||
R.string.home_settings_licenses, 0,
|
||||
v -> {
|
||||
startActivity(new Intent(this, LicenseActivity.class));
|
||||
overridePendingTransition(R.anim.home_activity_open, R.anim.home_activity_hold);
|
||||
});
|
||||
}
|
||||
|
||||
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() {
|
||||
if (mSmsRow == null) {
|
||||
return;
|
||||
}
|
||||
String summary = getString(R.string.default_sms_app,
|
||||
PhoneUtils.getDefault().getDefaultSmsAppLabel());
|
||||
if (TextUtils.isEmpty(PhoneUtils.getDefault().getDefaultSmsAppLabel())) {
|
||||
summary = getString(R.string.app_name);
|
||||
}
|
||||
HomeSettingsRow.bind(mSmsRow,
|
||||
R.drawable.ic_home_nav_messages,
|
||||
getString(R.string.home_settings_default_sms),
|
||||
summary, true, false, false,
|
||||
v -> {
|
||||
if (!PhoneUtils.getDefault().isDefaultSmsApp()) {
|
||||
final Intent intent = UIIntents.get().getChangeDefaultSmsAppIntent(this);
|
||||
mLauncher.launch(intent);
|
||||
}
|
||||
});
|
||||
|
||||
public ApplicationSettingsFragment() {
|
||||
// Required empty constructor
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreatePreferences(@Nullable Bundle savedInstanceState,
|
||||
@Nullable String rootKey) {
|
||||
getPreferenceManager().setSharedPreferencesName(BuglePrefs.SHARED_PREFERENCES_NAME);
|
||||
addPreferencesFromResource(R.xml.preferences_application);
|
||||
|
||||
mNotificationsPreferenceKey =
|
||||
getString(R.string.notifications_pref_key);
|
||||
mSmsEnabledPrefKey = getString(R.string.sms_enabled_pref_key);
|
||||
mSmsEnabledPreference = findPreference(mSmsEnabledPrefKey);
|
||||
|
||||
final PreferenceScreen advancedScreen = findPreference(
|
||||
getString(R.string.advanced_pref_key));
|
||||
final boolean topLevel = getActivity().getIntent().getBooleanExtra(
|
||||
UIIntents.UI_INTENT_EXTRA_TOP_LEVEL_SETTINGS, false);
|
||||
if (topLevel) {
|
||||
advancedScreen.setIntent(UIIntents.get()
|
||||
.getAdvancedSettingsIntent(getPreferenceScreen().getContext()));
|
||||
} else {
|
||||
// Hide the Advanced settings screen if this is not top-level; these are shown at
|
||||
// the parent SettingsActivity.
|
||||
getPreferenceScreen().removePreference(advancedScreen);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onPreferenceTreeClick(@NonNull Preference preference) {
|
||||
if (preference.getKey().equals(mNotificationsPreferenceKey)) {
|
||||
Intent intent = new Intent(Settings.ACTION_APP_NOTIFICATION_SETTINGS);
|
||||
intent.putExtra(Settings.EXTRA_APP_PACKAGE, getContext().getPackageName());
|
||||
startActivity(intent);
|
||||
}
|
||||
return super.onPreferenceTreeClick(preference);
|
||||
}
|
||||
|
||||
private void updateSmsEnabledPreferences() {
|
||||
String defaultSmsAppLabel = getString(R.string.default_sms_app,
|
||||
PhoneUtils.getDefault().getDefaultSmsAppLabel());
|
||||
if (!PhoneUtils.getDefault().isDefaultSmsApp()) {
|
||||
mSmsEnabledPreference.setOnPreferenceClickListener(preference -> {
|
||||
final Intent intent =
|
||||
UIIntents.get().getChangeDefaultSmsAppIntent(getActivity());
|
||||
mLauncher.launch(intent);
|
||||
return true;
|
||||
});
|
||||
} else {
|
||||
mSmsEnabledPreference.setOnPreferenceClickListener(null);
|
||||
// Fallback since we don't always get our own name
|
||||
if (TextUtils.isEmpty(defaultSmsAppLabel)) {
|
||||
defaultSmsAppLabel = getString(R.string.app_name);
|
||||
}
|
||||
}
|
||||
mSmsEnabledPreference.setSummary(defaultSmsAppLabel);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onResume() {
|
||||
super.onResume();
|
||||
updateSmsEnabledPreferences();
|
||||
}
|
||||
}, null);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,12 +16,13 @@
|
||||
*/
|
||||
package com.android.messaging.ui.appsettings;
|
||||
|
||||
import android.app.AlertDialog;
|
||||
import android.content.Context;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.widget.RadioButton;
|
||||
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
|
||||
import com.android.messaging.R;
|
||||
import com.android.messaging.util.Assert;
|
||||
import com.android.messaging.util.BuglePrefs;
|
||||
@@ -48,9 +49,9 @@ public class GroupMmsSettingDialog {
|
||||
|
||||
private void show() {
|
||||
Assert.isNull(mDialog);
|
||||
mDialog = new AlertDialog.Builder(mContext, R.style.AlertDialogTheme)
|
||||
mDialog = new AlertDialog.Builder(mContext, R.style.Home_Dialog)
|
||||
.setView(createView())
|
||||
.setTitle(R.string.group_mms_pref_title)
|
||||
.setTitle(R.string.home_settings_group_mms)
|
||||
.setNegativeButton(android.R.string.cancel, null)
|
||||
.show();
|
||||
}
|
||||
|
||||
+179
-135
@@ -17,174 +17,218 @@
|
||||
|
||||
package com.android.messaging.ui.appsettings;
|
||||
|
||||
import android.content.Intent;
|
||||
import android.content.SharedPreferences;
|
||||
import android.content.SharedPreferences.OnSharedPreferenceChangeListener;
|
||||
import android.os.Bundle;
|
||||
import android.text.TextUtils;
|
||||
import android.view.MenuItem;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.widget.EditText;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.core.app.NavUtils;
|
||||
import androidx.fragment.app.FragmentTransaction;
|
||||
import androidx.preference.Preference;
|
||||
import androidx.preference.PreferenceCategory;
|
||||
import androidx.preference.PreferenceFragmentCompat;
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
|
||||
import com.android.messaging.Factory;
|
||||
import com.android.messaging.R;
|
||||
import com.android.messaging.datamodel.ParticipantRefresh;
|
||||
import com.android.messaging.datamodel.data.ParticipantData;
|
||||
import com.android.messaging.sms.MmsConfig;
|
||||
import com.android.messaging.ui.BugleActionBarActivity;
|
||||
import com.android.messaging.ui.UIIntents;
|
||||
import com.android.messaging.ui.home.HomeSettingsRow;
|
||||
import com.android.messaging.ui.home.HomeSubHeader;
|
||||
import com.android.messaging.util.BuglePrefs;
|
||||
import com.android.messaging.util.PhoneUtils;
|
||||
|
||||
public class PerSubscriptionSettingsActivity extends BugleActionBarActivity {
|
||||
private int mSubId;
|
||||
private BuglePrefs mPrefs;
|
||||
private String mDefaultPhoneNumber;
|
||||
private View mGroupRow;
|
||||
private View mPhoneRow;
|
||||
private View mRoamingRow;
|
||||
|
||||
@Override
|
||||
protected void onCreate(final Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
|
||||
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
|
||||
final String title = getIntent().getStringExtra(
|
||||
UIIntents.UI_INTENT_EXTRA_PER_SUBSCRIPTION_SETTING_TITLE);
|
||||
if (!TextUtils.isEmpty(title)) {
|
||||
getSupportActionBar().setTitle(title);
|
||||
} else {
|
||||
// This will fall back to the default title, i.e. "Messaging settings," so No-op.
|
||||
setContentView(R.layout.home_settings_advanced_activity);
|
||||
if (getSupportActionBar() != null) {
|
||||
getSupportActionBar().hide();
|
||||
}
|
||||
|
||||
final FragmentTransaction ft = getSupportFragmentManager().beginTransaction();
|
||||
final PerSubscriptionSettingsFragment fragment = new PerSubscriptionSettingsFragment();
|
||||
ft.replace(android.R.id.content, fragment);
|
||||
ft.commit();
|
||||
mSubId = getIntent().getIntExtra(UIIntents.UI_INTENT_EXTRA_SUB_ID,
|
||||
ParticipantData.DEFAULT_SELF_SUB_ID);
|
||||
mPrefs = BuglePrefs.getSubscriptionPrefs(mSubId);
|
||||
mDefaultPhoneNumber = PhoneUtils.get(mSubId).getCanonicalForSelf(false /* allowOverride */);
|
||||
|
||||
final String title = getIntent().getStringExtra(
|
||||
UIIntents.UI_INTENT_EXTRA_PER_SUBSCRIPTION_SETTING_TITLE);
|
||||
HomeSubHeader.bind(this,
|
||||
TextUtils.isEmpty(title) ? getString(R.string.home_settings_advanced) : title,
|
||||
v -> finish());
|
||||
|
||||
mGroupRow = findViewById(R.id.home_settings_group_mms);
|
||||
mPhoneRow = findViewById(R.id.home_settings_phone_number);
|
||||
mRoamingRow = findViewById(R.id.home_settings_roaming_retrieve);
|
||||
bindRows();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onOptionsItemSelected(@NonNull final MenuItem item) {
|
||||
if (item.getItemId() == android.R.id.home) {
|
||||
NavUtils.navigateUpFromSameTask(this);
|
||||
return true;
|
||||
}
|
||||
return super.onOptionsItemSelected(item);
|
||||
protected void onResume() {
|
||||
super.onResume();
|
||||
updateGroupRow();
|
||||
updatePhoneRow();
|
||||
}
|
||||
|
||||
public static class PerSubscriptionSettingsFragment extends PreferenceFragmentCompat
|
||||
implements OnSharedPreferenceChangeListener {
|
||||
private PhoneNumberPreference mPhoneNumberPreference;
|
||||
private Preference mGroupMmsPreference;
|
||||
private String mGroupMmsPrefKey;
|
||||
private String mPhoneNumberKey;
|
||||
private int mSubId;
|
||||
@Override
|
||||
public void finish() {
|
||||
super.finish();
|
||||
overridePendingTransition(R.anim.home_activity_hold, R.anim.home_activity_close);
|
||||
}
|
||||
|
||||
public PerSubscriptionSettingsFragment() {
|
||||
// Required empty constructor
|
||||
private void bindRows() {
|
||||
final boolean defaultSms = PhoneUtils.getDefault().isDefaultSmsApp();
|
||||
final boolean groupEnabled = MmsConfig.get(mSubId).getGroupMmsEnabled();
|
||||
final boolean deliveryEnabled = MmsConfig.get(mSubId).getSMSDeliveryReportsEnabled();
|
||||
|
||||
if (groupEnabled) {
|
||||
mGroupRow.setVisibility(View.VISIBLE);
|
||||
updateGroupRow();
|
||||
HomeSettingsRow.setEnabled(mGroupRow, defaultSms);
|
||||
} else {
|
||||
mGroupRow.setVisibility(View.GONE);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreatePreferences(@Nullable Bundle savedInstanceState, String rootKey) {
|
||||
// Get sub id from launch intent
|
||||
final Intent intent = requireActivity().getIntent();
|
||||
mSubId = (intent != null) ? intent.getIntExtra(UIIntents.UI_INTENT_EXTRA_SUB_ID,
|
||||
ParticipantData.DEFAULT_SELF_SUB_ID) : ParticipantData.DEFAULT_SELF_SUB_ID;
|
||||
updatePhoneRow();
|
||||
|
||||
final BuglePrefs subPrefs = Factory.get().getSubscriptionPrefs(mSubId);
|
||||
getPreferenceManager().setSharedPreferencesName(subPrefs.getSharedPreferencesName());
|
||||
addPreferencesFromResource(R.xml.preferences_per_subscription);
|
||||
|
||||
mPhoneNumberKey = getString(R.string.mms_phone_number_pref_key);
|
||||
mPhoneNumberPreference = findPreference(mPhoneNumberKey);
|
||||
final PreferenceCategory advancedCategory =
|
||||
findPreference(getString(R.string.advanced_category_pref_key));
|
||||
final PreferenceCategory mmsCategory =
|
||||
findPreference(getString(R.string.mms_messaging_category_pref_key));
|
||||
|
||||
mPhoneNumberPreference.setDefaultPhoneNumber(
|
||||
PhoneUtils.get(mSubId).getCanonicalForSelf(false/*allowOverride*/), mSubId);
|
||||
|
||||
mGroupMmsPrefKey = getString(R.string.group_mms_pref_key);
|
||||
mGroupMmsPreference = findPreference(mGroupMmsPrefKey);
|
||||
if (!MmsConfig.get(mSubId).getGroupMmsEnabled()) {
|
||||
// Always show group messaging setting even if the SIM has no number
|
||||
// If broadcast sms is selected, the SIM number is not needed
|
||||
// If group mms is selected, the phone number dialog will popup when message
|
||||
// is being sent, making sure we will have a self number for group mms.
|
||||
mmsCategory.removePreference(mGroupMmsPreference);
|
||||
} else {
|
||||
mGroupMmsPreference.setOnPreferenceClickListener(pref -> {
|
||||
GroupMmsSettingDialog.showDialog(getActivity(), mSubId);
|
||||
return true;
|
||||
final String autoKey = getString(R.string.auto_retrieve_mms_pref_key);
|
||||
final boolean autoOn = mPrefs.getBoolean(autoKey,
|
||||
getResources().getBoolean(R.bool.auto_retrieve_mms_pref_default));
|
||||
HomeSettingsRow.bind(findViewById(R.id.home_settings_auto_retrieve),
|
||||
R.drawable.ic_home_file_line,
|
||||
getString(R.string.home_settings_auto_retrieve),
|
||||
getString(R.string.home_settings_auto_retrieve_subtitle),
|
||||
false, true, autoOn, null,
|
||||
(button, checked) -> {
|
||||
mPrefs.putBoolean(autoKey, checked);
|
||||
updateRoamingEnabled(checked && defaultSms);
|
||||
});
|
||||
updateGroupMmsPrefSummary();
|
||||
}
|
||||
HomeSettingsRow.setEnabled(findViewById(R.id.home_settings_auto_retrieve), defaultSms);
|
||||
|
||||
if (!MmsConfig.get(mSubId).getSMSDeliveryReportsEnabled()) {
|
||||
final Preference deliveryReportsPref = findPreference(
|
||||
getString(R.string.delivery_reports_pref_key));
|
||||
advancedCategory.removePreference(deliveryReportsPref);
|
||||
}
|
||||
final String roamingKey = getString(R.string.auto_retrieve_mms_when_roaming_pref_key);
|
||||
final boolean roamingOn = mPrefs.getBoolean(roamingKey,
|
||||
getResources().getBoolean(R.bool.auto_retrieve_mms_when_roaming_pref_default));
|
||||
HomeSettingsRow.bind(mRoamingRow,
|
||||
R.drawable.ic_home_place_line,
|
||||
getString(R.string.home_settings_roaming_retrieve),
|
||||
getString(R.string.home_settings_roaming_retrieve_subtitle),
|
||||
false, true, roamingOn, null,
|
||||
(button, checked) -> mPrefs.putBoolean(roamingKey, checked));
|
||||
updateRoamingEnabled(autoOn && defaultSms);
|
||||
|
||||
// We want to disable preferences if we are not the default app, but we do all of the
|
||||
// above first so that the user sees the correct information on the screen
|
||||
if (!PhoneUtils.getDefault().isDefaultSmsApp()) {
|
||||
mGroupMmsPreference.setEnabled(false);
|
||||
final Preference autoRetrieveMmsPreference =
|
||||
findPreference(getString(R.string.auto_retrieve_mms_pref_key));
|
||||
autoRetrieveMmsPreference.setEnabled(false);
|
||||
final Preference deliveryReportsPreference =
|
||||
findPreference(getString(R.string.delivery_reports_pref_key));
|
||||
if (deliveryReportsPreference != null) {
|
||||
deliveryReportsPreference.setEnabled(false);
|
||||
}
|
||||
}
|
||||
if (deliveryEnabled) {
|
||||
final String deliveryKey = getString(R.string.delivery_reports_pref_key);
|
||||
final boolean deliveryOn = mPrefs.getBoolean(deliveryKey,
|
||||
getResources().getBoolean(R.bool.delivery_reports_pref_default));
|
||||
HomeSettingsRow.bind(findViewById(R.id.home_settings_delivery_reports),
|
||||
R.drawable.ic_home_checklist,
|
||||
getString(R.string.home_settings_delivery_reports),
|
||||
getString(R.string.home_settings_delivery_reports_subtitle),
|
||||
false, true, deliveryOn, null,
|
||||
(button, checked) -> mPrefs.putBoolean(deliveryKey, checked));
|
||||
HomeSettingsRow.setEnabled(findViewById(R.id.home_settings_delivery_reports),
|
||||
defaultSms);
|
||||
} else {
|
||||
findViewById(R.id.home_settings_delivery_label).setVisibility(View.GONE);
|
||||
findViewById(R.id.home_settings_delivery_card).setVisibility(View.GONE);
|
||||
}
|
||||
}
|
||||
|
||||
if (advancedCategory.getPreferenceCount() == 0) {
|
||||
getPreferenceScreen().removePreference(advancedCategory);
|
||||
private void updateGroupRow() {
|
||||
if (mGroupRow == null || mGroupRow.getVisibility() != View.VISIBLE) {
|
||||
return;
|
||||
}
|
||||
final boolean groupOn = mPrefs.getBoolean(getString(R.string.group_mms_pref_key),
|
||||
getResources().getBoolean(R.bool.group_mms_pref_default));
|
||||
HomeSettingsRow.bind(mGroupRow,
|
||||
R.drawable.ic_home_nav_messages,
|
||||
getString(R.string.home_settings_group_mms),
|
||||
getString(groupOn
|
||||
? R.string.home_settings_group_mms_on
|
||||
: R.string.home_settings_group_mms_off),
|
||||
true, false, false,
|
||||
v -> GroupMmsSettingDialog.showDialog(this, mSubId),
|
||||
null);
|
||||
HomeSettingsRow.setEnabled(mGroupRow, PhoneUtils.getDefault().isDefaultSmsApp());
|
||||
}
|
||||
|
||||
private void updatePhoneRow() {
|
||||
if (mPhoneRow == null) {
|
||||
return;
|
||||
}
|
||||
HomeSettingsRow.bind(mPhoneRow,
|
||||
R.drawable.ic_home_menu_person,
|
||||
getString(R.string.home_settings_phone_number),
|
||||
formatPhoneSummary(),
|
||||
true, false, false,
|
||||
v -> showPhoneNumberDialog(),
|
||||
null);
|
||||
}
|
||||
|
||||
private void updateRoamingEnabled(final boolean enabled) {
|
||||
if (mRoamingRow != null) {
|
||||
HomeSettingsRow.setEnabled(mRoamingRow, enabled);
|
||||
}
|
||||
}
|
||||
|
||||
private String formatPhoneSummary() {
|
||||
String value = mPrefs.getString(getString(R.string.mms_phone_number_pref_key), "");
|
||||
if (TextUtils.isEmpty(value)) {
|
||||
value = mDefaultPhoneNumber;
|
||||
}
|
||||
if (TextUtils.isEmpty(value)) {
|
||||
return getString(R.string.home_settings_phone_unknown);
|
||||
}
|
||||
return PhoneUtils.get(mSubId).formatForDisplay(value);
|
||||
}
|
||||
|
||||
private void showPhoneNumberDialog() {
|
||||
final View content = LayoutInflater.from(this)
|
||||
.inflate(R.layout.home_settings_phone_dialog, null);
|
||||
final EditText field = content.findViewById(R.id.home_settings_phone_field);
|
||||
String value = mPrefs.getString(getString(R.string.mms_phone_number_pref_key), "");
|
||||
if (TextUtils.isEmpty(value)) {
|
||||
value = mDefaultPhoneNumber;
|
||||
}
|
||||
if (value == null) {
|
||||
value = "";
|
||||
}
|
||||
field.setText(value);
|
||||
if (!TextUtils.isEmpty(value)) {
|
||||
field.setSelection(value.length());
|
||||
}
|
||||
new AlertDialog.Builder(this, R.style.Home_Dialog)
|
||||
.setTitle(R.string.home_settings_phone_number)
|
||||
.setView(content)
|
||||
.setNegativeButton(android.R.string.cancel, null)
|
||||
.setPositiveButton(android.R.string.ok, (dialog, which) -> {
|
||||
savePhoneNumber(field.getText().toString().trim());
|
||||
updatePhoneRow();
|
||||
})
|
||||
.show();
|
||||
}
|
||||
|
||||
private void savePhoneNumber(final String text) {
|
||||
final String key = getString(R.string.mms_phone_number_pref_key);
|
||||
if (TextUtils.isEmpty(text)) {
|
||||
mPrefs.remove(key);
|
||||
} else {
|
||||
final PhoneUtils phoneUtils = PhoneUtils.get(mSubId);
|
||||
final String canonical = phoneUtils.getCanonicalBySystemLocale(text);
|
||||
if (!TextUtils.isEmpty(mDefaultPhoneNumber)
|
||||
&& TextUtils.equals(canonical,
|
||||
phoneUtils.getCanonicalBySystemLocale(mDefaultPhoneNumber))) {
|
||||
mPrefs.remove(key);
|
||||
} else {
|
||||
mPrefs.putString(key, text);
|
||||
}
|
||||
}
|
||||
|
||||
private void updateGroupMmsPrefSummary() {
|
||||
final boolean groupMmsEnabled = getPreferenceScreen().getSharedPreferences().getBoolean(
|
||||
mGroupMmsPrefKey, getResources().getBoolean(R.bool.group_mms_pref_default));
|
||||
mGroupMmsPreference.setSummary(groupMmsEnabled ?
|
||||
R.string.enable_group_mms : R.string.disable_group_mms);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onResume() {
|
||||
super.onResume();
|
||||
getPreferenceScreen().getSharedPreferences()
|
||||
.registerOnSharedPreferenceChangeListener(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSharedPreferenceChanged(final SharedPreferences sharedPreferences,
|
||||
final String key) {
|
||||
if (key.equals(mGroupMmsPrefKey)) {
|
||||
updateGroupMmsPrefSummary();
|
||||
} else if (key.equals(mPhoneNumberKey)) {
|
||||
// Save the changed phone number in preferences specific to the sub id
|
||||
final String newPhoneNumber = mPhoneNumberPreference.getText();
|
||||
final BuglePrefs subPrefs = BuglePrefs.getSubscriptionPrefs(mSubId);
|
||||
if (TextUtils.isEmpty(newPhoneNumber)) {
|
||||
subPrefs.remove(mPhoneNumberKey);
|
||||
} else {
|
||||
subPrefs.putString(getString(R.string.mms_phone_number_pref_key),
|
||||
newPhoneNumber);
|
||||
}
|
||||
// Update the self participants so the new phone number will be reflected
|
||||
// everywhere in the UI.
|
||||
ParticipantRefresh.refreshSelfParticipants();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPause() {
|
||||
super.onPause();
|
||||
getPreferenceScreen().getSharedPreferences()
|
||||
.unregisterOnSharedPreferenceChangeListener(this);
|
||||
}
|
||||
ParticipantRefresh.refreshSelfParticipants();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,20 +17,11 @@
|
||||
|
||||
package com.android.messaging.ui.appsettings;
|
||||
|
||||
import android.content.Context;
|
||||
import android.os.Bundle;
|
||||
import android.text.TextUtils;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.ArrayAdapter;
|
||||
import android.widget.ListView;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.core.app.NavUtils;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import androidx.loader.app.LoaderManager;
|
||||
|
||||
import com.android.messaging.R;
|
||||
@@ -42,137 +33,98 @@ import com.android.messaging.datamodel.data.SettingsData.SettingsDataListener;
|
||||
import com.android.messaging.datamodel.data.SettingsData.SettingsItem;
|
||||
import com.android.messaging.ui.BugleActionBarActivity;
|
||||
import com.android.messaging.ui.UIIntents;
|
||||
import com.android.messaging.ui.home.HomeSettingsRow;
|
||||
import com.android.messaging.ui.home.HomeSubHeader;
|
||||
import com.android.messaging.util.Assert;
|
||||
import com.android.messaging.util.PhoneUtils;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Shows the "primary" settings activity that contains two parts, one for application-wide settings
|
||||
* (dubbed "General settings"), and one or more for per-subscription settings (dubbed "Messaging
|
||||
* settings" for single-SIM, and the actual SIM name for multi-SIM). Clicking on either item
|
||||
* (e.g. "General settings") will open the detail settings activity (ApplicationSettingsActivity
|
||||
* in this case).
|
||||
* Multi-SIM settings hub. Single-SIM devices skip this and open ApplicationSettingsActivity.
|
||||
*/
|
||||
public class SettingsActivity extends BugleActionBarActivity {
|
||||
public class SettingsActivity extends BugleActionBarActivity implements SettingsDataListener {
|
||||
private final Binding<SettingsData> mBinding = BindingBase.createBinding(this);
|
||||
private ViewGroup mList;
|
||||
|
||||
@Override
|
||||
protected void onCreate(final Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
|
||||
|
||||
// Directly open the detailed settings page as the top-level settings activity if this is
|
||||
// not a multi-SIM device.
|
||||
if (PhoneUtils.getDefault().getActiveSubscriptionCount() <= 1) {
|
||||
UIIntents.get().launchApplicationSettingsActivity(this, true /* topLevel */);
|
||||
finish();
|
||||
} else {
|
||||
getSupportFragmentManager().beginTransaction()
|
||||
.replace(android.R.id.content, new SettingsFragment())
|
||||
.commit();
|
||||
overridePendingTransition(0, 0);
|
||||
return;
|
||||
}
|
||||
|
||||
setContentView(R.layout.home_settings_hub_activity);
|
||||
if (getSupportActionBar() != null) {
|
||||
getSupportActionBar().hide();
|
||||
}
|
||||
HomeSubHeader.bind(this, R.string.home_settings_title, v -> finish());
|
||||
mList = findViewById(R.id.home_settings_hub_list);
|
||||
|
||||
mBinding.bind(DataModel.get().createSettingsData(this, this));
|
||||
mBinding.getData().init(LoaderManager.getInstance(this), mBinding);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDestroy() {
|
||||
super.onDestroy();
|
||||
if (mBinding.isBound()) {
|
||||
mBinding.unbind();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onOptionsItemSelected(@NonNull final MenuItem item) {
|
||||
if (item.getItemId() == android.R.id.home) {
|
||||
NavUtils.navigateUpFromSameTask(this);
|
||||
return true;
|
||||
}
|
||||
return super.onOptionsItemSelected(item);
|
||||
public void finish() {
|
||||
super.finish();
|
||||
overridePendingTransition(R.anim.home_activity_hold, R.anim.home_activity_close);
|
||||
}
|
||||
|
||||
public static class SettingsFragment extends Fragment implements SettingsDataListener {
|
||||
private ListView mListView;
|
||||
private SettingsListAdapter mAdapter;
|
||||
private final Binding<SettingsData> mBinding = BindingBase.createBinding(this);
|
||||
|
||||
@Override
|
||||
public void onCreate(final Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
mBinding.bind(DataModel.get().createSettingsData(getActivity(), this));
|
||||
mBinding.getData().init(LoaderManager.getInstance(this), mBinding);
|
||||
@Override
|
||||
public void onSelfParticipantDataLoaded(final SettingsData data) {
|
||||
mBinding.ensureBound(data);
|
||||
if (mList == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
@Override
|
||||
public View onCreateView(final LayoutInflater inflater, final ViewGroup container,
|
||||
final Bundle savedInstanceState) {
|
||||
final View view = inflater.inflate(R.layout.settings_fragment, container, false);
|
||||
mListView = view.findViewById(android.R.id.list);
|
||||
mAdapter = new SettingsListAdapter(getActivity());
|
||||
mListView.setAdapter(mAdapter);
|
||||
return view;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroy() {
|
||||
super.onDestroy();
|
||||
mBinding.unbind();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSelfParticipantDataLoaded(SettingsData data) {
|
||||
mBinding.ensureBound(data);
|
||||
mAdapter.setSettingsItems(data.getSettingsItems());
|
||||
}
|
||||
|
||||
/**
|
||||
* An adapter that displays a list of SettingsItem.
|
||||
*/
|
||||
private class SettingsListAdapter extends ArrayAdapter<SettingsItem> {
|
||||
public SettingsListAdapter(final Context context) {
|
||||
super(context, R.layout.settings_item_view, new ArrayList<>());
|
||||
mList.removeAllViews();
|
||||
final LayoutInflater inflater = LayoutInflater.from(this);
|
||||
boolean first = true;
|
||||
for (final SettingsItem item : data.getSettingsItems()) {
|
||||
final View row = inflater.inflate(R.layout.home_settings_row, mList, false);
|
||||
if (!first) {
|
||||
final ViewGroup.MarginLayoutParams lp =
|
||||
(ViewGroup.MarginLayoutParams) row.getLayoutParams();
|
||||
lp.topMargin = getResources().getDimensionPixelSize(R.dimen.looply_space_xs);
|
||||
row.setLayoutParams(lp);
|
||||
}
|
||||
first = false;
|
||||
final int icon = item.getType() == SettingsItem.TYPE_GENERAL_SETTINGS
|
||||
? R.drawable.ic_home_nav_settings
|
||||
: R.drawable.ic_home_nav_messages;
|
||||
final CharSequence title = item.getType() == SettingsItem.TYPE_GENERAL_SETTINGS
|
||||
? getString(R.string.home_settings_general)
|
||||
: item.getDisplayName();
|
||||
HomeSettingsRow.bind(row, icon, title, item.getDisplayDetail(),
|
||||
true, false, false,
|
||||
v -> openItem(item), null);
|
||||
mList.addView(row);
|
||||
}
|
||||
}
|
||||
|
||||
public void setSettingsItems(final List<SettingsItem> newList) {
|
||||
clear();
|
||||
addAll(newList);
|
||||
notifyDataSetChanged();
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public View getView(final int position, final View convertView,
|
||||
@NonNull final ViewGroup parent) {
|
||||
View itemView;
|
||||
if (convertView != null) {
|
||||
itemView = convertView;
|
||||
} else {
|
||||
final LayoutInflater inflater = (LayoutInflater) getContext()
|
||||
.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
|
||||
itemView = inflater.inflate(
|
||||
R.layout.settings_item_view, parent, false);
|
||||
}
|
||||
final SettingsItem item = getItem(position);
|
||||
final TextView titleTextView = itemView.findViewById(R.id.title);
|
||||
final TextView subtitleTextView = itemView.findViewById(R.id.subtitle);
|
||||
final String summaryText = item.getDisplayDetail();
|
||||
titleTextView.setText(item.getDisplayName());
|
||||
if (!TextUtils.isEmpty(summaryText)) {
|
||||
subtitleTextView.setText(summaryText);
|
||||
subtitleTextView.setVisibility(View.VISIBLE);
|
||||
} else {
|
||||
subtitleTextView.setVisibility(View.GONE);
|
||||
}
|
||||
itemView.setOnClickListener(view -> {
|
||||
switch (item.getType()) {
|
||||
case SettingsItem.TYPE_GENERAL_SETTINGS:
|
||||
UIIntents.get().launchApplicationSettingsActivity(getActivity(),
|
||||
false /* topLevel */);
|
||||
break;
|
||||
|
||||
case SettingsItem.TYPE_PER_SUBSCRIPTION_SETTINGS:
|
||||
UIIntents.get().launchPerSubscriptionSettingsActivity(getActivity(),
|
||||
item.getSubId(), item.getActivityTitle());
|
||||
break;
|
||||
|
||||
default:
|
||||
Assert.fail("unrecognized setting type!");
|
||||
break;
|
||||
}
|
||||
});
|
||||
return itemView;
|
||||
}
|
||||
private void openItem(final SettingsItem item) {
|
||||
switch (item.getType()) {
|
||||
case SettingsItem.TYPE_GENERAL_SETTINGS:
|
||||
UIIntents.get().launchApplicationSettingsActivity(this, false /* topLevel */);
|
||||
overridePendingTransition(R.anim.home_activity_open, R.anim.home_activity_hold);
|
||||
break;
|
||||
case SettingsItem.TYPE_PER_SUBSCRIPTION_SETTINGS:
|
||||
UIIntents.get().launchPerSubscriptionSettingsActivity(this,
|
||||
item.getSubId(), item.getActivityTitle());
|
||||
overridePendingTransition(R.anim.home_activity_open, R.anim.home_activity_hold);
|
||||
break;
|
||||
default:
|
||||
Assert.fail("unrecognized setting type!");
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,6 +20,7 @@ import android.content.Context;
|
||||
import android.database.Cursor;
|
||||
import android.net.Uri;
|
||||
import android.provider.ContactsContract.CommonDataKinds.Phone;
|
||||
import android.text.TextUtils;
|
||||
import android.util.AttributeSet;
|
||||
import android.view.View;
|
||||
import android.view.View.OnClickListener;
|
||||
@@ -33,6 +34,7 @@ import com.android.messaging.datamodel.DataModel;
|
||||
import com.android.messaging.datamodel.data.ContactListItemData;
|
||||
import com.android.messaging.datamodel.data.ParticipantData;
|
||||
import com.android.messaging.ui.ContactIconView;
|
||||
import com.android.messaging.ui.home.HomeAvatarOverlay;
|
||||
import com.android.messaging.util.Assert;
|
||||
import com.android.messaging.util.AvatarUriUtil;
|
||||
|
||||
@@ -43,6 +45,10 @@ public class ContactListItemView extends LinearLayout implements OnClickListener
|
||||
public interface HostInterface {
|
||||
void onContactListItemClicked(ContactListItemData item, ContactListItemView view);
|
||||
boolean isContactSelected(ContactListItemData item);
|
||||
|
||||
default boolean isContactSelectionMode() {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
final ContactListItemData mData;
|
||||
@@ -53,9 +59,19 @@ public class ContactListItemView extends LinearLayout implements OnClickListener
|
||||
private ContactIconView mContactIconView;
|
||||
private ImageView mContactCheckmarkView;
|
||||
private ImageView mWorkProfileIcon;
|
||||
private View mSectionCard;
|
||||
private View mSectionDivider;
|
||||
private View mSectionGapTop;
|
||||
private View mSectionGapBottom;
|
||||
private HostInterface mHostInterface;
|
||||
private boolean mShouldShowAlphabetHeader;
|
||||
|
||||
public static final int SECTION_NONE = 0;
|
||||
public static final int SECTION_SINGLE = 1;
|
||||
public static final int SECTION_FIRST = 2;
|
||||
public static final int SECTION_MIDDLE = 3;
|
||||
public static final int SECTION_LAST = 4;
|
||||
|
||||
public ContactListItemView(final Context context, final AttributeSet attrs) {
|
||||
super(context, attrs);
|
||||
mData = DataModel.get().createContactListItemData();
|
||||
@@ -71,6 +87,55 @@ public class ContactListItemView extends LinearLayout implements OnClickListener
|
||||
mContactIconView = findViewById(R.id.contact_icon);
|
||||
mContactCheckmarkView = findViewById(R.id.contact_checkmark);
|
||||
mWorkProfileIcon = findViewById(R.id.work_profile_icon);
|
||||
mSectionCard = findViewById(R.id.contact_section_card);
|
||||
mSectionDivider = findViewById(R.id.contact_section_divider);
|
||||
mSectionGapTop = findViewById(R.id.contact_section_gap_top);
|
||||
mSectionGapBottom = findViewById(R.id.contact_section_gap_bottom);
|
||||
}
|
||||
|
||||
/** Draws this row as part of a letter-grouped card, or clears the chrome. */
|
||||
public void setSectionBox(final int role) {
|
||||
if (mSectionCard == null) {
|
||||
return;
|
||||
}
|
||||
if (role == SECTION_NONE) {
|
||||
mSectionCard.setBackground(null);
|
||||
mSectionCard.setClipToOutline(false);
|
||||
if (mSectionDivider != null) {
|
||||
mSectionDivider.setVisibility(GONE);
|
||||
}
|
||||
if (mSectionGapTop != null) {
|
||||
mSectionGapTop.setVisibility(GONE);
|
||||
}
|
||||
if (mSectionGapBottom != null) {
|
||||
mSectionGapBottom.setVisibility(GONE);
|
||||
}
|
||||
return;
|
||||
}
|
||||
final int background;
|
||||
if (role == SECTION_SINGLE) {
|
||||
background = R.drawable.home_bg_contact_section_single;
|
||||
} else if (role == SECTION_FIRST) {
|
||||
background = R.drawable.home_bg_contact_section_top;
|
||||
} else if (role == SECTION_LAST) {
|
||||
background = R.drawable.home_bg_contact_section_bottom;
|
||||
} else {
|
||||
background = R.drawable.home_bg_contact_section_middle;
|
||||
}
|
||||
mSectionCard.setBackgroundResource(background);
|
||||
mSectionCard.setClipToOutline(true);
|
||||
if (mSectionDivider != null) {
|
||||
mSectionDivider.setVisibility(
|
||||
role == SECTION_FIRST || role == SECTION_MIDDLE ? VISIBLE : GONE);
|
||||
}
|
||||
if (mSectionGapTop != null) {
|
||||
mSectionGapTop.setVisibility(
|
||||
role == SECTION_FIRST || role == SECTION_SINGLE ? VISIBLE : GONE);
|
||||
}
|
||||
if (mSectionGapBottom != null) {
|
||||
mSectionGapBottom.setVisibility(
|
||||
role == SECTION_LAST || role == SECTION_SINGLE ? VISIBLE : GONE);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -129,7 +194,8 @@ public class ContactListItemView extends LinearLayout implements OnClickListener
|
||||
mContactIconView.setImageResourceUri(avatarUri, mData.getContactId(),
|
||||
mData.getLookupKey(), destinationString);
|
||||
mContactIconView.setVisibility(VISIBLE);
|
||||
mContactCheckmarkView.setVisibility(GONE);
|
||||
applySelectionCheck(isContactChecked());
|
||||
bindAvatarClickTargets();
|
||||
mContactDetailTypeTextView.setVisibility(GONE);
|
||||
mContactDetailsTextView.setVisibility(GONE);
|
||||
mContactNameTextView.setVisibility(VISIBLE);
|
||||
@@ -142,7 +208,8 @@ public class ContactListItemView extends LinearLayout implements OnClickListener
|
||||
mContactNameTextView.setVisibility(VISIBLE);
|
||||
final boolean isSelected = mHostInterface.isContactSelected(mData);
|
||||
setSelected(isSelected);
|
||||
mContactCheckmarkView.setVisibility(isSelected ? VISIBLE : GONE);
|
||||
applySelectionCheck(isSelected);
|
||||
bindAvatarClickTargets();
|
||||
mContactDetailsTextView.setVisibility(VISIBLE);
|
||||
mContactDetailTypeTextView.setVisibility(VISIBLE);
|
||||
} else {
|
||||
@@ -151,7 +218,8 @@ public class ContactListItemView extends LinearLayout implements OnClickListener
|
||||
mContactNameTextView.setVisibility(GONE);
|
||||
final boolean isSelected = mHostInterface.isContactSelected(mData);
|
||||
setSelected(isSelected);
|
||||
mContactCheckmarkView.setVisibility(isSelected ? VISIBLE : GONE);
|
||||
applySelectionCheck(isSelected);
|
||||
bindAvatarClickTargets();
|
||||
mContactDetailsTextView.setVisibility(VISIBLE);
|
||||
mContactDetailTypeTextView.setVisibility(VISIBLE);
|
||||
}
|
||||
@@ -160,14 +228,44 @@ public class ContactListItemView extends LinearLayout implements OnClickListener
|
||||
} else {
|
||||
mWorkProfileIcon.setVisibility(GONE);
|
||||
}
|
||||
if (mShouldShowAlphabetHeader) {
|
||||
final String headerLabel = mData.getAlphabetHeader();
|
||||
if (mShouldShowAlphabetHeader && !TextUtils.isEmpty(headerLabel)
|
||||
&& !TextUtils.equals(headerLabel, " ")) {
|
||||
mAlphabetHeaderTextView.setVisibility(VISIBLE);
|
||||
mAlphabetHeaderTextView.setText(mData.getAlphabetHeader());
|
||||
mAlphabetHeaderTextView.setText(headerLabel);
|
||||
} else {
|
||||
mAlphabetHeaderTextView.setVisibility(GONE);
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isContactChecked() {
|
||||
return mHostInterface != null && mHostInterface.isContactSelected(mData);
|
||||
}
|
||||
|
||||
private void applySelectionCheck(final boolean selected) {
|
||||
HomeAvatarOverlay.apply(mContactCheckmarkView, isSelectionMode(), selected);
|
||||
}
|
||||
|
||||
private boolean isSelectionMode() {
|
||||
return mHostInterface != null && mHostInterface.isContactSelectionMode();
|
||||
}
|
||||
|
||||
private void bindAvatarClickTargets() {
|
||||
if (mContactIconView == null) {
|
||||
return;
|
||||
}
|
||||
mContactIconView.setImageClickHandlerDisabled(true);
|
||||
final boolean selectionMode = isSelectionMode();
|
||||
final OnClickListener selectRow = v -> performClick();
|
||||
if (mContactCheckmarkView != null) {
|
||||
mContactCheckmarkView.setOnClickListener(selectionMode ? selectRow : null);
|
||||
mContactCheckmarkView.setClickable(selectionMode);
|
||||
mContactCheckmarkView.setFocusable(false);
|
||||
}
|
||||
mContactIconView.setOnClickListener(selectionMode ? selectRow : null);
|
||||
mContactIconView.setClickable(selectionMode);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc} from OnClickListener
|
||||
*/
|
||||
|
||||
@@ -291,6 +291,13 @@ public class ContactPickerFragment extends Fragment implements ContactPickerData
|
||||
item.getRecipientEntry().getDestination()));
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isContactSelectionMode() {
|
||||
return mContactPickingMode == MODE_PICK_INITIAL_CONTACT
|
||||
|| mContactPickingMode == MODE_PICK_MORE_CONTACTS
|
||||
|| mContactPickingMode == MODE_PICK_MAX_PARTICIPANTS;
|
||||
}
|
||||
|
||||
/**
|
||||
* Call this immediately after attaching the fragment, or when there's a ui state change that
|
||||
* changes our host (i.e. restore from saved instance state).
|
||||
@@ -557,7 +564,7 @@ public class ContactPickerFragment extends Fragment implements ContactPickerData
|
||||
// etc. will take the spot of the action bar.
|
||||
actionBar.hide();
|
||||
UiUtils.setStatusBarColor(getActivity(),
|
||||
getResources().getColor(R.color.looply_bg, getActivity().getTheme()));
|
||||
getResources().getColor(R.color.home_bg, getActivity().getTheme()));
|
||||
}
|
||||
|
||||
private GetOrCreateConversationActionMonitor mMonitor;
|
||||
|
||||
@@ -118,7 +118,7 @@ public class ContactSectionIndexer implements SectionIndexer {
|
||||
if (TextUtils.isEmpty(mSections[i])) {
|
||||
mSections[i] = BLANK_HEADER_STRING;
|
||||
} else if (!mSections[i].equals(BLANK_HEADER_STRING)) {
|
||||
mSections[i] = mSections[i].trim();
|
||||
mSections[i] = normalizeProviderSection(mSections[i].trim());
|
||||
}
|
||||
|
||||
mSectionStartingPositions.add(position);
|
||||
@@ -141,9 +141,11 @@ public class ContactSectionIndexer implements SectionIndexer {
|
||||
// The sort key is typically the contact's display name, so for example, a contact
|
||||
// named "Bob" will go into section "B". The Contacts provider generally uses a
|
||||
// a slightly more sophisticated heuristic, but as a fallback this is good enough.
|
||||
final String sortKey = cursor.getString(ContactUtil.INDEX_SORT_KEY);
|
||||
final String section = TextUtils.isEmpty(sortKey) ? BLANK_HEADER_STRING :
|
||||
sortKey.substring(0, 1).toUpperCase();
|
||||
String sortKey = cursor.getString(ContactUtil.INDEX_SORT_KEY);
|
||||
if (TextUtils.isEmpty(sortKey)) {
|
||||
sortKey = cursor.getString(ContactUtil.INDEX_DISPLAY_NAME);
|
||||
}
|
||||
final String section = sectionLabelFor(sortKey);
|
||||
|
||||
final int lastIndex = sections.size() - 1;
|
||||
final String currentSection = lastIndex >= 0 ? sections.get(lastIndex) : null;
|
||||
@@ -158,6 +160,94 @@ public class ContactSectionIndexer implements SectionIndexer {
|
||||
sections.toArray(mSections);
|
||||
}
|
||||
|
||||
/** Maps a Hangul syllable section from the provider to its choseong; leaves other labels. */
|
||||
private static String normalizeProviderSection(final String title) {
|
||||
if (title.codePointCount(0, title.length()) != 1) {
|
||||
return title;
|
||||
}
|
||||
final int codePoint = title.codePointAt(0);
|
||||
if (codePoint >= 0xAC00 && codePoint <= 0xD7A3) {
|
||||
return sectionLabelFor(title);
|
||||
}
|
||||
return title;
|
||||
}
|
||||
|
||||
/**
|
||||
* Locale-aware bucket for a sort key: Latin A–Z, Hangul choseong (ㄱ…ㅎ), Japanese
|
||||
* gojuon rows, other letters uppercased, and # for digits or symbols.
|
||||
*/
|
||||
static String sectionLabelFor(final String sortKey) {
|
||||
if (TextUtils.isEmpty(sortKey)) {
|
||||
return BLANK_HEADER_STRING;
|
||||
}
|
||||
int index = 0;
|
||||
while (index < sortKey.length() && Character.isWhitespace(sortKey.charAt(index))) {
|
||||
index++;
|
||||
}
|
||||
if (index >= sortKey.length()) {
|
||||
return BLANK_HEADER_STRING;
|
||||
}
|
||||
final int codePoint = sortKey.codePointAt(index);
|
||||
if (codePoint >= 0xAC00 && codePoint <= 0xD7A3) {
|
||||
final int choseongIndex = (codePoint - 0xAC00) / (21 * 28);
|
||||
return String.valueOf(HANGUL_CHOSEONG[choseongIndex]);
|
||||
}
|
||||
if ((codePoint >= 0x3131 && codePoint <= 0x314E)
|
||||
|| (codePoint >= 0x1100 && codePoint <= 0x1112)) {
|
||||
return new String(Character.toChars(codePoint));
|
||||
}
|
||||
final int kana = toHiragana(codePoint);
|
||||
if (kana >= 0x3041 && kana <= 0x3096) {
|
||||
return String.valueOf(japaneseRow(kana));
|
||||
}
|
||||
if (Character.isDigit(codePoint) || isCjkIdeograph(codePoint)
|
||||
|| !Character.isLetter(codePoint)) {
|
||||
return "#";
|
||||
}
|
||||
return new String(Character.toChars(Character.toUpperCase(codePoint)));
|
||||
}
|
||||
|
||||
private static final char[] HANGUL_CHOSEONG = {
|
||||
'ㄱ', 'ㄲ', 'ㄴ', 'ㄷ', 'ㄸ', 'ㄹ', 'ㅁ', 'ㅂ', 'ㅃ', 'ㅅ',
|
||||
'ㅆ', 'ㅇ', 'ㅈ', 'ㅉ', 'ㅊ', 'ㅋ', 'ㅌ', 'ㅍ', 'ㅎ'
|
||||
};
|
||||
|
||||
private static int toHiragana(final int codePoint) {
|
||||
if (codePoint >= 0x30A1 && codePoint <= 0x30F6) {
|
||||
return codePoint - 0x60;
|
||||
}
|
||||
return codePoint;
|
||||
}
|
||||
|
||||
private static char japaneseRow(final int hiragana) {
|
||||
if (hiragana <= 0x304A) {
|
||||
return 'あ';
|
||||
} else if (hiragana <= 0x3054) {
|
||||
return 'か';
|
||||
} else if (hiragana <= 0x305E) {
|
||||
return 'さ';
|
||||
} else if (hiragana <= 0x3069) {
|
||||
return 'た';
|
||||
} else if (hiragana <= 0x306E) {
|
||||
return 'な';
|
||||
} else if (hiragana <= 0x307D) {
|
||||
return 'は';
|
||||
} else if (hiragana <= 0x3082) {
|
||||
return 'ま';
|
||||
} else if (hiragana <= 0x3088) {
|
||||
return 'や';
|
||||
} else if (hiragana <= 0x308D) {
|
||||
return 'ら';
|
||||
}
|
||||
return 'わ';
|
||||
}
|
||||
|
||||
private static boolean isCjkIdeograph(final int codePoint) {
|
||||
return (codePoint >= 0x4E00 && codePoint <= 0x9FFF)
|
||||
|| (codePoint >= 0x3400 && codePoint <= 0x4DBF)
|
||||
|| (codePoint >= 0x20000 && codePoint <= 0x2A6DF);
|
||||
}
|
||||
|
||||
private void buildIndexer(final Cursor cursor) {
|
||||
// First check if we get indexer label extras from the contact provider; if not, fall back
|
||||
// to building from display names.
|
||||
|
||||
@@ -16,7 +16,9 @@
|
||||
*/
|
||||
package com.android.messaging.ui.conversation;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.res.Resources;
|
||||
import android.graphics.Rect;
|
||||
import android.net.Uri;
|
||||
@@ -34,18 +36,25 @@ import android.text.format.Formatter;
|
||||
import android.util.AttributeSet;
|
||||
import android.view.ContextThemeWrapper;
|
||||
import android.view.KeyEvent;
|
||||
import android.view.MotionEvent;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.view.WindowManager;
|
||||
import android.view.accessibility.AccessibilityEvent;
|
||||
import android.view.inputmethod.EditorInfo;
|
||||
import android.view.inputmethod.InputMethodManager;
|
||||
import android.widget.ImageButton;
|
||||
import android.widget.LinearLayout;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.appcompat.app.ActionBar;
|
||||
import androidx.core.view.WindowInsetsCompat;
|
||||
import androidx.core.view.WindowInsetsControllerCompat;
|
||||
|
||||
import com.android.messaging.Factory;
|
||||
import com.android.messaging.R;
|
||||
import com.android.messaging.datamodel.ScheduledMessageStore;
|
||||
import com.android.messaging.datamodel.binding.Binding;
|
||||
import com.android.messaging.datamodel.binding.BindingBase;
|
||||
import com.android.messaging.datamodel.binding.ImmutableBindingRef;
|
||||
@@ -65,7 +74,14 @@ import com.android.messaging.ui.AttachmentPreview;
|
||||
import com.android.messaging.ui.BugleActionBarActivity;
|
||||
import com.android.messaging.ui.PlainTextEditText;
|
||||
import com.android.messaging.ui.conversation.ConversationInputManager.ConversationInputSink;
|
||||
import com.android.messaging.ui.UIIntents;
|
||||
import com.android.messaging.ui.home.HomeComposeAttachPanel;
|
||||
import com.android.messaging.ui.home.HomeSchedulePicker;
|
||||
import com.android.messaging.ui.home.HomeToast;
|
||||
import com.android.messaging.ui.home.HomeUiMotion;
|
||||
import com.android.messaging.util.AccessibilityUtil;
|
||||
import com.android.messaging.util.ImageUtils;
|
||||
import com.android.messaging.util.ImeUtil;
|
||||
import com.android.messaging.util.Assert;
|
||||
import com.android.messaging.util.AvatarUriUtil;
|
||||
import com.android.messaging.util.BuglePrefs;
|
||||
@@ -102,6 +118,10 @@ public class ComposeMessageView extends LinearLayout
|
||||
boolean tooManyVideos);
|
||||
void notifyOfAttachmentLoadFailed();
|
||||
void showAttachmentChooser();
|
||||
void launchSystemGalleryPicker();
|
||||
void requestMediaPermission(Runnable onGranted);
|
||||
void requestLocationPermission(Runnable onGranted);
|
||||
void launchLocationShare();
|
||||
boolean shouldShowSubjectEditor();
|
||||
boolean shouldHideAttachmentsWhenSimSelectorShown();
|
||||
Uri getSelfSendButtonIconUri();
|
||||
@@ -110,6 +130,7 @@ public class ComposeMessageView extends LinearLayout
|
||||
}
|
||||
|
||||
public static final int CODEPOINTS_REMAINING_BEFORE_COUNTER_SHOWN = 10;
|
||||
private static final String KEY_SCHEDULED_SEND_AT = "home_scheduled_send_at";
|
||||
|
||||
// There is no draft and there is no need for the SIM selector
|
||||
private static final int SEND_WIDGET_MODE_SELF_AVATAR = 1;
|
||||
@@ -128,6 +149,17 @@ public class ComposeMessageView extends LinearLayout
|
||||
private ImageButton mDeleteSubjectButton;
|
||||
private AttachmentPreview mAttachmentPreview;
|
||||
private ImageButton mAttachMediaButton;
|
||||
private View mAttachmentSheetContainer;
|
||||
private View mComposeBottomStack;
|
||||
private View mComposeBar;
|
||||
private boolean mAttachmentSheetOpen;
|
||||
private HomeComposeAttachPanel.Host mAttachHost;
|
||||
private int mComposeBarPaddingBottom;
|
||||
private ImageButton mVoiceButton;
|
||||
private View mSendColumn;
|
||||
private View mScheduleChipRow;
|
||||
private TextView mScheduleChipText;
|
||||
private long mScheduledSendAt;
|
||||
|
||||
private final Binding<DraftMessageData> mBinding;
|
||||
private IComposeMessageViewHost mHost;
|
||||
@@ -199,14 +231,24 @@ public class ComposeMessageView extends LinearLayout
|
||||
mComposeEditText.setOnEditorActionListener(this);
|
||||
mComposeEditText.addTextChangedListener(this);
|
||||
mComposeEditText.setOnFocusChangeListener((v, hasFocus) -> {
|
||||
if (mAttachMediaButton != null) {
|
||||
mAttachMediaButton.setSelected(hasFocus);
|
||||
}
|
||||
if (v == mComposeEditText && hasFocus) {
|
||||
mHost.onComposeEditTextFocused();
|
||||
if (mHost != null) {
|
||||
mHost.onComposeEditTextFocused();
|
||||
}
|
||||
showComposeKeyboard();
|
||||
}
|
||||
});
|
||||
mComposeEditText.setOnClickListener(arg0 -> {
|
||||
if (mHost.shouldHideAttachmentsWhenSimSelectorShown()) {
|
||||
hideSimSelector();
|
||||
mComposeEditText.setOnTouchListener((v, event) -> {
|
||||
if (event.getActionMasked() == MotionEvent.ACTION_DOWN) {
|
||||
if (mHost != null && mHost.shouldHideAttachmentsWhenSimSelectorShown()) {
|
||||
hideSimSelector();
|
||||
}
|
||||
showComposeKeyboard();
|
||||
}
|
||||
return false;
|
||||
});
|
||||
|
||||
// onFinishInflate() is called before self is loaded from db. We set the default text
|
||||
@@ -283,10 +325,53 @@ public class ComposeMessageView extends LinearLayout
|
||||
});
|
||||
|
||||
mAttachMediaButton = findViewById(R.id.attach_media_button);
|
||||
mAttachMediaButton.setOnClickListener(clickView -> {
|
||||
// Showing the media picker is treated as starting to compose the message.
|
||||
mInputManager.showHideMediaPicker(true /* show */, true /* animate */);
|
||||
});
|
||||
mAttachmentSheetContainer = findViewById(R.id.home_compose_attachment_sheet_container);
|
||||
mComposeBottomStack = findViewById(R.id.home_compose_bottom_stack);
|
||||
mComposeBar = findViewById(R.id.home_conversation_compose_bar);
|
||||
if (mComposeBar != null) {
|
||||
mComposeBarPaddingBottom = mComposeBar.getPaddingBottom();
|
||||
}
|
||||
mAttachHost = createComposeAttachHost();
|
||||
mScheduleChipRow = findViewById(R.id.home_schedule_chip_row);
|
||||
mScheduleChipText = findViewById(R.id.home_schedule_chip_text);
|
||||
final View scheduleChip = findViewById(R.id.home_schedule_chip);
|
||||
if (scheduleChip != null) {
|
||||
scheduleChip.setOnClickListener(v -> showSchedulePicker());
|
||||
}
|
||||
final View scheduleClear = findViewById(R.id.home_schedule_chip_clear);
|
||||
if (scheduleClear != null) {
|
||||
scheduleClear.setOnClickListener(v -> setScheduledSendAt(0L));
|
||||
}
|
||||
mAttachMediaButton.setOnClickListener(clickView -> toggleComposeAttachmentSheet());
|
||||
mAttachMediaButton.setSelected(mComposeEditText.hasFocus());
|
||||
|
||||
final ImageButton emojiButton = findViewById(R.id.home_compose_emoji_button);
|
||||
if (emojiButton != null) {
|
||||
emojiButton.setOnClickListener(v -> showComposeKeyboard());
|
||||
}
|
||||
final ImageButton galleryButton = findViewById(R.id.home_compose_gallery_button);
|
||||
if (galleryButton != null) {
|
||||
galleryButton.setOnClickListener(v -> {
|
||||
if (!mAttachmentSheetOpen) {
|
||||
toggleComposeAttachmentSheet();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
mVoiceButton = findViewById(R.id.home_conversation_voice_button);
|
||||
mSendColumn = findViewById(R.id.home_conversation_send_column);
|
||||
if (mVoiceButton != null) {
|
||||
mVoiceButton.setOnClickListener(v -> {
|
||||
final String messageText = mComposeEditText.getText().toString();
|
||||
final boolean hasDraft = TextUtils.getTrimmedLength(messageText) > 0
|
||||
|| (mBinding.isBound() && mBinding.getData().hasAttachments());
|
||||
if (hasDraft) {
|
||||
sendMessageInternal(true /* checkMessageSize */);
|
||||
} else {
|
||||
HomeToast.show(mOriginalContext, R.string.home_conversation_voice_coming_soon);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
mAttachmentPreview = findViewById(R.id.attachment_draft_view);
|
||||
mAttachmentPreview.setComposeMessageView(this);
|
||||
@@ -368,6 +453,9 @@ public class ComposeMessageView extends LinearLayout
|
||||
mBinding.ensureBound(data);
|
||||
switch (result) {
|
||||
case CheckDraftForSendTask.RESULT_PASSED:
|
||||
if (queueScheduledMessageIfNeeded()) {
|
||||
break;
|
||||
}
|
||||
// Continue sending after check succeeded.
|
||||
final MessageData message = mBinding.getData()
|
||||
.prepareMessageForSending(mBinding);
|
||||
@@ -537,11 +625,39 @@ public class ComposeMessageView extends LinearLayout
|
||||
|
||||
@Override
|
||||
public void resumeComposeMessage() {
|
||||
mComposeEditText.requestFocus();
|
||||
mInputManager.showHideImeKeyboard(true, true);
|
||||
showComposeKeyboard();
|
||||
announceAttachmentState();
|
||||
}
|
||||
|
||||
private void showComposeKeyboard() {
|
||||
if (mComposeEditText == null) {
|
||||
return;
|
||||
}
|
||||
mComposeEditText.setEnabled(true);
|
||||
mComposeEditText.setFocusable(true);
|
||||
mComposeEditText.setFocusableInTouchMode(true);
|
||||
mComposeEditText.setShowSoftInputOnFocus(true);
|
||||
mComposeEditText.requestFocus();
|
||||
final Activity activity = resolveActivity();
|
||||
if (activity != null && activity.getWindow() != null) {
|
||||
activity.getWindow().setSoftInputMode(
|
||||
WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE
|
||||
| WindowManager.LayoutParams.SOFT_INPUT_STATE_VISIBLE);
|
||||
new WindowInsetsControllerCompat(activity.getWindow(), mComposeEditText)
|
||||
.show(WindowInsetsCompat.Type.ime());
|
||||
final InputMethodManager imm = activity.getSystemService(InputMethodManager.class);
|
||||
if (imm != null) {
|
||||
imm.restartInput(mComposeEditText);
|
||||
imm.showSoftInput(mComposeEditText, InputMethodManager.SHOW_FORCED);
|
||||
}
|
||||
}
|
||||
final Context imeContext = activity != null ? activity : mOriginalContext;
|
||||
ImeUtil.get().showImeKeyboard(imeContext, mComposeEditText);
|
||||
if (mInputManager != null) {
|
||||
mInputManager.showHideImeKeyboard(true, false);
|
||||
}
|
||||
}
|
||||
|
||||
public void clearAttachments() {
|
||||
mBinding.getData().clearAttachments(mHost.getAttachmentsClearedFlags());
|
||||
mHost.onAttachmentsCleared();
|
||||
@@ -666,7 +782,7 @@ public class ComposeMessageView extends LinearLayout
|
||||
mMessageBodySize.setVisibility(View.VISIBLE);
|
||||
}
|
||||
} else {
|
||||
mMessageBodySize.setVisibility(View.INVISIBLE);
|
||||
mMessageBodySize.setVisibility(View.GONE);
|
||||
}
|
||||
} else { // SMS case
|
||||
// Update the SMS text counter.
|
||||
@@ -685,7 +801,7 @@ public class ComposeMessageView extends LinearLayout
|
||||
mMessageBodySize.setText(counterText);
|
||||
mMessageBodySize.setVisibility(View.VISIBLE);
|
||||
} else {
|
||||
mMessageBodySize.setVisibility(View.INVISIBLE);
|
||||
mMessageBodySize.setVisibility(View.GONE);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -695,35 +811,31 @@ public class ComposeMessageView extends LinearLayout
|
||||
int sendWidgetMode = SEND_WIDGET_MODE_SELF_AVATAR;
|
||||
if (selfSendButtonUri != null) {
|
||||
if (hasWorkingDraft && isDataLoadedForMessageSend()) {
|
||||
UiUtils.revealOrHideViewWithAnimation(mSendButton, VISIBLE, null);
|
||||
if (isOverriddenAvatarAGroup()) {
|
||||
// If the host has overriden the avatar to show a group avatar where the
|
||||
// send button sits, we have to hide the group avatar because it can be larger
|
||||
// than the send button and pieces of the avatar will stick out from behind
|
||||
// the send button.
|
||||
UiUtils.revealOrHideViewWithAnimation(mSelfSendIcon, GONE, null);
|
||||
}
|
||||
hideSelfSendIcon();
|
||||
mMmsIndicator.setVisibility(draftMessageData.getIsMms() ? VISIBLE : INVISIBLE);
|
||||
sendWidgetMode = SEND_WIDGET_MODE_SEND_BUTTON;
|
||||
} else {
|
||||
mSelfSendIcon.setImageResourceUri(selfSendButtonUri);
|
||||
if (isOverriddenAvatarAGroup()) {
|
||||
UiUtils.revealOrHideViewWithAnimation(mSelfSendIcon, VISIBLE, null);
|
||||
}
|
||||
UiUtils.revealOrHideViewWithAnimation(mSendButton, GONE, null);
|
||||
hideSelfSendIcon();
|
||||
mMmsIndicator.setVisibility(INVISIBLE);
|
||||
if (shouldShowSimSelector(mConversationDataModel.getData())) {
|
||||
sendWidgetMode = SEND_WIDGET_MODE_SIM_SELECTOR;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
mSelfSendIcon.setImageResourceUri(null);
|
||||
hideSelfSendIcon();
|
||||
if (hasWorkingDraft && isDataLoadedForMessageSend()) {
|
||||
mMmsIndicator.setVisibility(draftMessageData.getIsMms() ? VISIBLE : INVISIBLE);
|
||||
sendWidgetMode = SEND_WIDGET_MODE_SEND_BUTTON;
|
||||
} else {
|
||||
mMmsIndicator.setVisibility(INVISIBLE);
|
||||
}
|
||||
}
|
||||
|
||||
if (mSendWidgetMode != sendWidgetMode || sendWidgetMode == SEND_WIDGET_MODE_SIM_SELECTOR) {
|
||||
setSendButtonAccessibility(sendWidgetMode);
|
||||
mSendWidgetMode = sendWidgetMode;
|
||||
}
|
||||
updateVoiceAndSendColumnVisibility(sendWidgetMode);
|
||||
|
||||
// Update the text hint on the message box depending on the attachment type.
|
||||
final int attachmentCount = attachments.size();
|
||||
@@ -795,6 +907,30 @@ public class ComposeMessageView extends LinearLayout
|
||||
}
|
||||
}
|
||||
|
||||
private void updateVoiceAndSendColumnVisibility(final int sendWidgetMode) {
|
||||
if (mVoiceButton == null || mSendColumn == null) {
|
||||
return;
|
||||
}
|
||||
final boolean showSend = sendWidgetMode == SEND_WIDGET_MODE_SEND_BUTTON;
|
||||
if (showSend) {
|
||||
mSendColumn.setVisibility(VISIBLE);
|
||||
UiUtils.revealOrHideViewWithAnimation(mSendButton, VISIBLE, null);
|
||||
UiUtils.revealOrHideViewWithAnimation(mVoiceButton, GONE, null);
|
||||
return;
|
||||
}
|
||||
UiUtils.revealOrHideViewWithAnimation(mVoiceButton, VISIBLE, null);
|
||||
if (mSendColumn.getVisibility() == VISIBLE) {
|
||||
UiUtils.revealOrHideViewWithAnimation(mSendButton, GONE, () -> {
|
||||
if (mSendWidgetMode != SEND_WIDGET_MODE_SEND_BUTTON && mSendColumn != null) {
|
||||
mSendColumn.setVisibility(GONE);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
mSendButton.setVisibility(GONE);
|
||||
mSendColumn.setVisibility(GONE);
|
||||
}
|
||||
}
|
||||
|
||||
private void setSendButtonAccessibility(final int sendWidgetMode) {
|
||||
switch (sendWidgetMode) {
|
||||
case SEND_WIDGET_MODE_SELF_AVATAR:
|
||||
@@ -802,7 +938,6 @@ public class ComposeMessageView extends LinearLayout
|
||||
// important for accessibility.
|
||||
mSelfSendIcon.setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_NO);
|
||||
mSelfSendIcon.setContentDescription(null);
|
||||
mSendButton.setVisibility(View.GONE);
|
||||
setSendWidgetAccessibilityTraversalOrder(SEND_WIDGET_MODE_SELF_AVATAR);
|
||||
break;
|
||||
|
||||
@@ -920,18 +1055,56 @@ public class ComposeMessageView extends LinearLayout
|
||||
}
|
||||
|
||||
public void hideAllComposeInputs(final boolean animate) {
|
||||
if (mAttachmentSheetOpen) {
|
||||
closeComposeAttachmentSheet(animate, null);
|
||||
}
|
||||
mInputManager.hideAllInputs(animate);
|
||||
}
|
||||
|
||||
public void saveInputState(final Bundle outState) {
|
||||
mInputManager.onSaveInputState(outState);
|
||||
outState.putLong(KEY_SCHEDULED_SEND_AT, mScheduledSendAt);
|
||||
}
|
||||
|
||||
public void restoreScheduledSendAt(final Bundle state) {
|
||||
if (state == null) {
|
||||
return;
|
||||
}
|
||||
setScheduledSendAt(state.getLong(KEY_SCHEDULED_SEND_AT, 0L));
|
||||
}
|
||||
|
||||
public void applySharedLocation(final Intent data) {
|
||||
if (data == null) {
|
||||
return;
|
||||
}
|
||||
final String text = data.getStringExtra(UIIntents.UI_INTENT_EXTRA_LOCATION_TEXT);
|
||||
if (!TextUtils.isEmpty(text)) {
|
||||
mComposeEditText.setText(text);
|
||||
mComposeEditText.setSelection(text.length());
|
||||
if (mBinding.isBound()) {
|
||||
mBinding.getData().setMessageText(text);
|
||||
}
|
||||
}
|
||||
final Uri image = data.getParcelableExtra(
|
||||
UIIntents.UI_INTENT_EXTRA_LOCATION_IMAGE_URI, Uri.class);
|
||||
if (image != null) {
|
||||
onPendingAttachmentAdded(PendingAttachmentData.createPendingAttachmentData(
|
||||
"image/jpeg", image));
|
||||
}
|
||||
resumeComposeMessage();
|
||||
}
|
||||
|
||||
public void resetMediaPickerState() {
|
||||
if (mAttachmentSheetOpen) {
|
||||
closeComposeAttachmentSheet(false /* animate */, null);
|
||||
}
|
||||
mInputManager.resetMediaPickerState();
|
||||
}
|
||||
|
||||
public boolean onBackPressed() {
|
||||
if (HomeComposeAttachPanel.isShowing(resolveActivity())) {
|
||||
return HomeComposeAttachPanel.onBackPressed(resolveActivity());
|
||||
}
|
||||
return mInputManager.onBackPressed();
|
||||
}
|
||||
|
||||
@@ -960,12 +1133,13 @@ public class ComposeMessageView extends LinearLayout
|
||||
mHost.notifyOfAttachmentLoadFailed();
|
||||
}
|
||||
|
||||
private boolean isOverriddenAvatarAGroup() {
|
||||
final Uri overridenSelfUri = mHost.getSelfSendButtonIconUri();
|
||||
if (overridenSelfUri == null) {
|
||||
return false;
|
||||
private void hideSelfSendIcon() {
|
||||
if (mSelfSendIcon == null) {
|
||||
return;
|
||||
}
|
||||
return AvatarUriUtil.TYPE_GROUP_URI.equals(AvatarUriUtil.getAvatarType(overridenSelfUri));
|
||||
mSelfSendIcon.animate().cancel();
|
||||
mSelfSendIcon.setVisibility(GONE);
|
||||
mSelfSendIcon.setImageResourceUri(null);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -982,4 +1156,264 @@ public class ComposeMessageView extends LinearLayout
|
||||
mAttachMediaButton.setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_NO);
|
||||
}
|
||||
}
|
||||
|
||||
private HomeComposeAttachPanel.Host createComposeAttachHost() {
|
||||
return new HomeComposeAttachPanel.Host() {
|
||||
@Override
|
||||
public void onMediaPicked(@NonNull final List<Uri> uris,
|
||||
@Nullable final String caption) {
|
||||
if (!TextUtils.isEmpty(caption)) {
|
||||
mComposeEditText.setText(caption);
|
||||
mComposeEditText.setSelection(caption.length());
|
||||
if (mBinding.isBound()) {
|
||||
mBinding.getData().setMessageText(caption);
|
||||
}
|
||||
}
|
||||
attachPickedUris(uris);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onTakePhoto() {
|
||||
openMediaFromAttachmentSheet(ConversationInputManager.MEDIA_PICKER_TARGET_PHOTO);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRecordVideo() {
|
||||
openMediaFromAttachmentSheet(ConversationInputManager.MEDIA_PICKER_TARGET_VIDEO);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onOpenContact() {
|
||||
openMediaFromAttachmentSheet(ConversationInputManager.MEDIA_PICKER_TARGET_CONTACT);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onShareLocation() {
|
||||
if (mHost != null) {
|
||||
mHost.requestLocationPermission(() -> mHost.launchLocationShare());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onScheduleMessage() {
|
||||
showSchedulePicker();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRequestMediaPermission() {
|
||||
if (mHost != null) {
|
||||
mHost.requestMediaPermission(() ->
|
||||
HomeComposeAttachPanel.reloadIfShowing(resolveActivity()));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onComingSoon() {
|
||||
HomeToast.show(mOriginalContext, R.string.home_popover_coming_soon);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDismiss() {
|
||||
mAttachmentSheetOpen = false;
|
||||
updateAttachButtonContentDescription();
|
||||
updateComposeBottomStackBackground();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAttachSheetExtentChanged(final int overlapPx) {
|
||||
applyAttachSheetChatPush(overlapPx);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRequestComposeKeyboard() {
|
||||
showComposeKeyboard();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private void attachPickedUris(final List<Uri> uris) {
|
||||
if (uris == null || uris.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
final ExecutorService executor = Executors.newSingleThreadExecutor();
|
||||
final Handler handler = new Handler(Looper.getMainLooper());
|
||||
executor.execute(() -> {
|
||||
final List<PendingAttachmentData> pending = new ArrayList<>();
|
||||
for (final Uri uri : uris) {
|
||||
final String contentType = ImageUtils.getContentType(
|
||||
Factory.get().getApplicationContext().getContentResolver(), uri);
|
||||
if (contentType != null) {
|
||||
pending.add(PendingAttachmentData.createPendingAttachmentData(contentType, uri));
|
||||
}
|
||||
}
|
||||
handler.post(() -> {
|
||||
for (final PendingAttachmentData item : pending) {
|
||||
onPendingAttachmentAdded(item);
|
||||
}
|
||||
executor.shutdown();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private void applyAttachSheetChatPush(final int overlapPx) {
|
||||
final Activity activity = resolveActivity();
|
||||
if (activity == null) {
|
||||
return;
|
||||
}
|
||||
final View chatArea = activity.findViewById(R.id.conversation_chat_area);
|
||||
if (chatArea == null) {
|
||||
return;
|
||||
}
|
||||
final int inset = Math.max(0, overlapPx);
|
||||
if (chatArea.getPaddingBottom() != inset) {
|
||||
chatArea.setPaddingRelative(
|
||||
chatArea.getPaddingStart(),
|
||||
chatArea.getPaddingTop(),
|
||||
chatArea.getPaddingEnd(),
|
||||
inset);
|
||||
}
|
||||
}
|
||||
|
||||
private void toggleComposeAttachmentSheet() {
|
||||
if (mAttachmentSheetOpen) {
|
||||
closeComposeAttachmentSheet(true /* animate */, null);
|
||||
return;
|
||||
}
|
||||
final Activity activity = resolveActivity();
|
||||
if (activity == null || mAttachHost == null) {
|
||||
return;
|
||||
}
|
||||
if (mInputManager != null) {
|
||||
mInputManager.showHideImeKeyboard(false, true);
|
||||
mInputManager.showHideMediaPicker(false, false);
|
||||
}
|
||||
mAttachmentSheetOpen = true;
|
||||
updateAttachButtonContentDescription();
|
||||
updateComposeBottomStackBackground();
|
||||
HomeComposeAttachPanel.show(activity, mAttachHost);
|
||||
}
|
||||
|
||||
private void closeComposeAttachmentSheet(final boolean animate, final Runnable endAction) {
|
||||
final boolean dismissed = HomeComposeAttachPanel.dismissIfShowing(resolveActivity());
|
||||
if (!dismissed) {
|
||||
applyAttachSheetChatPush(0);
|
||||
}
|
||||
mAttachmentSheetOpen = false;
|
||||
updateAttachButtonContentDescription();
|
||||
updateComposeBottomStackBackground();
|
||||
if (endAction != null) {
|
||||
endAction.run();
|
||||
}
|
||||
if (!dismissed && endAction == null) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
private boolean queueScheduledMessageIfNeeded() {
|
||||
if (mScheduledSendAt <= System.currentTimeMillis()) {
|
||||
if (mScheduledSendAt > 0L) {
|
||||
setScheduledSendAt(0L);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
final DraftMessageData draft = mBinding.getData();
|
||||
final boolean empty = TextUtils.isEmpty(draft.getMessageText())
|
||||
&& TextUtils.isEmpty(draft.getMessageSubject())
|
||||
&& draft.getReadOnlyAttachments().isEmpty();
|
||||
if (empty) {
|
||||
HomeToast.show(getContext(), R.string.home_schedule_need_message);
|
||||
return true;
|
||||
}
|
||||
final MessageData message = draft.prepareMessageForSending(mBinding);
|
||||
if (message == null || !message.hasContent()) {
|
||||
HomeToast.show(getContext(), R.string.home_schedule_need_message);
|
||||
return true;
|
||||
}
|
||||
ScheduledMessageStore.schedule(getContext(), message, mScheduledSendAt);
|
||||
hideSubjectEditor();
|
||||
HomeToast.show(getContext(), getResources().getString(
|
||||
R.string.home_schedule_queued, HomeSchedulePicker.formatChip(
|
||||
getContext(), mScheduledSendAt)));
|
||||
setScheduledSendAt(0L);
|
||||
return true;
|
||||
}
|
||||
|
||||
private void showSchedulePicker() {
|
||||
final Activity activity = resolveActivity();
|
||||
if (activity == null) {
|
||||
return;
|
||||
}
|
||||
HomeSchedulePicker.show(activity, mScheduledSendAt, this::setScheduledSendAt);
|
||||
}
|
||||
|
||||
private void setScheduledSendAt(final long sendAt) {
|
||||
mScheduledSendAt = sendAt > System.currentTimeMillis() ? sendAt : 0L;
|
||||
if (mScheduleChipRow == null) {
|
||||
return;
|
||||
}
|
||||
if (mScheduledSendAt <= 0L) {
|
||||
mScheduleChipRow.setVisibility(GONE);
|
||||
return;
|
||||
}
|
||||
if (mScheduleChipText != null) {
|
||||
mScheduleChipText.setText(HomeSchedulePicker.formatChip(getContext(), mScheduledSendAt));
|
||||
}
|
||||
mScheduleChipRow.setVisibility(VISIBLE);
|
||||
}
|
||||
|
||||
private Activity resolveActivity() {
|
||||
if (mOriginalContext instanceof Activity) {
|
||||
return (Activity) mOriginalContext;
|
||||
}
|
||||
final Context context = getContext();
|
||||
if (context instanceof Activity) {
|
||||
return (Activity) context;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private void updateComposeBottomStackBackground() {
|
||||
if (mComposeBottomStack == null) {
|
||||
return;
|
||||
}
|
||||
mComposeBottomStack.setClipToOutline(false);
|
||||
mComposeBottomStack.setBackgroundColor(android.graphics.Color.TRANSPARENT);
|
||||
if (mComposeBar != null) {
|
||||
mComposeBar.setVisibility(View.VISIBLE);
|
||||
mComposeBar.setBackgroundColor(android.graphics.Color.TRANSPARENT);
|
||||
}
|
||||
}
|
||||
|
||||
private void openMediaFromAttachmentSheet(final int mediaPickerTarget) {
|
||||
if (mediaPickerTarget == ConversationInputManager.MEDIA_PICKER_TARGET_GALLERY) {
|
||||
closeComposeAttachmentSheet(false /* animate */, null);
|
||||
mHost.launchSystemGalleryPicker();
|
||||
return;
|
||||
}
|
||||
if (mediaPickerTarget == ConversationInputManager.MEDIA_PICKER_TARGET_CONTACT
|
||||
|| mediaPickerTarget == ConversationInputManager.MEDIA_PICKER_TARGET_PHOTO
|
||||
|| mediaPickerTarget == ConversationInputManager.MEDIA_PICKER_TARGET_VIDEO) {
|
||||
if (mInputManager != null) {
|
||||
mInputManager.showHideMediaPicker(true /* show */, false /* animate */,
|
||||
mediaPickerTarget);
|
||||
}
|
||||
closeComposeAttachmentSheet(false /* animate */, null);
|
||||
return;
|
||||
}
|
||||
closeComposeAttachmentSheet(true /* animate */, () -> {
|
||||
if (mInputManager != null) {
|
||||
mInputManager.showHideMediaPicker(true /* show */, true /* animate */,
|
||||
mediaPickerTarget);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void updateAttachButtonContentDescription() {
|
||||
if (mAttachMediaButton == null) {
|
||||
return;
|
||||
}
|
||||
mAttachMediaButton.setContentDescription(getResources().getText(mAttachmentSheetOpen
|
||||
? R.string.home_sub_header_close
|
||||
: R.string.attachMediaButtonContentDescription));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -61,6 +61,10 @@ public class ConversationActivity extends BugleActionBarActivity
|
||||
// Tracks whether onPause is called.
|
||||
private boolean mIsPaused;
|
||||
|
||||
// Photo Picker is a system Activity; skip its close animation so a black window
|
||||
// does not slide down over the conversation when the handle is dismissed.
|
||||
private boolean mSuppressNextResumeTransition;
|
||||
|
||||
@Override
|
||||
protected void onCreate(final Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
@@ -146,9 +150,28 @@ public class ConversationActivity extends BugleActionBarActivity
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@Deprecated
|
||||
protected void onActivityResult(final int requestCode, final int resultCode,
|
||||
final Intent data) {
|
||||
super.onActivityResult(requestCode, resultCode, data);
|
||||
final ConversationFragment conversationFragment = getConversationFragment();
|
||||
if (conversationFragment != null) {
|
||||
conversationFragment.onDefaultSmsAppResult(requestCode, resultCode);
|
||||
}
|
||||
}
|
||||
|
||||
public void suppressNextResumeTransition() {
|
||||
mSuppressNextResumeTransition = true;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onResume() {
|
||||
super.onResume();
|
||||
if (mSuppressNextResumeTransition) {
|
||||
overridePendingTransition(0, 0);
|
||||
mSuppressNextResumeTransition = false;
|
||||
}
|
||||
|
||||
// we need to reset the mInstanceStateSaved flag since we may have just been restored from
|
||||
// a previous onStop() instead of an onDestroy().
|
||||
|
||||
+38
@@ -0,0 +1,38 @@
|
||||
/*
|
||||
* Copyright (C) 2026 The LineageOS Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.android.messaging.ui.conversation;
|
||||
|
||||
import android.content.Context;
|
||||
import android.util.AttributeSet;
|
||||
import android.widget.FrameLayout;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
/**
|
||||
* Conversation chrome container. Compose keeps a higher elevation than the
|
||||
* decorative fades so taps reach the EditText through the normal dispatch path.
|
||||
*/
|
||||
public class ConversationChatAreaLayout extends FrameLayout {
|
||||
public ConversationChatAreaLayout(@NonNull final Context context) {
|
||||
super(context);
|
||||
}
|
||||
|
||||
public ConversationChatAreaLayout(@NonNull final Context context,
|
||||
@Nullable final AttributeSet attrs) {
|
||||
super(context, attrs);
|
||||
}
|
||||
}
|
||||
+1359
-230
File diff suppressed because it is too large
Load Diff
+66
-2
@@ -50,6 +50,15 @@ import java.util.Collection;
|
||||
* as the other components).
|
||||
*/
|
||||
public class ConversationInputManager implements ConversationInput.ConversationInputBase {
|
||||
|
||||
public static final int MEDIA_PICKER_TARGET_DEFAULT = 0;
|
||||
public static final int MEDIA_PICKER_TARGET_CAMERA = 1;
|
||||
public static final int MEDIA_PICKER_TARGET_GALLERY = 2;
|
||||
public static final int MEDIA_PICKER_TARGET_AUDIO = 3;
|
||||
public static final int MEDIA_PICKER_TARGET_CONTACT = 4;
|
||||
public static final int MEDIA_PICKER_TARGET_PHOTO = 5;
|
||||
public static final int MEDIA_PICKER_TARGET_VIDEO = 6;
|
||||
|
||||
/**
|
||||
* The host component where all input components are contained. This is typically the
|
||||
* conversation fragment but may be mocked in test code.
|
||||
@@ -189,6 +198,18 @@ public class ConversationInputManager implements ConversationInput.ConversationI
|
||||
}
|
||||
|
||||
public void showHideMediaPicker(final boolean show, final boolean animate) {
|
||||
showHideMediaPicker(show, animate, MEDIA_PICKER_TARGET_DEFAULT);
|
||||
}
|
||||
|
||||
public boolean isMediaPickerOpen() {
|
||||
return mMediaInput.mShowing;
|
||||
}
|
||||
|
||||
public void showHideMediaPicker(final boolean show, final boolean animate,
|
||||
final int mediaPickerTarget) {
|
||||
if (show) {
|
||||
mMediaInput.setOpenTarget(mediaPickerTarget);
|
||||
}
|
||||
showHideInternal(mMediaInput, show, animate);
|
||||
}
|
||||
|
||||
@@ -203,6 +224,16 @@ public class ConversationInputManager implements ConversationInput.ConversationI
|
||||
}
|
||||
|
||||
public void showHideImeKeyboard(final boolean show, final boolean animate) {
|
||||
if (show) {
|
||||
final EditText editText = mSink.getComposeEditText();
|
||||
if (editText != null) {
|
||||
ImeUtil.get().showImeKeyboard(mContext, editText);
|
||||
if (mConversationDataModel.isBound() && !mImeInput.mShowing) {
|
||||
showHideInternal(mImeInput, true, animate);
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
showHideInternal(mImeInput, show, animate);
|
||||
}
|
||||
|
||||
@@ -243,6 +274,9 @@ public class ConversationInputManager implements ConversationInput.ConversationI
|
||||
}
|
||||
|
||||
if (target.mShowing == show) {
|
||||
if (show && target instanceof ConversationImeKeyboard) {
|
||||
ImeUtil.get().showImeKeyboard(mContext, mSink.getComposeEditText());
|
||||
}
|
||||
return false;
|
||||
}
|
||||
beginUpdate();
|
||||
@@ -322,6 +356,11 @@ public class ConversationInputManager implements ConversationInput.ConversationI
|
||||
}
|
||||
|
||||
private MediaPicker mMediaPicker;
|
||||
private int mOpenTarget = MEDIA_PICKER_TARGET_DEFAULT;
|
||||
|
||||
void setOpenTarget(final int openTarget) {
|
||||
mOpenTarget = openTarget;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean show(boolean animate) {
|
||||
@@ -393,7 +432,32 @@ public class ConversationInputManager implements ConversationInput.ConversationI
|
||||
});
|
||||
}
|
||||
|
||||
mMediaPicker.open(MediaPicker.MEDIA_TYPE_DEFAULT, animate);
|
||||
switch (mOpenTarget) {
|
||||
case MEDIA_PICKER_TARGET_CAMERA:
|
||||
mMediaPicker.openCamera(animate);
|
||||
break;
|
||||
case MEDIA_PICKER_TARGET_GALLERY:
|
||||
mMediaPicker.openGallery(animate);
|
||||
// System picker only — do not keep the in-app panel marked open.
|
||||
return false;
|
||||
case MEDIA_PICKER_TARGET_AUDIO:
|
||||
mMediaPicker.openAudio(animate);
|
||||
break;
|
||||
case MEDIA_PICKER_TARGET_CONTACT:
|
||||
mMediaPicker.openContact(animate);
|
||||
// System picker only — do not keep the in-app panel marked open.
|
||||
return false;
|
||||
case MEDIA_PICKER_TARGET_PHOTO:
|
||||
mMediaPicker.openTakePicture();
|
||||
return false;
|
||||
case MEDIA_PICKER_TARGET_VIDEO:
|
||||
mMediaPicker.openTakeVideo();
|
||||
return false;
|
||||
default:
|
||||
mMediaPicker.open(MediaPicker.MEDIA_TYPE_DEFAULT, animate);
|
||||
break;
|
||||
}
|
||||
mOpenTarget = MEDIA_PICKER_TARGET_DEFAULT;
|
||||
|
||||
return isOpen();
|
||||
}
|
||||
@@ -436,7 +500,7 @@ public class ConversationInputManager implements ConversationInput.ConversationI
|
||||
mFragmentManager.beginTransaction().replace(
|
||||
R.id.mediapicker_container,
|
||||
mediaPicker,
|
||||
MediaPicker.FRAGMENT_TAG).commit();
|
||||
MediaPicker.FRAGMENT_TAG).commitNowAllowingStateLoss();
|
||||
}
|
||||
return mediaPicker;
|
||||
}
|
||||
|
||||
+120
-19
@@ -19,6 +19,7 @@ package com.android.messaging.ui.conversation;
|
||||
import android.content.Context;
|
||||
import android.content.res.Resources;
|
||||
import android.database.Cursor;
|
||||
import android.graphics.Paint;
|
||||
import android.graphics.Rect;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.net.Uri;
|
||||
@@ -37,6 +38,7 @@ import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.view.WindowManager;
|
||||
import android.widget.FrameLayout;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.ImageView.ScaleType;
|
||||
import android.widget.LinearLayout;
|
||||
import android.widget.TextView;
|
||||
@@ -87,6 +89,7 @@ public class ConversationMessageView extends FrameLayout implements View.OnClick
|
||||
Rect imageBounds, boolean longPress);
|
||||
SubscriptionListEntry getSubscriptionEntryForSelfParticipant(String selfParticipantId,
|
||||
boolean excludeDefault);
|
||||
void onMessageSendOptionsClick(ConversationMessageView view);
|
||||
}
|
||||
|
||||
private final ConversationMessageData mData;
|
||||
@@ -98,6 +101,8 @@ public class ConversationMessageView extends FrameLayout implements View.OnClick
|
||||
private boolean mMessageTextHasLinks;
|
||||
private boolean mMessageHasYouTubeLink;
|
||||
private TextView mStatusTextView;
|
||||
private TextView mStatusActionView;
|
||||
private ImageView mStatusIconView;
|
||||
private TextView mTitleTextView;
|
||||
private TextView mMmsInfoTextView;
|
||||
private LinearLayout mMessageTitleLayout;
|
||||
@@ -114,6 +119,7 @@ public class ConversationMessageView extends FrameLayout implements View.OnClick
|
||||
|
||||
private boolean mOneOnOne;
|
||||
private ConversationMessageViewHost mHost;
|
||||
private String mOptimisticSendingMessageId;
|
||||
|
||||
public ConversationMessageView(final Context context, final AttributeSet attrs) {
|
||||
super(context, attrs);
|
||||
@@ -143,6 +149,16 @@ public class ConversationMessageView extends FrameLayout implements View.OnClick
|
||||
IgnoreLinkLongClickHelper.ignoreLinkLongClick(mMessageTextView, this);
|
||||
|
||||
mStatusTextView = findViewById(R.id.message_status);
|
||||
mStatusActionView = findViewById(R.id.message_status_action);
|
||||
mStatusIconView = findViewById(R.id.message_status_icon);
|
||||
if (mStatusActionView != null) {
|
||||
mStatusActionView.setPaintFlags(
|
||||
mStatusActionView.getPaintFlags() | Paint.UNDERLINE_TEXT_FLAG);
|
||||
mStatusActionView.setOnClickListener(v -> notifySendOptionsClick());
|
||||
}
|
||||
if (mStatusIconView != null) {
|
||||
mStatusIconView.setOnClickListener(v -> notifySendOptionsClick());
|
||||
}
|
||||
mTitleTextView = findViewById(R.id.message_title);
|
||||
mMmsInfoTextView = findViewById(R.id.mms_info);
|
||||
mMessageTitleLayout = findViewById(R.id.message_title_layout);
|
||||
@@ -244,6 +260,11 @@ public class ConversationMessageView extends FrameLayout implements View.OnClick
|
||||
|
||||
// Update our UI model
|
||||
mData.bind(cursor);
|
||||
if (mOptimisticSendingMessageId != null
|
||||
&& (!TextUtils.equals(mOptimisticSendingMessageId, mData.getMessageId())
|
||||
|| !isOutgoingFailedStatus(mData.getStatus()))) {
|
||||
mOptimisticSendingMessageId = null;
|
||||
}
|
||||
setSelected(TextUtils.equals(mData.getMessageId(), selectedMessageId));
|
||||
|
||||
// Update text and image content for the view.
|
||||
@@ -272,6 +293,51 @@ public class ConversationMessageView extends FrameLayout implements View.OnClick
|
||||
return mData;
|
||||
}
|
||||
|
||||
public void showSendingStatus() {
|
||||
if (mData == null || !isOutgoingFailedStatus(mData.getStatus())) {
|
||||
return;
|
||||
}
|
||||
mOptimisticSendingMessageId = mData.getMessageId();
|
||||
updateViewContent();
|
||||
updateViewAppearance();
|
||||
}
|
||||
|
||||
private boolean isOptimisticSending() {
|
||||
return mOptimisticSendingMessageId != null
|
||||
&& mData != null
|
||||
&& TextUtils.equals(mOptimisticSendingMessageId, mData.getMessageId());
|
||||
}
|
||||
|
||||
private static boolean isOutgoingFailedStatus(final int status) {
|
||||
return status == MessageData.BUGLE_STATUS_OUTGOING_FAILED
|
||||
|| status == MessageData.BUGLE_STATUS_OUTGOING_FAILED_EMERGENCY_NUMBER;
|
||||
}
|
||||
|
||||
private boolean isOutgoingWaitingStatus() {
|
||||
if (mData == null || mData.getIsIncoming() || isOptimisticSending()) {
|
||||
return false;
|
||||
}
|
||||
final int status = mData.getStatus();
|
||||
return status == MessageData.BUGLE_STATUS_OUTGOING_AWAITING_RETRY;
|
||||
}
|
||||
|
||||
private boolean shouldShowSendOptions() {
|
||||
if (mData == null || mData.getIsIncoming() || isOptimisticSending()) {
|
||||
return false;
|
||||
}
|
||||
if (!PhoneUtils.getDefault().isDefaultSmsApp()) {
|
||||
return false;
|
||||
}
|
||||
return isOutgoingWaitingStatus()
|
||||
|| mData.getStatus() == MessageData.BUGLE_STATUS_OUTGOING_FAILED;
|
||||
}
|
||||
|
||||
private void notifySendOptionsClick() {
|
||||
if (mHost != null && shouldShowSendOptions()) {
|
||||
mHost.onMessageSendOptionsClick(this);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether we should show simplified visual style for the message view (i.e. hide the
|
||||
* avatar and bubble arrow, reduce padding).
|
||||
@@ -340,12 +406,12 @@ public class ConversationMessageView extends FrameLayout implements View.OnClick
|
||||
|
||||
case MessageData.BUGLE_STATUS_OUTGOING_YET_TO_SEND:
|
||||
case MessageData.BUGLE_STATUS_OUTGOING_SENDING:
|
||||
case MessageData.BUGLE_STATUS_OUTGOING_RESENDING:
|
||||
statusResId = R.string.message_status_sending;
|
||||
break;
|
||||
|
||||
case MessageData.BUGLE_STATUS_OUTGOING_RESENDING:
|
||||
case MessageData.BUGLE_STATUS_OUTGOING_AWAITING_RETRY:
|
||||
statusResId = R.string.message_status_send_retrying;
|
||||
statusText = getResources().getString(R.string.home_message_status_waiting);
|
||||
break;
|
||||
|
||||
case MessageData.BUGLE_STATUS_OUTGOING_FAILED_EMERGENCY_NUMBER:
|
||||
@@ -354,13 +420,12 @@ public class ConversationMessageView extends FrameLayout implements View.OnClick
|
||||
|
||||
case MessageData.BUGLE_STATUS_OUTGOING_FAILED:
|
||||
// don't show the error state unless we're the default sms app
|
||||
if (isOptimisticSending()) {
|
||||
statusResId = R.string.message_status_sending;
|
||||
break;
|
||||
}
|
||||
if (PhoneUtils.getDefault().isDefaultSmsApp()) {
|
||||
if (isSelected()) {
|
||||
statusResId = R.string.message_status_resend;
|
||||
} else {
|
||||
statusResId = MmsUtils.mapRawStatusToErrorResourceId(
|
||||
mData.getStatus(), mData.getRawTelephonyStatus());
|
||||
}
|
||||
statusText = getResources().getString(R.string.home_message_status_not_sent);
|
||||
break;
|
||||
}
|
||||
// FALL THROUGH HERE
|
||||
@@ -416,10 +481,15 @@ public class ConversationMessageView extends FrameLayout implements View.OnClick
|
||||
// We set the text even if the view will be GONE for accessibility
|
||||
mStatusTextView.setText(statusText);
|
||||
final boolean statusVisible = !TextUtils.isEmpty(statusText);
|
||||
if (statusVisible) {
|
||||
mStatusTextView.setVisibility(View.VISIBLE);
|
||||
} else {
|
||||
mStatusTextView.setVisibility(View.GONE);
|
||||
mStatusTextView.setVisibility(statusVisible ? View.VISIBLE : View.GONE);
|
||||
|
||||
final boolean sendOptionsVisible = shouldShowSendOptions();
|
||||
if (mStatusActionView != null) {
|
||||
mStatusActionView.setVisibility(sendOptionsVisible ? View.VISIBLE : View.GONE);
|
||||
}
|
||||
if (mStatusIconView != null) {
|
||||
mStatusIconView.setVisibility(
|
||||
isOutgoingWaitingStatus() ? View.VISIBLE : View.GONE);
|
||||
}
|
||||
|
||||
final boolean deliveredBadgeVisible =
|
||||
@@ -450,11 +520,11 @@ public class ConversationMessageView extends FrameLayout implements View.OnClick
|
||||
}
|
||||
|
||||
final boolean metadataVisible = senderNameVisible || statusVisible
|
||||
|| deliveredBadgeVisible || simNameVisible;
|
||||
|| sendOptionsVisible || deliveredBadgeVisible || simNameVisible;
|
||||
mMessageMetadataView.setVisibility(metadataVisible ? View.VISIBLE : View.GONE);
|
||||
|
||||
final boolean messageTextAndOrInfoVisible = titleVisible || subjectVisible
|
||||
|| mData.hasText() || metadataVisible;
|
||||
|| mData.hasText();
|
||||
mMessageTextAndInfoView.setVisibility(
|
||||
messageTextAndOrInfoVisible ? View.VISIBLE : View.GONE);
|
||||
|
||||
@@ -697,7 +767,7 @@ public class ConversationMessageView extends FrameLayout implements View.OnClick
|
||||
textBottomPadding = textBottomPaddingDefault;
|
||||
textLeftPadding = messageTextLeftRightPadding;
|
||||
textRightPadding = messageTextLeftRightPadding;
|
||||
mMessageTextView.setTextIsSelectable(isSelected());
|
||||
mMessageTextView.setTextIsSelectable(false);
|
||||
} else {
|
||||
// Attachment(s) only
|
||||
contentLeftPadding = incoming ? arrowWidth : 0;
|
||||
@@ -723,7 +793,7 @@ public class ConversationMessageView extends FrameLayout implements View.OnClick
|
||||
textTopMargin = 0;
|
||||
textTopPadding = textTopPaddingDefault;
|
||||
textBottomPadding = textBottomPaddingDefault;
|
||||
mMessageTextView.setTextIsSelectable(isSelected());
|
||||
mMessageTextView.setTextIsSelectable(false);
|
||||
textLeftPadding = messageTextLeftRightPadding;
|
||||
textRightPadding = messageTextLeftRightPadding;
|
||||
}
|
||||
@@ -760,6 +830,9 @@ public class ConversationMessageView extends FrameLayout implements View.OnClick
|
||||
updateMessageAttachmentsAppearance(gravity);
|
||||
|
||||
mMessageMetadataView.setPadding(0, metadataTopPadding, 0, 0);
|
||||
if (mMessageMetadataView.getVisibility() == View.VISIBLE) {
|
||||
setPadding(getPaddingStart(), getPaddingTop(), getPaddingEnd(), metadataTopPadding);
|
||||
}
|
||||
|
||||
updateTextAppearance();
|
||||
|
||||
@@ -817,6 +890,10 @@ public class ConversationMessageView extends FrameLayout implements View.OnClick
|
||||
description.append(separator);
|
||||
description.append(mStatusTextView.getText());
|
||||
}
|
||||
if (mStatusActionView != null && mStatusActionView.getVisibility() == View.VISIBLE) {
|
||||
description.append(separator);
|
||||
description.append(mStatusActionView.getText());
|
||||
}
|
||||
|
||||
if (mSimNameView.getVisibility() == View.VISIBLE) {
|
||||
description.append(separator);
|
||||
@@ -908,7 +985,11 @@ public class ConversationMessageView extends FrameLayout implements View.OnClick
|
||||
|
||||
case MessageData.BUGLE_STATUS_OUTGOING_FAILED:
|
||||
case MessageData.BUGLE_STATUS_OUTGOING_FAILED_EMERGENCY_NUMBER:
|
||||
timestampColorResId = R.color.message_failed_timestamp_text;
|
||||
if (isOptimisticSending()) {
|
||||
timestampColorResId = R.color.timestamp_text_outgoing;
|
||||
} else {
|
||||
timestampColorResId = R.color.message_failed_timestamp_text;
|
||||
}
|
||||
subjectLabelColorResId = R.color.timestamp_text_outgoing;
|
||||
break;
|
||||
|
||||
@@ -964,10 +1045,30 @@ public class ConversationMessageView extends FrameLayout implements View.OnClick
|
||||
mData.hasAttachments() && !shouldShowMessageTextBubble()) {
|
||||
timestampColorResId = R.color.timestamp_text_outgoing;
|
||||
}
|
||||
mStatusTextView.setTextColor(getResources().getColor(timestampColorResId, theme));
|
||||
// Status captions sit outside the bubble, so keep them readable against the canvas.
|
||||
final int captionColorResId;
|
||||
if (shouldShowSendOptions() && !isOutgoingWaitingStatus()) {
|
||||
captionColorResId = R.color.home_destructive;
|
||||
} else if (isOutgoingWaitingStatus() || (!mData.getIsIncoming()
|
||||
&& (mData.getStatus() == MessageData.BUGLE_STATUS_OUTGOING_SENDING
|
||||
|| mData.getStatus() == MessageData.BUGLE_STATUS_OUTGOING_RESENDING
|
||||
|| isOptimisticSending()))) {
|
||||
captionColorResId = R.color.home_text_secondary;
|
||||
} else {
|
||||
captionColorResId = mData.getIsIncoming()
|
||||
? R.color.timestamp_text_incoming : R.color.timestamp_text_outgoing;
|
||||
}
|
||||
final int captionColor = getResources().getColor(captionColorResId, theme);
|
||||
mStatusTextView.setTextColor(captionColor);
|
||||
if (mStatusActionView != null) {
|
||||
mStatusActionView.setTextColor(captionColor);
|
||||
}
|
||||
if (mStatusIconView != null) {
|
||||
mStatusIconView.setColorFilter(captionColor);
|
||||
}
|
||||
|
||||
mSubjectLabel.setTextColor(getResources().getColor(subjectLabelColorResId, theme));
|
||||
mSenderNameTextView.setTextColor(getResources().getColor(timestampColorResId, theme));
|
||||
mSenderNameTextView.setTextColor(captionColor);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
+383
-32
@@ -25,8 +25,8 @@ import android.content.Intent;
|
||||
import android.content.res.Resources;
|
||||
import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
import android.view.ActionMode;
|
||||
import android.view.View;
|
||||
|
||||
import androidx.activity.result.ActivityResultLauncher;
|
||||
import androidx.activity.result.contract.ActivityResultContracts;
|
||||
import androidx.annotation.NonNull;
|
||||
@@ -36,14 +36,20 @@ import androidx.appcompat.app.ActionBar;
|
||||
import com.android.messaging.R;
|
||||
import com.android.messaging.datamodel.action.DeleteConversationAction;
|
||||
import com.android.messaging.datamodel.action.UpdateConversationArchiveStatusAction;
|
||||
import com.android.messaging.datamodel.action.UpdateConversationTrashStatusAction;
|
||||
import com.android.messaging.datamodel.action.UpdateDestinationBlockedAction;
|
||||
import com.android.messaging.datamodel.data.ConversationListData;
|
||||
import com.android.messaging.datamodel.data.ConversationListItemData;
|
||||
import com.android.messaging.ui.BugleActionBarActivity;
|
||||
import com.android.messaging.ui.SnackBar;
|
||||
import com.android.messaging.ui.SnackBarInteraction;
|
||||
import com.android.messaging.datamodel.action.MarkAsUnreadAction;
|
||||
import com.android.messaging.ui.UIIntents;
|
||||
import com.android.messaging.ui.contact.AddContactsConfirmationDialog;
|
||||
import com.android.messaging.ui.home.HomeDeleteAlertDialog;
|
||||
import com.android.messaging.ui.home.HomeInboxMenu;
|
||||
import com.android.messaging.ui.home.HomeModal;
|
||||
import com.android.messaging.ui.home.HomeToast;
|
||||
import com.android.messaging.ui.conversationlist.ConversationListFragment.ConversationListFragmentHost;
|
||||
import com.android.messaging.ui.conversationlist.MultiSelectActionModeCallback.SelectedConversation;
|
||||
import com.android.messaging.util.ChangeDefaultSmsAppHelper;
|
||||
@@ -61,6 +67,10 @@ import java.util.List;
|
||||
public abstract class AbstractConversationListActivity extends BugleActionBarActivity
|
||||
implements ConversationListFragmentHost, MultiSelectActionModeCallback.Listener {
|
||||
|
||||
private static final UpdateDestinationBlockedAction.UpdateDestinationBlockedActionListener
|
||||
NOOP_BLOCK_LISTENER =
|
||||
(action, success, block, destination) -> { };
|
||||
|
||||
protected ConversationListFragment mConversationListFragment;
|
||||
|
||||
private ChangeDefaultSmsAppHelper mChangeDefaultSmsAppHelper;
|
||||
@@ -90,22 +100,147 @@ public abstract class AbstractConversationListActivity extends BugleActionBarAct
|
||||
return false;
|
||||
}
|
||||
|
||||
protected boolean isTrashList() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean shouldShowContextualActionBar(final ActionMode.Callback callback) {
|
||||
if (useHomeSelectionChrome() && callback instanceof MultiSelectActionModeCallback) {
|
||||
return !((MultiSelectActionModeCallback) callback).useHomeSelectionChrome();
|
||||
}
|
||||
return super.shouldShowContextualActionBar(callback);
|
||||
}
|
||||
|
||||
public void enterHomeEditMode() {
|
||||
if (!hasVisibleConversations()) {
|
||||
HomeToast.show(this, R.string.home_toast_nothing_to_select);
|
||||
return;
|
||||
}
|
||||
if (!isInConversationListSelectMode()) {
|
||||
startMultiSelectActionMode();
|
||||
}
|
||||
}
|
||||
|
||||
private boolean hasVisibleConversations() {
|
||||
return mConversationListFragment != null
|
||||
&& mConversationListFragment.getVisibleConversationCount() > 0;
|
||||
}
|
||||
|
||||
public void onSubListSelectionMode(final boolean enabled) {
|
||||
onSubListSelectionCount(0);
|
||||
HomeInboxMenu.setSelecting(this, enabled);
|
||||
}
|
||||
|
||||
public void onSubListSelectionCount(final int count) {
|
||||
final int visible = mConversationListFragment == null
|
||||
? 0 : mConversationListFragment.getVisibleConversationCount();
|
||||
final boolean hasWork = count > 0 || visible > 0;
|
||||
if (isTrashList()) {
|
||||
HomeInboxMenu.setSelectAction(this,
|
||||
count > 0 ? R.drawable.ic_delete_small_dark : R.drawable.ic_home_menu_trash,
|
||||
count > 0 ? R.string.home_delete_confirm : R.string.home_empty_trash_action,
|
||||
hasWork, true);
|
||||
} else {
|
||||
HomeInboxMenu.setSelectAction(this, R.drawable.ic_home_menu_archive,
|
||||
count > 0 ? R.string.home_sub_unarchive : R.string.home_sub_unarchive_all,
|
||||
hasWork, false);
|
||||
}
|
||||
}
|
||||
|
||||
public void onSubListPrimaryAction() {
|
||||
if (isTrashList()) {
|
||||
emptyTrashOrDeleteSelected();
|
||||
} else {
|
||||
unarchiveAllOrSelected();
|
||||
}
|
||||
}
|
||||
|
||||
private void unarchiveAllOrSelected() {
|
||||
final Collection<SelectedConversation> selected = getSelectedConversationsIfAny();
|
||||
if (selected != null && !selected.isEmpty()) {
|
||||
restoreSelectedConversations();
|
||||
return;
|
||||
}
|
||||
if (mConversationListFragment == null) {
|
||||
return;
|
||||
}
|
||||
final List<ConversationListItemData> items =
|
||||
mConversationListFragment.snapshotVisibleConversations();
|
||||
if (items.isEmpty()) {
|
||||
HomeToast.show(this, R.string.home_toast_nothing_to_unarchive);
|
||||
return;
|
||||
}
|
||||
for (final ConversationListItemData item : items) {
|
||||
UpdateConversationArchiveStatusAction.unarchiveConversation(item.getConversationId());
|
||||
}
|
||||
if (items.size() == 1) {
|
||||
HomeToast.show(this, R.string.home_profile_unarchived_toast);
|
||||
} else {
|
||||
HomeToast.show(this, getString(R.string.home_sub_unarchived_toast_many, items.size()));
|
||||
}
|
||||
exitMultiSelectState();
|
||||
}
|
||||
|
||||
private void emptyTrashOrDeleteSelected() {
|
||||
final Collection<SelectedConversation> selected = getSelectedConversationsIfAny();
|
||||
if (selected != null && !selected.isEmpty()) {
|
||||
confirmPermanentDelete(selected);
|
||||
return;
|
||||
}
|
||||
if (mConversationListFragment == null) {
|
||||
return;
|
||||
}
|
||||
final List<ConversationListItemData> items =
|
||||
mConversationListFragment.snapshotVisibleConversations();
|
||||
if (items.isEmpty()) {
|
||||
HomeToast.show(this, R.string.home_toast_nothing_to_empty);
|
||||
return;
|
||||
}
|
||||
final ArrayList<SelectedConversation> all = new ArrayList<>(items.size());
|
||||
for (final ConversationListItemData item : items) {
|
||||
all.add(new SelectedConversation(item));
|
||||
}
|
||||
confirmEmptyAll(all);
|
||||
}
|
||||
|
||||
private Collection<SelectedConversation> getSelectedConversationsIfAny() {
|
||||
if (!isInConversationListSelectMode()) {
|
||||
return null;
|
||||
}
|
||||
final MultiSelectActionModeCallback callback =
|
||||
(MultiSelectActionModeCallback) getActionModeCallback();
|
||||
return callback.getSelectedConversations();
|
||||
}
|
||||
|
||||
protected void startMultiSelectActionMode() {
|
||||
startActionMode(new MultiSelectActionModeCallback(this, useHomeSelectionChrome()));
|
||||
mConversationListFragment.dismissFab();
|
||||
if (useHomeSelectionChrome()) {
|
||||
mConversationListFragment.setHomeSelectionMode(true);
|
||||
if (mConversationListFragment == null) {
|
||||
return;
|
||||
}
|
||||
final boolean homeChrome = useHomeSelectionChrome();
|
||||
if (homeChrome) {
|
||||
hideHomeContextualActionBar();
|
||||
mConversationListFragment.dismissFab(true);
|
||||
}
|
||||
startActionMode(new MultiSelectActionModeCallback(this, homeChrome));
|
||||
if (homeChrome) {
|
||||
mConversationListFragment.setHomeSelectionMode(true);
|
||||
} else {
|
||||
mConversationListFragment.dismissFab();
|
||||
}
|
||||
}
|
||||
|
||||
public void exitMultiSelectState() {
|
||||
dismissActionMode();
|
||||
if (mConversationListFragment == null) {
|
||||
return;
|
||||
}
|
||||
if (useHomeSelectionChrome()) {
|
||||
mConversationListFragment.setHomeSelectionMode(false);
|
||||
} else {
|
||||
mConversationListFragment.updateUi();
|
||||
}
|
||||
mConversationListFragment.showFab();
|
||||
dismissActionMode();
|
||||
mConversationListFragment.updateUi();
|
||||
}
|
||||
|
||||
protected void hideHomeContextualActionBar() {
|
||||
@@ -121,7 +256,149 @@ public abstract class AbstractConversationListActivity extends BugleActionBarAct
|
||||
}
|
||||
final MultiSelectActionModeCallback callback =
|
||||
(MultiSelectActionModeCallback) getActionModeCallback();
|
||||
onActionBarDelete(callback.getSelectedConversations());
|
||||
final Collection<SelectedConversation> conversations =
|
||||
callback.getSelectedConversations();
|
||||
if (conversations.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
if (isTrashList()) {
|
||||
confirmPermanentDelete(conversations);
|
||||
return;
|
||||
}
|
||||
if (useHomeSelectionChrome()) {
|
||||
showHomeDeleteAlert(conversations);
|
||||
} else {
|
||||
onActionBarDelete(conversations);
|
||||
}
|
||||
}
|
||||
|
||||
public void confirmDeleteConversation(final ConversationListItemData data) {
|
||||
if (data == null) {
|
||||
return;
|
||||
}
|
||||
final java.util.List<SelectedConversation> one = java.util.Collections.singletonList(
|
||||
new MultiSelectActionModeCallback.SelectedConversation(data));
|
||||
if (isTrashList()) {
|
||||
confirmPermanentDelete(one);
|
||||
return;
|
||||
}
|
||||
showHomeDeleteAlert(one);
|
||||
}
|
||||
|
||||
public void markSelectedConversationsUnread() {
|
||||
if (!isInConversationListSelectMode()) {
|
||||
return;
|
||||
}
|
||||
final MultiSelectActionModeCallback callback =
|
||||
(MultiSelectActionModeCallback) getActionModeCallback();
|
||||
for (final MultiSelectActionModeCallback.SelectedConversation conversation
|
||||
: callback.getSelectedConversations()) {
|
||||
MarkAsUnreadAction.markAsUnread(conversation.conversationId);
|
||||
}
|
||||
exitMultiSelectState();
|
||||
}
|
||||
|
||||
public void restoreSelectedConversations() {
|
||||
if (!isInConversationListSelectMode()) {
|
||||
return;
|
||||
}
|
||||
final MultiSelectActionModeCallback callback =
|
||||
(MultiSelectActionModeCallback) getActionModeCallback();
|
||||
final Collection<SelectedConversation> conversations =
|
||||
callback.getSelectedConversations();
|
||||
if (conversations.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
for (final SelectedConversation conversation : conversations) {
|
||||
if (isTrashList()) {
|
||||
UpdateConversationTrashStatusAction.restoreFromTrash(conversation.conversationId);
|
||||
} else {
|
||||
UpdateConversationArchiveStatusAction.unarchiveConversation(
|
||||
conversation.conversationId);
|
||||
}
|
||||
}
|
||||
HomeToast.show(this, isTrashList()
|
||||
? R.string.home_trash_restored_toast
|
||||
: R.string.home_profile_unarchived_toast);
|
||||
exitMultiSelectState();
|
||||
}
|
||||
|
||||
public void archiveSelectedConversations() {
|
||||
if (!isInConversationListSelectMode()) {
|
||||
return;
|
||||
}
|
||||
final MultiSelectActionModeCallback callback =
|
||||
(MultiSelectActionModeCallback) getActionModeCallback();
|
||||
final Collection<SelectedConversation> conversations =
|
||||
callback.getSelectedConversations();
|
||||
if (conversations.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
int count = 0;
|
||||
for (final SelectedConversation conversation : conversations) {
|
||||
UpdateConversationArchiveStatusAction.archiveConversation(
|
||||
conversation.conversationId);
|
||||
count++;
|
||||
}
|
||||
if (count == 1) {
|
||||
HomeToast.show(this, R.string.home_profile_archived_toast);
|
||||
} else {
|
||||
HomeToast.show(this, getString(R.string.home_selection_archived_toast_many, count));
|
||||
}
|
||||
exitMultiSelectState();
|
||||
}
|
||||
|
||||
public void reportSelectedConversationsSpam() {
|
||||
if (!isInConversationListSelectMode()) {
|
||||
return;
|
||||
}
|
||||
final MultiSelectActionModeCallback callback =
|
||||
(MultiSelectActionModeCallback) getActionModeCallback();
|
||||
final Collection<SelectedConversation> conversations = new ArrayList<>(
|
||||
callback.getSelectedConversations());
|
||||
int reportableCount = 0;
|
||||
for (final SelectedConversation conversation : conversations) {
|
||||
if (!conversation.isGroup
|
||||
&& conversation.otherParticipantNormalizedDestination != null) {
|
||||
reportableCount++;
|
||||
}
|
||||
}
|
||||
if (reportableCount == 0) {
|
||||
HomeToast.show(this, R.string.home_report_spam_groups_only);
|
||||
return;
|
||||
}
|
||||
final int reportable = reportableCount;
|
||||
final String message = reportable == 1
|
||||
? getString(R.string.home_report_spam_message_one)
|
||||
: getString(R.string.home_report_spam_message_many, reportable);
|
||||
HomeModal.builder(this)
|
||||
.icon(R.drawable.ic_home_menu_spam)
|
||||
.title(R.string.home_report_spam_title)
|
||||
.message(message)
|
||||
.negative(android.R.string.cancel, null)
|
||||
.positive(R.string.home_unknown_sender_report_spam, () ->
|
||||
performReportSelectedSpam(conversations, reportable))
|
||||
.show();
|
||||
}
|
||||
|
||||
private void performReportSelectedSpam(
|
||||
final Collection<SelectedConversation> conversations, final int reportable) {
|
||||
for (final SelectedConversation conversation : conversations) {
|
||||
if (conversation.isGroup
|
||||
|| conversation.otherParticipantNormalizedDestination == null) {
|
||||
continue;
|
||||
}
|
||||
UpdateDestinationBlockedAction.updateDestinationBlocked(
|
||||
conversation.otherParticipantNormalizedDestination, true,
|
||||
conversation.conversationId,
|
||||
NOOP_BLOCK_LISTENER);
|
||||
}
|
||||
if (reportable == 1) {
|
||||
HomeToast.show(this, R.string.home_report_spam_toast_one);
|
||||
} else {
|
||||
HomeToast.show(this, getString(R.string.home_report_spam_toast_many, reportable));
|
||||
}
|
||||
exitMultiSelectState();
|
||||
}
|
||||
|
||||
protected boolean isInConversationListSelectMode() {
|
||||
@@ -133,23 +410,104 @@ public abstract class AbstractConversationListActivity extends BugleActionBarAct
|
||||
return isInConversationListSelectMode();
|
||||
}
|
||||
|
||||
private void showHomeDeleteAlert(
|
||||
final Collection<SelectedConversation> conversations) {
|
||||
if (!ensureDefaultSmsAppForDelete()) {
|
||||
return;
|
||||
}
|
||||
HomeDeleteAlertDialog.show(this, conversations.size(),
|
||||
() -> performDeleteConversations(conversations));
|
||||
}
|
||||
|
||||
private boolean ensureDefaultSmsAppForDelete() {
|
||||
if (PhoneUtils.getDefault().isDefaultSmsApp()) {
|
||||
return true;
|
||||
}
|
||||
// TODO: figure out a good way to combine this with the implementation in
|
||||
// ConversationFragment doing similar things
|
||||
final Activity activity = this;
|
||||
UiUtils.showSnackBarWithCustomAction(this,
|
||||
getWindow().getDecorView().getRootView(),
|
||||
getString(R.string.home_requires_default_sms_app),
|
||||
SnackBar.Action.createCustomAction(() -> {
|
||||
final Intent intent =
|
||||
UIIntents.get().getChangeDefaultSmsAppIntent(activity);
|
||||
mLauncher.launch(intent);
|
||||
},
|
||||
getString(R.string.home_requires_default_sms_change)),
|
||||
null /* interactions */,
|
||||
null /* placement */);
|
||||
return false;
|
||||
}
|
||||
|
||||
private void performDeleteConversations(
|
||||
final Collection<SelectedConversation> conversations) {
|
||||
final int count = conversations.size();
|
||||
if (isTrashList()) {
|
||||
for (final SelectedConversation conversation : conversations) {
|
||||
DeleteConversationAction.deleteConversation(
|
||||
conversation.conversationId,
|
||||
conversation.timestamp);
|
||||
}
|
||||
showDeleteCompletedToast(count);
|
||||
} else {
|
||||
for (final SelectedConversation conversation : conversations) {
|
||||
UpdateConversationTrashStatusAction.moveToTrash(conversation.conversationId);
|
||||
}
|
||||
if (useHomeSelectionChrome() && count > 0) {
|
||||
HomeToast.show(this, count == 1
|
||||
? getString(R.string.home_trash_moved_toast_one)
|
||||
: getString(R.string.home_trash_moved_toast_many, count));
|
||||
}
|
||||
}
|
||||
if (isInConversationListSelectMode()) {
|
||||
exitMultiSelectState();
|
||||
}
|
||||
}
|
||||
|
||||
private void confirmPermanentDelete(final Collection<SelectedConversation> conversations) {
|
||||
if (!ensureDefaultSmsAppForDelete()) {
|
||||
return;
|
||||
}
|
||||
final int count = conversations.size();
|
||||
HomeModal.builder(this)
|
||||
.icon(R.drawable.ic_home_menu_trash)
|
||||
.title(R.string.home_trash_permanent_title)
|
||||
.message(count == 1
|
||||
? getString(R.string.home_trash_permanent_message_one)
|
||||
: getString(R.string.home_trash_permanent_message_many, count))
|
||||
.negative(android.R.string.cancel, null)
|
||||
.positive(R.string.home_delete_confirm, () ->
|
||||
performDeleteConversations(conversations))
|
||||
.show();
|
||||
}
|
||||
|
||||
private void confirmEmptyAll(final Collection<SelectedConversation> conversations) {
|
||||
if (!ensureDefaultSmsAppForDelete()) {
|
||||
return;
|
||||
}
|
||||
HomeModal.builder(this)
|
||||
.icon(R.drawable.ic_home_menu_trash)
|
||||
.title(R.string.home_empty_all_title)
|
||||
.message(R.string.home_empty_all_message)
|
||||
.negative(android.R.string.cancel, null)
|
||||
.positive(R.string.home_empty_all_confirm, () ->
|
||||
performDeleteConversations(conversations))
|
||||
.show();
|
||||
}
|
||||
|
||||
private void showDeleteCompletedToast(final int count) {
|
||||
if (!useHomeSelectionChrome() || count <= 0) {
|
||||
return;
|
||||
}
|
||||
HomeToast.show(this, count == 1
|
||||
? getString(R.string.home_delete_toast_one)
|
||||
: getString(R.string.home_delete_toast_many, count));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onActionBarDelete(final Collection<SelectedConversation> conversations) {
|
||||
if (!PhoneUtils.getDefault().isDefaultSmsApp()) {
|
||||
// TODO: figure out a good way to combine this with the implementation in
|
||||
// ConversationFragment doing similar things
|
||||
final Activity activity = this;
|
||||
UiUtils.showSnackBarWithCustomAction(this,
|
||||
getWindow().getDecorView().getRootView(),
|
||||
getString(R.string.requires_default_sms_app),
|
||||
SnackBar.Action.createCustomAction(() -> {
|
||||
final Intent intent =
|
||||
UIIntents.get().getChangeDefaultSmsAppIntent(activity);
|
||||
mLauncher.launch(intent);
|
||||
},
|
||||
getString(R.string.requires_default_sms_change_button)),
|
||||
null /* interactions */,
|
||||
null /* placement */);
|
||||
if (!ensureDefaultSmsAppForDelete()) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -158,14 +516,7 @@ public abstract class AbstractConversationListActivity extends BugleActionBarAct
|
||||
R.plurals.delete_conversations_confirmation_dialog_title,
|
||||
conversations.size()))
|
||||
.setPositiveButton(R.string.delete_conversation_confirmation_button,
|
||||
(dialog, button) -> {
|
||||
for (final SelectedConversation conversation : conversations) {
|
||||
DeleteConversationAction.deleteConversation(
|
||||
conversation.conversationId,
|
||||
conversation.timestamp);
|
||||
}
|
||||
exitMultiSelectState();
|
||||
})
|
||||
(dialog, button) -> performDeleteConversations(conversations))
|
||||
.setNegativeButton(R.string.delete_conversation_decline_button, null)
|
||||
.show();
|
||||
}
|
||||
@@ -256,7 +607,7 @@ public abstract class AbstractConversationListActivity extends BugleActionBarAct
|
||||
@Override
|
||||
public void onSelectionCountChanged(final int count) {
|
||||
if (useHomeSelectionChrome()) {
|
||||
mConversationListFragment.updateHomeSelectionDeleteEnabled(count);
|
||||
mConversationListFragment.updateHomeSelectionBar(count);
|
||||
hideHomeContextualActionBar();
|
||||
}
|
||||
}
|
||||
|
||||
+130
-18
@@ -16,37 +16,143 @@
|
||||
*/
|
||||
package com.android.messaging.ui.conversationlist;
|
||||
|
||||
import android.graphics.drawable.ColorDrawable;
|
||||
import android.os.Bundle;
|
||||
import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.appcompat.app.ActionBar;
|
||||
import androidx.core.content.ContextCompat;
|
||||
|
||||
import com.android.messaging.R;
|
||||
import com.android.messaging.ui.home.HomeAutomationHome;
|
||||
import com.android.messaging.ui.home.HomeEmptyState;
|
||||
import com.android.messaging.ui.home.HomeInboxMenu;
|
||||
import com.android.messaging.ui.home.HomeSubHeader;
|
||||
|
||||
public class ArchivedConversationListActivity extends AbstractConversationListActivity {
|
||||
|
||||
private static final String STATE_SHOW_RULES = "home_archived_show_rules";
|
||||
|
||||
private TextView mChatsTab;
|
||||
private TextView mRulesTab;
|
||||
private View mListHost;
|
||||
private View mRulesHost;
|
||||
private View mSearchCollapse;
|
||||
private boolean mShowRules;
|
||||
|
||||
@Override
|
||||
protected void onCreate(final Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
setContentView(R.layout.home_archived_activity);
|
||||
if (getSupportActionBar() != null) {
|
||||
getSupportActionBar().hide();
|
||||
}
|
||||
HomeSubHeader.bind(this, R.string.home_archived_title, v -> onActionBarHome());
|
||||
HomeInboxMenu.bind(this, HomeInboxMenu.Screen.ARCHIVE);
|
||||
bindTabs();
|
||||
if (savedInstanceState != null) {
|
||||
mShowRules = savedInstanceState.getBoolean(STATE_SHOW_RULES, false);
|
||||
}
|
||||
showRules(mShowRules);
|
||||
|
||||
mConversationListFragment =
|
||||
ConversationListFragment.createArchivedConversationListFragment();
|
||||
getSupportFragmentManager()
|
||||
.beginTransaction()
|
||||
.add(android.R.id.content, mConversationListFragment)
|
||||
.commit();
|
||||
invalidateActionBar();
|
||||
if (savedInstanceState == null) {
|
||||
mConversationListFragment =
|
||||
ConversationListFragment.createArchivedConversationListFragment();
|
||||
getSupportFragmentManager()
|
||||
.beginTransaction()
|
||||
.replace(R.id.home_archived_list_host, mConversationListFragment)
|
||||
.commit();
|
||||
} else {
|
||||
mConversationListFragment = (ConversationListFragment) getSupportFragmentManager()
|
||||
.findFragmentById(R.id.home_archived_list_host);
|
||||
}
|
||||
}
|
||||
|
||||
protected void updateActionBar(ActionBar actionBar) {
|
||||
actionBar.setTitle(getString(R.string.archived_activity_title));
|
||||
actionBar.setDisplayShowTitleEnabled(true);
|
||||
actionBar.setDisplayHomeAsUpEnabled(true);
|
||||
actionBar.setBackgroundDrawable(new ColorDrawable(
|
||||
getResources().getColor(
|
||||
R.color.archived_conversation_action_bar_background_color_dark)));
|
||||
actionBar.show();
|
||||
@Override
|
||||
protected void onSaveInstanceState(final Bundle outState) {
|
||||
super.onSaveInstanceState(outState);
|
||||
outState.putBoolean(STATE_SHOW_RULES, mShowRules);
|
||||
}
|
||||
|
||||
private void bindTabs() {
|
||||
final View tabs = findViewById(R.id.home_archived_tabs);
|
||||
mChatsTab = findViewById(R.id.home_archived_tab_chats);
|
||||
mRulesTab = findViewById(R.id.home_archived_tab_rules);
|
||||
mListHost = findViewById(R.id.home_archived_list_host);
|
||||
mRulesHost = findViewById(R.id.home_archived_rules_host);
|
||||
mSearchCollapse = findViewById(R.id.home_sub_search_collapse);
|
||||
if (tabs != null) {
|
||||
tabs.setVisibility(View.VISIBLE);
|
||||
}
|
||||
if (mRulesHost != null) {
|
||||
HomeAutomationHome.bindArchive(mRulesHost);
|
||||
final HomeEmptyState empty = findViewById(R.id.home_archived_rules_empty);
|
||||
if (empty != null) {
|
||||
empty.setContent(R.drawable.ic_home_menu_archive,
|
||||
R.string.home_archived_rules_empty_title,
|
||||
R.string.home_archived_rules_empty_body);
|
||||
}
|
||||
}
|
||||
if (mChatsTab != null) {
|
||||
mChatsTab.setOnClickListener(v -> showRules(false));
|
||||
}
|
||||
if (mRulesTab != null) {
|
||||
mRulesTab.setOnClickListener(v -> showRules(true));
|
||||
}
|
||||
}
|
||||
|
||||
private void showRules(final boolean rules) {
|
||||
if (rules && isInConversationListSelectMode()) {
|
||||
exitMultiSelectState();
|
||||
}
|
||||
mShowRules = rules;
|
||||
applyPill(mChatsTab, !rules);
|
||||
applyPill(mRulesTab, rules);
|
||||
if (mListHost != null) {
|
||||
mListHost.setVisibility(rules ? View.GONE : View.VISIBLE);
|
||||
}
|
||||
if (mRulesHost != null) {
|
||||
mRulesHost.setVisibility(rules ? View.VISIBLE : View.GONE);
|
||||
}
|
||||
if (mSearchCollapse != null) {
|
||||
mSearchCollapse.setVisibility(rules ? View.GONE : View.VISIBLE);
|
||||
}
|
||||
final View edit = findViewById(R.id.home_sub_header_edit);
|
||||
if (edit != null) {
|
||||
edit.setVisibility(rules ? View.GONE : View.VISIBLE);
|
||||
}
|
||||
final View selectBar = findViewById(R.id.home_sub_select_bar);
|
||||
if (selectBar != null && rules) {
|
||||
selectBar.setVisibility(View.GONE);
|
||||
}
|
||||
if (rules && mRulesHost != null) {
|
||||
HomeAutomationHome.bindArchive(mRulesHost);
|
||||
}
|
||||
}
|
||||
|
||||
private void applyPill(final TextView pill, final boolean selected) {
|
||||
if (pill == null) {
|
||||
return;
|
||||
}
|
||||
pill.setBackgroundResource(selected
|
||||
? R.drawable.home_bg_automation_pill_selected
|
||||
: R.drawable.home_bg_automation_pill);
|
||||
pill.setTextColor(ContextCompat.getColor(this,
|
||||
selected ? android.R.color.white : R.color.home_text_primary));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean useHomeSelectionChrome() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void updateActionBar(final ActionBar actionBar) {
|
||||
if (actionBar == null) {
|
||||
return;
|
||||
}
|
||||
actionBar.hide();
|
||||
super.updateActionBar(actionBar);
|
||||
}
|
||||
|
||||
@@ -60,7 +166,7 @@ public class ArchivedConversationListActivity extends AbstractConversationListAc
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onOptionsItemSelected(MenuItem menuItem) {
|
||||
public boolean onOptionsItemSelected(final MenuItem menuItem) {
|
||||
if (menuItem.getItemId() == android.R.id.home) {
|
||||
onActionBarHome();
|
||||
return true;
|
||||
@@ -75,6 +181,12 @@ public class ArchivedConversationListActivity extends AbstractConversationListAc
|
||||
|
||||
@Override
|
||||
public boolean isSwipeAnimatable() {
|
||||
return false;
|
||||
return !isInConversationListSelectMode();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void finish() {
|
||||
super.finish();
|
||||
overridePendingTransition(R.anim.home_activity_hold, R.anim.home_activity_close);
|
||||
}
|
||||
}
|
||||
|
||||
+7
-19
@@ -18,11 +18,9 @@
|
||||
package com.android.messaging.ui.conversationlist;
|
||||
|
||||
import android.os.Bundle;
|
||||
import android.view.ActionMode;
|
||||
import android.window.SplashScreenView;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuItem;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.appcompat.app.ActionBar;
|
||||
import androidx.appcompat.widget.PopupMenu;
|
||||
@@ -36,6 +34,7 @@ public class ConversationListActivity extends AbstractConversationListActivity {
|
||||
@Override
|
||||
protected void onCreate(final Bundle savedInstanceState) {
|
||||
Trace.beginSection("ConversationListActivity.onCreate");
|
||||
getSplashScreen().setOnExitAnimationListener(SplashScreenView::remove);
|
||||
setTheme(R.style.BugleTheme_ConversationListActivity);
|
||||
super.onCreate(savedInstanceState);
|
||||
mConversationListFragment = ConversationListFragment.createConversationListFragment(null);
|
||||
@@ -52,20 +51,12 @@ public class ConversationListActivity extends AbstractConversationListActivity {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean shouldShowContextualActionBar(final ActionMode.Callback callback) {
|
||||
if (callback instanceof MultiSelectActionModeCallback) {
|
||||
return !((MultiSelectActionModeCallback) callback).useHomeSelectionChrome();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void updateActionBar(final ActionBar actionBar) {
|
||||
if (actionBar == null) {
|
||||
return;
|
||||
}
|
||||
// Custom Message+ chrome replaces the ActionBar on the home screen.
|
||||
// Custom Message Q chrome replaces the ActionBar on the home screen.
|
||||
actionBar.hide();
|
||||
super.updateActionBar(actionBar);
|
||||
}
|
||||
@@ -80,6 +71,9 @@ public class ConversationListActivity extends AbstractConversationListActivity {
|
||||
public void onBackPressed() {
|
||||
if (isInConversationListSelectMode()) {
|
||||
exitMultiSelectState();
|
||||
} else if (mConversationListFragment != null
|
||||
&& mConversationListFragment.onHomeBackPressed()) {
|
||||
return;
|
||||
} else {
|
||||
super.onBackPressed();
|
||||
}
|
||||
@@ -146,13 +140,7 @@ public class ConversationListActivity extends AbstractConversationListActivity {
|
||||
}
|
||||
|
||||
public void showAutomateComingSoon() {
|
||||
Toast.makeText(this, R.string.home_automate_coming_soon, Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
|
||||
public void enterHomeEditMode() {
|
||||
if (!isInConversationListSelectMode()) {
|
||||
startMultiSelectActionMode();
|
||||
}
|
||||
UIIntents.get().launchHomeAutomationActivity(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
+130
-9
@@ -32,6 +32,10 @@ import androidx.recyclerview.widget.RecyclerView;
|
||||
import com.android.messaging.R;
|
||||
import com.android.messaging.datamodel.data.ConversationListItemData;
|
||||
import com.android.messaging.ui.CursorRecyclerAdapter;
|
||||
import com.android.messaging.ui.home.HomeCategoryClassifier;
|
||||
import com.android.messaging.ui.home.HomeCategoryStore;
|
||||
import com.android.messaging.ui.home.HomeInboxFilter;
|
||||
import com.android.messaging.ui.home.HomeSubListSearch;
|
||||
import com.android.messaging.ui.home.PinnedConversationStore;
|
||||
|
||||
import java.util.ArrayList;
|
||||
@@ -49,14 +53,19 @@ public class ConversationListAdapter
|
||||
public static final int FILTER_ALL = 0;
|
||||
public static final int FILTER_UNREAD = 1;
|
||||
public static final int FILTER_GROUPS = 2;
|
||||
public static final int FILTER_WORK = 3;
|
||||
public static final int FILTER_PERSONAL = 4;
|
||||
|
||||
private static final int VIEW_TYPE_PINNED_HEADER = 1;
|
||||
private static final int VIEW_TYPE_CONVERSATION = 2;
|
||||
private static final long PINNED_HEADER_ITEM_ID = -9001L;
|
||||
static final Object PAYLOAD_SELECTION_CHROME = new Object();
|
||||
|
||||
private final ConversationListItemView.HostInterface mClivHostInterface;
|
||||
private final ConversationListItemData mFilterProbe = new ConversationListItemData();
|
||||
private int mFilterMode = FILTER_ALL;
|
||||
private String mCategoryId = HomeCategoryStore.ID_ALL;
|
||||
private String mSearchQuery = "";
|
||||
private int[] mDisplayPositions = new int[0];
|
||||
private View mPinnedStripRoot;
|
||||
private boolean mPinnedHeaderVisible;
|
||||
@@ -112,6 +121,93 @@ public class ConversationListAdapter
|
||||
return mFilterMode;
|
||||
}
|
||||
|
||||
public void setSearchQuery(final String query) {
|
||||
final String next = query == null ? "" : query.trim();
|
||||
if (next.equals(mSearchQuery)) {
|
||||
return;
|
||||
}
|
||||
mSearchQuery = next;
|
||||
rebuildFilterIndex();
|
||||
notifyDataSetChanged();
|
||||
}
|
||||
|
||||
public String getSearchQuery() {
|
||||
return mSearchQuery;
|
||||
}
|
||||
|
||||
public boolean hasSearchQuery() {
|
||||
return !mSearchQuery.isEmpty();
|
||||
}
|
||||
|
||||
public void setCategoryId(final String categoryId) {
|
||||
final String next = categoryId == null ? HomeCategoryStore.ID_ALL : categoryId;
|
||||
if (next.equals(mCategoryId)) {
|
||||
return;
|
||||
}
|
||||
mCategoryId = next;
|
||||
rebuildFilterIndex();
|
||||
notifyDataSetChanged();
|
||||
}
|
||||
|
||||
public String getCategoryId() {
|
||||
return mCategoryId;
|
||||
}
|
||||
|
||||
public void refreshCategoryFilter() {
|
||||
rebuildFilterIndex();
|
||||
notifyDataSetChanged();
|
||||
}
|
||||
|
||||
public List<ConversationListItemData> snapshotConversations() {
|
||||
final Cursor cursor = getCursor();
|
||||
if (cursor == null || cursor.getCount() == 0) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
final List<ConversationListItemData> items = new ArrayList<>(cursor.getCount());
|
||||
final int saved = cursor.getPosition();
|
||||
for (int i = 0; i < cursor.getCount(); i++) {
|
||||
if (!cursor.moveToPosition(i)) {
|
||||
continue;
|
||||
}
|
||||
final ConversationListItemData item = new ConversationListItemData();
|
||||
item.bind(cursor);
|
||||
items.add(item);
|
||||
}
|
||||
if (saved >= 0 && saved < cursor.getCount()) {
|
||||
cursor.moveToPosition(saved);
|
||||
}
|
||||
return items;
|
||||
}
|
||||
|
||||
/** Visible (filtered) conversation rows, not the raw cursor. */
|
||||
public List<ConversationListItemData> snapshotVisibleConversations() {
|
||||
final Cursor cursor = getCursor();
|
||||
if (cursor == null || mDisplayPositions.length == 0) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
final List<ConversationListItemData> items = new ArrayList<>(mDisplayPositions.length);
|
||||
final int saved = cursor.getPosition();
|
||||
for (final int position : mDisplayPositions) {
|
||||
if (!cursor.moveToPosition(position)) {
|
||||
continue;
|
||||
}
|
||||
final ConversationListItemData item = new ConversationListItemData();
|
||||
item.bind(cursor);
|
||||
items.add(item);
|
||||
}
|
||||
if (saved >= 0 && saved < cursor.getCount()) {
|
||||
cursor.moveToPosition(saved);
|
||||
}
|
||||
return items;
|
||||
}
|
||||
|
||||
public void notifySelectionChromeChanged() {
|
||||
final int count = getItemCount();
|
||||
if (count > 0) {
|
||||
notifyItemRangeChanged(0, count, PAYLOAD_SELECTION_CHROME);
|
||||
}
|
||||
}
|
||||
|
||||
public void refreshPinOrdering() {
|
||||
refreshPinOrderingAnimated();
|
||||
}
|
||||
@@ -237,16 +333,25 @@ public class ConversationListAdapter
|
||||
}
|
||||
|
||||
private boolean passesFilter() {
|
||||
if (mFilterMode == FILTER_ALL) {
|
||||
return true;
|
||||
if (mFilterMode == FILTER_UNREAD && mFilterProbe.getIsRead()) {
|
||||
return false;
|
||||
}
|
||||
if (mFilterMode == FILTER_UNREAD) {
|
||||
return !mFilterProbe.getIsRead();
|
||||
if (mFilterMode == FILTER_GROUPS && mFilterProbe.getParticipantCount() <= 1) {
|
||||
return false;
|
||||
}
|
||||
if (mFilterMode == FILTER_GROUPS) {
|
||||
return mFilterProbe.getParticipantCount() > 1;
|
||||
if (mFilterMode == FILTER_WORK && !HomeInboxFilter.matchesWork(mContext, mFilterProbe)) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
if (mFilterMode == FILTER_PERSONAL
|
||||
&& !HomeInboxFilter.matchesPersonal(mContext, mFilterProbe)) {
|
||||
return false;
|
||||
}
|
||||
if (!HomeSubListSearch.matches(mSearchQuery,
|
||||
mFilterProbe.getName(), mFilterProbe.getSnippetText(),
|
||||
mFilterProbe.getSnippetSenderName())) {
|
||||
return false;
|
||||
}
|
||||
return HomeCategoryClassifier.matches(mContext, mCategoryId, mFilterProbe);
|
||||
}
|
||||
|
||||
private void sortPinnedFirst(final ArrayList<Integer> positions, final Cursor cursor) {
|
||||
@@ -323,6 +428,22 @@ public class ConversationListAdapter
|
||||
return createConversationViewHolder(parent);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBindViewHolder(@NonNull final RecyclerView.ViewHolder holder,
|
||||
final int position, @NonNull final List<Object> payloads) {
|
||||
if (!payloads.isEmpty() && payloads.contains(PAYLOAD_SELECTION_CHROME)) {
|
||||
if (holder instanceof PinnedHeaderViewHolder) {
|
||||
attachPinnedStripToHeader((PinnedHeaderViewHolder) holder);
|
||||
return;
|
||||
}
|
||||
if (holder instanceof ConversationListViewHolder) {
|
||||
((ConversationListViewHolder) holder).mView.rebindSelectionChrome();
|
||||
return;
|
||||
}
|
||||
}
|
||||
onBindViewHolder(holder, position);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBindViewHolder(@NonNull final RecyclerView.ViewHolder holder,
|
||||
final int position) {
|
||||
@@ -348,8 +469,8 @@ public class ConversationListAdapter
|
||||
container.setLayoutParams(new RecyclerView.LayoutParams(
|
||||
ViewGroup.LayoutParams.MATCH_PARENT,
|
||||
ViewGroup.LayoutParams.WRAP_CONTENT));
|
||||
container.setClipChildren(true);
|
||||
container.setClipToPadding(true);
|
||||
container.setClipChildren(false);
|
||||
container.setClipToPadding(false);
|
||||
return new PinnedHeaderViewHolder(container);
|
||||
}
|
||||
|
||||
|
||||
+2184
-292
File diff suppressed because it is too large
Load Diff
+409
-139
@@ -20,14 +20,21 @@ import android.content.Context;
|
||||
import android.content.res.Resources;
|
||||
import android.database.Cursor;
|
||||
import android.graphics.Color;
|
||||
import android.graphics.Paint;
|
||||
import android.graphics.Rect;
|
||||
import android.graphics.Typeface;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.net.Uri;
|
||||
import android.support.v7.mms.pdu.ContentType;
|
||||
import android.text.Layout;
|
||||
import android.text.SpannableString;
|
||||
import android.text.Spanned;
|
||||
import android.text.StaticLayout;
|
||||
import android.text.TextPaint;
|
||||
import android.text.TextUtils;
|
||||
import android.text.style.LeadingMarginSpan;
|
||||
import android.telephony.PhoneNumberUtils;
|
||||
import android.util.AttributeSet;
|
||||
import android.util.TypedValue;
|
||||
import android.view.View;
|
||||
import android.view.View.OnClickListener;
|
||||
import android.view.View.OnLayoutChangeListener;
|
||||
@@ -39,7 +46,6 @@ import android.widget.ImageButton;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.core.content.res.ResourcesCompat;
|
||||
import androidx.core.text.BidiFormatter;
|
||||
import androidx.core.text.TextDirectionHeuristicsCompat;
|
||||
|
||||
@@ -47,7 +53,9 @@ import com.android.messaging.Factory;
|
||||
import com.android.messaging.R;
|
||||
import com.android.messaging.datamodel.MessagingContentProvider;
|
||||
import com.android.messaging.datamodel.action.UpdateConversationArchiveStatusAction;
|
||||
import com.android.messaging.datamodel.action.UpdateConversationTrashStatusAction;
|
||||
import com.android.messaging.datamodel.data.ConversationListItemData;
|
||||
import com.android.messaging.datamodel.data.ConversationMessageData;
|
||||
import com.android.messaging.datamodel.data.MessageData;
|
||||
import com.android.messaging.datamodel.media.UriImageRequestDescriptor;
|
||||
import com.android.messaging.sms.MmsUtils;
|
||||
@@ -56,9 +64,13 @@ import com.android.messaging.ui.AudioAttachmentView;
|
||||
import com.android.messaging.ui.ContactIconView;
|
||||
import com.android.messaging.ui.SnackBar;
|
||||
import com.android.messaging.ui.SnackBarInteraction;
|
||||
import com.android.messaging.ui.home.HomeAvatarOverlay;
|
||||
import com.android.messaging.ui.home.HomeToast;
|
||||
import com.android.messaging.ui.home.MutedConversationStore;
|
||||
import com.android.messaging.util.Assert;
|
||||
import com.android.messaging.util.ImageUtils;
|
||||
import com.android.messaging.util.PhoneUtils;
|
||||
import com.android.messaging.util.SimNameUtil;
|
||||
import com.android.messaging.util.Typefaces;
|
||||
import com.android.messaging.util.UiUtils;
|
||||
import com.android.messaging.util.UriUtil;
|
||||
@@ -72,8 +84,9 @@ import java.util.List;
|
||||
*/
|
||||
public class ConversationListItemView extends FrameLayout implements OnClickListener,
|
||||
OnLongClickListener, OnLayoutChangeListener {
|
||||
static final int UNREAD_SNIPPET_LINE_COUNT = 2;
|
||||
static final int NO_UNREAD_SNIPPET_LINE_COUNT = 2;
|
||||
static final int UNREAD_SNIPPET_LINE_COUNT = 3;
|
||||
static final int NO_UNREAD_SNIPPET_LINE_COUNT = 3;
|
||||
private static final int SNIPPET_ELLIPSIS_RESERVE_CHARS = 5;
|
||||
private static final int LIST_DIVIDER_INSET_DEFAULT_DP = 74;
|
||||
private static final int LIST_DIVIDER_INSET_SELECTION_DP = 104;
|
||||
private int mListItemReadColor;
|
||||
@@ -87,6 +100,8 @@ public class ConversationListItemView extends FrameLayout implements OnClickList
|
||||
boolean isConversationSelected(final String conversationId);
|
||||
void onConversationClicked(final ConversationListItemData conversationListItemData,
|
||||
boolean isLongClick, final ConversationListItemView conversationView);
|
||||
void onConversationAvatarClicked(final ConversationListItemData conversationListItemData,
|
||||
final View avatarView);
|
||||
boolean isSwipeAnimatable();
|
||||
List<SnackBarInteraction> getSnackBarInteractions();
|
||||
void startFullScreenPhotoViewer(final Uri initialPhoto, final Rect initialPhotoBounds,
|
||||
@@ -96,6 +111,8 @@ public class ConversationListItemView extends FrameLayout implements OnClickList
|
||||
boolean shouldShowConversationPinButton();
|
||||
boolean isConversationPinned(final String conversationId);
|
||||
void onConversationPinButtonClicked(final String conversationId);
|
||||
void onConversationSwipeMuteClicked(ConversationListItemData data);
|
||||
void onConversationSwipeDeleteClicked(ConversationListItemData data);
|
||||
}
|
||||
|
||||
private final OnClickListener fullScreenPreviewClickListener = new OnClickListener() {
|
||||
@@ -142,8 +159,11 @@ public class ConversationListItemView extends FrameLayout implements OnClickList
|
||||
private ContactIconView mContactIconView;
|
||||
private ImageView mContactCheckmarkView;
|
||||
private ImageView mFailedStatusIconView;
|
||||
private ImageView mMuteBadgeView;
|
||||
private ImageView mCrossSwipeArchiveLeftImageView;
|
||||
private ImageView mCrossSwipeArchiveRightImageView;
|
||||
private View mCrossSwipeRightActions;
|
||||
private ImageView mCrossSwipeMuteView;
|
||||
private ImageView mCrossSwipeDeleteView;
|
||||
private AsyncImageView mImagePreviewView;
|
||||
private AudioAttachmentView mAudioAttachmentView;
|
||||
private ImageView mChevronView;
|
||||
@@ -153,6 +173,7 @@ public class ConversationListItemView extends FrameLayout implements OnClickList
|
||||
|
||||
private static final int SELECTION_TOGGLE_ANIM_MS = 180;
|
||||
private String mSelectionStateConversationId;
|
||||
private Boolean mSelectionStateMode;
|
||||
private Boolean mSelectionStateChecked;
|
||||
|
||||
public ConversationListItemView(final Context context, final AttributeSet attrs) {
|
||||
@@ -181,8 +202,27 @@ public class ConversationListItemView extends FrameLayout implements OnClickList
|
||||
mContactIconView = findViewById(R.id.conversation_icon);
|
||||
mContactCheckmarkView = findViewById(R.id.conversation_checkmark);
|
||||
mFailedStatusIconView = findViewById(R.id.conversation_failed_status_icon);
|
||||
mMuteBadgeView = findViewById(R.id.conversation_mute_badge);
|
||||
mCrossSwipeArchiveLeftImageView = findViewById(R.id.crossSwipeArchiveIconLeft);
|
||||
mCrossSwipeArchiveRightImageView = findViewById(R.id.crossSwipeArchiveIconRight);
|
||||
mCrossSwipeRightActions = findViewById(R.id.cross_swipe_right_actions);
|
||||
mCrossSwipeMuteView = findViewById(R.id.cross_swipe_mute);
|
||||
mCrossSwipeDeleteView = findViewById(R.id.cross_swipe_delete);
|
||||
if (mCrossSwipeMuteView != null) {
|
||||
mCrossSwipeMuteView.setOnClickListener(v -> {
|
||||
if (mHostInterface != null) {
|
||||
mHostInterface.onConversationSwipeMuteClicked(mData);
|
||||
}
|
||||
setSwipeTranslationX(0);
|
||||
});
|
||||
}
|
||||
if (mCrossSwipeDeleteView != null) {
|
||||
mCrossSwipeDeleteView.setOnClickListener(v -> {
|
||||
if (mHostInterface != null) {
|
||||
mHostInterface.onConversationSwipeDeleteClicked(mData);
|
||||
}
|
||||
setSwipeTranslationX(0);
|
||||
});
|
||||
}
|
||||
mImagePreviewView = findViewById(R.id.conversation_image_preview);
|
||||
mAudioAttachmentView = findViewById(R.id.audio_attachment_view);
|
||||
mChevronView = findViewById(R.id.conversation_chevron);
|
||||
@@ -190,6 +230,9 @@ public class ConversationListItemView extends FrameLayout implements OnClickList
|
||||
mListDividerView = findViewById(R.id.conversation_list_divider);
|
||||
mConversationNameView.addOnLayoutChangeListener(this);
|
||||
mSnippetTextView.addOnLayoutChangeListener(this);
|
||||
if (mCategoryChipView != null) {
|
||||
mCategoryChipView.addOnLayoutChangeListener(this);
|
||||
}
|
||||
|
||||
final Resources resources = getContext().getResources();
|
||||
final Resources.Theme theme = getContext().getTheme();
|
||||
@@ -212,28 +255,32 @@ public class ConversationListItemView extends FrameLayout implements OnClickList
|
||||
setSnippet();
|
||||
} else if (v == mSubjectTextView) {
|
||||
setSubject();
|
||||
} else if (v == mCategoryChipView) {
|
||||
alignSimBadgeToFirstSnippetLine();
|
||||
}
|
||||
}
|
||||
|
||||
private void setWorkProfileIcon() {
|
||||
mWorkProfileIconView.setVisibility(mData.isEnterprise() ? View.VISIBLE : View.GONE);
|
||||
if (mCategoryChipView != null) {
|
||||
final boolean selectionMode =
|
||||
mHostInterface != null && mHostInterface.isSelectionMode();
|
||||
if (selectionMode) {
|
||||
mCategoryChipView.setVisibility(View.GONE);
|
||||
} else {
|
||||
mCategoryChipView.setVisibility(View.VISIBLE);
|
||||
mCategoryChipView.setText(mData.isEnterprise()
|
||||
? R.string.home_chip_work
|
||||
: R.string.home_chip_personal);
|
||||
}
|
||||
setSimBadge();
|
||||
}
|
||||
|
||||
private void setSimBadge() {
|
||||
if (mCategoryChipView == null) {
|
||||
return;
|
||||
}
|
||||
if (!SimNameUtil.shouldShowSimName(getContext())) {
|
||||
mCategoryChipView.setText(null);
|
||||
mCategoryChipView.setVisibility(GONE);
|
||||
mCategoryChipView.setTranslationY(0f);
|
||||
return;
|
||||
}
|
||||
mCategoryChipView.setText(SimNameUtil.getSimDisplayName(getContext(), mData.getSelfId()));
|
||||
mCategoryChipView.setVisibility(VISIBLE);
|
||||
alignSimBadgeToFirstSnippetLine();
|
||||
}
|
||||
|
||||
private void setConversationName() {
|
||||
final boolean selectionMode =
|
||||
mHostInterface != null && mHostInterface.isSelectionMode();
|
||||
final boolean showUnreadDot = !mData.getIsRead() && !mData.getShowDraft();
|
||||
if (mUnreadDotSlotView != null) {
|
||||
mUnreadDotSlotView.setVisibility(View.VISIBLE);
|
||||
@@ -241,10 +288,7 @@ public class ConversationListItemView extends FrameLayout implements OnClickList
|
||||
if (mUnreadDotView != null) {
|
||||
mUnreadDotView.setVisibility(showUnreadDot ? View.VISIBLE : View.INVISIBLE);
|
||||
}
|
||||
if (selectionMode) {
|
||||
mConversationNameView.setTextColor(mListItemReadColor);
|
||||
mConversationNameView.setTypeface(mListItemUnreadTypeface);
|
||||
} else if (mData.getIsRead() || mData.getShowDraft()) {
|
||||
if (mData.getIsRead() || mData.getShowDraft()) {
|
||||
mConversationNameView.setTextColor(mListItemReadColor);
|
||||
mConversationNameView.setTypeface(mListItemReadTypeface);
|
||||
} else {
|
||||
@@ -253,6 +297,8 @@ public class ConversationListItemView extends FrameLayout implements OnClickList
|
||||
}
|
||||
|
||||
final String conversationName = mData.getName();
|
||||
mConversationNameView.setTextSize(TypedValue.COMPLEX_UNIT_SP,
|
||||
looksLikePhoneNumberName(conversationName) ? 18 : 17);
|
||||
|
||||
// For group conversations, ellipsize the group members that do not fit
|
||||
final CharSequence ellipsizedName = UiUtils.commaEllipsize(
|
||||
@@ -270,6 +316,23 @@ public class ConversationListItemView extends FrameLayout implements OnClickList
|
||||
mConversationNameView.setText(bidiFormattedName);
|
||||
}
|
||||
|
||||
private static boolean looksLikePhoneNumberName(final String name) {
|
||||
if (TextUtils.isEmpty(name)) {
|
||||
return false;
|
||||
}
|
||||
final String digits = PhoneNumberUtils.normalizeNumber(name);
|
||||
if (digits.length() < 6) {
|
||||
return false;
|
||||
}
|
||||
for (int i = 0; i < digits.length(); i++) {
|
||||
final char c = digits.charAt(i);
|
||||
if (c != '+' && !Character.isDigit(c)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private static String getPlusOneString() {
|
||||
if (sPlusOneString == null) {
|
||||
sPlusOneString = Factory.get().getApplicationContext().getResources()
|
||||
@@ -287,100 +350,174 @@ public class ConversationListItemView extends FrameLayout implements OnClickList
|
||||
}
|
||||
|
||||
private void setSubject() {
|
||||
final String subjectText = mData.getShowDraft() ?
|
||||
mData.getDraftSubject() :
|
||||
MmsUtils.cleanseMmsSubject(getContext().getResources(), mData.getSubject());
|
||||
if (!TextUtils.isEmpty(subjectText)) {
|
||||
final String subjectPrepend = getResources().getString(R.string.subject_label);
|
||||
mSubjectTextView.setText(TextUtils.concat(subjectPrepend, subjectText));
|
||||
mSubjectTextView.setVisibility(VISIBLE);
|
||||
} else {
|
||||
if (mSubjectTextView != null) {
|
||||
mSubjectTextView.setVisibility(GONE);
|
||||
}
|
||||
}
|
||||
|
||||
private void setSnippet() {
|
||||
mSnippetTextView.setText(getSnippetText());
|
||||
final String snippetText = ellipsizeSnippetForMaxLines(getSnippetText());
|
||||
final int indent = getSnippetChipIndent();
|
||||
if (indent <= 0) {
|
||||
mSnippetTextView.setText(snippetText);
|
||||
} else {
|
||||
final SpannableString spanned = new SpannableString(snippetText == null ? "" : snippetText);
|
||||
spanned.setSpan(new LeadingMarginSpan.Standard(indent, 0),
|
||||
0, spanned.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||
mSnippetTextView.setText(spanned);
|
||||
}
|
||||
alignSimBadgeToFirstSnippetLine();
|
||||
}
|
||||
|
||||
private void alignSimBadgeToFirstSnippetLine() {
|
||||
if (mCategoryChipView == null) {
|
||||
return;
|
||||
}
|
||||
if (mCategoryChipView.getVisibility() != VISIBLE || mSnippetTextView == null) {
|
||||
mCategoryChipView.setTranslationY(0f);
|
||||
return;
|
||||
}
|
||||
int chipHeight = mCategoryChipView.getHeight();
|
||||
if (chipHeight <= 0) {
|
||||
mCategoryChipView.measure(
|
||||
View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED),
|
||||
View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED));
|
||||
chipHeight = mCategoryChipView.getMeasuredHeight();
|
||||
}
|
||||
if (chipHeight <= 0) {
|
||||
return;
|
||||
}
|
||||
final Paint.FontMetricsInt fm = mSnippetTextView.getPaint().getFontMetricsInt();
|
||||
final int textHeight = fm.descent - fm.ascent;
|
||||
int textTop = mSnippetTextView.getPaddingTop();
|
||||
final Layout layout = mSnippetTextView.getLayout();
|
||||
if (layout != null && layout.getLineCount() > 0) {
|
||||
textTop = mSnippetTextView.getPaddingTop() + layout.getLineBaseline(0) + fm.ascent;
|
||||
}
|
||||
mCategoryChipView.setTranslationY(textTop + (textHeight - chipHeight) / 2f);
|
||||
}
|
||||
|
||||
private int getSnippetChipIndent() {
|
||||
if (mCategoryChipView == null || mCategoryChipView.getVisibility() != VISIBLE) {
|
||||
return 0;
|
||||
}
|
||||
int width = mCategoryChipView.getWidth();
|
||||
if (width <= 0) {
|
||||
mCategoryChipView.measure(
|
||||
View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED),
|
||||
View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED));
|
||||
width = mCategoryChipView.getMeasuredWidth();
|
||||
}
|
||||
if (width <= 0) {
|
||||
return 0;
|
||||
}
|
||||
return width + getResources().getDimensionPixelSize(R.dimen.looply_space_sm);
|
||||
}
|
||||
|
||||
private String ellipsizeSnippetForMaxLines(final String source) {
|
||||
if (TextUtils.isEmpty(source)) {
|
||||
return source;
|
||||
}
|
||||
final int maxLines = mSnippetTextView.getMaxLines();
|
||||
final int width = mSnippetTextView.getWidth()
|
||||
- mSnippetTextView.getPaddingLeft() - mSnippetTextView.getPaddingRight();
|
||||
if (maxLines <= 0 || width <= 0) {
|
||||
return source;
|
||||
}
|
||||
final int indent = getSnippetChipIndent();
|
||||
final CharSequence measured;
|
||||
if (indent > 0) {
|
||||
final SpannableString spanned = new SpannableString(source);
|
||||
spanned.setSpan(new LeadingMarginSpan.Standard(indent, 0),
|
||||
0, spanned.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||
measured = spanned;
|
||||
} else {
|
||||
measured = source;
|
||||
}
|
||||
final StaticLayout layout = StaticLayout.Builder
|
||||
.obtain(measured, 0, measured.length(), mSnippetTextView.getPaint(), width)
|
||||
.setMaxLines(maxLines + 1)
|
||||
.setEllipsize(null)
|
||||
.setIncludePad(false)
|
||||
.setLineSpacing(mSnippetTextView.getLineSpacingExtra(),
|
||||
mSnippetTextView.getLineSpacingMultiplier())
|
||||
.build();
|
||||
if (layout.getLineCount() <= maxLines) {
|
||||
return source;
|
||||
}
|
||||
final int last = maxLines - 1;
|
||||
final int lineStart = layout.getLineStart(last);
|
||||
int cut = layout.getLineEnd(last);
|
||||
while (cut > lineStart && Character.isWhitespace(source.charAt(cut - 1))) {
|
||||
cut--;
|
||||
}
|
||||
int reserved = 0;
|
||||
while (cut > lineStart && reserved < SNIPPET_ELLIPSIS_RESERVE_CHARS) {
|
||||
cut -= Character.charCount(Character.codePointBefore(source, cut));
|
||||
reserved++;
|
||||
}
|
||||
while (cut > lineStart && Character.isWhitespace(source.charAt(cut - 1))) {
|
||||
cut--;
|
||||
}
|
||||
if (cut <= 0) {
|
||||
return "...";
|
||||
}
|
||||
return source.substring(0, cut) + "...";
|
||||
}
|
||||
|
||||
private void updateSelectionIndicator(final boolean selectionMode, final boolean isSelected,
|
||||
final Resources resources) {
|
||||
if (mSelectionIndicatorView == null || mSelectionRingView == null
|
||||
|| mSelectionCheckView == null) {
|
||||
return;
|
||||
}
|
||||
if (!selectionMode) {
|
||||
if (mSelectionIndicatorView != null) {
|
||||
mSelectionIndicatorView.animate().cancel();
|
||||
mSelectionCheckView.animate().cancel();
|
||||
mSelectionIndicatorView.setScaleX(1f);
|
||||
mSelectionIndicatorView.setScaleY(1f);
|
||||
mSelectionCheckView.setAlpha(1f);
|
||||
mSelectionCheckView.setScaleX(1f);
|
||||
mSelectionCheckView.setScaleY(1f);
|
||||
mSelectionIndicatorView.setVisibility(GONE);
|
||||
mSelectionStateConversationId = null;
|
||||
mSelectionStateChecked = null;
|
||||
}
|
||||
if (mContactCheckmarkView == null) {
|
||||
return;
|
||||
}
|
||||
mSelectionIndicatorView.setVisibility(VISIBLE);
|
||||
final String conversationId = mData.getConversationId();
|
||||
final boolean sameConversation = conversationId != null
|
||||
&& conversationId.equals(mSelectionStateConversationId);
|
||||
final boolean animateToggle = sameConversation && mSelectionStateChecked != null
|
||||
&& mSelectionStateChecked.booleanValue() != isSelected;
|
||||
final boolean wasMode = Boolean.TRUE.equals(mSelectionStateMode);
|
||||
final boolean wasSelected = Boolean.TRUE.equals(mSelectionStateChecked);
|
||||
final boolean animateToggle = sameConversation
|
||||
&& (wasMode != selectionMode || wasSelected != isSelected);
|
||||
mSelectionStateConversationId = conversationId;
|
||||
mSelectionStateMode = selectionMode;
|
||||
mSelectionStateChecked = isSelected;
|
||||
|
||||
mSelectionIndicatorView.animate().cancel();
|
||||
mSelectionCheckView.animate().cancel();
|
||||
|
||||
if (isSelected) {
|
||||
mSelectionRingView.setBackground(resources.getDrawable(
|
||||
R.drawable.home_bg_selection_ring_checked, getContext().getTheme()));
|
||||
mSelectionCheckView.setVisibility(VISIBLE);
|
||||
if (animateToggle) {
|
||||
mSelectionIndicatorView.setScaleX(0.88f);
|
||||
mSelectionIndicatorView.setScaleY(0.88f);
|
||||
mSelectionIndicatorView.animate()
|
||||
.scaleX(1f)
|
||||
.scaleY(1f)
|
||||
mContactCheckmarkView.animate().cancel();
|
||||
if (!selectionMode) {
|
||||
if (animateToggle && mContactCheckmarkView.getVisibility() == VISIBLE) {
|
||||
mContactCheckmarkView.animate()
|
||||
.alpha(0f)
|
||||
.scaleX(0.6f)
|
||||
.scaleY(0.6f)
|
||||
.setDuration(SELECTION_TOGGLE_ANIM_MS)
|
||||
.setInterpolator(new OvershootInterpolator(1.6f));
|
||||
mSelectionCheckView.setAlpha(0f);
|
||||
mSelectionCheckView.setScaleX(0.4f);
|
||||
mSelectionCheckView.setScaleY(0.4f);
|
||||
mSelectionCheckView.animate()
|
||||
.alpha(1f)
|
||||
.scaleX(1f)
|
||||
.scaleY(1f)
|
||||
.setDuration(SELECTION_TOGGLE_ANIM_MS)
|
||||
.setInterpolator(UiUtils.DEFAULT_INTERPOLATOR);
|
||||
.setInterpolator(UiUtils.DEFAULT_INTERPOLATOR)
|
||||
.withEndAction(() ->
|
||||
HomeAvatarOverlay.apply(mContactCheckmarkView, false, false))
|
||||
.start();
|
||||
} else {
|
||||
mSelectionIndicatorView.setScaleX(1f);
|
||||
mSelectionIndicatorView.setScaleY(1f);
|
||||
mSelectionCheckView.setAlpha(1f);
|
||||
mSelectionCheckView.setScaleX(1f);
|
||||
mSelectionCheckView.setScaleY(1f);
|
||||
HomeAvatarOverlay.apply(mContactCheckmarkView, false, false);
|
||||
}
|
||||
return;
|
||||
}
|
||||
HomeAvatarOverlay.apply(mContactCheckmarkView, true, isSelected);
|
||||
if (animateToggle && isSelected && !wasSelected) {
|
||||
mContactCheckmarkView.setAlpha(0f);
|
||||
mContactCheckmarkView.setScaleX(0.6f);
|
||||
mContactCheckmarkView.setScaleY(0.6f);
|
||||
mContactCheckmarkView.animate()
|
||||
.alpha(1f)
|
||||
.scaleX(1f)
|
||||
.scaleY(1f)
|
||||
.setDuration(SELECTION_TOGGLE_ANIM_MS)
|
||||
.setInterpolator(new OvershootInterpolator(1.6f))
|
||||
.start();
|
||||
} else {
|
||||
mSelectionRingView.setBackground(resources.getDrawable(
|
||||
R.drawable.home_bg_selection_ring, getContext().getTheme()));
|
||||
mSelectionCheckView.setVisibility(GONE);
|
||||
mSelectionCheckView.setAlpha(1f);
|
||||
mSelectionCheckView.setScaleX(1f);
|
||||
mSelectionCheckView.setScaleY(1f);
|
||||
if (animateToggle) {
|
||||
mSelectionIndicatorView.setScaleX(0.92f);
|
||||
mSelectionIndicatorView.setScaleY(0.92f);
|
||||
mSelectionIndicatorView.animate()
|
||||
.scaleX(1f)
|
||||
.scaleY(1f)
|
||||
.setDuration(SELECTION_TOGGLE_ANIM_MS)
|
||||
.setInterpolator(UiUtils.DEFAULT_INTERPOLATOR);
|
||||
} else {
|
||||
mSelectionIndicatorView.setScaleX(1f);
|
||||
mSelectionIndicatorView.setScaleY(1f);
|
||||
}
|
||||
mContactCheckmarkView.setAlpha(1f);
|
||||
mContactCheckmarkView.setScaleX(1f);
|
||||
mContactCheckmarkView.setScaleY(1f);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -421,8 +558,7 @@ public class ConversationListItemView extends FrameLayout implements OnClickList
|
||||
}
|
||||
final ViewGroup.MarginLayoutParams layoutParams =
|
||||
(ViewGroup.MarginLayoutParams) mListDividerView.getLayoutParams();
|
||||
final int insetDp = selectionMode ? LIST_DIVIDER_INSET_SELECTION_DP
|
||||
: LIST_DIVIDER_INSET_DEFAULT_DP;
|
||||
final int insetDp = LIST_DIVIDER_INSET_DEFAULT_DP;
|
||||
final float density = getResources().getDisplayMetrics().density;
|
||||
final int insetPx = (int) (insetDp * density + 0.5f);
|
||||
if (layoutParams.leftMargin != insetPx) {
|
||||
@@ -561,34 +697,27 @@ public class ConversationListItemView extends FrameLayout implements OnClickList
|
||||
final String snippetText = getSnippetText();
|
||||
|
||||
final boolean selectionMode = mHostInterface.isSelectionMode();
|
||||
if (selectionMode) {
|
||||
maxLines = 0;
|
||||
typeface = mListItemReadTypeface;
|
||||
} else if (mData.getIsRead() || mData.getShowDraft()) {
|
||||
if (mData.getIsRead() || mData.getShowDraft()) {
|
||||
maxLines = TextUtils.isEmpty(snippetText) ? 0 : NO_UNREAD_SNIPPET_LINE_COUNT;
|
||||
typeface = mListItemReadTypeface;
|
||||
} else {
|
||||
maxLines = TextUtils.isEmpty(snippetText) ? 0 : UNREAD_SNIPPET_LINE_COUNT;
|
||||
typeface = mListItemUnreadTypeface;
|
||||
}
|
||||
color = getResources().getColor(R.color.home_text_secondary, getContext().getTheme());
|
||||
color = getResources().getColor(R.color.home_text_snippet, getContext().getTheme());
|
||||
|
||||
mSnippetTextView.setMaxLines(maxLines);
|
||||
mSnippetTextView.setEllipsize(TextUtils.TruncateAt.END);
|
||||
mSnippetTextView.setTextColor(color);
|
||||
mSnippetTextView.setTypeface(typeface, typefaceStyle);
|
||||
mSubjectTextView.setTextColor(color);
|
||||
mSubjectTextView.setTypeface(typeface, typefaceStyle);
|
||||
|
||||
if (selectionMode) {
|
||||
mSnippetTextView.setVisibility(GONE);
|
||||
mSubjectTextView.setVisibility(GONE);
|
||||
} else {
|
||||
mSnippetTextView.setVisibility(VISIBLE);
|
||||
setSnippet();
|
||||
setSubject();
|
||||
}
|
||||
setConversationName();
|
||||
mSnippetTextView.setVisibility(VISIBLE);
|
||||
setWorkProfileIcon();
|
||||
setSnippet();
|
||||
setSubject();
|
||||
setConversationName();
|
||||
setContentDescription(buildContentDescription(resources, mData,
|
||||
mConversationNameView.getPaint()));
|
||||
|
||||
@@ -616,9 +745,13 @@ public class ConversationListItemView extends FrameLayout implements OnClickList
|
||||
mTimestampTextView.setText(resources.getString(
|
||||
R.string.conversation_list_item_view_draft_message));
|
||||
} else {
|
||||
mTimestampTextView.setTextColor(resources.getColor(R.color.home_text_secondary,
|
||||
getContext().getTheme()));
|
||||
mTimestampTextView.setTypeface(mListItemReadTypeface, typefaceStyle);
|
||||
final boolean unread = !mData.getIsRead();
|
||||
mTimestampTextView.setTextColor(unread
|
||||
? color
|
||||
: resources.getColor(R.color.home_text_secondary, getContext().getTheme()));
|
||||
mTimestampTextView.setTypeface(
|
||||
unread ? mListItemUnreadTypeface : mListItemReadTypeface,
|
||||
unread ? Typeface.BOLD : typefaceStyle);
|
||||
final String formattedTimestamp = mData.getFormattedTimestamp();
|
||||
if (mData.getIsSendRequested()) {
|
||||
mTimestampTextView.setText(R.string.message_status_sending);
|
||||
@@ -631,7 +764,6 @@ public class ConversationListItemView extends FrameLayout implements OnClickList
|
||||
setSelected(isSelected);
|
||||
Uri iconUri = null;
|
||||
int contactIconVisibility = VISIBLE;
|
||||
int checkmarkVisiblity = GONE;
|
||||
int failStatusVisiblity = GONE;
|
||||
if (!isSelected) {
|
||||
// Only show the fail icon if it is not a group conversation.
|
||||
@@ -647,12 +779,19 @@ public class ConversationListItemView extends FrameLayout implements OnClickList
|
||||
mContactIconView.setImageResourceUri(iconUri, mData.getParticipantContactId(),
|
||||
mData.getParticipantLookupKey(), mData.getOtherParticipantNormalizedDestination());
|
||||
mContactIconView.setVisibility(contactIconVisibility);
|
||||
mContactIconView.setOnLongClickListener(this);
|
||||
mContactIconView.setClickable(!mHostInterface.isSelectionMode());
|
||||
mContactIconView.setLongClickable(!mHostInterface.isSelectionMode());
|
||||
mContactIconView.setImageClickHandlerDisabled(true);
|
||||
bindAvatarClickTargets(selectionMode);
|
||||
|
||||
mContactCheckmarkView.setVisibility(checkmarkVisiblity);
|
||||
mFailedStatusIconView.setVisibility(failStatusVisiblity);
|
||||
final boolean muted = MutedConversationStore.isMuted(getContext(),
|
||||
mData.getConversationId());
|
||||
final boolean trashed = mData.getIsTrashed();
|
||||
if (mMuteBadgeView != null) {
|
||||
mMuteBadgeView.setVisibility(
|
||||
muted && !trashed && failStatusVisiblity != VISIBLE && !isSelected
|
||||
? VISIBLE : GONE);
|
||||
}
|
||||
updateSwipeMuteButton(muted, trashed);
|
||||
|
||||
updatePinButton(selectionMode, resources);
|
||||
updateListDividerInset(selectionMode);
|
||||
@@ -691,22 +830,36 @@ public class ConversationListItemView extends FrameLayout implements OnClickList
|
||||
ImageUtils.DEFAULT_CIRCLE_STROKE_COLOR /* circleStrokeColor */));
|
||||
mImagePreviewView.setOnLongClickListener(this);
|
||||
mImagePreviewView.setVisibility(previewImageVisibility);
|
||||
mImagePreviewView.setOnClickListener(previewClickListener);
|
||||
mImagePreviewView.setOnClickListener(selectionMode
|
||||
? v -> processClick(mSwipeableContainer, false)
|
||||
: previewClickListener);
|
||||
mAudioAttachmentView.setOnLongClickListener(this);
|
||||
mAudioAttachmentView.setVisibility(audioPreviewVisiblity);
|
||||
|
||||
Drawable archiveDrawable = ResourcesCompat.getDrawable(
|
||||
getResources(), R.drawable.ic_archive_small_dark, getContext().getTheme());
|
||||
if (PrefsUtils.isSwipeRightToDeleteEnabled()) {
|
||||
mCrossSwipeArchiveLeftImageView.setImageDrawable(ResourcesCompat.getDrawable(
|
||||
getResources(), R.drawable.ic_delete_small_dark, getContext().getTheme()));
|
||||
mCrossSwipeArchiveRightImageView.setImageDrawable(archiveDrawable);
|
||||
} else {
|
||||
mCrossSwipeArchiveLeftImageView.setImageDrawable(archiveDrawable);
|
||||
mCrossSwipeArchiveRightImageView.setImageDrawable(archiveDrawable);
|
||||
if (mCrossSwipeArchiveLeftImageView != null) {
|
||||
mCrossSwipeArchiveLeftImageView.setImageResource(
|
||||
PrefsUtils.isSwipeRightToDeleteEnabled()
|
||||
? R.drawable.ic_delete_small_dark
|
||||
: R.drawable.ic_archive_small_dark);
|
||||
}
|
||||
}
|
||||
|
||||
/** Lightweight refresh when toggling home selection chrome. */
|
||||
public void rebindSelectionChrome() {
|
||||
if (mHostInterface == null || mData == null) {
|
||||
return;
|
||||
}
|
||||
final Resources resources = getContext().getResources();
|
||||
final boolean selectionMode = mHostInterface.isSelectionMode();
|
||||
final boolean isSelected = mHostInterface.isConversationSelected(
|
||||
mData.getConversationId());
|
||||
setSelected(isSelected);
|
||||
updateSelectionIndicator(selectionMode, isSelected, resources);
|
||||
bindAvatarClickTargets(selectionMode);
|
||||
updatePinButton(selectionMode, resources);
|
||||
updateListDividerInset(selectionMode);
|
||||
}
|
||||
|
||||
public boolean isSwipeAnimatable() {
|
||||
return mHostInterface.isSwipeAnimatable();
|
||||
}
|
||||
@@ -715,24 +868,97 @@ public class ConversationListItemView extends FrameLayout implements OnClickList
|
||||
return mSwipeableContainer.getTranslationX();
|
||||
}
|
||||
|
||||
public float getSwipeActionsRevealWidth() {
|
||||
if (mCrossSwipeRightActions == null) {
|
||||
return 0f;
|
||||
}
|
||||
if (mCrossSwipeRightActions.getWidth() <= 0) {
|
||||
mCrossSwipeRightActions.measure(
|
||||
MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED),
|
||||
MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED));
|
||||
return mCrossSwipeRightActions.getMeasuredWidth()
|
||||
+ getResources().getDimensionPixelSize(R.dimen.looply_space_lg);
|
||||
}
|
||||
return mCrossSwipeRightActions.getWidth()
|
||||
+ getResources().getDimensionPixelSize(R.dimen.looply_space_lg);
|
||||
}
|
||||
|
||||
public boolean isSwipeActionsOpen() {
|
||||
final float reveal = getSwipeActionsRevealWidth();
|
||||
return reveal > 0 && getSwipeTranslationX() <= -reveal * 0.85f;
|
||||
}
|
||||
|
||||
public boolean isTouchOnSwipeActions(final float parentX, final float parentY) {
|
||||
if (!isSwipeActionsOpen() || mCrossSwipeRightActions == null
|
||||
|| mCrossSwipeRightActions.getVisibility() != VISIBLE) {
|
||||
return false;
|
||||
}
|
||||
final int[] parentLoc = new int[2];
|
||||
getLocationOnScreen(parentLoc);
|
||||
final int[] actionLoc = new int[2];
|
||||
mCrossSwipeRightActions.getLocationOnScreen(actionLoc);
|
||||
final float screenX = parentLoc[0] + parentX;
|
||||
final float screenY = parentLoc[1] + parentY;
|
||||
return screenX >= actionLoc[0]
|
||||
&& screenX <= actionLoc[0] + mCrossSwipeRightActions.getWidth()
|
||||
&& screenY >= actionLoc[1]
|
||||
&& screenY <= actionLoc[1] + mCrossSwipeRightActions.getHeight();
|
||||
}
|
||||
|
||||
private void updateSwipeMuteButton(final boolean muted, final boolean trashed) {
|
||||
if (mCrossSwipeMuteView == null) {
|
||||
return;
|
||||
}
|
||||
mCrossSwipeMuteView.setVisibility(trashed ? GONE : VISIBLE);
|
||||
if (mCrossSwipeDeleteView != null) {
|
||||
final ViewGroup.MarginLayoutParams lp =
|
||||
(ViewGroup.MarginLayoutParams) mCrossSwipeDeleteView.getLayoutParams();
|
||||
if (lp != null) {
|
||||
lp.setMarginStart(trashed ? 0
|
||||
: getResources().getDimensionPixelSize(R.dimen.looply_space_sm));
|
||||
mCrossSwipeDeleteView.setLayoutParams(lp);
|
||||
}
|
||||
}
|
||||
if (trashed) {
|
||||
return;
|
||||
}
|
||||
mCrossSwipeMuteView.setImageResource(muted
|
||||
? R.drawable.ic_home_bell
|
||||
: R.drawable.ic_home_bell_off);
|
||||
mCrossSwipeMuteView.setContentDescription(getResources().getString(muted
|
||||
? R.string.home_swipe_unmute
|
||||
: R.string.home_swipe_mute));
|
||||
}
|
||||
|
||||
public void setSwipeTranslationX(final float translationX) {
|
||||
mSwipeableContainer.setTranslationX(translationX);
|
||||
if (translationX == 0) {
|
||||
mCrossSwipeBackground.setVisibility(View.GONE);
|
||||
mCrossSwipeArchiveLeftImageView.setVisibility(GONE);
|
||||
mCrossSwipeArchiveRightImageView.setVisibility(GONE);
|
||||
|
||||
if (mCrossSwipeArchiveLeftImageView != null) {
|
||||
mCrossSwipeArchiveLeftImageView.setVisibility(GONE);
|
||||
}
|
||||
if (mCrossSwipeRightActions != null) {
|
||||
mCrossSwipeRightActions.setVisibility(GONE);
|
||||
}
|
||||
mSwipeableContainer.setBackgroundColor(Color.TRANSPARENT);
|
||||
} else {
|
||||
mCrossSwipeBackground.setVisibility(View.VISIBLE);
|
||||
if (translationX > 0) {
|
||||
mCrossSwipeArchiveLeftImageView.setVisibility(VISIBLE);
|
||||
mCrossSwipeArchiveRightImageView.setVisibility(GONE);
|
||||
if (mCrossSwipeArchiveLeftImageView != null) {
|
||||
mCrossSwipeArchiveLeftImageView.setVisibility(VISIBLE);
|
||||
}
|
||||
if (mCrossSwipeRightActions != null) {
|
||||
mCrossSwipeRightActions.setVisibility(GONE);
|
||||
}
|
||||
} else {
|
||||
mCrossSwipeArchiveLeftImageView.setVisibility(GONE);
|
||||
mCrossSwipeArchiveRightImageView.setVisibility(VISIBLE);
|
||||
if (mCrossSwipeArchiveLeftImageView != null) {
|
||||
mCrossSwipeArchiveLeftImageView.setVisibility(GONE);
|
||||
}
|
||||
if (mCrossSwipeRightActions != null) {
|
||||
mCrossSwipeRightActions.setVisibility(VISIBLE);
|
||||
}
|
||||
}
|
||||
mSwipeableContainer.setBackgroundResource(R.color.window_background);
|
||||
mSwipeableContainer.setBackgroundResource(R.color.home_bg);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -740,9 +966,24 @@ public class ConversationListItemView extends FrameLayout implements OnClickList
|
||||
final String conversationId = mData.getConversationId();
|
||||
if (PrefsUtils.isSwipeRightToDeleteEnabled()
|
||||
&& swipeDirection == ConversationListSwipeHelper.SWIPE_DIRECTION_RIGHT) {
|
||||
final boolean alreadyTrashed = mData.getIsTrashed();
|
||||
mData.deleteConversation();
|
||||
UiUtils.showSnackBar(getContext(), getRootView(),
|
||||
getResources().getString(R.string.conversation_deleted));
|
||||
if (alreadyTrashed) {
|
||||
UiUtils.showSnackBar(getContext(), getRootView(),
|
||||
getResources().getString(R.string.conversation_deleted));
|
||||
} else {
|
||||
HomeToast.show(getContext(), R.string.home_trash_moved_toast_one);
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (mData.getIsTrashed()) {
|
||||
UpdateConversationTrashStatusAction.restoreFromTrash(conversationId);
|
||||
HomeToast.show(getContext(), R.string.home_trash_restored_toast);
|
||||
return;
|
||||
}
|
||||
if (mData.getIsArchived()) {
|
||||
UpdateConversationArchiveStatusAction.unarchiveConversation(conversationId);
|
||||
HomeToast.show(getContext(), R.string.home_profile_unarchived_toast);
|
||||
return;
|
||||
}
|
||||
UpdateConversationArchiveStatusAction.archiveConversation(conversationId);
|
||||
@@ -812,10 +1053,38 @@ public class ConversationListItemView extends FrameLayout implements OnClickList
|
||||
return processClick(v, true);
|
||||
}
|
||||
|
||||
private void bindAvatarClickTargets(final boolean selectionMode) {
|
||||
final OnClickListener selectRow = v -> processClick(mSwipeableContainer, false);
|
||||
if (mContactCheckmarkView != null) {
|
||||
mContactCheckmarkView.setOnClickListener(selectionMode ? selectRow : null);
|
||||
mContactCheckmarkView.setClickable(selectionMode);
|
||||
mContactCheckmarkView.setFocusable(false);
|
||||
}
|
||||
mContactIconView.setOnLongClickListener(this);
|
||||
if (selectionMode) {
|
||||
mContactIconView.setOnClickListener(selectRow);
|
||||
mContactIconView.setClickable(true);
|
||||
mContactIconView.setLongClickable(false);
|
||||
} else {
|
||||
mContactIconView.setOnClickListener(v -> mHostInterface.onConversationAvatarClicked(
|
||||
mData, mContactIconView));
|
||||
mContactIconView.setClickable(true);
|
||||
mContactIconView.setLongClickable(true);
|
||||
}
|
||||
}
|
||||
|
||||
private boolean processClick(final View v, final boolean isLongClick) {
|
||||
Assert.isTrue(v == mSwipeableContainer || v == mContactIconView || v == mImagePreviewView);
|
||||
if (!mHostInterface.isSelectionMode() && v == mContactIconView) {
|
||||
return false;
|
||||
}
|
||||
Assert.isTrue(v == mSwipeableContainer || v == mImagePreviewView);
|
||||
Assert.notNull(mData.getName());
|
||||
|
||||
if (!isLongClick && isSwipeActionsOpen()) {
|
||||
setSwipeTranslationX(0);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (mHostInterface != null) {
|
||||
mHostInterface.onConversationClicked(mData, isLongClick, this);
|
||||
return true;
|
||||
@@ -836,6 +1105,7 @@ public class ConversationListItemView extends FrameLayout implements OnClickList
|
||||
mData.getDraftSnippetText() : mData.getSnippetText();
|
||||
final String previewContentType = mData.getShowDraft() ?
|
||||
mData.getDraftPreviewContentType() : mData.getPreviewContentType();
|
||||
snippetText = ConversationMessageData.flattenPreviewText(snippetText);
|
||||
if (TextUtils.isEmpty(snippetText)) {
|
||||
Resources resources = getResources();
|
||||
// Use the attachment type as a snippet so the preview doesn't look odd
|
||||
|
||||
+74
-9
@@ -63,8 +63,10 @@ public class ConversationListSwipeHelper implements OnItemTouchListener {
|
||||
private VelocityTracker mVelocityTracker;
|
||||
private float mInitialX;
|
||||
private float mInitialY;
|
||||
private float mStartTranslationX;
|
||||
private boolean mIsSwiping;
|
||||
private ConversationListItemView mListItemView;
|
||||
private ConversationListItemView mOpenActionsItem;
|
||||
|
||||
public ConversationListSwipeHelper(final RecyclerView recyclerView) {
|
||||
mRecyclerView = recyclerView;
|
||||
@@ -104,12 +106,19 @@ public class ConversationListSwipeHelper implements OnItemTouchListener {
|
||||
mInitialY = event.getY();
|
||||
|
||||
final View viewAtPoint = mRecyclerView.findChildViewUnder(mInitialX, mInitialY);
|
||||
final ConversationListItemView child = (ConversationListItemView) viewAtPoint;
|
||||
if (viewAtPoint instanceof ConversationListItemView &&
|
||||
child != null && child.isSwipeAnimatable()) {
|
||||
// Begin detecting swipe on the target for the rest of the gesture.
|
||||
mListItemView = child;
|
||||
if (mListItemView.isAnimating()) {
|
||||
if (viewAtPoint instanceof ConversationListItemView) {
|
||||
final ConversationListItemView child = (ConversationListItemView) viewAtPoint;
|
||||
if (child.isTouchOnSwipeActions(mInitialX - child.getLeft(),
|
||||
mInitialY - child.getTop())) {
|
||||
mListItemView = null;
|
||||
} else if (child.isSwipeAnimatable()) {
|
||||
// Begin detecting swipe on the target for the rest of the gesture.
|
||||
mListItemView = child;
|
||||
mStartTranslationX = child.getSwipeTranslationX();
|
||||
if (mListItemView.isAnimating()) {
|
||||
mListItemView = null;
|
||||
}
|
||||
} else {
|
||||
mListItemView = null;
|
||||
}
|
||||
} else {
|
||||
@@ -186,7 +195,8 @@ public class ConversationListSwipeHelper implements OnItemTouchListener {
|
||||
case MotionEvent.ACTION_OUTSIDE:
|
||||
case MotionEvent.ACTION_MOVE:
|
||||
if (hasValidGestureSwipeTarget()) {
|
||||
mListItemView.setSwipeTranslationX(event.getX() - mInitialX);
|
||||
mListItemView.setSwipeTranslationX(clampTranslation(
|
||||
mListItemView, mStartTranslationX + event.getX() - mInitialX));
|
||||
}
|
||||
break;
|
||||
case MotionEvent.ACTION_UP:
|
||||
@@ -208,17 +218,27 @@ public class ConversationListSwipeHelper implements OnItemTouchListener {
|
||||
|
||||
final boolean fastEnough = isTargetSwipedFastEnough();
|
||||
final boolean farEnough = isTargetSwipedFarEnough();
|
||||
|
||||
final boolean shouldDismiss = (fastEnough || farEnough);
|
||||
final boolean shouldRevealActions = swipeDirection == SWIPE_DIRECTION_LEFT
|
||||
&& isTargetSwipedFarEnoughForActions();
|
||||
final boolean shouldDismiss = swipeDirection == SWIPE_DIRECTION_RIGHT
|
||||
&& (fastEnough || farEnough);
|
||||
|
||||
if (shouldDismiss) {
|
||||
closeOpenActions(mListItemView);
|
||||
if (fastEnough) {
|
||||
animateDismiss(mListItemView, velocityX);
|
||||
} else {
|
||||
animateDismiss(mListItemView, swipeDirection);
|
||||
}
|
||||
} else if (shouldRevealActions) {
|
||||
closeOpenActions(mListItemView);
|
||||
animateRevealActions(mListItemView, velocityX);
|
||||
mOpenActionsItem = mListItemView;
|
||||
} else {
|
||||
animateRestore(mListItemView, velocityX);
|
||||
if (mOpenActionsItem == mListItemView) {
|
||||
mOpenActionsItem = null;
|
||||
}
|
||||
}
|
||||
|
||||
onSwipeGestureEnd(mListItemView,
|
||||
@@ -266,6 +286,51 @@ public class ConversationListSwipeHelper implements OnItemTouchListener {
|
||||
mRecyclerView.getParent().requestDisallowInterceptTouchEvent(true);
|
||||
setHardwareAnimatingLayerType(itemView, ANIMATING);
|
||||
itemView.setAnimating(true);
|
||||
closeOpenActions(itemView);
|
||||
}
|
||||
|
||||
private void closeOpenActions(final ConversationListItemView keepOpen) {
|
||||
if (mOpenActionsItem != null && mOpenActionsItem != keepOpen) {
|
||||
animateRestore(mOpenActionsItem, 0f);
|
||||
mOpenActionsItem = null;
|
||||
}
|
||||
}
|
||||
|
||||
private float clampTranslation(final ConversationListItemView itemView,
|
||||
final float translationX) {
|
||||
final float reveal = itemView.getSwipeActionsRevealWidth();
|
||||
if (translationX < 0) {
|
||||
return Math.max(translationX, -reveal * 1.2f);
|
||||
}
|
||||
return translationX;
|
||||
}
|
||||
|
||||
private boolean isTargetSwipedFarEnoughForActions() {
|
||||
final float translationX = mListItemView.getSwipeTranslationX();
|
||||
final float reveal = mListItemView.getSwipeActionsRevealWidth();
|
||||
return translationX < 0 && reveal > 0 && Math.abs(translationX) > reveal * 0.45f;
|
||||
}
|
||||
|
||||
private void animateRevealActions(final ConversationListItemView itemView,
|
||||
final float velocityX) {
|
||||
onSwipeAnimationStart(itemView);
|
||||
final float animateTo = -itemView.getSwipeActionsRevealWidth();
|
||||
final long duration;
|
||||
if (velocityX != 0) {
|
||||
duration = calculateTranslationDuration(
|
||||
animateTo - itemView.getSwipeTranslationX(), velocityX);
|
||||
} else {
|
||||
duration = mDefaultRestoreAnimationDuration;
|
||||
}
|
||||
final ObjectAnimator animator = getSwipeTranslationXAnimator(
|
||||
itemView, animateTo, duration, UiUtils.DEFAULT_INTERPOLATOR);
|
||||
animator.addListener(new AnimatorListenerAdapter() {
|
||||
@Override
|
||||
public void onAnimationEnd(final Animator animation) {
|
||||
onSwipeAnimationEnd(itemView);
|
||||
}
|
||||
});
|
||||
animator.start();
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
+105
@@ -0,0 +1,105 @@
|
||||
/*
|
||||
* Copyright (C) 2015 The Android Open Source Project
|
||||
* Copyright (C) 2024-2025 The LineageOS Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.android.messaging.ui.conversationlist;
|
||||
|
||||
import android.os.Bundle;
|
||||
import android.view.MenuItem;
|
||||
|
||||
import androidx.appcompat.app.ActionBar;
|
||||
|
||||
import com.android.messaging.R;
|
||||
import com.android.messaging.ui.home.HomeInboxMenu;
|
||||
import com.android.messaging.ui.home.HomeSubHeader;
|
||||
|
||||
public class RecentlyDeletedConversationListActivity extends AbstractConversationListActivity {
|
||||
|
||||
@Override
|
||||
protected void onCreate(final Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
setContentView(R.layout.home_archived_activity);
|
||||
if (getSupportActionBar() != null) {
|
||||
getSupportActionBar().hide();
|
||||
}
|
||||
HomeSubHeader.bind(this, R.string.home_recently_deleted_title, v -> onActionBarHome());
|
||||
HomeInboxMenu.bind(this, HomeInboxMenu.Screen.TRASH);
|
||||
|
||||
if (savedInstanceState == null) {
|
||||
mConversationListFragment =
|
||||
ConversationListFragment.createTrashConversationListFragment();
|
||||
getSupportFragmentManager()
|
||||
.beginTransaction()
|
||||
.replace(R.id.home_archived_list_host, mConversationListFragment)
|
||||
.commit();
|
||||
} else {
|
||||
mConversationListFragment = (ConversationListFragment) getSupportFragmentManager()
|
||||
.findFragmentById(R.id.home_archived_list_host);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean useHomeSelectionChrome() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean isTrashList() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void updateActionBar(final ActionBar actionBar) {
|
||||
if (actionBar == null) {
|
||||
return;
|
||||
}
|
||||
actionBar.hide();
|
||||
super.updateActionBar(actionBar);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBackPressed() {
|
||||
if (isInConversationListSelectMode()) {
|
||||
exitMultiSelectState();
|
||||
} else {
|
||||
super.onBackPressed();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onOptionsItemSelected(final MenuItem menuItem) {
|
||||
if (menuItem.getItemId() == android.R.id.home) {
|
||||
onActionBarHome();
|
||||
return true;
|
||||
}
|
||||
return super.onOptionsItemSelected(menuItem);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onActionBarHome() {
|
||||
onBackPressed();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isSwipeAnimatable() {
|
||||
return !isInConversationListSelectMode();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void finish() {
|
||||
super.finish();
|
||||
overridePendingTransition(R.anim.home_activity_hold, R.anim.home_activity_close);
|
||||
}
|
||||
}
|
||||
-66
@@ -1,66 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2015 The Android Open Source Project
|
||||
* Copyright (C) 2024 The LineageOS Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.android.messaging.ui.conversationsettings;
|
||||
|
||||
import android.os.Bundle;
|
||||
import android.view.MenuItem;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.fragment.app.Fragment;
|
||||
|
||||
import com.android.messaging.R;
|
||||
import com.android.messaging.ui.BugleActionBarActivity;
|
||||
import com.android.messaging.ui.UIIntents;
|
||||
import com.android.messaging.util.Assert;
|
||||
|
||||
/**
|
||||
* Shows a list of participants in a conversation.
|
||||
*/
|
||||
public class PeopleAndOptionsActivity extends BugleActionBarActivity {
|
||||
|
||||
@Override
|
||||
protected void onCreate(final Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
setContentView(R.layout.people_and_options_activity);
|
||||
|
||||
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAttachFragment(@NonNull final Fragment fragment) {
|
||||
if (fragment instanceof PeopleAndOptionsFragment) {
|
||||
final String conversationId =
|
||||
getIntent().getStringExtra(UIIntents.UI_INTENT_EXTRA_CONVERSATION_ID);
|
||||
Assert.notNull(conversationId);
|
||||
final PeopleAndOptionsFragment peopleAndOptionsFragment =
|
||||
(PeopleAndOptionsFragment) fragment;
|
||||
peopleAndOptionsFragment.setConversationId(conversationId);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onOptionsItemSelected(@NonNull final MenuItem item) {
|
||||
if (item.getItemId() == android.R.id.home) {
|
||||
// Treat the home press as back press so that when we go back to
|
||||
// ConversationActivity, it doesn't lose its original intent (conversation id etc.)
|
||||
onBackPressed();
|
||||
return true;
|
||||
}
|
||||
return super.onOptionsItemSelected(item);
|
||||
}
|
||||
}
|
||||
-314
@@ -1,314 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2015 The Android Open Source Project
|
||||
* Copyright (C) 2024 The LineageOS Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.android.messaging.ui.conversationsettings;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.app.AlertDialog;
|
||||
import android.app.NotificationManager;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.res.Resources;
|
||||
import android.database.Cursor;
|
||||
import android.os.Bundle;
|
||||
import android.provider.Settings;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.ArrayAdapter;
|
||||
import android.widget.BaseAdapter;
|
||||
import android.widget.ListView;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import androidx.loader.app.LoaderManager;
|
||||
|
||||
import com.android.messaging.R;
|
||||
import com.android.messaging.datamodel.DataModel;
|
||||
import com.android.messaging.datamodel.binding.Binding;
|
||||
import com.android.messaging.datamodel.binding.BindingBase;
|
||||
import com.android.messaging.datamodel.data.ParticipantData;
|
||||
import com.android.messaging.datamodel.data.ParticipantListItemData;
|
||||
import com.android.messaging.datamodel.data.PeopleAndOptionsData;
|
||||
import com.android.messaging.datamodel.data.PeopleAndOptionsData.PeopleAndOptionsDataListener;
|
||||
import com.android.messaging.datamodel.data.PeopleOptionsItemData;
|
||||
import com.android.messaging.datamodel.data.PersonItemData;
|
||||
import com.android.messaging.ui.CompositeAdapter;
|
||||
import com.android.messaging.ui.PersonItemView;
|
||||
import com.android.messaging.ui.conversation.ConversationActivity;
|
||||
import com.android.messaging.util.Assert;
|
||||
import com.android.messaging.util.NotificationsUtil;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Shows a list of participants of a conversation and displays options.
|
||||
*/
|
||||
public class PeopleAndOptionsFragment extends Fragment
|
||||
implements PeopleAndOptionsDataListener, PeopleOptionsItemView.HostInterface {
|
||||
private ListView mListView;
|
||||
private OptionsListAdapter mOptionsListAdapter;
|
||||
private PeopleListAdapter mPeopleListAdapter;
|
||||
private List<ParticipantData> mOtherParticipants;
|
||||
private final Binding<PeopleAndOptionsData> mBinding =
|
||||
BindingBase.createBinding(this);
|
||||
|
||||
@Override
|
||||
public void onCreate(final Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
mBinding.getData().init(LoaderManager.getInstance(this), mBinding);
|
||||
}
|
||||
|
||||
@Override
|
||||
public View onCreateView(final LayoutInflater inflater, final ViewGroup container,
|
||||
final Bundle savedInstanceState) {
|
||||
final View view = inflater.inflate(R.layout.people_and_options_fragment, container, false);
|
||||
mListView = view.findViewById(android.R.id.list);
|
||||
mPeopleListAdapter = new PeopleListAdapter(getActivity());
|
||||
mOptionsListAdapter = new OptionsListAdapter();
|
||||
final CompositeAdapter compositeAdapter = new CompositeAdapter(getActivity());
|
||||
compositeAdapter.addPartition(new PeopleAndOptionsPartition(mOptionsListAdapter,
|
||||
R.string.general_settings_title, false));
|
||||
compositeAdapter.addPartition(new PeopleAndOptionsPartition(mPeopleListAdapter,
|
||||
R.string.participant_list_title, true));
|
||||
mListView.setAdapter(compositeAdapter);
|
||||
return view;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroy() {
|
||||
super.onDestroy();
|
||||
mBinding.unbind();
|
||||
}
|
||||
|
||||
public void setConversationId(final String conversationId) {
|
||||
Assert.isTrue(getView() == null);
|
||||
Assert.notNull(conversationId);
|
||||
mBinding.bind(DataModel.get().createPeopleAndOptionsData(conversationId, getActivity(),
|
||||
this));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onOptionsCursorUpdated(final PeopleAndOptionsData data, final Cursor cursor) {
|
||||
Assert.isTrue(cursor == null || cursor.getCount() == 1);
|
||||
mBinding.ensureBound(data);
|
||||
mOptionsListAdapter.swapCursor(cursor);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onParticipantsListLoaded(final PeopleAndOptionsData data,
|
||||
final List<ParticipantData> participants) {
|
||||
mBinding.ensureBound(data);
|
||||
mPeopleListAdapter.updateParticipants(participants);
|
||||
mOtherParticipants = participants;
|
||||
final ParticipantData otherParticipant = participants.size() == 1 ?
|
||||
participants.get(0) : null;
|
||||
mOptionsListAdapter.setOtherParticipant(otherParticipant);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onOptionsItemViewClicked(final PeopleOptionsItemData item) {
|
||||
if (item.getItemId() == PeopleOptionsItemData.SETTING_NOTIFICATION) {
|
||||
ArrayList<String> participantsNames = new ArrayList<>();
|
||||
for (ParticipantData participant : mOtherParticipants) {
|
||||
participantsNames.add(participant.getDisplayName(true));
|
||||
}
|
||||
NotificationsUtil.createNotificationChannelGroup(getActivity(),
|
||||
NotificationsUtil.CONVERSATION_GROUP_NAME,
|
||||
R.string.notification_channel_messages_title);
|
||||
NotificationsUtil.createNotificationChannel(getActivity(),
|
||||
mBinding.getData().getConversationId(),
|
||||
String.join(", ", participantsNames),
|
||||
NotificationManager.IMPORTANCE_HIGH,
|
||||
NotificationsUtil.CONVERSATION_GROUP_NAME);
|
||||
Intent intent = new Intent(Settings.ACTION_APP_NOTIFICATION_SETTINGS);
|
||||
intent.putExtra(Settings.EXTRA_APP_PACKAGE, getContext().getPackageName());
|
||||
startActivity(intent);
|
||||
} else if (item.getItemId() == PeopleOptionsItemData.SETTING_BLOCKED) {
|
||||
if (item.getOtherParticipant().isBlocked()) {
|
||||
mBinding.getData().setDestinationBlocked(mBinding, false);
|
||||
return;
|
||||
}
|
||||
final Resources res = getResources();
|
||||
final Activity activity = getActivity();
|
||||
new AlertDialog.Builder(activity, R.style.AlertDialogTheme)
|
||||
.setTitle(res.getString(R.string.block_confirmation_title,
|
||||
item.getOtherParticipant().getDisplayDestination()))
|
||||
.setMessage(res.getString(R.string.block_confirmation_message))
|
||||
.setNegativeButton(android.R.string.cancel, null)
|
||||
.setPositiveButton(android.R.string.ok, (arg0, arg1) -> {
|
||||
mBinding.getData().setDestinationBlocked(mBinding, true);
|
||||
activity.setResult(ConversationActivity.FINISH_RESULT_CODE);
|
||||
activity.finish();
|
||||
})
|
||||
.create()
|
||||
.show();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A simple adapter that takes a conversation metadata cursor and binds
|
||||
* PeopleAndOptionsItemViews to individual COLUMNS of the first cursor record. (Note
|
||||
* that this is not a CursorAdapter because it treats individual columns of the cursor as
|
||||
* separate options to display for the conversation, e.g. notification settings).
|
||||
*/
|
||||
private class OptionsListAdapter extends BaseAdapter {
|
||||
private Cursor mOptionsCursor;
|
||||
private ParticipantData mOtherParticipantData;
|
||||
|
||||
public Cursor swapCursor(final Cursor newCursor) {
|
||||
final Cursor oldCursor = mOptionsCursor;
|
||||
if (newCursor != oldCursor) {
|
||||
mOptionsCursor = newCursor;
|
||||
notifyDataSetChanged();
|
||||
}
|
||||
return oldCursor;
|
||||
}
|
||||
|
||||
public void setOtherParticipant(final ParticipantData participantData) {
|
||||
if (mOtherParticipantData != participantData) {
|
||||
mOtherParticipantData = participantData;
|
||||
notifyDataSetChanged();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getCount() {
|
||||
int count = PeopleOptionsItemData.SETTINGS_COUNT;
|
||||
if (mOtherParticipantData == null) {
|
||||
count--;
|
||||
}
|
||||
return mOptionsCursor == null ? 0 : count;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object getItem(final int position) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getItemId(final int position) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public View getView(final int position, final View convertView, final ViewGroup parent) {
|
||||
final PeopleOptionsItemView itemView;
|
||||
if (convertView != null && convertView instanceof PeopleOptionsItemView) {
|
||||
itemView = (PeopleOptionsItemView) convertView;
|
||||
} else {
|
||||
final LayoutInflater inflater = (LayoutInflater) getActivity()
|
||||
.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
|
||||
itemView = (PeopleOptionsItemView)
|
||||
inflater.inflate(R.layout.people_options_item_view, parent, false);
|
||||
}
|
||||
mOptionsCursor.moveToFirst();
|
||||
itemView.bind(mOptionsCursor, position, mOtherParticipantData,
|
||||
PeopleAndOptionsFragment.this);
|
||||
return itemView;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* An adapter that takes a list of ParticipantData and displays them as a list of
|
||||
* ParticipantListItemViews.
|
||||
*/
|
||||
private class PeopleListAdapter extends ArrayAdapter<ParticipantData> {
|
||||
public PeopleListAdapter(final Context context) {
|
||||
super(context, R.layout.people_list_item_view, new ArrayList<>());
|
||||
}
|
||||
|
||||
public void updateParticipants(final List<ParticipantData> newList) {
|
||||
clear();
|
||||
addAll(newList);
|
||||
notifyDataSetChanged();
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public View getView(final int position, final View convertView,
|
||||
@NonNull final ViewGroup parent) {
|
||||
PersonItemView itemView;
|
||||
final ParticipantData item = getItem(position);
|
||||
if (convertView != null && convertView instanceof PersonItemView) {
|
||||
itemView = (PersonItemView) convertView;
|
||||
} else {
|
||||
final LayoutInflater inflater = (LayoutInflater) getContext()
|
||||
.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
|
||||
itemView = (PersonItemView) inflater.inflate(R.layout.people_list_item_view, parent,
|
||||
false);
|
||||
}
|
||||
final ParticipantListItemData itemData =
|
||||
DataModel.get().createParticipantListItemData(item);
|
||||
itemView.bind(itemData);
|
||||
|
||||
// Any click on the row should have the same effect as clicking the avatar icon
|
||||
final PersonItemView itemViewClosure = itemView;
|
||||
itemView.setListener(new PersonItemView.PersonItemViewListener() {
|
||||
@Override
|
||||
public void onPersonClicked(final PersonItemData data) {
|
||||
itemViewClosure.performClickOnAvatar();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onPersonLongClicked(final PersonItemData data) {
|
||||
if (mBinding.isBound()) {
|
||||
final CopyContactDetailDialog dialog = new CopyContactDetailDialog(
|
||||
getContext(), data.getDetails());
|
||||
dialog.show();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
});
|
||||
return itemView;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents a partition/section in the People & Options list (e.g. "general options" and
|
||||
* "people in this conversation" sections).
|
||||
*/
|
||||
private class PeopleAndOptionsPartition extends CompositeAdapter.Partition {
|
||||
private final int mHeaderResId;
|
||||
private final boolean mNeedDivider;
|
||||
|
||||
public PeopleAndOptionsPartition(final BaseAdapter adapter, final int headerResId,
|
||||
final boolean needDivider) {
|
||||
super(true /* showIfEmpty */, true /* hasHeader */, adapter);
|
||||
mHeaderResId = headerResId;
|
||||
mNeedDivider = needDivider;
|
||||
}
|
||||
|
||||
@Override
|
||||
public View getHeaderView(final View convertView, final ViewGroup parentView) {
|
||||
View view;
|
||||
if (convertView != null && convertView.getId() == R.id.people_and_options_header) {
|
||||
view = convertView;
|
||||
} else {
|
||||
view = LayoutInflater.from(getActivity()).inflate(
|
||||
R.layout.people_and_options_section_header, parentView, false);
|
||||
}
|
||||
final TextView text = view.findViewById(R.id.header_text);
|
||||
final View divider = view.findViewById(R.id.divider);
|
||||
text.setText(mHeaderResId);
|
||||
divider.setVisibility(mNeedDivider ? View.VISIBLE : View.GONE);
|
||||
return view;
|
||||
}
|
||||
}
|
||||
}
|
||||
-70
@@ -1,70 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2015 The Android Open Source Project
|
||||
* Copyright (C) 2024 The LineageOS Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.android.messaging.ui.conversationsettings;
|
||||
|
||||
import android.content.Context;
|
||||
import android.database.Cursor;
|
||||
import androidx.appcompat.widget.SwitchCompat;
|
||||
|
||||
import android.util.AttributeSet;
|
||||
import android.widget.LinearLayout;
|
||||
import android.widget.TextView;
|
||||
|
||||
import com.android.messaging.R;
|
||||
import com.android.messaging.datamodel.DataModel;
|
||||
import com.android.messaging.datamodel.data.ParticipantData;
|
||||
import com.android.messaging.datamodel.data.PeopleOptionsItemData;
|
||||
import com.android.messaging.util.Assert;
|
||||
|
||||
/**
|
||||
* The view for a single entry in the options section of people & options activity.
|
||||
*/
|
||||
public class PeopleOptionsItemView extends LinearLayout {
|
||||
/**
|
||||
* Implemented by the host of this view that handles options click event.
|
||||
*/
|
||||
public interface HostInterface {
|
||||
void onOptionsItemViewClicked(PeopleOptionsItemData item);
|
||||
}
|
||||
|
||||
private TextView mTitle;
|
||||
private TextView mSubtitle;
|
||||
private SwitchCompat mSwitch;
|
||||
private final PeopleOptionsItemData mData;
|
||||
private HostInterface mHostInterface;
|
||||
|
||||
public PeopleOptionsItemView(final Context context, final AttributeSet attrs) {
|
||||
super(context, attrs);
|
||||
mData = DataModel.get().createPeopleOptionsItemData(context);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onFinishInflate () {
|
||||
super.onFinishInflate();
|
||||
mTitle = findViewById(R.id.title);
|
||||
setOnClickListener(v -> mHostInterface.onOptionsItemViewClicked(mData));
|
||||
}
|
||||
|
||||
public void bind(final Cursor cursor, final int columnIndex, ParticipantData otherParticipant,
|
||||
final HostInterface hostInterface) {
|
||||
Assert.isTrue(columnIndex < PeopleOptionsItemData.SETTINGS_COUNT && columnIndex >= 0);
|
||||
mData.bind(cursor, otherParticipant, columnIndex);
|
||||
mHostInterface = hostInterface;
|
||||
|
||||
mTitle.setText(mData.getTitle());
|
||||
}
|
||||
}
|
||||
@@ -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,520 @@
|
||||
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.Gravity;
|
||||
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.LinearLayout;
|
||||
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);
|
||||
if (mMode != HomeAppLock.MODE_UNLOCK) {
|
||||
relocateLockPrompt(body);
|
||||
}
|
||||
} else if (isPatternMode()) {
|
||||
inflater.inflate(R.layout.home_lock_pattern_body, body, true);
|
||||
bindPattern(body);
|
||||
if (mMode != HomeAppLock.MODE_UNLOCK) {
|
||||
relocateLockPrompt(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 relocateLockPrompt(final View host) {
|
||||
final ViewGroup prompt = host.findViewById(R.id.home_lock_prompt);
|
||||
if (prompt == null) {
|
||||
return;
|
||||
}
|
||||
if (mSubtitle != null) {
|
||||
mSubtitle.setGravity(Gravity.CENTER);
|
||||
mSubtitle.setTextAlignment(View.TEXT_ALIGNMENT_CENTER);
|
||||
final LinearLayout.LayoutParams lp = new LinearLayout.LayoutParams(
|
||||
ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT);
|
||||
lp.bottomMargin = getResources().getDimensionPixelSize(R.dimen.looply_space_sm);
|
||||
mSubtitle.setLayoutParams(lp);
|
||||
moveChild(prompt, mSubtitle, 0);
|
||||
}
|
||||
if (mError != null) {
|
||||
moveChild(prompt, mError, mSubtitle == null ? 0 : 1);
|
||||
}
|
||||
}
|
||||
|
||||
private static void moveChild(final ViewGroup parent, final View child, final int index) {
|
||||
if (parent == null || child == null) {
|
||||
return;
|
||||
}
|
||||
if (child.getParent() instanceof ViewGroup) {
|
||||
((ViewGroup) child.getParent()).removeView(child);
|
||||
}
|
||||
parent.addView(child, Math.min(index, parent.getChildCount()));
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,105 @@
|
||||
/*
|
||||
* Copyright (C) 2026 The LineageOS Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.android.messaging.ui.home;
|
||||
|
||||
import android.content.Context;
|
||||
import android.util.AttributeSet;
|
||||
import android.view.MotionEvent;
|
||||
import android.view.View;
|
||||
import android.widget.FrameLayout;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
/**
|
||||
* Full-screen attach overlay. Sheet/dock keep their own touches. A tap above the
|
||||
* sheet dismisses it; a tap on the conversation compose field also asks for the IME.
|
||||
*/
|
||||
public class HomeAttachOverlayLayout extends FrameLayout {
|
||||
public interface OutsideTapListener {
|
||||
void onOutsideTap(boolean onComposeField);
|
||||
}
|
||||
|
||||
@Nullable private View mSheet;
|
||||
@Nullable private View mDock;
|
||||
@Nullable private View mComposeField;
|
||||
@Nullable private OutsideTapListener mOutsideTapListener;
|
||||
private boolean mDownOutside;
|
||||
|
||||
public HomeAttachOverlayLayout(@NonNull final Context context) {
|
||||
super(context);
|
||||
}
|
||||
|
||||
public HomeAttachOverlayLayout(@NonNull final Context context,
|
||||
@Nullable final AttributeSet attrs) {
|
||||
super(context, attrs);
|
||||
}
|
||||
|
||||
public void bind(@Nullable final View sheet, @Nullable final View dock,
|
||||
@Nullable final View composeField, @Nullable final OutsideTapListener listener) {
|
||||
mSheet = sheet;
|
||||
mDock = dock;
|
||||
mComposeField = composeField;
|
||||
mOutsideTapListener = listener;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean dispatchTouchEvent(final MotionEvent ev) {
|
||||
final int action = ev.getActionMasked();
|
||||
if (action == MotionEvent.ACTION_DOWN) {
|
||||
mDownOutside = !isOnSheetOrDock(ev);
|
||||
}
|
||||
if (mDownOutside) {
|
||||
if (action == MotionEvent.ACTION_UP && mOutsideTapListener != null) {
|
||||
mOutsideTapListener.onOutsideTap(isOnComposeField(ev));
|
||||
}
|
||||
if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_CANCEL) {
|
||||
mDownOutside = false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return super.dispatchTouchEvent(ev);
|
||||
}
|
||||
|
||||
private boolean isOnSheetOrDock(final MotionEvent ev) {
|
||||
final float x = ev.getX();
|
||||
final float y = ev.getY();
|
||||
if (containsLocal(mDock, x, y)) {
|
||||
return true;
|
||||
}
|
||||
return mSheet != null && mSheet.getVisibility() == VISIBLE && y >= mSheet.getY();
|
||||
}
|
||||
|
||||
private boolean isOnComposeField(final MotionEvent ev) {
|
||||
if (mComposeField == null || mComposeField.getVisibility() != VISIBLE) {
|
||||
return false;
|
||||
}
|
||||
final int[] loc = new int[2];
|
||||
mComposeField.getLocationInWindow(loc);
|
||||
final float x = ev.getRawX();
|
||||
final float y = ev.getRawY();
|
||||
return x >= loc[0] && x < loc[0] + mComposeField.getWidth()
|
||||
&& y >= loc[1] && y < loc[1] + mComposeField.getHeight();
|
||||
}
|
||||
|
||||
private static boolean containsLocal(@Nullable final View view, final float x, final float y) {
|
||||
if (view == null || view.getVisibility() != VISIBLE) {
|
||||
return false;
|
||||
}
|
||||
return x >= view.getX() && x < view.getX() + view.getWidth()
|
||||
&& y >= view.getY() && y < view.getY() + view.getHeight();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
/*
|
||||
* Copyright (C) 2026 The LineageOS Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.android.messaging.ui.home;
|
||||
|
||||
import android.os.Bundle;
|
||||
|
||||
import com.android.messaging.R;
|
||||
import com.android.messaging.ui.BugleActionBarActivity;
|
||||
|
||||
/** Home Automation screen launched from the bottom nav. */
|
||||
public class HomeAutomationActivity extends BugleActionBarActivity {
|
||||
|
||||
@Override
|
||||
protected void onCreate(final Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
setContentView(R.layout.home_automation_activity);
|
||||
if (getSupportActionBar() != null) {
|
||||
getSupportActionBar().hide();
|
||||
}
|
||||
HomeSubHeader.bind(this, R.string.home_automation_title, v -> finish());
|
||||
HomeAutomationHome.bind(findViewById(R.id.home_automation_pane));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void finish() {
|
||||
super.finish();
|
||||
overridePendingTransition(R.anim.home_activity_hold, R.anim.home_activity_close);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,407 @@
|
||||
/*
|
||||
* Copyright (C) 2026 The LineageOS Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.android.messaging.ui.home;
|
||||
|
||||
import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
import android.text.TextUtils;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.LinearLayout;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.annotation.DrawableRes;
|
||||
import androidx.annotation.StringRes;
|
||||
import androidx.core.content.ContextCompat;
|
||||
|
||||
import com.android.messaging.R;
|
||||
import com.android.messaging.ui.BugleActionBarActivity;
|
||||
import com.android.messaging.ui.ContactIconView;
|
||||
|
||||
import java.util.ArrayList;
|
||||
|
||||
/** Sheet to create a new automation rule. */
|
||||
public class HomeAutomationCreateActivity extends BugleActionBarActivity {
|
||||
|
||||
private View mTriggerMissed;
|
||||
private View mTriggerThanks;
|
||||
private View mTriggerSchedule;
|
||||
private View mTriggerKeyword;
|
||||
private TextView mRecipientAll;
|
||||
private TextView mRecipientContacts;
|
||||
private TextView mRecipientUnknown;
|
||||
private TextView mSelectContacts;
|
||||
private HomeFadeScrollView mRecipientScroll;
|
||||
private View mTargetChipHost;
|
||||
private View mTargetChipScroll;
|
||||
private LinearLayout mTargetChips;
|
||||
|
||||
private static final String STATE_TRIGGER = "home_automation_trigger";
|
||||
private static final String STATE_RECIPIENT = "home_automation_recipient";
|
||||
private static final String STATE_TARGETS = "home_automation_targets";
|
||||
|
||||
private int mTrigger;
|
||||
private int mRecipient;
|
||||
private final ArrayList<HomeAutomationTarget> mTargets = new ArrayList<>();
|
||||
private boolean mRestorePromptOpen;
|
||||
|
||||
@Override
|
||||
protected void onCreate(final Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
setContentView(R.layout.home_automation_create_activity);
|
||||
if (getSupportActionBar() != null) {
|
||||
getSupportActionBar().hide();
|
||||
}
|
||||
|
||||
final View close = findViewById(R.id.home_sub_header_close);
|
||||
if (close != null) {
|
||||
close.setOnClickListener(v -> finish());
|
||||
}
|
||||
final View scrim = findViewById(R.id.home_automation_create_scrim);
|
||||
if (scrim != null) {
|
||||
scrim.setOnClickListener(v -> finish());
|
||||
}
|
||||
|
||||
mTriggerMissed = bindTriggerRow(R.id.home_automation_trigger_missed,
|
||||
R.drawable.ic_home_automation_phone,
|
||||
R.string.home_automation_trigger_missed_title,
|
||||
R.string.home_automation_trigger_missed_body, true, 0);
|
||||
mTriggerThanks = bindTriggerRow(R.id.home_automation_trigger_thanks,
|
||||
R.drawable.ic_home_automation_phone,
|
||||
R.string.home_automation_trigger_thanks_title,
|
||||
R.string.home_automation_trigger_thanks_body, false, 1);
|
||||
mTriggerSchedule = bindTriggerTile(R.id.home_automation_trigger_schedule,
|
||||
R.drawable.ic_home_schedule_line,
|
||||
R.string.home_automation_trigger_schedule_title,
|
||||
R.string.home_automation_trigger_schedule_body, 2);
|
||||
mTriggerKeyword = bindTriggerTile(R.id.home_automation_trigger_keyword,
|
||||
R.drawable.ic_home_automation_chat,
|
||||
R.string.home_automation_trigger_keyword_title,
|
||||
R.string.home_automation_trigger_keyword_body, 3);
|
||||
|
||||
mRecipientScroll = findViewById(R.id.home_automation_recipient_scroll);
|
||||
mRecipientAll = findViewById(R.id.home_automation_recipient_all);
|
||||
mRecipientContacts = findViewById(R.id.home_automation_recipient_contacts);
|
||||
mRecipientUnknown = findViewById(R.id.home_automation_recipient_unknown);
|
||||
bindRecipient(mRecipientAll, 0);
|
||||
bindRecipient(mRecipientContacts, 1);
|
||||
bindRecipient(mRecipientUnknown, 2);
|
||||
|
||||
mSelectContacts = findViewById(R.id.home_automation_select_contacts);
|
||||
mTargetChipHost = findViewById(R.id.home_automation_target_chip_host);
|
||||
mTargetChipScroll = findViewById(R.id.home_automation_target_chip_scroll);
|
||||
mTargetChips = findViewById(R.id.home_automation_target_chips);
|
||||
getSupportFragmentManager().setFragmentResultListener(
|
||||
HomeAutomationTargetPickerSheet.REQUEST_KEY, this, (key, bundle) -> {
|
||||
final ArrayList<HomeAutomationTarget> selected = bundle
|
||||
.getParcelableArrayList(HomeAutomationTargetPickerSheet.EXTRA_TARGETS);
|
||||
mTargets.clear();
|
||||
if (selected != null) {
|
||||
mTargets.addAll(selected);
|
||||
}
|
||||
bindTargets();
|
||||
});
|
||||
if (mSelectContacts != null) {
|
||||
mSelectContacts.setOnClickListener(v -> openTargetPicker());
|
||||
}
|
||||
|
||||
comingSoon(R.id.home_automation_preview);
|
||||
comingSoon(R.id.home_automation_activate);
|
||||
|
||||
if (savedInstanceState != null) {
|
||||
restoreState(savedInstanceState);
|
||||
} else if (HomeAutomationDraftStore.hasDraft(this)) {
|
||||
promptRestoreDraft();
|
||||
}
|
||||
|
||||
applyTrigger();
|
||||
applyRecipient();
|
||||
bindTargets();
|
||||
}
|
||||
|
||||
private View bindTriggerRow(final int id, @DrawableRes final int icon,
|
||||
@StringRes final int title, @StringRes final int body, final boolean recommended,
|
||||
final int triggerIndex) {
|
||||
final View row = findViewById(id);
|
||||
if (row == null) {
|
||||
return null;
|
||||
}
|
||||
final ImageView iconView = row.findViewById(R.id.home_automation_trigger_icon);
|
||||
if (iconView != null) {
|
||||
iconView.setImageResource(icon);
|
||||
}
|
||||
final TextView titleView = row.findViewById(R.id.home_automation_trigger_title);
|
||||
if (titleView != null) {
|
||||
titleView.setText(title);
|
||||
}
|
||||
final TextView bodyView = row.findViewById(R.id.home_automation_trigger_body);
|
||||
if (bodyView != null) {
|
||||
bodyView.setText(body);
|
||||
}
|
||||
final View badge = row.findViewById(R.id.home_automation_trigger_badge);
|
||||
if (badge != null) {
|
||||
badge.setVisibility(recommended ? View.VISIBLE : View.GONE);
|
||||
}
|
||||
if (triggerIndex >= 0) {
|
||||
row.setOnClickListener(v -> selectTrigger(triggerIndex));
|
||||
}
|
||||
return row;
|
||||
}
|
||||
|
||||
private View bindTriggerTile(final int id, @DrawableRes final int icon,
|
||||
@StringRes final int title, @StringRes final int body, final int triggerIndex) {
|
||||
final View tile = findViewById(id);
|
||||
if (tile == null) {
|
||||
return null;
|
||||
}
|
||||
final ImageView iconView = tile.findViewById(R.id.home_automation_trigger_icon);
|
||||
if (iconView != null) {
|
||||
iconView.setImageResource(icon);
|
||||
}
|
||||
final TextView titleView = tile.findViewById(R.id.home_automation_trigger_title);
|
||||
if (titleView != null) {
|
||||
titleView.setText(title);
|
||||
}
|
||||
final TextView bodyView = tile.findViewById(R.id.home_automation_trigger_body);
|
||||
if (bodyView != null) {
|
||||
bodyView.setText(body);
|
||||
}
|
||||
tile.setOnClickListener(v -> selectTrigger(triggerIndex));
|
||||
return tile;
|
||||
}
|
||||
|
||||
private void bindRecipient(final TextView pill, final int index) {
|
||||
if (pill != null) {
|
||||
pill.setOnClickListener(v -> selectRecipient(index));
|
||||
}
|
||||
}
|
||||
|
||||
private void comingSoon(final int id) {
|
||||
final View view = findViewById(id);
|
||||
if (view != null) {
|
||||
view.setOnClickListener(v -> HomeToast.show(this, R.string.home_automate_coming_soon));
|
||||
}
|
||||
}
|
||||
|
||||
private void selectTrigger(final int index) {
|
||||
if (mTrigger == index) {
|
||||
return;
|
||||
}
|
||||
mTrigger = index;
|
||||
applyTrigger();
|
||||
}
|
||||
|
||||
private void selectRecipient(final int index) {
|
||||
if (mRecipient == index) {
|
||||
return;
|
||||
}
|
||||
mRecipient = index;
|
||||
applyRecipient();
|
||||
}
|
||||
|
||||
private void applyTrigger() {
|
||||
applyTriggerRow(mTriggerMissed, mTrigger == 0, true);
|
||||
applyTriggerRow(mTriggerThanks, mTrigger == 1, true);
|
||||
applyTriggerTile(mTriggerSchedule, mTrigger == 2);
|
||||
applyTriggerTile(mTriggerKeyword, mTrigger == 3);
|
||||
}
|
||||
|
||||
private void applyRecipient() {
|
||||
applyPill(mRecipientAll, mRecipient == 0);
|
||||
applyPill(mRecipientContacts, mRecipient == 1);
|
||||
applyPill(mRecipientUnknown, mRecipient == 2);
|
||||
if (mRecipientScroll != null) {
|
||||
mRecipientScroll.centerChild(mRecipient == 0 ? mRecipientAll
|
||||
: mRecipient == 1 ? mRecipientContacts : mRecipientUnknown);
|
||||
}
|
||||
}
|
||||
|
||||
private void applySelectContacts() {
|
||||
final boolean selected = !mTargets.isEmpty();
|
||||
applyPill(mSelectContacts, selected);
|
||||
if (selected && mRecipientScroll != null) {
|
||||
mRecipientScroll.centerChild(mSelectContacts);
|
||||
}
|
||||
}
|
||||
|
||||
private void openTargetPicker() {
|
||||
HomeAutomationTargetPickerSheet.show(getSupportFragmentManager(),
|
||||
new ArrayList<>(mTargets));
|
||||
}
|
||||
|
||||
private void bindTargets() {
|
||||
if (mTargetChips == null) {
|
||||
return;
|
||||
}
|
||||
mTargetChips.removeAllViews();
|
||||
final boolean empty = mTargets.isEmpty();
|
||||
if (mTargetChipHost != null) {
|
||||
mTargetChipHost.setVisibility(empty ? View.GONE : View.VISIBLE);
|
||||
} else if (mTargetChipScroll != null) {
|
||||
mTargetChipScroll.setVisibility(empty ? View.GONE : View.VISIBLE);
|
||||
}
|
||||
applySelectContacts();
|
||||
if (empty) {
|
||||
return;
|
||||
}
|
||||
final LayoutInflater inflater = LayoutInflater.from(this);
|
||||
for (final HomeAutomationTarget target : mTargets) {
|
||||
final View chip = inflater.inflate(R.layout.home_automation_target_chip, mTargetChips,
|
||||
false);
|
||||
final ContactIconView avatar = chip.findViewById(R.id.home_compose_chip_avatar);
|
||||
if (!TextUtils.isEmpty(target.iconUri)) {
|
||||
avatar.setImageResourceUri(Uri.parse(target.iconUri), target.contactId,
|
||||
target.lookupKey, target.destination);
|
||||
}
|
||||
avatar.setImageClickHandlerDisabled(true);
|
||||
final TextView name = chip.findViewById(R.id.home_compose_chip_name);
|
||||
name.setText(target.displayName());
|
||||
final View remove = chip.findViewById(R.id.home_automation_target_chip_remove);
|
||||
if (remove != null) {
|
||||
remove.setOnClickListener(v -> {
|
||||
mTargets.remove(target);
|
||||
bindTargets();
|
||||
});
|
||||
}
|
||||
final LinearLayout.LayoutParams params =
|
||||
(LinearLayout.LayoutParams) chip.getLayoutParams();
|
||||
params.setMarginEnd(getResources().getDimensionPixelSize(R.dimen.looply_space_xs));
|
||||
mTargetChips.addView(chip, params);
|
||||
}
|
||||
}
|
||||
|
||||
private void applyTriggerRow(final View row, final boolean selected, final boolean tintIcon) {
|
||||
if (row == null) {
|
||||
return;
|
||||
}
|
||||
row.setBackgroundResource(selected
|
||||
? R.drawable.home_bg_automation_option_selected
|
||||
: R.drawable.home_bg_automation_option);
|
||||
final ImageView icon = row.findViewById(R.id.home_automation_trigger_icon);
|
||||
if (icon != null && tintIcon) {
|
||||
if (selected) {
|
||||
icon.setBackgroundResource(R.drawable.home_bg_automation_icon_on);
|
||||
icon.setColorFilter(ContextCompat.getColor(this, android.R.color.white));
|
||||
} else {
|
||||
icon.setBackground(null);
|
||||
icon.setColorFilter(ContextCompat.getColor(this, R.color.home_text_primary));
|
||||
}
|
||||
}
|
||||
applyCheck(row.findViewById(R.id.home_automation_trigger_check), selected);
|
||||
}
|
||||
|
||||
private void applyTriggerTile(final View tile, final boolean selected) {
|
||||
if (tile == null) {
|
||||
return;
|
||||
}
|
||||
tile.setBackgroundResource(selected
|
||||
? R.drawable.home_bg_automation_option_selected
|
||||
: R.drawable.home_bg_automation_option);
|
||||
}
|
||||
|
||||
private void applyPill(final TextView pill, final boolean selected) {
|
||||
if (pill == null) {
|
||||
return;
|
||||
}
|
||||
pill.setBackgroundResource(selected
|
||||
? R.drawable.home_bg_automation_pill_selected
|
||||
: R.drawable.home_bg_automation_pill);
|
||||
pill.setTextColor(ContextCompat.getColor(this,
|
||||
selected ? android.R.color.white : R.color.home_text_primary));
|
||||
}
|
||||
|
||||
private void applyCheck(final ImageView check, final boolean selected) {
|
||||
if (check == null) {
|
||||
return;
|
||||
}
|
||||
if (selected) {
|
||||
check.setBackgroundResource(R.drawable.home_bg_selection_ring_checked);
|
||||
check.setImageResource(R.drawable.ic_home_check);
|
||||
check.setColorFilter(ContextCompat.getColor(this, android.R.color.white));
|
||||
check.setPadding(4, 4, 4, 4);
|
||||
} else {
|
||||
check.setBackgroundResource(R.drawable.home_bg_selection_ring);
|
||||
check.setImageDrawable(null);
|
||||
check.setColorFilter(null);
|
||||
check.setPadding(0, 0, 0, 0);
|
||||
}
|
||||
}
|
||||
|
||||
private void promptRestoreDraft() {
|
||||
mRestorePromptOpen = true;
|
||||
final HomeModal.Handle handle = HomeModal.builder(this)
|
||||
.icon(R.drawable.ic_home_nav_automate)
|
||||
.title(R.string.home_automation_draft_title)
|
||||
.message(R.string.home_automation_draft_body)
|
||||
.positive(R.string.home_automation_draft_resume, this::applyStoredDraft)
|
||||
.negative(R.string.home_automation_draft_start_new, () ->
|
||||
HomeAutomationDraftStore.clear(this))
|
||||
.show();
|
||||
handle.dialog.setOnDismissListener(dialog -> mRestorePromptOpen = false);
|
||||
}
|
||||
|
||||
private void applyStoredDraft() {
|
||||
final HomeAutomationDraftStore.Draft draft = HomeAutomationDraftStore.load(this);
|
||||
if (draft == null) {
|
||||
return;
|
||||
}
|
||||
applyDraft(draft.trigger, draft.recipient, draft.targets);
|
||||
}
|
||||
|
||||
private void restoreState(final Bundle savedInstanceState) {
|
||||
final ArrayList<HomeAutomationTarget> targets =
|
||||
savedInstanceState.getParcelableArrayList(STATE_TARGETS);
|
||||
applyDraft(savedInstanceState.getInt(STATE_TRIGGER, 0),
|
||||
savedInstanceState.getInt(STATE_RECIPIENT, 0), targets);
|
||||
}
|
||||
|
||||
private void applyDraft(final int trigger, final int recipient,
|
||||
final ArrayList<HomeAutomationTarget> targets) {
|
||||
mTrigger = trigger;
|
||||
mRecipient = recipient;
|
||||
mTargets.clear();
|
||||
if (targets != null) {
|
||||
mTargets.addAll(targets);
|
||||
}
|
||||
applyTrigger();
|
||||
applyRecipient();
|
||||
bindTargets();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onSaveInstanceState(final Bundle outState) {
|
||||
super.onSaveInstanceState(outState);
|
||||
outState.putInt(STATE_TRIGGER, mTrigger);
|
||||
outState.putInt(STATE_RECIPIENT, mRecipient);
|
||||
outState.putParcelableArrayList(STATE_TARGETS, new ArrayList<>(mTargets));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPause() {
|
||||
super.onPause();
|
||||
if (mRestorePromptOpen) {
|
||||
return;
|
||||
}
|
||||
HomeAutomationDraftStore.save(this, mTrigger, mRecipient, mTargets);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void finish() {
|
||||
super.finish();
|
||||
overridePendingTransition(R.anim.home_activity_hold, R.anim.home_sheet_close);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,130 @@
|
||||
/*
|
||||
* Copyright (C) 2026 The LineageOS Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.android.messaging.ui.home;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.SharedPreferences;
|
||||
import android.text.TextUtils;
|
||||
|
||||
import org.json.JSONArray;
|
||||
import org.json.JSONObject;
|
||||
|
||||
import java.util.ArrayList;
|
||||
|
||||
/** Local draft for an automation rule that was left unfinished. */
|
||||
public final class HomeAutomationDraftStore {
|
||||
|
||||
private static final String PREFS_NAME = "home_automation_draft";
|
||||
private static final String KEY_JSON = "draft_json";
|
||||
|
||||
public static final class Draft {
|
||||
public final int trigger;
|
||||
public final int recipient;
|
||||
public final ArrayList<HomeAutomationTarget> targets;
|
||||
|
||||
public Draft(final int trigger, final int recipient,
|
||||
final ArrayList<HomeAutomationTarget> targets) {
|
||||
this.trigger = trigger;
|
||||
this.recipient = recipient;
|
||||
this.targets = targets == null ? new ArrayList<>() : targets;
|
||||
}
|
||||
|
||||
public boolean isDirty() {
|
||||
return trigger != 0 || recipient != 0 || !targets.isEmpty();
|
||||
}
|
||||
}
|
||||
|
||||
private HomeAutomationDraftStore() {
|
||||
}
|
||||
|
||||
public static boolean hasDraft(final Context context) {
|
||||
final Draft draft = load(context);
|
||||
return draft != null && draft.isDirty();
|
||||
}
|
||||
|
||||
public static Draft load(final Context context) {
|
||||
final String json = prefs(context).getString(KEY_JSON, null);
|
||||
if (TextUtils.isEmpty(json)) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
final JSONObject object = new JSONObject(json);
|
||||
final ArrayList<HomeAutomationTarget> targets = new ArrayList<>();
|
||||
final JSONArray array = object.optJSONArray("targets");
|
||||
if (array != null) {
|
||||
for (int i = 0; i < array.length(); i++) {
|
||||
final JSONObject item = array.optJSONObject(i);
|
||||
if (item == null) {
|
||||
continue;
|
||||
}
|
||||
targets.add(new HomeAutomationTarget(
|
||||
item.optString("name", null),
|
||||
item.optString("destination", null),
|
||||
item.optBoolean("group", false),
|
||||
item.optLong("contactId", -1),
|
||||
emptyToNull(item.optString("lookupKey", null)),
|
||||
emptyToNull(item.optString("iconUri", null))));
|
||||
}
|
||||
}
|
||||
return new Draft(object.optInt("trigger", 0), object.optInt("recipient", 0), targets);
|
||||
} catch (final Exception ignored) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public static void save(final Context context, final int trigger, final int recipient,
|
||||
final ArrayList<HomeAutomationTarget> targets) {
|
||||
final Draft draft = new Draft(trigger, recipient, targets);
|
||||
if (!draft.isDirty()) {
|
||||
clear(context);
|
||||
return;
|
||||
}
|
||||
try {
|
||||
final JSONObject object = new JSONObject();
|
||||
object.put("trigger", trigger);
|
||||
object.put("recipient", recipient);
|
||||
final JSONArray array = new JSONArray();
|
||||
for (final HomeAutomationTarget target : draft.targets) {
|
||||
if (target == null) {
|
||||
continue;
|
||||
}
|
||||
final JSONObject item = new JSONObject();
|
||||
item.put("name", target.name);
|
||||
item.put("destination", target.destination);
|
||||
item.put("group", target.group);
|
||||
item.put("contactId", target.contactId);
|
||||
item.put("lookupKey", target.lookupKey);
|
||||
item.put("iconUri", target.iconUri);
|
||||
array.put(item);
|
||||
}
|
||||
object.put("targets", array);
|
||||
prefs(context).edit().putString(KEY_JSON, object.toString()).apply();
|
||||
} catch (final Exception ignored) {
|
||||
}
|
||||
}
|
||||
|
||||
public static void clear(final Context context) {
|
||||
prefs(context).edit().remove(KEY_JSON).apply();
|
||||
}
|
||||
|
||||
private static String emptyToNull(final String value) {
|
||||
return TextUtils.isEmpty(value) ? null : value;
|
||||
}
|
||||
|
||||
private static SharedPreferences prefs(final Context context) {
|
||||
return context.getApplicationContext().getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,182 @@
|
||||
/*
|
||||
* Copyright (C) 2026 The LineageOS Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.android.messaging.ui.home;
|
||||
|
||||
import android.view.View;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.TextView;
|
||||
|
||||
import com.android.messaging.R;
|
||||
|
||||
/** Binds the Automation home preview and archived-rule list. */
|
||||
public final class HomeAutomationHome {
|
||||
|
||||
private HomeAutomationHome() {
|
||||
}
|
||||
|
||||
public static void bind(final View root) {
|
||||
bind(root, false);
|
||||
}
|
||||
|
||||
public static void bindArchive(final View root) {
|
||||
bind(root, true);
|
||||
}
|
||||
|
||||
public static void applyVisibility(final View root) {
|
||||
applyVisibility(root, false);
|
||||
}
|
||||
|
||||
private static void bind(final View root, final boolean archiveList) {
|
||||
if (root == null) {
|
||||
return;
|
||||
}
|
||||
bindChip(root, R.id.home_automation_rule_missed_chip1,
|
||||
R.string.home_automation_rule_missed_chip1);
|
||||
bindChip(root, R.id.home_automation_rule_missed_chip2,
|
||||
R.string.home_automation_rule_missed_chip2);
|
||||
bindChip(root, R.id.home_automation_rule_thanks_chip1,
|
||||
R.string.home_automation_rule_thanks_chip1);
|
||||
bindChip(root, R.id.home_automation_rule_thanks_chip2,
|
||||
R.string.home_automation_rule_thanks_chip2);
|
||||
bindChip(root, R.id.home_automation_rule_weekly_chip1,
|
||||
R.string.home_automation_rule_weekly_chip1);
|
||||
bindChip(root, R.id.home_automation_rule_weekly_chip2,
|
||||
R.string.home_automation_rule_weekly_chip2);
|
||||
|
||||
if (!archiveList) {
|
||||
comingSoon(root, R.id.home_automation_modes_more);
|
||||
comingSoon(root, R.id.home_automation_mode_call);
|
||||
comingSoon(root, R.id.home_automation_mode_cards);
|
||||
comingSoon(root, R.id.home_automation_rules_sort);
|
||||
}
|
||||
comingSoon(root, R.id.home_automation_rule_missed);
|
||||
comingSoon(root, R.id.home_automation_rule_thanks);
|
||||
comingSoon(root, R.id.home_automation_rule_weekly);
|
||||
|
||||
bindSwitch(root, R.id.home_automation_rule_missed_switch, true);
|
||||
bindSwitch(root, R.id.home_automation_rule_thanks_switch, true);
|
||||
bindSwitch(root, R.id.home_automation_rule_weekly_switch, true);
|
||||
|
||||
bindSwipe(root, R.id.home_automation_rule_missed, archiveList);
|
||||
bindSwipe(root, R.id.home_automation_rule_thanks, archiveList);
|
||||
bindSwipe(root, R.id.home_automation_rule_weekly, archiveList);
|
||||
applyVisibility(root, archiveList);
|
||||
}
|
||||
|
||||
private static void bindSwipe(final View root, final int id, final boolean archiveList) {
|
||||
final View card = root.findViewById(id);
|
||||
if (card == null || card.getParent() instanceof HomeSwipeCard) {
|
||||
return;
|
||||
}
|
||||
final String key = HomeAutomationRuleStore.keyForView(id);
|
||||
HomeSwipeCard.install(card, new HomeSwipeCard.Listener() {
|
||||
@Override
|
||||
public void onArchive(final HomeSwipeCard swipe) {
|
||||
if (archiveList) {
|
||||
HomeAutomationRuleStore.unarchive(root.getContext(), key);
|
||||
HomeToast.show(root.getContext(), R.string.home_automation_rule_restored);
|
||||
} else {
|
||||
HomeAutomationRuleStore.archive(root.getContext(), key);
|
||||
HomeToast.show(root.getContext(), R.string.home_automation_rule_archived);
|
||||
}
|
||||
applyVisibility(root, archiveList);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDelete(final HomeSwipeCard swipe) {
|
||||
HomeAutomationRuleStore.delete(root.getContext(), key);
|
||||
HomeToast.show(root.getContext(), R.string.home_automation_rule_deleted);
|
||||
applyVisibility(root, archiveList);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private static void applyVisibility(final View root, final boolean archiveList) {
|
||||
if (root == null) {
|
||||
return;
|
||||
}
|
||||
int visible = 0;
|
||||
for (final int id : HomeAutomationRuleStore.RULE_VIEW_IDS) {
|
||||
visible += applyRuleVisibility(root, id, archiveList) ? 1 : 0;
|
||||
}
|
||||
final TextView count = root.findViewById(R.id.home_automation_rules_count);
|
||||
if (count != null) {
|
||||
count.setText(String.valueOf(visible));
|
||||
count.setVisibility(visible > 0 ? View.VISIBLE : View.GONE);
|
||||
}
|
||||
final View empty = root.findViewById(R.id.home_archived_rules_empty);
|
||||
if (empty != null) {
|
||||
empty.setVisibility(archiveList && visible == 0 ? View.VISIBLE : View.GONE);
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean applyRuleVisibility(final View root, final int id,
|
||||
final boolean archiveList) {
|
||||
View card = root.findViewById(id);
|
||||
if (card == null) {
|
||||
return false;
|
||||
}
|
||||
if (card.getParent() instanceof HomeSwipeCard) {
|
||||
card = (View) card.getParent();
|
||||
}
|
||||
final String key = HomeAutomationRuleStore.keyForView(id);
|
||||
final boolean deleted = HomeAutomationRuleStore.isDeleted(root.getContext(), key);
|
||||
final boolean archived = HomeAutomationRuleStore.isArchived(root.getContext(), key);
|
||||
final boolean show = archiveList ? (!deleted && archived) : (!deleted && !archived);
|
||||
if (card instanceof HomeSwipeCard) {
|
||||
if (show) {
|
||||
((HomeSwipeCard) card).setSwipeTranslationX(0f);
|
||||
}
|
||||
}
|
||||
card.setVisibility(show ? View.VISIBLE : View.GONE);
|
||||
return show;
|
||||
}
|
||||
|
||||
private static void bindChip(final View root, final int chipId, final int labelId) {
|
||||
final View chip = root.findViewById(chipId);
|
||||
if (chip == null) {
|
||||
return;
|
||||
}
|
||||
final TextView label = chip.findViewById(R.id.home_automation_chip_label);
|
||||
if (label != null) {
|
||||
label.setText(labelId);
|
||||
}
|
||||
}
|
||||
|
||||
private static void comingSoon(final View root, final int id) {
|
||||
final View view = root.findViewById(id);
|
||||
if (view != null) {
|
||||
view.setOnClickListener(v -> HomeToast.show(v.getContext(),
|
||||
R.string.home_automate_coming_soon));
|
||||
}
|
||||
}
|
||||
|
||||
private static void bindSwitch(final View root, final int id, final boolean on) {
|
||||
final View view = root.findViewById(id);
|
||||
if (!(view instanceof ImageView)) {
|
||||
return;
|
||||
}
|
||||
final ImageView toggle = (ImageView) view;
|
||||
applySwitch(toggle, on);
|
||||
toggle.setOnClickListener(v -> applySwitch(toggle, !toggle.isSelected()));
|
||||
}
|
||||
|
||||
private static void applySwitch(final ImageView toggle, final boolean on) {
|
||||
toggle.setSelected(on);
|
||||
toggle.setContentDescription(toggle.getResources().getString(
|
||||
on ? R.string.home_automation_on : R.string.home_automation_off));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,120 @@
|
||||
/*
|
||||
* Copyright (C) 2026 The LineageOS Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.android.messaging.ui.home;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.SharedPreferences;
|
||||
|
||||
import com.android.messaging.R;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
|
||||
/** Local archive/delete flags for the demo automation rules. */
|
||||
public final class HomeAutomationRuleStore {
|
||||
public static final String MISSED = "missed";
|
||||
public static final String THANKS = "thanks";
|
||||
public static final String WEEKLY = "weekly";
|
||||
|
||||
static final int[] RULE_VIEW_IDS = {
|
||||
R.id.home_automation_rule_missed,
|
||||
R.id.home_automation_rule_thanks,
|
||||
R.id.home_automation_rule_weekly
|
||||
};
|
||||
|
||||
private static final String PREFS = "home_automation_rules";
|
||||
private static final String KEY_ARCHIVED = "archived";
|
||||
private static final String KEY_DELETED = "deleted";
|
||||
|
||||
private HomeAutomationRuleStore() {
|
||||
}
|
||||
|
||||
static String keyForView(final int viewId) {
|
||||
if (viewId == R.id.home_automation_rule_missed) {
|
||||
return MISSED;
|
||||
}
|
||||
if (viewId == R.id.home_automation_rule_thanks) {
|
||||
return THANKS;
|
||||
}
|
||||
if (viewId == R.id.home_automation_rule_weekly) {
|
||||
return WEEKLY;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public static boolean isArchived(final Context context, final String key) {
|
||||
return set(context, KEY_ARCHIVED).contains(key);
|
||||
}
|
||||
|
||||
public static boolean isDeleted(final Context context, final String key) {
|
||||
return set(context, KEY_DELETED).contains(key);
|
||||
}
|
||||
|
||||
public static void archive(final Context context, final String key) {
|
||||
add(context, KEY_ARCHIVED, key);
|
||||
remove(context, KEY_DELETED, key);
|
||||
}
|
||||
|
||||
public static void unarchive(final Context context, final String key) {
|
||||
remove(context, KEY_ARCHIVED, key);
|
||||
}
|
||||
|
||||
public static void delete(final Context context, final String key) {
|
||||
add(context, KEY_DELETED, key);
|
||||
remove(context, KEY_ARCHIVED, key);
|
||||
}
|
||||
|
||||
public static boolean hasArchived(final Context context) {
|
||||
if (context == null) {
|
||||
return false;
|
||||
}
|
||||
for (final String key : set(context, KEY_ARCHIVED)) {
|
||||
if (!isDeleted(context, key)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private static Set<String> set(final Context context, final String key) {
|
||||
return new HashSet<>(prefs(context).getStringSet(key, Collections.emptySet()));
|
||||
}
|
||||
|
||||
private static void add(final Context context, final String key, final String value) {
|
||||
if (value == null) {
|
||||
return;
|
||||
}
|
||||
final Set<String> next = set(context, key);
|
||||
if (next.add(value)) {
|
||||
prefs(context).edit().putStringSet(key, next).apply();
|
||||
}
|
||||
}
|
||||
|
||||
private static void remove(final Context context, final String key, final String value) {
|
||||
if (value == null) {
|
||||
return;
|
||||
}
|
||||
final Set<String> next = set(context, key);
|
||||
if (next.remove(value)) {
|
||||
prefs(context).edit().putStringSet(key, next).apply();
|
||||
}
|
||||
}
|
||||
|
||||
private static SharedPreferences prefs(final Context context) {
|
||||
return context.getApplicationContext().getSharedPreferences(PREFS, Context.MODE_PRIVATE);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
/*
|
||||
* Copyright (C) 2026 The LineageOS Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.android.messaging.ui.home;
|
||||
|
||||
import android.os.Parcel;
|
||||
import android.os.Parcelable;
|
||||
import android.text.TextUtils;
|
||||
|
||||
/** A contact or group chosen as an automation include/exclude target. */
|
||||
public final class HomeAutomationTarget implements Parcelable {
|
||||
|
||||
public final String name;
|
||||
public final String destination;
|
||||
public final boolean group;
|
||||
public final long contactId;
|
||||
public final String lookupKey;
|
||||
public final String iconUri;
|
||||
|
||||
public HomeAutomationTarget(final String name, final String destination, final boolean group,
|
||||
final long contactId, final String lookupKey, final String iconUri) {
|
||||
this.name = name;
|
||||
this.destination = destination;
|
||||
this.group = group;
|
||||
this.contactId = contactId;
|
||||
this.lookupKey = lookupKey;
|
||||
this.iconUri = iconUri;
|
||||
}
|
||||
|
||||
protected HomeAutomationTarget(final Parcel in) {
|
||||
name = in.readString();
|
||||
destination = in.readString();
|
||||
group = in.readInt() == 1;
|
||||
contactId = in.readLong();
|
||||
lookupKey = in.readString();
|
||||
iconUri = in.readString();
|
||||
}
|
||||
|
||||
public String key() {
|
||||
return (group ? "g:" : "c:") + (destination == null ? "" : destination);
|
||||
}
|
||||
|
||||
public String displayName() {
|
||||
return TextUtils.isEmpty(name) ? destination : name;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void writeToParcel(final Parcel dest, final int flags) {
|
||||
dest.writeString(name);
|
||||
dest.writeString(destination);
|
||||
dest.writeInt(group ? 1 : 0);
|
||||
dest.writeLong(contactId);
|
||||
dest.writeString(lookupKey);
|
||||
dest.writeString(iconUri);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int describeContents() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
public static final Creator<HomeAutomationTarget> CREATOR =
|
||||
new Creator<HomeAutomationTarget>() {
|
||||
@Override
|
||||
public HomeAutomationTarget createFromParcel(final Parcel in) {
|
||||
return new HomeAutomationTarget(in);
|
||||
}
|
||||
|
||||
@Override
|
||||
public HomeAutomationTarget[] newArray(final int size) {
|
||||
return new HomeAutomationTarget[size];
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,372 @@
|
||||
/*
|
||||
* Copyright (C) 2026 The LineageOS Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.android.messaging.ui.home;
|
||||
|
||||
import android.database.Cursor;
|
||||
import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.BaseAdapter;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.ListView;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.activity.OnBackPressedCallback;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.core.content.ContextCompat;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import androidx.fragment.app.FragmentManager;
|
||||
import androidx.loader.app.LoaderManager;
|
||||
import androidx.loader.content.CursorLoader;
|
||||
import androidx.loader.content.Loader;
|
||||
|
||||
import com.android.ex.chips.RecipientEntry;
|
||||
import com.android.messaging.R;
|
||||
import com.android.messaging.datamodel.data.ParticipantData;
|
||||
import com.android.messaging.ui.ContactIconView;
|
||||
import com.android.messaging.util.AvatarUriUtil;
|
||||
import com.android.messaging.util.ContactRecipientEntryUtils;
|
||||
import com.android.messaging.util.ContactUtil;
|
||||
import com.android.messaging.util.PhoneUtils;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
|
||||
/** In-window sheet to pick contacts and contact groups as automation targets. */
|
||||
public class HomeAutomationTargetPickerSheet extends Fragment
|
||||
implements HomeContactsFragment.GroupSelectionHost, LoaderManager.LoaderCallbacks<Cursor> {
|
||||
|
||||
public static final String REQUEST_KEY = "home_automation_target_picker";
|
||||
public static final String EXTRA_TARGETS = "home_automation_targets";
|
||||
|
||||
private static final String TAG = "home_automation_target_picker";
|
||||
private static final int GROUPS_LOADER = 1;
|
||||
|
||||
private View mDim;
|
||||
private View mPanel;
|
||||
private boolean mClosing;
|
||||
private TextView mContactsTab;
|
||||
private TextView mGroupsTab;
|
||||
private View mContactsHost;
|
||||
private ListView mGroupsList;
|
||||
private HomeEmptyState mGroupsEmpty;
|
||||
private final LinkedHashMap<String, HomeAutomationTarget> mSelectedGroups =
|
||||
new LinkedHashMap<>();
|
||||
private final ArrayList<HomeContactGroups.Item> mGroups = new ArrayList<>();
|
||||
private final GroupAdapter mGroupAdapter = new GroupAdapter();
|
||||
private boolean mShowGroups;
|
||||
private boolean mContactsSeeded;
|
||||
|
||||
public static void show(@NonNull final FragmentManager fragmentManager,
|
||||
@Nullable final ArrayList<HomeAutomationTarget> incoming) {
|
||||
final HomeAutomationTargetPickerSheet sheet = new HomeAutomationTargetPickerSheet();
|
||||
final Bundle args = new Bundle();
|
||||
args.putParcelableArrayList(EXTRA_TARGETS, incoming);
|
||||
if (fragmentManager.findFragmentByTag(TAG) != null) {
|
||||
return;
|
||||
}
|
||||
sheet.setArguments(args);
|
||||
fragmentManager.beginTransaction()
|
||||
.replace(R.id.home_automation_picker_host, sheet, TAG)
|
||||
.commit();
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public View onCreateView(@NonNull final LayoutInflater inflater,
|
||||
@Nullable final ViewGroup container, @Nullable final Bundle savedInstanceState) {
|
||||
return inflater.inflate(R.layout.home_automation_target_picker_sheet, container, false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onViewCreated(@NonNull final View view, @Nullable final Bundle savedInstanceState) {
|
||||
super.onViewCreated(view, savedInstanceState);
|
||||
mDim = view.findViewById(R.id.home_automation_picker_dim);
|
||||
mPanel = view.findViewById(R.id.home_automation_picker_panel);
|
||||
if (mDim != null) {
|
||||
mDim.setOnClickListener(v -> dismissSheet());
|
||||
}
|
||||
view.findViewById(R.id.home_automation_picker_close).setOnClickListener(v -> dismissSheet());
|
||||
view.findViewById(R.id.home_automation_picker_done).setOnClickListener(v -> finishWithResult());
|
||||
final View host = requireActivity().findViewById(R.id.home_automation_picker_host);
|
||||
if (host != null) {
|
||||
host.setVisibility(View.VISIBLE);
|
||||
}
|
||||
requireActivity().getOnBackPressedDispatcher().addCallback(getViewLifecycleOwner(),
|
||||
new OnBackPressedCallback(true) {
|
||||
@Override
|
||||
public void handleOnBackPressed() {
|
||||
dismissSheet();
|
||||
}
|
||||
});
|
||||
if (savedInstanceState == null) {
|
||||
view.post(() -> HomeUiMotion.gallerySheetOpen(mDim, mPanel));
|
||||
}
|
||||
|
||||
mContactsTab = view.findViewById(R.id.home_automation_picker_contacts);
|
||||
mGroupsTab = view.findViewById(R.id.home_automation_picker_groups);
|
||||
mContactsHost = view.findViewById(R.id.home_automation_picker_contacts_host);
|
||||
mGroupsList = view.findViewById(R.id.home_automation_picker_groups_list);
|
||||
mGroupsEmpty = view.findViewById(R.id.home_automation_picker_groups_empty);
|
||||
mGroupsList.setAdapter(mGroupAdapter);
|
||||
if (mGroupsEmpty != null) {
|
||||
mGroupsEmpty.setContent(R.drawable.ic_home_automation_person,
|
||||
R.string.home_automation_no_groups_title,
|
||||
R.string.home_automation_no_groups_body);
|
||||
}
|
||||
|
||||
HomeContactGroups.ensureDemo(requireContext());
|
||||
seedIncoming(getArguments() == null ? null
|
||||
: getArguments().getParcelableArrayList(EXTRA_TARGETS));
|
||||
mContactsTab.setOnClickListener(v -> showGroups(false));
|
||||
mGroupsTab.setOnClickListener(v -> showGroups(true));
|
||||
showGroups(false);
|
||||
|
||||
prepareContactsFragment();
|
||||
if (ContactUtil.hasReadContactsPermission()) {
|
||||
LoaderManager.getInstance(this).initLoader(GROUPS_LOADER, null, this);
|
||||
}
|
||||
}
|
||||
|
||||
private void dismissSheet() {
|
||||
if (mClosing || !isAdded()) {
|
||||
return;
|
||||
}
|
||||
mClosing = true;
|
||||
HomeUiMotion.gallerySheetClose(mDim, mPanel, () -> {
|
||||
if (!isAdded()) {
|
||||
return;
|
||||
}
|
||||
final View host = requireActivity().findViewById(R.id.home_automation_picker_host);
|
||||
getParentFragmentManager().beginTransaction().remove(this).commitAllowingStateLoss();
|
||||
if (host != null) {
|
||||
host.setVisibility(View.GONE);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void prepareContactsFragment() {
|
||||
HomeContactsFragment fragment = getContactsFragment();
|
||||
if (fragment == null) {
|
||||
fragment = new HomeContactsFragment();
|
||||
getChildFragmentManager().beginTransaction()
|
||||
.replace(R.id.home_automation_picker_contacts_host, fragment)
|
||||
.commitNow();
|
||||
}
|
||||
fragment.setGroupMode(true);
|
||||
if (!mContactsSeeded) {
|
||||
fragment.seedSelection(contactSeeds());
|
||||
mContactsSeeded = true;
|
||||
}
|
||||
}
|
||||
|
||||
private ArrayList<String> contactSeeds() {
|
||||
final ArrayList<HomeAutomationTarget> incoming = getArguments() == null
|
||||
? null : getArguments().getParcelableArrayList(EXTRA_TARGETS);
|
||||
final ArrayList<String> seeds = new ArrayList<>();
|
||||
if (incoming == null) {
|
||||
return seeds;
|
||||
}
|
||||
for (final HomeAutomationTarget target : incoming) {
|
||||
if (target != null && !target.group && target.destination != null) {
|
||||
seeds.add(target.destination);
|
||||
}
|
||||
}
|
||||
return seeds;
|
||||
}
|
||||
|
||||
private void seedIncoming(final ArrayList<HomeAutomationTarget> incoming) {
|
||||
mSelectedGroups.clear();
|
||||
if (incoming == null) {
|
||||
return;
|
||||
}
|
||||
for (final HomeAutomationTarget target : incoming) {
|
||||
if (target != null && target.group && target.destination != null) {
|
||||
mSelectedGroups.put(target.destination, target);
|
||||
}
|
||||
}
|
||||
mGroupAdapter.notifyDataSetChanged();
|
||||
}
|
||||
|
||||
private HomeContactsFragment getContactsFragment() {
|
||||
return (HomeContactsFragment) getChildFragmentManager()
|
||||
.findFragmentById(R.id.home_automation_picker_contacts_host);
|
||||
}
|
||||
|
||||
private void showGroups(final boolean groups) {
|
||||
mShowGroups = groups;
|
||||
applyPill(mContactsTab, !groups);
|
||||
applyPill(mGroupsTab, groups);
|
||||
if (mContactsHost != null) {
|
||||
mContactsHost.setVisibility(groups ? View.GONE : View.VISIBLE);
|
||||
}
|
||||
updateGroupsVisibility();
|
||||
}
|
||||
|
||||
private void updateGroupsVisibility() {
|
||||
final boolean empty = mGroups.isEmpty();
|
||||
if (mGroupsList != null) {
|
||||
mGroupsList.setVisibility(mShowGroups && !empty ? View.VISIBLE : View.GONE);
|
||||
}
|
||||
if (mGroupsEmpty != null) {
|
||||
mGroupsEmpty.setVisibility(mShowGroups && empty ? View.VISIBLE : View.GONE);
|
||||
}
|
||||
}
|
||||
|
||||
private void applyPill(final TextView pill, final boolean selected) {
|
||||
if (pill == null) {
|
||||
return;
|
||||
}
|
||||
pill.setBackgroundResource(selected
|
||||
? R.drawable.home_bg_automation_pill_selected
|
||||
: R.drawable.home_bg_automation_pill);
|
||||
pill.setTextColor(ContextCompat.getColor(requireContext(),
|
||||
selected ? android.R.color.white : R.color.home_text_primary));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onGroupSelectionChanged(final List<RecipientEntry> selected) {
|
||||
// Selection is read from the fragment when Done is pressed.
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public Loader<Cursor> onCreateLoader(final int id, @Nullable final Bundle args) {
|
||||
return new CursorLoader(requireContext(), HomeContactGroups.SUMMARY_URI,
|
||||
HomeContactGroups.PROJECTION, HomeContactGroups.SELECTION, null,
|
||||
HomeContactGroups.SORT);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLoadFinished(@NonNull final Loader<Cursor> loader, final Cursor data) {
|
||||
mGroups.clear();
|
||||
if (data != null && data.moveToFirst()) {
|
||||
do {
|
||||
final HomeContactGroups.Item item = HomeContactGroups.fromCursor(data);
|
||||
if (!item.title.isEmpty()) {
|
||||
mGroups.add(item);
|
||||
}
|
||||
} while (data.moveToNext());
|
||||
}
|
||||
mGroupAdapter.notifyDataSetChanged();
|
||||
updateGroupsVisibility();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLoaderReset(@NonNull final Loader<Cursor> loader) {
|
||||
mGroups.clear();
|
||||
mGroupAdapter.notifyDataSetChanged();
|
||||
updateGroupsVisibility();
|
||||
}
|
||||
|
||||
private void finishWithResult() {
|
||||
final LinkedHashMap<String, HomeAutomationTarget> targets = new LinkedHashMap<>();
|
||||
final HomeContactsFragment fragment = getContactsFragment();
|
||||
if (fragment != null) {
|
||||
for (final RecipientEntry entry : fragment.getSelectedEntries()) {
|
||||
final HomeAutomationTarget target = fromEntry(entry);
|
||||
targets.put(target.key(), target);
|
||||
}
|
||||
}
|
||||
for (final HomeAutomationTarget group : mSelectedGroups.values()) {
|
||||
targets.put(group.key(), group);
|
||||
}
|
||||
final Bundle result = new Bundle();
|
||||
result.putParcelableArrayList(EXTRA_TARGETS, new ArrayList<>(targets.values()));
|
||||
getParentFragmentManager().setFragmentResult(REQUEST_KEY, result);
|
||||
dismissSheet();
|
||||
}
|
||||
|
||||
private static HomeAutomationTarget fromEntry(final RecipientEntry entry) {
|
||||
final ParticipantData participant = ParticipantData.getFromRecipientEntry(entry);
|
||||
final Uri icon = AvatarUriUtil.createAvatarUri(participant);
|
||||
return new HomeAutomationTarget(
|
||||
ContactRecipientEntryUtils.getDisplayNameForContactList(entry),
|
||||
PhoneUtils.getDefault().getCanonicalBySystemLocale(entry.getDestination()),
|
||||
false,
|
||||
participant.getContactId(),
|
||||
participant.getLookupKey(),
|
||||
icon == null ? null : icon.toString());
|
||||
}
|
||||
|
||||
private static HomeAutomationTarget fromGroup(final HomeContactGroups.Item item) {
|
||||
final Uri icon = item.avatarUri();
|
||||
return new HomeAutomationTarget(item.title, item.destination(), true, 0, null,
|
||||
icon == null ? null : icon.toString());
|
||||
}
|
||||
|
||||
private void applyCheck(final View check, final boolean selected) {
|
||||
if (check instanceof ImageView) {
|
||||
HomeAvatarOverlay.apply((ImageView) check, true, selected);
|
||||
}
|
||||
}
|
||||
|
||||
private final class GroupAdapter extends BaseAdapter {
|
||||
@Override
|
||||
public int getCount() {
|
||||
return mGroups.size();
|
||||
}
|
||||
|
||||
@Override
|
||||
public HomeContactGroups.Item getItem(final int position) {
|
||||
return mGroups.get(position);
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getItemId(final int position) {
|
||||
return getItem(position).id;
|
||||
}
|
||||
|
||||
@Override
|
||||
public View getView(final int position, final View convertView, final ViewGroup parent) {
|
||||
final View row = convertView != null ? convertView
|
||||
: LayoutInflater.from(parent.getContext())
|
||||
.inflate(R.layout.home_automation_target_row, parent, false);
|
||||
final HomeContactGroups.Item item = getItem(position);
|
||||
final ContactIconView avatar = row.findViewById(R.id.home_automation_target_avatar);
|
||||
final TextView name = row.findViewById(R.id.home_automation_target_name);
|
||||
final TextView meta = row.findViewById(R.id.home_automation_target_meta);
|
||||
final View check = row.findViewById(R.id.home_automation_target_check);
|
||||
avatar.setImageResourceUri(item.avatarUri());
|
||||
avatar.setImageClickHandlerDisabled(true);
|
||||
name.setText(item.title);
|
||||
if (meta != null) {
|
||||
if (item.memberCount > 0) {
|
||||
meta.setVisibility(View.VISIBLE);
|
||||
meta.setText(getResources().getQuantityString(
|
||||
R.plurals.home_automation_group_member_count, item.memberCount,
|
||||
item.memberCount));
|
||||
} else {
|
||||
meta.setVisibility(View.GONE);
|
||||
}
|
||||
}
|
||||
final boolean selected = mSelectedGroups.containsKey(item.destination());
|
||||
applyCheck(check, selected);
|
||||
row.setOnClickListener(v -> {
|
||||
final String id = item.destination();
|
||||
if (mSelectedGroups.remove(id) == null) {
|
||||
mSelectedGroups.put(id, fromGroup(item));
|
||||
}
|
||||
notifyDataSetChanged();
|
||||
});
|
||||
return row;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
/*
|
||||
* Copyright (C) 2026 The LineageOS Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License and the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.android.messaging.ui.home;
|
||||
|
||||
import android.view.View;
|
||||
import android.widget.ImageView;
|
||||
|
||||
import com.android.messaging.R;
|
||||
|
||||
/** Avatar wash + check used while a people/message list is in edit/selection mode. */
|
||||
public final class HomeAvatarOverlay {
|
||||
private HomeAvatarOverlay() {
|
||||
}
|
||||
|
||||
public static void apply(final ImageView overlay, final boolean selectionMode,
|
||||
final boolean selected) {
|
||||
if (overlay == null) {
|
||||
return;
|
||||
}
|
||||
if (!selectionMode) {
|
||||
overlay.animate().cancel();
|
||||
overlay.setBackground(null);
|
||||
overlay.setImageDrawable(null);
|
||||
overlay.setPadding(0, 0, 0, 0);
|
||||
overlay.setVisibility(View.GONE);
|
||||
overlay.setAlpha(1f);
|
||||
overlay.setScaleX(1f);
|
||||
overlay.setScaleY(1f);
|
||||
return;
|
||||
}
|
||||
overlay.setBackgroundResource(selected
|
||||
? R.drawable.home_bg_avatar_selected_wash
|
||||
: R.drawable.home_bg_avatar_edit);
|
||||
if (selected) {
|
||||
overlay.setScaleType(ImageView.ScaleType.FIT_CENTER);
|
||||
applyHeaderCheckRatio(overlay);
|
||||
overlay.setImageResource(R.drawable.ic_home_check_stroke);
|
||||
} else {
|
||||
overlay.setPadding(0, 0, 0, 0);
|
||||
overlay.setImageDrawable(null);
|
||||
}
|
||||
overlay.setVisibility(View.VISIBLE);
|
||||
}
|
||||
|
||||
/**
|
||||
* Matches {@code ic_checkmark_circle_primary}: the tick is 16.3/42 of the circle. The
|
||||
* stroke asset has extra viewport padding, so inset is reduced to keep that same visible size.
|
||||
*/
|
||||
private static void applyHeaderCheckRatio(final ImageView overlay) {
|
||||
int side = overlay.getWidth();
|
||||
if (side <= 0 && overlay.getLayoutParams() != null) {
|
||||
side = overlay.getLayoutParams().width;
|
||||
}
|
||||
if (side <= 0) {
|
||||
side = overlay.getResources().getDimensionPixelSize(R.dimen.home_list_avatar_size);
|
||||
}
|
||||
final int inset = Math.round(side * ((42f - 16.3f) / 42f / 2f) * (16f / 24f));
|
||||
overlay.setPadding(inset, inset, inset, inset);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
package com.android.messaging.ui.home;
|
||||
|
||||
import android.content.Context;
|
||||
import android.text.TextUtils;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.view.Window;
|
||||
import android.view.WindowManager;
|
||||
import android.widget.FrameLayout;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.StringRes;
|
||||
|
||||
import com.android.messaging.R;
|
||||
import com.google.android.material.bottomsheet.BottomSheetDialog;
|
||||
|
||||
/** Default home bottom sheet: handle, close X, optional title, custom body, dismiss on outside tap. */
|
||||
public final class HomeBottomSheet {
|
||||
|
||||
public static final class Builder {
|
||||
private final Context mContext;
|
||||
private CharSequence mTitle;
|
||||
private View mCustom;
|
||||
private boolean mCancelable = true;
|
||||
|
||||
public Builder(@NonNull final Context context) {
|
||||
mContext = context;
|
||||
}
|
||||
|
||||
public Builder title(@StringRes final int titleRes) {
|
||||
mTitle = mContext.getString(titleRes);
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder title(final CharSequence title) {
|
||||
mTitle = title;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder custom(final View custom) {
|
||||
mCustom = custom;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder cancelable(final boolean cancelable) {
|
||||
mCancelable = cancelable;
|
||||
return this;
|
||||
}
|
||||
|
||||
public BottomSheetDialog show() {
|
||||
return HomeBottomSheet.show(this);
|
||||
}
|
||||
}
|
||||
|
||||
private HomeBottomSheet() {
|
||||
}
|
||||
|
||||
public static Builder builder(@NonNull final Context context) {
|
||||
return new Builder(context);
|
||||
}
|
||||
|
||||
private static BottomSheetDialog show(final Builder options) {
|
||||
final Context context = options.mContext;
|
||||
final View root = LayoutInflater.from(context).inflate(R.layout.home_bottom_sheet, null, false);
|
||||
final TextView title = root.findViewById(R.id.home_sheet_title);
|
||||
if (title != null) {
|
||||
title.setText(options.mTitle);
|
||||
title.setVisibility(TextUtils.isEmpty(options.mTitle) ? View.GONE : View.VISIBLE);
|
||||
}
|
||||
final FrameLayout custom = root.findViewById(R.id.home_sheet_custom);
|
||||
if (custom != null && options.mCustom != null) {
|
||||
if (options.mCustom.getParent() instanceof ViewGroup) {
|
||||
((ViewGroup) options.mCustom.getParent()).removeView(options.mCustom);
|
||||
}
|
||||
custom.addView(options.mCustom, new FrameLayout.LayoutParams(
|
||||
ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT));
|
||||
}
|
||||
|
||||
final BottomSheetDialog dialog = new BottomSheetDialog(context,
|
||||
R.style.Home_BottomSheetDialog);
|
||||
dialog.setContentView(root);
|
||||
dialog.setCancelable(options.mCancelable);
|
||||
dialog.setCanceledOnTouchOutside(options.mCancelable);
|
||||
final View close = root.findViewById(R.id.home_sheet_close);
|
||||
if (close != null) {
|
||||
close.setOnClickListener(v -> dialog.dismiss());
|
||||
close.setVisibility(options.mCancelable ? View.VISIBLE : View.GONE);
|
||||
}
|
||||
final Window window = dialog.getWindow();
|
||||
if (window != null) {
|
||||
window.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE);
|
||||
}
|
||||
dialog.show();
|
||||
HomeInputFocus.install(dialog.getWindow());
|
||||
return dialog;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
package com.android.messaging.ui.home;
|
||||
|
||||
import android.content.Context;
|
||||
import android.text.TextUtils;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.widget.EditText;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import com.android.messaging.R;
|
||||
import com.android.messaging.util.ImeUtil;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
/** Shared Add category name modal. */
|
||||
public final class HomeCategoryAddModal {
|
||||
|
||||
public interface Listener {
|
||||
void onCategoryCreated(HomeCategoryStore.Category created);
|
||||
}
|
||||
|
||||
private static boolean sShowing;
|
||||
|
||||
private HomeCategoryAddModal() {
|
||||
}
|
||||
|
||||
public static void show(@NonNull final Context context, @Nullable final Listener listener) {
|
||||
if (sShowing) {
|
||||
return;
|
||||
}
|
||||
sShowing = true;
|
||||
final View content = LayoutInflater.from(context)
|
||||
.inflate(R.layout.home_category_name_dialog, null, false);
|
||||
final EditText field = content.findViewById(R.id.home_category_name_field);
|
||||
final HomeModal.Handle handle = HomeModal.builder(context)
|
||||
.icon(R.drawable.ic_home_category)
|
||||
.title(R.string.home_category_add)
|
||||
.message(R.string.home_category_name_message)
|
||||
.custom(content)
|
||||
.negative(android.R.string.cancel, null)
|
||||
.positive(R.string.home_category_add_new, () -> {
|
||||
final String name = field == null ? "" : field.getText().toString();
|
||||
final HomeCategoryStore.Category created = tryCreate(context, name, "",
|
||||
Collections.emptyList());
|
||||
if (created != null && listener != null) {
|
||||
listener.onCategoryCreated(created);
|
||||
}
|
||||
})
|
||||
.show();
|
||||
if (handle == null || handle.dialog == null) {
|
||||
sShowing = false;
|
||||
return;
|
||||
}
|
||||
handle.dialog.setOnDismissListener(dialog -> sShowing = false);
|
||||
if (field != null) {
|
||||
field.requestFocus();
|
||||
ImeUtil.get().showImeKeyboard(context, field);
|
||||
}
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public static HomeCategoryStore.Category tryCreate(@NonNull final Context context,
|
||||
final String name, final String kind, @Nullable final List<String> conversationIds) {
|
||||
final String trimmed = name == null ? "" : name.trim();
|
||||
if (TextUtils.isEmpty(trimmed)) {
|
||||
return null;
|
||||
}
|
||||
if (HomeCategoryStore.hasName(context, trimmed)) {
|
||||
HomeToast.show(context, R.string.home_category_exists_toast);
|
||||
return null;
|
||||
}
|
||||
if (HomeCategoryStore.listCustom(context).size() >= HomeCategoryStore.MAX_CUSTOM) {
|
||||
HomeToast.show(context, R.string.home_category_limit_toast);
|
||||
return null;
|
||||
}
|
||||
final HomeCategoryStore.Category created = HomeCategoryStore.addCustom(context, trimmed,
|
||||
kind);
|
||||
if (created == null) {
|
||||
HomeToast.show(context, R.string.home_category_exists_toast);
|
||||
return null;
|
||||
}
|
||||
if (conversationIds != null && !conversationIds.isEmpty()) {
|
||||
HomeCategoryStore.assignAll(context, conversationIds, created.id);
|
||||
}
|
||||
HomeToast.show(context, R.string.home_category_added_toast);
|
||||
return created;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,134 @@
|
||||
package com.android.messaging.ui.home;
|
||||
|
||||
import android.content.Context;
|
||||
import android.text.TextUtils;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.core.content.ContextCompat;
|
||||
|
||||
import com.android.messaging.R;
|
||||
import com.android.messaging.datamodel.data.ConversationListItemData;
|
||||
import com.google.android.material.chip.Chip;
|
||||
import com.google.android.material.chip.ChipGroup;
|
||||
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
/** Assigns a conversation to categories using the default home modal. */
|
||||
public final class HomeCategoryAssignSheet {
|
||||
|
||||
public interface Listener {
|
||||
void onAssignmentsChanged();
|
||||
}
|
||||
|
||||
private HomeCategoryAssignSheet() {
|
||||
}
|
||||
|
||||
public static void show(@NonNull final Context context,
|
||||
@NonNull final ConversationListItemData data, final Listener listener) {
|
||||
final String conversationId = data.getConversationId();
|
||||
if (TextUtils.isEmpty(conversationId)) {
|
||||
return;
|
||||
}
|
||||
final View custom = LayoutInflater.from(context)
|
||||
.inflate(R.layout.home_category_assign_sheet, null, false);
|
||||
final ChipGroup chips = custom.findViewById(R.id.home_category_assign_chips);
|
||||
final Set<String> selected = new LinkedHashSet<>(
|
||||
HomeCategoryStore.assignedIds(context, conversationId));
|
||||
if (!selected.contains(HomeCategoryStore.ID_PERSONAL)
|
||||
&& !selected.contains(HomeCategoryStore.ID_PROMOTION)) {
|
||||
selected.add(HomeCategoryClassifier.isPromotion(data)
|
||||
? HomeCategoryStore.ID_PROMOTION : HomeCategoryStore.ID_PERSONAL);
|
||||
}
|
||||
|
||||
final Runnable[] bindChips = new Runnable[1];
|
||||
bindChips[0] = () -> {
|
||||
if (chips == null) {
|
||||
return;
|
||||
}
|
||||
chips.removeAllViews();
|
||||
final List<HomeCategoryStore.Category> categories =
|
||||
HomeCategoryStore.listTabs(context);
|
||||
for (final HomeCategoryStore.Category category : categories) {
|
||||
if (HomeCategoryStore.ID_ALL.equals(category.id)) {
|
||||
continue;
|
||||
}
|
||||
final Chip chip = new Chip(context);
|
||||
chip.setText(category.name);
|
||||
chip.setCheckable(true);
|
||||
chip.setCheckedIconVisible(false);
|
||||
chip.setChecked(selected.contains(category.id));
|
||||
styleChip(chip);
|
||||
chip.setOnCheckedChangeListener((button, isChecked) -> {
|
||||
if (isChecked) {
|
||||
selected.add(category.id);
|
||||
} else {
|
||||
selected.remove(category.id);
|
||||
}
|
||||
styleChip(chip);
|
||||
});
|
||||
chips.addView(chip);
|
||||
}
|
||||
final Chip addChip = new Chip(context);
|
||||
addChip.setText(R.string.home_category_add_new);
|
||||
addChip.setCheckable(false);
|
||||
addChip.setCheckedIconVisible(false);
|
||||
styleChip(addChip);
|
||||
addChip.setOnClickListener(v -> HomeCategoryAddModal.show(context, created -> {
|
||||
selected.add(created.id);
|
||||
bindChips[0].run();
|
||||
}));
|
||||
chips.addView(addChip);
|
||||
};
|
||||
|
||||
bindChips[0].run();
|
||||
HomeModal.builder(context)
|
||||
.icon(R.drawable.ic_home_category)
|
||||
.title(R.string.home_category_assign)
|
||||
.message(R.string.home_category_assign_message)
|
||||
.custom(custom)
|
||||
.negative(android.R.string.cancel, null)
|
||||
.positive(R.string.home_category_done, () -> {
|
||||
applyAssignments(context, conversationId, selected);
|
||||
if (listener != null) {
|
||||
listener.onAssignmentsChanged();
|
||||
}
|
||||
HomeToast.show(context, R.string.home_category_updated_toast);
|
||||
})
|
||||
.show();
|
||||
}
|
||||
|
||||
private static void applyAssignments(final Context context, final String conversationId,
|
||||
final Set<String> selected) {
|
||||
final Set<String> current = HomeCategoryStore.assignedIds(context, conversationId);
|
||||
for (final String id : selected) {
|
||||
if (!current.contains(id)) {
|
||||
HomeCategoryStore.assign(context, conversationId, id);
|
||||
}
|
||||
}
|
||||
for (final String id : current) {
|
||||
if (!selected.contains(id)) {
|
||||
HomeCategoryStore.unassign(context, conversationId, id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void styleChip(final Chip chip) {
|
||||
final float density = chip.getResources().getDisplayMetrics().density;
|
||||
chip.setChipBackgroundColorResource(R.color.home_category_chip_bg);
|
||||
chip.setChipStrokeColorResource(R.color.home_category_chip_stroke);
|
||||
chip.setChipStrokeWidth(density);
|
||||
chip.setChipMinHeight(36f * density);
|
||||
chip.setChipStartPadding(12f * density);
|
||||
chip.setChipEndPadding(12f * density);
|
||||
chip.setTextSize(14f);
|
||||
chip.setTypeface(chip.getTypeface(), android.graphics.Typeface.BOLD);
|
||||
chip.setTextColor(ContextCompat.getColorStateList(chip.getContext(),
|
||||
R.color.home_category_chip_text));
|
||||
chip.setCheckedIconVisible(false);
|
||||
chip.setEnsureMinTouchTargetSize(false);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,149 @@
|
||||
package com.android.messaging.ui.home;
|
||||
|
||||
import android.content.Context;
|
||||
import android.telephony.PhoneNumberUtils;
|
||||
import android.text.TextUtils;
|
||||
|
||||
import com.android.messaging.R;
|
||||
import com.android.messaging.datamodel.data.ConversationListItemData;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
|
||||
/** Personal / promotion split and suggested categories from conversation text. */
|
||||
public final class HomeCategoryClassifier {
|
||||
|
||||
public static final class Suggestion {
|
||||
public final String kind;
|
||||
public final String name;
|
||||
public final List<String> conversationIds = new ArrayList<>();
|
||||
|
||||
Suggestion(final String kind, final String name) {
|
||||
this.kind = kind;
|
||||
this.name = name;
|
||||
}
|
||||
}
|
||||
|
||||
private HomeCategoryClassifier() {
|
||||
}
|
||||
|
||||
public static boolean matches(final Context context, final String categoryId,
|
||||
final ConversationListItemData item) {
|
||||
if (item == null || TextUtils.isEmpty(categoryId)
|
||||
|| HomeCategoryStore.ID_ALL.equals(categoryId)) {
|
||||
return true;
|
||||
}
|
||||
if (HomeCategoryStore.ID_PERSONAL.equals(categoryId)) {
|
||||
if (HomeCategoryStore.isAssigned(context, item.getConversationId(),
|
||||
HomeCategoryStore.ID_PERSONAL)) {
|
||||
return true;
|
||||
}
|
||||
if (HomeCategoryStore.isAssigned(context, item.getConversationId(),
|
||||
HomeCategoryStore.ID_PROMOTION)) {
|
||||
return false;
|
||||
}
|
||||
return isPersonal(item);
|
||||
}
|
||||
if (HomeCategoryStore.ID_PROMOTION.equals(categoryId)) {
|
||||
if (HomeCategoryStore.isAssigned(context, item.getConversationId(),
|
||||
HomeCategoryStore.ID_PROMOTION)) {
|
||||
return true;
|
||||
}
|
||||
if (HomeCategoryStore.isAssigned(context, item.getConversationId(),
|
||||
HomeCategoryStore.ID_PERSONAL)) {
|
||||
return false;
|
||||
}
|
||||
return isPromotion(item);
|
||||
}
|
||||
return HomeCategoryStore.isAssigned(context, item.getConversationId(), categoryId);
|
||||
}
|
||||
|
||||
public static boolean isPersonal(final ConversationListItemData item) {
|
||||
return !isPromotion(item);
|
||||
}
|
||||
|
||||
public static boolean isPromotion(final ConversationListItemData item) {
|
||||
if (item == null) {
|
||||
return false;
|
||||
}
|
||||
if (item.getIsGroup() || item.getParticipantContactId() > 0) {
|
||||
return false;
|
||||
}
|
||||
if (looksLikeMobile(item)) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
public static boolean isVerification(final ConversationListItemData item) {
|
||||
return containsAny(haystack(item),
|
||||
"verification", "verify", "otp", "passcode", "auth code",
|
||||
"인증", "인증번호", "인증코드", "인증 번호");
|
||||
}
|
||||
|
||||
public static List<Suggestion> suggestions(final Context context,
|
||||
final List<ConversationListItemData> items) {
|
||||
final List<Suggestion> out = new ArrayList<>();
|
||||
if (HomeCategoryStore.hasKind(context, HomeCategoryStore.KIND_VERIFICATION)) {
|
||||
return out;
|
||||
}
|
||||
final Suggestion verification = new Suggestion(HomeCategoryStore.KIND_VERIFICATION,
|
||||
context.getString(R.string.home_category_suggest_verification));
|
||||
if (items != null) {
|
||||
for (final ConversationListItemData item : items) {
|
||||
if (item != null && isVerification(item)
|
||||
&& !TextUtils.isEmpty(item.getConversationId())) {
|
||||
verification.conversationIds.add(item.getConversationId());
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!verification.conversationIds.isEmpty()) {
|
||||
out.add(verification);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
private static boolean looksLikeMobile(final ConversationListItemData item) {
|
||||
final String dest = item.getOtherParticipantNormalizedDestination();
|
||||
if (looksLikeMobileNumber(dest)) {
|
||||
return true;
|
||||
}
|
||||
return looksLikeMobileNumber(item.getName());
|
||||
}
|
||||
|
||||
private static boolean looksLikeMobileNumber(final String raw) {
|
||||
if (TextUtils.isEmpty(raw)) {
|
||||
return false;
|
||||
}
|
||||
final String digits = PhoneNumberUtils.normalizeNumber(raw);
|
||||
if (TextUtils.isEmpty(digits)) {
|
||||
return false;
|
||||
}
|
||||
for (int i = 0; i < digits.length(); i++) {
|
||||
final char c = digits.charAt(i);
|
||||
if (c != '+' && !Character.isDigit(c)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return digits.length() >= 8;
|
||||
}
|
||||
|
||||
private static String haystack(final ConversationListItemData item) {
|
||||
final String name = item.getName() == null ? "" : item.getName();
|
||||
final String snippet = item.getSnippetText() == null ? "" : item.getSnippetText();
|
||||
return (name + " " + snippet).toLowerCase(Locale.ROOT);
|
||||
}
|
||||
|
||||
private static boolean containsAny(final String haystack, final String... needles) {
|
||||
if (TextUtils.isEmpty(haystack)) {
|
||||
return false;
|
||||
}
|
||||
for (final String needle : needles) {
|
||||
if (haystack.contains(needle)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,143 @@
|
||||
/*
|
||||
* Copyright (C) 2026 The LineageOS Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.android.messaging.ui.home;
|
||||
|
||||
import android.content.Context;
|
||||
import android.util.AttributeSet;
|
||||
import android.view.MotionEvent;
|
||||
import android.view.View;
|
||||
import android.view.ViewConfiguration;
|
||||
import android.view.ViewParent;
|
||||
import android.widget.HorizontalScrollView;
|
||||
|
||||
import com.android.messaging.R;
|
||||
|
||||
/** Category tab strip that keeps horizontal swipes instead of letting parents steal them. */
|
||||
public final class HomeCategoryScroller extends HorizontalScrollView {
|
||||
|
||||
private final int mTouchSlop;
|
||||
private float mDownX;
|
||||
private float mDownY;
|
||||
private boolean mDragging;
|
||||
|
||||
public HomeCategoryScroller(final Context context) {
|
||||
this(context, null);
|
||||
}
|
||||
|
||||
public HomeCategoryScroller(final Context context, final AttributeSet attrs) {
|
||||
this(context, attrs, 0);
|
||||
}
|
||||
|
||||
public HomeCategoryScroller(final Context context, final AttributeSet attrs,
|
||||
final int defStyleAttr) {
|
||||
super(context, attrs, defStyleAttr);
|
||||
mTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop();
|
||||
setHorizontalScrollBarEnabled(false);
|
||||
setHorizontalFadingEdgeEnabled(false);
|
||||
setOverScrollMode(OVER_SCROLL_IF_CONTENT_SCROLLS);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onLayout(final boolean changed, final int l, final int t, final int r,
|
||||
final int b) {
|
||||
super.onLayout(changed, l, t, r, b);
|
||||
updateEdgeFades();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onScrollChanged(final int l, final int t, final int oldl, final int oldt) {
|
||||
super.onScrollChanged(l, t, oldl, oldt);
|
||||
updateEdgeFades();
|
||||
}
|
||||
|
||||
private void updateEdgeFades() {
|
||||
final ViewParent parent = getParent();
|
||||
if (!(parent instanceof View)) {
|
||||
return;
|
||||
}
|
||||
final View host = (View) parent;
|
||||
HomeScrollEdgeFades.apply(this, host.findViewById(R.id.home_category_fade_start),
|
||||
host.findViewById(R.id.home_category_fade_end));
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onInterceptTouchEvent(final MotionEvent ev) {
|
||||
switch (ev.getActionMasked()) {
|
||||
case MotionEvent.ACTION_DOWN:
|
||||
mDownX = ev.getX();
|
||||
mDownY = ev.getY();
|
||||
mDragging = false;
|
||||
disallowParent(canScrollContent());
|
||||
break;
|
||||
case MotionEvent.ACTION_MOVE:
|
||||
if (isHorizontalDrag(ev)) {
|
||||
mDragging = true;
|
||||
disallowParent(true);
|
||||
return true;
|
||||
}
|
||||
break;
|
||||
case MotionEvent.ACTION_UP:
|
||||
case MotionEvent.ACTION_CANCEL:
|
||||
mDragging = false;
|
||||
disallowParent(false);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
return super.onInterceptTouchEvent(ev) || mDragging;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onTouchEvent(final MotionEvent ev) {
|
||||
final int action = ev.getActionMasked();
|
||||
if (action == MotionEvent.ACTION_DOWN) {
|
||||
mDownX = ev.getX();
|
||||
mDownY = ev.getY();
|
||||
mDragging = false;
|
||||
} else if (action == MotionEvent.ACTION_MOVE && isHorizontalDrag(ev)) {
|
||||
mDragging = true;
|
||||
}
|
||||
if (mDragging || canScrollContent()) {
|
||||
disallowParent(action != MotionEvent.ACTION_UP && action != MotionEvent.ACTION_CANCEL);
|
||||
}
|
||||
if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_CANCEL) {
|
||||
mDragging = false;
|
||||
disallowParent(false);
|
||||
}
|
||||
return super.onTouchEvent(ev);
|
||||
}
|
||||
|
||||
private boolean isHorizontalDrag(final MotionEvent ev) {
|
||||
final float dx = Math.abs(ev.getX() - mDownX);
|
||||
final float dy = Math.abs(ev.getY() - mDownY);
|
||||
return dx > mTouchSlop && dx > dy;
|
||||
}
|
||||
|
||||
private boolean canScrollContent() {
|
||||
if (getChildCount() == 0) {
|
||||
return false;
|
||||
}
|
||||
final View child = getChildAt(0);
|
||||
return child.getWidth() > getWidth() - getPaddingLeft() - getPaddingRight();
|
||||
}
|
||||
|
||||
private void disallowParent(final boolean disallow) {
|
||||
final ViewParent parent = getParent();
|
||||
if (parent != null) {
|
||||
parent.requestDisallowInterceptTouchEvent(disallow);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,256 @@
|
||||
package com.android.messaging.ui.home;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.SharedPreferences;
|
||||
import android.text.TextUtils;
|
||||
|
||||
import com.android.messaging.R;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
|
||||
/** Built-in and user-created conversation categories for the home list. */
|
||||
public final class HomeCategoryStore {
|
||||
|
||||
public static final String ID_ALL = "all";
|
||||
public static final String ID_PERSONAL = "personal";
|
||||
public static final String ID_PROMOTION = "promotion";
|
||||
|
||||
public static final String KIND_VERIFICATION = "verification";
|
||||
|
||||
private static final String PREFS_NAME = "home_categories";
|
||||
private static final String KEY_CUSTOM = "custom";
|
||||
private static final String KEY_ASSIGN = "assign";
|
||||
private static final String KEY_SELECTED = "selected";
|
||||
private static final String ROW = "\n";
|
||||
private static final String COL = "\t";
|
||||
public static final int MAX_CUSTOM = 12;
|
||||
public static final int MAX_NAME = 24;
|
||||
|
||||
public static final class Category {
|
||||
public final String id;
|
||||
public final String name;
|
||||
public final String kind;
|
||||
public final boolean builtIn;
|
||||
|
||||
Category(final String id, final String name, final String kind, final boolean builtIn) {
|
||||
this.id = id;
|
||||
this.name = name;
|
||||
this.kind = kind == null ? "" : kind;
|
||||
this.builtIn = builtIn;
|
||||
}
|
||||
}
|
||||
|
||||
private HomeCategoryStore() {
|
||||
}
|
||||
|
||||
public static boolean isBuiltIn(final String id) {
|
||||
return ID_ALL.equals(id) || ID_PERSONAL.equals(id) || ID_PROMOTION.equals(id);
|
||||
}
|
||||
|
||||
public static List<Category> listTabs(final Context context) {
|
||||
final List<Category> tabs = new ArrayList<>();
|
||||
tabs.add(new Category(ID_ALL, context.getString(R.string.home_category_all), "", true));
|
||||
tabs.add(new Category(ID_PERSONAL, context.getString(R.string.home_category_personal),
|
||||
"", true));
|
||||
tabs.add(new Category(ID_PROMOTION, context.getString(R.string.home_category_promotion),
|
||||
"", true));
|
||||
tabs.addAll(listCustom(context));
|
||||
return tabs;
|
||||
}
|
||||
|
||||
public static List<Category> listCustom(final Context context) {
|
||||
final String raw = prefs(context).getString(KEY_CUSTOM, "");
|
||||
if (TextUtils.isEmpty(raw)) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
final List<Category> items = new ArrayList<>();
|
||||
for (final String line : raw.split(ROW, -1)) {
|
||||
if (TextUtils.isEmpty(line)) {
|
||||
continue;
|
||||
}
|
||||
final String[] parts = line.split(COL, -1);
|
||||
if (parts.length < 2 || TextUtils.isEmpty(parts[0]) || TextUtils.isEmpty(parts[1])) {
|
||||
continue;
|
||||
}
|
||||
final String kind = parts.length > 2 ? parts[2] : "";
|
||||
items.add(new Category(parts[0], parts[1], kind, false));
|
||||
}
|
||||
return items;
|
||||
}
|
||||
|
||||
public static boolean hasKind(final Context context, final String kind) {
|
||||
if (TextUtils.isEmpty(kind)) {
|
||||
return false;
|
||||
}
|
||||
for (final Category category : listCustom(context)) {
|
||||
if (kind.equals(category.kind)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public static boolean hasName(final Context context, final String name) {
|
||||
final String trimmed = name == null ? "" : name.trim();
|
||||
if (TextUtils.isEmpty(trimmed)) {
|
||||
return false;
|
||||
}
|
||||
for (final Category category : listTabs(context)) {
|
||||
if (trimmed.equalsIgnoreCase(category.name)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public static Category addCustom(final Context context, final String name, final String kind) {
|
||||
String trimmed = name == null ? "" : name.trim();
|
||||
if (TextUtils.isEmpty(trimmed)) {
|
||||
return null;
|
||||
}
|
||||
if (trimmed.length() > MAX_NAME) {
|
||||
trimmed = trimmed.substring(0, MAX_NAME);
|
||||
}
|
||||
if (hasName(context, trimmed)) {
|
||||
return null;
|
||||
}
|
||||
final List<Category> items = new ArrayList<>(listCustom(context));
|
||||
if (items.size() >= MAX_CUSTOM) {
|
||||
return null;
|
||||
}
|
||||
final Category created = new Category("c_" + UUID.randomUUID().toString(), trimmed,
|
||||
kind == null ? "" : kind, false);
|
||||
items.add(created);
|
||||
saveCustom(context, items);
|
||||
return created;
|
||||
}
|
||||
|
||||
public static void removeCustom(final Context context, final String categoryId) {
|
||||
if (isBuiltIn(categoryId)) {
|
||||
return;
|
||||
}
|
||||
final List<Category> items = new ArrayList<>(listCustom(context));
|
||||
boolean removed = false;
|
||||
for (int i = items.size() - 1; i >= 0; i--) {
|
||||
if (items.get(i).id.equals(categoryId)) {
|
||||
items.remove(i);
|
||||
removed = true;
|
||||
}
|
||||
}
|
||||
if (!removed) {
|
||||
return;
|
||||
}
|
||||
saveCustom(context, items);
|
||||
final List<String> kept = new ArrayList<>();
|
||||
for (final String row : assignmentRows(context)) {
|
||||
final String[] parts = row.split(COL, -1);
|
||||
if (parts.length >= 2 && !categoryId.equals(parts[1])) {
|
||||
kept.add(row);
|
||||
}
|
||||
}
|
||||
prefs(context).edit().putString(KEY_ASSIGN, TextUtils.join(ROW, kept)).apply();
|
||||
if (categoryId.equals(getSelectedId(context))) {
|
||||
setSelectedId(context, ID_ALL);
|
||||
}
|
||||
}
|
||||
|
||||
public static void assign(final Context context, final String conversationId,
|
||||
final String categoryId) {
|
||||
if (TextUtils.isEmpty(conversationId) || TextUtils.isEmpty(categoryId)
|
||||
|| ID_ALL.equals(categoryId)) {
|
||||
return;
|
||||
}
|
||||
final LinkedHashSet<String> rows = new LinkedHashSet<>(assignmentRows(context));
|
||||
rows.add(conversationId + COL + categoryId);
|
||||
prefs(context).edit().putString(KEY_ASSIGN, TextUtils.join(ROW, rows)).apply();
|
||||
}
|
||||
|
||||
public static void assignAll(final Context context, final List<String> conversationIds,
|
||||
final String categoryId) {
|
||||
if (conversationIds == null) {
|
||||
return;
|
||||
}
|
||||
for (final String id : conversationIds) {
|
||||
assign(context, id, categoryId);
|
||||
}
|
||||
}
|
||||
|
||||
public static void unassign(final Context context, final String conversationId,
|
||||
final String categoryId) {
|
||||
if (TextUtils.isEmpty(conversationId) || TextUtils.isEmpty(categoryId)) {
|
||||
return;
|
||||
}
|
||||
final String needle = conversationId + COL + categoryId;
|
||||
final List<String> rows = assignmentRows(context);
|
||||
if (!rows.remove(needle)) {
|
||||
return;
|
||||
}
|
||||
prefs(context).edit().putString(KEY_ASSIGN, TextUtils.join(ROW, rows)).apply();
|
||||
}
|
||||
|
||||
public static Set<String> assignedIds(final Context context,
|
||||
final String conversationId) {
|
||||
final LinkedHashSet<String> ids = new LinkedHashSet<>();
|
||||
if (TextUtils.isEmpty(conversationId)) {
|
||||
return ids;
|
||||
}
|
||||
for (final String row : assignmentRows(context)) {
|
||||
final String[] parts = row.split(COL, -1);
|
||||
if (parts.length >= 2 && conversationId.equals(parts[0])) {
|
||||
ids.add(parts[1]);
|
||||
}
|
||||
}
|
||||
return ids;
|
||||
}
|
||||
|
||||
public static boolean isAssigned(final Context context, final String conversationId,
|
||||
final String categoryId) {
|
||||
if (TextUtils.isEmpty(conversationId) || TextUtils.isEmpty(categoryId)) {
|
||||
return false;
|
||||
}
|
||||
final String needle = conversationId + COL + categoryId;
|
||||
return assignmentRows(context).contains(needle);
|
||||
}
|
||||
|
||||
public static String getSelectedId(final Context context) {
|
||||
final String selected = prefs(context).getString(KEY_SELECTED, ID_ALL);
|
||||
return TextUtils.isEmpty(selected) ? ID_ALL : selected;
|
||||
}
|
||||
|
||||
public static void setSelectedId(final Context context, final String categoryId) {
|
||||
prefs(context).edit().putString(KEY_SELECTED,
|
||||
TextUtils.isEmpty(categoryId) ? ID_ALL : categoryId).apply();
|
||||
}
|
||||
|
||||
private static List<String> assignmentRows(final Context context) {
|
||||
final String raw = prefs(context).getString(KEY_ASSIGN, "");
|
||||
if (TextUtils.isEmpty(raw)) {
|
||||
return new ArrayList<>();
|
||||
}
|
||||
final List<String> rows = new ArrayList<>();
|
||||
for (final String line : raw.split(ROW, -1)) {
|
||||
if (!TextUtils.isEmpty(line)) {
|
||||
rows.add(line);
|
||||
}
|
||||
}
|
||||
return rows;
|
||||
}
|
||||
|
||||
private static void saveCustom(final Context context, final List<Category> items) {
|
||||
final List<String> rows = new ArrayList<>(items.size());
|
||||
for (final Category item : items) {
|
||||
rows.add(item.id + COL + item.name + COL + item.kind);
|
||||
}
|
||||
prefs(context).edit().putString(KEY_CUSTOM, TextUtils.join(ROW, rows)).apply();
|
||||
}
|
||||
|
||||
private static SharedPreferences prefs(final Context context) {
|
||||
return context.getApplicationContext().getSharedPreferences(PREFS_NAME,
|
||||
Context.MODE_PRIVATE);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,112 @@
|
||||
/*
|
||||
* Copyright (C) 2026 The LineageOS Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.android.messaging.ui.home;
|
||||
|
||||
import android.content.Context;
|
||||
import android.util.AttributeSet;
|
||||
import android.view.View;
|
||||
import android.widget.LinearLayout;
|
||||
|
||||
import com.android.messaging.R;
|
||||
|
||||
/**
|
||||
* Category tabs + add button. Tabs stay left of the add control; the add button
|
||||
* follows the last tab and never crosses the strip's end padding.
|
||||
*/
|
||||
public final class HomeCategoryStrip extends LinearLayout {
|
||||
|
||||
public HomeCategoryStrip(final Context context) {
|
||||
this(context, null);
|
||||
}
|
||||
|
||||
public HomeCategoryStrip(final Context context, final AttributeSet attrs) {
|
||||
this(context, attrs, 0);
|
||||
}
|
||||
|
||||
public HomeCategoryStrip(final Context context, final AttributeSet attrs,
|
||||
final int defStyleAttr) {
|
||||
super(context, attrs, defStyleAttr);
|
||||
setOrientation(HORIZONTAL);
|
||||
setClipToPadding(true);
|
||||
setClipChildren(true);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onMeasure(final int widthMeasureSpec, final int heightMeasureSpec) {
|
||||
final View all = findViewById(R.id.home_category_all);
|
||||
final View add = findViewById(R.id.home_category_add);
|
||||
final View scroll = findViewById(R.id.home_category_scroll_host);
|
||||
int pinned = 0;
|
||||
int contentHeight = 0;
|
||||
if (all != null && all.getVisibility() != GONE) {
|
||||
measureChildWithMargins(all, widthMeasureSpec, 0, heightMeasureSpec, 0);
|
||||
pinned += measuredWidthWithMargins(all);
|
||||
contentHeight = Math.max(contentHeight, measuredHeightWithMargins(all));
|
||||
}
|
||||
if (add != null && add.getVisibility() != GONE) {
|
||||
measureChildWithMargins(add, widthMeasureSpec, pinned, heightMeasureSpec, 0);
|
||||
pinned += measuredWidthWithMargins(add);
|
||||
contentHeight = Math.max(contentHeight, measuredHeightWithMargins(add));
|
||||
}
|
||||
int contentWidth = pinned;
|
||||
if (scroll != null && scroll.getVisibility() != GONE) {
|
||||
measureChildWithMargins(scroll, widthMeasureSpec, pinned, heightMeasureSpec, 0);
|
||||
contentWidth += measuredWidthWithMargins(scroll);
|
||||
contentHeight = Math.max(contentHeight, measuredHeightWithMargins(scroll));
|
||||
}
|
||||
setMeasuredDimension(
|
||||
resolveSize(contentWidth + getPaddingLeft() + getPaddingRight(),
|
||||
widthMeasureSpec),
|
||||
resolveSize(contentHeight + getPaddingTop() + getPaddingBottom(),
|
||||
heightMeasureSpec));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onLayout(final boolean changed, final int left, final int top, final int right,
|
||||
final int bottom) {
|
||||
final int innerTop = getPaddingTop();
|
||||
final int innerBottom = bottom - top - getPaddingBottom();
|
||||
int x = getPaddingLeft();
|
||||
x = layoutChild(findViewById(R.id.home_category_all), x, innerTop, innerBottom);
|
||||
x = layoutChild(findViewById(R.id.home_category_scroll_host), x, innerTop, innerBottom);
|
||||
layoutChild(findViewById(R.id.home_category_add), x, innerTop, innerBottom);
|
||||
}
|
||||
|
||||
private int layoutChild(final View child, final int x, final int innerTop,
|
||||
final int innerBottom) {
|
||||
if (child == null || child.getVisibility() == GONE) {
|
||||
return x;
|
||||
}
|
||||
final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
|
||||
final int height = child.getMeasuredHeight();
|
||||
final int childTop = innerTop + lp.topMargin
|
||||
+ Math.max(0, (innerBottom - innerTop - height - lp.topMargin - lp.bottomMargin) / 2);
|
||||
final int childLeft = x + lp.leftMargin;
|
||||
child.layout(childLeft, childTop, childLeft + child.getMeasuredWidth(),
|
||||
childTop + height);
|
||||
return childLeft + child.getMeasuredWidth() + lp.rightMargin;
|
||||
}
|
||||
|
||||
private static int measuredWidthWithMargins(final View child) {
|
||||
final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
|
||||
return child.getMeasuredWidth() + lp.leftMargin + lp.rightMargin;
|
||||
}
|
||||
|
||||
private static int measuredHeightWithMargins(final View child) {
|
||||
final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
|
||||
return child.getMeasuredHeight() + lp.topMargin + lp.bottomMargin;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,344 @@
|
||||
/*
|
||||
* Copyright (C) 2025 The LineageOS Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.android.messaging.ui.home;
|
||||
|
||||
import android.content.Intent;
|
||||
import android.os.Bundle;
|
||||
import android.os.Handler;
|
||||
import android.os.Looper;
|
||||
import android.text.Editable;
|
||||
import android.text.TextUtils;
|
||||
import android.text.TextWatcher;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.widget.EditText;
|
||||
import android.widget.HorizontalScrollView;
|
||||
import android.widget.ImageButton;
|
||||
import android.widget.LinearLayout;
|
||||
import android.widget.TextView;
|
||||
|
||||
import com.android.ex.chips.RecipientEntry;
|
||||
import com.android.messaging.R;
|
||||
import com.android.messaging.datamodel.data.ParticipantData;
|
||||
import com.android.messaging.ui.BugleActionBarActivity;
|
||||
import com.android.messaging.ui.ContactIconView;
|
||||
import com.android.messaging.util.AvatarUriUtil;
|
||||
import com.android.messaging.util.ContactRecipientEntryUtils;
|
||||
import com.android.messaging.util.ContactUtil;
|
||||
import com.android.messaging.util.ImeUtil;
|
||||
import com.google.android.material.bottomsheet.BottomSheetDialog;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
|
||||
/** Home-style "new chat" screen: recipient field, group entry point and contact list. */
|
||||
public class HomeComposeActivity extends BugleActionBarActivity
|
||||
implements HomeContactsFragment.GroupSelectionHost {
|
||||
|
||||
/** Matches the duration of {@code R.anim.home_sheet_open}. */
|
||||
private static final long SHEET_ENTER_MS = 300L;
|
||||
private static final int REQUEST_GROUP_NAME = 1;
|
||||
private static final int MIN_GROUP_SIZE = 2;
|
||||
|
||||
private EditText mRecipientField;
|
||||
private TextView mHeaderTitle;
|
||||
private View mCreateGroupButton;
|
||||
private HorizontalScrollView mChipScroll;
|
||||
private LinearLayout mChipContainer;
|
||||
private TextView mChipHint;
|
||||
private TextView mNextButton;
|
||||
private TextView mAccountLabel;
|
||||
private boolean mGroupMode;
|
||||
private ContactUtil.AccountFilter mAccountFilter;
|
||||
private final ExecutorService mAccountExecutor = Executors.newSingleThreadExecutor();
|
||||
private final Handler mMainHandler = new Handler(Looper.getMainLooper());
|
||||
|
||||
@Override
|
||||
protected void onCreate(final Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
setContentView(R.layout.home_compose_activity);
|
||||
if (getSupportActionBar() != null) {
|
||||
getSupportActionBar().hide();
|
||||
}
|
||||
|
||||
// Presented as a sheet, so the header closes instead of navigating back.
|
||||
final ImageButton back = findViewById(R.id.home_sub_header_back);
|
||||
back.setVisibility(View.GONE);
|
||||
final ImageButton close = findViewById(R.id.home_sub_header_close);
|
||||
close.setVisibility(View.VISIBLE);
|
||||
close.setOnClickListener(v -> finish());
|
||||
|
||||
mHeaderTitle = findViewById(R.id.home_sub_header_title);
|
||||
mHeaderTitle.setText(R.string.home_compose_title);
|
||||
|
||||
final View scrim = findViewById(R.id.home_compose_scrim);
|
||||
// The tap that opened the sheet can land here while it is still sliding up, so the
|
||||
// dismiss handler only arms once the entry animation is done.
|
||||
scrim.postDelayed(() -> scrim.setOnClickListener(v -> finish()), SHEET_ENTER_MS);
|
||||
|
||||
mChipScroll = findViewById(R.id.home_compose_chip_scroll);
|
||||
mChipContainer = findViewById(R.id.home_compose_chips);
|
||||
mChipHint = findViewById(R.id.home_compose_chip_hint);
|
||||
mNextButton = findViewById(R.id.home_compose_next);
|
||||
mNextButton.setOnClickListener(v -> openGroupNameStep());
|
||||
|
||||
mRecipientField = findViewById(R.id.home_compose_recipient);
|
||||
final View recipientBox = findViewById(R.id.home_compose_recipient_box);
|
||||
if (recipientBox != null) {
|
||||
recipientBox.setOnClickListener(v -> {
|
||||
if (mRecipientField.getVisibility() != View.VISIBLE) {
|
||||
return;
|
||||
}
|
||||
mRecipientField.requestFocus();
|
||||
ImeUtil.get().showImeKeyboard(this, mRecipientField);
|
||||
});
|
||||
}
|
||||
mRecipientField.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) {
|
||||
final HomeContactsFragment fragment = getContactsFragment();
|
||||
if (fragment != null) {
|
||||
fragment.setFilterText(s.toString());
|
||||
}
|
||||
}
|
||||
});
|
||||
mRecipientField.setOnEditorActionListener((v, actionId, event) -> {
|
||||
final HomeContactsFragment fragment = getContactsFragment();
|
||||
return fragment != null && fragment.startConversationWithFilterText();
|
||||
});
|
||||
|
||||
mCreateGroupButton = findViewById(R.id.home_compose_create_group);
|
||||
mCreateGroupButton.setOnClickListener(v -> enterGroupMode());
|
||||
|
||||
mAccountLabel = findViewById(R.id.home_compose_account_label);
|
||||
final View accountChip = findViewById(R.id.home_compose_account_chip);
|
||||
accountChip.setOnClickListener(v -> showAccountSheet());
|
||||
}
|
||||
|
||||
private HomeContactsFragment getContactsFragment() {
|
||||
return (HomeContactsFragment) getSupportFragmentManager()
|
||||
.findFragmentById(R.id.home_compose_fragment_host);
|
||||
}
|
||||
|
||||
private void showAccountSheet() {
|
||||
mAccountExecutor.execute(() -> {
|
||||
final List<ContactUtil.ContactAccount> accounts =
|
||||
ContactUtil.getContactAccounts(this);
|
||||
mMainHandler.post(() -> presentAccountSheet(accounts));
|
||||
});
|
||||
}
|
||||
|
||||
private void presentAccountSheet(final List<ContactUtil.ContactAccount> accounts) {
|
||||
if (isFinishing() || isDestroyed()) {
|
||||
return;
|
||||
}
|
||||
final LayoutInflater inflater = LayoutInflater.from(this);
|
||||
final LinearLayout list = (LinearLayout) inflater.inflate(
|
||||
R.layout.home_compose_account_sheet, null, false);
|
||||
final BottomSheetDialog[] dialog = new BottomSheetDialog[1];
|
||||
|
||||
int allCount = 0;
|
||||
for (final ContactUtil.ContactAccount account : accounts) {
|
||||
allCount += account.contactCount;
|
||||
}
|
||||
list.addView(createAccountRow(inflater, list,
|
||||
getString(R.string.home_compose_all_contacts), null, allCount,
|
||||
mAccountFilter == null, () -> {
|
||||
applyAccountFilter(null, getString(R.string.home_compose_all_contacts));
|
||||
if (dialog[0] != null) {
|
||||
dialog[0].dismiss();
|
||||
}
|
||||
}));
|
||||
|
||||
for (final ContactUtil.ContactAccount account : accounts) {
|
||||
final String title = account.deviceOnly
|
||||
? getString(R.string.home_compose_device_contacts)
|
||||
: (!TextUtils.isEmpty(account.title) ? account.title
|
||||
: getString(R.string.home_compose_device_contacts));
|
||||
final String subtitle = account.deviceOnly ? null : account.subtitle;
|
||||
final boolean selected = mAccountFilter != null
|
||||
&& account.toFilter().matches(mAccountFilter);
|
||||
list.addView(createAccountRow(inflater, list, title, subtitle, account.contactCount,
|
||||
selected, () -> {
|
||||
applyAccountFilter(account.toFilter(), title);
|
||||
if (dialog[0] != null) {
|
||||
dialog[0].dismiss();
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
dialog[0] = HomeBottomSheet.builder(this)
|
||||
.title(R.string.home_compose_contacts_source_title)
|
||||
.custom(list)
|
||||
.show();
|
||||
}
|
||||
|
||||
private View createAccountRow(final LayoutInflater inflater, final LinearLayout parent,
|
||||
final String title, final String subtitle, final int count, final boolean selected,
|
||||
final Runnable onClick) {
|
||||
final View row = inflater.inflate(R.layout.home_compose_account_row, parent, false);
|
||||
((TextView) row.findViewById(R.id.home_compose_account_title)).setText(title);
|
||||
final TextView subtitleView = row.findViewById(R.id.home_compose_account_subtitle);
|
||||
if (TextUtils.isEmpty(subtitle)) {
|
||||
subtitleView.setVisibility(View.GONE);
|
||||
} else {
|
||||
subtitleView.setVisibility(View.VISIBLE);
|
||||
subtitleView.setText(subtitle);
|
||||
}
|
||||
((TextView) row.findViewById(R.id.home_compose_account_count))
|
||||
.setText(String.valueOf(count));
|
||||
row.findViewById(R.id.home_compose_account_check)
|
||||
.setVisibility(selected ? View.VISIBLE : View.GONE);
|
||||
row.setOnClickListener(v -> onClick.run());
|
||||
final LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(
|
||||
LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.WRAP_CONTENT);
|
||||
params.topMargin = parent.getChildCount() == 0
|
||||
? 0 : getResources().getDimensionPixelSize(R.dimen.looply_space_sm);
|
||||
row.setLayoutParams(params);
|
||||
return row;
|
||||
}
|
||||
|
||||
private void applyAccountFilter(final ContactUtil.AccountFilter filter, final String label) {
|
||||
mAccountFilter = filter;
|
||||
mAccountLabel.setText(label);
|
||||
final HomeContactsFragment fragment = getContactsFragment();
|
||||
if (fragment != null) {
|
||||
fragment.setAccountFilter(filter);
|
||||
}
|
||||
}
|
||||
|
||||
private void enterGroupMode() {
|
||||
mGroupMode = true;
|
||||
ImeUtil.get().hideImeKeyboard(this, mRecipientField);
|
||||
mHeaderTitle.setText(R.string.home_compose_group_title);
|
||||
mRecipientField.setText("");
|
||||
mRecipientField.setVisibility(View.GONE);
|
||||
mCreateGroupButton.setVisibility(View.GONE);
|
||||
mChipScroll.setVisibility(View.VISIBLE);
|
||||
mChipHint.setVisibility(View.VISIBLE);
|
||||
mNextButton.setVisibility(View.VISIBLE);
|
||||
final HomeContactsFragment fragment = getContactsFragment();
|
||||
if (fragment != null) {
|
||||
fragment.setGroupMode(true);
|
||||
}
|
||||
updateNextButton(0);
|
||||
}
|
||||
|
||||
private void exitGroupMode() {
|
||||
mGroupMode = false;
|
||||
mHeaderTitle.setText(R.string.home_compose_title);
|
||||
mRecipientField.setVisibility(View.VISIBLE);
|
||||
mCreateGroupButton.setVisibility(View.VISIBLE);
|
||||
mChipScroll.setVisibility(View.GONE);
|
||||
mChipHint.setVisibility(View.GONE);
|
||||
mNextButton.setVisibility(View.GONE);
|
||||
mChipContainer.removeAllViews();
|
||||
final HomeContactsFragment fragment = getContactsFragment();
|
||||
if (fragment != null) {
|
||||
fragment.setGroupMode(false);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onGroupSelectionChanged(final List<RecipientEntry> selected) {
|
||||
mChipContainer.removeAllViews();
|
||||
final LayoutInflater inflater = LayoutInflater.from(this);
|
||||
for (final RecipientEntry entry : selected) {
|
||||
final View chip = inflater.inflate(R.layout.home_compose_chip, mChipContainer, false);
|
||||
final ContactIconView avatar = chip.findViewById(R.id.home_compose_chip_avatar);
|
||||
final ParticipantData participant = ParticipantData.getFromRecipientEntry(entry);
|
||||
avatar.setImageResourceUri(AvatarUriUtil.createAvatarUri(participant),
|
||||
participant.getContactId(), participant.getLookupKey(),
|
||||
participant.getNormalizedDestination());
|
||||
avatar.setImageClickHandlerDisabled(true);
|
||||
final TextView name = chip.findViewById(R.id.home_compose_chip_name);
|
||||
name.setText(ContactRecipientEntryUtils.getDisplayNameForContactList(entry));
|
||||
final LinearLayout.LayoutParams params =
|
||||
(LinearLayout.LayoutParams) chip.getLayoutParams();
|
||||
params.setMarginEnd(getResources().getDimensionPixelSize(R.dimen.looply_space_xs));
|
||||
mChipContainer.addView(chip, params);
|
||||
}
|
||||
mChipHint.setVisibility(selected.isEmpty() ? View.VISIBLE : View.GONE);
|
||||
updateNextButton(selected.size());
|
||||
}
|
||||
|
||||
private void updateNextButton(final int selectedCount) {
|
||||
final boolean enabled = selectedCount >= MIN_GROUP_SIZE;
|
||||
mNextButton.setEnabled(enabled);
|
||||
mNextButton.setAlpha(enabled ? 1f : 0.4f);
|
||||
}
|
||||
|
||||
private void openGroupNameStep() {
|
||||
final HomeContactsFragment fragment = getContactsFragment();
|
||||
if (fragment == null) {
|
||||
return;
|
||||
}
|
||||
final ArrayList<ParticipantData> participants = fragment.getSelectedParticipants();
|
||||
if (participants.size() < MIN_GROUP_SIZE) {
|
||||
return;
|
||||
}
|
||||
startActivityForResult(HomeGroupNameActivity.buildIntent(this, participants),
|
||||
REQUEST_GROUP_NAME);
|
||||
overridePendingTransition(R.anim.home_sheet_open, R.anim.home_activity_hold);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onActivityResult(final int requestCode, final int resultCode,
|
||||
final Intent data) {
|
||||
super.onActivityResult(requestCode, resultCode, data);
|
||||
if (requestCode == REQUEST_GROUP_NAME && resultCode == RESULT_OK) {
|
||||
// The conversation is open, so this sheet should not be behind it.
|
||||
finish();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBackPressed() {
|
||||
if (mGroupMode) {
|
||||
exitGroupMode();
|
||||
return;
|
||||
}
|
||||
super.onBackPressed();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDestroy() {
|
||||
mAccountExecutor.shutdownNow();
|
||||
super.onDestroy();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void finish() {
|
||||
if (mRecipientField != null && !TextUtils.isEmpty(mRecipientField.getText())) {
|
||||
ImeUtil.get().hideImeKeyboard(this, mRecipientField);
|
||||
}
|
||||
super.finish();
|
||||
overridePendingTransition(R.anim.home_activity_hold, R.anim.home_sheet_close);
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,209 @@
|
||||
/*
|
||||
* Copyright (C) 2026 The LineageOS Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package com.android.messaging.ui.home;
|
||||
|
||||
import android.Manifest;
|
||||
import android.content.ContentResolver;
|
||||
import android.content.ContentValues;
|
||||
import android.content.Context;
|
||||
import android.content.SharedPreferences;
|
||||
import android.database.Cursor;
|
||||
import android.net.Uri;
|
||||
import android.provider.ContactsContract;
|
||||
import android.provider.ContactsContract.CommonDataKinds.GroupMembership;
|
||||
import android.provider.ContactsContract.CommonDataKinds.Phone;
|
||||
import android.provider.ContactsContract.Groups;
|
||||
import android.provider.ContactsContract.RawContacts;
|
||||
import android.text.TextUtils;
|
||||
|
||||
import com.android.messaging.util.AvatarUriUtil;
|
||||
import com.android.messaging.util.ContactUtil;
|
||||
import com.android.messaging.util.OsUtil;
|
||||
|
||||
/** Android Contacts groups (not chat threads) for automation include/exclude. */
|
||||
final class HomeContactGroups {
|
||||
static final String[] PROJECTION = {
|
||||
Groups._ID,
|
||||
Groups.TITLE,
|
||||
Groups.SUMMARY_COUNT,
|
||||
Groups.SUMMARY_WITH_PHONES
|
||||
};
|
||||
static final String SELECTION = Groups.DELETED + "=0 AND (" + Groups.AUTO_ADD
|
||||
+ "=0 OR " + Groups.AUTO_ADD + " IS NULL)";
|
||||
static final String SORT = Groups.TITLE + " COLLATE LOCALIZED ASC";
|
||||
static final Uri SUMMARY_URI = Groups.CONTENT_SUMMARY_URI;
|
||||
|
||||
private static final String PREFS = "home_demo";
|
||||
private static final String KEY_SEEDED = "contact_groups_seeded_v1";
|
||||
private static final String DEST_PREFIX = "cg:";
|
||||
|
||||
private HomeContactGroups() {
|
||||
}
|
||||
|
||||
static void ensureDemo(final Context context) {
|
||||
if (context == null || !OsUtil.hasPermission(Manifest.permission.WRITE_CONTACTS)
|
||||
|| !ContactUtil.hasReadContactsPermission()) {
|
||||
return;
|
||||
}
|
||||
final SharedPreferences prefs =
|
||||
context.getSharedPreferences(PREFS, Context.MODE_PRIVATE);
|
||||
if (prefs.getBoolean(KEY_SEEDED, false)) {
|
||||
return;
|
||||
}
|
||||
final ContentResolver resolver = context.getContentResolver();
|
||||
seedGroup(resolver, "Family",
|
||||
"+15551230008", "+15551230003", "+15551230023", "+15551230005");
|
||||
seedGroup(resolver, "Friends",
|
||||
"+15551230006", "+15551230012", "+15551230015", "+15551230018",
|
||||
"+15551230020", "+15551230007");
|
||||
seedGroup(resolver, "Work",
|
||||
"+15551230001", "+15551230002", "+15551230004", "+15551230011",
|
||||
"+15551230016", "+15551230013");
|
||||
seedGroup(resolver, "Clients",
|
||||
"+15551230009", "+15551230010", "+15551230014", "+15551230017",
|
||||
"+15551230021", "+15551230022");
|
||||
prefs.edit().putBoolean(KEY_SEEDED, true).apply();
|
||||
}
|
||||
|
||||
static Item fromCursor(final Cursor cursor) {
|
||||
final long id = cursor.getLong(0);
|
||||
final String title = cursor.getString(1);
|
||||
int count = 0;
|
||||
if (!cursor.isNull(3)) {
|
||||
count = cursor.getInt(3);
|
||||
} else if (!cursor.isNull(2)) {
|
||||
count = cursor.getInt(2);
|
||||
}
|
||||
return new Item(id, title, count);
|
||||
}
|
||||
|
||||
static final class Item {
|
||||
final long id;
|
||||
final String title;
|
||||
final int memberCount;
|
||||
|
||||
Item(final long id, final String title, final int memberCount) {
|
||||
this.id = id;
|
||||
this.title = title == null ? "" : title;
|
||||
this.memberCount = memberCount;
|
||||
}
|
||||
|
||||
String destination() {
|
||||
return DEST_PREFIX + id;
|
||||
}
|
||||
|
||||
Uri avatarUri() {
|
||||
if (TextUtils.isEmpty(title)) {
|
||||
return AvatarUriUtil.createAvatarUri(null, null, destination(), null);
|
||||
}
|
||||
return AvatarUriUtil.createAvatarUri(null, title, destination(), null);
|
||||
}
|
||||
}
|
||||
|
||||
private static void seedGroup(final ContentResolver resolver, final String title,
|
||||
final String... numbers) {
|
||||
if (findGroupId(resolver, title) >= 0) {
|
||||
return;
|
||||
}
|
||||
final ContentValues values = new ContentValues();
|
||||
values.put(Groups.TITLE, title);
|
||||
values.put(Groups.GROUP_VISIBLE, 1);
|
||||
bindAccount(resolver, values);
|
||||
final Uri inserted = resolver.insert(Groups.CONTENT_URI, values);
|
||||
if (inserted == null) {
|
||||
return;
|
||||
}
|
||||
final long groupId = android.content.ContentUris.parseId(inserted);
|
||||
if (groupId <= 0 || numbers == null) {
|
||||
return;
|
||||
}
|
||||
for (final String number : numbers) {
|
||||
final long rawId = findRawContactId(resolver, number);
|
||||
if (rawId > 0) {
|
||||
addMembership(resolver, rawId, groupId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static long findGroupId(final ContentResolver resolver, final String title) {
|
||||
try (Cursor cursor = resolver.query(Groups.CONTENT_URI,
|
||||
new String[] { Groups._ID },
|
||||
Groups.DELETED + "=0 AND " + Groups.TITLE + "=?",
|
||||
new String[] { title }, null)) {
|
||||
if (cursor != null && cursor.moveToFirst()) {
|
||||
return cursor.getLong(0);
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
private static void bindAccount(final ContentResolver resolver, final ContentValues values) {
|
||||
try (Cursor cursor = resolver.query(RawContacts.CONTENT_URI,
|
||||
new String[] { RawContacts.ACCOUNT_NAME, RawContacts.ACCOUNT_TYPE },
|
||||
RawContacts.DELETED + "=0", null, RawContacts._ID + " ASC")) {
|
||||
if (cursor != null && cursor.moveToFirst()) {
|
||||
final String name = cursor.getString(0);
|
||||
final String type = cursor.getString(1);
|
||||
if (!TextUtils.isEmpty(type)) {
|
||||
values.put(Groups.ACCOUNT_NAME, name);
|
||||
values.put(Groups.ACCOUNT_TYPE, type);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static long findRawContactId(final ContentResolver resolver, final String number) {
|
||||
if (TextUtils.isEmpty(number)) {
|
||||
return -1;
|
||||
}
|
||||
final String digits = digitsOnly(number);
|
||||
try (Cursor cursor = resolver.query(Phone.CONTENT_URI,
|
||||
new String[] { Phone.RAW_CONTACT_ID, Phone.NORMALIZED_NUMBER, Phone.NUMBER },
|
||||
null, null, null)) {
|
||||
if (cursor == null) {
|
||||
return -1;
|
||||
}
|
||||
while (cursor.moveToNext()) {
|
||||
final String normalized = cursor.getString(1);
|
||||
final String raw = cursor.getString(2);
|
||||
if (sameNumber(digits, normalized) || sameNumber(digits, raw)) {
|
||||
return cursor.getLong(0);
|
||||
}
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
private static void addMembership(final ContentResolver resolver, final long rawContactId,
|
||||
final long groupId) {
|
||||
final ContentValues values = new ContentValues();
|
||||
values.put(ContactsContract.Data.RAW_CONTACT_ID, rawContactId);
|
||||
values.put(ContactsContract.Data.MIMETYPE, GroupMembership.CONTENT_ITEM_TYPE);
|
||||
values.put(GroupMembership.GROUP_ROW_ID, groupId);
|
||||
resolver.insert(ContactsContract.Data.CONTENT_URI, values);
|
||||
}
|
||||
|
||||
private static boolean sameNumber(final String expectedDigits, final String candidate) {
|
||||
if (TextUtils.isEmpty(candidate)) {
|
||||
return false;
|
||||
}
|
||||
return TextUtils.equals(expectedDigits, digitsOnly(candidate));
|
||||
}
|
||||
|
||||
private static String digitsOnly(final String value) {
|
||||
final StringBuilder out = new StringBuilder(value.length());
|
||||
for (int i = 0; i < value.length(); i++) {
|
||||
final char c = value.charAt(i);
|
||||
if (c >= '0' && c <= '9') {
|
||||
out.append(c);
|
||||
}
|
||||
}
|
||||
return out.toString();
|
||||
}
|
||||
}
|
||||
@@ -5,30 +5,116 @@ import android.database.Cursor;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.AbsListView;
|
||||
import android.widget.CursorAdapter;
|
||||
import android.widget.SectionIndexer;
|
||||
|
||||
import com.android.messaging.R;
|
||||
import com.android.messaging.ui.contact.ContactListItemView;
|
||||
import com.android.messaging.ui.contact.ContactSectionIndexer;
|
||||
import com.android.messaging.util.Assert;
|
||||
|
||||
public class HomeContactListAdapter extends CursorAdapter {
|
||||
public class HomeContactListAdapter extends CursorAdapter implements SectionIndexer {
|
||||
private final ContactListItemView.HostInterface mHost;
|
||||
private ContactSectionIndexer mSectionIndexer;
|
||||
private boolean mNeedAlphabetHeader = true;
|
||||
|
||||
public HomeContactListAdapter(final Context context, final Cursor cursor,
|
||||
final ContactListItemView.HostInterface host) {
|
||||
super(context, cursor, 0);
|
||||
mHost = host;
|
||||
mSectionIndexer = new ContactSectionIndexer(cursor);
|
||||
}
|
||||
|
||||
public void setNeedAlphabetHeader(final boolean needAlphabetHeader) {
|
||||
if (mNeedAlphabetHeader == needAlphabetHeader) {
|
||||
return;
|
||||
}
|
||||
mNeedAlphabetHeader = needAlphabetHeader;
|
||||
notifyDataSetChanged();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void bindView(final View view, final Context context, final Cursor cursor) {
|
||||
Assert.isTrue(view instanceof ContactListItemView);
|
||||
((ContactListItemView) view).bind(cursor, mHost, false, null);
|
||||
String alphabetHeader = null;
|
||||
if (mNeedAlphabetHeader && mSectionIndexer.getSections().length > 0) {
|
||||
final int position = cursor.getPosition();
|
||||
final int section = mSectionIndexer.getSectionForPosition(position);
|
||||
if (section >= 0 && mSectionIndexer.getPositionForSection(section) == position) {
|
||||
alphabetHeader = (String) mSectionIndexer.getSections()[section];
|
||||
}
|
||||
}
|
||||
final ContactListItemView itemView = (ContactListItemView) view;
|
||||
itemView.bind(cursor, mHost, mNeedAlphabetHeader, alphabetHeader);
|
||||
itemView.setSectionBox(sectionRole(cursor.getPosition()));
|
||||
wrapListRow(itemView);
|
||||
}
|
||||
|
||||
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
|
||||
public View newView(final Context context, final Cursor cursor, final ViewGroup parent) {
|
||||
return LayoutInflater.from(context).inflate(R.layout.home_contacts_list_item, parent,
|
||||
false);
|
||||
final View view = LayoutInflater.from(context).inflate(R.layout.home_contacts_list_item,
|
||||
parent, false);
|
||||
wrapListRow(view);
|
||||
return view;
|
||||
}
|
||||
|
||||
private static void wrapListRow(final View view) {
|
||||
ViewGroup.LayoutParams params = view.getLayoutParams();
|
||||
if (params instanceof AbsListView.LayoutParams) {
|
||||
params.height = ViewGroup.LayoutParams.WRAP_CONTENT;
|
||||
view.setLayoutParams(params);
|
||||
return;
|
||||
}
|
||||
view.setLayoutParams(new AbsListView.LayoutParams(
|
||||
ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT));
|
||||
}
|
||||
|
||||
@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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,25 +2,24 @@ package com.android.messaging.ui.home;
|
||||
|
||||
import android.database.Cursor;
|
||||
import android.os.Bundle;
|
||||
import android.text.TextUtils;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.ListView;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import androidx.loader.app.LoaderManager;
|
||||
|
||||
import com.android.ex.chips.RecipientEntry;
|
||||
import com.android.messaging.R;
|
||||
import com.android.messaging.datamodel.DataModel;
|
||||
import com.android.messaging.datamodel.action.GetOrCreateConversationAction;
|
||||
import com.android.messaging.datamodel.action.ActionMonitor;
|
||||
import com.android.messaging.datamodel.action.GetOrCreateConversationAction;
|
||||
import com.android.messaging.datamodel.action.GetOrCreateConversationAction.GetOrCreateConversationActionListener;
|
||||
import com.android.messaging.datamodel.action.GetOrCreateConversationAction.GetOrCreateConversationActionMonitor;
|
||||
import com.android.messaging.util.Assert;
|
||||
import com.android.messaging.util.Assert.RunsOnMainThread;
|
||||
import com.android.messaging.datamodel.binding.Binding;
|
||||
import com.android.messaging.datamodel.binding.BindingBase;
|
||||
import com.android.messaging.datamodel.data.ContactListItemData;
|
||||
@@ -29,17 +28,39 @@ import com.android.messaging.datamodel.data.ContactPickerData.ContactPickerDataL
|
||||
import com.android.messaging.datamodel.data.ParticipantData;
|
||||
import com.android.messaging.ui.UIIntents;
|
||||
import com.android.messaging.ui.contact.ContactListItemView;
|
||||
import com.android.messaging.util.Assert;
|
||||
import com.android.messaging.util.Assert.RunsOnMainThread;
|
||||
import com.android.messaging.util.ContactRecipientEntryUtils;
|
||||
import com.android.messaging.util.ContactUtil;
|
||||
import com.android.messaging.util.LogUtil;
|
||||
import com.android.messaging.util.PhoneUtils;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Contact list for the home "new chat" screen. Tapping a row opens that conversation, or picks
|
||||
* the contact when the host screen is building a group.
|
||||
*/
|
||||
public class HomeContactsFragment extends Fragment implements ContactPickerDataListener,
|
||||
ContactListItemView.HostInterface, GetOrCreateConversationActionListener {
|
||||
|
||||
/** Implemented by a host that lets the user pick several contacts at once. */
|
||||
public interface GroupSelectionHost {
|
||||
void onGroupSelectionChanged(List<RecipientEntry> selected);
|
||||
}
|
||||
|
||||
private final Binding<ContactPickerData> mBinding = BindingBase.createBinding(this);
|
||||
private final LinkedHashMap<String, RecipientEntry> mSelected = new LinkedHashMap<>();
|
||||
private HomeContactListAdapter mAdapter;
|
||||
private GetOrCreateConversationActionMonitor mMonitor;
|
||||
private HomeEmptyState mEmptyView;
|
||||
private ContactListItemView mSendToView;
|
||||
private Cursor mAllContactsCursor;
|
||||
private String mFilterText;
|
||||
private boolean mGroupMode;
|
||||
private java.util.Set<String> mPendingSeed;
|
||||
|
||||
@Override
|
||||
public void onCreate(@Nullable final Bundle savedInstanceState) {
|
||||
@@ -61,23 +82,82 @@ public class HomeContactsFragment extends Fragment implements ContactPickerDataL
|
||||
public void onViewCreated(@NonNull final View view, @Nullable final Bundle savedInstanceState) {
|
||||
super.onViewCreated(view, savedInstanceState);
|
||||
final ListView listView = view.findViewById(R.id.home_contacts_list);
|
||||
mEmptyView = view.findViewById(R.id.home_contacts_empty);
|
||||
mSendToView = view.findViewById(R.id.home_contacts_send_to);
|
||||
mAdapter = new HomeContactListAdapter(requireContext(), null, this);
|
||||
listView.setAdapter(mAdapter);
|
||||
if (getParentFragment() instanceof GroupSelectionHost
|
||||
|| getActivity() instanceof GroupSelectionHost) {
|
||||
mGroupMode = true;
|
||||
}
|
||||
updateResultViews();
|
||||
}
|
||||
|
||||
view.findViewById(R.id.home_contacts_search).setOnClickListener(v ->
|
||||
Toast.makeText(requireContext(), R.string.home_popover_coming_soon,
|
||||
Toast.LENGTH_SHORT).show());
|
||||
view.findViewById(R.id.home_contacts_invite).setOnClickListener(v ->
|
||||
Toast.makeText(requireContext(), R.string.home_popover_coming_soon,
|
||||
Toast.LENGTH_SHORT).show());
|
||||
view.findViewById(R.id.home_contacts_recent_calls).setOnClickListener(v ->
|
||||
Toast.makeText(requireContext(), R.string.home_popover_coming_soon,
|
||||
Toast.LENGTH_SHORT).show());
|
||||
view.findViewById(R.id.home_contacts_sort).setOnClickListener(v ->
|
||||
Toast.makeText(requireContext(), R.string.home_popover_coming_soon,
|
||||
Toast.LENGTH_SHORT).show());
|
||||
view.findViewById(R.id.home_contacts_add).setOnClickListener(v ->
|
||||
UIIntents.get().launchCreateNewConversationActivity(requireActivity(), null));
|
||||
/** Switches between opening a conversation on tap and collecting group members. */
|
||||
public void setGroupMode(final boolean groupMode) {
|
||||
if (mGroupMode == groupMode) {
|
||||
return;
|
||||
}
|
||||
mGroupMode = groupMode;
|
||||
if (!groupMode) {
|
||||
mSelected.clear();
|
||||
}
|
||||
notifyGroupSelectionChanged();
|
||||
if (mAdapter != null) {
|
||||
mAdapter.notifyDataSetChanged();
|
||||
}
|
||||
}
|
||||
|
||||
public List<RecipientEntry> getSelectedEntries() {
|
||||
return new ArrayList<>(mSelected.values());
|
||||
}
|
||||
|
||||
public void seedSelection(final List<String> destinations) {
|
||||
mPendingSeed = destinations == null
|
||||
? null : new java.util.LinkedHashSet<>(destinations);
|
||||
applyPendingSeed();
|
||||
}
|
||||
|
||||
public ArrayList<ParticipantData> getSelectedParticipants() {
|
||||
final ArrayList<ParticipantData> participants = new ArrayList<>(mSelected.size());
|
||||
for (final RecipientEntry entry : mSelected.values()) {
|
||||
participants.add(ParticipantData.getFromRecipientEntry(entry));
|
||||
}
|
||||
return participants;
|
||||
}
|
||||
|
||||
/** Narrows the list as the host screen's recipient field is typed into. */
|
||||
public void setFilterText(final String filterText) {
|
||||
mFilterText = filterText;
|
||||
if (mAdapter != null) {
|
||||
mAdapter.setNeedAlphabetHeader(TextUtils.isEmpty(filterText));
|
||||
}
|
||||
if (mBinding.isBound()) {
|
||||
mBinding.getData().setSearchText(mBinding, filterText);
|
||||
}
|
||||
updateResultViews();
|
||||
}
|
||||
|
||||
/** Limits the list to one contacts account, or every account when {@code filter} is null. */
|
||||
public void setAccountFilter(final ContactUtil.AccountFilter filter) {
|
||||
if (mBinding.isBound()) {
|
||||
mBinding.getData().setAccountFilter(mBinding, filter);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Starts a conversation with the raw text currently typed, if it can be messaged.
|
||||
*
|
||||
* @return whether the text was a usable destination
|
||||
*/
|
||||
public boolean startConversationWithFilterText() {
|
||||
final String destination = mFilterText == null ? null : mFilterText.trim();
|
||||
if (TextUtils.isEmpty(destination)
|
||||
|| !PhoneUtils.isValidSmsMmsDestination(destination)) {
|
||||
return false;
|
||||
}
|
||||
startConversation(ContactRecipientEntryUtils.constructSendToDestinationEntry(destination));
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -95,7 +175,21 @@ public class HomeContactsFragment extends Fragment implements ContactPickerDataL
|
||||
@Override
|
||||
public void onAllContactsCursorUpdated(final Cursor data) {
|
||||
mBinding.ensureBound();
|
||||
mAdapter.changeCursor(data);
|
||||
mAllContactsCursor = data;
|
||||
if (TextUtils.isEmpty(mFilterText)) {
|
||||
mAdapter.changeCursor(data);
|
||||
applyPendingSeed();
|
||||
updateResultViews();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFilteredContactsCursorUpdated(final Cursor data) {
|
||||
mBinding.ensureBound();
|
||||
// A null cursor means the search was cleared, so fall back to the full list.
|
||||
mAdapter.changeCursor(data != null ? data : mAllContactsCursor);
|
||||
applyPendingSeed();
|
||||
updateResultViews();
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -112,29 +206,90 @@ public class HomeContactsFragment extends Fragment implements ContactPickerDataL
|
||||
@Override
|
||||
public void onContactListItemClicked(final ContactListItemData item,
|
||||
final ContactListItemView view) {
|
||||
if (!mGroupMode) {
|
||||
startConversation(item.getRecipientEntry());
|
||||
return;
|
||||
}
|
||||
final RecipientEntry entry = item.getRecipientEntry();
|
||||
final String key = selectionKey(entry);
|
||||
if (mSelected.remove(key) == null) {
|
||||
mSelected.put(key, entry);
|
||||
}
|
||||
notifyGroupSelectionChanged();
|
||||
if (mAdapter != null) {
|
||||
mAdapter.notifyDataSetChanged();
|
||||
}
|
||||
}
|
||||
|
||||
private void applyPendingSeed() {
|
||||
if (mPendingSeed == null || mPendingSeed.isEmpty() || mAdapter == null) {
|
||||
return;
|
||||
}
|
||||
final Cursor cursor = mAdapter.getCursor();
|
||||
if (cursor == null || cursor.getCount() == 0) {
|
||||
return;
|
||||
}
|
||||
final ContactListItemData item = DataModel.get().createContactListItemData();
|
||||
final int position = cursor.getPosition();
|
||||
if (cursor.moveToFirst()) {
|
||||
do {
|
||||
item.bind(cursor, null);
|
||||
final RecipientEntry entry = item.getRecipientEntry();
|
||||
final String key = selectionKey(entry);
|
||||
if (mPendingSeed.remove(key)) {
|
||||
mSelected.put(key, entry);
|
||||
}
|
||||
} while (cursor.moveToNext() && !mPendingSeed.isEmpty());
|
||||
}
|
||||
cursor.moveToPosition(position);
|
||||
notifyGroupSelectionChanged();
|
||||
mAdapter.notifyDataSetChanged();
|
||||
}
|
||||
|
||||
private static String selectionKey(final RecipientEntry entry) {
|
||||
return PhoneUtils.getDefault().getCanonicalBySystemLocale(entry.getDestination());
|
||||
}
|
||||
|
||||
private void notifyGroupSelectionChanged() {
|
||||
if (getParentFragment() instanceof GroupSelectionHost) {
|
||||
((GroupSelectionHost) getParentFragment()).onGroupSelectionChanged(
|
||||
new ArrayList<>(mSelected.values()));
|
||||
} else if (getActivity() instanceof GroupSelectionHost) {
|
||||
((GroupSelectionHost) getActivity()).onGroupSelectionChanged(
|
||||
new ArrayList<>(mSelected.values()));
|
||||
}
|
||||
}
|
||||
|
||||
private void startConversation(final RecipientEntry entry) {
|
||||
if (mMonitor != null) {
|
||||
return;
|
||||
}
|
||||
final ArrayList<ParticipantData> participants = new ArrayList<>(1);
|
||||
participants.add(ParticipantData.getFromRecipientEntry(item.getRecipientEntry()));
|
||||
mMonitor = GetOrCreateConversationAction.getOrCreateConversation(participants, null,
|
||||
this);
|
||||
participants.add(ParticipantData.getFromRecipientEntry(entry));
|
||||
mMonitor = GetOrCreateConversationAction.getOrCreateConversation(participants, null, this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isContactSelected(final ContactListItemData item) {
|
||||
return false;
|
||||
return mGroupMode && mSelected.containsKey(selectionKey(item.getRecipientEntry()));
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isContactSelectionMode() {
|
||||
return mGroupMode || getParentFragment() instanceof GroupSelectionHost
|
||||
|| getActivity() instanceof HomeComposeActivity;
|
||||
}
|
||||
|
||||
@Override
|
||||
@RunsOnMainThread
|
||||
public void onGetOrCreateConversationSucceeded(final ActionMonitor monitor,
|
||||
final Object data, final String conversationId) {
|
||||
public void onGetOrCreateConversationSucceeded(final ActionMonitor monitor, final Object data,
|
||||
final String conversationId) {
|
||||
Assert.isTrue(monitor == mMonitor);
|
||||
mMonitor = null;
|
||||
if (conversationId != null) {
|
||||
if (conversationId != null && getActivity() != null) {
|
||||
UIIntents.get().launchConversationActivity(requireActivity(), conversationId, null,
|
||||
null, false);
|
||||
getActivity().finish();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -145,4 +300,63 @@ public class HomeContactsFragment extends Fragment implements ContactPickerDataL
|
||||
LogUtil.e(LogUtil.BUGLE_TAG, "HomeContactsFragment onGetOrCreateConversationFailed");
|
||||
mMonitor = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Shows the typed destination as a tappable row whenever the filter matched no contact, so a
|
||||
* number that is not in the address book can still start a conversation.
|
||||
*/
|
||||
private void updateResultViews() {
|
||||
if (mEmptyView == null || mSendToView == null) {
|
||||
return;
|
||||
}
|
||||
final String destination = mFilterText == null ? null : mFilterText.trim();
|
||||
final boolean filtering = !TextUtils.isEmpty(destination);
|
||||
final boolean noMatches = mAdapter == null || mAdapter.getCount() == 0;
|
||||
final boolean showSendTo = filtering && noMatches
|
||||
&& PhoneUtils.isValidSmsMmsDestination(destination);
|
||||
|
||||
if (showSendTo) {
|
||||
final RecipientEntry entry =
|
||||
ContactRecipientEntryUtils.constructSendToDestinationEntry(destination);
|
||||
mSendToView.bind(entry, getString(R.string.home_compose_send_to,
|
||||
ContactRecipientEntryUtils.formatDestination(entry)), null, this, true, false);
|
||||
// The RecipientEntry bind path is meant for dropdowns, which handle clicks themselves.
|
||||
mSendToView.setOnClickListener(v -> {
|
||||
if (mGroupMode) {
|
||||
final String key = selectionKey(entry);
|
||||
if (mSelected.remove(key) == null) {
|
||||
mSelected.put(key, entry);
|
||||
}
|
||||
notifyGroupSelectionChanged();
|
||||
if (mAdapter != null) {
|
||||
mAdapter.notifyDataSetChanged();
|
||||
}
|
||||
} else {
|
||||
startConversation(entry);
|
||||
}
|
||||
});
|
||||
mSendToView.setVisibility(View.VISIBLE);
|
||||
} else {
|
||||
mSendToView.setVisibility(View.GONE);
|
||||
}
|
||||
|
||||
if (!noMatches || showSendTo) {
|
||||
mEmptyView.setVisibility(View.GONE);
|
||||
return;
|
||||
}
|
||||
if (filtering) {
|
||||
mEmptyView.setContent(R.drawable.ic_home_nav_contacts,
|
||||
R.string.home_empty_contacts_no_results_title,
|
||||
R.string.home_empty_contacts_no_results_body);
|
||||
} else if (ContactUtil.hasReadContactsPermission()) {
|
||||
mEmptyView.setContent(R.drawable.ic_home_nav_contacts,
|
||||
R.string.home_empty_contacts_title,
|
||||
R.string.home_empty_contacts_body);
|
||||
} else {
|
||||
mEmptyView.setContent(R.drawable.ic_home_nav_contacts,
|
||||
R.string.home_empty_contacts_permission_title,
|
||||
R.string.home_empty_contacts_permission_body);
|
||||
}
|
||||
mEmptyView.setVisibility(View.VISIBLE);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,231 @@
|
||||
/*
|
||||
* Copyright (C) 2025 The LineageOS Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.android.messaging.ui.home;
|
||||
|
||||
import android.content.Context;
|
||||
import android.database.Cursor;
|
||||
import android.os.Bundle;
|
||||
import android.text.Editable;
|
||||
import android.text.Spannable;
|
||||
import android.text.SpannableString;
|
||||
import android.text.TextUtils;
|
||||
import android.text.TextWatcher;
|
||||
import android.text.style.BackgroundColorSpan;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.EditText;
|
||||
import android.widget.ImageButton;
|
||||
import android.widget.ListView;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.cursoradapter.widget.CursorAdapter;
|
||||
import androidx.loader.app.LoaderManager;
|
||||
|
||||
import com.android.messaging.R;
|
||||
import com.android.messaging.datamodel.DataModel;
|
||||
import com.android.messaging.datamodel.binding.Binding;
|
||||
import com.android.messaging.datamodel.binding.BindingBase;
|
||||
import com.android.messaging.datamodel.data.ConversationMessageData;
|
||||
import com.android.messaging.datamodel.data.ConversationSearchData;
|
||||
import com.android.messaging.datamodel.data.ConversationSearchData.ConversationSearchDataListener;
|
||||
import com.android.messaging.ui.BugleActionBarActivity;
|
||||
import com.android.messaging.ui.ContactIconView;
|
||||
import com.android.messaging.ui.UIIntents;
|
||||
import com.android.messaging.util.AvatarUriUtil;
|
||||
import com.android.messaging.util.Dates;
|
||||
import com.android.messaging.util.ImeUtil;
|
||||
|
||||
/** Searches the messages of a single conversation as the user types. */
|
||||
public class HomeConversationSearchActivity extends BugleActionBarActivity
|
||||
implements ConversationSearchDataListener {
|
||||
|
||||
private final Binding<ConversationSearchData> mBinding = BindingBase.createBinding(this);
|
||||
|
||||
private String mConversationId;
|
||||
private EditText mSearchField;
|
||||
private ImageButton mClearButton;
|
||||
private ListView mListView;
|
||||
private HomeEmptyState mEmptyView;
|
||||
private SearchResultAdapter mAdapter;
|
||||
|
||||
@Override
|
||||
protected void onCreate(final Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
setContentView(R.layout.home_conversation_search_activity);
|
||||
if (getSupportActionBar() != null) {
|
||||
getSupportActionBar().hide();
|
||||
}
|
||||
|
||||
mConversationId = getIntent().getStringExtra(UIIntents.UI_INTENT_EXTRA_CONVERSATION_ID);
|
||||
if (TextUtils.isEmpty(mConversationId)) {
|
||||
finish();
|
||||
return;
|
||||
}
|
||||
|
||||
mListView = findViewById(R.id.home_conversation_search_list);
|
||||
mEmptyView = findViewById(R.id.home_conversation_search_empty);
|
||||
mAdapter = new SearchResultAdapter(this);
|
||||
mListView.setAdapter(mAdapter);
|
||||
mListView.setOnItemClickListener((parent, view, position, id) -> openConversation());
|
||||
|
||||
HomeSubHeader.bind(this, R.string.home_search_title, v -> finish());
|
||||
|
||||
mClearButton = findViewById(R.id.home_conversation_search_clear);
|
||||
mClearButton.setOnClickListener(v -> mSearchField.setText(""));
|
||||
|
||||
mSearchField = findViewById(R.id.home_conversation_search_field);
|
||||
mSearchField.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) {
|
||||
mClearButton.setVisibility(TextUtils.isEmpty(s) ? View.GONE : View.VISIBLE);
|
||||
mBinding.getData().setSearchText(mBinding, s.toString());
|
||||
updateEmptyView();
|
||||
}
|
||||
});
|
||||
mSearchField.setOnEditorActionListener((v, actionId, event) -> {
|
||||
ImeUtil.get().hideImeKeyboard(this, mSearchField);
|
||||
return true;
|
||||
});
|
||||
|
||||
mBinding.bind(DataModel.get().createConversationSearchData(this, mConversationId, this));
|
||||
mBinding.getData().init(LoaderManager.getInstance(this));
|
||||
|
||||
mSearchField.requestFocus();
|
||||
mSearchField.post(() -> ImeUtil.get().showImeKeyboard(this, mSearchField));
|
||||
updateEmptyView();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDestroy() {
|
||||
super.onDestroy();
|
||||
mBinding.unbind();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onConversationSearchCursorUpdated(final Cursor cursor) {
|
||||
mAdapter.swapCursor(cursor);
|
||||
updateEmptyView();
|
||||
}
|
||||
|
||||
private void updateEmptyView() {
|
||||
if (mEmptyView == null || mListView == null) {
|
||||
return;
|
||||
}
|
||||
final String searchText = mBinding.isBound() ? mBinding.getData().getSearchText() : null;
|
||||
if (TextUtils.isEmpty(searchText)) {
|
||||
mEmptyView.setContent(R.drawable.ic_home_search_line,
|
||||
R.string.home_empty_conversation_search_idle_title,
|
||||
R.string.home_empty_conversation_search_idle_body);
|
||||
mEmptyView.setVisibility(View.VISIBLE);
|
||||
mListView.setVisibility(View.GONE);
|
||||
return;
|
||||
}
|
||||
final boolean empty = mAdapter.getCount() == 0;
|
||||
if (empty) {
|
||||
mEmptyView.setContent(R.drawable.ic_home_search_line,
|
||||
getString(R.string.home_empty_conversation_search_title),
|
||||
getString(R.string.home_empty_conversation_search_body, searchText));
|
||||
}
|
||||
mEmptyView.setVisibility(empty ? View.VISIBLE : View.GONE);
|
||||
mListView.setVisibility(empty ? View.GONE : View.VISIBLE);
|
||||
}
|
||||
|
||||
private void openConversation() {
|
||||
// Bugle cannot open a conversation anchored on a single message, so land on the latest.
|
||||
UIIntents.get().launchConversationActivity(this, mConversationId, null, null, false);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPause() {
|
||||
if (mSearchField != null) {
|
||||
ImeUtil.get().hideImeKeyboard(this, mSearchField);
|
||||
}
|
||||
super.onPause();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void finish() {
|
||||
if (mSearchField != null) {
|
||||
ImeUtil.get().hideImeKeyboard(this, mSearchField);
|
||||
}
|
||||
super.finish();
|
||||
overridePendingTransition(R.anim.home_activity_hold, R.anim.home_activity_close);
|
||||
}
|
||||
|
||||
private class SearchResultAdapter extends CursorAdapter {
|
||||
|
||||
SearchResultAdapter(final Context context) {
|
||||
super(context, null, 0);
|
||||
}
|
||||
|
||||
@Override
|
||||
public View newView(final Context context, final Cursor cursor, final ViewGroup parent) {
|
||||
return LayoutInflater.from(context)
|
||||
.inflate(R.layout.home_conversation_search_item, parent, false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void bindView(final View view, final Context context, final Cursor cursor) {
|
||||
final ConversationMessageData message = new ConversationMessageData();
|
||||
message.bind(cursor);
|
||||
|
||||
final ContactIconView avatar =
|
||||
view.findViewById(R.id.home_conversation_search_avatar);
|
||||
avatar.setImageResourceUri(AvatarUriUtil.createAvatarUri(
|
||||
message.getSenderProfilePhotoUri(), message.getSenderFullName(),
|
||||
message.getSenderNormalizedDestination(),
|
||||
message.getSenderContactLookupKey()),
|
||||
message.getSenderContactId(), message.getSenderContactLookupKey(),
|
||||
message.getSenderNormalizedDestination());
|
||||
avatar.setImageClickHandlerDisabled(true);
|
||||
|
||||
((TextView) view.findViewById(R.id.home_conversation_search_sender)).setText(
|
||||
message.getIsIncoming() ? message.getSenderDisplayName()
|
||||
: getString(R.string.home_conversation_search_you));
|
||||
((TextView) view.findViewById(R.id.home_conversation_search_time)).setText(
|
||||
Dates.getConversationTimeString(message.getReceivedTimeStamp()));
|
||||
((TextView) view.findViewById(R.id.home_conversation_search_text))
|
||||
.setText(highlight(message.getText()));
|
||||
}
|
||||
|
||||
private CharSequence highlight(final String text) {
|
||||
final String searchText = mBinding.getData().getSearchText();
|
||||
if (TextUtils.isEmpty(text) || TextUtils.isEmpty(searchText)) {
|
||||
return text;
|
||||
}
|
||||
final int start = text.toLowerCase().indexOf(searchText.toLowerCase());
|
||||
if (start < 0) {
|
||||
return text;
|
||||
}
|
||||
final SpannableString spannable = new SpannableString(text);
|
||||
spannable.setSpan(new BackgroundColorSpan(getColor(R.color.home_accent_soft)),
|
||||
start, start + searchText.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||
return spannable;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
/*
|
||||
* Copyright (C) 2025 The LineageOS Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package com.android.messaging.ui.home;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import com.android.messaging.R;
|
||||
|
||||
/** Home modal for moving conversations to Trash from the conversation list. */
|
||||
public final class HomeDeleteAlertDialog {
|
||||
|
||||
public interface Listener {
|
||||
void onDelete();
|
||||
}
|
||||
|
||||
private HomeDeleteAlertDialog() {
|
||||
}
|
||||
|
||||
public static void show(@NonNull final Context context, final int conversationCount,
|
||||
@NonNull final Listener listener) {
|
||||
final String title = context.getResources().getQuantityString(
|
||||
R.plurals.delete_conversations_confirmation_dialog_title, conversationCount);
|
||||
final String message = conversationCount == 1
|
||||
? context.getString(R.string.home_delete_alert_message_one)
|
||||
: context.getString(R.string.home_delete_alert_message_many, conversationCount);
|
||||
HomeModal.builder(context)
|
||||
.icon(R.drawable.ic_home_menu_trash)
|
||||
.title(title)
|
||||
.message(message)
|
||||
.positive(R.string.home_delete_confirm, listener::onDelete)
|
||||
.negative(R.string.delete_conversation_decline_button, null)
|
||||
.show();
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user