parent
de6dcd59f4
commit
cc5408d717
|
@ -149,6 +149,9 @@
|
||||||
android:resource="@xml/search_widget_info" />
|
android:resource="@xml/search_widget_info" />
|
||||||
</receiver>
|
</receiver>
|
||||||
|
|
||||||
|
<service android:name=".session.SessionNotificationService"
|
||||||
|
android:exported="false" />
|
||||||
|
|
||||||
<service
|
<service
|
||||||
android:name=".components.FirebasePush"
|
android:name=".components.FirebasePush"
|
||||||
android:exported="false">
|
android:exported="false">
|
||||||
|
|
|
@ -10,6 +10,7 @@ import android.os.Build
|
||||||
import android.os.Build.VERSION.SDK_INT
|
import android.os.Build.VERSION.SDK_INT
|
||||||
import android.os.StrictMode
|
import android.os.StrictMode
|
||||||
import androidx.appcompat.app.AppCompatDelegate
|
import androidx.appcompat.app.AppCompatDelegate
|
||||||
|
import androidx.core.content.getSystemService
|
||||||
import io.reactivex.plugins.RxJavaPlugins
|
import io.reactivex.plugins.RxJavaPlugins
|
||||||
import kotlinx.coroutines.Deferred
|
import kotlinx.coroutines.Deferred
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
@ -31,6 +32,8 @@ import mozilla.components.support.rusthttp.RustHttpConfig
|
||||||
import mozilla.components.support.rustlog.RustLog
|
import mozilla.components.support.rustlog.RustLog
|
||||||
import org.mozilla.fenix.GleanMetrics.ExperimentsMetrics
|
import org.mozilla.fenix.GleanMetrics.ExperimentsMetrics
|
||||||
import org.mozilla.fenix.components.Components
|
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 org.mozilla.fenix.utils.Settings
|
||||||
import java.io.File
|
import java.io.File
|
||||||
|
|
||||||
|
@ -43,6 +46,9 @@ open class FenixApplication : Application() {
|
||||||
|
|
||||||
open val components by lazy { Components(this) }
|
open val components by lazy { Components(this) }
|
||||||
|
|
||||||
|
var visibilityLifecycleCallback: VisibilityLifecycleCallback? = null
|
||||||
|
private set
|
||||||
|
|
||||||
override fun onCreate() {
|
override fun onCreate() {
|
||||||
super.onCreate()
|
super.onCreate()
|
||||||
|
|
||||||
|
@ -99,6 +105,11 @@ open class FenixApplication : Application() {
|
||||||
}
|
}
|
||||||
|
|
||||||
setupPush()
|
setupPush()
|
||||||
|
|
||||||
|
visibilityLifecycleCallback = VisibilityLifecycleCallback(getSystemService())
|
||||||
|
registerActivityLifecycleCallbacks(visibilityLifecycleCallback)
|
||||||
|
|
||||||
|
components.core.sessionManager.register(NotificationSessionObserver(this))
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun registerRxExceptionHandling() {
|
private fun registerRxExceptionHandling() {
|
||||||
|
|
|
@ -46,6 +46,7 @@ import org.mozilla.fenix.ext.nav
|
||||||
import org.mozilla.fenix.home.HomeFragmentDirections
|
import org.mozilla.fenix.home.HomeFragmentDirections
|
||||||
import org.mozilla.fenix.home.intent.CrashReporterIntentProcessor
|
import org.mozilla.fenix.home.intent.CrashReporterIntentProcessor
|
||||||
import org.mozilla.fenix.home.intent.DeepLinkIntentProcessor
|
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.OpenBrowserIntentProcessor
|
||||||
import org.mozilla.fenix.home.intent.SpeechProcessingIntentProcessor
|
import org.mozilla.fenix.home.intent.SpeechProcessingIntentProcessor
|
||||||
import org.mozilla.fenix.home.intent.StartSearchIntentProcessor
|
import org.mozilla.fenix.home.intent.StartSearchIntentProcessor
|
||||||
|
@ -72,6 +73,7 @@ open class HomeActivity : AppCompatActivity(), ShareFragment.TabsSharedCallback
|
||||||
|
|
||||||
private val externalSourceIntentProcessors by lazy {
|
private val externalSourceIntentProcessors by lazy {
|
||||||
listOf(
|
listOf(
|
||||||
|
NotificationsIntentProcessor(this),
|
||||||
SpeechProcessingIntentProcessor(this),
|
SpeechProcessingIntentProcessor(this),
|
||||||
StartSearchIntentProcessor(components.analytics.metrics),
|
StartSearchIntentProcessor(components.analytics.metrics),
|
||||||
DeepLinkIntentProcessor(this),
|
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 = "open_to_browser"
|
||||||
const val OPEN_TO_BROWSER_AND_LOAD = "open_to_browser_and_load"
|
const val OPEN_TO_BROWSER_AND_LOAD = "open_to_browser_and_load"
|
||||||
const val OPEN_TO_SEARCH = "open_to_search"
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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 -->
|
<!-- Search Widget -->
|
||||||
<color name="search_widget_text">#737373</color>
|
<color name="search_widget_text">#737373</color>
|
||||||
|
|
||||||
|
<!-- Private Browsing Mode Persistent Notification -->
|
||||||
|
<color name="pbm_notification_color">#592ACB</color>
|
||||||
</resources>
|
</resources>
|
||||||
|
|
|
@ -5,6 +5,7 @@
|
||||||
<resources>
|
<resources>
|
||||||
<!-- Name of the application -->
|
<!-- Name of the application -->
|
||||||
<string name="app_name" translatable="false">Firefox Preview</string>
|
<string name="app_name" translatable="false">Firefox Preview</string>
|
||||||
|
<string name="app_name_private" translatable="false">Firefox Preview (Private)</string>
|
||||||
|
|
||||||
<!-- Preference for developers -->
|
<!-- Preference for developers -->
|
||||||
<string name="preference_leakcanary" translatable="false">LeakCanary</string>
|
<string name="preference_leakcanary" translatable="false">LeakCanary</string>
|
||||||
|
|
|
@ -568,6 +568,14 @@
|
||||||
<string name="sync_confirmation_button">Got it</string>
|
<string name="sync_confirmation_button">Got it</string>
|
||||||
|
|
||||||
<!-- Notifications -->
|
<!-- 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 -->
|
<!-- Text shown in snackbar when user deletes a collection -->
|
||||||
<string name="snackbar_collection_deleted">Collection deleted</string>
|
<string name="snackbar_collection_deleted">Collection deleted</string>
|
||||||
<!-- Text shown in snackbar when user renames a collection -->
|
<!-- Text shown in snackbar when user renames a collection -->
|
||||||
|
|
Loading…
Reference in New Issue