1
0
Fork 0

For #2053: Add persistent notification to close all private browsing tabs (#4913)

master
Yeon Taek Jeong 2019-08-28 12:55:24 -07:00 committed by GitHub
parent de6dcd59f4
commit cc5408d717
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 356 additions and 0 deletions

View File

@ -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">

View File

@ -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() {

View File

@ -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"
}
}

View File

@ -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
}
}
}

View File

@ -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)
}
}
}

View File

@ -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)
})
}
}
}

View File

@ -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
}
}
}

View 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>

View File

@ -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>

View File

@ -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>

View File

@ -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 -->