Compare commits

..
54 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
LineageOS Infra 039afc3213 Automatic translation import
Change-Id: I53bd8d5ce4d19a19bf1ea9567dc6426c7bda204d
2026-06-01 17:55:39 +00:00
LineageOS Infra 2848bf3f84 Automatic translation import
Change-Id: Id13969cfecf8ddbe0e856597c5b5da1668af3c85
2026-05-15 22:34:04 +00:00
LineageOS Infra 70d5007e57 Automatic translation import
Change-Id: I5737865b4a1662903971a4857cb9b3bd8abf7521
2026-03-01 20:42:26 +00:00
LineageOS Infra 0965ba5984 Automatic translation import
Change-Id: Ifb1378276707ece167f863d93e631fcf8c45c288
2026-02-01 18:36:35 +00:00
LineageOS Infra 1347bcf4df Automatic translation import
Change-Id: I8d7f968c156b952340346dc0edb72d7c8ee5c326
2025-09-24 20:06:46 +00:00
Michael Bestas b57c41709d Merge tag 'android-16.0.0_r1' into lineage-23.0
Android 16.0.0 release 1

# -----BEGIN PGP SIGNATURE-----
#
# iF0EABECAB0WIQRDQNE1cO+UXoOBCWTorT+BmrEOeAUCaEhhFAAKCRDorT+BmrEO
# eLOdAKCHPJhTvoM0MAV2zS8sTjDG0Y4hwQCfckYdgli0zMEeKt+oxUJzQ0ORmNA=
# =f32s
# -----END PGP SIGNATURE-----
# gpg: Signature made Tue Jun 10 19:45:08 2025 EEST
# gpg:                using DSA key 4340D13570EF945E83810964E8AD3F819AB10E78
# gpg: Good signature from "The Android Open Source Project <initial-contribution@android.com>" [ultimate]

# By Christoffer Adamsen (2) and others
# Via Automerger Merge Worker (15) and others
* tag 'android-16.0.0_r1':
  [owners] Remove rtenneti@google.com from OWNERS
  packages/apps/Messaging: remove deprecated Mockito usages
  Do not keep runtime invisible annotations in proguard.flags
  Explicitly keep default constructor in rules without members
  Revert "Remove abandoned implementation."
  Remove abandoned implementation.

 Conflicts:
	proguard.flags
	tests/src/com/android/messaging/FakeFactory.java
	tests/src/com/android/messaging/datamodel/media/ImageRequestTest.java
	tests/src/com/android/messaging/ui/attachmentchooser/AttachmentChooserFragmentTest.java
	tests/src/com/android/messaging/ui/contact/ContactPickerFragmentTest.java
	tests/src/com/android/messaging/ui/conversation/ComposeMessageViewTest.java
	tests/src/com/android/messaging/ui/conversation/ConversationFragmentTest.java
	tests/src/com/android/messaging/ui/conversationlist/ConversationListFragmentTest.java
	tests/src/com/android/messaging/ui/mediapicker/AudioRecordViewTest.java
	tests/src/com/android/messaging/ui/mediapicker/GalleryGridItemViewTest.java
	tests/src/com/android/messaging/ui/mediapicker/MediaPickerTest.java

Change-Id: Ica660f1b6ac136254bc8e91ebee9469f30ebd7c1
2025-06-15 21:21:15 +03:00
Michael W 8af12822f8 Messaging: Remove setHost() logic
* This relies too much on call order and other things to go right
* We can use the activity just fine for it

Fixes: https://gitlab.com/LineageOS/issues/android/-/issues/8623
Change-Id: I048910258cedaa21e26fb88a57e5028ae1df9f83
2025-05-18 09:18:48 +00:00
Michael W 6161d0fca1 Messaging: Get rid of everything around the local apn database
Change-Id: Ie1916e93b274009494e76d6d99f36a025139127d
2025-05-18 09:18:48 +00:00
Michael W aad904a01b Messaging: Use Start and End instead of Left and Right
That way we don't need a separate file for these styles anymore

Change-Id: If689fba727f9b678b628c7bfb51fa8e4474d68de
2025-05-18 09:18:48 +00:00
Michael W 2f9b1386f8 Messaging: Remove unused or not required classes and methods
Change-Id: I28ebaecc503ee58c74fc8081162777f861dd4516
2025-05-18 09:18:48 +00:00
Android Build Coastguard Worker 82c6ca9602 Snap for 13135932 from bc5b8a2d4f to 25Q2-release
Change-Id: I82f72500b62137cdd125b2835d36753c2b14d786
2025-02-27 16:19:24 -08:00
Treehugger Robot bc5b8a2d4f Merge "packages/apps/Messaging: remove deprecated Mockito usages" into main am: cf93bb5c3d am: de315b7623
Original change: https://android-review.googlesource.com/c/platform/packages/apps/Messaging/+/3508038

Change-Id: I69a284022c22523dd1b7c04179345811d5aa8ab0
Signed-off-by: Automerger Merge Worker <android-build-automerger-merge-worker@system.gserviceaccount.com>
2025-02-27 11:45:18 -08:00
Android Build Coastguard Worker 777797fa53 Snap for 13129673 from a7f581e01b to 25Q2-release
Change-Id: Id6afe5adc14394481567530a7101c7bd185bebcd
2025-02-26 16:24:19 -08:00
Treehugger Robot de315b7623 Merge "packages/apps/Messaging: remove deprecated Mockito usages" into main am: cf93bb5c3d
Original change: https://android-review.googlesource.com/c/platform/packages/apps/Messaging/+/3508038

Change-Id: Ie5e4bf0a521a4a5dec5fe231082b3e8d663a0724
Signed-off-by: Automerger Merge Worker <android-build-automerger-merge-worker@system.gserviceaccount.com>
2025-02-26 09:34:34 -08:00
Treehugger Robot cf93bb5c3d Merge "packages/apps/Messaging: remove deprecated Mockito usages" into main 2025-02-26 09:09:41 -08:00
Owner Cleanup Bot a7f581e01b [owners] Remove rtenneti@google.com from OWNERS am: cc819dae0b am: 5d53470df3
Original change: https://android-review.googlesource.com/c/platform/packages/apps/Messaging/+/3505964

Change-Id: I9c9d6cf6d32b0df880905469ea7b98f5dc4aa0d7
Signed-off-by: Automerger Merge Worker <android-build-automerger-merge-worker@system.gserviceaccount.com>
2025-02-25 16:11:45 -08:00
Owner Cleanup Bot 5d53470df3 [owners] Remove rtenneti@google.com from OWNERS am: cc819dae0b
Original change: https://android-review.googlesource.com/c/platform/packages/apps/Messaging/+/3505964

Change-Id: Ibc4ee28788e023d707884cddec42e0edb2d9ddbb
Signed-off-by: Automerger Merge Worker <android-build-automerger-merge-worker@system.gserviceaccount.com>
2025-02-25 15:36:18 -08:00
Owner Cleanup Bot cc819dae0b [owners] Remove rtenneti@google.com from OWNERS
This suggested change is automatically generated based on group
memberships and affiliations.

Please approve this change and vote the highest CR. This will keep the OWNERs
file tidy.  We ask that you do not ignore this change and approve it
unless you know a reason the OWNER should remain.  It can always be
reverted if needed.

If this change is in error, vote the lowest CR value (i.e. reject the CL)
and the bot will abandon it.

See the owner's recent review activity for context:
https://android-review.googlesource.com/q/rtenneti@google.com

To report an issue, file a bug in the Infra>Codereview component.

Change-Id: I4b84a77bf8a7bd01b8d58315021ea2249a6f539e
2025-02-25 09:31:37 -08:00
Adrian Roos 29980d714f packages/apps/Messaging: remove deprecated Mockito usages
Test: m checkbuild
Flag: TEST_ONLY
Change-Id: I9e62dd82bfbf00b13dce8210574bf146eea191f0
2025-02-25 16:51:18 +01:00
Android Build Coastguard Worker 8900099676 Snap for 12991197 from 49c3d392ad to 25Q2-release
Change-Id: I35c42e11115b922f2b21949b12a5094e454c097e
2025-01-30 16:18:04 -08:00
Christoffer Adamsen 49c3d392ad Do not keep runtime invisible annotations in proguard.flags am: b0f8c82e37 am: 817de4cf62
Original change: https://android-review.googlesource.com/c/platform/packages/apps/Messaging/+/3455602

Change-Id: I874a545cfde2310b4714879d1d001c3c4f47737d
Signed-off-by: Automerger Merge Worker <android-build-automerger-merge-worker@system.gserviceaccount.com>
2025-01-30 11:13:31 -08:00
Christoffer Adamsen 817de4cf62 Do not keep runtime invisible annotations in proguard.flags am: b0f8c82e37
Original change: https://android-review.googlesource.com/c/platform/packages/apps/Messaging/+/3455602

Change-Id: I5c7bcb5fbf8a125fa2993f1da79c6e2ae50ba623
Signed-off-by: Automerger Merge Worker <android-build-automerger-merge-worker@system.gserviceaccount.com>
2025-01-30 10:49:58 -08:00
Christoffer Adamsen b0f8c82e37 Do not keep runtime invisible annotations in proguard.flags
This updates the corresponding Android.bp file to keep the runtime invisible annotations, meaning this should be a no-op.

When RELEASE_R8_ONLY_RUNTIME_VISIBLE_ANNOTATIONS is enabled, keep_runtime_invisible_annotations will become a no-op, which should lead to all runtime invisible annotations being stripped from this build.

Bug: 387958004
Change-Id: Ib0090cd30d44630a1b0a1d1f0605edb7fef8c5cf
Test: existing
2025-01-17 04:34:59 -08:00
Android Build Coastguard Worker 81920e8179 Snap for 12896565 from 3e8c4c36c6 to 25Q2-release
Change-Id: I92020d203fe787ccbf0735f50dc4efa223506327
2025-01-09 18:17:44 -08:00
Christoffer Adamsen 3e8c4c36c6 Explicitly keep default constructor in rules without members am: 96af708145 am: e891b3615d
Original change: https://android-review.googlesource.com/c/platform/packages/apps/Messaging/+/3438699

Change-Id: I0d9ba3367e893f5299436eb3c12ad4e86f96cc78
Signed-off-by: Automerger Merge Worker <android-build-automerger-merge-worker@system.gserviceaccount.com>
2025-01-09 05:30:12 -08:00
Christoffer Adamsen e891b3615d Explicitly keep default constructor in rules without members am: 96af708145
Original change: https://android-review.googlesource.com/c/platform/packages/apps/Messaging/+/3438699

Change-Id: Ic7a716a9384ebc40499b03980fe3317ad6e741c3
Signed-off-by: Automerger Merge Worker <android-build-automerger-merge-worker@system.gserviceaccount.com>
2025-01-09 05:04:04 -08:00
Christoffer Adamsen 96af708145 Explicitly keep default constructor in rules without members
R8 implicitly keeps the default constructor when no member rules are specified. Future versions of R8 will require apps to explicitly keep the default constructors that are required for the build to work.

This conservatively changes all keep rules that currently keep the default constructor to explicitly keep the default constructor.

Future work will attempt to tighten up the rules that do not require the default constructor to be kept.

Bug: 373579455
Test: existing
Change-Id: I46dc4b01c0ee38a7033d8f16419dac00fe414fb1
2025-01-08 00:04:06 -08:00
Android Build Coastguard Worker 7091eb1724 Snap for 12337407 from 2247f9f97d to 25Q2-release
Change-Id: I0ab2079df5ea86fc55904170395b1723eb98a690
2024-09-07 23:19:45 +00:00
Xin Li 2247f9f97d Revert "Remove abandoned implementation." am: 8a3204f485 am: 47b44bbd3e
Original change: https://android-review.googlesource.com/c/platform/packages/apps/Messaging/+/3255819

Change-Id: I92d6c8d6895b4b51d2745558003cbf3b11b03f14
Signed-off-by: Automerger Merge Worker <android-build-automerger-merge-worker@system.gserviceaccount.com>
2024-09-05 00:43:03 +00:00
Xin Li 47b44bbd3e Revert "Remove abandoned implementation." am: 8a3204f485
Original change: https://android-review.googlesource.com/c/platform/packages/apps/Messaging/+/3255819

Change-Id: Ic1f03ccb7320d2a21e3d9fea55d56528a72bd8d7
Signed-off-by: Automerger Merge Worker <android-build-automerger-merge-worker@system.gserviceaccount.com>
2024-09-05 00:33:14 +00:00
Xin Li 8a3204f485 Revert "Remove abandoned implementation."
Revert submission 3242160-remove-messaging-package

Reason for revert: Droidmonitor created revert due to b/364617310, b/364626927, b/364617560

Reverted changes: /q/submissionid:3241481-remove-messaging-package

This reverts commit 93b5e63726.

Change-Id: I182cbf1758988d9a4950d39354266a5fb3aaabce
2024-09-04 16:17:30 -07:00
Elliott Hughes f962801737 Remove abandoned implementation. am: 93b5e63726 am: 364eb68f5f
Original change: https://android-review.googlesource.com/c/platform/packages/apps/Messaging/+/3242160

Change-Id: Ic02a0af401b5316611b81305b8cbca3a9e70815d
Signed-off-by: Automerger Merge Worker <android-build-automerger-merge-worker@system.gserviceaccount.com>
2024-09-04 14:22:38 +00:00
Elliott Hughes 364eb68f5f Remove abandoned implementation. am: 93b5e63726
Original change: https://android-review.googlesource.com/c/platform/packages/apps/Messaging/+/3242160

Change-Id: I491f5ce9a4a93f7295d7b1f1a46ac37204a6dced
Signed-off-by: Automerger Merge Worker <android-build-automerger-merge-worker@system.gserviceaccount.com>
2024-09-04 14:13:34 +00:00
Elliott Hughes 93b5e63726 Remove abandoned implementation.
To prevent accidental ressurection via manifest errors.

Change-Id: I269dc248c2cb5e4bfdcb38334962070f1aba339e
2024-08-26 14:55:13 +00:00
Xin Li 846cede0c2 [automerger skipped] Mark ab/11976889 as merged in aosp-main-future am: 842c0b5dd2 -s ours
am skip reason: Merged-In Iee4b892eba5739f01f0d4632cf8aed23372270af with SHA-1 55830d1d7a is already in history

Original change: https://googleplex-android-review.googlesource.com/c/platform/packages/apps/Messaging/+/27904098

Change-Id: Ic5a88c5149d7946245566d3f664f8f766aa573bc
Signed-off-by: Automerger Merge Worker <android-build-automerger-merge-worker@system.gserviceaccount.com>
2024-06-18 20:43:47 +00:00
Xin Li 842c0b5dd2 Mark ab/11976889 as merged in aosp-main-future
Bug: 347831320
Merged-In: Iee4b892eba5739f01f0d4632cf8aed23372270af
Change-Id: I43dbcc25e5e56c45105e1d62d7b39f317016c177
2024-06-18 11:43:46 -07:00
Xin Li 5bfddff502 [automerger skipped] Merge Android 14 QPR3 to AOSP main am: 98492677cc -s ours am: 55830d1d7a -s ours
am skip reason: Merged-In I7d4c5e5a30eb6b63834adfaee83d44c8836c66f4 with SHA-1 66fe5627e6 is already in history

Original change: https://android-review.googlesource.com/c/platform/packages/apps/Messaging/+/3132474

Change-Id: I14d2774d9e3e8f54b6d035f8cab50df33b1a45fd
Signed-off-by: Automerger Merge Worker <android-build-automerger-merge-worker@system.gserviceaccount.com>
2024-06-18 05:55:41 +00:00
Xin Li 55830d1d7a [automerger skipped] Merge Android 14 QPR3 to AOSP main am: 98492677cc -s ours
am skip reason: Merged-In I7d4c5e5a30eb6b63834adfaee83d44c8836c66f4 with SHA-1 66fe5627e6 is already in history

Original change: https://android-review.googlesource.com/c/platform/packages/apps/Messaging/+/3132474

Change-Id: Iee4b892eba5739f01f0d4632cf8aed23372270af
Signed-off-by: Automerger Merge Worker <android-build-automerger-merge-worker@system.gserviceaccount.com>
2024-06-17 20:41:22 +00:00
Xin Li 98492677cc Merge Android 14 QPR3 to AOSP main
Bug: 346855327
Merged-In: I7d4c5e5a30eb6b63834adfaee83d44c8836c66f4
Change-Id: I3e38f8739ba7ebb731e3366232a03d06c479c594
2024-06-13 10:50:26 -07:00
1726 changed files with 79573 additions and 46024 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
+7 -3
View File
@@ -33,6 +33,7 @@ messaging_java_defaults {
soong_config_variables: {
build_variant_eng: {
optimize: {
keep_runtime_invisible_annotations: true,
proguard_flags_files: [
"proguard.flags",
"proguard-test.flags",
@@ -40,6 +41,7 @@ messaging_java_defaults {
},
conditions_default: {
optimize: {
keep_runtime_invisible_annotations: true,
proguard_flags_files: [
"proguard.flags",
"proguard-release.flags",
@@ -85,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,
@@ -103,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,
}
-1
View File
@@ -1,4 +1,3 @@
# Default code reviewers picked from top 3 or more developers.
# Please update this list if you find better candidates.
tomtaylor@google.com
rtenneti@google.com
+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>
@@ -1,6 +1,6 @@
/*
* Copyright (C) 2015 The Android Open Source Project
* Copyright (C) 2024 The LineageOS 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.
@@ -18,8 +18,6 @@
package android.support.v7.mms;
import android.content.Context;
import android.content.res.Resources;
import android.content.res.XmlResourceParser;
import android.database.Cursor;
import android.database.sqlite.SQLiteException;
import android.net.Uri;
@@ -28,8 +26,6 @@ import android.text.TextUtils;
import android.util.Log;
import android.util.SparseArray;
import com.android.messaging.R;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.ArrayList;
@@ -112,95 +108,6 @@ class DefaultApnSettingsLoader implements ApnSettingsLoader {
public void setSuccess() {
// Do nothing
}
public boolean equals(final BaseApn other) {
return TextUtils.equals(mMmsc, other.getMmsc()) &&
TextUtils.equals(mMmsProxy, other.getMmsProxy()) &&
mMmsProxyPort == other.getMmsProxyPort();
}
}
/**
* An in-memory implementation of an APN. These APNs are organized into an in-memory list.
* The order of the list can be changed by the setSuccess method.
*/
private static class MemoryApn implements Apn {
/**
* Create an in-memory APN loaded from resources
*
* @param apns the in-memory APN list
* @param typesIn the APN type field
* @param mmscIn the APN mmsc field
* @param proxyIn the APN mmsproxy field
* @param portIn the APN mmsport field
* @return an in-memory APN instance, null if there is invalid parameter
*/
public static MemoryApn from(final List<Apn> apns, final String typesIn,
final String mmscIn, final String proxyIn, final String portIn) {
if (apns == null) {
return null;
}
final BaseApn base = BaseApn.from(typesIn, mmscIn, proxyIn, portIn);
if (base == null) {
return null;
}
for (final Apn apn : apns) {
if (apn instanceof MemoryApn && ((MemoryApn) apn).equals(base)) {
return null;
}
}
return new MemoryApn(apns, base);
}
private final List<Apn> mApns;
private final BaseApn mBase;
public MemoryApn(final List<Apn> apns, final BaseApn base) {
mApns = apns;
mBase = base;
}
@Override
public String getMmsc() {
return mBase.getMmsc();
}
@Override
public String getMmsProxy() {
return mBase.getMmsProxy();
}
@Override
public int getMmsProxyPort() {
return mBase.getMmsProxyPort();
}
@Override
public void setSuccess() {
// If this is being marked as a successful APN, move it to the top of the list so
// next time it will be tried first
boolean moved = false;
synchronized (mApns) {
if (mApns.get(0) != this) {
mApns.remove(this);
mApns.add(0, this);
moved = true;
}
}
if (moved) {
Log.d(MmsService.TAG, "Set APN ["
+ "MMSC=" + getMmsc() + ", "
+ "PROXY=" + getMmsProxy() + ", "
+ "PORT=" + getMmsProxyPort() + "] to be first");
}
}
public boolean equals(final BaseApn other) {
if (other == null) {
return false;
}
return mBase.equals(other);
}
}
/**
@@ -222,14 +129,6 @@ class DefaultApnSettingsLoader implements ApnSettingsLoader {
private static final int COLUMN_MMSPROXY = 2;
private static final int COLUMN_MMSPORT = 3;
private static final String APN_MCC = "mcc";
private static final String APN_MNC = "mnc";
private static final String APN_APN = "apn";
private static final String APN_TYPE = "type";
private static final String APN_MMSC = "mmsc";
private static final String APN_MMSPROXY = "mmsproxy";
private static final String APN_MMSPORT = "mmsport";
private final Context mContext;
// Cached APNs for subIds
@@ -263,16 +162,6 @@ class DefaultApnSettingsLoader implements ApnSettingsLoader {
private void loadLocked(final int subId, final String apnName, final List<Apn> apns) {
// Try system APN table first
loadFromSystem(subId, apnName, apns);
if (apns.size() > 0) {
return;
}
// Try loading from apns.xml in resources
loadFromResources(subId, apnName, apns);
if (apns.size() > 0) {
return;
}
// Try resources but without APN name
loadFromResources(subId, null/*apnName*/, apns);
}
/**
@@ -384,48 +273,6 @@ class DefaultApnSettingsLoader implements ApnSettingsLoader {
return null;
}
/**
* Find matching APNs using builtin APN list resource
*
* @param subId the SIM subId
* @param apnName the APN name to match
* @param apns the list for returning results
*/
private void loadFromResources(final int subId, final String apnName, final List<Apn> apns) {
Log.i(MmsService.TAG, "Loading APNs from resources, apnName=" + apnName);
final int[] mccMnc = Utils.getMccMnc(mContext, subId);
if (mccMnc[0] == 0) {
Log.w(MmsService.TAG, "Can not get valid mcc/mnc from system");
return;
}
// MCC/MNC is good, loading/querying APNs from XML
try (XmlResourceParser xml = mContext.getResources().getXml(R.xml.apns)) {
new ApnsXmlParser(xml, apnValues -> {
final String mcc = trimWithNullCheck(apnValues.getAsString(APN_MCC));
final String mnc = trimWithNullCheck(apnValues.getAsString(APN_MNC));
final String apn = trimWithNullCheck(apnValues.getAsString(APN_APN));
try {
if (mccMnc[0] == Integer.parseInt(mcc) &&
mccMnc[1] == Integer.parseInt(mnc) &&
(TextUtils.isEmpty(apnName) || apnName.equalsIgnoreCase(apn))) {
final String type = apnValues.getAsString(APN_TYPE);
final String mmsc = apnValues.getAsString(APN_MMSC);
final String mmsproxy = apnValues.getAsString(APN_MMSPROXY);
final String mmsport = apnValues.getAsString(APN_MMSPORT);
final Apn newApn = MemoryApn.from(apns, type, mmsc, mmsproxy, mmsport);
if (newApn != null) {
apns.add(newApn);
}
}
} catch (final NumberFormatException e) {
// Ignore
}
}).parse();
} catch (final Resources.NotFoundException e) {
Log.w(MmsService.TAG, "Can not get apns.xml " + e);
}
}
private static String trimWithNullCheck(final String value) {
return value != null ? value.trim() : null;
}
@@ -1,6 +1,6 @@
/*
* Copyright (C) 2015 The Android Open Source Project
* Copyright (C) 2024 The LineageOS 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.
@@ -24,7 +24,6 @@ import android.content.Intent;
import android.content.IntentFilter;
import android.content.res.Configuration;
import android.os.Handler;
import android.os.Looper;
import android.support.v7.mms.CarrierConfigValuesLoader;
import android.support.v7.mms.MmsManager;
import android.telephony.CarrierConfigManager;
@@ -33,18 +32,17 @@ import androidx.annotation.NonNull;
import com.android.messaging.datamodel.DataModel;
import com.android.messaging.receiver.SmsReceiver;
import com.android.messaging.sms.ApnDatabase;
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.DebugUtils;
import com.android.messaging.util.LogUtil;
import com.android.messaging.util.PhoneUtils;
import com.android.messaging.util.Trace;
import java.io.File;
import java.lang.Thread.UncaughtExceptionHandler;
/**
@@ -61,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);
@@ -84,14 +84,10 @@ public class BugleApplication extends Application implements UncaughtExceptionHa
final CarrierConfigValuesLoader carrierConfigValuesLoader =
factory.getCarrierConfigValuesLoader();
maybeStartProfiling();
BugleApplication.updateAppConfig(context);
// Initialize MMS lib
initMmsLib(context, carrierConfigValuesLoader);
// Initialize APN database
ApnDatabase.initializeAppContext(context);
// Fixup messages in flight if we crashed and send any pending
dataModel.onApplicationCreated();
// Register carrier config change receiver
@@ -132,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();
@@ -139,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
@@ -155,27 +170,6 @@ public class BugleApplication extends Application implements UncaughtExceptionHa
}
}
private void maybeStartProfiling() {
// App startup profiling support. To use it:
// adb shell setprop log.tag.BugleProfile DEBUG
// # Start the app, wait for a 30s, download trace file:
// adb pull /data/data/com.android.messaging/cache/startup.trace /tmp
// # Open trace file (using adt/tools/traceview)
if (android.util.Log.isLoggable(LogUtil.PROFILE_TAG, android.util.Log.DEBUG)) {
// Start method tracing with a big enough buffer and let it run for 30s.
// Note we use a logging tag as we don't want to wait for gservices to start up.
final File file = DebugUtils.getDebugFile("startup.trace", true);
android.os.Debug.startMethodTracing(file.getAbsolutePath(), 160 * 1024 * 1024);
new Handler(Looper.getMainLooper()).postDelayed(() -> {
android.os.Debug.stopMethodTracing();
// Allow world to see trace file
DebugUtils.ensureReadable(file);
LogUtil.d(LogUtil.PROFILE_TAG, "Tracing complete - "
+ file.getAbsolutePath());
}, 30000);
}
}
private void maybeHandleSharedPrefsUpgrade(final Factory factory) {
final int existingVersion = factory.getApplicationPrefs().getInt(
BuglePrefsKeys.SHARED_PREFERENCES_VERSION,
@@ -66,6 +66,4 @@ public abstract class Factory {
public abstract BugleCarrierConfigValuesLoader getCarrierConfigValuesLoader();
// Note this needs to run from any thread
public abstract void reclaimMemory();
public abstract void onActivityResume();
}
@@ -199,10 +199,6 @@ class FactoryImpl extends Factory {
mMemoryCacheManager.reclaimMemory();
}
@Override
public void onActivityResume() {
}
@Override
public MediaUtil getMediaUtil() {
return mMediaUtil;
@@ -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;
@@ -234,14 +236,6 @@ public abstract class MessageNotificationState extends NotificationState {
mParticipantCount = participantCount;
}
public int getLatestMessageNotificationType() {
final MessageLineInfo messageLineInfo = getLatestMessageLineInfo();
if (messageLineInfo == null) {
return BugleNotifications.LOCAL_SMS_NOTIFICATION;
}
return messageLineInfo.mNotificationType;
}
public String getLatestMessageId() {
final MessageLineInfo messageLineInfo = getLatestMessageLineInfo();
if (messageLineInfo == null) {
@@ -362,7 +356,6 @@ public abstract class MessageNotificationState extends NotificationState {
if (!(convInfo.mLineInfos.get(0) instanceof MessageLineInfo)) {
continue;
}
setPeopleForConversation(convInfo.mConversationId);
final ConversationInfoList list = new ConversationInfoList(
convInfo.mTotalMessageCount, Lists.newArrayList(convInfo));
mChildren.add(new BundledMessageNotificationState(list, i));
@@ -452,8 +445,6 @@ public abstract class MessageNotificationState extends NotificationState {
super(convList);
// This conversation has been accepted.
final ConversationLineInfo convInfo = convList.mConvInfos.get(0);
setAvatarUrlsForConversation(convInfo.mConversationId);
setPeopleForConversation(convInfo.mConversationId);
final Context context = Factory.get().getApplicationContext();
MessageLineInfo messageInfo = (MessageLineInfo) convInfo.mLineInfos.get(0);
@@ -476,7 +467,7 @@ public abstract class MessageNotificationState extends NotificationState {
final String attachment = context.getString(message);
final SpannableStringBuilder spanBuilder = new SpannableStringBuilder();
if (!TextUtils.isEmpty(mContent)) {
spanBuilder.append(mContent).append(System.getProperty("line.separator"));
spanBuilder.append(mContent).append(System.lineSeparator());
}
final int start = spanBuilder.length();
spanBuilder.append(attachment);
@@ -505,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;
}
}
@@ -835,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;
@@ -851,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();
@@ -917,7 +891,7 @@ public abstract class MessageNotificationState extends NotificationState {
Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
if (!TextUtils.isEmpty(text)) {
// Now add the actual message text below the subject header.
spanBuilder.append(System.getProperty("line.separator") + text);
spanBuilder.append(System.lineSeparator() + text);
}
text = spanBuilder;
}
@@ -1059,18 +1033,6 @@ public abstract class MessageNotificationState extends NotificationState {
return mTitle;
}
@Override
public int getLatestMessageNotificationType() {
// This function is called to determine whether the most recent notification applies
// to an sms conversation or a hangout conversation. We have different ringtone/vibrate
// settings for both types of conversations.
if (mConvList.mConvInfos.size() > 0) {
final ConversationLineInfo convInfo = mConvList.mConvInfos.get(0);
return convInfo.getLatestMessageNotificationType();
}
return BugleNotifications.LOCAL_SMS_NOTIFICATION;
}
protected CharSequence getTicker() {
return BugleNotifications.buildColonSeparatedMessage(
mTickerSender != null ? mTickerSender : mTitle,
@@ -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";
/**
@@ -94,12 +94,6 @@ public abstract class NotificationState {
*/
protected abstract NotificationCompat.Style build(NotificationCompat.Builder builder);
protected void setAvatarUrlsForConversation(final String conversationId) {
}
protected void setPeopleForConversation(final String conversationId) {
}
/**
* Reserves request codes for this notification type. By default 2 codes are reserved, one for
* the main intent and another for the cancel intent. Override this function to reserve more.
@@ -108,10 +102,6 @@ public abstract class NotificationState {
return NUM_REQUEST_CODES_NEEDED;
}
public int getContentIntentRequestCode() {
return mBaseRequestCode + CONTENT_INTENT_REQUEST_CODE_OFFSET;
}
public int getClearIntentRequestCode() {
return mBaseRequestCode + CLEAR_INTENT_REQUEST_CODE_OFFSET;
}
@@ -121,14 +111,6 @@ public abstract class NotificationState {
*/
public abstract int getIcon();
/**
* @return the type of notification that should be used from {@link RealTimeChatNotifications}
* so that the proper ringtone and vibrate settings can be used.
*/
public int getLatestMessageNotificationType() {
return BugleNotifications.LOCAL_SMS_NOTIFICATION;
}
/**
* @return the notification priority level for this notification.
*/
@@ -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.
@@ -203,17 +203,11 @@ public class ActionServiceImpl extends JobIntentService {
mBackgroundWorker = DataModel.get().getBackgroundWorkerForActionService();
}
@Override
public void onDestroy() {
super.onDestroy();
}
/**
* Queue intent to the ActionService.
*/
private static void startServiceWithIntent(final Intent intent) {
final Context context = Factory.get().getApplicationContext();
final int opcode = intent.getIntExtra(EXTRA_OP_CODE, 0);
intent.setClass(context, ActionServiceImpl.class);
enqueueWork(context, intent);
}
@@ -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