From 360b33eb825eee70718c10c81c8ef5826cafe364 Mon Sep 17 00:00:00 2001 From: Alex Vanyo Date: Wed, 25 Oct 2023 17:55:03 -0700 Subject: [PATCH] Add automatic checks for badging Change-Id: Ic8fca86d7aa84675fa91fb1aa99abcc6e19d663e --- .github/workflows/Build.yaml | 3 + app/prodRelease-badging.txt | 121 +++++++++++++++ .../AndroidApplicationConventionPlugin.kt | 4 + .../samples/apps/nowinandroid/Badging.kt | 143 ++++++++++++++++++ 4 files changed, 271 insertions(+) create mode 100644 app/prodRelease-badging.txt create mode 100644 build-logic/convention/src/main/kotlin/com/google/samples/apps/nowinandroid/Badging.kt diff --git a/.github/workflows/Build.yaml b/.github/workflows/Build.yaml index 4a4320590..ccea5dc4f 100644 --- a/.github/workflows/Build.yaml +++ b/.github/workflows/Build.yaml @@ -125,6 +125,9 @@ jobs: name: lint-reports path: '**/build/reports/lint-results-*.html' + - name: Check badging + run: ./gradlew :app:checkProdReleaseBadging + androidTest: needs: build runs-on: macOS-latest # enables hardware acceleration in the virtual machine diff --git a/app/prodRelease-badging.txt b/app/prodRelease-badging.txt new file mode 100644 index 000000000..6c3a859c7 --- /dev/null +++ b/app/prodRelease-badging.txt @@ -0,0 +1,121 @@ +package: name='com.google.samples.apps.nowinandroid' versionCode='8' versionName='0.1.2' platformBuildVersionName='14' platformBuildVersionCode='34' compileSdkVersion='34' compileSdkVersionCodename='14' +sdkVersion:'21' +targetSdkVersion:'34' +uses-permission: name='android.permission.INTERNET' +uses-permission: name='android.permission.ACCESS_NETWORK_STATE' +uses-permission: name='android.permission.POST_NOTIFICATIONS' +uses-permission: name='android.permission.WAKE_LOCK' +uses-permission: name='com.google.android.c2dm.permission.RECEIVE' +uses-permission: name='com.google.android.finsky.permission.BIND_GET_INSTALL_REFERRER_SERVICE' +uses-permission: name='android.permission.RECEIVE_BOOT_COMPLETED' +uses-permission: name='android.permission.FOREGROUND_SERVICE' +uses-permission: name='com.google.samples.apps.nowinandroid.DYNAMIC_RECEIVER_NOT_EXPORTED_PERMISSION' +application-label:'Now in Android' +application-label-af:'Now in Android' +application-label-am:'Now in Android' +application-label-ar:'Now in Android' +application-label-as:'Now in Android' +application-label-az:'Now in Android' +application-label-be:'Now in Android' +application-label-bg:'Now in Android' +application-label-bn:'Now in Android' +application-label-bs:'Now in Android' +application-label-ca:'Now in Android' +application-label-cs:'Now in Android' +application-label-da:'Now in Android' +application-label-de:'Now in Android' +application-label-el:'Now in Android' +application-label-en-AU:'Now in Android' +application-label-en-CA:'Now in Android' +application-label-en-GB:'Now in Android' +application-label-en-IN:'Now in Android' +application-label-en-XC:'Now in Android' +application-label-es:'Now in Android' +application-label-es-US:'Now in Android' +application-label-et:'Now in Android' +application-label-eu:'Now in Android' +application-label-fa:'Now in Android' +application-label-fi:'Now in Android' +application-label-fr:'Now in Android' +application-label-fr-CA:'Now in Android' +application-label-gl:'Now in Android' +application-label-gu:'Now in Android' +application-label-hi:'Now in Android' +application-label-hr:'Now in Android' +application-label-hu:'Now in Android' +application-label-hy:'Now in Android' +application-label-in:'Now in Android' +application-label-is:'Now in Android' +application-label-it:'Now in Android' +application-label-iw:'Now in Android' +application-label-ja:'Now in Android' +application-label-ka:'Now in Android' +application-label-kk:'Now in Android' +application-label-km:'Now in Android' +application-label-kn:'Now in Android' +application-label-ko:'Now in Android' +application-label-ky:'Now in Android' +application-label-lo:'Now in Android' +application-label-lt:'Now in Android' +application-label-lv:'Now in Android' +application-label-mk:'Now in Android' +application-label-ml:'Now in Android' +application-label-mn:'Now in Android' +application-label-mr:'Now in Android' +application-label-ms:'Now in Android' +application-label-my:'Now in Android' +application-label-nb:'Now in Android' +application-label-ne:'Now in Android' +application-label-nl:'Now in Android' +application-label-or:'Now in Android' +application-label-pa:'Now in Android' +application-label-pl:'Now in Android' +application-label-pt:'Now in Android' +application-label-pt-BR:'Now in Android' +application-label-pt-PT:'Now in Android' +application-label-ro:'Now in Android' +application-label-ru:'Now in Android' +application-label-si:'Now in Android' +application-label-sk:'Now in Android' +application-label-sl:'Now in Android' +application-label-sq:'Now in Android' +application-label-sr:'Now in Android' +application-label-sr-Latn:'Now in Android' +application-label-sv:'Now in Android' +application-label-sw:'Now in Android' +application-label-ta:'Now in Android' +application-label-te:'Now in Android' +application-label-th:'Now in Android' +application-label-tl:'Now in Android' +application-label-tr:'Now in Android' +application-label-uk:'Now in Android' +application-label-ur:'Now in Android' +application-label-uz:'Now in Android' +application-label-vi:'Now in Android' +application-label-zh-CN:'Now in Android' +application-label-zh-HK:'Now in Android' +application-label-zh-TW:'Now in Android' +application-label-zu:'Now in Android' +application-icon-120:'res/mipmap-anydpi-v26/ic_launcher.xml' +application-icon-160:'res/mipmap-anydpi-v26/ic_launcher.xml' +application-icon-240:'res/mipmap-anydpi-v26/ic_launcher.xml' +application-icon-320:'res/mipmap-anydpi-v26/ic_launcher.xml' +application-icon-480:'res/mipmap-anydpi-v26/ic_launcher.xml' +application-icon-640:'res/mipmap-anydpi-v26/ic_launcher.xml' +application-icon-65534:'res/mipmap-anydpi-v26/ic_launcher.xml' +application: label='Now in Android' icon='res/mipmap-anydpi-v26/ic_launcher.xml' +launchable-activity: name='com.google.samples.apps.nowinandroid.MainActivity' label='' icon='' +uses-library-not-required:'androidx.window.extensions' +uses-library-not-required:'androidx.window.sidecar' +uses-library-not-required:'android.ext.adservices' +feature-group: label='' + uses-feature: name='android.hardware.faketouch' + uses-implied-feature: name='android.hardware.faketouch' reason='default feature for all apps' +main +other-activities +other-receivers +other-services +supports-screens: 'small' 'normal' 'large' 'xlarge' +supports-any-density: 'true' +locales: '--_--' 'af' 'am' 'ar' 'as' 'az' 'be' 'bg' 'bn' 'bs' 'ca' 'cs' 'da' 'de' 'el' 'en-AU' 'en-CA' 'en-GB' 'en-IN' 'en-XC' 'es' 'es-US' 'et' 'eu' 'fa' 'fi' 'fr' 'fr-CA' 'gl' 'gu' 'hi' 'hr' 'hu' 'hy' 'in' 'is' 'it' 'iw' 'ja' 'ka' 'kk' 'km' 'kn' 'ko' 'ky' 'lo' 'lt' 'lv' 'mk' 'ml' 'mn' 'mr' 'ms' 'my' 'nb' 'ne' 'nl' 'or' 'pa' 'pl' 'pt' 'pt-BR' 'pt-PT' 'ro' 'ru' 'si' 'sk' 'sl' 'sq' 'sr' 'sr-Latn' 'sv' 'sw' 'ta' 'te' 'th' 'tl' 'tr' 'uk' 'ur' 'uz' 'vi' 'zh-CN' 'zh-HK' 'zh-TW' 'zu' +densities: '120' '160' '240' '320' '480' '640' '65534' diff --git a/build-logic/convention/src/main/kotlin/AndroidApplicationConventionPlugin.kt b/build-logic/convention/src/main/kotlin/AndroidApplicationConventionPlugin.kt index 50baf3dc6..f73ed1478 100644 --- a/build-logic/convention/src/main/kotlin/AndroidApplicationConventionPlugin.kt +++ b/build-logic/convention/src/main/kotlin/AndroidApplicationConventionPlugin.kt @@ -17,11 +17,14 @@ import com.android.build.api.dsl.ApplicationExtension import com.google.samples.apps.nowinandroid.configureGradleManagedDevices import com.android.build.api.variant.ApplicationAndroidComponentsExtension +import com.android.build.gradle.BaseExtension +import com.google.samples.apps.nowinandroid.configureBadgingTasks import com.google.samples.apps.nowinandroid.configureKotlinAndroid import com.google.samples.apps.nowinandroid.configurePrintApksTask import org.gradle.api.Plugin import org.gradle.api.Project import org.gradle.kotlin.dsl.configure +import org.gradle.kotlin.dsl.getByType class AndroidApplicationConventionPlugin : Plugin { override fun apply(target: Project) { @@ -39,6 +42,7 @@ class AndroidApplicationConventionPlugin : Plugin { } extensions.configure { configurePrintApksTask(this) + configureBadgingTasks(extensions.getByType(), this) } } } diff --git a/build-logic/convention/src/main/kotlin/com/google/samples/apps/nowinandroid/Badging.kt b/build-logic/convention/src/main/kotlin/com/google/samples/apps/nowinandroid/Badging.kt new file mode 100644 index 000000000..609961a2f --- /dev/null +++ b/build-logic/convention/src/main/kotlin/com/google/samples/apps/nowinandroid/Badging.kt @@ -0,0 +1,143 @@ +/* + * Copyright 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 + * + * https://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.google.samples.apps.nowinandroid + +import com.android.build.api.artifact.SingleArtifact +import com.android.build.api.variant.ApplicationAndroidComponentsExtension +import com.android.build.gradle.BaseExtension +import org.gradle.api.DefaultTask +import org.gradle.api.GradleException +import org.gradle.api.Project +import org.gradle.api.file.DirectoryProperty +import org.gradle.api.file.RegularFileProperty +import org.gradle.api.tasks.Copy +import org.gradle.api.tasks.InputFile +import org.gradle.api.tasks.OutputDirectory +import org.gradle.api.tasks.OutputFile +import org.gradle.api.tasks.TaskAction +import org.gradle.kotlin.dsl.configure +import org.gradle.kotlin.dsl.register +import org.gradle.language.base.plugins.LifecycleBasePlugin +import org.gradle.process.ExecOperations +import java.io.File +import java.nio.file.Files +import javax.inject.Inject + +abstract class GenerateBadgingTask : DefaultTask() { + + @get:OutputFile + abstract val badging: RegularFileProperty + + @get:InputFile + abstract val apk: RegularFileProperty + + @get:InputFile + abstract val aapt2Executable: RegularFileProperty + + @get:Inject + abstract val execOperations: ExecOperations + + @TaskAction + fun taskAction() { + execOperations.exec { + commandLine( + aapt2Executable.get().asFile.absolutePath, + "dump", + "badging", + apk.get().asFile.absolutePath + ) + standardOutput = badging.asFile.get().outputStream() + } + } +} + +abstract class CheckBadgingTask : DefaultTask() { + + // In order for the task to be up-to-date when the inputs have not changed, + // the task must declare an output, even if it's not used. Tasks with no + // output are always run regardless of whether the inputs changed + @get:OutputDirectory + abstract val output: DirectoryProperty + + @get:InputFile + abstract val goldenBadging: RegularFileProperty + + @get:InputFile + abstract val generatedBadging: RegularFileProperty + + override fun getGroup(): String = LifecycleBasePlugin.VERIFICATION_GROUP + + @TaskAction + fun taskAction() { + if ( + Files.mismatch( + goldenBadging.get().asFile.toPath(), + generatedBadging.get().asFile.toPath() + ) != -1L + ) { + throw GradleException( + "Generated badging is different from golden badging! " + + "If this change is intended, run ./gradlew updateBadging" + ) + } + } +} + +fun Project.configureBadgingTasks( + baseExtension: BaseExtension, + componentsExtension: ApplicationAndroidComponentsExtension +) { + // Registers a callback to be called, when a new variant is configured + componentsExtension.onVariants { variant -> + // Registers a new task to verify the app bundle. + val generateBadging = tasks.register("generate${variant.name}Badging") { + apk.set( + variant.artifacts.get(SingleArtifact.APK_FROM_BUNDLE) + ) + aapt2Executable.set( + File( + baseExtension.sdkDirectory, + "build-tools/${baseExtension.buildToolsVersion}/aapt2" + ) + ) + + badging.set( + project.layout.buildDirectory.file( + "outputs/apk_from_bundle/${variant.name}/${variant.name}-badging.txt" + ) + ) + } + + tasks.register("update${variant.name}Badging") { + from(generateBadging.get().badging) + into(project.layout.projectDirectory) + } + + val checkBadgingTaskName = "check${variant.name}Badging" + tasks.register(checkBadgingTaskName) { + goldenBadging.set( + project.layout.projectDirectory.file("${variant.name}-badging.txt")) + generatedBadging.set( + generateBadging.get().badging + ) + + output.set( + project.layout.buildDirectory.dir("intermediates/$checkBadgingTaskName") + ) + } + } +}