diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json
new file mode 100644
index 000000000..39bbd2681
--- /dev/null
+++ b/.devcontainer/devcontainer.json
@@ -0,0 +1,4 @@
+{
+ "image": "mcr.microsoft.com/devcontainers/universal:2",
+ "features": {}
+}
diff --git a/.github/workflows/build-snapshot.yml b/.github/workflows/build-snapshot.yml
index 9f09e694d..4c8f16d2f 100644
--- a/.github/workflows/build-snapshot.yml
+++ b/.github/workflows/build-snapshot.yml
@@ -34,7 +34,9 @@ jobs:
java-version: 17
- name: Decrypt secrets
- run: release/signing-setup.sh ${{ secrets.ENCRYPT_KEY }}
+ run: release/signing-setup.sh
+ env:
+ ENCRYPT_KEY: ${{ secrets.ENCRYPT_KEY }}
- name: Generate cache key
run: ./checksum.sh checksum.txt
@@ -105,7 +107,9 @@ jobs:
java-version: 17
- name: Decrypt secrets
- run: release/signing-setup.sh ${{ secrets.ENCRYPT_KEY }}
+ run: release/signing-setup.sh
+ env:
+ ENCRYPT_KEY: ${{ secrets.ENCRYPT_KEY }}
- name: Generate cache key
run: ./checksum.sh checksum.txt
@@ -180,7 +184,9 @@ jobs:
java-version: 17
- name: Decrypt secrets
- run: release/signing-setup.sh ${{ secrets.ENCRYPT_KEY }}
+ run: release/signing-setup.sh
+ env:
+ ENCRYPT_KEY: ${{ secrets.ENCRYPT_KEY }}
- name: Generate cache key
run: ./checksum.sh checksum.txt
diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index bcbba65e5..56967c264 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -39,7 +39,9 @@ jobs:
java-version: 17
- name: Decrypt secrets
- run: release/signing-setup.sh ${{ secrets.ENCRYPT_KEY }}
+ run: release/signing-setup.sh
+ env:
+ ENCRYPT_KEY: ${{ secrets.ENCRYPT_KEY }}
- name: Setup Gradle
uses: gradle/gradle-build-action@v2
@@ -109,7 +111,9 @@ jobs:
java-version: 17
- name: Decrypt secrets
- run: release/signing-setup.sh ${{ secrets.ENCRYPT_KEY }}
+ run: release/signing-setup.sh
+ env:
+ ENCRYPT_KEY: ${{ secrets.ENCRYPT_KEY }}
- name: Setup Gradle
uses: gradle/gradle-build-action@v2
@@ -187,7 +191,9 @@ jobs:
java-version: 17
- name: Decrypt secrets
- run: release/signing-setup.sh ${{ secrets.ENCRYPT_KEY }}
+ run: release/signing-setup.sh
+ env:
+ ENCRYPT_KEY: ${{ secrets.ENCRYPT_KEY }}
- name: Setup Gradle
uses: gradle/gradle-build-action@v2
diff --git a/.jules/palette.md b/.jules/palette.md
new file mode 100644
index 000000000..d6c47a7f7
--- /dev/null
+++ b/.jules/palette.md
@@ -0,0 +1,11 @@
+## 2024-05-22 - [Permission Samples UX]
+**Learning:** Enhancing permission rationale screens with illustrative icons and specific action-oriented button labels (e.g., "Allow camera access" vs. "Request permission") improves user trust and clarity. Using `Arrangement.spacedBy` in `Column` ensures consistent visual rhythm without manual spacers.
+**Action:** Use `Icons.Default.Face` for rationale prompts and `Icons.Default.Done` with success colors for granted states in Compose-based permission flows.
+
+## 2024-06-12 - [Hierarchical Sample Navigation]
+**Learning:** In sample apps with nested categories, static TopAppBar titles and missing back buttons disorient users. Contextual titles (last segment of path) and a standard back icon improve orientation and navigation flow.
+**Action:** Use `Icons.AutoMirrored.Filled.ArrowBack` for back buttons and always add `Role.Button` to clickable list items for accessibility.
+
+## 2024-12-19 - [Navigation Feedback in Samples]
+**Learning:** Interactive navigation components (NavRail, NavDrawer) in sample apps feel "broken" if they don't provide immediate feedback in the main content area. Lifting selection state and displaying the selected item's identity (icon/label) validates user interaction.
+**Action:** Always provide a visual response in the main content area when a navigation item is selected, and set `contentDescription = null` for icons accompanied by labels to avoid redundant screen reader announcements.
diff --git a/.jules/sentinel.md b/.jules/sentinel.md
new file mode 100644
index 000000000..f6b4e8f7e
--- /dev/null
+++ b/.jules/sentinel.md
@@ -0,0 +1,14 @@
+## 2025-05-14 - [Activity Injection in Sample App Launcher]
+**Vulnerability:** The sample application's `MainActivity` used `queryIntentActivities` to dynamically populate its list of samples based on an intent category (`com.google.accompanist.sample.SAMPLE_CODE`) without verifying the package name of the returned activities.
+**Learning:** Malicious external applications could register activities with the same category and inject themselves into the sample app's UI, potentially leading to phishing or UI redressing attacks.
+**Prevention:** Always verify that `info.activityInfo.packageName` matches the application's own `packageName` when dynamically loading components from `PackageManager` based on shared intent categories.
+
+## 2024-05-15 - [Insecure Hashing and Shell Script Weakness]
+**Vulnerability:** The `checksum.sh` script used MD5 for integrity checks and lacked variable quoting, making it susceptible to collisions and word-splitting vulnerabilities.
+**Learning:** Legacy scripts often use older hashing algorithms like MD5 which are cryptographically broken. Additionally, unquoted variables in shell scripts can lead to unintended execution or script failure if paths contain special characters.
+**Prevention:** Use SHA-256 for all integrity and security-sensitive hashing. Always wrap shell variables in double quotes (e.g., `"$VAR"`) to prevent word-splitting and globbing attacks.
+
+## 2024-05-16 - [Secret Exposure in Process List via openssl -k]
+**Vulnerability:** The `release/signing-setup.sh` script used the legacy `openssl -k` flag to pass an encryption key as a command-line argument, making it visible to all users on the system via process monitoring tools.
+**Learning:** Command-line arguments are inherently public on most systems. Using them for secrets is a major security risk, especially in shared CI/CD environments.
+**Prevention:** Always use environment variables, files, or descriptor-based methods to pass secrets to CLI tools. In `openssl`, use `-pass env:VARIABLE_NAME` to read the secret from an environment variable securely.
diff --git a/checksum.sh b/checksum.sh
index 7d480521c..a5a06fc8f 100755
--- a/checksum.sh
+++ b/checksum.sh
@@ -14,25 +14,32 @@
# See the License for the specific language governing permissions and
# limitations under the License.
-RESULT_FILE=$1
+set -e
-if [ -f $RESULT_FILE ]; then
- rm $RESULT_FILE
+RESULT_FILE="$1"
+
+if [ -f "$RESULT_FILE" ]; then
+ rm "$RESULT_FILE"
fi
-touch $RESULT_FILE
+touch "$RESULT_FILE"
checksum_file() {
- echo $(openssl md5 $1 | awk '{print $2}')
+ # Use SHA-256 for better security and collision resistance compared to MD5.
+ if command -v sha256sum >/dev/null 2>&1; then
+ sha256sum "$1" | awk '{print $1}'
+ else
+ openssl dgst -sha256 -r "$1" | awk '{print $1}'
+ fi
}
FILES=()
-while read -r -d ''; do
- FILES+=("$REPLY")
+while read -r -d '' FILE; do
+ FILES+=("$FILE")
done < <(find . -type f \( -name "build.gradle*" -o -name "*.versions.toml" -o -name "gradle-wrapper.properties" \) -print0)
-# Loop through files and append MD5 to result file
-for FILE in ${FILES[@]}; do
- echo $(checksum_file $FILE) >> $RESULT_FILE
+# Loop through files and append SHA-256 to result file
+for FILE in "${FILES[@]}"; do
+ echo "$(checksum_file "$FILE")" >> "$RESULT_FILE"
done
# Now sort the file so that it is idempotent
-sort $RESULT_FILE -o $RESULT_FILE
+sort "$RESULT_FILE" -o "$RESULT_FILE"
diff --git a/gradle.properties b/gradle.properties
index 7ef419307..7d9c13321 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -39,11 +39,11 @@ POM_DESCRIPTION=Utilities for Jetpack Compose
POM_URL=https://github.com/google/accompanist/
POM_SCM_URL=https://github.com/google/accompanist/
-POM_SCM_CONNECTION=scm:git:git://github.com/google/accompanist.git
-POM_SCM_DEV_CONNECTION=scm:git:git://github.com/google/accompanist.git
+POM_SCM_CONNECTION=scm:git:https://github.com/google/accompanist.git
+POM_SCM_DEV_CONNECTION=scm:git:https://github.com/google/accompanist.git
POM_LICENCE_NAME=The Apache Software License, Version 2.0
-POM_LICENCE_URL=http://www.apache.org/licenses/LICENSE-2.0.txt
+POM_LICENCE_URL=https://www.apache.org/licenses/LICENSE-2.0.txt
POM_LICENCE_DIST=repo
POM_DEVELOPER_ID=google
diff --git a/release/signing-setup.sh b/release/signing-setup.sh
index de0d70ecd..14d97aa6b 100755
--- a/release/signing-setup.sh
+++ b/release/signing-setup.sh
@@ -14,15 +14,18 @@
# See the License for the specific language governing permissions and
# limitations under the License.
-ENCRYPT_KEY=$1
+set -e
-if [[ ! -z "$ENCRYPT_KEY" ]]; then
+ENCRYPT_KEY="${ENCRYPT_KEY:-$1}"
+
+if [[ -n "$ENCRYPT_KEY" ]]; then
# Decrypt GnuPG keyring
- openssl aes-256-cbc -md sha256 -d -in release/secring.gpg.aes -out release/secring.gpg -k ${ENCRYPT_KEY}
+ openssl aes-256-cbc -md sha256 -d -in release/secring.gpg.aes -out release/secring.gpg -pass env:ENCRYPT_KEY
# Decrypt Play Store key
- openssl aes-256-cbc -md sha256 -d -in release/signing.properties.aes -out release/signing.properties -k ${ENCRYPT_KEY}
+ openssl aes-256-cbc -md sha256 -d -in release/signing.properties.aes -out release/signing.properties -pass env:ENCRYPT_KEY
else
- echo "ENCRYPT_KEY is empty"
+ echo "ENCRYPT_KEY is empty, skipping decryption"
+ exit 0
fi
diff --git a/sample/src/main/AndroidManifest.xml b/sample/src/main/AndroidManifest.xml
index 8b670aab3..384a67db6 100644
--- a/sample/src/main/AndroidManifest.xml
+++ b/sample/src/main/AndroidManifest.xml
@@ -34,7 +34,8 @@
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
- android:theme="@android:style/Theme.Material.NoActionBar">
+ android:theme="@android:style/Theme.Material.NoActionBar"
+ android:usesCleartextTraffic="false">
@@ -51,10 +52,14 @@
+
+ android:exported="false">
@@ -64,7 +69,7 @@
+ android:exported="false">
@@ -74,7 +79,7 @@
+ android:exported="false">
@@ -84,7 +89,7 @@
+ android:exported="false">
@@ -94,7 +99,7 @@
+ android:exported="false">
@@ -104,7 +109,7 @@
+ android:exported="false">
@@ -114,7 +119,7 @@
+ android:exported="false">
@@ -124,7 +129,7 @@
+ android:exported="false">
@@ -134,7 +139,7 @@
+ android:exported="false">
diff --git a/sample/src/main/java/com/google/accompanist/sample/MainActivity.kt b/sample/src/main/java/com/google/accompanist/sample/MainActivity.kt
index 0f8a2e59a..905ae4d72 100644
--- a/sample/src/main/java/com/google/accompanist/sample/MainActivity.kt
+++ b/sample/src/main/java/com/google/accompanist/sample/MainActivity.kt
@@ -22,6 +22,7 @@ import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
+import androidx.compose.ui.res.stringResource
/**
* A list which automatically populates the list of sample activities in this app
@@ -32,13 +33,20 @@ class MainActivity : ComponentActivity() {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
- val data = getData(intent.getStringExtra(EXTRA_PATH))
+ val path = intent.getStringExtra(EXTRA_PATH)
+ val data = getData(path)
setContent {
AccompanistSampleTheme {
MainScreen(
listData = data,
- onItemClick = { startActivity(it) }
+ onItemClick = { startActivity(it) },
+ title = path?.substringAfterLast('/') ?: stringResource(R.string.app_name),
+ onBackClick = if (path != null) {
+ { finish() }
+ } else {
+ null
+ }
)
}
}
@@ -66,6 +74,8 @@ class MainActivity : ComponentActivity() {
val entries = mutableMapOf()
list.forEach { info ->
+ if (info.activityInfo.packageName != packageName) return@forEach
+
val labelSeq = info.loadLabel(packageManager)
val label = labelSeq?.toString() ?: info.activityInfo.name
@@ -117,4 +127,8 @@ class MainActivity : ComponentActivity() {
}
}
-private const val EXTRA_PATH = "com.example.android.apis.Path"
+/*
+ * Use a project-specific prefix for intent extras to avoid collisions
+ * and follow security best practices.
+ */
+private const val EXTRA_PATH = "com.google.accompanist.sample.PATH"
diff --git a/sample/src/main/java/com/google/accompanist/sample/MainScreen.kt b/sample/src/main/java/com/google/accompanist/sample/MainScreen.kt
index 7ec1e17b9..259239ac9 100644
--- a/sample/src/main/java/com/google/accompanist/sample/MainScreen.kt
+++ b/sample/src/main/java/com/google/accompanist/sample/MainScreen.kt
@@ -30,7 +30,12 @@ import androidx.compose.foundation.layout.windowInsetsBottomHeight
import androidx.compose.foundation.layout.windowInsetsPadding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.automirrored.filled.ArrowBack
+import androidx.compose.material.icons.automirrored.filled.KeyboardArrowRight
import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.material3.Icon
+import androidx.compose.material3.IconButton
import androidx.compose.material3.ListItem
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
@@ -41,6 +46,8 @@ import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.semantics.Role
+import androidx.compose.ui.text.style.TextOverflow
data class AccompanistSample(
val title: String,
@@ -52,13 +59,25 @@ data class AccompanistSample(
fun MainScreen(
listData: List,
onItemClick: (Intent) -> Unit,
- modifier: Modifier = Modifier
+ modifier: Modifier = Modifier,
+ title: String = stringResource(R.string.app_name),
+ onBackClick: (() -> Unit)? = null
) {
Surface(modifier) {
Column {
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState())
TopAppBar(
- title = { Text(stringResource(R.string.app_name)) },
+ title = { Text(title) },
+ navigationIcon = {
+ if (onBackClick != null) {
+ IconButton(onClick = onBackClick) {
+ Icon(
+ imageVector = Icons.AutoMirrored.Filled.ArrowBack,
+ contentDescription = stringResource(R.string.navigation_back)
+ )
+ }
+ }
+ },
scrollBehavior = scrollBehavior
)
@@ -88,8 +107,23 @@ private fun ContentList(
) {
items(listData) {
ListItem(
- headlineText = { Text(it.title) },
- modifier = Modifier.clickable { onItemClick(it.intent) }
+ headlineText = {
+ Text(
+ text = it.title,
+ maxLines = 1,
+ overflow = TextOverflow.Ellipsis
+ )
+ },
+ trailingContent = {
+ Icon(
+ imageVector = Icons.AutoMirrored.Filled.KeyboardArrowRight,
+ contentDescription = null
+ )
+ },
+ modifier = Modifier.clickable(
+ role = Role.Button,
+ onClick = { onItemClick(it.intent) }
+ )
)
}
diff --git a/sample/src/main/java/com/google/accompanist/sample/adaptive/NavDrawerFoldAwareColumnSample.kt b/sample/src/main/java/com/google/accompanist/sample/adaptive/NavDrawerFoldAwareColumnSample.kt
index 9be1475f0..cd1e89927 100644
--- a/sample/src/main/java/com/google/accompanist/sample/adaptive/NavDrawerFoldAwareColumnSample.kt
+++ b/sample/src/main/java/com/google/accompanist/sample/adaptive/NavDrawerFoldAwareColumnSample.kt
@@ -21,9 +21,12 @@ import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.border
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Done
@@ -46,7 +49,9 @@ import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
+import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.unit.dp
import com.google.accompanist.adaptive.FoldAwareColumn
import com.google.accompanist.adaptive.calculateDisplayFeatures
@@ -57,7 +62,25 @@ class NavDrawerFoldAwareColumnSample : ComponentActivity() {
super.onCreate(savedInstanceState)
setContent {
AccompanistSample(contentPadding = PaddingValues(all = 0.dp)) {
- NavDrawerExample(this)
+ val icons = remember {
+ listOf(
+ Icons.Default.Done,
+ Icons.Default.Face,
+ Icons.Default.Lock,
+ Icons.Default.Search,
+ Icons.Default.ThumbUp,
+ Icons.Default.Warning,
+ Icons.Default.Star
+ )
+ }
+ var selectedIcon by remember { mutableStateOf(icons[0]) }
+
+ NavDrawerExample(
+ activity = this@NavDrawerFoldAwareColumnSample,
+ icons = icons,
+ selectedIcon = selectedIcon,
+ onIconSelected = { selectedIcon = it }
+ )
}
}
}
@@ -65,19 +88,12 @@ class NavDrawerFoldAwareColumnSample : ComponentActivity() {
@OptIn(ExperimentalMaterial3Api::class)
@Composable
-fun NavDrawerExample(activity: Activity) {
- val icons = listOf(
- Icons.Default.Done,
- Icons.Default.Face,
- Icons.Default.Lock,
- Icons.Default.Search,
- Icons.Default.ThumbUp,
- Icons.Default.Warning,
- Icons.Default.Star
- )
-
- var selectedIcon by remember { mutableStateOf(icons[0]) }
-
+fun NavDrawerExample(
+ activity: Activity,
+ icons: List,
+ selectedIcon: ImageVector,
+ onIconSelected: (ImageVector) -> Unit
+) {
ModalNavigationDrawer(
drawerContent = {
ModalDrawerSheet {
@@ -90,15 +106,32 @@ fun NavDrawerExample(activity: Activity) {
modifier = Modifier
.padding(5.dp)
.border(2.dp, MaterialTheme.colorScheme.primary, CircleShape),
- icon = { Icon(imageVector = it, contentDescription = it.name) },
+ icon = { Icon(imageVector = it, contentDescription = null) },
label = { Text(it.name.substringAfter('.')) },
selected = it == selectedIcon,
- onClick = { selectedIcon = it }
+ onClick = { onIconSelected(it) }
)
}
}
}
},
- content = { Surface(modifier = Modifier.fillMaxSize()) {} }
+ content = {
+ Surface(modifier = Modifier.fillMaxSize()) {
+ Box(contentAlignment = Alignment.Center) {
+ Column(horizontalAlignment = Alignment.CenterHorizontally) {
+ Icon(
+ imageVector = selectedIcon,
+ contentDescription = null,
+ modifier = Modifier.size(64.dp),
+ tint = MaterialTheme.colorScheme.primary
+ )
+ Text(
+ text = selectedIcon.name.substringAfter('.'),
+ style = MaterialTheme.typography.headlineSmall
+ )
+ }
+ }
+ }
+ }
)
}
diff --git a/sample/src/main/java/com/google/accompanist/sample/adaptive/NavRailFoldAwareColumnSample.kt b/sample/src/main/java/com/google/accompanist/sample/adaptive/NavRailFoldAwareColumnSample.kt
index 5eb0a4836..21e6cc393 100644
--- a/sample/src/main/java/com/google/accompanist/sample/adaptive/NavRailFoldAwareColumnSample.kt
+++ b/sample/src/main/java/com/google/accompanist/sample/adaptive/NavRailFoldAwareColumnSample.kt
@@ -21,9 +21,12 @@ import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.border
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Done
import androidx.compose.material.icons.filled.Face
@@ -43,7 +46,9 @@ import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
+import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.unit.dp
import com.google.accompanist.adaptive.FoldAwareColumn
import com.google.accompanist.adaptive.calculateDisplayFeatures
@@ -54,9 +59,42 @@ class NavRailFoldAwareColumnSample : ComponentActivity() {
super.onCreate(savedInstanceState)
setContent {
AccompanistSample {
+ val icons = remember {
+ listOf(
+ Icons.Default.Done,
+ Icons.Default.Face,
+ Icons.Default.Lock,
+ Icons.Default.Search,
+ Icons.Default.ThumbUp,
+ Icons.Default.Warning,
+ Icons.Default.Star
+ )
+ }
+ var selectedIcon by remember { mutableStateOf(icons[0]) }
+
Row {
- NavRail(this@NavRailFoldAwareColumnSample)
- Surface(modifier = Modifier.fillMaxSize()) {}
+ NavRail(
+ activity = this@NavRailFoldAwareColumnSample,
+ icons = icons,
+ selectedIcon = selectedIcon,
+ onIconSelected = { selectedIcon = it }
+ )
+ Surface(modifier = Modifier.fillMaxSize()) {
+ Box(contentAlignment = Alignment.Center) {
+ Column(horizontalAlignment = Alignment.CenterHorizontally) {
+ Icon(
+ imageVector = selectedIcon,
+ contentDescription = null,
+ modifier = Modifier.size(64.dp),
+ tint = MaterialTheme.colorScheme.primary
+ )
+ Text(
+ text = selectedIcon.name.substringAfter('.'),
+ style = MaterialTheme.typography.headlineSmall
+ )
+ }
+ }
+ }
}
}
}
@@ -64,19 +102,12 @@ class NavRailFoldAwareColumnSample : ComponentActivity() {
}
@Composable
-fun NavRail(activity: Activity) {
- val icons = listOf(
- Icons.Default.Done,
- Icons.Default.Face,
- Icons.Default.Lock,
- Icons.Default.Search,
- Icons.Default.ThumbUp,
- Icons.Default.Warning,
- Icons.Default.Star
- )
-
- var selectedIcon by remember { mutableStateOf(icons[0]) }
-
+fun NavRail(
+ activity: Activity,
+ icons: List,
+ selectedIcon: ImageVector,
+ onIconSelected: (ImageVector) -> Unit
+) {
NavigationRail {
FoldAwareColumn(displayFeatures = calculateDisplayFeatures(activity)) {
icons.forEach {
@@ -85,8 +116,8 @@ fun NavRail(activity: Activity) {
.padding(5.dp)
.border(2.dp, MaterialTheme.colorScheme.primary),
selected = it == selectedIcon,
- onClick = { selectedIcon = it },
- icon = { Icon(imageVector = it, contentDescription = it.name) },
+ onClick = { onIconSelected(it) },
+ icon = { Icon(imageVector = it, contentDescription = null) },
label = { Text(it.name.substringAfter('.')) }
)
}
diff --git a/sample/src/main/java/com/google/accompanist/sample/permissions/RequestLocationPermissionsSample.kt b/sample/src/main/java/com/google/accompanist/sample/permissions/RequestLocationPermissionsSample.kt
index 51d136ad0..c8863d89e 100644
--- a/sample/src/main/java/com/google/accompanist/sample/permissions/RequestLocationPermissionsSample.kt
+++ b/sample/src/main/java/com/google/accompanist/sample/permissions/RequestLocationPermissionsSample.kt
@@ -19,13 +19,23 @@ package com.google.accompanist.sample.permissions
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
-import androidx.compose.foundation.layout.Spacer
-import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.size
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.Done
+import androidx.compose.material.icons.filled.Face
import androidx.compose.material3.Button
+import androidx.compose.material3.Icon
+import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
+import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import com.google.accompanist.permissions.ExperimentalPermissionsApi
import com.google.accompanist.permissions.rememberMultiplePermissionsState
@@ -53,39 +63,68 @@ private fun Sample() {
)
)
- if (locationPermissionsState.allPermissionsGranted) {
- Text("Thanks! I can access your exact location :D")
- } else {
- Column {
- val allPermissionsRevoked =
- locationPermissionsState.permissions.size ==
- locationPermissionsState.revokedPermissions.size
-
- val textToShow = if (!allPermissionsRevoked) {
- // If not all the permissions are revoked, it's because the user accepted the COARSE
- // location permission, but not the FINE one.
- "Yay! Thanks for letting me access your approximate location. " +
- "But you know what would be great? If you allow me to know where you " +
- "exactly are. Thank you!"
- } else if (locationPermissionsState.shouldShowRationale) {
- // Both location permissions have been denied
- "Getting your exact location is important for this app. " +
- "Please grant us fine location. Thank you :D"
- } else {
- // First time the user sees this feature or the user doesn't want to be asked again
- "This feature requires location permission"
+ Box(
+ modifier = Modifier.fillMaxSize(),
+ contentAlignment = Alignment.Center
+ ) {
+ if (locationPermissionsState.allPermissionsGranted) {
+ Row(
+ verticalAlignment = Alignment.CenterVertically,
+ horizontalArrangement = Arrangement.spacedBy(8.dp)
+ ) {
+ Icon(
+ imageVector = Icons.Default.Done,
+ contentDescription = null,
+ tint = MaterialTheme.colorScheme.primary
+ )
+ Text(
+ "Thank you! Precise location access is granted.",
+ textAlign = TextAlign.Center
+ )
}
+ } else {
+ Column(
+ horizontalAlignment = Alignment.CenterHorizontally,
+ verticalArrangement = Arrangement.spacedBy(16.dp)
+ ) {
+ Icon(
+ imageVector = Icons.Default.Face,
+ contentDescription = null,
+ modifier = Modifier.size(48.dp)
+ )
- val buttonText = if (!allPermissionsRevoked) {
- "Allow precise location"
- } else {
- "Request permissions"
- }
+ val allPermissionsRevoked =
+ locationPermissionsState.permissions.size ==
+ locationPermissionsState.revokedPermissions.size
+
+ val textToShow = if (!allPermissionsRevoked) {
+ // If not all the permissions are revoked, it's because the user accepted the COARSE
+ // location permission, but not the FINE one.
+ "Yay! Thanks for letting me access your approximate location. " +
+ "But you know what would be great? If you allow me to know where you " +
+ "exactly are. Thank you!"
+ } else if (locationPermissionsState.shouldShowRationale) {
+ // Both location permissions have been denied
+ "Getting your exact location is important for this app. " +
+ "Please grant us fine location. Thank you :D"
+ } else {
+ // First time the user sees this feature or the user doesn't want to be asked again
+ "This feature requires location permission"
+ }
+
+ val buttonText = if (!allPermissionsRevoked) {
+ "Allow precise location"
+ } else {
+ "Allow location access"
+ }
- Text(text = textToShow)
- Spacer(modifier = Modifier.height(8.dp))
- Button(onClick = { locationPermissionsState.launchMultiplePermissionRequest() }) {
- Text(buttonText)
+ Text(
+ text = textToShow,
+ textAlign = TextAlign.Center
+ )
+ Button(onClick = { locationPermissionsState.launchMultiplePermissionRequest() }) {
+ Text(buttonText)
+ }
}
}
}
diff --git a/sample/src/main/java/com/google/accompanist/sample/permissions/RequestMultiplePermissionsSample.kt b/sample/src/main/java/com/google/accompanist/sample/permissions/RequestMultiplePermissionsSample.kt
index 2c40b76bf..37973c0f0 100644
--- a/sample/src/main/java/com/google/accompanist/sample/permissions/RequestMultiplePermissionsSample.kt
+++ b/sample/src/main/java/com/google/accompanist/sample/permissions/RequestMultiplePermissionsSample.kt
@@ -20,13 +20,23 @@ import android.Manifest
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
-import androidx.compose.foundation.layout.Spacer
-import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.size
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.Done
+import androidx.compose.material.icons.filled.Face
import androidx.compose.material3.Button
+import androidx.compose.material3.Icon
+import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
+import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import com.google.accompanist.permissions.ExperimentalPermissionsApi
import com.google.accompanist.permissions.MultiplePermissionsState
@@ -56,20 +66,44 @@ class RequestMultiplePermissionsSample : ComponentActivity() {
@OptIn(ExperimentalPermissionsApi::class)
@Composable
private fun Sample(multiplePermissionsState: MultiplePermissionsState) {
- if (multiplePermissionsState.allPermissionsGranted) {
- // If all permissions are granted, then show screen with the feature enabled
- Text("Camera and Read storage permissions Granted! Thank you!")
- } else {
- Column {
- Text(
- getTextToShowGivenPermissions(
- multiplePermissionsState.revokedPermissions,
- multiplePermissionsState.shouldShowRationale
+ Box(
+ modifier = Modifier.fillMaxSize(),
+ contentAlignment = Alignment.Center
+ ) {
+ if (multiplePermissionsState.allPermissionsGranted) {
+ // If all permissions are granted, then show screen with the feature enabled
+ Row(
+ verticalAlignment = Alignment.CenterVertically,
+ horizontalArrangement = Arrangement.spacedBy(8.dp)
+ ) {
+ Icon(
+ imageVector = Icons.Default.Done,
+ contentDescription = null,
+ tint = MaterialTheme.colorScheme.primary
)
- )
- Spacer(modifier = Modifier.height(8.dp))
- Button(onClick = { multiplePermissionsState.launchMultiplePermissionRequest() }) {
- Text("Request permissions")
+ Text("Camera and Audio permissions Granted! Thank you!")
+ }
+ } else {
+ Column(
+ horizontalAlignment = Alignment.CenterHorizontally,
+ verticalArrangement = Arrangement.spacedBy(16.dp)
+ ) {
+ Icon(
+ imageVector = Icons.Default.Face,
+ contentDescription = null,
+ modifier = Modifier.size(48.dp)
+ )
+
+ Text(
+ getTextToShowGivenPermissions(
+ multiplePermissionsState.revokedPermissions,
+ multiplePermissionsState.shouldShowRationale
+ ),
+ textAlign = TextAlign.Center
+ )
+ Button(onClick = { multiplePermissionsState.launchMultiplePermissionRequest() }) {
+ Text("Allow permissions")
+ }
}
}
}
@@ -88,7 +122,17 @@ private fun getTextToShowGivenPermissions(
}
for (i in permissions.indices) {
- textToShow.append(permissions[i].permission)
+ val readableName = when (permissions[i].permission) {
+ Manifest.permission.CAMERA -> "camera"
+ Manifest.permission.RECORD_AUDIO -> "audio"
+ else -> {
+ permissions[i].permission
+ .substringAfterLast(".")
+ .replace("_", " ")
+ .lowercase()
+ }
+ }
+ textToShow.append(readableName)
when {
revokedPermissionsSize > 1 && i == revokedPermissionsSize - 2 -> {
textToShow.append(", and ")
diff --git a/sample/src/main/java/com/google/accompanist/sample/permissions/RequestPermissionSample.kt b/sample/src/main/java/com/google/accompanist/sample/permissions/RequestPermissionSample.kt
index 6960db44f..b6d8e2a3c 100644
--- a/sample/src/main/java/com/google/accompanist/sample/permissions/RequestPermissionSample.kt
+++ b/sample/src/main/java/com/google/accompanist/sample/permissions/RequestPermissionSample.kt
@@ -19,13 +19,23 @@ package com.google.accompanist.sample.permissions
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
-import androidx.compose.foundation.layout.Spacer
-import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.size
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.Done
+import androidx.compose.material.icons.filled.Face
import androidx.compose.material3.Button
+import androidx.compose.material3.Icon
+import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
+import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
+import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import com.google.accompanist.permissions.ExperimentalPermissionsApi
import com.google.accompanist.permissions.isGranted
@@ -49,20 +59,43 @@ class RequestPermissionSample : ComponentActivity() {
@Composable
private fun Sample() {
val cameraPermissionState = rememberPermissionState(android.Manifest.permission.CAMERA)
- if (cameraPermissionState.status.isGranted) {
- Text("Camera permission Granted")
- } else {
- Column {
- val textToShow = if (cameraPermissionState.status.shouldShowRationale) {
- "The camera is important for this app. Please grant the permission."
- } else {
- "Camera not available"
+ Box(
+ modifier = Modifier.fillMaxSize(),
+ contentAlignment = Alignment.Center
+ ) {
+ if (cameraPermissionState.status.isGranted) {
+ Row(
+ verticalAlignment = Alignment.CenterVertically,
+ horizontalArrangement = Arrangement.spacedBy(8.dp)
+ ) {
+ Icon(
+ imageVector = Icons.Default.Done,
+ contentDescription = null,
+ tint = MaterialTheme.colorScheme.primary
+ )
+ Text("Camera permission Granted")
}
+ } else {
+ Column(
+ horizontalAlignment = Alignment.CenterHorizontally,
+ verticalArrangement = Arrangement.spacedBy(16.dp)
+ ) {
+ Icon(
+ imageVector = Icons.Default.Face,
+ contentDescription = null,
+ modifier = Modifier.size(48.dp)
+ )
- Text(textToShow)
- Spacer(modifier = Modifier.height(8.dp))
- Button(onClick = { cameraPermissionState.launchPermissionRequest() }) {
- Text("Request permission")
+ val textToShow = if (cameraPermissionState.status.shouldShowRationale) {
+ "The camera is important for this app. Please grant the permission."
+ } else {
+ "The camera permission is required for this feature."
+ }
+
+ Text(textToShow, textAlign = TextAlign.Center)
+ Button(onClick = { cameraPermissionState.launchPermissionRequest() }) {
+ Text("Allow camera access")
+ }
}
}
}
diff --git a/sample/src/main/res/values/strings.xml b/sample/src/main/res/values/strings.xml
index 5a9126c54..b19be0369 100644
--- a/sample/src/main/res/values/strings.xml
+++ b/sample/src/main/res/values/strings.xml
@@ -16,6 +16,7 @@
Accompanist Sample
+ Back
Insets: Basic
Insets: Fragment