Allow copy version & package name in App Info page
Fix: 312095344
Test: manual - on "App info" page
Test: unit test
Change-Id: I0dd7c9a4ccc34375cb3dac62115b736dfcffe76a
diff --git a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/GallerySpaEnvironment.kt b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/GallerySpaEnvironment.kt
index b1e1585..90c7d46 100644
--- a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/GallerySpaEnvironment.kt
+++ b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/GallerySpaEnvironment.kt
@@ -49,6 +49,7 @@
import com.android.settingslib.spa.gallery.preference.TwoTargetSwitchPreferencePageProvider
import com.android.settingslib.spa.gallery.scaffold.SearchScaffoldPageProvider
import com.android.settingslib.spa.gallery.ui.CategoryPageProvider
+import com.android.settingslib.spa.gallery.ui.CopyablePageProvider
import com.android.settingslib.spa.gallery.ui.SpinnerPageProvider
import com.android.settingslib.spa.slice.SpaSliceBroadcastReceiver
@@ -100,6 +101,7 @@
SettingsTextFieldPasswordPageProvider,
SearchScaffoldPageProvider,
CardPageProvider,
+ CopyablePageProvider,
),
rootPages = listOf(
HomePageProvider.createSettingsPage(),
diff --git a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/home/HomePageProvider.kt b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/home/HomePageProvider.kt
index f52ceec..1d897f7 100644
--- a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/home/HomePageProvider.kt
+++ b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/home/HomePageProvider.kt
@@ -44,6 +44,7 @@
import com.android.settingslib.spa.gallery.preference.PreferenceMainPageProvider
import com.android.settingslib.spa.gallery.scaffold.SearchScaffoldPageProvider
import com.android.settingslib.spa.gallery.ui.CategoryPageProvider
+import com.android.settingslib.spa.gallery.ui.CopyablePageProvider
import com.android.settingslib.spa.gallery.ui.SpinnerPageProvider
import com.android.settingslib.spa.widget.scaffold.HomeScaffold
@@ -71,6 +72,7 @@
AlertDialogPageProvider.buildInjectEntry().setLink(fromPage = owner).build(),
EditorMainPageProvider.buildInjectEntry().setLink(fromPage = owner).build(),
CardPageProvider.buildInjectEntry().setLink(fromPage = owner).build(),
+ CopyablePageProvider.buildInjectEntry().setLink(fromPage = owner).build(),
)
}
diff --git a/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/ui/CopyablePageProvider.kt b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/ui/CopyablePageProvider.kt
new file mode 100644
index 0000000..f897d8c
--- /dev/null
+++ b/packages/SettingsLib/Spa/gallery/src/com/android/settingslib/spa/gallery/ui/CopyablePageProvider.kt
@@ -0,0 +1,61 @@
+/*
+ * 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.settingslib.spa.gallery.ui
+
+import android.os.Bundle
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.padding
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import com.android.settingslib.spa.framework.common.EntrySearchData
+import com.android.settingslib.spa.framework.common.SettingsEntryBuilder
+import com.android.settingslib.spa.framework.common.SettingsPageProvider
+import com.android.settingslib.spa.framework.common.createSettingsPage
+import com.android.settingslib.spa.framework.compose.navigator
+import com.android.settingslib.spa.framework.theme.SettingsDimension
+import com.android.settingslib.spa.widget.preference.Preference
+import com.android.settingslib.spa.widget.preference.PreferenceModel
+import com.android.settingslib.spa.widget.scaffold.RegularScaffold
+import com.android.settingslib.spa.widget.ui.CopyableBody
+
+private const val TITLE = "Sample Copyable"
+
+object CopyablePageProvider : SettingsPageProvider {
+ override val name = "Copyable"
+
+ private val owner = createSettingsPage()
+
+ fun buildInjectEntry(): SettingsEntryBuilder {
+ return SettingsEntryBuilder.createInject(owner)
+ .setUiLayoutFn {
+ Preference(object : PreferenceModel {
+ override val title = TITLE
+ override val onClick = navigator(name)
+ })
+ }
+ .setSearchDataFn { EntrySearchData(title = TITLE) }
+ }
+
+ @Composable
+ override fun Page(arguments: Bundle?) {
+ RegularScaffold(title = TITLE) {
+ Box(modifier = Modifier.padding(SettingsDimension.itemPadding)) {
+ CopyableBody(body = "Copyable body")
+ }
+ }
+ }
+}
diff --git a/packages/SettingsLib/Spa/gradle/libs.versions.toml b/packages/SettingsLib/Spa/gradle/libs.versions.toml
index 905640f..b40e911 100644
--- a/packages/SettingsLib/Spa/gradle/libs.versions.toml
+++ b/packages/SettingsLib/Spa/gradle/libs.versions.toml
@@ -15,7 +15,7 @@
#
[versions]
-agp = "8.1.3"
+agp = "8.1.4"
compose-compiler = "1.5.1"
dexmaker-mockito = "2.28.3"
jvm = "17"
diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/ui/CopyableBody.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/ui/CopyableBody.kt
new file mode 100644
index 0000000..930d0a1
--- /dev/null
+++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/ui/CopyableBody.kt
@@ -0,0 +1,102 @@
+/*
+ * 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.settingslib.spa.widget.ui
+
+import androidx.compose.foundation.gestures.detectTapGestures
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.padding
+import androidx.compose.material3.DropdownMenu
+import androidx.compose.material3.DropdownMenuItem
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.MenuDefaults
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.input.pointer.pointerInput
+import androidx.compose.ui.platform.LocalClipboardManager
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.text.AnnotatedString
+import androidx.compose.ui.unit.DpOffset
+import com.android.settingslib.spa.framework.theme.SettingsDimension
+import com.android.settingslib.spa.framework.theme.SettingsTheme
+
+@Composable
+fun CopyableBody(body: String) {
+ var expanded by remember { mutableStateOf(false) }
+ var dpOffset by remember { mutableStateOf(DpOffset.Unspecified) }
+
+ Box(modifier = Modifier
+ .fillMaxWidth()
+ .pointerInput(Unit) {
+ detectTapGestures(
+ onLongPress = {
+ dpOffset = DpOffset(it.x.toDp(), it.y.toDp())
+ expanded = true
+ },
+ )
+ }
+ ) {
+ SettingsBody(body)
+
+ DropdownMenu(
+ expanded = expanded,
+ onDismissRequest = { expanded = false },
+ offset = dpOffset,
+ ) {
+ DropdownMenuTitle(body)
+ DropdownMenuCopy(body) { expanded = false }
+ }
+ }
+}
+
+@Composable
+private fun DropdownMenuTitle(text: String) {
+ Text(
+ text = text,
+ modifier = Modifier
+ .padding(MenuDefaults.DropdownMenuItemContentPadding)
+ .padding(
+ top = SettingsDimension.itemPaddingAround,
+ bottom = SettingsDimension.buttonPaddingVertical,
+ ),
+ color = SettingsTheme.colorScheme.categoryTitle,
+ style = MaterialTheme.typography.labelMedium,
+ )
+}
+
+@Composable
+private fun DropdownMenuCopy(body: String, onCopy: () -> Unit) {
+ val clipboardManager = LocalClipboardManager.current
+ DropdownMenuItem(
+ text = {
+ Text(
+ text = stringResource(android.R.string.copy),
+ color = MaterialTheme.colorScheme.onSurface,
+ style = MaterialTheme.typography.bodyLarge,
+ )
+ },
+ onClick = {
+ onCopy()
+ clipboardManager.setText(AnnotatedString(body))
+ }
+ )
+}
diff --git a/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/widget/ui/CopyableBodyTest.kt b/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/widget/ui/CopyableBodyTest.kt
new file mode 100644
index 0000000..71072a5
--- /dev/null
+++ b/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/widget/ui/CopyableBodyTest.kt
@@ -0,0 +1,83 @@
+/*
+ * 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.settingslib.spa.widget.ui
+
+import android.content.ClipData
+import android.content.ClipboardManager
+import android.content.Context
+import androidx.compose.ui.test.assertIsDisplayed
+import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.test.longClick
+import androidx.compose.ui.test.onNodeWithText
+import androidx.compose.ui.test.performClick
+import androidx.compose.ui.test.performTouchInput
+import androidx.test.core.app.ApplicationProvider
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import com.google.common.truth.Truth.assertThat
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+class CopyableBodyTest {
+ @get:Rule
+ val composeTestRule = createComposeRule()
+
+ private val context: Context = ApplicationProvider.getApplicationContext()
+
+ @Test
+ fun text_isDisplayed() {
+ composeTestRule.setContent {
+ CopyableBody(TEXT)
+ }
+
+ composeTestRule.onNodeWithText(TEXT).assertIsDisplayed()
+ }
+
+ @Test
+ fun onLongPress_contextMenuDisplayed() {
+ composeTestRule.setContent {
+ CopyableBody(TEXT)
+ }
+
+ composeTestRule.onNodeWithText(TEXT).performTouchInput {
+ longClick()
+ }
+
+ composeTestRule.onNodeWithText(context.getString(android.R.string.copy)).assertIsDisplayed()
+ }
+
+ @Test
+ fun onCopy_saveToClipboard() {
+ val clipboardManager = context.getSystemService(ClipboardManager::class.java)!!
+ clipboardManager.setPrimaryClip(ClipData.newPlainText("", ""))
+ composeTestRule.setContent {
+ CopyableBody(TEXT)
+ }
+
+ composeTestRule.onNodeWithText(TEXT).performTouchInput {
+ longClick()
+ }
+ composeTestRule.onNodeWithText(context.getString(android.R.string.copy)).performClick()
+
+ assertThat(clipboardManager.primaryClip!!.getItemAt(0).text.toString()).isEqualTo(TEXT)
+ }
+
+ private companion object {
+ const val TEXT = "Text"
+ }
+}
diff --git a/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/app/AppInfo.kt b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/app/AppInfo.kt
index fc10a27..45295b0 100644
--- a/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/app/AppInfo.kt
+++ b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/app/AppInfo.kt
@@ -36,10 +36,10 @@
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.unit.Dp
-import androidx.compose.ui.unit.dp
import com.android.settingslib.development.DevelopmentSettingsEnabler
import com.android.settingslib.spa.framework.compose.rememberDrawablePainter
import com.android.settingslib.spa.framework.theme.SettingsDimension
+import com.android.settingslib.spa.widget.ui.CopyableBody
import com.android.settingslib.spa.widget.ui.SettingsBody
import com.android.settingslib.spa.widget.ui.SettingsTitle
import com.android.settingslib.spaprivileged.R
@@ -71,26 +71,38 @@
@Composable
private fun InstallType(app: ApplicationInfo) {
if (!app.isInstantApp) return
- Spacer(modifier = Modifier.height(4.dp))
- SettingsBody(stringResource(com.android.settingslib.widget.preference.app.R.string.install_type_instant))
+ Spacer(modifier = Modifier.height(SettingsDimension.paddingSmall))
+ SettingsBody(
+ stringResource(
+ com.android.settingslib.widget.preference.app.R.string.install_type_instant
+ )
+ )
}
@Composable
private fun AppVersion() {
- if (packageInfo.versionName == null) return
- Spacer(modifier = Modifier.height(4.dp))
- SettingsBody(packageInfo.versionNameBidiWrapped)
+ val versionName = packageInfo.versionNameBidiWrapped ?: return
+ Spacer(modifier = Modifier.height(SettingsDimension.paddingSmall))
+ SettingsBody(versionName)
}
@Composable
fun FooterAppVersion(showPackageName: Boolean = rememberIsDevelopmentSettingsEnabled()) {
- if (packageInfo.versionName == null) return
+ val context = LocalContext.current
+ val footer = remember(showPackageName) {
+ val list = mutableListOf<String>()
+ packageInfo.versionNameBidiWrapped?.let {
+ list += context.getString(R.string.version_text, it)
+ }
+ if (showPackageName) {
+ list += packageInfo.packageName
+ }
+ list.joinToString(separator = System.lineSeparator())
+ }
+ if (footer.isBlank()) return
HorizontalDivider()
Column(modifier = Modifier.padding(SettingsDimension.itemPadding)) {
- SettingsBody(stringResource(R.string.version_text, packageInfo.versionNameBidiWrapped))
- if (showPackageName) {
- SettingsBody(packageInfo.packageName)
- }
+ CopyableBody(footer)
}
}
@@ -104,7 +116,7 @@
private companion object {
/** Wrapped the version name, so its directionality still keep same when RTL. */
- val PackageInfo.versionNameBidiWrapped: String
+ val PackageInfo.versionNameBidiWrapped: String?
get() = BidiFormatter.getInstance().unicodeWrap(versionName)
}
}
diff --git a/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/template/app/AppInfoTest.kt b/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/template/app/AppInfoTest.kt
index ab34f68..72a5bd7 100644
--- a/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/template/app/AppInfoTest.kt
+++ b/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/template/app/AppInfoTest.kt
@@ -105,7 +105,8 @@
}
}
- composeTestRule.onNodeWithText("version $VERSION_NAME").assertIsDisplayed()
+ composeTestRule.onNodeWithText(text = "version $VERSION_NAME", substring = true)
+ .assertIsDisplayed()
}
@Test
@@ -119,10 +120,10 @@
composeTestRule.setContent {
CompositionLocalProvider(LocalContext provides context) {
- appInfoProvider.FooterAppVersion(true)
+ appInfoProvider.FooterAppVersion(showPackageName = true)
}
}
- composeTestRule.onNodeWithText(PACKAGE_NAME).assertIsDisplayed()
+ composeTestRule.onNodeWithText(text = PACKAGE_NAME, substring = true).assertIsDisplayed()
}
@@ -137,10 +138,10 @@
composeTestRule.setContent {
CompositionLocalProvider(LocalContext provides context) {
- appInfoProvider.FooterAppVersion(false)
+ appInfoProvider.FooterAppVersion(showPackageName = false)
}
}
- composeTestRule.onNodeWithText(PACKAGE_NAME).assertDoesNotExist()
+ composeTestRule.onNodeWithText(text = PACKAGE_NAME, substring = true).assertDoesNotExist()
}
private companion object {