Compare commits

..
15 Commits
Author SHA1 Message Date
privaongandCursor 0b2f7dfdb5 Messaging: Add automation compose and rebrand as com.bnkode.notie.
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-27 04:55:27 +09:00
privaongandCursor a34cccdf76 Messaging: Show the automation home preview and shorten pill shadows.
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-07 03:18:08 +09:00
privaongandCursor 49789fc4a5 Messaging: Vendor chips, colorpicker, photoviewer, and vcard in-tree
Clone no longer depends on LineageOS gitlinks; the libraries used by the app live in this repo.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-29 06:19:53 +09:00
privaongandCursor a6b23f0f3c Messaging: Split trash from archive and match home selection chrome
Inbox delete now parks conversations in a 30-day trash, and spam/archive/trash lists share the home select/search chrome.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-29 06:16:01 +09:00
privaongandCursor 47d735be2c Messaging: Meet Play memory/R8 rules, ship Q icon, and polish home chrome
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-28 23:44:47 +09:00
privaongandCursor 74fb80f783 Messaging: Add scheduled send, location share, app lock, and settings polish
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-27 08:13:21 +09:00
privaongandCursor 42148360e0 Messaging: Add home categories and shared modal/input chrome
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-27 02:41:59 +09:00
privaongandCursor a32250fe34 Messaging: Looply search hub with frequent contacts and nav inset
Redesign Search around frequent contacts, category counts, and recent chips, and keep the Messages/Automation pill on the left guideline without moving compose.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-26 05:36:08 +09:00
privaongandCursor 88529c3634 Messaging: Looply empty states, popovers, and conversation polish
Keep English-only UI, align home menus to the screen guideline, and
match message-action icon weight across conversation and list popovers.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-26 05:03:33 +09:00
privaongandCursor 8e5d88183c Messaging: Looply conversation profile, search, and in-app gallery
Replace People & Options with the home profile and search screens, and attach media from an in-conversation gallery sheet that avoids the system picker flash.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-25 01:23:10 +09:00
privaongandCursor 349eeb7025 Messaging: Looply home UI and Gradle app module layout
Restructure the tree for Android Studio/Gradle builds and add the iOS-style home experience with pinned conversations, selection chrome, and scroll-aware search padding.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-24 18:19:48 +09:00
SaeedDev94 6d131ed8e7 Messaging: Fix white background revealed by conversation list swipe in dark theme
Change-Id: Ifb9bb7dfed7ea7b5a8f1308de99d53f7103400d7
2026-08-28 10:55:43 -05:00
SaeedDev94 2f374d38c5 Messaging: Fix white background in contact search/recipient picker in dark theme
Change-Id: If55125ac2e3eb8c5c8c68bcfb9afde23bb052d76
2026-08-28 10:53:57 -05:00
SaeedDev94 cfb5f5d367 Messaging: Enable elegant text height for arabic script rendering
Change-Id: I2056c445890f5fd10e24c0f21439b9d11fcec119
2026-08-28 10:52:51 -05:00
LineageOS Infra 117df5fedc Automatic translation import
Change-Id: I42ddc8fd1da0ca068fb5729dab4dcd38ea39360a
2026-06-15 17:55:01 +00:00
1722 changed files with 79349 additions and 41130 deletions
+27
View File
@@ -0,0 +1,27 @@
*.iml
.cursor/
.gradle/
/local.properties
/keystore.properties
/keystore/
*.jks
*.keystore
/.idea/
.DS_Store
/build/
/captures/
.externalNativeBuild/
.cxx/
app/build/
modules/*/build/
modules/*/.git/
modules/*/sample/
*.apk
*.ap_
*.dex
*.class
bin/
gen/
out/
tools/messaging_*.png
tools/stitch-*.txt
+5 -3
View File
@@ -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,
}
+110
View File
@@ -0,0 +1,110 @@
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.bnkode.notie'
minSdk 33
targetSdk 34
versionCode 10001040
versionName '1.0.001'
vectorDrawables.useSupportLibrary true
resourceConfigurations += ['en', 'ko']
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_17
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')
}
debug {
minifyEnabled false
}
}
lint {
abortOnError false
}
sourceSets {
main {
manifest.srcFile 'src/main/AndroidManifest.xml'
java.srcDirs = ['src/main/java']
res.srcDirs = ['src/main/res']
assets.srcDirs = ['src/main/assets']
}
}
}
dependencies {
implementation project(':modules:colorpicker')
implementation project(':modules:chips')
implementation project(':modules:photoviewer')
implementation project(':modules:vcard')
implementation 'androidx.activity:activity:1.9.3'
implementation 'androidx.annotation:annotation:1.8.2'
implementation 'androidx.appcompat:appcompat:1.7.0'
implementation 'androidx.collection:collection:1.4.4'
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'
implementation 'androidx.recyclerview:recyclerview:1.3.2'
implementation 'androidx.viewpager:viewpager:1.0.0'
implementation 'com.google.android.material:material:1.12.0'
implementation 'com.google.guava:guava:33.3.1-android'
implementation 'com.googlecode.libphonenumber:libphonenumber:8.13.50'
}
def messagingAvdSerial = rootProject.findProperty('messaging.avd.serial') ?: 'emulator-5556'
def adbExecutable = new File(android.sdkDirectory, 'platform-tools/adb.exe')
tasks.register('installMessaging', Exec) {
group = 'install'
description = "Install debug APK on ${messagingAvdSerial} only"
dependsOn 'assembleDebug'
def apk = layout.buildDirectory.file('outputs/apk/debug/app-debug.apk').get().asFile
commandLine adbExecutable.absolutePath, '-s', messagingAvdSerial, 'install', '-r', apk.absolutePath
}
tasks.register('launchMessaging', Exec) {
group = 'install'
description = "Launch Messaging on ${messagingAvdSerial}"
dependsOn 'installMessaging'
commandLine adbExecutable.absolutePath, '-s', messagingAvdSerial, 'shell', 'am', 'start',
'-n', 'com.bnkode.notie/com.android.messaging.ui.conversationlist.ConversationListActivity'
}
@@ -19,10 +19,6 @@
package="com.android.messaging"
android:installLocation="internalOnly">
<uses-sdk
android:minSdkVersion="33"
android:targetSdkVersion="34" />
<!-- Application holds CPU wakelock while working in background -->
<uses-permission android:name="android.permission.WAKE_LOCK" />
<!-- Application needs SMS/MMS permissions -->
@@ -48,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
@@ -65,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>
@@ -83,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">
@@ -136,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
@@ -159,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"
@@ -175,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"
@@ -183,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"
@@ -192,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" />
@@ -227,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
@@ -263,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 -->
@@ -286,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" />
@@ -355,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>
@@ -405,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"
@@ -416,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: '&copy; OpenStreetMap'
}).addTo(map);
function notify() {
var c = map.getCenter();
if (window.LocationBridge) {
LocationBridge.onMapMoved(c.lat, c.lng);
}
}
map.on('moveend', notify);
function setCenter(lat, lng) {
map.setView([lat, lng], Math.max(map.getZoom(), 16));
}
</script>
</body>
</html>
@@ -36,6 +36,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);
}
}

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