Skip to content
This repository has been archived by the owner on Jan 12, 2023. It is now read-only.

Bug 1802577 - Target Android 13 (SDK 33) #7841

Closed
wants to merge 4 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
2 changes: 1 addition & 1 deletion app/build.gradle
Expand Up @@ -23,7 +23,7 @@ android {
defaultConfig {
applicationId "org.mozilla"
minSdkVersion 21
targetSdkVersion 32
targetSdkVersion 33
versionCode 11 // This versionCode is "frozen" for local builds. For "release" builds we
// override this with a generated versionCode at build time.
versionName "106.0"
Expand Down
4 changes: 4 additions & 0 deletions app/src/main/AndroidManifest.xml
Expand Up @@ -38,6 +38,10 @@
<!-- Needed to post notifications on devices with Android 13 and later-->
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />

<uses-permission android:name="android.permission.READ_MEDIA_VIDEO" />
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES" />
<uses-permission android:name="android.permission.READ_MEDIA_AUDIO" />
mcarare marked this conversation as resolved.
Show resolved Hide resolved

<application
android:allowBackup="false"
android:extractNativeLibs="true"
Expand Down
2 changes: 0 additions & 2 deletions app/src/main/java/org/mozilla/focus/Components.kt
Expand Up @@ -61,7 +61,6 @@ import org.mozilla.focus.experiments.createNimbus
import org.mozilla.focus.ext.components
import org.mozilla.focus.ext.settings
import org.mozilla.focus.media.MediaSessionService
import org.mozilla.focus.notification.PrivateNotificationMiddleware
import org.mozilla.focus.search.SearchFilterMiddleware
import org.mozilla.focus.search.SearchMigration
import org.mozilla.focus.state.AppState
Expand Down Expand Up @@ -142,7 +141,6 @@ class Components(
val store by lazy {
BrowserStore(
middleware = listOf(
PrivateNotificationMiddleware(context),
TelemetryMiddleware(),
DownloadMiddleware(context, DownloadService::class.java),
SanityCheckMiddleware(),
Expand Down
58 changes: 52 additions & 6 deletions app/src/main/java/org/mozilla/focus/activity/MainActivity.kt
Expand Up @@ -4,6 +4,7 @@

package org.mozilla.focus.activity

import android.Manifest.permission.POST_NOTIFICATIONS
import android.content.Context
import android.content.Intent
import android.content.res.Configuration
Expand All @@ -14,6 +15,7 @@ import android.util.AttributeSet
import android.view.MenuItem
import android.view.View
import android.view.ViewTreeObserver
import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.app.ActionBar
import androidx.appcompat.widget.Toolbar
import androidx.core.content.ContextCompat
Expand Down Expand Up @@ -45,6 +47,8 @@ import org.mozilla.focus.navigation.MainActivityNavigation
import org.mozilla.focus.navigation.Navigator
import org.mozilla.focus.searchwidget.ExternalIntentNavigation
import org.mozilla.focus.session.IntentProcessor
import org.mozilla.focus.session.PrivateNotificationFeature
import org.mozilla.focus.session.SessionNotificationService
import org.mozilla.focus.shortcut.HomeScreen
import org.mozilla.focus.state.AppAction
import org.mozilla.focus.state.Screen
Expand All @@ -71,6 +75,15 @@ open class MainActivity : LocaleAwareAppCompatActivity() {
private lateinit var startupTypeTelemetry: StartupTypeTelemetry
private var _binding: ActivityMainBinding? = null
private val binding get() = _binding!!
private lateinit var privateNotificationObserver: PrivateNotificationFeature
private val notificationPermission =
registerForActivityResult(ActivityResultContracts.RequestPermission()) { granted ->
when {
granted -> {
privateNotificationObserver.start()
}
}
}

override fun onCreate(savedInstanceState: Bundle?) {
installSplashScreen()
Expand Down Expand Up @@ -107,9 +120,10 @@ open class MainActivity : LocaleAwareAppCompatActivity() {
setContentView(binding.root)

startupPathProvider.attachOnActivityOnCreate(lifecycle, intent)
startupTypeTelemetry = StartupTypeTelemetry(components.startupStateProvider, startupPathProvider).apply {
attachOnMainActivityOnCreate(lifecycle)
}
startupTypeTelemetry =
StartupTypeTelemetry(components.startupStateProvider, startupPathProvider).apply {
attachOnMainActivityOnCreate(lifecycle)
}

val safeIntent = SafeIntent(intent)

Expand All @@ -135,6 +149,23 @@ open class MainActivity : LocaleAwareAppCompatActivity() {
.apply()

AppReviewUtils.showAppReview(this)

privateNotificationObserver = PrivateNotificationFeature(
context = applicationContext,
browserStore = components.store,
sessionNotificationServiceClass = SessionNotificationService::class,
permissionRequestHandler = { requestNotificationPermission() },
).also {
it.start()
}
}

private fun requestNotificationPermission() {
privateNotificationObserver.stop()

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
notificationPermission.launch(POST_NOTIFICATIONS)
}
}

private fun setSplashScreenPreDrawListener(safeIntent: SafeIntent) {
Expand All @@ -161,7 +192,12 @@ open class MainActivity : LocaleAwareAppCompatActivity() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N && isInPictureInPictureMode && intent != null) {
// Exit PiP mode
moveTaskToBack(false)
startActivity(Intent(this, this::class.java).setFlags(Intent.FLAG_ACTIVITY_REORDER_TO_FRONT))
startActivity(
Intent(
this,
this::class.java,
).setFlags(Intent.FLAG_ACTIVITY_REORDER_TO_FRONT),
)
}
}

Expand Down Expand Up @@ -270,7 +306,11 @@ open class MainActivity : LocaleAwareAppCompatActivity() {
components.tabsUseCases.removeAllTabs()

if (fromNotificationAction) {
Notifications.eraseOpenButtonTapped.record(Notifications.EraseOpenButtonTappedExtra(tabCount))
Notifications.eraseOpenButtonTapped.record(
Notifications.EraseOpenButtonTappedExtra(
tabCount,
),
)
}

if (fromShortcut) {
Expand All @@ -280,7 +320,12 @@ open class MainActivity : LocaleAwareAppCompatActivity() {
}
}

override fun onCreateView(parent: View?, name: String, context: Context, attrs: AttributeSet): View? {
override fun onCreateView(
parent: View?,
name: String,
context: Context,
attrs: AttributeSet,
): View? {
return if (name == EngineView::class.java.name) {
components.engine.createView(context, attrs).asView()
} else {
Expand Down Expand Up @@ -409,6 +454,7 @@ open class MainActivity : LocaleAwareAppCompatActivity() {
override fun onDestroy() {
super.onDestroy()
_binding = null
privateNotificationObserver.stop()
}

enum class AppOpenType(val type: String) {
Expand Down

This file was deleted.

@@ -0,0 +1,52 @@
/* 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.focus.session

import android.content.Context
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.cancel
import kotlinx.coroutines.flow.map
import mozilla.components.browser.state.selector.privateTabs
import mozilla.components.browser.state.store.BrowserStore
import mozilla.components.lib.state.ext.flowScoped
import mozilla.components.support.base.feature.LifecycleAwareFeature
import mozilla.components.support.ktx.kotlinx.coroutines.flow.ifChanged
import kotlin.reflect.KClass

/**
* Responsible for starting or stopping up a [SessionNotificationService]
* depending on whether a private tab is opened.
*
* @param browserStore Browser store reference used to observe the number of private tabs.
* @param sessionNotificationServiceClass The service sub-class that creates and controls the notification.
*/
class PrivateNotificationFeature(
context: Context,
private val browserStore: BrowserStore,
private val sessionNotificationServiceClass: KClass<SessionNotificationService>,
private val permissionRequestHandler: (() -> Unit),
) : LifecycleAwareFeature {

private val applicationContext = context.applicationContext
private var scope: CoroutineScope? = null

override fun start() {
browserStore.flowScoped { flow ->
flow.map { state -> state.privateTabs.isNotEmpty() }
.ifChanged()
.collect { hasPrivateTabs ->
if (hasPrivateTabs) {
SessionNotificationService.start(applicationContext, permissionRequestHandler)
} else {
SessionNotificationService.stop(applicationContext)
}
}
}
}

override fun stop() {
scope?.cancel()
}
}
Expand Up @@ -13,9 +13,11 @@ import android.content.Context
import android.content.Intent
import android.os.Build
import android.os.IBinder
import android.os.RemoteException
import androidx.core.app.NotificationCompat
import androidx.core.content.ContextCompat
import mozilla.components.service.glean.private.NoExtras
import mozilla.components.support.base.log.logger.Logger
import mozilla.components.support.utils.ThreadUtils
import mozilla.components.support.utils.ext.stopForegroundCompat
import org.mozilla.focus.GleanMetrics.Notifications
Expand All @@ -33,13 +35,34 @@ class SessionNotificationService : Service() {

private var shouldSendTaskRemovedTelemetry = true

private val notificationManager: NotificationManager by lazy {
applicationContext.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
}

override fun onStartCommand(intent: Intent, flags: Int, startId: Int): Int {
val action = intent.action ?: return START_NOT_STICKY

when (action) {
ACTION_START -> {
createNotificationChannelIfNeeded()
startForeground(NOTIFICATION_ID, buildNotification())
// Notifications permission is only needed on Android 13 devices and later.
val areNotificationsEnabled =
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
try {
notificationManager.areNotificationsEnabled()
} catch (e: RemoteException) {
Logger.warn("Failed to check notifications state", e)
false
}
} else {
true
}

if (areNotificationsEnabled) {
createNotificationChannelIfNeeded()
startForeground(NOTIFICATION_ID, buildNotification())
} else {
permissionHandler?.invoke()
}
}

ACTION_ERASE -> {
Expand Down Expand Up @@ -110,23 +133,26 @@ class SessionNotificationService : Service() {
}

private fun createNotificationIntent(): PendingIntent {
val notificationIntentFlags = IntentUtils.defaultIntentPendingFlags or PendingIntent.FLAG_ONE_SHOT
val notificationIntentFlags =
IntentUtils.defaultIntentPendingFlags or PendingIntent.FLAG_ONE_SHOT
val intent = Intent(this, SessionNotificationService::class.java)
intent.action = ACTION_ERASE

return PendingIntent.getService(this, 0, intent, notificationIntentFlags)
}

private fun createOpenActionIntent(): PendingIntent {
val openActionIntentFlags = IntentUtils.defaultIntentPendingFlags or PendingIntent.FLAG_UPDATE_CURRENT
val openActionIntentFlags =
IntentUtils.defaultIntentPendingFlags or PendingIntent.FLAG_UPDATE_CURRENT
val intent = Intent(this, MainActivity::class.java)
intent.action = MainActivity.ACTION_OPEN

return PendingIntent.getActivity(this, 1, intent, openActionIntentFlags)
}

private fun createOpenAndEraseActionIntent(): PendingIntent {
val openAndEraseActionIntentFlags = IntentUtils.defaultIntentPendingFlags or PendingIntent.FLAG_UPDATE_CURRENT
val openAndEraseActionIntentFlags =
IntentUtils.defaultIntentPendingFlags or PendingIntent.FLAG_UPDATE_CURRENT
val intent = Intent(this, MainActivity::class.java)

intent.action = MainActivity.ACTION_ERASE
Expand All @@ -142,8 +168,6 @@ class SessionNotificationService : Service() {
return
}

val notificationManager = getSystemService(NotificationManager::class.java) ?: return

val notificationChannelName = getString(R.string.notification_browsing_session_channel_name)
val notificationChannelDescription = getString(
R.string.notification_browsing_session_channel_description,
Expand All @@ -169,15 +193,17 @@ class SessionNotificationService : Service() {
}

companion object {
private var permissionHandler: (() -> Unit)? = null
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) {
internal fun start(context: Context, permissionHandler: (() -> Unit)) {
val intent = Intent(context, SessionNotificationService::class.java)
intent.action = ACTION_START
this.permissionHandler = permissionHandler

// For #2901: The application is crashing due to the service not calling `startForeground`
// before it times out. so this is a speculative fix to decrease the time between these two
Expand Down
2 changes: 1 addition & 1 deletion service-telemetry/build.gradle
Expand Up @@ -10,7 +10,7 @@ android {

defaultConfig {
minSdkVersion 21
targetSdkVersion 32
targetSdkVersion 33
}

buildTypes {
Expand Down