Fix pre release bugs - #281
Conversation
8ff691f to
ecf9026
Compare
b175fa4 to
954bd42
Compare
| builder.setContentText(line2); | ||
| } | ||
|
|
||
| if (builder != null) { |
There was a problem hiding this comment.
builder is created a few lines up and cannot be null. The block is rewritten anyway, drop the check?
|
|
||
| private fun onPhoneNumberConfirmed(phoneNumber: String) { | ||
| val isValid = phoneNumber.isEmpty() || | ||
| PhoneUtils.get(subId.value).getValidSelfE164Number(phoneNumber) != null |
There was a problem hiding this comment.
Could this be an injected use case?
| modifier: Modifier = Modifier, | ||
| ) { | ||
| val scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior() | ||
| var showGroupMmsDialog by remember { mutableStateOf(false) } |
There was a problem hiding this comment.
showGroupMmsDialog is still local, so one dialog survives rotation and the other does not. Pick one?
| return@LaunchedEffect | ||
| } | ||
|
|
||
| BuglePrefs.getApplicationPrefs().putBoolean( |
There was a problem hiding this comment.
BuglePrefs and ParticipantRefresh straight from the composable, elsewhere this goes through a use case. Not blocking.
| _phoneNumberDialogState.update { it.copy(isInvalid = false) } | ||
| } | ||
|
|
||
| is Action.PhoneNumberChanged -> { |
There was a problem hiding this comment.
PhoneNumberConfirmed would match what this now does?
| if (isInvalid) { | ||
| onNumberEdited() | ||
| } |
There was a problem hiding this comment.
This only fires while the error is showing, so the name promises more than it does. onErrorDismissed?
| final PhoneNumberUtil phoneNumberUtil = PhoneNumberUtil.getInstance(); | ||
| try { | ||
| final PhoneNumber phoneNumber = phoneNumberUtil.parse(phoneText, country); | ||
| if (phoneNumberUtil.isPossibleNumber(phoneNumber)) { |
There was a problem hiding this comment.
isPossibleNumber accepts a number lifted out of text: Call me at 5551234567 becomes +3725551234567 on an EE SIM and +15551234567 on a US one, and that value goes out as the MMS sender. Reject input with letters? Worth a test either way.
954bd42 to
44390ab
Compare
| val durationsByContentUri = coroutineScope { | ||
| audioContentUris | ||
| .map { contentUri -> | ||
| async { contentUri to resolveAudioDurationMillis(contentUri) } | ||
| } | ||
| .awaitAll() | ||
| .toMap() | ||
| } |
There was a problem hiding this comment.
Could it be a good idea to batch these resolves? Is there some parallelism cap somewhere I missed? Or is this a less expensive operation than it seems?
| message.setData(mAttachmentType, | ||
| SharedMemoryImageProvider.Companion.buildUri(mAttachmentUri, mAttachmentType)); | ||
| message.setData(mAttachmentType, mAttachmentUri); |
There was a problem hiding this comment.
The deleted provider decoded the image and recompressed it as PNG before exposing it. That stripped the original container metadata. The replacement puts the original MMS part URI into MessagingStyle. NotificationRecord collects that URI, and NotificationManagerService grants it to each visible NotificationListenerService before invoking notifyPosted().
An enabled notification listener can now read the original attachment, including EXIF/location or other container data that is not needed to render the notification.
There was a problem hiding this comment.
Fixed. BugleNotifications.getNotificationImageUri() decodes the attachment, bakes the EXIF orientation into it and writes a JPEG into the media scratch space.
| // Send per-conversation notifications (if there are multiple conversations). The list | ||
| // holds every conversation with unseen messages, so it has to be iterated: picking one | ||
| // entry notifies whichever conversation holds the newest unseen message and silently | ||
| // drops the rest, and there is no summary notification to surface them. | ||
| state.mConversationsList.mConversations.stream() | ||
| .filter(conv -> !isConversationBlocked(conv.mConversationId)) | ||
| .findFirst(); | ||
| conversation.ifPresent(conv -> processAndSend(state, conv)); | ||
| .filter(conv -> !ConversationSnoozeQuery.isConversationSnoozed( | ||
| conv.mConversationId)) | ||
| .forEach(conv -> processAndSend(state, conv)); |
There was a problem hiding this comment.
Note that NotificationManagerService enforces maximum of 50 notifications (https://github.com/GrapheneOS/platform_frameworks_base/blob/17/services/core/java/com/android/server/notification/NotificationManagerService.java#L493), maybe the remainder can be in an overflow/summary notification when at >= 51
| /** | ||
| * Purges the rows orphaned while the declared foreign keys went unenforced, parent before | ||
| * child, so foreign_key_check comes back empty before onConfigure() turns enforcement on. The | ||
| * view change needs no statement, the views are rebuilt unconditionally above - but the handler | ||
| * must still exist, or the version check throws and every table is wiped. |
There was a problem hiding this comment.
foreign_key_check comes back empty before onConfigure() turns enforcement on
SQLiteOpenHelper calls onConfigure() before it begins the upgrade transaction and invokes onUpgrade(), so enforcement is already enabled here
The cleanup is needed because enabling foreign keys does not retroactively repair existing violations. This comment could be rewritten to preserve that actual invariant; the current lifecycle description is reversed
The only snooze check guarded the conversation id passed by the caller, so it was bypassed entirely whenever that id was null -- which is what the boot, app-update and redownload paths pass. Every reboot re-notified conversations snoozed "Always". A snoozed conversation could also consume the single notification slot and silence the conversation that had actually received a message. Filter snooze per conversation in the selection stream, where the blocked check already lives, instead of relying on the caller's argument. The guard in update() stays for its cancel() side effect.
setIdentifier(currentTimeMillis()) made every clear intent unique, so the platform minted a fresh PendingIntent on each notification update instead of reusing one per conversation. Uniqueness has to come from the intent itself: PendingIntent identity is (requestCode, filterEquals), the request code is constant across conversations, and filterEquals ignores extras -- so the conversation id set, which lives only in an extra, cannot distinguish them. Set the data URI to the conversation's metadata URI instead. Dismissing one conversation's notification then marks only that conversation as seen.
The conversations list holds every conversation with unseen incoming messages, ordered newest first. Taking findFirst() from it notified whichever conversation happened to hold the globally-newest unseen message and silently dropped the rest, with no summary notification to surface them -- so a reboot with three unread threads announced exactly one. It also ignored the conversation id the update was triggered for, so an arriving message could be passed over in favour of an unrelated newer thread. Iterate the list instead. The early returns in processAndSend already diff against the active notification, so untouched conversations do not re-alert.
44390ab to
85347e8
Compare
| final Uri scratchUri = MediaScratchFileProvider.buildMediaScratchSpaceUri( | ||
| partId, | ||
| NOTIFICATION_IMAGE_EXTENSION | ||
| ); |
There was a problem hiding this comment.
This creates a successful scratch space file without a cleanup path. Most of the existing scratch users clean up when ownership ends:
- GIF transcoding deletes its output immediately after reading it
- Failed/discarded camera, video, and audio captures are deleted
- Rejected or removed draft attachments use deleteTemporaryAttachment()
- Scratch attachments used for MMS retries are destroyed once permanent telephony parts replace them
Notification images don't have an equivalent terminal lifecycle and will accumulate until Android evicts the cache. Could these use a dedicated notification image cache with e.g. bounded age/active-notification-based pruning (e.g. via NotificationManagerCompat.from(context).getActiveNotifications())? Cleanup should probably be rate-limited like run when creating a notification image only if the last sweep is old enough or the cache exceeds a size threshold
Notification images keyed by part ID should also be invalidated when DatabaseHelper.rebuildTables() runs, since old part IDs would no longer be valid. rebuildTables is after an upgrade handler fails or when opening the database with an older schema version (so rare fallback paths and not a normal startup or a successful upgrade)
There was a problem hiding this comment.
Also, while the MMS remains in the telephony database, the cached JPEG is mostly a duplicate, although it has had EXIF removed. But after the message or conversation is deleted, the derived JPEG can remain, retaining the visible image beyond the expected message lifetime.
Internal cache files are app-private and in credential-encrypted storage in Android 17, so unrelated apps cannot directly read them.
Android may evict them, but its documentation explicitly says applications should not rely on automatic cache cleanup. Android app-specific storage guidance (https://developer.android.com/training/data-storage/app-specific#internal-remove-cache)
Even though Android sometimes deletes cache files on its own, you shouldn't rely on the system to clean up these files for you. You should always maintain your app's cache files within internal storage.
There was a problem hiding this comment.
General issue on cache management for scratch space URIs: #284
There was a problem hiding this comment.
Done. Notification images now live in their own provider and cache directory, swept after every notification pass against getActiveNotifications().
85347e8 to
68c7ec8
Compare
68c7ec8 to
f2e5935
Compare
No description provided.