diff --git a/.ci.yaml b/.ci.yaml index a6a88812b8..0025d663c0 100644 --- a/.ci.yaml +++ b/.ci.yaml @@ -1001,7 +1001,8 @@ targets: - bin/** - .ci.yaml - - name: Linux module_test + - name: Linux build_android_host_app_with_module_aar + bringup: true recipe: devicelab/devicelab_drone timeout: 60 properties: @@ -1013,7 +1014,27 @@ targets: ] tags: > ["devicelab", "hostonly", "linux"] - task_name: module_test + task_name: build_android_host_app_with_module_aar + runIf: + - dev/** + - packages/flutter_tools/** + - bin/** + - .ci.yaml + + - name: Linux build_android_host_app_with_module_source + bringup: true + recipe: devicelab/devicelab_drone + timeout: 60 + properties: + dependencies: >- + [ + {"dependency": "android_sdk", "version": "version:34v3"}, + {"dependency": "chrome_and_driver", "version": "version:125.0.6422.141"}, + {"dependency": "open_jdk", "version": "version:17"} + ] + tags: > + ["devicelab", "hostonly", "linux"] + task_name: build_android_host_app_with_module_source runIf: - dev/** - packages/flutter_tools/** @@ -4067,7 +4088,8 @@ targets: - bin/** - .ci.yaml - - name: Mac module_test + - name: Mac build_android_host_app_with_module_aar + bringup: true recipe: devicelab/devicelab_drone timeout: 60 properties: @@ -4078,7 +4100,26 @@ targets: ] tags: > ["devicelab", "hostonly", "mac"] - task_name: module_test + task_name: build_android_host_app_with_module_aar + runIf: + - dev/** + - packages/flutter_tools/** + - bin/** + - .ci.yaml + + - name: Mac build_android_host_app_with_module_source + bringup: true + recipe: devicelab/devicelab_drone + timeout: 60 + properties: + dependencies: >- + [ + {"dependency": "android_sdk", "version": "version:34v3"}, + {"dependency": "open_jdk", "version": "version:17"} + ] + tags: > + ["devicelab", "hostonly", "mac"] + task_name: build_android_host_app_with_module_source runIf: - dev/** - packages/flutter_tools/** @@ -5703,7 +5744,8 @@ targets: - bin/** - .ci.yaml - - name: Windows module_test + - name: Windows build_android_host_app_with_module_aar + bringup: true recipe: devicelab/devicelab_drone timeout: 60 properties: @@ -5715,7 +5757,27 @@ targets: ] tags: > ["devicelab", "hostonly", "windows"] - task_name: module_test + task_name: build_android_host_app_with_module_aar + runIf: + - dev/** + - packages/flutter_tools/** + - bin/** + - .ci.yaml + + - name: Windows build_android_host_app_with_module_source + bringup: true + recipe: devicelab/devicelab_drone + timeout: 60 + properties: + dependencies: >- + [ + {"dependency": "android_sdk", "version": "version:34v3"}, + {"dependency": "chrome_and_driver", "version": "version:125.0.6422.141"}, + {"dependency": "open_jdk", "version": "version:17"} + ] + tags: > + ["devicelab", "hostonly", "windows"] + task_name: build_android_host_app_with_module_source runIf: - dev/** - packages/flutter_tools/** diff --git a/TESTOWNERS b/TESTOWNERS index 6c86948765..fa5f96b2b1 100644 --- a/TESTOWNERS +++ b/TESTOWNERS @@ -265,7 +265,8 @@ /dev/devicelab/bin/tasks/macos_chrome_dev_mode.dart @andrewkolos @flutter/tool /dev/devicelab/bin/tasks/module_custom_host_app_name_test.dart @andrewkolos @flutter/tool /dev/devicelab/bin/tasks/module_host_with_custom_build_test.dart @andrewkolos @flutter/tool -/dev/devicelab/bin/tasks/module_test.dart @andrewkolos @flutter/tool +/dev/devicelab/bin/tasks/build_android_host_app_with_module_aar.dart @andrewkolos @flutter/tool +/dev/devicelab/bin/tasks/build_android_host_app_with_module_source.dart @gmackall @flutter/android /dev/devicelab/bin/tasks/module_test_ios.dart @jmagman @flutter/tool /dev/devicelab/bin/tasks/native_assets_ios_simulator.dart @dcharkes @flutter/ios /dev/devicelab/bin/tasks/native_ui_tests_macos.dart @cbracken @flutter/desktop diff --git a/dev/devicelab/bin/tasks/module_test.dart b/dev/devicelab/bin/tasks/build_android_host_app_with_module_aar.dart similarity index 98% rename from dev/devicelab/bin/tasks/module_test.dart rename to dev/devicelab/bin/tasks/build_android_host_app_with_module_aar.dart index 84118421ec..0b1f16820f 100644 --- a/dev/devicelab/bin/tasks/module_test.dart +++ b/dev/devicelab/bin/tasks/build_android_host_app_with_module_aar.dart @@ -35,16 +35,15 @@ TaskFunction combine(List tasks) { /// Tests that the Flutter module project template works and supports /// adding Flutter to an existing Android app. class ModuleTest { - ModuleTest( - this.buildTarget, { + ModuleTest({ this.gradleVersion = '7.6.3', }); - final String buildTarget; + static const String buildTarget = 'module-gradle'; final String gradleVersion; Future call() async { - section('Running: $buildTarget'); + section('Running: $buildTarget-$gradleVersion'); section('Find Java'); final String? javaHome = await findJavaHome(); @@ -228,6 +227,7 @@ class ModuleTest { flutterDirectory.path, 'dev', 'integration_tests', + 'pure_android_host_apps', 'android_host_app_v2_embedding', ), ), @@ -449,7 +449,7 @@ class ModuleTest { Future main() async { await task(combine([ // ignore: avoid_redundant_argument_values - ModuleTest('module-gradle-7.6', gradleVersion: '8.4').call, - ModuleTest('module-gradle-7.6', gradleVersion: '8.4-rc-3').call, + ModuleTest(gradleVersion: '8.4').call, + ModuleTest(gradleVersion: '8.4-rc-3').call, ])); } diff --git a/dev/devicelab/bin/tasks/build_android_host_app_with_module_source.dart b/dev/devicelab/bin/tasks/build_android_host_app_with_module_source.dart new file mode 100644 index 0000000000..542ba31f8c --- /dev/null +++ b/dev/devicelab/bin/tasks/build_android_host_app_with_module_source.dart @@ -0,0 +1,430 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:convert'; +import 'dart:io'; +import 'dart:typed_data'; + +import 'package:archive/archive.dart'; +import 'package:flutter_devicelab/framework/apk_utils.dart'; +import 'package:flutter_devicelab/framework/framework.dart'; +import 'package:flutter_devicelab/framework/task_result.dart'; +import 'package:flutter_devicelab/framework/utils.dart'; +import 'package:path/path.dart' as path; + +final String gradlew = Platform.isWindows ? 'gradlew.bat' : 'gradlew'; +final String gradlewExecutable = +Platform.isWindows ? '.\\$gradlew' : './$gradlew'; +final String fileReadWriteMode = Platform.isWindows ? 'rw-rw-rw-' : 'rw-r--r--'; +final String platformLineSep = Platform.isWindows ? '\r\n' : '\n'; + +/// Combines several TaskFunctions with trivial success value into one. +TaskFunction combine(List tasks) { + return () async { + for (final TaskFunction task in tasks) { + final TaskResult result = await task(); + if (result.failed) { + return result; + } + } + return TaskResult.success(null); + }; +} + +/// Tests that the Flutter module project template works and supports +/// adding Flutter to an existing Android app. +class ModuleTest { + ModuleTest({ + this.gradleVersion = '7.6.3', + }); + + static const String buildTarget = 'module-gradle'; + final String gradleVersion; + + Future call() async { + section('Running: $buildTarget-$gradleVersion'); + section('Find Java'); + + final String? javaHome = await findJavaHome(); + if (javaHome == null) { + return TaskResult.failure('Could not find Java'); + } + print('\nUsing JAVA_HOME=$javaHome'); + + section('Create Flutter module project'); + + final Directory tempDir = Directory.systemTemp.createTempSync('flutter_module_test.'); + final Directory projectDir = Directory(path.join(tempDir.path, 'hello')); + try { + await inDirectory(tempDir, () async { + await flutter( + 'create', + options: ['--org', 'io.flutter.devicelab', '--template=module', 'hello'], + ); + }); + + section('Create package with native assets'); + + await flutter( + 'config', + options: ['--enable-native-assets'], + ); + + const String ffiPackageName = 'ffi_package'; + await createFfiPackage(ffiPackageName, tempDir); + + section('Add FFI package'); + + final File pubspec = File(path.join(projectDir.path, 'pubspec.yaml')); + String content = await pubspec.readAsString(); + content = content.replaceFirst( + 'dependencies:$platformLineSep', + 'dependencies:$platformLineSep $ffiPackageName:$platformLineSep path: ..${Platform.pathSeparator}$ffiPackageName$platformLineSep', + ); + await pubspec.writeAsString(content, flush: true); + await inDirectory(projectDir, () async { + await flutter( + 'packages', + options: ['get'], + ); + }); + + section('Add read-only asset'); + + final File readonlyTxtAssetFile = await File(path.join( + projectDir.path, + 'assets', + 'read-only.txt' + )) + .create(recursive: true); + + if (!exists(readonlyTxtAssetFile)) { + return TaskResult.failure('Failed to create read-only asset'); + } + + if (!Platform.isWindows) { + await exec('chmod', [ + '444', + readonlyTxtAssetFile.path, + ]); + } + + content = content.replaceFirst( + '$platformLineSep # assets:$platformLineSep', + '$platformLineSep assets:$platformLineSep - assets/read-only.txt$platformLineSep', + ); + await pubspec.writeAsString(content, flush: true); + + section('Add plugins'); + + content = content.replaceFirst( + '${platformLineSep}dependencies:$platformLineSep', + '${platformLineSep}dependencies:$platformLineSep', + ); + await pubspec.writeAsString(content, flush: true); + await inDirectory(projectDir, () async { + await flutter( + 'packages', + options: ['get'], + ); + }); + + section('Build ephemeral host app'); + + await inDirectory(projectDir, () async { + await flutter( + 'build', + options: ['apk'], + ); + }); + + final bool ephemeralHostApkBuilt = exists(File(path.join( + projectDir.path, + 'build', + 'host', + 'outputs', + 'apk', + 'release', + 'app-release.apk', + ))); + + if (!ephemeralHostApkBuilt) { + return TaskResult.failure('Failed to build ephemeral host .apk'); + } + + section('Clean build'); + + await inDirectory(projectDir, () async { + await flutter('clean'); + }); + + section('Make Android host app editable'); + + await inDirectory(projectDir, () async { + await flutter( + 'make-host-app-editable', + options: ['android'], + ); + }); + + section('Build editable host app'); + + await inDirectory(projectDir, () async { + await flutter( + 'build', + options: ['apk'], + ); + }); + + final bool editableHostApkBuilt = exists(File(path.join( + projectDir.path, + 'build', + 'host', + 'outputs', + 'apk', + 'release', + 'app-release.apk', + ))); + + if (!editableHostApkBuilt) { + return TaskResult.failure('Failed to build editable host .apk'); + } + + section('Add to existing Android app'); + + final Directory hostApp = Directory(path.join(tempDir.path, 'hello_host_app')); + mkdir(hostApp); + recursiveCopy( + Directory( + path.join( + flutterDirectory.path, + 'dev', + 'integration_tests', + 'pure_android_host_apps', + 'host_app_kotlin_gradle_dsl', + ), + ), + hostApp, + ); + copy( + File(path.join(projectDir.path, '.android', gradlew)), + hostApp, + ); + copy( + File(path.join(projectDir.path, '.android', 'gradle', 'wrapper', + 'gradle-wrapper.jar')), + Directory(path.join(hostApp.path, 'gradle', 'wrapper')), + ); + + // Modify gradle version to passed in version. + // This is somehow the wrong file. + final File gradleWrapperProperties = File(path.join( + hostApp.path, 'gradle', 'wrapper', 'gradle-wrapper.properties')); + String propertyContent = await gradleWrapperProperties.readAsString(); + propertyContent = propertyContent.replaceFirst( + 'REPLACEME', + gradleVersion, + ); + section(propertyContent); + await gradleWrapperProperties.writeAsString(propertyContent, flush: true); + + final File analyticsOutputFile = + File(path.join(tempDir.path, 'analytics.log')); + + section('Build debug host APK'); + + await inDirectory(hostApp, () async { + if (!Platform.isWindows) { + await exec('chmod', ['+x', 'gradlew']); + } + await exec(gradlewExecutable, + ['app:assembleDebug'], + environment: { + 'JAVA_HOME': javaHome, + 'FLUTTER_ANALYTICS_LOG_FILE': analyticsOutputFile.path, + }, + ); + }); + + section('Check debug APK exists'); + + final String debugHostApk = path.join( + hostApp.path, + 'app', + 'build', + 'outputs', + 'apk', + 'debug', + 'app-debug.apk', + ); + if (!exists(File(debugHostApk))) { + return TaskResult.failure('Failed to build debug host APK'); + } + + section('Check files in debug APK'); + + checkCollectionContains([ + ...flutterAssets, + ...debugAssets, + ...baseApkFiles, + 'lib/arm64-v8a/lib$ffiPackageName.so', + 'lib/armeabi-v7a/lib$ffiPackageName.so', + ], await getFilesInApk(debugHostApk)); + + section('Check debug AndroidManifest.xml'); + + final String androidManifestDebug = await getAndroidManifest(debugHostApk); + if (!androidManifestDebug.contains(''' + ''') + ) { + return TaskResult.failure("Debug host APK doesn't contain metadata: flutterProjectType = module "); + } + + final String analyticsOutput = analyticsOutputFile.readAsStringSync(); + if (!analyticsOutput.contains('cd24: android') + || !analyticsOutput.contains('cd25: true') + || !analyticsOutput.contains('viewName: assemble')) { + return TaskResult.failure( + 'Building outer app produced the following analytics: "$analyticsOutput" ' + 'but not the expected strings: "cd24: android", "cd25: true" and ' + '"viewName: assemble"' + ); + } + + section('Check file access modes for read-only asset from Flutter module'); + + final String readonlyDebugAssetFilePath = path.joinAll([ + hostApp.path, + 'app', + 'build', + 'intermediates', + 'assets', + 'debug', + 'mergeDebugAssets', + 'flutter_assets', + 'assets', + 'read-only.txt', + ]); + final File readonlyDebugAssetFile = File(readonlyDebugAssetFilePath); + if (!exists(readonlyDebugAssetFile)) { + return TaskResult.failure('Failed to copy read-only asset file'); + } + + String modes = readonlyDebugAssetFile.statSync().modeString(); + print('\nread-only.txt file access modes = $modes'); + if (modes.compareTo(fileReadWriteMode) != 0) { + return TaskResult.failure('Failed to make assets user-readable and writable'); + } + + section('Build release host APK'); + + await inDirectory(hostApp, () async { + await exec(gradlewExecutable, + ['app:assembleRelease'], + environment: { + 'JAVA_HOME': javaHome, + 'FLUTTER_ANALYTICS_LOG_FILE': analyticsOutputFile.path, + }, + ); + }); + + final String releaseHostApk = path.join( + hostApp.path, + 'app', + 'build', + 'outputs', + 'apk', + 'release', + 'app-release-unsigned.apk', + ); + if (!exists(File(releaseHostApk))) { + return TaskResult.failure('Failed to build release host APK'); + } + + section('Check files in release APK'); + + checkCollectionContains([ + ...flutterAssets, + ...baseApkFiles, + 'lib/arm64-v8a/lib$ffiPackageName.so', + 'lib/arm64-v8a/libapp.so', + 'lib/arm64-v8a/libflutter.so', + 'lib/armeabi-v7a/lib$ffiPackageName.so', + 'lib/armeabi-v7a/libapp.so', + 'lib/armeabi-v7a/libflutter.so', + ], await getFilesInApk(releaseHostApk)); + + section('Check the NOTICE file is correct'); + + await inDirectory(hostApp, () async { + final File apkFile = File(releaseHostApk); + final Archive apk = ZipDecoder().decodeBytes(apkFile.readAsBytesSync()); + // Shouldn't be missing since we already checked it exists above. + final ArchiveFile? noticesFile = apk.findFile('assets/flutter_assets/NOTICES.Z'); + + final Uint8List? licenseData = noticesFile?.content as Uint8List?; + if (licenseData == null) { + return TaskResult.failure('Invalid license file.'); + } + final String licenseString = utf8.decode(gzip.decode(licenseData)); + if (!licenseString.contains('skia') || !licenseString.contains('Flutter Authors')) { + return TaskResult.failure('License content missing.'); + } + }); + + section('Check release AndroidManifest.xml'); + + final String androidManifestRelease = await getAndroidManifest(debugHostApk); + if (!androidManifestRelease.contains(''' + ''') + ) { + return TaskResult.failure("Release host APK doesn't contain metadata: flutterProjectType = module "); + } + + section('Check file access modes for read-only asset from Flutter module'); + + final String readonlyReleaseAssetFilePath = path.joinAll([ + hostApp.path, + 'app', + 'build', + 'intermediates', + 'assets', + 'release', + 'mergeReleaseAssets', + 'flutter_assets', + 'assets', + 'read-only.txt', + ]); + final File readonlyReleaseAssetFile = File(readonlyReleaseAssetFilePath); + if (!exists(readonlyReleaseAssetFile)) { + return TaskResult.failure('Failed to copy read-only asset file'); + } + + modes = readonlyReleaseAssetFile.statSync().modeString(); + print('\nread-only.txt file access modes = $modes'); + if (modes.compareTo(fileReadWriteMode) != 0) { + return TaskResult.failure('Failed to make assets user-readable and writable'); + } + + return TaskResult.success(null); + } on TaskResult catch (taskResult) { + return taskResult; + } catch (e) { + return TaskResult.failure(e.toString()); + } finally { + rmTree(tempDir); + } + } +} + +Future main() async { + await task(combine([ + // ignore: avoid_redundant_argument_values + ModuleTest(gradleVersion: '8.7').call, + ])); +} diff --git a/dev/devicelab/bin/tasks/module_custom_host_app_name_test.dart b/dev/devicelab/bin/tasks/module_custom_host_app_name_test.dart index fd12a871c7..9cb5a63787 100644 --- a/dev/devicelab/bin/tasks/module_custom_host_app_name_test.dart +++ b/dev/devicelab/bin/tasks/module_custom_host_app_name_test.dart @@ -178,6 +178,7 @@ Future main() async { flutterDirectory.path, 'dev', 'integration_tests', + 'pure_android_host_apps', 'android_custom_host_app', ), ), diff --git a/dev/integration_tests/pure_android_host_apps/README.md b/dev/integration_tests/pure_android_host_apps/README.md new file mode 100644 index 0000000000..37db4fe175 --- /dev/null +++ b/dev/integration_tests/pure_android_host_apps/README.md @@ -0,0 +1,2 @@ +This directory contains minimal Android apps used by integration tests for testing add-to-app +use cases. \ No newline at end of file diff --git a/dev/integration_tests/android_custom_host_app/README.md b/dev/integration_tests/pure_android_host_apps/android_custom_host_app/README.md similarity index 100% rename from dev/integration_tests/android_custom_host_app/README.md rename to dev/integration_tests/pure_android_host_apps/android_custom_host_app/README.md diff --git a/dev/integration_tests/android_custom_host_app/SampleApp/build.gradle b/dev/integration_tests/pure_android_host_apps/android_custom_host_app/SampleApp/build.gradle similarity index 100% rename from dev/integration_tests/android_custom_host_app/SampleApp/build.gradle rename to dev/integration_tests/pure_android_host_apps/android_custom_host_app/SampleApp/build.gradle diff --git a/dev/integration_tests/android_custom_host_app/SampleApp/src/main/AndroidManifest.xml b/dev/integration_tests/pure_android_host_apps/android_custom_host_app/SampleApp/src/main/AndroidManifest.xml similarity index 100% rename from dev/integration_tests/android_custom_host_app/SampleApp/src/main/AndroidManifest.xml rename to dev/integration_tests/pure_android_host_apps/android_custom_host_app/SampleApp/src/main/AndroidManifest.xml diff --git a/dev/integration_tests/android_custom_host_app/SampleApp/src/main/java/io/flutter/add2app/MainActivity.java b/dev/integration_tests/pure_android_host_apps/android_custom_host_app/SampleApp/src/main/java/io/flutter/add2app/MainActivity.java similarity index 100% rename from dev/integration_tests/android_custom_host_app/SampleApp/src/main/java/io/flutter/add2app/MainActivity.java rename to dev/integration_tests/pure_android_host_apps/android_custom_host_app/SampleApp/src/main/java/io/flutter/add2app/MainActivity.java diff --git a/dev/integration_tests/android_custom_host_app/build.gradle b/dev/integration_tests/pure_android_host_apps/android_custom_host_app/build.gradle similarity index 100% rename from dev/integration_tests/android_custom_host_app/build.gradle rename to dev/integration_tests/pure_android_host_apps/android_custom_host_app/build.gradle diff --git a/dev/integration_tests/android_custom_host_app/gradle.properties b/dev/integration_tests/pure_android_host_apps/android_custom_host_app/gradle.properties similarity index 100% rename from dev/integration_tests/android_custom_host_app/gradle.properties rename to dev/integration_tests/pure_android_host_apps/android_custom_host_app/gradle.properties diff --git a/dev/integration_tests/android_custom_host_app/gradle/wrapper/gradle-wrapper.properties b/dev/integration_tests/pure_android_host_apps/android_custom_host_app/gradle/wrapper/gradle-wrapper.properties similarity index 100% rename from dev/integration_tests/android_custom_host_app/gradle/wrapper/gradle-wrapper.properties rename to dev/integration_tests/pure_android_host_apps/android_custom_host_app/gradle/wrapper/gradle-wrapper.properties diff --git a/dev/integration_tests/android_custom_host_app/settings.gradle b/dev/integration_tests/pure_android_host_apps/android_custom_host_app/settings.gradle similarity index 100% rename from dev/integration_tests/android_custom_host_app/settings.gradle rename to dev/integration_tests/pure_android_host_apps/android_custom_host_app/settings.gradle diff --git a/dev/integration_tests/android_host_app_v2_embedding/README.md b/dev/integration_tests/pure_android_host_apps/android_host_app_v2_embedding/README.md similarity index 100% rename from dev/integration_tests/android_host_app_v2_embedding/README.md rename to dev/integration_tests/pure_android_host_apps/android_host_app_v2_embedding/README.md diff --git a/dev/integration_tests/android_host_app_v2_embedding/app/build.gradle b/dev/integration_tests/pure_android_host_apps/android_host_app_v2_embedding/app/build.gradle similarity index 100% rename from dev/integration_tests/android_host_app_v2_embedding/app/build.gradle rename to dev/integration_tests/pure_android_host_apps/android_host_app_v2_embedding/app/build.gradle diff --git a/dev/integration_tests/android_host_app_v2_embedding/app/src/main/AndroidManifest.xml b/dev/integration_tests/pure_android_host_apps/android_host_app_v2_embedding/app/src/main/AndroidManifest.xml similarity index 100% rename from dev/integration_tests/android_host_app_v2_embedding/app/src/main/AndroidManifest.xml rename to dev/integration_tests/pure_android_host_apps/android_host_app_v2_embedding/app/src/main/AndroidManifest.xml diff --git a/dev/integration_tests/android_host_app_v2_embedding/app/src/main/java/io/flutter/add2app/MainActivity.java b/dev/integration_tests/pure_android_host_apps/android_host_app_v2_embedding/app/src/main/java/io/flutter/add2app/MainActivity.java similarity index 100% rename from dev/integration_tests/android_host_app_v2_embedding/app/src/main/java/io/flutter/add2app/MainActivity.java rename to dev/integration_tests/pure_android_host_apps/android_host_app_v2_embedding/app/src/main/java/io/flutter/add2app/MainActivity.java diff --git a/dev/integration_tests/android_host_app_v2_embedding/build.gradle b/dev/integration_tests/pure_android_host_apps/android_host_app_v2_embedding/build.gradle similarity index 100% rename from dev/integration_tests/android_host_app_v2_embedding/build.gradle rename to dev/integration_tests/pure_android_host_apps/android_host_app_v2_embedding/build.gradle diff --git a/dev/integration_tests/android_host_app_v2_embedding/gradle.properties b/dev/integration_tests/pure_android_host_apps/android_host_app_v2_embedding/gradle.properties similarity index 100% rename from dev/integration_tests/android_host_app_v2_embedding/gradle.properties rename to dev/integration_tests/pure_android_host_apps/android_host_app_v2_embedding/gradle.properties diff --git a/dev/integration_tests/android_host_app_v2_embedding/gradle/wrapper/gradle-wrapper.properties b/dev/integration_tests/pure_android_host_apps/android_host_app_v2_embedding/gradle/wrapper/gradle-wrapper.properties similarity index 100% rename from dev/integration_tests/android_host_app_v2_embedding/gradle/wrapper/gradle-wrapper.properties rename to dev/integration_tests/pure_android_host_apps/android_host_app_v2_embedding/gradle/wrapper/gradle-wrapper.properties diff --git a/dev/integration_tests/android_host_app_v2_embedding/settings.gradle b/dev/integration_tests/pure_android_host_apps/android_host_app_v2_embedding/settings.gradle similarity index 100% rename from dev/integration_tests/android_host_app_v2_embedding/settings.gradle rename to dev/integration_tests/pure_android_host_apps/android_host_app_v2_embedding/settings.gradle diff --git a/dev/integration_tests/pure_android_host_apps/host_app_kotlin_gradle_dsl/.editorconfig b/dev/integration_tests/pure_android_host_apps/host_app_kotlin_gradle_dsl/.editorconfig new file mode 100644 index 0000000000..247372d591 --- /dev/null +++ b/dev/integration_tests/pure_android_host_apps/host_app_kotlin_gradle_dsl/.editorconfig @@ -0,0 +1,2 @@ +[*.{kt,kts}] +ktlint = disabled diff --git a/dev/integration_tests/pure_android_host_apps/host_app_kotlin_gradle_dsl/.gitignore b/dev/integration_tests/pure_android_host_apps/host_app_kotlin_gradle_dsl/.gitignore new file mode 100644 index 0000000000..aa724b7707 --- /dev/null +++ b/dev/integration_tests/pure_android_host_apps/host_app_kotlin_gradle_dsl/.gitignore @@ -0,0 +1,15 @@ +*.iml +.gradle +/local.properties +/.idea/caches +/.idea/libraries +/.idea/modules.xml +/.idea/workspace.xml +/.idea/navEditor.xml +/.idea/assetWizardSettings.xml +.DS_Store +/build +/captures +.externalNativeBuild +.cxx +local.properties diff --git a/dev/integration_tests/pure_android_host_apps/host_app_kotlin_gradle_dsl/README.md b/dev/integration_tests/pure_android_host_apps/host_app_kotlin_gradle_dsl/README.md new file mode 100644 index 0000000000..17eeba520f --- /dev/null +++ b/dev/integration_tests/pure_android_host_apps/host_app_kotlin_gradle_dsl/README.md @@ -0,0 +1,2 @@ +This directory contains a minimal Android app that uses the Kotlin DSL for its Gradle files. +It is used in add-to-app integration testing. \ No newline at end of file diff --git a/dev/integration_tests/pure_android_host_apps/host_app_kotlin_gradle_dsl/app/.gitignore b/dev/integration_tests/pure_android_host_apps/host_app_kotlin_gradle_dsl/app/.gitignore new file mode 100644 index 0000000000..42afabfd2a --- /dev/null +++ b/dev/integration_tests/pure_android_host_apps/host_app_kotlin_gradle_dsl/app/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/dev/integration_tests/pure_android_host_apps/host_app_kotlin_gradle_dsl/app/build.gradle.kts b/dev/integration_tests/pure_android_host_apps/host_app_kotlin_gradle_dsl/app/build.gradle.kts new file mode 100644 index 0000000000..80fc8488e9 --- /dev/null +++ b/dev/integration_tests/pure_android_host_apps/host_app_kotlin_gradle_dsl/app/build.gradle.kts @@ -0,0 +1,70 @@ +plugins { + alias(libs.plugins.android.application) + alias(libs.plugins.jetbrains.kotlin.android) +} + +android { + namespace = "com.example.myapplication" + compileSdk = 34 + + defaultConfig { + applicationId = "com.example.myapplication" + minSdk = 24 + targetSdk = 34 + versionCode = 1 + versionName = "1.0" + + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + vectorDrawables { + useSupportLibrary = true + } + } + + buildTypes { + release { + isMinifyEnabled = false + proguardFiles( + getDefaultProguardFile("proguard-android-optimize.txt"), + "proguard-rules.pro" + ) + } + } + compileOptions { + sourceCompatibility = JavaVersion.VERSION_1_8 + targetCompatibility = JavaVersion.VERSION_1_8 + } + kotlinOptions { + jvmTarget = "1.8" + } + buildFeatures { + compose = true + } + composeOptions { + kotlinCompilerExtensionVersion = "1.5.1" + } + packaging { + resources { + excludes += "/META-INF/{AL2.0,LGPL2.1}" + } + } +} + +dependencies { + + implementation(project(":flutter")) + implementation(libs.androidx.core.ktx) + implementation(libs.androidx.lifecycle.runtime.ktx) + implementation(libs.androidx.activity.compose) + implementation(platform(libs.androidx.compose.bom)) + implementation(libs.androidx.ui) + implementation(libs.androidx.ui.graphics) + implementation(libs.androidx.ui.tooling.preview) + implementation(libs.androidx.material3) + testImplementation(libs.junit) + androidTestImplementation(libs.androidx.junit) + androidTestImplementation(libs.androidx.espresso.core) + androidTestImplementation(platform(libs.androidx.compose.bom)) + androidTestImplementation(libs.androidx.ui.test.junit4) + debugImplementation(libs.androidx.ui.tooling) + debugImplementation(libs.androidx.ui.test.manifest) +} diff --git a/dev/integration_tests/pure_android_host_apps/host_app_kotlin_gradle_dsl/app/proguard-rules.pro b/dev/integration_tests/pure_android_host_apps/host_app_kotlin_gradle_dsl/app/proguard-rules.pro new file mode 100644 index 0000000000..481bb43481 --- /dev/null +++ b/dev/integration_tests/pure_android_host_apps/host_app_kotlin_gradle_dsl/app/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/dev/integration_tests/pure_android_host_apps/host_app_kotlin_gradle_dsl/app/src/androidTest/java/com/example/myapplication/ExampleInstrumentedTest.kt b/dev/integration_tests/pure_android_host_apps/host_app_kotlin_gradle_dsl/app/src/androidTest/java/com/example/myapplication/ExampleInstrumentedTest.kt new file mode 100644 index 0000000000..e9283cf4d8 --- /dev/null +++ b/dev/integration_tests/pure_android_host_apps/host_app_kotlin_gradle_dsl/app/src/androidTest/java/com/example/myapplication/ExampleInstrumentedTest.kt @@ -0,0 +1,24 @@ +package com.example.myapplication + +import androidx.test.platform.app.InstrumentationRegistry +import androidx.test.ext.junit.runners.AndroidJUnit4 + +import org.junit.Test +import org.junit.runner.RunWith + +import org.junit.Assert.* + +/** + * Instrumented test, which will execute on an Android device. + * + * See [testing documentation](http://d.android.com/tools/testing). + */ +@RunWith(AndroidJUnit4::class) +class ExampleInstrumentedTest { + @Test + fun useAppContext() { + // Context of the app under test. + val appContext = InstrumentationRegistry.getInstrumentation().targetContext + assertEquals("com.example.myapplication", appContext.packageName) + } +} \ No newline at end of file diff --git a/dev/integration_tests/pure_android_host_apps/host_app_kotlin_gradle_dsl/app/src/main/AndroidManifest.xml b/dev/integration_tests/pure_android_host_apps/host_app_kotlin_gradle_dsl/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000000..556be8696c --- /dev/null +++ b/dev/integration_tests/pure_android_host_apps/host_app_kotlin_gradle_dsl/app/src/main/AndroidManifest.xml @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/dev/integration_tests/pure_android_host_apps/host_app_kotlin_gradle_dsl/app/src/main/java/com/example/myapplication/MainActivity.kt b/dev/integration_tests/pure_android_host_apps/host_app_kotlin_gradle_dsl/app/src/main/java/com/example/myapplication/MainActivity.kt new file mode 100644 index 0000000000..9d5d8f1833 --- /dev/null +++ b/dev/integration_tests/pure_android_host_apps/host_app_kotlin_gradle_dsl/app/src/main/java/com/example/myapplication/MainActivity.kt @@ -0,0 +1,55 @@ +package com.example.myapplication + +import android.os.Bundle +import androidx.activity.ComponentActivity +import androidx.activity.compose.setContent +import androidx.activity.enableEdgeToEdge +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.tooling.preview.Preview +import com.example.myapplication.ui.theme.MyApplicationTheme +import io.flutter.embedding.android.FlutterActivity + +class MainActivity : ComponentActivity() { + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + enableEdgeToEdge() + setContent { + MyApplicationTheme { + Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding -> + Greeting( + name = "Android", + modifier = Modifier.padding(innerPadding) + ) + } + } + } + + startActivity( + FlutterActivity.createDefaultIntent(this) + ) + } +} + +@Composable +fun Greeting( + name: String, + modifier: Modifier = Modifier +) { + Text( + text = "Hello $name!", + modifier = modifier + ) +} + +@Preview(showBackground = true) +@Composable +fun GreetingPreview() { + MyApplicationTheme { + Greeting("Android") + } +} diff --git a/dev/integration_tests/pure_android_host_apps/host_app_kotlin_gradle_dsl/app/src/main/java/com/example/myapplication/ui/theme/Color.kt b/dev/integration_tests/pure_android_host_apps/host_app_kotlin_gradle_dsl/app/src/main/java/com/example/myapplication/ui/theme/Color.kt new file mode 100644 index 0000000000..1ce3e5dd7b --- /dev/null +++ b/dev/integration_tests/pure_android_host_apps/host_app_kotlin_gradle_dsl/app/src/main/java/com/example/myapplication/ui/theme/Color.kt @@ -0,0 +1,11 @@ +package com.example.myapplication.ui.theme + +import androidx.compose.ui.graphics.Color + +val Purple80 = Color(0xFFD0BCFF) +val PurpleGrey80 = Color(0xFFCCC2DC) +val Pink80 = Color(0xFFEFB8C8) + +val Purple40 = Color(0xFF6650a4) +val PurpleGrey40 = Color(0xFF625b71) +val Pink40 = Color(0xFF7D5260) \ No newline at end of file diff --git a/dev/integration_tests/pure_android_host_apps/host_app_kotlin_gradle_dsl/app/src/main/java/com/example/myapplication/ui/theme/Theme.kt b/dev/integration_tests/pure_android_host_apps/host_app_kotlin_gradle_dsl/app/src/main/java/com/example/myapplication/ui/theme/Theme.kt new file mode 100644 index 0000000000..174f73f960 --- /dev/null +++ b/dev/integration_tests/pure_android_host_apps/host_app_kotlin_gradle_dsl/app/src/main/java/com/example/myapplication/ui/theme/Theme.kt @@ -0,0 +1,58 @@ +package com.example.myapplication.ui.theme + +import android.app.Activity +import android.os.Build +import androidx.compose.foundation.isSystemInDarkTheme +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.darkColorScheme +import androidx.compose.material3.dynamicDarkColorScheme +import androidx.compose.material3.dynamicLightColorScheme +import androidx.compose.material3.lightColorScheme +import androidx.compose.runtime.Composable +import androidx.compose.ui.platform.LocalContext + +private val DarkColorScheme = darkColorScheme( + primary = Purple80, + secondary = PurpleGrey80, + tertiary = Pink80 +) + +private val LightColorScheme = lightColorScheme( + primary = Purple40, + secondary = PurpleGrey40, + tertiary = Pink40 + + /* Other default colors to override + background = Color(0xFFFFFBFE), + surface = Color(0xFFFFFBFE), + onPrimary = Color.White, + onSecondary = Color.White, + onTertiary = Color.White, + onBackground = Color(0xFF1C1B1F), + onSurface = Color(0xFF1C1B1F), + */ +) + +@Composable +fun MyApplicationTheme( + darkTheme: Boolean = isSystemInDarkTheme(), + // Dynamic color is available on Android 12+ + dynamicColor: Boolean = true, + content: @Composable () -> Unit +) { + val colorScheme = when { + dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> { + val context = LocalContext.current + if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context) + } + + darkTheme -> DarkColorScheme + else -> LightColorScheme + } + + MaterialTheme( + colorScheme = colorScheme, + typography = Typography, + content = content + ) +} \ No newline at end of file diff --git a/dev/integration_tests/pure_android_host_apps/host_app_kotlin_gradle_dsl/app/src/main/java/com/example/myapplication/ui/theme/Type.kt b/dev/integration_tests/pure_android_host_apps/host_app_kotlin_gradle_dsl/app/src/main/java/com/example/myapplication/ui/theme/Type.kt new file mode 100644 index 0000000000..3721e8c196 --- /dev/null +++ b/dev/integration_tests/pure_android_host_apps/host_app_kotlin_gradle_dsl/app/src/main/java/com/example/myapplication/ui/theme/Type.kt @@ -0,0 +1,20 @@ +package com.example.myapplication.ui.theme + +import androidx.compose.material3.Typography +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.font.FontFamily +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.sp + +// Set of Material typography styles to start with +val Typography = + Typography( + bodyLarge = + TextStyle( + fontFamily = FontFamily.Default, + fontWeight = FontWeight.Normal, + fontSize = 16.sp, + lineHeight = 24.sp, + letterSpacing = 0.5.sp + ) + ) diff --git a/dev/integration_tests/pure_android_host_apps/host_app_kotlin_gradle_dsl/app/src/main/res/values/colors.xml b/dev/integration_tests/pure_android_host_apps/host_app_kotlin_gradle_dsl/app/src/main/res/values/colors.xml new file mode 100644 index 0000000000..fdceae5973 --- /dev/null +++ b/dev/integration_tests/pure_android_host_apps/host_app_kotlin_gradle_dsl/app/src/main/res/values/colors.xml @@ -0,0 +1,14 @@ + + + + + #FFBB86FC + #FF6200EE + #FF3700B3 + #FF03DAC5 + #FF018786 + #FF000000 + #FFFFFFFF + \ No newline at end of file diff --git a/dev/integration_tests/pure_android_host_apps/host_app_kotlin_gradle_dsl/app/src/main/res/values/strings.xml b/dev/integration_tests/pure_android_host_apps/host_app_kotlin_gradle_dsl/app/src/main/res/values/strings.xml new file mode 100644 index 0000000000..3d52828be9 --- /dev/null +++ b/dev/integration_tests/pure_android_host_apps/host_app_kotlin_gradle_dsl/app/src/main/res/values/strings.xml @@ -0,0 +1,7 @@ + + + + My Application + \ No newline at end of file diff --git a/dev/integration_tests/pure_android_host_apps/host_app_kotlin_gradle_dsl/app/src/main/res/values/themes.xml b/dev/integration_tests/pure_android_host_apps/host_app_kotlin_gradle_dsl/app/src/main/res/values/themes.xml new file mode 100644 index 0000000000..a696214125 --- /dev/null +++ b/dev/integration_tests/pure_android_host_apps/host_app_kotlin_gradle_dsl/app/src/main/res/values/themes.xml @@ -0,0 +1,9 @@ + + + + + +