diff --git a/CHANGELOG.md b/CHANGELOG.md index c3bbc35..b1d1337 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,15 @@ Add entries under **Unreleased** in each pull request. The *Bump version* workfl ### Changed - The sample's fake update feed offers 1.1.0 now that the sample is 1.0.0. - README install lines use a `LATEST_VERSION` placeholder that matches the JitPack badge, plus a version catalog example. +- Drawer pages slide in and out instead of switching instantly. +- The network inspector's clear button sits in the count row, like the other lists. +- The device & app snapshot shows share, save and copy as small icons on the card; they are not part of the shared image. +- The deep link launcher opens links from a send icon inside the field, leaving more room on small phones. +- The sample app is named DevDeck. + +### Fixed +- Search placeholders and the network count row stay on one line on small phones. +- The permissions list refreshes after a permission dialog or settings screen closes, and shows when a revoke only applies after the app restarts. ## [1.0.0] - 2026-10-24 diff --git a/README.md b/README.md index 384801d..2c1592c 100644 --- a/README.md +++ b/README.md @@ -303,10 +303,10 @@ The last 50 steps are saved with every crash and shown as *Steps before the cras ### Screen capture, bug reports and device info - **Screenshot / screen recording / gallery:** under User Interface. Recording asks for Android's screen-capture permission and shows a Stop notification. -- **Device & app snapshot:** under Info. One image with app version, build type, SDKs, device, Android version, screen and locale, plus your `buildInfo`. Share it, save it, or copy it as text. +- **Device & app snapshot:** under Info. One image with app version, build type, SDKs, device, Android version, screen and locale, plus your `buildInfo`. Share it, save it, or copy it as text from the icons on the card; the icons are left out of the image. - **App settings:** the ⚙ button next to the drawer's close button opens the app's system settings page. - **Bug report:** a description, recent steps, your logs, recent logcat, device info and chosen captures, sent through the share sheet. -- **Permissions:** under System. Each permission shows Granted or Denied with what you can do about it. *Request* shows the system dialog, or opens the app's settings once Android stops asking. *Revoke* (Android 13+) removes a runtime permission when the app closes. *Open settings* handles special permissions such as drawing over other apps. Install-time permissions can't change, so they are only listed. To grant or revoke silently, use `adb shell pm grant` / `pm revoke`. +- **Permissions:** under System. Each permission shows Granted or Denied with what you can do about it. *Request* shows the system dialog, or opens the app's settings once Android stops asking. *Revoke* (Android 13+) removes a runtime permission when the app closes; until then the row shows *Revoked when the app restarts*. The list refreshes when you come back from a permission dialog or settings screen. *Open settings* handles special permissions such as drawing over other apps. Install-time permissions can't change, so they are only listed. To grant or revoke silently, use `adb shell pm grant` / `pm revoke`. --- diff --git a/devdeck/src/main/kotlin/io/devdeck/DeckPanel.kt b/devdeck/src/main/kotlin/io/devdeck/DeckPanel.kt index 3afb0ce..0aba93d 100644 --- a/devdeck/src/main/kotlin/io/devdeck/DeckPanel.kt +++ b/devdeck/src/main/kotlin/io/devdeck/DeckPanel.kt @@ -6,6 +6,11 @@ import android.content.Context import android.content.Intent import android.net.Uri import android.provider.Settings +import androidx.compose.animation.AnimatedContent +import androidx.compose.animation.core.tween +import androidx.compose.animation.slideInHorizontally +import androidx.compose.animation.slideOutHorizontally +import androidx.compose.animation.togetherWith import androidx.compose.foundation.BorderStroke import androidx.compose.foundation.background import androidx.compose.foundation.border @@ -41,7 +46,6 @@ import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.key import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @@ -79,38 +83,73 @@ internal fun DeckPanel( Surface(modifier = modifier, shape = shape, color = colors?.panel ?: MaterialTheme.colorScheme.surface, shadowElevation = 12.dp) { CompositionLocalProvider(LocalDeckState provides state) { - Column(Modifier.fillMaxSize().windowInsetsPadding(WindowInsets.safeDrawing.only(insetSides))) { - val page = state.pages.lastOrNull() - if (page == null) { - DeckHeader(title = style.title, subtitle = style.subtitle, onClose = state::close) - } else { - DeckPageBar(title = page.title, onBack = { state.navigateBack() }, onClose = state::close) - } - key(page) { - Column( - Modifier - .weight(1f) - .fillMaxWidth() - .imePadding() - .verticalScroll(rememberScrollState()) - .padding(top = 4.dp, bottom = 16.dp), - ) { - if (page == null) { - scope.drawerContent() - } else { - DeckCard(Modifier.padding(horizontal = 12.dp)) { - Spacer(Modifier.height(4.dp)) - page.content(scope) - Spacer(Modifier.height(4.dp)) - } - } - } + val page = state.pages.lastOrNull() + AnimatedContent( + targetState = PageEntry(page, state.pages.size), + transitionSpec = { + val forward = targetState.depth > initialState.depth + val enter = slideInHorizontally(tween(PAGE_TRANSITION_MILLIS)) { if (forward) it else -it / 3 } + val exit = slideOutHorizontally(tween(PAGE_TRANSITION_MILLIS)) { if (forward) -it / 3 else it } + (enter togetherWith exit).apply { targetContentZIndex = if (forward) 1f else -1f } + }, + contentKey = { it.page }, + label = "DeckPage", + ) { entry -> + DeckPageContent( + entry.page, + state, + style, + scope, + colors?.panel ?: MaterialTheme.colorScheme.surface, + insetSides, + drawerContent, + ) + } + } + } +} + +private data class PageEntry(val page: DeckPage?, val depth: Int) + +@Composable +private fun DeckPageContent( + page: DeckPage?, + state: DeckState, + style: DeckStyle, + scope: DeckScope, + background: Color, + insetSides: WindowInsetsSides, + drawerContent: @Composable DeckScope.() -> Unit, +) { + Column(Modifier.fillMaxSize().background(background).windowInsetsPadding(WindowInsets.safeDrawing.only(insetSides))) { + if (page == null) { + DeckHeader(title = style.title, subtitle = style.subtitle, onClose = state::close) + } else { + DeckPageBar(title = page.title, onBack = { state.navigateBack() }, onClose = state::close) + } + Column( + Modifier + .weight(1f) + .fillMaxWidth() + .imePadding() + .verticalScroll(rememberScrollState()) + .padding(top = 4.dp, bottom = 16.dp), + ) { + if (page == null) { + scope.drawerContent() + } else { + DeckCard(Modifier.padding(horizontal = 12.dp)) { + Spacer(Modifier.height(4.dp)) + page.content(scope) + Spacer(Modifier.height(4.dp)) } } } } } +private const val PAGE_TRANSITION_MILLIS = 280 + @Composable private fun DeckHeader(title: String, subtitle: String?, onClose: () -> Unit) { val context = LocalContext.current diff --git a/devdeck/src/main/kotlin/io/devdeck/actions/DeepLinkLauncher.kt b/devdeck/src/main/kotlin/io/devdeck/actions/DeepLinkLauncher.kt index 0749e8c..af11c15 100644 --- a/devdeck/src/main/kotlin/io/devdeck/actions/DeepLinkLauncher.kt +++ b/devdeck/src/main/kotlin/io/devdeck/actions/DeepLinkLauncher.kt @@ -3,14 +3,14 @@ package io.devdeck.actions import android.content.Context import android.content.Intent import android.widget.Toast -import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.text.KeyboardActions import androidx.compose.foundation.text.KeyboardOptions -import androidx.compose.material3.Button +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.Text import androidx.compose.runtime.Composable @@ -19,16 +19,17 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue -import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.text.input.KeyboardType +import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import androidx.core.content.edit import androidx.core.net.toUri import io.devdeck.components.BulletItem import io.devdeck.components.SectionTitle +import io.devdeck.internal.DeckIcons import io.devdeck.internal.DeckSettings @Composable @@ -40,22 +41,24 @@ public fun DeepLinkLauncher(modifier: Modifier = Modifier, suggestions: List diff --git a/devdeck/src/main/kotlin/io/devdeck/components/SearchField.kt b/devdeck/src/main/kotlin/io/devdeck/components/SearchField.kt index c4761a6..596938d 100644 --- a/devdeck/src/main/kotlin/io/devdeck/components/SearchField.kt +++ b/devdeck/src/main/kotlin/io/devdeck/components/SearchField.kt @@ -12,6 +12,7 @@ import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import io.devdeck.internal.DeckIcons @@ -21,7 +22,7 @@ public fun SearchField(query: String, onQueryChange: (String) -> Unit, modifier: OutlinedTextField( value = query, onValueChange = onQueryChange, - placeholder = { Text(placeholder) }, + placeholder = { Text(placeholder, maxLines = 1, overflow = TextOverflow.Ellipsis) }, singleLine = true, shape = CircleShape, leadingIcon = { Icon(DeckIcons.Search, contentDescription = null, tint = colors.onSurfaceVariant) }, diff --git a/devdeck/src/main/kotlin/io/devdeck/device/AppPermissions.kt b/devdeck/src/main/kotlin/io/devdeck/device/AppPermissions.kt index 780e869..b8dab74 100644 --- a/devdeck/src/main/kotlin/io/devdeck/device/AppPermissions.kt +++ b/devdeck/src/main/kotlin/io/devdeck/device/AppPermissions.kt @@ -11,6 +11,7 @@ import android.net.Uri import android.os.Build import android.os.Environment import android.provider.Settings +import androidx.compose.runtime.mutableStateListOf import androidx.core.app.ActivityCompat import androidx.core.content.ContextCompat import androidx.core.content.edit @@ -31,6 +32,9 @@ internal val PermissionOrder: Comparator = internal object AppPermissions { + // Revocations requested this process; Android only applies them once the process dies. + val pendingRevocations = mutableStateListOf() + fun load(context: Context): List { val info = runCatching { context.packageManager.getPackageInfo(context.packageName, PackageManager.GET_PERMISSIONS) @@ -61,6 +65,7 @@ internal object AppPermissions { fun revoke(activity: Activity, permission: AppPermission): Boolean { if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) return false activity.revokeSelfPermissionOnKill(permission.name) + if (permission.name !in pendingRevocations) pendingRevocations += permission.name activity.moveTaskToBack(true) return true } diff --git a/devdeck/src/main/kotlin/io/devdeck/device/DeviceSnapshotCard.kt b/devdeck/src/main/kotlin/io/devdeck/device/DeviceSnapshotCard.kt index a3aa00a..096dee4 100644 --- a/devdeck/src/main/kotlin/io/devdeck/device/DeviceSnapshotCard.kt +++ b/devdeck/src/main/kotlin/io/devdeck/device/DeviceSnapshotCard.kt @@ -5,7 +5,6 @@ import android.graphics.Bitmap import android.widget.Toast import androidx.compose.foundation.background import androidx.compose.foundation.border -import androidx.compose.foundation.clickable import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box @@ -14,10 +13,10 @@ import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size -import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable @@ -25,7 +24,6 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.drawWithContent import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.asAndroidBitmap @@ -34,11 +32,11 @@ import androidx.compose.ui.graphics.layer.drawLayer import androidx.compose.ui.graphics.rememberGraphicsLayer import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.semantics.Role import androidx.compose.ui.text.SpanStyle import androidx.compose.ui.text.buildAnnotatedString import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.withStyle +import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import io.devdeck.LocalDeckColors import io.devdeck.capture.ScreenCapture @@ -60,22 +58,24 @@ public fun DeviceSnapshotCard(modifier: Modifier = Modifier, buildInfo: () -> Ma val layer = rememberGraphicsLayer() val scope = rememberCoroutineScope() - Column(modifier.fillMaxWidth().padding(horizontal = 16.dp, vertical = 8.dp), verticalArrangement = Arrangement.spacedBy(8.dp)) { + // The actions sit on top of the snapshot but outside the recorded layer, so they never show up in the shared image. + Box(modifier.fillMaxWidth().padding(horizontal = 16.dp, vertical = 8.dp)) { SnapshotImage( snapshot = snapshot, + headerEndPadding = SnapshotActionSize * 3 - 10.dp, modifier = Modifier.drawWithContent { layer.record { this@drawWithContent.drawContent() } drawLayer(layer) }, ) - Row(Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceEvenly) { - SnapshotAction(DeckIcons.Share, "Share", primary = true) { + Row(Modifier.align(Alignment.TopEnd).padding(top = 6.dp, end = 6.dp)) { + SnapshotAction(DeckIcons.Share, "Share snapshot") { scope.launch { val file = saveSnapshot(context, layer) ?: return@launch context.shareFiles(listOf(file), text = snapshot.toText(), subject = "${snapshot.appName} device & app info") } } - SnapshotAction(DeckIcons.Download, "Save") { + SnapshotAction(DeckIcons.Download, "Save image") { scope.launch { val saved = saveSnapshot(context, layer) != null Toast.makeText( @@ -85,36 +85,22 @@ public fun DeviceSnapshotCard(modifier: Modifier = Modifier, buildInfo: () -> Ma ).show() } } - SnapshotAction(DeckIcons.ContentCopy, "Copy text") { context.copyToClipboard("Device & app info", snapshot.toText()) } + SnapshotAction(DeckIcons.ContentCopy, "Copy as text") { context.copyToClipboard("Device & app info", snapshot.toText()) } } } } @Composable -private fun SnapshotAction(icon: ImageVector, label: String, primary: Boolean = false, onClick: () -> Unit) { - val colors = MaterialTheme.colorScheme - Column( - Modifier - .clip(RoundedCornerShape(12.dp)) - .clickable(role = Role.Button, onClickLabel = label, onClick = onClick) - .padding(horizontal = 12.dp, vertical = 6.dp), - horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.spacedBy(6.dp), - ) { - Box( - Modifier - .size(48.dp) - .background(if (primary) colors.primary else colors.primary.copy(alpha = 0.12f), CircleShape), - contentAlignment = Alignment.Center, - ) { - Icon(icon, contentDescription = null, tint = if (primary) colors.onPrimary else colors.primary, modifier = Modifier.size(22.dp)) - } - Text(label, style = MaterialTheme.typography.labelMedium, color = colors.onSurface) +private fun SnapshotAction(icon: ImageVector, description: String, onClick: () -> Unit) { + IconButton(onClick = onClick, modifier = Modifier.size(SnapshotActionSize)) { + Icon(icon, contentDescription = description, tint = MaterialTheme.colorScheme.primary, modifier = Modifier.size(20.dp)) } } +private val SnapshotActionSize = 40.dp + @Composable -private fun SnapshotImage(snapshot: SystemSnapshot, modifier: Modifier = Modifier) { +private fun SnapshotImage(snapshot: SystemSnapshot, modifier: Modifier = Modifier, headerEndPadding: Dp = 0.dp) { val shape = RoundedCornerShape(12.dp) val palette = if (LocalDeckColors.current?.isDark ?: isSystemInDarkTheme()) DarkSnapshotPalette else LightSnapshotPalette Column( @@ -125,7 +111,13 @@ private fun SnapshotImage(snapshot: SystemSnapshot, modifier: Modifier = Modifie .padding(16.dp), verticalArrangement = Arrangement.spacedBy(6.dp), ) { - Text(snapshot.appName, style = MaterialTheme.typography.titleLarge, fontWeight = FontWeight.Bold, color = palette.text) + Text( + snapshot.appName, + style = MaterialTheme.typography.titleLarge, + fontWeight = FontWeight.Bold, + color = palette.text, + modifier = Modifier.padding(end = headerEndPadding), + ) Text("Device & app snapshot", style = MaterialTheme.typography.bodyMedium, color = palette.muted) snapshot.sections.forEach { section -> HorizontalDivider(Modifier.padding(vertical = 4.dp), color = palette.border) diff --git a/devdeck/src/main/kotlin/io/devdeck/device/PermissionsList.kt b/devdeck/src/main/kotlin/io/devdeck/device/PermissionsList.kt index 29e7537..106c708 100644 --- a/devdeck/src/main/kotlin/io/devdeck/device/PermissionsList.kt +++ b/devdeck/src/main/kotlin/io/devdeck/device/PermissionsList.kt @@ -29,6 +29,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalView +import androidx.compose.ui.platform.LocalWindowInfo import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp import androidx.lifecycle.Lifecycle @@ -47,6 +48,9 @@ public fun PermissionsList(modifier: Modifier = Modifier) { var version by remember { mutableIntStateOf(0) } val permissions = remember(context, version) { AppPermissions.load(context) } RefreshOnResume { version++ } + // Permission dialogs and settings screens take window focus, so regaining it means a result may have landed. + val windowFocused = LocalWindowInfo.current.isWindowFocused + LaunchedEffect(windowFocused) { if (windowFocused) version++ } Column(modifier.fillMaxWidth()) { if (permissions.isEmpty()) { TextItem("No permissions requested") @@ -103,6 +107,9 @@ private fun PermissionAction(permission: AppPermission) { permission.kind == PermissionKind.Unavailable -> Note("Not on this Android version") + permission.granted && permission.name in AppPermissions.pendingRevocations -> + Note("Revoked when the app restarts", MaterialTheme.colorScheme.error) + permission.kind == PermissionKind.Special -> ActionPill(text = "Open settings", onClick = { AppPermissions.openSettings(context, permission) }, compact = true) diff --git a/devdeck/src/main/kotlin/io/devdeck/internal/DeckIcons.kt b/devdeck/src/main/kotlin/io/devdeck/internal/DeckIcons.kt index bd9cce6..db5f69f 100644 --- a/devdeck/src/main/kotlin/io/devdeck/internal/DeckIcons.kt +++ b/devdeck/src/main/kotlin/io/devdeck/internal/DeckIcons.kt @@ -72,6 +72,9 @@ internal object DeckIcons { val Download: ImageVector by lazy { icon("Download", "M19,9h-4V3H9v6H5l7,7 7,-7zM5,18v2h14v-2H5z") } + val Send: ImageVector by lazy { + icon("Send", "M2.01,21L23,12 2.01,3 2,10l15,2 -15,2z") + } val Settings: ImageVector by lazy { icon( "Settings", diff --git a/devdeck/src/main/kotlin/io/devdeck/network/NetworkInspector.kt b/devdeck/src/main/kotlin/io/devdeck/network/NetworkInspector.kt index f37f74c..141cbd2 100644 --- a/devdeck/src/main/kotlin/io/devdeck/network/NetworkInspector.kt +++ b/devdeck/src/main/kotlin/io/devdeck/network/NetworkInspector.kt @@ -1,6 +1,7 @@ package io.devdeck.network import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.material3.Icon @@ -17,6 +18,7 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import io.devdeck.components.FilterPill import io.devdeck.components.FilterPillRow @@ -60,12 +62,23 @@ public fun NetworkInspector( val endpoints = remember(calls) { calls.distinctBy { it.groupKey }.size } Column(modifier.fillMaxWidth()) { - Text( - "${calls.size} requests · $endpoints endpoints · $failures failed", - style = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.onSurfaceVariant, - modifier = Modifier.padding(horizontal = 16.dp, vertical = 6.dp), - ) + Row(Modifier.fillMaxWidth().padding(start = 16.dp, end = 4.dp), verticalAlignment = Alignment.CenterVertically) { + Text( + "${calls.size} requests · $endpoints endpoints · $failures failed", + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + modifier = Modifier.weight(1f), + ) + IconButton(onClick = NetworkLogger::clear, enabled = calls.isNotEmpty()) { + Icon( + DeckIcons.Delete, + contentDescription = "Clear requests", + tint = MaterialTheme.colorScheme.error.copy(alpha = if (calls.isEmpty()) 0.38f else 1f), + ) + } + } SegmentedPills( options = NetworkView.entries, selected = view, @@ -74,7 +87,7 @@ public fun NetworkInspector( modifier = Modifier.padding(horizontal = 16.dp, vertical = 6.dp), ) SearchField(query = query, onQueryChange = { query = it }, placeholder = "Search URL, operation, status") - FilterPillRow(verticalAlignment = Alignment.CenterVertically) { + FilterPillRow { FilterPill(text = "All ${calls.size}", selected = filter == FILTER_ALL, onClick = { filter = FILTER_ALL }) FilterPill( text = "Errors $failures", @@ -92,13 +105,6 @@ public fun NetworkInspector( showDot = true, ) } - IconButton(onClick = NetworkLogger::clear, enabled = calls.isNotEmpty()) { - Icon( - DeckIcons.Delete, - contentDescription = "Clear requests", - tint = MaterialTheme.colorScheme.error.copy(alpha = if (calls.isEmpty()) 0.38f else 1f), - ) - } } when { visible.isEmpty() -> TextItem(if (calls.isEmpty()) "No requests yet" else "No requests match") diff --git a/sample/README.md b/sample/README.md index e8200f3..efb0941 100644 --- a/sample/README.md +++ b/sample/README.md @@ -213,5 +213,5 @@ DevDeckHost( 9. **Storage:** change a flag, then open *System → Shared preferences → devdeck_flags* to see and edit the raw value. 10. **Deep link:** in *App → Open a deep link*, tap the `devdeck://sample/legacy` suggestion. 11. **Screen capture and bug report:** take a screenshot, then attach it in *Info → Generate a bug report* and share. - - Open *Info → Device & app snapshot* and tap *Share image*. The saved PNG also appears in the capture gallery and can be attached to bug reports. + - Open *Info → Device & app snapshot* and tap the share icon in the top-right corner of the card. The save icon puts the PNG in the capture gallery, where it can be attached to bug reports, and the copy icon copies the details as text. 12. **Two-finger swipe:** swipe left with two fingers over any control on either screen. The drawer opens, and single-finger taps and scrolls on the screen keep working as normal. diff --git a/sample/src/main/res/values/strings.xml b/sample/src/main/res/values/strings.xml index e96f67e..436bf45 100644 --- a/sample/src/main/res/values/strings.xml +++ b/sample/src/main/res/values/strings.xml @@ -1,5 +1,5 @@ - DevDeck Sample + DevDeck Plain View-based screen.\n\nThe drawer here comes from DevDeck.install() in SampleApp. Swipe left with two fingers, tap the floating bug, press Ctrl + Shift + D, or use the buttons. Open drawer Toggle drawer availability