Skip to content

Commit

Permalink
Display MayHaveMessagesNotification on push with locked db (#238)
Browse files Browse the repository at this point in the history
  • Loading branch information
valldrac authored Nov 16, 2023
2 parents 8ec49f2 + 7fba50a commit 89ba25a
Show file tree
Hide file tree
Showing 18 changed files with 119 additions and 70 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
import androidx.annotation.WorkerThread;
import androidx.core.content.ContextCompat;
import androidx.multidex.MultiDexApplication;

import com.google.android.gms.security.ProviderInstaller;
Expand Down Expand Up @@ -84,6 +85,7 @@
import org.thoughtcrime.securesms.mms.SignalGlideComponents;
import org.thoughtcrime.securesms.mms.SignalGlideModule;
import org.thoughtcrime.securesms.net.NetworkManager;
import org.thoughtcrime.securesms.notifications.MessageNotifier;
import org.thoughtcrime.securesms.providers.BlobProvider;
import org.thoughtcrime.securesms.ratelimit.RateLimitUtil;
import org.thoughtcrime.securesms.recipients.Recipient;
Expand Down Expand Up @@ -310,7 +312,7 @@ public void onUnlock() {
}

@MainThread
public void onLock() {
public void onLock(boolean keyExpired) {
Log.i(TAG, "onLock()");

stopService(new Intent(this, WebRtcCallService.class));
Expand All @@ -319,6 +321,16 @@ public void onLock() {
finalizeMessageRetrieval();
unregisterKeyEventReceiver();

MessageNotifier messageNotifier = ApplicationDependencies.getMessageNotifier();
messageNotifier.cancelDelayedNotifications();
boolean hadActiveNotifications = messageNotifier.clearNotifications(this);

if (hadActiveNotifications && keyExpired && SignalStore.account().isPushAvailable() &&
TextSecurePreferences.isPassphraseLockNotificationsEnabled(this) ) {
Log.d(TAG, "Replacing active notifications with may-have-messages notification");
FcmFetchManager.postMayHaveMessagesNotification(this);
}

ThreadUtil.runOnMainDelayed(() -> {
ApplicationDependencies.getJobManager().shutdown(TimeUnit.SECONDS.toMillis(10));
KeyCachingService.clearMasterSecret();
Expand Down Expand Up @@ -628,15 +640,15 @@ public void e(@NonNull String tag, @NonNull String message, @Nullable Throwable
private final BroadcastReceiver keyEventReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
onLock();
boolean keyExpired = intent.getBooleanExtra(KeyCachingService.EXTRA_KEY_EXPIRED, false);
onLock(keyExpired);
}
};

private void registerKeyEventReceiver() {
IntentFilter filter = new IntentFilter();
filter.addAction(KeyCachingService.CLEAR_KEY_EVENT);

registerReceiver(keyEventReceiver, filter, KeyCachingService.KEY_PERMISSION, null);
ContextCompat.registerReceiver(this, keyEventReceiver, filter, KeyCachingService.KEY_PERMISSION, null, ContextCompat.RECEIVER_NOT_EXPORTED);
}

private void unregisterKeyEventReceiver() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import androidx.annotation.RequiresApi
import androidx.core.content.ContextCompat
import androidx.lifecycle.ViewModelProvider
import androidx.navigation.fragment.findNavController
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import org.signal.core.util.getParcelableExtraCompat
import org.signal.core.util.logging.Log
import org.thoughtcrime.securesms.R
Expand Down Expand Up @@ -276,6 +277,25 @@ class NotificationsSettingsFragment : DSLSettingsFragment(R.string.preferences__

sectionHeaderPref(R.string.NotificationsSettingsFragment__notify_when)

switchPref(
title = DSLSettingsText.from(R.string.NotificationsSettingsFragment__new_activity_while_locked),
summary = DSLSettingsText.from(R.string.NotificationsSettingsFragment__receive_notifications_for_messages_or_missed_calls_when_the_app_is_locked),
isChecked = state.notifyWhileLocked,
onToggle = { isChecked ->
if (isChecked && !state.canEnableNotifyWhileLocked) {
MaterialAlertDialogBuilder(requireContext())
.setTitle(R.string.NotificationsSettingsFragment__push_notifications_unavailable)
.setMessage(R.string.NotificationsSettingsFragment__sorry_this_feature_requires_push_notifications_delivered_via_fcm_or_unifiedpush)
.setPositiveButton(android.R.string.ok, null)
.show()
false
} else {
viewModel.setNotifyWhileLocked(isChecked)
true
}
}
)

switchPref(
title = DSLSettingsText.from(R.string.NotificationsSettingsFragment__contact_joins_signal),
isChecked = state.notifyWhenContactJoinsSignal,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import android.net.Uri
data class NotificationsSettingsState(
val messageNotificationsState: MessageNotificationsState,
val callNotificationsState: CallNotificationsState,
val notifyWhileLocked: Boolean,
val canEnableNotifyWhileLocked: Boolean,
val notifyWhenContactJoinsSignal: Boolean
)

Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package org.thoughtcrime.securesms.components.settings.app.notifications

import android.app.Application
import android.content.SharedPreferences
import android.net.Uri
import android.os.Build
Expand All @@ -17,6 +18,8 @@ class NotificationsSettingsViewModel(private val sharedPreferences: SharedPrefer

private val store = Store(getState())

private val application: Application = ApplicationDependencies.getApplication()

val state: LiveData<NotificationsSettingsState> = store.stateLiveData

init {
Expand Down Expand Up @@ -96,6 +99,11 @@ class NotificationsSettingsViewModel(private val sharedPreferences: SharedPrefer
refresh()
}

fun setNotifyWhileLocked(enabled: Boolean) {
TextSecurePreferences.setPassphraseLockNotificationsEnabled(application, enabled)
refresh()
}

fun setNotifyWhenContactJoinsSignal(enabled: Boolean) {
SignalStore.settings().isNotifyWhenContactJoinsSignal = enabled
refresh()
Expand All @@ -117,14 +125,16 @@ class NotificationsSettingsViewModel(private val sharedPreferences: SharedPrefer
inChatSoundsEnabled = SignalStore.settings().isMessageNotificationsInChatSoundsEnabled,
repeatAlerts = SignalStore.settings().messageNotificationsRepeatAlerts,
messagePrivacy = SignalStore.settings().messageNotificationsPrivacy.toString(),
priority = TextSecurePreferences.getNotificationPriority(ApplicationDependencies.getApplication()),
priority = TextSecurePreferences.getNotificationPriority(application),
),
callNotificationsState = CallNotificationsState(
notificationsEnabled = SignalStore.settings().isCallNotificationsEnabled && canEnableNotifications(),
canEnableNotifications = canEnableNotifications(),
ringtone = SignalStore.settings().callRingtone,
vibrateEnabled = SignalStore.settings().isCallVibrateEnabled
),
notifyWhileLocked = TextSecurePreferences.isPassphraseLockNotificationsEnabled(application) && SignalStore.account().pushAvailable,
canEnableNotifyWhileLocked = SignalStore.account().pushAvailable,
notifyWhenContactJoinsSignal = SignalStore.settings().isNotifyWhenContactJoinsSignal
)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import android.content.Intent
import android.os.Build
import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat
import androidx.core.content.ContextCompat
import org.signal.core.util.PendingIntentFlags.mutable
import org.signal.core.util.concurrent.SignalExecutors
import org.signal.core.util.logging.Log
Expand Down Expand Up @@ -70,21 +71,26 @@ object FcmFetchManager {
}
}

private fun postMayHaveMessagesNotification(context: Context) {
if (FeatureFlags.fcmMayHaveMessagesNotificationKillSwitch()) {
Log.w(TAG, "May have messages notification kill switch")
@JvmStatic
fun postMayHaveMessagesNotification(context: Context) {
val notificationManager = NotificationManagerCompat.from(context)

if (notificationManager.getNotificationChannel(NotificationChannels.ADDITIONAL_MESSAGE_NOTIFICATIONS) == null) {
Log.e(TAG, "Notification channel for MAY_HAVE_MESSAGES_NOTIFICATION does not exist.")
return
}
val mayHaveMessagesNotification: Notification = NotificationCompat.Builder(context, NotificationChannels.getInstance().ADDITIONAL_MESSAGE_NOTIFICATIONS)

val mayHaveMessagesNotification: Notification = NotificationCompat.Builder(context, NotificationChannels.ADDITIONAL_MESSAGE_NOTIFICATIONS)
.setSmallIcon(R.drawable.ic_notification)
.setColor(ContextCompat.getColor(context, R.color.core_ultramarine))
.setContentTitle(context.getString(R.string.FcmFetchManager__you_may_have_messages))
.setCategory(NotificationCompat.CATEGORY_MESSAGE)
.setContentIntent(PendingIntent.getActivity(context, 0, MainActivity.clearTop(context), mutable()))
.setVibrate(longArrayOf(0))
.setOnlyAlertOnce(true)
.build()

NotificationManagerCompat.from(context)
notificationManager
.notify(NotificationIds.MAY_HAVE_MESSAGES_NOTIFICATION_ID, mayHaveMessagesNotification)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
import org.thoughtcrime.securesms.service.KeyCachingService;
import org.thoughtcrime.securesms.util.NetworkUtil;
import org.thoughtcrime.securesms.util.SignalLocalMetrics;
import org.thoughtcrime.securesms.util.TextSecurePreferences;

import java.util.Locale;

Expand All @@ -26,8 +27,13 @@ public class FcmReceiveService extends FirebaseMessagingService {
private static final String TAG = Log.tag(FcmReceiveService.class);

@Override
public void onMessageReceived(RemoteMessage remoteMessage) {
public void onMessageReceived(@NonNull RemoteMessage remoteMessage) {
if (KeyCachingService.isLocked()) {
if (remoteMessage.getPriority() == RemoteMessage.PRIORITY_HIGH &&
TextSecurePreferences.isPassphraseLockNotificationsEnabled(this)) {
Log.d(TAG, "New urgent message received while app is locked.");
FcmFetchManager.postMayHaveMessagesNotification(this);
}
return;
}

Expand Down Expand Up @@ -63,7 +69,7 @@ public void onDeletedMessages() {
}

@Override
public void onNewToken(String token) {
public void onNewToken(@NonNull String token) {
Log.i(TAG, "onNewToken()");

if (KeyCachingService.isLocked()) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -292,6 +292,10 @@ internal class AccountValues internal constructor(store: KeyValueStore) : Signal
@get:JvmName("isFcmEnabled")
var fcmEnabled: Boolean by booleanValue(KEY_FCM_ENABLED, false)

@get:JvmName("isPushAvailable")
val pushAvailable: Boolean
get() = fcmEnabled

/** The FCM token, which allows the server to send us FCM messages. */
var fcmToken: String?
get() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ public interface MessageNotifier {
void updateNotification(@NonNull Context context, @NonNull ConversationId conversationId, @NonNull BubbleUtil.BubbleState defaultBubbleState);
void updateNotification(@NonNull Context context, @NonNull ConversationId conversationId, boolean signal);
void updateNotification(@NonNull Context context, @Nullable ConversationId conversationId, boolean signal, int reminderCount, @NonNull BubbleUtil.BubbleState defaultBubbleState);
void clearNotifications(@NonNull Context context);
boolean clearNotifications(@NonNull Context context);
void clearReminder(@NonNull Context context);
void addStickyThread(@NonNull ConversationId conversationId, long earliestTimestamp);
void removeStickyThread(@NonNull ConversationId conversationId);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ private static class Version {
public final String BACKGROUND = "background_connection";
public final String CALL_STATUS = "call_status";
public final String APP_ALERTS = "app_alerts";
public final String ADDITIONAL_MESSAGE_NOTIFICATIONS = "additional_message_notifications";
public static String ADDITIONAL_MESSAGE_NOTIFICATIONS = "additional_message_notifications";

private static volatile NotificationChannels instance;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -122,8 +122,8 @@ public void updateNotification(@NonNull Context context, @Nullable ConversationI
}

@Override
public void clearNotifications(@NonNull Context context) {
getNotifier().clearNotifications(context);
public boolean clearNotifications(@NonNull Context context) {
return getNotifier().clearNotifications(context);
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -241,19 +241,23 @@ class DefaultMessageNotifier(context: Application) : MessageNotifier {

if (Build.VERSION.SDK_INT >= 24) {
val ids = state.conversations.filter { it.thread != visibleThread }.map { it.notificationId } + stickyThreads.map { (_, stickyThread) -> stickyThread.notificationId }
val notShown = ids - ServiceUtil.getNotificationManager(context).getDisplayedNotificationIds().getOrDefault(emptySet())
val notShown = ids - getDisplayedNotificationIds(context)
if (notShown.isNotEmpty()) {
Log.e(TAG, "Notifications should be showing but are not for ${notShown.size} threads")
}
}
}

override fun clearNotifications(context: Context) {
NotificationCancellationHelper.cancelAllMessageNotifications(context, stickyThreads.map { it.value.notificationId }.toSet())
override fun clearNotifications(context: Context): Boolean {
val activeNotifications = getDisplayedNotificationIds(context)
NotificationCancellationHelper.cancelAllMessageNotifications(context)
updateBadge(context, 0)
clearReminderInternal(context)
return activeNotifications.isNotEmpty()
}

private fun getDisplayedNotificationIds(context: Context) = ServiceUtil.getNotificationManager(context).getDisplayedNotificationIds().getOrDefault(emptySet())

override fun clearReminder(context: Context) {
// Intentionally left blank
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,6 @@
import org.thoughtcrime.securesms.crypto.MasterSecret;
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
import org.thoughtcrime.securesms.migrations.ApplicationMigrations;
import org.thoughtcrime.securesms.notifications.MessageNotifier;
import org.thoughtcrime.securesms.notifications.NotificationChannels;
import org.thoughtcrime.securesms.util.DynamicLanguage;
import org.thoughtcrime.securesms.util.ServiceUtil;
Expand All @@ -66,6 +65,8 @@ public class KeyCachingService extends Service {
public static final String CLEAR_KEY_ACTION = BuildConfig.APPLICATION_ID + ".service.action.CLEAR_KEY";
public static final String LOCALE_CHANGE_EVENT = BuildConfig.APPLICATION_ID + ".service.action.LOCALE_CHANGE_EVENT";

public static final String EXTRA_KEY_EXPIRED = "extra.key_expired";

private DynamicLanguage dynamicLanguage = new DynamicLanguage();

private final IBinder binder = new KeySetBinder();
Expand Down Expand Up @@ -110,9 +111,9 @@ public int onStartCommand(Intent intent, int flags, int startId) {

if (intent.getAction() != null) {
switch (intent.getAction()) {
case CLEAR_KEY_ACTION: handleClearKey(); break;
case PASSPHRASE_EXPIRED_EVENT: handleClearKey(); break;
case LOCALE_CHANGE_EVENT: handleLocaleChanged(); break;
case CLEAR_KEY_ACTION -> handleClearKey(false);
case PASSPHRASE_EXPIRED_EVENT -> handleClearKey(true);
case LOCALE_CHANGE_EVENT -> handleLocaleChanged();
}
} else {
handleCacheKey();
Expand Down Expand Up @@ -165,8 +166,8 @@ private void handleCacheKey() {
});
}

private void handleClearKey() {
Log.i(TAG, "handleClearKey");
private void handleClearKey(boolean keyExpired) {
Log.d(TAG, "handleClearKey() keyExpired: " + keyExpired);

cancelTimeout();

Expand All @@ -178,13 +179,12 @@ private void handleClearKey() {

KeyCachingService.locking = true;

sendPackageBroadcast(CLEAR_KEY_EVENT);
Log.i(TAG, "Broadcasting " + CLEAR_KEY_EVENT);

SignalExecutors.BOUNDED.execute(() -> {
MessageNotifier messageNotifier = ApplicationDependencies.getMessageNotifier();
messageNotifier.cancelDelayedNotifications();
messageNotifier.clearNotifications(KeyCachingService.this);
});
Intent intent = new Intent(CLEAR_KEY_EVENT);
intent.putExtra(EXTRA_KEY_EXPIRED, keyExpired);
intent.setPackage(getPackageName());
sendBroadcast(intent, KEY_PERMISSION);
}

private void handleLocaleChanged() {
Expand Down Expand Up @@ -252,18 +252,9 @@ private void foregroundService() {
startForeground(SERVICE_RUNNING_ID, builder.build());
}

private void sendPackageBroadcast(String action) {
Log.i(TAG, "Broadcasting " + action);

Intent intent = new Intent(action);
intent.setPackage(getApplicationContext().getPackageName());

sendBroadcast(intent, KEY_PERMISSION);
}

private PendingIntent buildLockIntent() {
Intent intent = new Intent(this, KeyCachingService.class);
intent.setAction(PASSPHRASE_EXPIRED_EVENT);
intent.setAction(CLEAR_KEY_ACTION);
return PendingIntent.getService(getApplicationContext(), 0, intent, getPendingIntentFlags());
}

Expand Down
Loading

0 comments on commit 89ba25a

Please sign in to comment.