From 189ee518b6eeb79d2fb31cb0d19813ceff5c6ed4 Mon Sep 17 00:00:00 2001 From: Shane <6071159+smashedr@users.noreply.github.com> Date: Thu, 8 Oct 2026 14:45:12 -0700 Subject: [PATCH 1/2] Add Display Metrics --- .../org/cssnr/deviceinfo/data/DeviceInfo.kt | 147 +++++++++++++++++- app/src/main/res/values/strings.xml | 14 ++ 2 files changed, 160 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/org/cssnr/deviceinfo/data/DeviceInfo.kt b/app/src/main/java/org/cssnr/deviceinfo/data/DeviceInfo.kt index a43ce99..a3f1a32 100644 --- a/app/src/main/java/org/cssnr/deviceinfo/data/DeviceInfo.kt +++ b/app/src/main/java/org/cssnr/deviceinfo/data/DeviceInfo.kt @@ -3,9 +3,11 @@ package org.cssnr.deviceinfo.data import android.app.LocaleManager import android.content.Context import android.content.pm.PackageManager +import android.hardware.display.DisplayManager import android.net.Uri import android.os.Build import android.provider.Settings +import android.view.Display import androidx.annotation.StringRes import androidx.core.net.toUri import org.cssnr.deviceinfo.R @@ -83,11 +85,12 @@ data class InfoCategory( * process died. The UI resolves every label and every localized value during composition, where a * locale change reaches it. * - * The three groups are cut by what a fact is about rather than by where the field lives in the + * The four groups are cut by what a fact is about rather than by where the field lives in the * framework, because the copy actions are per group and a report is only useful if selecting one * group selects one kind of thing. `Build` fields are therefore spread across `device` and `os` * rather than kept in a group of their own: * - `device` is the physical hardware: who made it, what board it is, what chip is in it. + * - `display` is the physical screen: its native resolution, density and refresh capabilities. * - `os` is the software build: the Android version, the build that identifies it, the runtime it * runs on. * - `system` is everything the platform does rather than is: the boot chain, the Google components @@ -100,6 +103,7 @@ object DeviceInfoCollector { fun collect(context: Context): List = listOf( osCategory(context), deviceCategory(), + displayCategory(context), systemCategory(context), ) @@ -210,6 +214,103 @@ object DeviceInfoCollector { }, ) + /** + * The physical screen, read off the default [Display] plus `DisplayMetrics`. + * + * `resolution` is the panel's native size from the active `Display.Mode`; `absolute_resolution` + * is the app-visible size from `DisplayMetrics`, which is smaller once decor, cutouts and + * compatibility scaling are subtracted. Brightness, brightness mode and orientation are + * deliberately left out: they are momentary state rather than device facts, and this snapshot + * is collected once per process. + * + * The [Display] comes from `DisplayManager.getDisplay(Display.DEFAULT_DISPLAY)` rather than a + * window, so an application context is enough and there is nothing an Activity would add. When + * the display is absent the mode rows are omitted and the `DisplayMetrics` rows still show. + */ + private fun displayCategory(context: Context): InfoCategory = InfoCategory( + id = "display", + titleRes = R.string.info_group_display, + items = buildList { + val metrics = context.resources.displayMetrics + val display = primaryDisplay(context) + val mode = display?.mode + + if (mode != null) { + item( + "display.resolution", + R.string.info_display_resolution, + "${mode.physicalWidth} x ${mode.physicalHeight}", + ) + } + item( + "display.absolute_resolution", + R.string.info_display_absolute_resolution, + "${metrics.widthPixels} x ${metrics.heightPixels}", + ) + if (mode != null) { + item( + "display.aspect_ratio", + R.string.info_display_aspect_ratio, + aspectRatio(mode.physicalWidth, mode.physicalHeight), + ) + item( + "display.screen_size", + R.string.info_display_screen_size, + screenSizeInches(mode.physicalWidth, mode.physicalHeight, metrics.xdpi), + ) + } + item( + "display.screen_density", + R.string.info_display_screen_density, + metrics.xdpi.takeIf { it > 0f }?.let { "${it.toInt()} ppi" }, + ) + item( + "display.density_dpi", + R.string.info_display_density_dpi, + "${metrics.densityDpi} dpi", + ) + item( + "display.density", + R.string.info_display_density, + metrics.density.toString(), + ) + if (mode != null) { + item( + "display.refresh_rate", + R.string.info_display_refresh_rate, + "${Math.round(mode.refreshRate)} Hz", + ) + } + val supportedRates = display?.supportedModes + ?.map { Math.round(it.refreshRate) } + ?.distinct() + ?.sorted() + ?.joinToString(separator = " Hz, ", postfix = " Hz") { it.toString() } + item( + "display.supported_refresh_rates", + R.string.info_display_supported_refresh_rates, + supportedRates, + ) + if (display != null) { + add( + InfoItem( + "display.hdr", + R.string.info_display_hdr, + InfoValue.Resource(if (display.isHdr) R.string.info_yes else R.string.info_no), + ), + ) + item("display.hdr_types", R.string.info_display_hdr_types, hdrTypes(display)) + add( + InfoItem( + "display.wide_color_gamut", + R.string.info_display_wide_color_gamut, + InfoValue.Resource(if (display.isWideColorGamut) R.string.info_yes else R.string.info_no), + ), + ) + } + }, + ) + private fun systemCategory(context: Context): InfoCategory = InfoCategory( id = "system", titleRes = R.string.info_group_system, @@ -271,6 +372,50 @@ object DeviceInfoCollector { private fun String.isKnown(): Boolean = isNotBlank() && !equals(Build.UNKNOWN, ignoreCase = true) + private fun primaryDisplay(context: Context): Display? = + context.getSystemService(DisplayManager::class.java)?.getDisplay(Display.DEFAULT_DISPLAY) + + /** Reduced `width:height` pair, e.g. 1080x2400 becomes 9:20. */ + private fun aspectRatio(width: Int, height: Int): String? { + if (width <= 0 || height <= 0) return null + fun gcd(a: Int, b: Int): Int = if (b == 0) a else gcd(b, a % b) + val divisor = gcd(width, height) + if (divisor <= 0) return null + return "${width / divisor}:${height / divisor}" + } + + /** Diagonal panel size from the native resolution and the physical x density. */ + private fun screenSizeInches(width: Int, height: Int, xdpi: Float): String? { + if (width <= 0 || height <= 0 || xdpi <= 0f) return null + val w = width / xdpi.toDouble() + val h = height / xdpi.toDouble() + return String.format(Locale.US, "%.2f inches", Math.sqrt(w * w + h * h)) + } + + /** + * Human names for the display's HDR types, or null when it reports none. + * + * Matched on `HdrCapabilities` constants rather than raw ints. An unrecognized type is + * dropped; a display advertising only those reads as having no known HDR. + * + * Still read off `HdrCapabilities` rather than `Mode.getSupportedHdrTypes()`: that + * replacement needs API 34 and this collector starts at 26. + */ + @Suppress("DEPRECATION") + private fun hdrTypes(display: Display): String? { + val supported = display.hdrCapabilities?.supportedHdrTypes ?: IntArray(0) + val types = mutableListOf() + for (type in supported) { + when (type) { + Display.HdrCapabilities.HDR_TYPE_DOLBY_VISION -> types.add("Dolby Vision") + Display.HdrCapabilities.HDR_TYPE_HDR10 -> types.add("HDR10") + Display.HdrCapabilities.HDR_TYPE_HLG -> types.add("Hybrid Log-Gamma") + Display.HdrCapabilities.HDR_TYPE_HDR10_PLUS -> types.add("HDR10+") + } + } + return types.takeIf { it.isNotEmpty() }?.joinToString() + } + private fun property(key: String): String? = System.getProperty(key) /** diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 1bc74a8..1686f7e 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -64,6 +64,20 @@ Google Services Framework ID Rooted + Display + Resolution + Absolute Resolution + Aspect Ratio + Screen Size + Screen Density + Density DPI + Density + Refresh Rate + Supported Refresh Rates + HDR + HDR Types + Wide Color Gamut + Yes No N/A From e7671905069a2cfacda1f7f0874c17bb5f9436d1 Mon Sep 17 00:00:00 2001 From: Shane <6071159+smashedr@users.noreply.github.com> Date: Thu, 8 Oct 2026 14:57:12 -0700 Subject: [PATCH 2/2] Update release.yaml --- .github/workflows/release.yaml | 191 ++++++++++++++++++--------------- 1 file changed, 106 insertions(+), 85 deletions(-) diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index 4e346dd..7942eab 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -12,6 +12,7 @@ concurrency: env: key_name: deviceinfo-release package_name: org.cssnr.deviceinfo + acra_id: 5 jobs: release: @@ -32,6 +33,11 @@ jobs: key_file: release.keystore gradle_file: app/build.gradle.kts signed_apk: app-release.apk + # Optional Step Variables + secret_properties: secret.properties + google_services: # app/google-services.json + play_store_channel: # internal + run_prepare: # yes steps: - name: "Checkout" @@ -85,42 +91,47 @@ jobs: cat ${{ env.gradle_file }} echo "::endgroup::" - #- name: "Write Google Services File" - # run: | - # echo "${{ secrets.GOOGLE_SERVICES }}" | base64 --decode > app/google-services.json - # stat app/google-services.json + - name: "Write Google Services File" + if: ${{ env.google_services }} + run: | + echo "${{ secrets.GOOGLE_SERVICES }}" | base64 --decode > ${{ env.google_services }} + wc -l ${{ env.google_services }} - name: "Write Secret Properties" + if: ${{ env.secret_properties }} run: | - echo "${{ secrets.SECRET_PROPERTIES }}" | base64 --decode > secret.properties - wc -l secret.properties + echo "${{ secrets.SECRET_PROPERTIES }}" | base64 --decode > ${{ env.secret_properties }} + wc -l ${{ env.secret_properties }} - name: "Write Keystore File" run: | echo "${{ secrets.ANDROID_KEYSTORE }}" | base64 --decode > ${{ env.key_file }} echo "${{ secrets.ANDROID_KEYSTORE_PASS }}" | keytool -list -keystore ${{ env.key_file }} - #- name: "Setup Node 24" - # uses: actions/setup-node@v7 - # with: - # node-version: 24 - # - #- name: "Prepare Build" - # working-directory: ".github/scripts" - # run: | - # bash prepare.sh - # - #- name: "Debug Prepare" - # continue-on-error: true - # run: | - # ls . - # ls -lAh app + - name: "Setup Node 24" + if: ${{ env.run_prepare }} + uses: actions/setup-node@v7 + with: + node-version: 24 + + - name: "Prepare Build" + if: ${{ env.run_prepare }} + working-directory: ".github/scripts" + run: | + bash prepare.sh + + - name: "Debug Prepare" + if: ${{ env.run_prepare }} + continue-on-error: true + run: | + ls . + ls -lAh app - name: "Setup Java" uses: actions/setup-java@v6 with: distribution: "zulu" - java-version: "17" + java-version: "21" #cache: "gradle" - name: "Gradle Assemble APK" @@ -160,6 +171,16 @@ jobs: name: mapping path: ${{ env.mapping_file }} + - name: "Create Version in Acrarium" + if: ${{ env.acra_id }} + continue-on-error: true + run: | + echo -n "created by release workflow" | curl -m 120 \ + -H "Content-Type: text/plain" \ + -u "${{ secrets.ACRARIUM_USER }}:${{ secrets.ACRARIUM_PASS }}" \ + --data-binary @- \ + "${{ secrets.ACRARIUM_HOST }}/api/apps/${{ env.acra_id }}/version/upload/${{ github.run_number }}?name=${{ github.ref_name }}" + - name: "Upload APK to Artifacts" uses: actions/upload-artifact@v7 with: @@ -173,69 +194,69 @@ jobs: files: ${{ env.apk_path }}/${{ env.signed_apk }} overwrite: true - #- name: "Gradle Bundle AAB" - # if: ${{ !github.event.release.prerelease }} - # run: | - # chmod +x ./gradlew - # ./gradlew bundleRelease - # - #- name: "Debug Bundle" - # if: ${{ !github.event.release.prerelease }} - # continue-on-error: true - # run: | - # echo "env.aab_path: ${{ env.aab_path }}" - # ls -lAh "${{ env.aab_path }}" ||: - # echo "env.debug_symbols: ${{ env.debug_symbols }}" - # ls -lAh "${{ env.debug_symbols }}" ||: - # echo "env.mapping_file: ${{ env.mapping_file }}" - # ls -lAh "$(dirname "${{ env.debug_symbols }}")" ||: - # - #- name: "Sign Bundle" - # if: ${{ !github.event.release.prerelease }} - # run: | - # apksigner sign --ks ${{ env.key_file }} \ - # --min-sdk-version 26 \ - # --v1-signing-enabled true \ - # --v2-signing-enabled true \ - # --ks-pass pass:${{ secrets.ANDROID_KEYSTORE_PASS }} \ - # --ks-key-alias ${{ env.key_name }} \ - # "${{ env.aab_path }}/${{ env.aab_file }}" - # - #- name: "Upload Bundle to Artifacts" - # if: ${{ !github.event.release.prerelease }} - # uses: actions/upload-artifact@v7 - # with: - # name: bundle - # path: ${{ env.aab_path }} - # - #- name: "Parse Release Notes" - # if: ${{ github.event_name == 'release' && !github.event.release.prerelease }} - # id: notes - # run: | - # body="${{ github.event.release.body }}" - # summary="$(echo "${body%%#*}" | tr -d '\r' | sed ':a;N;$!ba;s/\n*$//')" - # echo "summary<> "$GITHUB_OUTPUT" - # echo "$summary" >> "$GITHUB_OUTPUT" - # echo "EOF" >> "$GITHUB_OUTPUT" - # - #- name: "Generate Whats New" - # if: ${{ github.event_name == 'release' && !github.event.release.prerelease }} - # run: | - # mkdir -p whatsNew - # echo -n "${{ steps.notes.outputs.summary }}" > whatsNew/whatsnew-en-US - # - #- name: "Upload Google Play" - # if: ${{ github.event_name == 'release' && !github.event.release.prerelease }} - # uses: r0adkll/upload-google-play@v1 - # with: - # serviceAccountJsonPlainText: ${{ secrets.GOOGLE_SERVICE_ACCOUNT_JSON }} - # packageName: ${{ env.package_name }} - # releaseFiles: ${{ env.aab_path }}/${{ env.aab_file }} - # debugSymbols: ${{ env.debug_symbols }} - # mappingFile: ${{ env.mapping_file }} - # whatsNewDirectory: whatsNew - # releaseName: "${{ github.run_number }} (${{ github.ref_name }})" - # track: internal + - name: "Gradle Bundle AAB" + if: ${{ env.play_store_channel && !github.event.release.prerelease }} + run: | + chmod +x ./gradlew + ./gradlew bundleRelease + + - name: "Debug Bundle" + if: ${{ env.play_store_channel && !github.event.release.prerelease }} + continue-on-error: true + run: | + echo "env.aab_path: ${{ env.aab_path }}" + ls -lAh "${{ env.aab_path }}" ||: + echo "env.debug_symbols: ${{ env.debug_symbols }}" + ls -lAh "${{ env.debug_symbols }}" ||: + echo "env.mapping_file: ${{ env.mapping_file }}" + ls -lAh "$(dirname "${{ env.debug_symbols }}")" ||: + + - name: "Sign Bundle" + if: ${{ env.play_store_channel && !github.event.release.prerelease }} + run: | + apksigner sign --ks ${{ env.key_file }} \ + --min-sdk-version 26 \ + --v1-signing-enabled true \ + --v2-signing-enabled true \ + --ks-pass pass:${{ secrets.ANDROID_KEYSTORE_PASS }} \ + --ks-key-alias ${{ env.key_name }} \ + "${{ env.aab_path }}/${{ env.aab_file }}" + + - name: "Upload Bundle to Artifacts" + if: ${{ env.play_store_channel && !github.event.release.prerelease }} + uses: actions/upload-artifact@v7 + with: + name: bundle + path: ${{ env.aab_path }} + + - name: "Parse Release Notes" + if: ${{ env.play_store_channel && github.event_name == 'release' && !github.event.release.prerelease }} + id: notes + run: | + body="${{ github.event.release.body }}" + summary="$(echo "${body%%#*}" | tr -d '\r' | sed ':a;N;$!ba;s/\n*$//')" + echo "summary<> "$GITHUB_OUTPUT" + echo "$summary" >> "$GITHUB_OUTPUT" + echo "EOF" >> "$GITHUB_OUTPUT" + + - name: "Generate Whats New" + if: ${{ env.play_store_channel && github.event_name == 'release' && !github.event.release.prerelease }} + run: | + mkdir -p whatsNew + echo -n "${{ steps.notes.outputs.summary }}" > whatsNew/whatsnew-en-US + + - name: "Upload Google Play" + if: ${{ env.play_store_channel && github.event_name == 'release' && !github.event.release.prerelease }} + uses: r0adkll/upload-google-play@v1 + with: + serviceAccountJsonPlainText: ${{ secrets.GOOGLE_SERVICE_ACCOUNT_JSON }} + packageName: ${{ env.package_name }} + releaseFiles: ${{ env.aab_path }}/${{ env.aab_file }} + debugSymbols: ${{ env.debug_symbols }} + mappingFile: ${{ env.mapping_file }} + whatsNewDirectory: whatsNew + releaseName: "${{ github.run_number }} (${{ github.ref_name }})" + tracks: ${{ env.play_store_channel }} - name: "Update Release Notes Action" if: ${{ github.event_name == 'release' }}