diff --git a/app/src/main/java/org/mozilla/fenix/FenixApplication.kt b/app/src/main/java/org/mozilla/fenix/FenixApplication.kt index 9582d1b995..a373570ceb 100644 --- a/app/src/main/java/org/mozilla/fenix/FenixApplication.kt +++ b/app/src/main/java/org/mozilla/fenix/FenixApplication.kt @@ -43,7 +43,6 @@ import org.mozilla.fenix.ext.settings import org.mozilla.fenix.perf.StartupTimeline import org.mozilla.fenix.push.PushFxaIntegration import org.mozilla.fenix.push.WebPushEngineIntegration -import org.mozilla.fenix.session.NotificationSessionObserver import org.mozilla.fenix.session.PerformanceActivityLifecycleCallbacks import org.mozilla.fenix.session.VisibilityLifecycleCallback import org.mozilla.fenix.utils.BrowsersCache @@ -157,9 +156,6 @@ open class FenixApplication : LocaleAwareApplication() { visibilityLifecycleCallback = VisibilityLifecycleCallback(getSystemService()) registerActivityLifecycleCallbacks(visibilityLifecycleCallback) - val privateNotificationObserver = NotificationSessionObserver(this) - privateNotificationObserver.start() - // Storage maintenance disabled, for now, as it was interfering with background migrations. // See https://github.com/mozilla-mobile/fenix/issues/7227 for context. // if ((System.currentTimeMillis() - settings().lastPlacesStorageMaintenance) > ONE_DAY_MILLIS) { diff --git a/app/src/main/java/org/mozilla/fenix/HomeActivity.kt b/app/src/main/java/org/mozilla/fenix/HomeActivity.kt index 190a4cc344..13e8b7cacf 100644 --- a/app/src/main/java/org/mozilla/fenix/HomeActivity.kt +++ b/app/src/main/java/org/mozilla/fenix/HomeActivity.kt @@ -74,6 +74,7 @@ import org.mozilla.fenix.library.history.HistoryFragmentDirections import org.mozilla.fenix.perf.Performance import org.mozilla.fenix.perf.StartupTimeline import org.mozilla.fenix.search.SearchFragmentDirections +import org.mozilla.fenix.session.NotificationSessionObserver import org.mozilla.fenix.settings.SettingsFragmentDirections import org.mozilla.fenix.settings.TrackingProtectionFragmentDirections import org.mozilla.fenix.settings.about.AboutFragmentDirections @@ -154,6 +155,10 @@ open class HomeActivity : LocaleAwareAppCompatActivity() { sessionObserver = UriOpenedObserver(this) + checkPrivateShortcutEntryPoint(intent) + val privateNotificationObserver = NotificationSessionObserver(this) + privateNotificationObserver.start() + if (isActivityColdStarted(intent, savedInstanceState)) { externalSourceIntentProcessors.any { it.process(intent, navHost.navController, this.intent) } } @@ -176,6 +181,11 @@ open class HomeActivity : LocaleAwareAppCompatActivity() { StartupTimeline.homeActivityLifecycleObserver ) StartupTimeline.onActivityCreateEndHome(this) + + if (shouldAddToRecentsScreen(intent)) { + intent.removeExtra(START_IN_RECENTS_SCREEN) + moveTaskToBack(true) + } } @CallSuper @@ -313,6 +323,30 @@ open class HomeActivity : LocaleAwareAppCompatActivity() { return settings().lastKnownMode } + /** + * Determines whether the activity should be pushed to be backstack (i.e., 'minimized' to the recents + * screen) upon starting. + * @param intent - The intent that started this activity. Is checked for having the 'START_IN_RECENTS_SCREEN'-extra. + * @return true if the activity should be started and pushed to the recents screen, false otherwise. + */ + private fun shouldAddToRecentsScreen(intent: Intent?): Boolean { + intent?.toSafeIntent()?.let { + return it.getBooleanExtra(START_IN_RECENTS_SCREEN, false) + } + return false + } + + private fun checkPrivateShortcutEntryPoint(intent: Intent) { + if (intent.hasExtra(OPEN_TO_SEARCH) && + (intent.getStringExtra(OPEN_TO_SEARCH) == + StartSearchIntentProcessor.STATIC_SHORTCUT_NEW_PRIVATE_TAB || + intent.getStringExtra(OPEN_TO_SEARCH) == + StartSearchIntentProcessor.PRIVATE_BROWSING_PINNED_SHORTCUT) + ) { + NotificationSessionObserver.isStartedFromPrivateShortcut = true + } + } + private fun setupThemeAndBrowsingMode(mode: BrowsingMode) { settings().lastKnownMode = mode browsingModeManager = createBrowsingModeManager(mode) @@ -499,5 +533,6 @@ open class HomeActivity : LocaleAwareAppCompatActivity() { const val EXTRA_DELETE_PRIVATE_TABS = "notification_delete_and_open" const val EXTRA_OPENED_FROM_NOTIFICATION = "notification_open" const val delay = 5000L + const val START_IN_RECENTS_SCREEN = "start_in_recents_screen" } } diff --git a/app/src/main/java/org/mozilla/fenix/session/NotificationSessionObserver.kt b/app/src/main/java/org/mozilla/fenix/session/NotificationSessionObserver.kt index 9a3dbfd72e..456789cb6a 100644 --- a/app/src/main/java/org/mozilla/fenix/session/NotificationSessionObserver.kt +++ b/app/src/main/java/org/mozilla/fenix/session/NotificationSessionObserver.kt @@ -34,7 +34,7 @@ class NotificationSessionObserver( .ifChanged() .collect { hasPrivateTabs -> if (hasPrivateTabs) { - notificationService.start(context) + notificationService.start(context, isStartedFromPrivateShortcut) started = true } else if (started) { notificationService.stop(context) @@ -47,4 +47,8 @@ class NotificationSessionObserver( fun stop() { scope?.cancel() } + + companion object { + var isStartedFromPrivateShortcut = false + } } diff --git a/app/src/main/java/org/mozilla/fenix/session/SessionNotificationService.kt b/app/src/main/java/org/mozilla/fenix/session/SessionNotificationService.kt index df91b96758..ea9049c93a 100644 --- a/app/src/main/java/org/mozilla/fenix/session/SessionNotificationService.kt +++ b/app/src/main/java/org/mozilla/fenix/session/SessionNotificationService.kt @@ -37,32 +37,41 @@ import org.mozilla.fenix.ext.sessionsOfType */ class SessionNotificationService : Service() { + private var isStartedFromPrivateShortcut: Boolean = false + override fun onStartCommand(intent: Intent, flags: Int, startId: Int): Int { - val action = intent.action ?: return Service.START_NOT_STICKY + val action = intent.action ?: return START_NOT_STICKY when (action) { ACTION_START -> { + isStartedFromPrivateShortcut = intent.getBooleanExtra(STARTED_FROM_PRIVATE_SHORTCUT, false) createNotificationChannelIfNeeded() startForeground(NOTIFICATION_ID, buildNotification()) } ACTION_ERASE -> { metrics.track(Event.PrivateBrowsingNotificationTapped) - components.core.sessionManager.removeAndCloseAllPrivateSessions() - if (!VisibilityLifecycleCallback.finishAndRemoveTaskIfInBackground(this)) { - startActivity( - Intent(this, HomeActivity::class.java).apply { - this.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK - } - ) + val homeScreenIntent = Intent(this, HomeActivity::class.java) + val intentFlags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK + homeScreenIntent.apply { + setFlags(intentFlags) + putExtra(HomeActivity.PRIVATE_BROWSING_MODE, isStartedFromPrivateShortcut) } + if (VisibilityLifecycleCallback.finishAndRemoveTaskIfInBackground(this)) { + // Set start mode to be in background (recents screen) + homeScreenIntent.apply { + putExtra(HomeActivity.START_IN_RECENTS_SCREEN, true) + } + } + startActivity(homeScreenIntent) + components.core.sessionManager.removeAndCloseAllPrivateSessions() } else -> throw IllegalStateException("Unknown intent: $intent") } - return Service.START_NOT_STICKY + return START_NOT_STICKY } override fun onTaskRemoved(rootIntent: Intent) { @@ -125,13 +134,18 @@ class SessionNotificationService : Service() { companion object { private const val NOTIFICATION_ID = 83 private const val NOTIFICATION_CHANNEL_ID = "browsing-session" + private const val STARTED_FROM_PRIVATE_SHORTCUT = "STARTED_FROM_PRIVATE_SHORTCUT" private const val ACTION_START = "start" private const val ACTION_ERASE = "erase" - internal fun start(context: Context) { + internal fun start( + context: Context, + startedFromPrivateShortcut: Boolean + ) { val intent = Intent(context, SessionNotificationService::class.java) intent.action = ACTION_START + intent.putExtra(STARTED_FROM_PRIVATE_SHORTCUT, startedFromPrivateShortcut) // From Focus #2901: The application is crashing due to the service not calling `startForeground` // before it times out. This is a speculative fix to decrease the time between these two diff --git a/app/src/main/java/org/mozilla/fenix/session/VisibilityLifecycleCallback.kt b/app/src/main/java/org/mozilla/fenix/session/VisibilityLifecycleCallback.kt index 20c6a365f5..131bfbd733 100644 --- a/app/src/main/java/org/mozilla/fenix/session/VisibilityLifecycleCallback.kt +++ b/app/src/main/java/org/mozilla/fenix/session/VisibilityLifecycleCallback.kt @@ -25,6 +25,13 @@ class VisibilityLifecycleCallback(private val activityManager: ActivityManager?) */ private var activitiesInStartedState: Int = 0 + /** + * Finishes and removes the list of AppTasks only if the application is in the background. + * The application is considered to be in the background if it has at least 1 Activity in the + * started state + * @return True if application is in background (also finishes and removes all AppTasks), + * false otherwise + */ private fun finishAndRemoveTaskIfInBackground(): Boolean { if (activitiesInStartedState == 0) { activityManager?.let { @@ -59,6 +66,9 @@ class VisibilityLifecycleCallback(private val activityManager: ActivityManager?) /** * If all activities of this app are in the background then finish and remove all tasks. After * that the app won't show up in "recent apps" anymore. + * + * @return True if application is in background (and consequently, finishes and removes all tasks), + * false otherwise. */ internal fun finishAndRemoveTaskIfInBackground(context: Context): Boolean { return (context.applicationContext as FenixApplication) diff --git a/app/src/test/java/org/mozilla/fenix/session/NotificationSessionObserverTest.kt b/app/src/test/java/org/mozilla/fenix/session/NotificationSessionObserverTest.kt index 77d9e2bee2..aa313ec159 100644 --- a/app/src/test/java/org/mozilla/fenix/session/NotificationSessionObserverTest.kt +++ b/app/src/test/java/org/mozilla/fenix/session/NotificationSessionObserverTest.kt @@ -35,6 +35,7 @@ class NotificationSessionObserverTest { store = BrowserStore() every { context.components.core.store } returns store observer = NotificationSessionObserver(context, notificationService) + NotificationSessionObserver.isStartedFromPrivateShortcut = false } @Test @@ -44,7 +45,7 @@ class NotificationSessionObserverTest { store.dispatch(TabListAction.AddTabAction(privateSession)).join() observer.start() - verify(exactly = 1) { notificationService.start(context) } + verify(exactly = 1) { notificationService.start(context, false) } confirmVerified(notificationService) } @@ -57,10 +58,10 @@ class NotificationSessionObserverTest { verify { notificationService wasNot Called } store.dispatch(TabListAction.AddTabAction(normalSession)).join() - verify(exactly = 0) { notificationService.start(context) } + verify(exactly = 0) { notificationService.start(context, false) } store.dispatch(CustomTabListAction.AddCustomTabAction(customSession)).join() - verify(exactly = 0) { notificationService.start(context) } + verify(exactly = 0) { notificationService.start(context, false) } } @Test @@ -74,9 +75,9 @@ class NotificationSessionObserverTest { verify { notificationService wasNot Called } store.dispatch(CustomTabListAction.AddCustomTabAction(privateCustomSession)).join() - verify(exactly = 0) { notificationService.start(context) } + verify(exactly = 0) { notificationService.start(context, false) } store.dispatch(CustomTabListAction.AddCustomTabAction(customSession)).join() - verify(exactly = 0) { notificationService.start(context) } + verify(exactly = 0) { notificationService.start(context, false) } } }