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