Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,17 @@ updates:
interval: weekly
open-pull-requests-limit: 5
labels: [dependencies, skip-changelog]
ignore:
- dependency-name: "androidx.*"
update-types: ["version-update:semver-major", "version-update:semver-minor"]
- dependency-name: "org.jetbrains.kotlin*"
update-types: ["version-update:semver-major", "version-update:semver-minor"]
- dependency-name: "com.squareup.okhttp3:*"
update-types: ["version-update:semver-major"]
- dependency-name: "com.jakewharton.timber:*"
update-types: ["version-update:semver-major"]
- dependency-name: "com.google.firebase:*"
update-types: ["version-update:semver-major"]
groups:
androidx-and-compose:
patterns: ["androidx.*"]
Expand Down
37 changes: 37 additions & 0 deletions .github/workflows/changelog.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
name: Changelog

on:
pull_request:
types: [opened, synchronize, reopened, labeled, unlabeled]

concurrency:
group: changelog-${{ github.ref }}
cancel-in-progress: true

permissions:
contents: read

jobs:
changelog:
name: Changelog
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1
with:
fetch-depth: 0
persist-credentials: false
- name: Require a CHANGELOG.md entry
env:
SKIP: ${{ contains(github.event.pull_request.labels.*.name, 'skip-changelog') }}
BASE_REF: ${{ github.base_ref }}
run: |
if [ "$SKIP" = "true" ]; then
echo "skip-changelog label set"
exit 0
fi
if git diff --name-only "origin/${BASE_REF}...HEAD" | grep -qx 'CHANGELOG.md'; then
echo "CHANGELOG.md updated"
else
echo "::error::Add an entry under [Unreleased] in CHANGELOG.md, or add the skip-changelog label."
exit 1
fi
25 changes: 0 additions & 25 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -58,28 +58,3 @@ jobs:
name: test-reports
path: '*/build/reports/tests/'
if-no-files-found: ignore

changelog:
name: Changelog
if: github.event_name == 'pull_request'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1
with:
fetch-depth: 0
persist-credentials: false
- name: Require a CHANGELOG.md entry
env:
SKIP: ${{ contains(github.event.pull_request.labels.*.name, 'skip-changelog') }}
BASE_REF: ${{ github.base_ref }}
run: |
if [ "$SKIP" = "true" ]; then
echo "skip-changelog label set"
exit 0
fi
if git diff --name-only "origin/${BASE_REF}...HEAD" | grep -qx 'CHANGELOG.md'; then
echo "CHANGELOG.md updated"
else
echo "::error::Add an entry under [Unreleased] in CHANGELOG.md, or add the skip-changelog label."
exit 1
fi
Original file line number Diff line number Diff line change
Expand Up @@ -50,10 +50,12 @@ public class DevDeckOkHttpInterceptor(
val response = try {
when (effect) {
is NetworkEffect.Fail -> simulateFailure(chain, effect.failure)

is NetworkEffect.Slow -> {
pause(chain, effect.delayMillis)
chain.proceed(request)
}

null -> chain.proceed(request)
}
} catch (error: Exception) {
Expand Down Expand Up @@ -89,10 +91,12 @@ public class DevDeckOkHttpInterceptor(
when (failure) {
NetworkFailure.NoConnection ->
throw UnknownHostException("Unable to resolve host \"${request.url.host}\": No address associated with hostname $SIMULATED")

NetworkFailure.Timeout -> {
pause(chain, chain.readTimeoutMillis().takeIf { it > 0 }?.toLong() ?: DEFAULT_TIMEOUT_MILLIS)
throw SocketTimeoutException("timeout $SIMULATED")
}

NetworkFailure.ServerError -> return serverError(request)
}
}
Expand All @@ -115,10 +119,11 @@ public class DevDeckOkHttpInterceptor(
val deadline = System.nanoTime() + TimeUnit.MILLISECONDS.toNanos(millis)
while (true) {
if (chain.call().isCanceled()) throw IOException("Canceled")
val remaining = TimeUnit.NANOSECONDS.toMillis(deadline - System.nanoTime())
if (remaining <= 0) return
val remainingNanos = deadline - System.nanoTime()
if (remainingNanos <= 0) return
val step = minOf(remainingNanos, TimeUnit.MILLISECONDS.toNanos(PAUSE_STEP_MILLIS))
try {
Thread.sleep(minOf(remaining, PAUSE_STEP_MILLIS))
Thread.sleep(step / NANOS_PER_MILLI, (step % NANOS_PER_MILLI).toInt())
} catch (error: InterruptedException) {
Thread.currentThread().interrupt()
throw InterruptedIOException("Interrupted").apply { initCause(error) }
Expand Down Expand Up @@ -179,6 +184,7 @@ public class DevDeckOkHttpInterceptor(
const val HTTP_UNAVAILABLE = 503
const val DEFAULT_TIMEOUT_MILLIS = 10_000L
const val PAUSE_STEP_MILLIS = 50L
const val NANOS_PER_MILLI = 1_000_000L
const val LARGE_BODY_FACTOR = 4
const val THROTTLE_CHUNK_BYTES = 8_192L
val STREAMING_SUBTYPES = listOf("event-stream", "x-mixed-replace", "x-ndjson")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,7 @@ internal object BreadcrumbCollector {
downX = event.rawX
downY = event.rawY
}

MotionEvent.ACTION_UP -> if (taps && abs(event.rawX - downX) < slop && abs(event.rawY - downY) < slop) {
val label = describeTap(activity, event.rawX.toInt(), event.rawY.toInt())
if (label !=
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -104,12 +104,14 @@ private fun loadThumbnail(file: File): ImageBitmap? = runCatching {
val sample = (maxOf(bounds.outWidth, bounds.outHeight) / THUMBNAIL_SIZE_PX).coerceAtLeast(1)
BitmapFactory.decodeFile(file.path, BitmapFactory.Options().apply { inSampleSize = sample })?.asImageBitmap()
}

"mp4" -> if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
android.media.ThumbnailUtils.createVideoThumbnail(file, android.util.Size(THUMBNAIL_SIZE_PX, THUMBNAIL_SIZE_PX), null)
.asImageBitmap()
} else {
null
}

else -> null
}
}.getOrNull()
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ public class ScreenRecordingService : Service() {
start(resultCode, data)
}
}

ACTION_STOP -> finish()
}
return START_NOT_STICKY
Expand Down
2 changes: 2 additions & 0 deletions devdeck/src/main/kotlin/io/devdeck/console/ConsoleViewer.kt
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,7 @@ public fun ConsoleViewer(
}
when {
visible.isEmpty() -> TextItem(if (source.isEmpty()) "Nothing logged yet" else "No lines match")

view == ConsoleView.ByTag -> {
val groups = visible.groupBy { it.tag }.entries.sortedByDescending { it.value.last().timestampMillis }
InfoGroup(dividerThreshold = LIST_DIVIDER_THRESHOLD) {
Expand All @@ -134,6 +135,7 @@ public fun ConsoleViewer(
}
}
}

else -> {
InfoGroup(dividerThreshold = LIST_DIVIDER_THRESHOLD) {
visible.asReversed().take(maxRendered).forEach { line -> ConsoleLineRow(line, showTag = fixedTag == null) }
Expand Down
10 changes: 10 additions & 0 deletions devdeck/src/main/kotlin/io/devdeck/device/AppPermissions.kt
Original file line number Diff line number Diff line change
Expand Up @@ -103,23 +103,33 @@ internal object AppPermissions {

private fun specialGranted(context: Context, name: String): Boolean? = when {
name == SYSTEM_ALERT_WINDOW -> Settings.canDrawOverlays(context)

name == WRITE_SETTINGS -> Settings.System.canWrite(context)

name == REQUEST_INSTALL_PACKAGES && Build.VERSION.SDK_INT >= Build.VERSION_CODES.O ->
context.packageManager.canRequestPackageInstalls()

name == MANAGE_EXTERNAL_STORAGE && Build.VERSION.SDK_INT >= Build.VERSION_CODES.R -> Environment.isExternalStorageManager()

name == SCHEDULE_EXACT_ALARM && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S ->
(context.getSystemService(Context.ALARM_SERVICE) as AlarmManager).canScheduleExactAlarms()

else -> null
}

private fun specialSettingsIntent(context: Context, name: String): Intent? {
val action = when {
name == SYSTEM_ALERT_WINDOW -> Settings.ACTION_MANAGE_OVERLAY_PERMISSION

name == WRITE_SETTINGS -> Settings.ACTION_MANAGE_WRITE_SETTINGS

name == REQUEST_INSTALL_PACKAGES && Build.VERSION.SDK_INT >= Build.VERSION_CODES.O -> Settings.ACTION_MANAGE_UNKNOWN_APP_SOURCES

name == MANAGE_EXTERNAL_STORAGE && Build.VERSION.SDK_INT >= Build.VERSION_CODES.R ->
Settings.ACTION_MANAGE_APP_ALL_FILES_ACCESS_PERMISSION

name == SCHEDULE_EXACT_ALARM && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> Settings.ACTION_REQUEST_SCHEDULE_EXACT_ALARM

else -> return null
}
return Intent(action, packageUri(context))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -100,11 +100,15 @@ private fun PermissionAction(permission: AppPermission) {
val muted = MaterialTheme.colorScheme.onSurfaceVariant
when {
permission.kind == PermissionKind.InstallTime -> Note("Always granted at install")

permission.kind == PermissionKind.Unavailable -> Note("Not on this Android version")

permission.kind == PermissionKind.Special ->
ActionPill(text = "Open settings", onClick = { AppPermissions.openSettings(context, permission) }, compact = true)

!permission.granted && activity != null ->
ActionPill(text = "Request", onClick = { AppPermissions.request(activity, permission) }, compact = true)

AppPermissions.canRevoke(permission) && activity != null -> RevokePill(onRevoke = {
if (AppPermissions.revoke(activity, permission)) {
Toast.makeText(
Expand All @@ -114,6 +118,7 @@ private fun PermissionAction(permission: AppPermission) {
).show()
}
})

permission.granted -> Note("Revoke needs Android 13+ or App settings", muted)
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ internal class TwoFingerSwipeTracker(private val minDistancePx: Float) {
consumed = false
return false
}

pointers.size != FINGERS -> {
start = null
if (pointers.size > FINGERS) consumed = true
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,24 +47,29 @@ internal fun highlightJson(text: String, colors: JsonColors): AnnotatedString =
addStyle(SpanStyle(color = color), index, end)
index = end
}

char == '-' || char.isDigit() -> {
var end = index + 1
while (end < text.length && (text[end].isDigit() || text[end] in ".eE+-")) end++
addStyle(SpanStyle(color = colors.number), index, end)
index = end
}

text.startsWith("true", index) || text.startsWith("null", index) -> {
addStyle(SpanStyle(color = colors.literal), index, index + 4)
index += 4
}

text.startsWith("false", index) -> {
addStyle(SpanStyle(color = colors.literal), index, index + 5)
index += 5
}

char in "{}[]:," -> {
addStyle(SpanStyle(color = colors.punctuation), index, index + 1)
index++
}

else -> index++
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -102,11 +102,13 @@ public fun NetworkInspector(
}
when {
visible.isEmpty() -> TextItem(if (calls.isEmpty()) "No requests yet" else "No requests match")

view == NetworkView.Timeline -> InfoGroup(dividerThreshold = 2) {
visible.take(maxItems).forEach { call ->
NetworkCallCard(call, highlights, repeatCount = repeatCounts[call.groupKey] ?: 1)
}
}

else -> InfoGroup(dividerThreshold = 2) { groups.take(maxItems).forEach { group -> NetworkGroupCard(group, highlights) } }
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -69,12 +69,14 @@ public class FeatureFlagsPlugin(
enabled = flag.editable,
onCheckedChange = { store[flag] = it },
)

is FeatureFlag.Text -> TextInputItem(
title = flag.title,
value = store[flag],
enabled = flag.editable,
onValueCommit = { store[flag] = it },
)

is FeatureFlag.Number -> NumberInputItem(
title = flag.title,
value = store[flag],
Expand All @@ -83,6 +85,7 @@ public class FeatureFlagsPlugin(
range = flag.range,
onValueCommit = { store[flag] = it },
)

is FeatureFlag.Json -> JsonInputItem(
title = flag.title,
value = store[flag],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,9 @@ public object NetworkSimulator {
if (current.urlFilter.isNotEmpty() && !url.contains(current.urlFilter, ignoreCase = true)) return null
return when (current.condition) {
NetworkCondition.Normal -> null

NetworkCondition.Slow -> NetworkEffect.Slow(current.delayMillis, current.bytesPerSecond)

NetworkCondition.Failing ->
if (random.nextInt(100) < current.failureRatePercent) NetworkEffect.Fail(current.failure) else null
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -124,18 +124,25 @@ private fun PreferenceEditor(name: String, key: String) {
val editor = prefs.values
when (val value = editor.all[key]) {
null -> TextItem("This key no longer exists")

is Boolean -> SwitchItem(key, checked = value, subtitle = "Boolean", onCheckedChange = {
editor.edit { putBoolean(key, it) }
})

is Int -> NumberInputItem(key, value = value.toLong(), onValueCommit = { editor.edit { putInt(key, it.toInt()) } })

is Long -> NumberInputItem(key, value = value, onValueCommit = { editor.edit { putLong(key, it) } })

is Float -> NumberInputItem(key, value = value.toDouble(), onValueCommit = { editor.edit { putFloat(key, it.toFloat()) } })

is String -> if (Json.isJson(value)) {
JsonInputItem(key, value = value, onValueCommit = { editor.edit { putString(key, it) } })
} else {
TextInputItem(key, value = value, singleLine = false, onValueCommit = { editor.edit { putString(key, it) } })
}

is Set<*> -> value.forEach { BulletItem(it.toString()) }

else -> BulletItem(key, value.toString())
}
SectionDivider()
Expand Down
5 changes: 5 additions & 0 deletions devdeck/src/main/kotlin/io/devdeck/updates/UpdatesPanel.kt
Original file line number Diff line number Diff line change
Expand Up @@ -156,11 +156,16 @@ internal fun updateAvailableColor(isDark: Boolean): Color = if (isDark) Color(0x

private fun statusText(status: UpdateStatus, source: UpdateSource): String = when (status) {
UpdateStatus.Idle -> if (source.requiresSignIn && !UpdateChecker.isSignedIn) "Sign in to check ${source.name}" else "Not checked yet"

UpdateStatus.Checking -> "Checking ${source.name}…"

UpdateStatus.UpToDate -> "You're on the latest version · checked ${UpdateChecker.lastCheckedAtMillis?.let(::time).orEmpty()}"

is UpdateStatus.Available -> "${status.update.label} is available"

is UpdateStatus.Installing -> status.progress?.let { "Downloading ${status.update.label} · ${(it * 100).toInt()}%" }
?: "Preparing ${status.update.label}…"

is UpdateStatus.Failed -> "Check failed: ${status.message}"
}

Expand Down
2 changes: 1 addition & 1 deletion gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ core = "1.17.0"
okhttp = "4.12.0"
appdistribution = "16.0.0-beta21"
timber = "4.7.1"
ktlint = "1.3.1"
ktlint = "1.8.0"
junit = "4.13.2"
json = "20231013"

Expand Down
Loading