mirror of
https://github.com/fork-maintainers/iceraven-browser
synced 2024-11-19 09:25:34 +00:00
[fenix] For https://github.com/mozilla-mobile/fenix/issues/2053: Add persistent notification to close all private browsing tabs (https://github.com/mozilla-mobile/fenix/pull/4913)
This commit is contained in:
parent
a6dec046ea
commit
cc75c0df87
@ -149,6 +149,9 @@
|
||||
android:resource="@xml/search_widget_info" />
|
||||
</receiver>
|
||||
|
||||
<service android:name=".session.SessionNotificationService"
|
||||
android:exported="false" />
|
||||
|
||||
<service
|
||||
android:name=".components.FirebasePush"
|
||||
android:exported="false">
|
||||
|
@ -10,6 +10,7 @@ import android.os.Build
|
||||
import android.os.Build.VERSION.SDK_INT
|
||||
import android.os.StrictMode
|
||||
import androidx.appcompat.app.AppCompatDelegate
|
||||
import androidx.core.content.getSystemService
|
||||
import io.reactivex.plugins.RxJavaPlugins
|
||||
import kotlinx.coroutines.Deferred
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
@ -31,6 +32,8 @@ import mozilla.components.support.rusthttp.RustHttpConfig
|
||||
import mozilla.components.support.rustlog.RustLog
|
||||
import org.mozilla.fenix.GleanMetrics.ExperimentsMetrics
|
||||
import org.mozilla.fenix.components.Components
|
||||
import org.mozilla.fenix.session.NotificationSessionObserver
|
||||
import org.mozilla.fenix.session.VisibilityLifecycleCallback
|
||||
import org.mozilla.fenix.utils.Settings
|
||||
import java.io.File
|
||||
|
||||
@ -43,6 +46,9 @@ open class FenixApplication : Application() {
|
||||
|
||||
open val components by lazy { Components(this) }
|
||||
|
||||
var visibilityLifecycleCallback: VisibilityLifecycleCallback? = null
|
||||
private set
|
||||
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
|
||||
@ -99,6 +105,11 @@ open class FenixApplication : Application() {
|
||||
}
|
||||
|
||||
setupPush()
|
||||
|
||||
visibilityLifecycleCallback = VisibilityLifecycleCallback(getSystemService())
|
||||
registerActivityLifecycleCallbacks(visibilityLifecycleCallback)
|
||||
|
||||
components.core.sessionManager.register(NotificationSessionObserver(this))
|
||||
}
|
||||
|
||||
private fun registerRxExceptionHandling() {
|
||||
|
@ -46,6 +46,7 @@ import org.mozilla.fenix.ext.nav
|
||||
import org.mozilla.fenix.home.HomeFragmentDirections
|
||||
import org.mozilla.fenix.home.intent.CrashReporterIntentProcessor
|
||||
import org.mozilla.fenix.home.intent.DeepLinkIntentProcessor
|
||||
import org.mozilla.fenix.home.intent.NotificationsIntentProcessor
|
||||
import org.mozilla.fenix.home.intent.OpenBrowserIntentProcessor
|
||||
import org.mozilla.fenix.home.intent.SpeechProcessingIntentProcessor
|
||||
import org.mozilla.fenix.home.intent.StartSearchIntentProcessor
|
||||
@ -72,6 +73,7 @@ open class HomeActivity : AppCompatActivity(), ShareFragment.TabsSharedCallback
|
||||
|
||||
private val externalSourceIntentProcessors by lazy {
|
||||
listOf(
|
||||
NotificationsIntentProcessor(this),
|
||||
SpeechProcessingIntentProcessor(this),
|
||||
StartSearchIntentProcessor(components.analytics.metrics),
|
||||
DeepLinkIntentProcessor(this),
|
||||
@ -337,5 +339,6 @@ open class HomeActivity : AppCompatActivity(), ShareFragment.TabsSharedCallback
|
||||
const val OPEN_TO_BROWSER = "open_to_browser"
|
||||
const val OPEN_TO_BROWSER_AND_LOAD = "open_to_browser_and_load"
|
||||
const val OPEN_TO_SEARCH = "open_to_search"
|
||||
const val EXTRA_DELETE_PRIVATE_TABS = "notification"
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,32 @@
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
package org.mozilla.fenix.home.intent
|
||||
|
||||
import android.content.Intent
|
||||
import androidx.navigation.NavController
|
||||
import org.mozilla.fenix.HomeActivity
|
||||
import org.mozilla.fenix.ext.components
|
||||
import org.mozilla.fenix.ext.sessionsOfType
|
||||
|
||||
/**
|
||||
* The Private Browsing Mode notification has an "Delete and Open" button to let users delete all
|
||||
* of their private tabs.
|
||||
*/
|
||||
class NotificationsIntentProcessor(
|
||||
private val activity: HomeActivity
|
||||
) : HomeIntentProcessor {
|
||||
|
||||
override fun process(intent: Intent, navController: NavController, out: Intent): Boolean {
|
||||
return if (intent.extras?.getBoolean(HomeActivity.EXTRA_DELETE_PRIVATE_TABS) == true) {
|
||||
out.putExtra(HomeActivity.EXTRA_DELETE_PRIVATE_TABS, false)
|
||||
activity.components.core.sessionManager.run {
|
||||
sessionsOfType(private = true).forEach { remove(it) }
|
||||
}
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,39 @@
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
package org.mozilla.fenix.session
|
||||
|
||||
import android.content.Context
|
||||
import mozilla.components.browser.session.Session
|
||||
import mozilla.components.browser.session.SessionManager
|
||||
import org.mozilla.fenix.ext.components
|
||||
import org.mozilla.fenix.ext.sessionsOfType
|
||||
|
||||
/**
|
||||
* This observer starts and stops the service to show a notification
|
||||
* indicating that a private tab is open.
|
||||
*/
|
||||
|
||||
class NotificationSessionObserver(
|
||||
private val context: Context
|
||||
) : SessionManager.Observer {
|
||||
|
||||
override fun onSessionRemoved(session: Session) {
|
||||
val privateTabsEmpty = !context.components.core.sessionManager.sessionsOfType(private = true).none()
|
||||
|
||||
if (privateTabsEmpty) {
|
||||
SessionNotificationService.stop(context)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onAllSessionsRemoved() {
|
||||
SessionNotificationService.stop(context)
|
||||
}
|
||||
|
||||
override fun onSessionAdded(session: Session) {
|
||||
if (session.private) {
|
||||
SessionNotificationService.start(context)
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,175 @@
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
package org.mozilla.fenix.session
|
||||
|
||||
import android.app.Notification
|
||||
import android.app.NotificationChannel
|
||||
import android.app.NotificationManager
|
||||
import android.app.PendingIntent
|
||||
import android.app.Service
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.os.Build
|
||||
import android.os.IBinder
|
||||
import androidx.core.app.NotificationCompat
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.content.getSystemService
|
||||
import mozilla.components.browser.session.SessionManager
|
||||
import mozilla.components.support.utils.ThreadUtils
|
||||
|
||||
import org.mozilla.fenix.R
|
||||
import org.mozilla.fenix.HomeActivity
|
||||
import org.mozilla.fenix.ext.components
|
||||
import org.mozilla.fenix.ext.sessionsOfType
|
||||
|
||||
/**
|
||||
* As long as a session is active this service will keep the notification (and our process) alive.
|
||||
*/
|
||||
class SessionNotificationService : Service() {
|
||||
|
||||
override fun onStartCommand(intent: Intent, flags: Int, startId: Int): Int {
|
||||
val action = intent.action ?: return Service.START_NOT_STICKY
|
||||
|
||||
when (action) {
|
||||
ACTION_START -> {
|
||||
createNotificationChannelIfNeeded()
|
||||
startForeground(NOTIFICATION_ID, buildNotification())
|
||||
}
|
||||
|
||||
ACTION_ERASE -> {
|
||||
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
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
else -> throw IllegalStateException("Unknown intent: $intent")
|
||||
}
|
||||
|
||||
return Service.START_NOT_STICKY
|
||||
}
|
||||
|
||||
override fun onTaskRemoved(rootIntent: Intent) {
|
||||
components.core.sessionManager.removeAndCloseAllPrivateSessions()
|
||||
|
||||
stopForeground(true)
|
||||
stopSelf()
|
||||
}
|
||||
|
||||
private fun buildNotification(): Notification {
|
||||
return NotificationCompat.Builder(this, NOTIFICATION_CHANNEL_ID)
|
||||
.setOngoing(true)
|
||||
.setSmallIcon(R.drawable.ic_pbm_notification)
|
||||
.setContentTitle(getString(R.string.app_name_private))
|
||||
.setContentText(getString(R.string.notification_pbm_delete_text))
|
||||
.setContentIntent(createNotificationIntent())
|
||||
.setVisibility(NotificationCompat.VISIBILITY_SECRET)
|
||||
.setShowWhen(false)
|
||||
.setLocalOnly(true)
|
||||
.setColor(ContextCompat.getColor(this, R.color.pbm_notification_color))
|
||||
.addAction(
|
||||
NotificationCompat.Action(
|
||||
0,
|
||||
getString(R.string.notification_pbm_action_open),
|
||||
createOpenActionIntent()
|
||||
)
|
||||
)
|
||||
.addAction(
|
||||
NotificationCompat.Action(
|
||||
0,
|
||||
getString(R.string.notification_pbm_action_delete_and_open),
|
||||
createOpenAndEraseActionIntent()
|
||||
)
|
||||
)
|
||||
.build()
|
||||
}
|
||||
|
||||
private fun createNotificationIntent(): PendingIntent {
|
||||
val intent = Intent(this, SessionNotificationService::class.java)
|
||||
intent.action = ACTION_ERASE
|
||||
|
||||
return PendingIntent.getService(this, 0, intent, PendingIntent.FLAG_ONE_SHOT)
|
||||
}
|
||||
|
||||
private fun createOpenActionIntent(): PendingIntent {
|
||||
val intent = Intent(this, HomeActivity::class.java)
|
||||
|
||||
return PendingIntent.getActivity(this, 1, intent, PendingIntent.FLAG_UPDATE_CURRENT)
|
||||
}
|
||||
|
||||
private fun createOpenAndEraseActionIntent(): PendingIntent {
|
||||
val intent = Intent(this, HomeActivity::class.java)
|
||||
|
||||
intent.putExtra(HomeActivity.EXTRA_DELETE_PRIVATE_TABS, true)
|
||||
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
|
||||
|
||||
return PendingIntent.getActivity(this, 2, intent, PendingIntent.FLAG_CANCEL_CURRENT)
|
||||
}
|
||||
|
||||
private fun createNotificationChannelIfNeeded() {
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
|
||||
// Notification channels are only available on Android O or higher.
|
||||
return
|
||||
}
|
||||
|
||||
val notificationManager = getSystemService<NotificationManager>() ?: return
|
||||
|
||||
val notificationChannelName = getString(R.string.notification_pbm_channel_name)
|
||||
|
||||
val channel = NotificationChannel(
|
||||
NOTIFICATION_CHANNEL_ID, notificationChannelName, NotificationManager.IMPORTANCE_MIN
|
||||
)
|
||||
channel.importance = NotificationManager.IMPORTANCE_LOW
|
||||
channel.enableLights(false)
|
||||
channel.enableVibration(false)
|
||||
channel.setShowBadge(false)
|
||||
|
||||
notificationManager.createNotificationChannel(channel)
|
||||
}
|
||||
|
||||
private fun SessionManager.removeAndCloseAllPrivateSessions() {
|
||||
sessionsOfType(private = true).forEach { remove(it) }
|
||||
}
|
||||
|
||||
override fun onBind(intent: Intent): IBinder? {
|
||||
return null
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val NOTIFICATION_ID = 83
|
||||
private const val NOTIFICATION_CHANNEL_ID = "browsing-session"
|
||||
|
||||
private const val ACTION_START = "start"
|
||||
private const val ACTION_ERASE = "erase"
|
||||
|
||||
internal fun start(context: Context) {
|
||||
val intent = Intent(context, SessionNotificationService::class.java)
|
||||
intent.action = ACTION_START
|
||||
|
||||
// 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
|
||||
// calls by running this after potentially expensive calls in FocusApplication.onCreate and
|
||||
// BrowserFragment.inflateView by posting it to the end of the main thread.
|
||||
ThreadUtils.postToMainThread(Runnable {
|
||||
context.startService(intent)
|
||||
})
|
||||
}
|
||||
|
||||
internal fun stop(context: Context) {
|
||||
val intent = Intent(context, SessionNotificationService::class.java)
|
||||
|
||||
// We want to make sure we always call stop after start. So we're
|
||||
// putting these actions on the same sequential run queue.
|
||||
ThreadUtils.postToMainThread(Runnable {
|
||||
context.stopService(intent)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,68 @@
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
package org.mozilla.fenix.session
|
||||
|
||||
import android.app.Activity
|
||||
import android.app.ActivityManager
|
||||
import android.app.Application
|
||||
import android.content.Context
|
||||
import android.os.Bundle
|
||||
import org.mozilla.fenix.FenixApplication
|
||||
|
||||
/**
|
||||
* This ActivityLifecycleCallbacks implementations tracks if there is at least one activity in the
|
||||
* STARTED state (meaning some part of our application is visible).
|
||||
* Based on this information the current task can be removed if the app is not visible.
|
||||
*/
|
||||
@SuppressWarnings("EmptyFunctionBlock")
|
||||
class VisibilityLifecycleCallback(private val activityManager: ActivityManager?) :
|
||||
Application.ActivityLifecycleCallbacks {
|
||||
|
||||
/**
|
||||
* Activities are not stopped/started in an ordered way. So we are using
|
||||
*/
|
||||
private var activitiesInStartedState: Int = 0
|
||||
|
||||
private fun finishAndRemoveTaskIfInBackground(): Boolean {
|
||||
if (activitiesInStartedState == 0) {
|
||||
activityManager?.let {
|
||||
for (task in it.appTasks) {
|
||||
task.finishAndRemoveTask()
|
||||
}
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
override fun onActivityStarted(activity: Activity?) {
|
||||
activitiesInStartedState++
|
||||
}
|
||||
|
||||
override fun onActivityStopped(activity: Activity?) {
|
||||
activitiesInStartedState--
|
||||
}
|
||||
|
||||
override fun onActivityResumed(activity: Activity?) {}
|
||||
|
||||
override fun onActivityPaused(activity: Activity?) {}
|
||||
|
||||
override fun onActivityCreated(activity: Activity?, bundle: Bundle?) {}
|
||||
|
||||
override fun onActivitySaveInstanceState(activity: Activity?, bundle: Bundle?) {}
|
||||
|
||||
override fun onActivityDestroyed(activity: Activity?) {}
|
||||
|
||||
companion object {
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
internal fun finishAndRemoveTaskIfInBackground(context: Context): Boolean {
|
||||
return (context.applicationContext as FenixApplication)
|
||||
.visibilityLifecycleCallback?.finishAndRemoveTaskIfInBackground() ?: false
|
||||
}
|
||||
}
|
||||
}
|
13
app/src/main/res/drawable/ic_pbm_notification.xml
Normal file
13
app/src/main/res/drawable/ic_pbm_notification.xml
Normal file
@ -0,0 +1,13 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- This Source Code Form is subject to the terms of the Mozilla Public
|
||||
- License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
- file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path android:fillColor="@android:color/white"
|
||||
android:pathData="M12 23a11 11 0 1 0 0-22 11 11 0 0 0 0 22zm3.3-7.94c-1.25 0-2.1-1.53-3.3-1.53-1.2 0-2.13 1.53-3.3 1.53-1.53 0-2.67-1.5-2.69-4 0-1.58 0.45-2.1 2.45-2.08 2 0 2.57 0.83 3.54 0.83 1 0 1.55-0.83 3.54-0.83 2 0 2.46 0.5 2.45 2.08-0 2.54-1.16 4.03-2.7 4zm-5.87-4.17c0.74-0.1 1.43 0.37 1.6 1.11 0 0.26-1 0.56-1.72 0.56-0.78 0-1.6-0.52-1.6-0.7 0-0.18 0.5-1 1.71-1zm5.13 0c-0.73-0.1-1.42 0.38-1.6 1.11 0 0.26 1 0.56 1.72 0.56 0.78 0 1.58-0.52 1.58-0.7 0-0.18-0.5-0.9-1.7-1z"/>
|
||||
</vector>
|
||||
|
@ -214,4 +214,7 @@
|
||||
|
||||
<!-- Search Widget -->
|
||||
<color name="search_widget_text">#737373</color>
|
||||
|
||||
<!-- Private Browsing Mode Persistent Notification -->
|
||||
<color name="pbm_notification_color">#592ACB</color>
|
||||
</resources>
|
||||
|
@ -5,6 +5,7 @@
|
||||
<resources>
|
||||
<!-- Name of the application -->
|
||||
<string name="app_name" translatable="false">Firefox Preview</string>
|
||||
<string name="app_name_private" translatable="false">Firefox Preview (Private)</string>
|
||||
|
||||
<!-- Preference for developers -->
|
||||
<string name="preference_leakcanary" translatable="false">LeakCanary</string>
|
||||
|
@ -568,6 +568,14 @@
|
||||
<string name="sync_confirmation_button">Got it</string>
|
||||
|
||||
<!-- Notifications -->
|
||||
<!-- The user visible name of the "notification channel" (Android 8+ feature) for the ongoing notification shown while a browsing session is active. -->
|
||||
<string name="notification_pbm_channel_name">Private browsing session</string>
|
||||
<!-- Text shown in the notification that pops up to remind the user that a private browsing session is active. -->
|
||||
<string name="notification_pbm_delete_text">Delete private tabs</string>
|
||||
<!-- Notification action to open Fenix and resume the current browsing session. -->
|
||||
<string name="notification_pbm_action_open">Open</string>
|
||||
<!-- Notification action to delete all current private browsing sessions AND switch to Fenix (bring it to the foreground) -->
|
||||
<string name="notification_pbm_action_delete_and_open">Delete and Open</string>
|
||||
<!-- Text shown in snackbar when user deletes a collection -->
|
||||
<string name="snackbar_collection_deleted">Collection deleted</string>
|
||||
<!-- Text shown in snackbar when user renames a collection -->
|
||||
|
Loading…
Reference in New Issue
Block a user