Merge "Move launch button from 3-buttons panel to the top right corner" into main
diff --git a/src/com/android/settings/spa/app/appinfo/AppButtons.kt b/src/com/android/settings/spa/app/appinfo/AppButtons.kt
index 3200b81..307ff11 100644
--- a/src/com/android/settings/spa/app/appinfo/AppButtons.kt
+++ b/src/com/android/settings/spa/app/appinfo/AppButtons.kt
@@ -17,6 +17,8 @@
package com.android.settings.spa.app.appinfo
import android.content.pm.ApplicationInfo
+import android.content.pm.FeatureFlags
+import android.content.pm.FeatureFlagsImpl
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.lifecycle.compose.collectAsStateWithLifecycle
@@ -25,16 +27,22 @@
import com.android.settingslib.spa.widget.button.ActionButtons
@Composable
-fun AppButtons(packageInfoPresenter: PackageInfoPresenter) {
+/**
+ * @param featureFlags can be overridden in tests
+ */
+fun AppButtons(packageInfoPresenter: PackageInfoPresenter, featureFlags: FeatureFlags = FeatureFlagsImpl()) {
if (remember(packageInfoPresenter) { packageInfoPresenter.isMainlineModule() }) return
- val presenter = remember { AppButtonsPresenter(packageInfoPresenter) }
+ val presenter = remember { AppButtonsPresenter(packageInfoPresenter, featureFlags) }
ActionButtons(actionButtons = presenter.getActionButtons())
}
private fun PackageInfoPresenter.isMainlineModule(): Boolean =
AppUtils.isMainlineModule(userPackageManager, packageName)
-private class AppButtonsPresenter(private val packageInfoPresenter: PackageInfoPresenter) {
+private class AppButtonsPresenter(
+ private val packageInfoPresenter: PackageInfoPresenter,
+ private val featureFlags: FeatureFlags
+) {
private val appLaunchButton = AppLaunchButton(packageInfoPresenter)
private val appInstallButton = AppInstallButton(packageInfoPresenter)
private val appDisableButton = AppDisableButton(packageInfoPresenter)
@@ -50,7 +58,7 @@
@Composable
private fun getActionButtons(app: ApplicationInfo): List<ActionButton> = listOfNotNull(
- appLaunchButton.getActionButton(app),
+ if (featureFlags.archiving()) null else appLaunchButton.getActionButton(app),
appInstallButton.getActionButton(app),
appDisableButton.getActionButton(app),
appUninstallButton.getActionButton(app),
diff --git a/src/com/android/settings/spa/app/appinfo/AppInfoSettings.kt b/src/com/android/settings/spa/app/appinfo/AppInfoSettings.kt
index a9d16ae..ed912c3 100644
--- a/src/com/android/settings/spa/app/appinfo/AppInfoSettings.kt
+++ b/src/com/android/settings/spa/app/appinfo/AppInfoSettings.kt
@@ -18,6 +18,8 @@
import android.app.settings.SettingsEnums
import android.content.pm.ApplicationInfo
+import android.content.pm.FeatureFlags
+import android.content.pm.FeatureFlagsImpl
import android.os.Bundle
import android.os.UserHandle
import android.util.FeatureFlagUtils
@@ -119,9 +121,11 @@
LifecycleEffect(onStart = { packageInfoPresenter.reloadPackageInfo() })
val packageInfo = packageInfoPresenter.flow.collectAsStateWithLifecycle().value ?: return
val app = checkNotNull(packageInfo.applicationInfo)
+ val featureFlags: FeatureFlags = FeatureFlagsImpl()
RegularScaffold(
title = stringResource(R.string.application_info_label),
actions = {
+ if (featureFlags.archiving()) TopBarAppLaunchButton(packageInfoPresenter, app)
AppInfoSettingsMoreOptions(packageInfoPresenter, app)
}
) {
diff --git a/src/com/android/settings/spa/app/appinfo/TopBarAppLaunchButton.kt b/src/com/android/settings/spa/app/appinfo/TopBarAppLaunchButton.kt
new file mode 100644
index 0000000..92ad139
--- /dev/null
+++ b/src/com/android/settings/spa/app/appinfo/TopBarAppLaunchButton.kt
@@ -0,0 +1,59 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settings.spa.app.appinfo
+
+import android.content.ActivityNotFoundException
+import android.content.Intent
+import android.content.pm.ApplicationInfo
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.automirrored.outlined.Launch
+import androidx.compose.material3.Icon
+import androidx.compose.material3.IconButton
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.res.stringResource
+import com.android.settings.R
+import com.android.settingslib.spaprivileged.model.app.userHandle
+
+@Composable
+fun TopBarAppLaunchButton(packageInfoPresenter: PackageInfoPresenter, app: ApplicationInfo) {
+ val intent = packageInfoPresenter.launchIntent(app = app) ?: return
+ IconButton({ launchButtonAction(intent, app, packageInfoPresenter) }) {
+ Icon(
+ imageVector = Icons.AutoMirrored.Outlined.Launch,
+ contentDescription = stringResource(R.string.launch_instant_app),
+ )
+ }
+}
+
+private fun PackageInfoPresenter.launchIntent(
+ app: ApplicationInfo
+): Intent? {
+ return userPackageManager.getLaunchIntentForPackage(app.packageName)
+}
+
+private fun launchButtonAction(
+ intent: Intent,
+ app: ApplicationInfo,
+ packageInfoPresenter: PackageInfoPresenter
+) {
+ try {
+ packageInfoPresenter.context.startActivityAsUser(intent, app.userHandle)
+ } catch (_: ActivityNotFoundException) {
+ // Only happens after package changes like uninstall, and before page auto refresh or
+ // close, so ignore this exception is safe.
+ }
+}
\ No newline at end of file
diff --git a/tests/spa_unit/src/com/android/settings/spa/app/appinfo/AppButtonsTest.kt b/tests/spa_unit/src/com/android/settings/spa/app/appinfo/AppButtonsTest.kt
index 8faf5c9..e2f55ef 100644
--- a/tests/spa_unit/src/com/android/settings/spa/app/appinfo/AppButtonsTest.kt
+++ b/tests/spa_unit/src/com/android/settings/spa/app/appinfo/AppButtonsTest.kt
@@ -17,16 +17,21 @@
package com.android.settings.spa.app.appinfo
import android.content.Context
+import android.content.Intent
import android.content.pm.ApplicationInfo
+import android.content.pm.FakeFeatureFlagsImpl
+import android.content.pm.Flags
import android.content.pm.PackageInfo
import android.content.pm.PackageManager
import androidx.compose.ui.test.assertIsDisplayed
import androidx.compose.ui.test.assertIsNotDisplayed
import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.test.onNodeWithText
import androidx.compose.ui.test.onRoot
import androidx.test.core.app.ApplicationProvider
import androidx.test.ext.junit.runners.AndroidJUnit4
import com.android.dx.mockito.inline.extended.ExtendedMockito
+import com.android.settings.R
import com.android.settingslib.applications.AppUtils
import com.android.settingslib.spa.testutils.delay
import kotlinx.coroutines.flow.MutableStateFlow
@@ -57,6 +62,8 @@
@Mock
private lateinit var packageManager: PackageManager
+ private val featureFlags = FakeFeatureFlagsImpl()
+
@Before
fun setUp() {
mockSession = ExtendedMockito.mockitoSession()
@@ -69,6 +76,7 @@
whenever(packageInfoPresenter.userPackageManager).thenReturn(packageManager)
whenever(packageManager.getPackageInfo(PACKAGE_NAME, 0)).thenReturn(PACKAGE_INFO)
whenever(AppUtils.isMainlineModule(packageManager, PACKAGE_NAME)).thenReturn(false)
+ featureFlags.setFlag(Flags.FLAG_ARCHIVING, true)
}
@After
@@ -92,10 +100,28 @@
composeTestRule.onRoot().assertIsDisplayed()
}
+ @Test
+ fun launchButton_displayed_archivingDisabled() {
+ whenever(packageManager.getLaunchIntentForPackage(PACKAGE_NAME)).thenReturn(Intent())
+ featureFlags.setFlag(Flags.FLAG_ARCHIVING, false)
+ setContent()
+
+ composeTestRule.onNodeWithText(context.getString(R.string.launch_instant_app)).assertIsDisplayed()
+ }
+
+ @Test
+ fun launchButton_notDisplayed_archivingEnabled() {
+ whenever(packageManager.getLaunchIntentForPackage(PACKAGE_NAME)).thenReturn(Intent())
+ featureFlags.setFlag(Flags.FLAG_ARCHIVING, true)
+ setContent()
+
+ composeTestRule.onNodeWithText(context.getString(R.string.launch_instant_app)).assertIsNotDisplayed()
+ }
+
private fun setContent() {
whenever(packageInfoPresenter.flow).thenReturn(MutableStateFlow(PACKAGE_INFO))
composeTestRule.setContent {
- AppButtons(packageInfoPresenter)
+ AppButtons(packageInfoPresenter, featureFlags)
}
composeTestRule.delay()
diff --git a/tests/spa_unit/src/com/android/settings/spa/app/appinfo/TopBarAppLaunchButtonTest.kt b/tests/spa_unit/src/com/android/settings/spa/app/appinfo/TopBarAppLaunchButtonTest.kt
new file mode 100644
index 0000000..7b54247
--- /dev/null
+++ b/tests/spa_unit/src/com/android/settings/spa/app/appinfo/TopBarAppLaunchButtonTest.kt
@@ -0,0 +1,119 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.settings.spa.app.appinfo
+
+import android.content.Context
+import android.content.Intent
+import android.content.pm.ApplicationInfo
+import android.content.pm.PackageManager
+import androidx.compose.runtime.CompositionLocalProvider
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.test.hasContentDescription
+import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.test.onNodeWithContentDescription
+import androidx.compose.ui.test.performClick
+import androidx.test.core.app.ApplicationProvider
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import com.android.dx.mockito.inline.extended.ExtendedMockito
+import com.android.settings.R
+import com.android.settingslib.spa.testutils.waitUntilExists
+import com.android.settingslib.spaprivileged.model.app.userHandle
+import org.junit.After
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.ArgumentMatchers.any
+import org.mockito.ArgumentMatchers.eq
+import org.mockito.Mock
+import org.mockito.Mockito.verify
+import org.mockito.MockitoSession
+import org.mockito.Spy
+import org.mockito.quality.Strictness
+import org.mockito.Mockito.`when` as whenever
+
+@RunWith(AndroidJUnit4::class)
+class TopBarAppLaunchButtonTest {
+ @get:Rule
+ val composeTestRule = createComposeRule()
+
+ private lateinit var mockSession: MockitoSession
+
+ @Spy
+ private val context: Context = ApplicationProvider.getApplicationContext()
+
+ @Mock
+ private lateinit var packageInfoPresenter: PackageInfoPresenter
+
+ @Mock
+ private lateinit var userPackageManager: PackageManager
+
+ @Before
+ fun setUp() {
+ mockSession = ExtendedMockito.mockitoSession()
+ .initMocks(this)
+ .strictness(Strictness.LENIENT)
+ .startMocking()
+ whenever(packageInfoPresenter.context).thenReturn(context)
+ whenever(packageInfoPresenter.userPackageManager).thenReturn(userPackageManager)
+ val intent = Intent()
+ whenever(userPackageManager.getLaunchIntentForPackage(PACKAGE_NAME)).thenReturn(intent)
+ }
+
+ @After
+ fun tearDown() {
+ mockSession.finishMocking()
+ }
+
+ @Test
+ fun topBarAppLaunchButton_isDisplayed() {
+ val app = ApplicationInfo().apply {
+ packageName = PACKAGE_NAME
+ }
+
+ setContent(app)
+
+ composeTestRule.waitUntilExists(
+ hasContentDescription(context.getString(R.string.launch_instant_app))
+ )
+ }
+
+ @Test
+ fun topBarAppLaunchButton_opensApp() {
+ val app = ApplicationInfo().apply {
+ packageName = PACKAGE_NAME
+ }
+
+ setContent(app)
+ composeTestRule.onNodeWithContentDescription(context.getString(R.string.launch_instant_app))
+ .performClick()
+
+ verify(context).startActivityAsUser(any(), eq(app.userHandle))
+ }
+
+ private fun setContent(app: ApplicationInfo) {
+ composeTestRule.setContent {
+ CompositionLocalProvider(LocalContext provides context) {
+ TopBarAppLaunchButton(packageInfoPresenter, app)
+ }
+ }
+ }
+
+ private companion object {
+ const val PACKAGE_NAME = "package.name"
+ }
+}