[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)
parent
a6dec046ea
commit
cc75c0df87
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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>
|
||||||
|
|
Loading…
Reference in New Issue