Unverified Commit 5961bcc5 authored by Emmanuel Garcia's avatar Emmanuel Garcia Committed by GitHub

Generate projects using the new Android embedding (#41666)

* Generate projects using the new Android embedding

* Add comment about usesNewEmbedding:true

* Feedback

* Rework way to detect new embedding in new apps
parent 890b9394
...@@ -940,7 +940,6 @@ Future<void> _buildGradleProjectV2( ...@@ -940,7 +940,6 @@ Future<void> _buildGradleProjectV2(
/// Returns [true] if the current app uses AndroidX. /// Returns [true] if the current app uses AndroidX.
// TODO(egarciad): https://github.com/flutter/flutter/issues/40800 // TODO(egarciad): https://github.com/flutter/flutter/issues/40800
// Remove `FlutterManifest.usesAndroidX` and provide a unified `AndroidProject.usesAndroidX`. // Remove `FlutterManifest.usesAndroidX` and provide a unified `AndroidProject.usesAndroidX`.
@visibleForTesting
bool isAppUsingAndroidX(Directory androidDirectory) { bool isAppUsingAndroidX(Directory androidDirectory) {
final File properties = androidDirectory.childFile('gradle.properties'); final File properties = androidDirectory.childFile('gradle.properties');
if (!properties.existsSync()) { if (!properties.existsSync()) {
......
...@@ -623,6 +623,7 @@ To edit platform code in an IDE see https://flutter.dev/developing-packages/#edi ...@@ -623,6 +623,7 @@ To edit platform code in an IDE see https://flutter.dev/developing-packages/#edi
'description': projectDescription, 'description': projectDescription,
'dartSdk': '$flutterRoot/bin/cache/dart-sdk', 'dartSdk': '$flutterRoot/bin/cache/dart-sdk',
'androidX': androidX, 'androidX': androidX,
'useNewAndroidEmbedding': featureFlags.isNewAndroidEmbeddingEnabled,
'androidMinApiLevel': android.minApiLevel, 'androidMinApiLevel': android.minApiLevel,
'androidSdkVersion': android_sdk.minimumAndroidSdkVersion, 'androidSdkVersion': android_sdk.minimumAndroidSdkVersion,
'androidFlutterJar': '$flutterRoot/bin/cache/artifacts/engine/android-arm/flutter.jar', 'androidFlutterJar': '$flutterRoot/bin/cache/artifacts/engine/android-arm/flutter.jar',
......
...@@ -36,6 +36,9 @@ class FeatureFlags { ...@@ -36,6 +36,9 @@ class FeatureFlags {
/// Whether flutter desktop for Windows is enabled. /// Whether flutter desktop for Windows is enabled.
bool get isWindowsEnabled => _isEnabled(flutterWindowsDesktopFeature); bool get isWindowsEnabled => _isEnabled(flutterWindowsDesktopFeature);
/// Whether the new Android embedding is enabled.
bool get isNewAndroidEmbeddingEnabled => _isEnabled(flutterNewAndroidEmbeddingFeature);
// Calculate whether a particular feature is enabled for the current channel. // Calculate whether a particular feature is enabled for the current channel.
static bool _isEnabled(Feature feature) { static bool _isEnabled(Feature feature) {
final String currentChannel = FlutterVersion.instance.channel; final String currentChannel = FlutterVersion.instance.channel;
...@@ -66,6 +69,7 @@ const List<Feature> allFeatures = <Feature>[ ...@@ -66,6 +69,7 @@ const List<Feature> allFeatures = <Feature>[
flutterMacOSDesktopFeature, flutterMacOSDesktopFeature,
flutterWindowsDesktopFeature, flutterWindowsDesktopFeature,
flutterBuildPluginAsAarFeature, flutterBuildPluginAsAarFeature,
flutterNewAndroidEmbeddingFeature,
]; ];
/// The [Feature] for flutter web. /// The [Feature] for flutter web.
...@@ -126,6 +130,16 @@ const Feature flutterBuildPluginAsAarFeature = Feature( ...@@ -126,6 +130,16 @@ const Feature flutterBuildPluginAsAarFeature = Feature(
), ),
); );
/// The [Feature] for generating projects using the new Android embedding.
const Feature flutterNewAndroidEmbeddingFeature = Feature(
name: 'flutter create generates projects using the new Android embedding',
configSetting: 'enable-new-android-embedding',
master: FeatureChannelSetting(
available: true,
enabledByDefault: false,
),
);
/// A [Feature] is a process for conditionally enabling tool features. /// A [Feature] is a process for conditionally enabling tool features.
/// ///
/// All settings are optional, and if not provided will generally default to /// All settings are optional, and if not provided will generally default to
......
...@@ -5,6 +5,10 @@ ...@@ -5,6 +5,10 @@
import 'package:meta/meta.dart'; import 'package:meta/meta.dart';
import 'package:yaml/yaml.dart'; import 'package:yaml/yaml.dart';
import 'base/common.dart';
import 'base/file_system.dart';
import 'features.dart';
/// Marker interface for all platform specific plugin config impls. /// Marker interface for all platform specific plugin config impls.
abstract class PluginPlatform { abstract class PluginPlatform {
const PluginPlatform(); const PluginPlatform();
...@@ -17,18 +21,20 @@ abstract class PluginPlatform { ...@@ -17,18 +21,20 @@ abstract class PluginPlatform {
/// The required fields include: [name] of the plugin, [package] of the plugin and /// The required fields include: [name] of the plugin, [package] of the plugin and
/// the [pluginClass] that will be the entry point to the plugin's native code. /// the [pluginClass] that will be the entry point to the plugin's native code.
class AndroidPlugin extends PluginPlatform { class AndroidPlugin extends PluginPlatform {
const AndroidPlugin({ AndroidPlugin({
@required this.name, @required this.name,
@required this.package, @required this.package,
@required this.pluginClass, @required this.pluginClass,
@required this.pluginPath,
}); });
factory AndroidPlugin.fromYaml(String name, YamlMap yaml) { factory AndroidPlugin.fromYaml(String name, YamlMap yaml, String pluginPath) {
assert(validate(yaml)); assert(validate(yaml));
return AndroidPlugin( return AndroidPlugin(
name: name, name: name,
package: yaml['package'], package: yaml['package'],
pluginClass: yaml['pluginClass'], pluginClass: yaml['pluginClass'],
pluginPath: pluginPath,
); );
} }
...@@ -41,18 +47,80 @@ class AndroidPlugin extends PluginPlatform { ...@@ -41,18 +47,80 @@ class AndroidPlugin extends PluginPlatform {
static const String kConfigKey = 'android'; static const String kConfigKey = 'android';
/// The plugin name defined in pubspec.yaml.
final String name; final String name;
/// The plugin package name defined in pubspec.yaml.
final String package; final String package;
/// The plugin main class defined in pubspec.yaml.
final String pluginClass; final String pluginClass;
/// The absolute path to the plugin in the pub cache.
final String pluginPath;
@override @override
Map<String, dynamic> toMap() { Map<String, dynamic> toMap() {
return <String, dynamic>{ return <String, dynamic>{
'name': name, 'name': name,
'package': package, 'package': package,
'class': pluginClass, 'class': pluginClass,
'usesEmbedding2': _embeddingVersion == '2',
}; };
} }
String _cachedEmbeddingVersion;
/// Returns the version of the Android embedding.
String get _embeddingVersion => _cachedEmbeddingVersion ??= _getEmbeddingVersion();
String _getEmbeddingVersion() {
if (!featureFlags.isNewAndroidEmbeddingEnabled) {
return '1';
}
assert(pluginPath != null);
final String baseMainPath = fs.path.join(
pluginPath,
'android',
'src',
'main',
);
File mainPluginClass = fs.file(
fs.path.join(
baseMainPath,
'java',
package.replaceAll('.', fs.path.separator),
'$pluginClass.java',
)
);
// Check if the plugin is implemented in Kotlin since the plugin's pubspec.yaml
// doesn't include this information.
if (!mainPluginClass.existsSync()) {
mainPluginClass = fs.file(
fs.path.join(
baseMainPath,
'kotlin',
package.replaceAll('.', fs.path.separator),
'$pluginClass.kt',
)
);
}
assert(mainPluginClass.existsSync());
String mainClassContent;
try {
mainClassContent = mainPluginClass.readAsStringSync();
} on FileSystemException {
throwToolExit(
'Couldn\'t read file $mainPluginClass even though it exists. '
'Please verify that this file has read permission and try again.'
);
}
if (mainClassContent
.contains('io.flutter.embedding.engine.plugins.FlutterPlugin')) {
return '2';
}
return '1';
}
} }
/// Contains the parameters to template an iOS plugin. /// Contains the parameters to template an iOS plugin.
......
...@@ -5,8 +5,10 @@ ...@@ -5,8 +5,10 @@
import 'dart:async'; import 'dart:async';
import 'package:mustache/mustache.dart' as mustache; import 'package:mustache/mustache.dart' as mustache;
import 'package:xml/xml.dart' as xml;
import 'package:yaml/yaml.dart'; import 'package:yaml/yaml.dart';
import 'android/gradle.dart';
import 'base/common.dart'; import 'base/common.dart';
import 'base/file_system.dart'; import 'base/file_system.dart';
import 'dart/package_map.dart'; import 'dart/package_map.dart';
...@@ -63,9 +65,8 @@ class Plugin { ...@@ -63,9 +65,8 @@ class Plugin {
} }
if (pluginYaml != null && pluginYaml['platforms'] != null) { if (pluginYaml != null && pluginYaml['platforms'] != null) {
return Plugin._fromMultiPlatformYaml(name, path, pluginYaml); return Plugin._fromMultiPlatformYaml(name, path, pluginYaml);
} else {
return Plugin._fromLegacyYaml(name, path, pluginYaml); // ignore: deprecated_member_use_from_same_package
} }
return Plugin._fromLegacyYaml(name, path, pluginYaml); // ignore: deprecated_member_use_from_same_package
} }
factory Plugin._fromMultiPlatformYaml(String name, String path, dynamic pluginYaml) { factory Plugin._fromMultiPlatformYaml(String name, String path, dynamic pluginYaml) {
...@@ -79,8 +80,11 @@ class Plugin { ...@@ -79,8 +80,11 @@ class Plugin {
final Map<String, PluginPlatform> platforms = <String, PluginPlatform>{}; final Map<String, PluginPlatform> platforms = <String, PluginPlatform>{};
if (platformsYaml[AndroidPlugin.kConfigKey] != null) { if (platformsYaml[AndroidPlugin.kConfigKey] != null) {
platforms[AndroidPlugin.kConfigKey] = platforms[AndroidPlugin.kConfigKey] = AndroidPlugin.fromYaml(
AndroidPlugin.fromYaml(name, platformsYaml[AndroidPlugin.kConfigKey]); name,
platformsYaml[AndroidPlugin.kConfigKey],
path,
);
} }
if (platformsYaml[IOSPlugin.kConfigKey] != null) { if (platformsYaml[IOSPlugin.kConfigKey] != null) {
...@@ -122,12 +126,12 @@ class Plugin { ...@@ -122,12 +126,12 @@ class Plugin {
if (pluginYaml != null && pluginClass != null) { if (pluginYaml != null && pluginClass != null) {
final String androidPackage = pluginYaml['androidPackage']; final String androidPackage = pluginYaml['androidPackage'];
if (androidPackage != null) { if (androidPackage != null) {
platforms[AndroidPlugin.kConfigKey] = platforms[AndroidPlugin.kConfigKey] = AndroidPlugin(
AndroidPlugin( name: name,
name: name, package: pluginYaml['androidPackage'],
package: pluginYaml['androidPackage'], pluginClass: pluginClass,
pluginClass: pluginClass, pluginPath: path,
); );
} }
final String iosPrefix = pluginYaml['iosPrefix'] ?? ''; final String iosPrefix = pluginYaml['iosPrefix'] ?? '';
...@@ -221,14 +225,21 @@ Plugin _pluginFromPubspec(String name, Uri packageRoot) { ...@@ -221,14 +225,21 @@ Plugin _pluginFromPubspec(String name, Uri packageRoot) {
} }
final String packageRootPath = fs.path.fromUri(packageRoot); final String packageRootPath = fs.path.fromUri(packageRoot);
printTrace('Found plugin $name at $packageRootPath'); printTrace('Found plugin $name at $packageRootPath');
return Plugin.fromYaml(name, packageRootPath, flutterConfig['plugin']); return Plugin.fromYaml(
name,
packageRootPath,
flutterConfig['plugin'],
);
} }
List<Plugin> findPlugins(FlutterProject project) { List<Plugin> findPlugins(FlutterProject project) {
final List<Plugin> plugins = <Plugin>[]; final List<Plugin> plugins = <Plugin>[];
Map<String, Uri> packages; Map<String, Uri> packages;
try { try {
final String packagesFile = fs.path.join(project.directory.path, PackageMap.globalPackagesPath); final String packagesFile = fs.path.join(
project.directory.path,
PackageMap.globalPackagesPath,
);
packages = PackageMap(packagesFile).map; packages = PackageMap(packagesFile).map;
} on FormatException catch (e) { } on FormatException catch (e) {
printTrace('Invalid .packages file: $e'); printTrace('Invalid .packages file: $e');
...@@ -269,7 +280,7 @@ String _readFlutterPluginsList(FlutterProject project) { ...@@ -269,7 +280,7 @@ String _readFlutterPluginsList(FlutterProject project) {
: null; : null;
} }
const String _androidPluginRegistryTemplate = '''package io.flutter.plugins; const String _androidPluginRegistryTemplateOldEmbedding = '''package io.flutter.plugins;
import io.flutter.plugin.common.PluginRegistry; import io.flutter.plugin.common.PluginRegistry;
{{#plugins}} {{#plugins}}
...@@ -300,6 +311,41 @@ public final class GeneratedPluginRegistrant { ...@@ -300,6 +311,41 @@ public final class GeneratedPluginRegistrant {
} }
'''; ''';
const String _androidPluginRegistryTemplateNewEmbedding = '''package dev.flutter.plugins;
{{#androidX}}
import androidx.annotation.NonNull;
{{/androidX}}
{{^androidX}}
import android.support.annotation.NonNull;
{{/androidX}}
import io.flutter.embedding.engine.FlutterEngine;
{{#needsShim}}
import io.flutter.embedding.engine.plugins.shim.ShimPluginRegistry;
{{/needsShim}}
/**
* Generated file. Do not edit.
* This file is generated by the Flutter tool based on the
* plugins that support the Android platform.
*/
public final class GeneratedPluginRegistrant {
public static void registerWith(@NonNull FlutterEngine flutterEngine) {
{{#needsShim}}
ShimPluginRegistry shimPluginRegistry = new ShimPluginRegistry(flutterEngine);
{{/needsShim}}
{{#plugins}}
{{#usesEmbedding2}}
flutterEngine.getPlugins().add(new {{package}}.{{class}}());
{{/usesEmbedding2}}
{{^usesEmbedding2}}
{{package}}.{{class}}.registerWith(shimPluginRegistry.registrarFor("{{package}}.{{class}}"));
{{/usesEmbedding2}}
{{/plugins}}
}
}
''';
List<Map<String, dynamic>> _extractPlatformMaps(List<Plugin> plugins, String type) { List<Map<String, dynamic>> _extractPlatformMaps(List<Plugin> plugins, String type) {
final List<Map<String, dynamic>> pluginConfigs = <Map<String, dynamic>>[]; final List<Map<String, dynamic>> pluginConfigs = <Map<String, dynamic>>[];
for (Plugin p in plugins) { for (Plugin p in plugins) {
...@@ -311,26 +357,92 @@ List<Map<String, dynamic>> _extractPlatformMaps(List<Plugin> plugins, String typ ...@@ -311,26 +357,92 @@ List<Map<String, dynamic>> _extractPlatformMaps(List<Plugin> plugins, String typ
return pluginConfigs; return pluginConfigs;
} }
/// Returns the version of the Android embedding that the current
/// [project] is using.
String _getAndroidEmbeddingVersion(FlutterProject project) {
if (!featureFlags.isNewAndroidEmbeddingEnabled) {
return '1';
}
assert(project.android != null);
final File androidManifest = project.android.appManifestFile;
assert(androidManifest.existsSync());
xml.XmlDocument document;
try {
document = xml.parse(androidManifest.readAsStringSync());
} on xml.XmlParserException {
throwToolExit('Error parsing ${project.android.appManifestFile} '
'Please ensure that the android manifest is a valid XML document and try again.');
} on FileSystemException {
throwToolExit('Error reading ${project.android.appManifestFile} even though it exists. '
'Please ensure that you have read permission to this file and try again.');
}
for (xml.XmlElement metaData in document.findAllElements('meta-data')) {
final String name = metaData.getAttribute('android:name');
if (name == 'flutterEmbedding') {
return metaData.getAttribute('android:value');
}
}
return '1';
}
Future<void> _writeAndroidPluginRegistrant(FlutterProject project, List<Plugin> plugins) async { Future<void> _writeAndroidPluginRegistrant(FlutterProject project, List<Plugin> plugins) async {
final List<Map<String, dynamic>> androidPlugins = _extractPlatformMaps(plugins, AndroidPlugin.kConfigKey); final List<Map<String, dynamic>> androidPlugins =
final Map<String, dynamic> context = <String, dynamic>{ _extractPlatformMaps(plugins, AndroidPlugin.kConfigKey);
final Map<String, dynamic> templateContext = <String, dynamic>{
'plugins': androidPlugins, 'plugins': androidPlugins,
'androidX': isAppUsingAndroidX(project.android.hostAppGradleRoot),
}; };
final String javaSourcePath = fs.path.join( final String javaSourcePath = fs.path.join(
project.android.pluginRegistrantHost.path, project.android.pluginRegistrantHost.path,
'src', 'src',
'main', 'main',
'java', 'java',
); );
final String registryPath = fs.path.join(
javaSourcePath, String registryPath;
'io', String templateContent;
'flutter',
'plugins', final String appEmbeddingVersion = _getAndroidEmbeddingVersion(project);
'GeneratedPluginRegistrant.java', switch (appEmbeddingVersion) {
case '2':
templateContext['needsShim'] = false;
// If a plugin is using an embedding version older than 2.0 and the app is using 2.0,
// then add shim for the old plugins.
for (Map<String, dynamic> plugin in androidPlugins) {
if (!plugin['usesEmbedding2']) {
templateContext['needsShim'] = true;
break;
}
}
registryPath = fs.path.join(
javaSourcePath,
'dev',
'flutter',
'plugins',
'GeneratedPluginRegistrant.java',
);
templateContent = _androidPluginRegistryTemplateNewEmbedding;
break;
case '1':
registryPath = fs.path.join(
javaSourcePath,
'io',
'flutter',
'plugins',
'GeneratedPluginRegistrant.java',
);
templateContent = _androidPluginRegistryTemplateOldEmbedding;
break;
default:
throwToolExit('Unsupported Android embedding');
}
printTrace('Generating $registryPath');
_renderTemplateToFile(
templateContent,
templateContext,
registryPath,
); );
_renderTemplateToFile(_androidPluginRegistryTemplate, context, registryPath);
} }
const String _objcPluginRegistryHeaderTemplate = '''// const String _objcPluginRegistryHeaderTemplate = '''//
......
...@@ -608,7 +608,11 @@ class AndroidProject { ...@@ -608,7 +608,11 @@ class AndroidProject {
void _regenerateLibrary() { void _regenerateLibrary() {
_deleteIfExistsSync(ephemeralDirectory); _deleteIfExistsSync(ephemeralDirectory);
_overwriteFromTemplate(fs.path.join('module', 'android', 'library'), ephemeralDirectory); _overwriteFromTemplate(fs.path.join(
'module',
'android',
featureFlags.isNewAndroidEmbeddingEnabled ? 'library_new_embedding' : 'library',
), ephemeralDirectory);
_overwriteFromTemplate(fs.path.join('module', 'android', 'gradle'), ephemeralDirectory); _overwriteFromTemplate(fs.path.join('module', 'android', 'gradle'), ephemeralDirectory);
gradle.injectGradleWrapperIfNeeded(ephemeralDirectory); gradle.injectGradleWrapperIfNeeded(ephemeralDirectory);
} }
...@@ -621,6 +625,7 @@ class AndroidProject { ...@@ -621,6 +625,7 @@ class AndroidProject {
'projectName': parent.manifest.appName, 'projectName': parent.manifest.appName,
'androidIdentifier': parent.manifest.androidPackage, 'androidIdentifier': parent.manifest.androidPackage,
'androidX': usesAndroidX, 'androidX': usesAndroidX,
'useNewAndroidEmbedding': featureFlags.isNewAndroidEmbeddingEnabled,
}, },
printStatusWhenWriting: false, printStatusWhenWriting: false,
overwriteExisting: true, overwriteExisting: true,
......
package {{androidIdentifier}}; package {{androidIdentifier}};
{{#useNewAndroidEmbedding}}
{{#androidX}}
import androidx.annotation.NonNull;
{{/androidX}}
{{^androidX}}
import android.support.annotation.NonNull;
{{/androidX}}
import dev.flutter.plugins.GeneratedPluginRegistrant;
import io.flutter.embedding.android.FlutterActivity;
import io.flutter.embedding.engine.FlutterEngine;
public class MainActivity extends FlutterActivity {
@Override
public void configureFlutterEngine(@NonNull FlutterEngine flutterEngine) {
GeneratedPluginRegistrant.registerWith(flutterEngine);
}
}
{{/useNewAndroidEmbedding}}
{{^useNewAndroidEmbedding}}
import android.os.Bundle; import android.os.Bundle;
import io.flutter.app.FlutterActivity; import io.flutter.app.FlutterActivity;
import io.flutter.plugins.GeneratedPluginRegistrant; import io.flutter.plugins.GeneratedPluginRegistrant;
...@@ -11,3 +30,4 @@ public class MainActivity extends FlutterActivity { ...@@ -11,3 +30,4 @@ public class MainActivity extends FlutterActivity {
GeneratedPluginRegistrant.registerWith(this); GeneratedPluginRegistrant.registerWith(this);
} }
} }
{{/useNewAndroidEmbedding}}
package {{androidIdentifier}} package {{androidIdentifier}}
import android.os.Bundle {{#useNewAndroidEmbedding}}
{{#androidX}}
import androidx.annotation.NonNull;
{{/androidX}}
{{^androidX}}
import android.support.annotation.NonNull;
{{/androidX}}
import dev.flutter.plugins.GeneratedPluginRegistrant
import io.flutter.embedding.android.FlutterActivity
import io.flutter.embedding.engine.FlutterEngine
class MainActivity: FlutterActivity() {
override fun configureFlutterEngine(@NonNull flutterEngine: FlutterEngine) {
GeneratedPluginRegistrant.registerWith(flutterEngine);
}
}
{{/useNewAndroidEmbedding}}
{{^useNewAndroidEmbedding}}
import android.os.Bundle
import io.flutter.app.FlutterActivity import io.flutter.app.FlutterActivity
import io.flutter.plugins.GeneratedPluginRegistrant import io.flutter.plugins.GeneratedPluginRegistrant
...@@ -11,3 +28,4 @@ class MainActivity: FlutterActivity() { ...@@ -11,3 +28,4 @@ class MainActivity: FlutterActivity() {
GeneratedPluginRegistrant.registerWith(this) GeneratedPluginRegistrant.registerWith(this)
} }
} }
{{/useNewAndroidEmbedding}}
<manifest xmlns:android="http://schemas.android.com/apk/res/android" <manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="{{androidIdentifier}}"> package="{{androidIdentifier}}">
<!-- io.flutter.app.FlutterApplication is an android.app.Application that <!-- io.flutter.app.FlutterApplication is an android.app.Application that
calls FlutterMain.startInitialization(this); in its onCreate method. calls FlutterMain.startInitialization(this); in its onCreate method.
In most cases you can leave this as-is, but you if you want to provide In most cases you can leave this as-is, but you if you want to provide
...@@ -29,5 +28,12 @@ ...@@ -29,5 +28,12 @@
<category android:name="android.intent.category.LAUNCHER"/> <category android:name="android.intent.category.LAUNCHER"/>
</intent-filter> </intent-filter>
</activity> </activity>
{{#useNewAndroidEmbedding}}
<!-- Don't delete the meta-data below.
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
<meta-data
android:name="flutterEmbedding"
android:value="2" />
{{/useNewAndroidEmbedding}}
</application> </application>
</manifest> </manifest>
...@@ -36,5 +36,12 @@ ...@@ -36,5 +36,12 @@
<category android:name="android.intent.category.LAUNCHER"/> <category android:name="android.intent.category.LAUNCHER"/>
</intent-filter> </intent-filter>
</activity> </activity>
{{#useNewAndroidEmbedding}}
<!-- Don't delete the meta-data below.
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
<meta-data
android:name="flutterEmbedding"
android:value="2" />
{{/useNewAndroidEmbedding}}
</application> </application>
</manifest> </manifest>
package {{androidIdentifier}}.host; package {{androidIdentifier}}.host;
import android.os.Bundle; import android.os.Bundle;
{{#useNewAndroidEmbedding}}
{{#androidX}}
import androidx.annotation.NonNull;
{{/androidX}}
{{^androidX}}
import android.support.annotation.NonNull;
{{/androidX}}
import dev.flutter.plugins.GeneratedPluginRegistrant;
import io.flutter.embedding.android.FlutterActivity;
import io.flutter.embedding.engine.FlutterEngine;
public class MainActivity extends FlutterActivity {
@Override
public void configureFlutterEngine(@NonNull FlutterEngine flutterEngine) {
GeneratedPluginRegistrant.registerWith(flutterEngine);
}
}
{{/useNewAndroidEmbedding}}
{{^useNewAndroidEmbedding}}
import io.flutter.app.FlutterActivity; import io.flutter.app.FlutterActivity;
import io.flutter.plugins.GeneratedPluginRegistrant; import io.flutter.plugins.GeneratedPluginRegistrant;
...@@ -11,3 +30,4 @@ public class MainActivity extends FlutterActivity { ...@@ -11,3 +30,4 @@ public class MainActivity extends FlutterActivity {
GeneratedPluginRegistrant.registerWith(this); GeneratedPluginRegistrant.registerWith(this);
} }
} }
{{/useNewAndroidEmbedding}}
// Generated file. Do not edit.
def localProperties = new Properties()
def localPropertiesFile = new File(buildscript.sourceFile.parentFile.parentFile, 'local.properties')
if (localPropertiesFile.exists()) {
localPropertiesFile.withReader('UTF-8') { reader ->
localProperties.load(reader)
}
}
def flutterRoot = localProperties.getProperty('flutter.sdk')
if (flutterRoot == null) {
throw new GradleException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.")
}
def flutterVersionCode = localProperties.getProperty('flutter.versionCode')
if (flutterVersionCode == null) {
flutterVersionCode = '1'
}
def flutterVersionName = localProperties.getProperty('flutter.versionName')
if (flutterVersionName == null) {
flutterVersionName = '1.0'
}
apply plugin: 'com.android.library'
apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle"
group '{{androidIdentifier}}'
version '1.0'
android {
compileSdkVersion 28
defaultConfig {
minSdkVersion 16
targetSdkVersion 28
versionCode flutterVersionCode.toInteger()
versionName flutterVersionName
{{#androidX}}
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
{{/androidX}}
{{^androidX}}
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
{{/androidX}}
}
}
flutter {
source '../..'
}
dependencies {
testImplementation 'junit:junit:4.12'
}
<?xml version="1.0" encoding="UTF-8"?>
<module external.linked.project.id=":flutter" external.linked.project.path="$MODULE_DIR$" external.root.project.path="$MODULE_DIR$/../../.." external.system.id="GRADLE" type="JAVA_MODULE" version="4">
<component name="FacetManager">
<facet type="android-gradle" name="Android-Gradle">
<configuration>
<option name="GRADLE_PROJECT_PATH" value=":flutter" />
</configuration>
</facet>
<facet type="android" name="Android">
<configuration>
<option name="ALLOW_USER_CONFIGURATION" value="false" />
<option name="MANIFEST_FILE_RELATIVE_PATH" value="/src/main/AndroidManifest.xml" />
</configuration>
</facet>
</component>
<component name="NewModuleRootManager" LANGUAGE_LEVEL="JDK_1_8">
<output url="file://$MODULE_DIR$/build/intermediates/javac/debug/compileDebugJavaWithJavac/classes" />
<output-test url="file://$MODULE_DIR$/build/intermediates/javac/debugUnitTest/compileDebugUnitTestJavaWithJavac/classes" />
<exclude-output />
<content url="file://$MODULE_DIR$">
<sourceFolder url="file://$MODULE_DIR$/src/main/java" isTestSource="false" />
</content>
<orderEntry type="jdk" jdkName="Android API 28 Platform" jdkType="Android SDK" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
</module>
\ No newline at end of file
<!-- Generated file. Do not edit. -->
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="{{androidIdentifier}}"
xmlns:tools="http://schemas.android.com/tools">
<uses-permission android:name="android.permission.INTERNET"/>
<application tools:node="merge">
<meta-data
android:name="flutterProjectType"
android:value="module" />
<!-- Don't delete the meta-data below.
It is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
<meta-data
android:name="flutterEmbedding"
android:value="2" />
</application>
</manifest>
// Generated file. Do not edit.
def scriptFile = getClass().protectionDomain.codeSource.location.toURI()
def flutterProjectRoot = new File(scriptFile).parentFile.parentFile
gradle.include ':flutter'
gradle.project(':flutter').projectDir = new File(flutterProjectRoot, '.android/Flutter')
if (System.getProperty('build-plugins-as-aars') != 'true') {
def plugins = new Properties()
def pluginsFile = new File(flutterProjectRoot, '.flutter-plugins')
if (pluginsFile.exists()) {
pluginsFile.withReader('UTF-8') { reader -> plugins.load(reader) }
}
plugins.each { name, path ->
def pluginDirectory = flutterProjectRoot.toPath().resolve(path).resolve('android').toFile()
gradle.include ":$name"
gradle.project(":$name").projectDir = pluginDirectory
}
}
gradle.getGradle().projectsLoaded { g ->
g.rootProject.beforeEvaluate { p ->
_mainModuleName = binding.variables['mainModuleName']
if (_mainModuleName != null && !_mainModuleName.empty) {
p.ext.mainModuleName = _mainModuleName
}
def subprojects = []
def flutterProject
p.subprojects { sp ->
if (sp.name == 'flutter') {
flutterProject = sp
} else {
subprojects.add(sp)
}
}
assert flutterProject != null
flutterProject.ext.hostProjects = subprojects
flutterProject.ext.pluginBuildDir = new File(flutterProjectRoot, 'build/host')
}
g.rootProject.afterEvaluate { p ->
p.subprojects { sp ->
if (sp.name != 'flutter') {
sp.evaluationDependsOn(':flutter')
}
}
}
}
// Generated file. Do not edit.
rootProject.name = 'android_generated'
setBinding(new Binding([gradle: this]))
evaluate(new File(settingsDir, 'include_flutter.groovy'))
package {{androidIdentifier}}; package {{androidIdentifier}};
{{#useNewAndroidEmbedding}}
{{#androidX}}
import androidx.annotation.NonNull;
{{/androidX}}
{{^androidX}}
import android.support.annotation.NonNull;
{{/androidX}}
import io.flutter.embedding.engine.plugins.FlutterPlugin;
import io.flutter.plugin.common.MethodCall;
import io.flutter.plugin.common.MethodChannel;
import io.flutter.plugin.common.MethodChannel.MethodCallHandler;
import io.flutter.plugin.common.MethodChannel.Result;
/** {{pluginClass}} */
public class {{pluginClass}} implements FlutterPlugin, MethodCallHandler {
@Override
public void onAttachedToEngine(@NonNull FlutterPluginBinding flutterPluginBinding) {
final MethodChannel channel = new MethodChannel(flutterPluginBinding.getFlutterEngine().getDartExecutor(), "{{projectName}}");
channel.setMethodCallHandler(new {{pluginClass}}());
}
@Override
public void onMethodCall(@NonNull MethodCall call, @NonNull Result result) {
if (call.method.equals("getPlatformVersion")) {
result.success("Android " + android.os.Build.VERSION.RELEASE);
} else {
result.notImplemented();
}
}
}
{{/useNewAndroidEmbedding}}
{{^useNewAndroidEmbedding}}
import io.flutter.plugin.common.MethodCall; import io.flutter.plugin.common.MethodCall;
import io.flutter.plugin.common.MethodChannel; import io.flutter.plugin.common.MethodChannel;
import io.flutter.plugin.common.MethodChannel.MethodCallHandler; import io.flutter.plugin.common.MethodChannel.MethodCallHandler;
...@@ -23,3 +55,4 @@ public class {{pluginClass}} implements MethodCallHandler { ...@@ -23,3 +55,4 @@ public class {{pluginClass}} implements MethodCallHandler {
} }
} }
} }
{{/useNewAndroidEmbedding}}
package {{androidIdentifier}} package {{androidIdentifier}}
{{#useNewAndroidEmbedding}}
{{#androidX}}
import androidx.annotation.NonNull;
{{/androidX}}
{{^androidX}}
import android.support.annotation.NonNull;
{{/androidX}}
import io.flutter.embedding.engine.plugins.FlutterPlugin
import io.flutter.plugin.common.MethodCall
import io.flutter.plugin.common.MethodChannel
import io.flutter.plugin.common.MethodChannel.MethodCallHandler
import io.flutter.plugin.common.MethodChannel.Result
/** {{pluginClass}} */
public class {{pluginClass}}: FlutterPlugin, MethodCallHandler {
override fun onAttachedToEngine(@NonNull flutterPluginBinding: FlutterPluginBinding) {
val channel = MethodChannel(flutterPluginBinding.getFlutterEngine().getDartExecutor(), "{{projectName}}")
channel.setMethodCallHandler({{pluginClass}}());
}
override fun onMethodCall(@NonNull call: MethodCall, @NonNull result: Result) {
if (call.method == "getPlatformVersion") {
result.success("Android ${android.os.Build.VERSION.RELEASE}")
} else {
result.notImplemented()
}
}
}
{{/useNewAndroidEmbedding}}
{{^useNewAndroidEmbedding}}
import io.flutter.plugin.common.MethodCall import io.flutter.plugin.common.MethodCall
import io.flutter.plugin.common.MethodChannel import io.flutter.plugin.common.MethodChannel
import io.flutter.plugin.common.MethodChannel.MethodCallHandler import io.flutter.plugin.common.MethodChannel.MethodCallHandler
...@@ -23,3 +53,4 @@ class {{pluginClass}}: MethodCallHandler { ...@@ -23,3 +53,4 @@ class {{pluginClass}}: MethodCallHandler {
} }
} }
} }
{{/useNewAndroidEmbedding}}
...@@ -5,22 +5,22 @@ ...@@ -5,22 +5,22 @@
import 'package:file/file.dart'; import 'package:file/file.dart';
import 'package:file/memory.dart'; import 'package:file/memory.dart';
import 'package:flutter_tools/src/dart/package_map.dart'; import 'package:flutter_tools/src/dart/package_map.dart';
import 'package:flutter_tools/src/features.dart';
import 'package:flutter_tools/src/ios/xcodeproj.dart';
import 'package:flutter_tools/src/plugins.dart'; import 'package:flutter_tools/src/plugins.dart';
import 'package:flutter_tools/src/project.dart'; import 'package:flutter_tools/src/project.dart';
import 'package:mockito/mockito.dart'; import 'package:mockito/mockito.dart';
import '../src/common.dart'; import '../src/common.dart';
import '../src/context.dart'; import '../src/context.dart';
class MockFlutterProject extends Mock implements FlutterProject {}
class MockIosProject extends Mock implements IosProject {}
class MockMacOSProject extends Mock implements MacOSProject {}
void main() { void main() {
FileSystem fs; FileSystem fs;
MockFlutterProject flutterProject; MockFlutterProject flutterProject;
MockIosProject iosProject; MockIosProject iosProject;
MockMacOSProject macosProject; MockMacOSProject macosProject;
MockAndroidProject androidProject;
File packagesFile; File packagesFile;
Directory dummyPackageDirectory; Directory dummyPackageDirectory;
...@@ -33,10 +33,17 @@ void main() { ...@@ -33,10 +33,17 @@ void main() {
when(flutterProject.flutterPluginsFile).thenReturn(flutterProject.directory.childFile('.plugins')); when(flutterProject.flutterPluginsFile).thenReturn(flutterProject.directory.childFile('.plugins'));
iosProject = MockIosProject(); iosProject = MockIosProject();
when(flutterProject.ios).thenReturn(iosProject); when(flutterProject.ios).thenReturn(iosProject);
when(iosProject.pluginRegistrantHost).thenReturn(flutterProject.directory.childDirectory('Runner'));
when(iosProject.podfile).thenReturn(flutterProject.directory.childDirectory('ios').childFile('Podfile'));
when(iosProject.podManifestLock).thenReturn(flutterProject.directory.childDirectory('ios').childFile('Podfile.lock')); when(iosProject.podManifestLock).thenReturn(flutterProject.directory.childDirectory('ios').childFile('Podfile.lock'));
macosProject = MockMacOSProject(); macosProject = MockMacOSProject();
when(flutterProject.macos).thenReturn(macosProject); when(flutterProject.macos).thenReturn(macosProject);
when(macosProject.podfile).thenReturn(flutterProject.directory.childDirectory('macos').childFile('Podfile'));
when(macosProject.podManifestLock).thenReturn(flutterProject.directory.childDirectory('macos').childFile('Podfile.lock')); when(macosProject.podManifestLock).thenReturn(flutterProject.directory.childDirectory('macos').childFile('Podfile.lock'));
androidProject = MockAndroidProject();
when(flutterProject.android).thenReturn(androidProject);
when(androidProject.pluginRegistrantHost).thenReturn(flutterProject.directory.childDirectory('android').childDirectory('app'));
when(androidProject.hostAppGradleRoot).thenReturn(flutterProject.directory.childDirectory('android'));
// Set up a simple .packages file for all the tests to use, pointing to one package. // Set up a simple .packages file for all the tests to use, pointing to one package.
dummyPackageDirectory = fs.directory('/pubcache/apackage/lib/'); dummyPackageDirectory = fs.directory('/pubcache/apackage/lib/');
...@@ -103,4 +110,276 @@ flutter: ...@@ -103,4 +110,276 @@ flutter:
FileSystem: () => fs, FileSystem: () => fs,
}); });
}); });
group('injectPlugins', () {
MockFeatureFlags featureFlags;
MockXcodeProjectInterpreter xcodeProjectInterpreter;
const String kAndroidManifestUsingOldEmbedding = '''
<manifest>
<application>
</application>
</manifest>
''';
const String kAndroidManifestUsingNewEmbedding = '''
<manifest>
<application>
<meta-data
android:name="flutterEmbedding"
android:value="2" />
</application>
</manifest>
''';
setUp(() {
featureFlags = MockFeatureFlags();
when(featureFlags.isLinuxEnabled).thenReturn(false);
when(featureFlags.isMacOSEnabled).thenReturn(false);
when(featureFlags.isWindowsEnabled).thenReturn(false);
when(featureFlags.isWebEnabled).thenReturn(false);
xcodeProjectInterpreter = MockXcodeProjectInterpreter();
when(xcodeProjectInterpreter.isInstalled).thenReturn(false);
});
testUsingContext('Registrant uses old embedding in app project', () async {
when(flutterProject.isModule).thenReturn(false);
when(featureFlags.isNewAndroidEmbeddingEnabled).thenReturn(false);
await injectPlugins(flutterProject);
final File registrant = flutterProject.directory
.childDirectory(fs.path.join('android', 'app', 'src', 'main', 'java', 'io', 'flutter', 'plugins'))
.childFile('GeneratedPluginRegistrant.java');
expect(registrant.existsSync(), isTrue);
expect(registrant.readAsStringSync(), contains('package io.flutter.plugins'));
expect(registrant.readAsStringSync(), contains('class GeneratedPluginRegistrant'));
}, overrides: <Type, Generator>{
FileSystem: () => fs,
FeatureFlags: () => featureFlags,
});
testUsingContext('Registrant uses new embedding if app uses new embedding', () async {
when(flutterProject.isModule).thenReturn(false);
when(featureFlags.isNewAndroidEmbeddingEnabled).thenReturn(true);
final File androidManifest = flutterProject.directory
.childDirectory('android')
.childFile('AndroidManifest.xml')
..createSync(recursive: true)
..writeAsStringSync(kAndroidManifestUsingNewEmbedding);
when(androidProject.appManifestFile).thenReturn(androidManifest);
await injectPlugins(flutterProject);
final File registrant = flutterProject.directory
.childDirectory(fs.path.join('android', 'app', 'src', 'main', 'java', 'dev', 'flutter', 'plugins'))
.childFile('GeneratedPluginRegistrant.java');
expect(registrant.existsSync(), isTrue);
expect(registrant.readAsStringSync(), contains('package dev.flutter.plugins'));
expect(registrant.readAsStringSync(), contains('class GeneratedPluginRegistrant'));
}, overrides: <Type, Generator>{
FileSystem: () => fs,
FeatureFlags: () => featureFlags,
});
testUsingContext('Registrant uses shim for plugins using old embedding if app uses new embedding', () async {
when(flutterProject.isModule).thenReturn(false);
when(featureFlags.isNewAndroidEmbeddingEnabled).thenReturn(true);
final File androidManifest = flutterProject.directory
.childDirectory('android')
.childFile('AndroidManifest.xml')
..createSync(recursive: true)
..writeAsStringSync(kAndroidManifestUsingNewEmbedding);
when(androidProject.appManifestFile).thenReturn(androidManifest);
final Directory pluginUsingJavaAndNewEmbeddingDir =
fs.systemTempDirectory.createTempSync('pluginUsingJavaAndNewEmbeddingDir.');
pluginUsingJavaAndNewEmbeddingDir
.childFile('pubspec.yaml')
.writeAsStringSync('''
flutter:
plugin:
androidPackage: plugin1
pluginClass: UseNewEmbedding
''');
pluginUsingJavaAndNewEmbeddingDir
.childDirectory('android')
.childDirectory('src')
.childDirectory('main')
.childDirectory('java')
.childDirectory('plugin1')
.childFile('UseNewEmbedding.java')
..createSync(recursive: true)
..writeAsStringSync('import io.flutter.embedding.engine.plugins.FlutterPlugin;');
final Directory pluginUsingKotlinAndNewEmbeddingDir =
fs.systemTempDirectory.createTempSync('pluginUsingKotlinAndNewEmbeddingDir.');
pluginUsingKotlinAndNewEmbeddingDir
.childFile('pubspec.yaml')
.writeAsStringSync('''
flutter:
plugin:
androidPackage: plugin2
pluginClass: UseNewEmbedding
''');
pluginUsingKotlinAndNewEmbeddingDir
.childDirectory('android')
.childDirectory('src')
.childDirectory('main')
.childDirectory('kotlin')
.childDirectory('plugin2')
.childFile('UseNewEmbedding.kt')
..createSync(recursive: true)
..writeAsStringSync('import io.flutter.embedding.engine.plugins.FlutterPlugin');
final Directory pluginUsingOldEmbeddingDir =
fs.systemTempDirectory.createTempSync('pluginUsingOldEmbeddingDir.');
pluginUsingOldEmbeddingDir
.childFile('pubspec.yaml')
.writeAsStringSync('''
flutter:
plugin:
androidPackage: plugin3
pluginClass: UseOldEmbedding
''');
pluginUsingOldEmbeddingDir
.childDirectory('android')
.childDirectory('src')
.childDirectory('main')
.childDirectory('java')
.childDirectory('plugin3')
.childFile('UseOldEmbedding.java')
..createSync(recursive: true);
flutterProject.directory
.childFile('.packages')
.writeAsStringSync('''
plugin1:${pluginUsingJavaAndNewEmbeddingDir.childDirectory('lib').uri.toString()}
plugin2:${pluginUsingKotlinAndNewEmbeddingDir.childDirectory('lib').uri.toString()}
plugin3:${pluginUsingOldEmbeddingDir.childDirectory('lib').uri.toString()}
''');
await injectPlugins(flutterProject);
final File registrant = flutterProject.directory
.childDirectory(fs.path.join('android', 'app', 'src', 'main', 'java', 'dev', 'flutter', 'plugins'))
.childFile('GeneratedPluginRegistrant.java');
expect(registrant.readAsStringSync(),
contains('flutterEngine.getPlugins().add(new plugin1.UseNewEmbedding());'));
expect(registrant.readAsStringSync(),
contains('flutterEngine.getPlugins().add(new plugin2.UseNewEmbedding());'));
expect(registrant.readAsStringSync(),
contains('plugin3.UseOldEmbedding.registerWith(shimPluginRegistry.registrarFor("plugin3.UseOldEmbedding"));'));
}, overrides: <Type, Generator>{
FileSystem: () => fs,
FeatureFlags: () => featureFlags,
XcodeProjectInterpreter: () => xcodeProjectInterpreter,
});
testUsingContext('Registrant doesn\'t use new embedding if app doesn\'t use new embedding', () async {
when(flutterProject.isModule).thenReturn(false);
when(featureFlags.isNewAndroidEmbeddingEnabled).thenReturn(true);
final File androidManifest = flutterProject.directory
.childDirectory('android')
.childFile('AndroidManifest.xml')
..createSync(recursive: true)
..writeAsStringSync(kAndroidManifestUsingOldEmbedding);
when(androidProject.appManifestFile).thenReturn(androidManifest);
await injectPlugins(flutterProject);
final File registrant = flutterProject.directory
.childDirectory(fs.path.join('android', 'app', 'src', 'main', 'java', 'io', 'flutter', 'plugins'))
.childFile('GeneratedPluginRegistrant.java');
expect(registrant.existsSync(), isTrue);
expect(registrant.readAsStringSync(), contains('package io.flutter.plugins'));
expect(registrant.readAsStringSync(), contains('class GeneratedPluginRegistrant'));
}, overrides: <Type, Generator>{
FileSystem: () => fs,
FeatureFlags: () => featureFlags,
});
testUsingContext('Registrant uses old embedding in module project', () async {
when(flutterProject.isModule).thenReturn(true);
when(featureFlags.isNewAndroidEmbeddingEnabled).thenReturn(false);
await injectPlugins(flutterProject);
final File registrant = flutterProject.directory
.childDirectory(fs.path.join('android', 'app', 'src', 'main', 'java', 'io', 'flutter', 'plugins'))
.childFile('GeneratedPluginRegistrant.java');
expect(registrant.existsSync(), isTrue);
expect(registrant.readAsStringSync(), contains('package io.flutter.plugins'));
expect(registrant.readAsStringSync(), contains('class GeneratedPluginRegistrant'));
}, overrides: <Type, Generator>{
FileSystem: () => fs,
FeatureFlags: () => featureFlags,
});
testUsingContext('Registrant uses new embedding if module uses new embedding', () async {
when(flutterProject.isModule).thenReturn(true);
when(featureFlags.isNewAndroidEmbeddingEnabled).thenReturn(true);
final File androidManifest = flutterProject.directory
.childDirectory('android')
.childFile('AndroidManifest.xml')
..createSync(recursive: true)
..writeAsStringSync(kAndroidManifestUsingNewEmbedding);
when(androidProject.appManifestFile).thenReturn(androidManifest);
await injectPlugins(flutterProject);
final File registrant = flutterProject.directory
.childDirectory(fs.path.join('android', 'app', 'src', 'main', 'java', 'dev', 'flutter', 'plugins'))
.childFile('GeneratedPluginRegistrant.java');
expect(registrant.existsSync(), isTrue);
expect(registrant.readAsStringSync(), contains('package dev.flutter.plugins'));
expect(registrant.readAsStringSync(), contains('class GeneratedPluginRegistrant'));
}, overrides: <Type, Generator>{
FileSystem: () => fs,
FeatureFlags: () => featureFlags,
});
testUsingContext('Registrant doesn\'t use new embedding if module doesn\'t use new embedding', () async {
when(flutterProject.isModule).thenReturn(true);
when(featureFlags.isNewAndroidEmbeddingEnabled).thenReturn(true);
final File androidManifest = flutterProject.directory
.childDirectory('android')
.childFile('AndroidManifest.xml')
..createSync(recursive: true)
..writeAsStringSync(kAndroidManifestUsingOldEmbedding);
when(androidProject.appManifestFile).thenReturn(androidManifest);
await injectPlugins(flutterProject);
final File registrant = flutterProject.directory
.childDirectory(fs.path.join('android', 'app', 'src', 'main', 'java', 'io', 'flutter', 'plugins'))
.childFile('GeneratedPluginRegistrant.java');
expect(registrant.existsSync(), isTrue);
expect(registrant.readAsStringSync(), contains('package io.flutter.plugins'));
expect(registrant.readAsStringSync(), contains('class GeneratedPluginRegistrant'));
}, overrides: <Type, Generator>{
FileSystem: () => fs,
FeatureFlags: () => featureFlags,
});
});
} }
class MockAndroidProject extends Mock implements AndroidProject {}
class MockFeatureFlags extends Mock implements FeatureFlags {}
class MockFlutterProject extends Mock implements FlutterProject {}
class MockIosProject extends Mock implements IosProject {}
class MockMacOSProject extends Mock implements MacOSProject {}
class MockXcodeProjectInterpreter extends Mock implements XcodeProjectInterpreter {}
...@@ -689,6 +689,7 @@ class TestFeatureFlags implements FeatureFlags { ...@@ -689,6 +689,7 @@ class TestFeatureFlags implements FeatureFlags {
this.isMacOSEnabled = false, this.isMacOSEnabled = false,
this.isWebEnabled = false, this.isWebEnabled = false,
this.isWindowsEnabled = false, this.isWindowsEnabled = false,
this.isNewAndroidEmbeddingEnabled = false,
}); });
@override @override
...@@ -702,4 +703,7 @@ class TestFeatureFlags implements FeatureFlags { ...@@ -702,4 +703,7 @@ class TestFeatureFlags implements FeatureFlags {
@override @override
final bool isWindowsEnabled; final bool isWindowsEnabled;
@override
final bool isNewAndroidEmbeddingEnabled;
} }
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment