From 18b33a7776b00436a2917a2c4da10752dc82b613 Mon Sep 17 00:00:00 2001 From: Alex Hart Date: Mon, 11 Sep 2023 15:27:58 -0300 Subject: [PATCH] Ensure lower api levels do not try to use Uri based IconCompat. --- .../notifications/v2/NotificationBuilder.kt | 6 +- .../notifications/v2/NotificationItem.kt | 4 +- .../service/webrtc/WebRtcCallService.java | 33 ++++++++- .../securesms/util/AvatarUtil.java | 69 +++++++++++-------- .../securesms/util/ConversationUtil.java | 5 +- .../webrtc/CallNotificationBuilder.java | 52 +++++++++++--- 6 files changed, 119 insertions(+), 50 deletions(-) diff --git a/app/src/main/java/org/thoughtcrime/securesms/notifications/v2/NotificationBuilder.kt b/app/src/main/java/org/thoughtcrime/securesms/notifications/v2/NotificationBuilder.kt index 121295d4e5..27e02d1eff 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/notifications/v2/NotificationBuilder.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/notifications/v2/NotificationBuilder.kt @@ -274,7 +274,7 @@ sealed class NotificationBuilder(protected val context: Context) { val self: PersonCompat = PersonCompat.Builder() .setBot(false) .setName(if (includeShortcut) Recipient.self().getDisplayName(context) else context.getString(R.string.SingleRecipientNotificationBuilder_you)) - .setIcon(AvatarUtil.getIconWithUriForNotification(Recipient.self().id)) + .setIcon(AvatarUtil.getIconCompat(context, Recipient.self())) .setKey(ConversationUtil.getShortcutId(Recipient.self().id)) .build() @@ -290,7 +290,7 @@ sealed class NotificationBuilder(protected val context: Context) { .setBot(false) .setName(notificationItem.getPersonName(context)) .setUri(notificationItem.getPersonUri()) - .setIcon(notificationItem.getPersonIcon()) + .setIcon(notificationItem.getPersonIcon(context)) if (includeShortcut) { personBuilder.setKey(ConversationUtil.getShortcutId(notificationItem.authorRecipient)) @@ -360,7 +360,7 @@ sealed class NotificationBuilder(protected val context: Context) { ) if (intent != null) { - val bubbleMetadata = NotificationCompat.BubbleMetadata.Builder(intent, AvatarUtil.getIconCompatForShortcut(context, conversation.recipient)) + val bubbleMetadata = NotificationCompat.BubbleMetadata.Builder(intent, AvatarUtil.getIconCompat(context, conversation.recipient)) .setAutoExpandBubble(bubbleState === BubbleUtil.BubbleState.SHOWN) .setDesiredHeight(600) .setSuppressNotification(bubbleState === BubbleUtil.BubbleState.SHOWN) diff --git a/app/src/main/java/org/thoughtcrime/securesms/notifications/v2/NotificationItem.kt b/app/src/main/java/org/thoughtcrime/securesms/notifications/v2/NotificationItem.kt index df578f6259..412179763a 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/notifications/v2/NotificationItem.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/notifications/v2/NotificationItem.kt @@ -123,9 +123,9 @@ sealed class NotificationItem(val threadRecipient: Recipient, protected val reco } } - fun getPersonIcon(): IconCompat? { + fun getPersonIcon(context: Context): IconCompat? { return if (SignalStore.settings().messageNotificationsPrivacy.isDisplayContact) { - AvatarUtil.getIconWithUriForNotification(authorRecipient.id) + AvatarUtil.getIconCompat(context, authorRecipient) } else { null } diff --git a/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/WebRtcCallService.java b/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/WebRtcCallService.java index e4e0a0ae63..451bc50fc1 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/WebRtcCallService.java +++ b/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/WebRtcCallService.java @@ -19,6 +19,7 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; import org.signal.core.util.ThreadUtil; +import org.signal.core.util.concurrent.SignalExecutors; import org.signal.core.util.logging.Log; import org.thoughtcrime.securesms.dependencies.ApplicationDependencies; import org.thoughtcrime.securesms.jobs.ForegroundServiceUtil; @@ -33,8 +34,14 @@ import org.thoughtcrime.securesms.webrtc.locks.LockManager; import java.util.Objects; import java.util.Set; +import java.util.concurrent.Executor; import java.util.concurrent.TimeUnit; +import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers; +import io.reactivex.rxjava3.core.Single; +import io.reactivex.rxjava3.disposables.Disposable; +import io.reactivex.rxjava3.schedulers.Schedulers; + /** * Provide a foreground service for {@link SignalCallManager} to leverage to run in the background when necessary. Also * provides devices listeners needed for during a call (i.e., bluetooth, power button). @@ -62,6 +69,7 @@ public final class WebRtcCallService extends Service implements SignalAudioManag private static final long FOREGROUND_SERVICE_TIMEOUT = TimeUnit.SECONDS.toMillis(10); private final WebSocketKeepAliveTask webSocketKeepAliveTask = new WebSocketKeepAliveTask(); + private final Executor singleThreadExecutor = SignalExecutors.newCachedSingleThreadExecutor("signal-webrtc-in-call", ThreadUtil.PRIORITY_IMPORTANT_BACKGROUND_THREAD); private SignalCallManager callManager; @@ -72,7 +80,9 @@ public final class WebRtcCallService extends Service implements SignalAudioManag private SignalAudioManager signalAudioManager; private int lastNotificationId; private Notification lastNotification; - private boolean stopping = false; + private long lastNotificationRequestTime; + private Disposable lastNotificationDisposable = Disposable.disposed(); + private boolean stopping = false; public static void update(@NonNull Context context, int type, @NonNull RecipientId recipientId, boolean isVideoCall) { Intent intent = new Intent(context, WebRtcCallService.class); @@ -219,6 +229,7 @@ public final class WebRtcCallService extends Service implements SignalAudioManag private void setCallNotification() { setCallNotification(false); } + private void setCallNotification(boolean stopping) { if (!stopping && lastNotificationId != INVALID_NOTIFICATION_ID) { startForegroundCompat(lastNotificationId, lastNotification); @@ -230,10 +241,28 @@ public final class WebRtcCallService extends Service implements SignalAudioManag } public void setCallInProgressNotification(int type, @NonNull RecipientId id, boolean isVideoCall) { + lastNotificationDisposable.dispose(); + + boolean requiresAsyncNotificationLoad = Build.VERSION.SDK_INT <= 29; + lastNotificationId = CallNotificationBuilder.getNotificationId(type); - lastNotification = CallNotificationBuilder.getCallInProgressNotification(this, type, Recipient.resolved(id), isVideoCall); + lastNotification = CallNotificationBuilder.getCallInProgressNotification(this, type, Recipient.resolved(id), isVideoCall, requiresAsyncNotificationLoad); startForegroundCompat(lastNotificationId, lastNotification); + + if (requiresAsyncNotificationLoad) { + final long requestTime = System.currentTimeMillis(); + lastNotificationRequestTime = requestTime; + lastNotificationDisposable = Single + .fromCallable(() -> CallNotificationBuilder.getCallInProgressNotification(this, type, Recipient.resolved(id), isVideoCall, false)) + .subscribeOn(Schedulers.from(singleThreadExecutor)) + .observeOn(AndroidSchedulers.mainThread()) + .filter(unused -> requestTime == lastNotificationRequestTime && !stopping) + .subscribe(notification -> { + lastNotification = notification; + startForegroundCompat(lastNotificationId, lastNotification); + }); + } } private synchronized void startForegroundCompat(int notificationId, Notification notification) { diff --git a/app/src/main/java/org/thoughtcrime/securesms/util/AvatarUtil.java b/app/src/main/java/org/thoughtcrime/securesms/util/AvatarUtil.java index d21f9719d6..71298fc0ab 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/util/AvatarUtil.java +++ b/app/src/main/java/org/thoughtcrime/securesms/util/AvatarUtil.java @@ -22,7 +22,7 @@ import com.bumptech.glide.request.target.CustomTarget; import com.bumptech.glide.request.target.CustomViewTarget; import com.bumptech.glide.request.transition.Transition; -import org.signal.core.util.concurrent.SignalExecutors; +import org.signal.core.util.ThreadUtil; import org.signal.core.util.logging.Log; import org.thoughtcrime.securesms.R; import org.thoughtcrime.securesms.contacts.avatars.ContactPhoto; @@ -30,10 +30,11 @@ import org.thoughtcrime.securesms.contacts.avatars.GeneratedContactPhoto; import org.thoughtcrime.securesms.contacts.avatars.ProfileContactPhoto; import org.thoughtcrime.securesms.mms.GlideApp; import org.thoughtcrime.securesms.mms.GlideRequest; +import org.thoughtcrime.securesms.mms.GlideRequests; import org.thoughtcrime.securesms.providers.AvatarProvider; import org.thoughtcrime.securesms.recipients.Recipient; -import org.thoughtcrime.securesms.recipients.RecipientId; +import java.util.Objects; import java.util.Optional; import java.util.concurrent.CountDownLatch; import java.util.concurrent.ExecutionException; @@ -91,7 +92,7 @@ public final class AvatarUtil { } public static void loadIconIntoImageView(@NonNull Recipient recipient, @NonNull ImageView target, int requestedSize) { - Context context = target.getContext(); + Context context = target.getContext(); requestCircle(GlideApp.with(context).asDrawable(), context, recipient, requestedSize).into(target); } @@ -103,23 +104,18 @@ public final class AvatarUtil { throws ExecutionException, InterruptedException { return requestSquare(GlideApp.with(context).asBitmap(), context, recipient) - .skipMemoryCache(true) - .diskCacheStrategy(DiskCacheStrategy.NONE) - .submit(width, height) - .get(); + .skipMemoryCache(true) + .diskCacheStrategy(DiskCacheStrategy.NONE) + .submit(width, height) + .get(); } @WorkerThread - public static IconCompat getIconWithUriForNotification(@NonNull RecipientId recipientId) { - return IconCompat.createWithContentUri(AvatarProvider.getContentUri(recipientId)); - } - - @WorkerThread - public static @NonNull IconCompat getIconCompatForShortcut(@NonNull Context context, @NonNull Recipient recipient) { + public static @NonNull IconCompat getIconCompat(@NonNull Context context, @NonNull Recipient recipient) { if (Build.VERSION.SDK_INT > 29) { - return getIconWithUriForNotification(recipient.getId()); + return IconCompat.createWithContentUri(AvatarProvider.getContentUri(recipient.getId())); } else { - return IconCompat.createWithBitmap(getBitmapForNotification(context, recipient)); + return IconCompat.createWithBitmap(getBitmapForNotification(context, recipient, DrawableUtil.SHORTCUT_INFO_WRAPPED_SIZE)); } } @@ -129,17 +125,19 @@ public final class AvatarUtil { } @WorkerThread - public static Bitmap getBitmapForNotification(@NonNull Context context, @NonNull Recipient recipient, int size) { + public static @NonNull Bitmap getBitmapForNotification(@NonNull Context context, @NonNull Recipient recipient, int size) { + ThreadUtil.assertNotMainThread(); + try { - AvatarTarget avatarTarget = new AvatarTarget(size); + AvatarTarget avatarTarget = new AvatarTarget(size); + GlideRequests glideRequests = GlideApp.with(context); - SignalExecutors.BOUNDED_IO.submit(() -> { - requestCircle(GlideApp.with(context).asBitmap(), context, recipient, size).into(avatarTarget); - }); + requestCircle(glideRequests.asBitmap(), context, recipient, size).into(avatarTarget); - return avatarTarget.await(); + Bitmap bitmap = avatarTarget.await(); + return Objects.requireNonNullElseGet(bitmap, () -> DrawableUtil.toBitmap(getFallback(context, recipient, size), size, size)); } catch (InterruptedException e) { - return null; + return DrawableUtil.toBitmap(getFallback(context, recipient, size), size, size); } } @@ -156,6 +154,7 @@ public final class AvatarUtil { } private static GlideRequest request(@NonNull GlideRequest glideRequest, @NonNull Context context, @NonNull Recipient recipient, boolean loadSelf, int targetSize, @Nullable BitmapTransformation transformation) { + final ContactPhoto photo; if (Recipient.self().equals(recipient) && loadSelf) { photo = new ProfileContactPhoto(recipient); @@ -163,10 +162,11 @@ public final class AvatarUtil { photo = recipient.getContactPhoto(); } + final int size = targetSize == -1 ? DrawableUtil.SHORTCUT_INFO_WRAPPED_SIZE : targetSize; final GlideRequest request = glideRequest.load(photo) - .error(getFallback(context, recipient, targetSize)) + .error(getFallback(context, recipient, size)) .diskCacheStrategy(DiskCacheStrategy.ALL) - .override(targetSize); + .override(size); if (recipient.shouldBlurAvatar()) { BlurTransformation blur = new BlurTransformation(context, 0.25f, BlurTransformation.MAX_RADIUS); @@ -203,7 +203,6 @@ public final class AvatarUtil { } public @Nullable Bitmap await() throws InterruptedException { - Log.d(TAG, "AvatarTarget#await:"); if (countDownLatch.await(1, TimeUnit.SECONDS)) { return bitmap.get(); } else { @@ -212,17 +211,28 @@ public final class AvatarUtil { } } + @Override + public void onDestroy() { + Log.d(TAG, "AvatarTarget: onDestroy"); + super.onDestroy(); + } + + @Override + public void onLoadStarted(@Nullable Drawable placeholder) { + Log.d(TAG, "AvatarTarget: onLoadStarted"); + super.onLoadStarted(placeholder); + } + @Override public void onResourceReady(@NonNull Bitmap resource, @Nullable Transition transition) { - Log.d(TAG, "AvatarTarget#onResourceReady: " + resource.getWidth() + ", " + resource.getHeight() + ", s:" + size); + Log.d(TAG, "AvatarTarget: onResourceReady"); bitmap.set(resource); countDownLatch.countDown(); } @Override public void onLoadFailed(@Nullable Drawable errorDrawable) { - Log.d(TAG, "AvatarTarget#onLoadFailed:"); - + Log.d(TAG, "AvatarTarget: onLoadFailed"); if (errorDrawable == null) { throw new AssertionError("Expected an error drawable."); } @@ -234,8 +244,7 @@ public final class AvatarUtil { @Override public void onLoadCleared(@Nullable Drawable placeholder) { - Log.d(TAG, "AvatarTarget#onLoadCleared:"); - + Log.d(TAG, "AvatarTarget: onLoadCleared"); bitmap.set(null); countDownLatch.countDown(); } diff --git a/app/src/main/java/org/thoughtcrime/securesms/util/ConversationUtil.java b/app/src/main/java/org/thoughtcrime/securesms/util/ConversationUtil.java index bd20b8944a..ed45d85d99 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/util/ConversationUtil.java +++ b/app/src/main/java/org/thoughtcrime/securesms/util/ConversationUtil.java @@ -226,7 +226,7 @@ public final class ConversationUtil { .setIntent(ConversationIntents.createBuilderSync(context, resolved.getId(), threadId).build()) .setShortLabel(shortName) .setLongLabel(longName) - .setIcon(AvatarUtil.getIconCompatForShortcut(context, resolved)) + .setIcon(AvatarUtil.getIconCompat(context, resolved)) .setPersons(persons) .setCategories(Sets.newHashSet(CATEGORY_SHARE_TARGET)) .setActivity(activity) @@ -286,11 +286,12 @@ public final class ConversationUtil { /** * @return A Compat Library Person object representing the given Recipient */ + @WorkerThread public static @NonNull Person buildPerson(@NonNull Context context, @NonNull Recipient recipient) { return new Person.Builder() .setKey(getShortcutId(recipient.getId())) .setName(recipient.getDisplayName(context)) - .setIcon(AvatarUtil.getIconWithUriForNotification(recipient.getId())) + .setIcon(AvatarUtil.getIconCompat(context, recipient)) .setUri(recipient.isSystemContact() ? recipient.getContactUri().toString() : null) .build(); } diff --git a/app/src/main/java/org/thoughtcrime/securesms/webrtc/CallNotificationBuilder.java b/app/src/main/java/org/thoughtcrime/securesms/webrtc/CallNotificationBuilder.java index 415b9173eb..e60417eedc 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/webrtc/CallNotificationBuilder.java +++ b/app/src/main/java/org/thoughtcrime/securesms/webrtc/CallNotificationBuilder.java @@ -6,6 +6,7 @@ import android.content.Context; import android.content.Intent; import android.os.Build; +import androidx.annotation.IntDef; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.core.app.NotificationCompat; @@ -36,13 +37,22 @@ public class CallNotificationBuilder { public static final int TYPE_ESTABLISHED = 3; public static final int TYPE_INCOMING_CONNECTING = 4; + @IntDef(value = { + TYPE_INCOMING_RINGING, + TYPE_OUTGOING_RINGING, + TYPE_ESTABLISHED, + TYPE_INCOMING_CONNECTING + }) + public @interface CallNotificationType { + } + private enum LaunchCallScreenIntentState { CONTENT(null, 0), AUDIO(WebRtcCallActivity.ANSWER_ACTION, 1), VIDEO(WebRtcCallActivity.ANSWER_VIDEO_ACTION, 2); final @Nullable String action; - final int requestCode; + final int requestCode; LaunchCallScreenIntentState(@Nullable String action, int requestCode) { this.action = action; @@ -61,9 +71,25 @@ public class CallNotificationBuilder { */ public static final int API_LEVEL_CALL_STYLE = 29; - public static Notification getCallInProgressNotification(Context context, int type, Recipient recipient, boolean isVideoCall) { - PendingIntent pendingIntent = getActivityPendingIntent(context, LaunchCallScreenIntentState.CONTENT); - NotificationCompat.Builder builder = new NotificationCompat.Builder(context, getNotificationChannel(type)) + /** + * Gets the Notification for the current in-progress call. + * + * @param context Context, normally the service requesting this notification + * @param type The type of notification desired + * @param recipient The target of the call (group, call link, or 1:1 recipient) + * @param isVideoCall Whether the call is a video call + * @param skipPersonIcon Whether to skip loading the icon for a person, used to avoid blocking the UI thread on older apis. + */ + public static Notification getCallInProgressNotification( + Context context, + @CallNotificationType int type, + Recipient recipient, + boolean isVideoCall, + boolean skipPersonIcon + ) + { + PendingIntent pendingIntent = getActivityPendingIntent(context, LaunchCallScreenIntentState.CONTENT); + NotificationCompat.Builder builder = new NotificationCompat.Builder(context, getNotificationChannel(type)) .setSmallIcon(R.drawable.ic_call_secure_white_24dp) .setContentIntent(pendingIntent) .setOngoing(true) @@ -80,7 +106,9 @@ public class CallNotificationBuilder { builder.setCategory(NotificationCompat.CATEGORY_CALL); builder.setFullScreenIntent(pendingIntent, true); - Person person = ConversationUtil.buildPerson(context, recipient); + Person person = skipPersonIcon ? ConversationUtil.buildPersonWithoutIcon(context, recipient) + : ConversationUtil.buildPerson(context.getApplicationContext(), recipient); + builder.addPerson(person); if (deviceVersionSupportsIncomingCallStyle()) { @@ -102,7 +130,9 @@ public class CallNotificationBuilder { builder.setPriority(NotificationCompat.PRIORITY_DEFAULT); builder.setCategory(NotificationCompat.CATEGORY_CALL); - Person person = ConversationUtil.buildPerson(context, recipient); + Person person = skipPersonIcon ? ConversationUtil.buildPersonWithoutIcon(context, recipient) + : ConversationUtil.buildPerson(context.getApplicationContext(), recipient); + builder.addPerson(person); if (deviceVersionSupportsIncomingCallStyle()) { @@ -126,11 +156,11 @@ public class CallNotificationBuilder { public static @NonNull Notification getStartingNotification(@NonNull Context context) { return new NotificationCompat.Builder(context, NotificationChannels.getInstance().CALL_STATUS) - .setSmallIcon(R.drawable.ic_call_secure_white_24dp) - .setOngoing(true) - .setContentTitle(context.getString(R.string.NotificationBarManager__starting_signal_call_service)) - .setPriority(NotificationCompat.PRIORITY_MIN) - .build(); + .setSmallIcon(R.drawable.ic_call_secure_white_24dp) + .setOngoing(true) + .setContentTitle(context.getString(R.string.NotificationBarManager__starting_signal_call_service)) + .setPriority(NotificationCompat.PRIORITY_MIN) + .build(); } public static @NonNull Notification getStoppingNotification(@NonNull Context context) {