From 30216c4a4c06c20f9c4c2b9a235a4aa9a48816a0 Mon Sep 17 00:00:00 2001 From: Mike Diarmid Date: Tue, 3 May 2022 11:59:42 +0100 Subject: [PATCH] feat(android,core): allow initializing default Firebase apps via `FirebaseOptions.fromResource` on Android (#8566) --- .../core/FlutterFirebaseCorePlugin.java | 67 ++++++++------ .../example/android/app/build.gradle | 58 ++++++------- .../android/app/src/main/AndroidManifest.xml | 1 + .../app/src/main/res/values/values.xml | 11 +++ .../example/android/build.gradle | 1 - .../example/android/gradle.properties | 4 - .../gradle/wrapper/gradle-wrapper.properties | 2 +- .../example/android/settings.gradle | 18 ++-- .../example/lib/firebase_config.dart | 35 -------- .../example/lib/firebase_options.dart | 87 +++++++++++++++++++ .../firebase_core/example/lib/main.dart | 44 ++++++---- .../method_channel_firebase.dart | 38 +++++--- 12 files changed, 229 insertions(+), 137 deletions(-) create mode 100644 packages/firebase_core/firebase_core/example/android/app/src/main/res/values/values.xml delete mode 100644 packages/firebase_core/firebase_core/example/lib/firebase_config.dart create mode 100644 packages/firebase_core/firebase_core/example/lib/firebase_options.dart diff --git a/packages/firebase_core/firebase_core/android/src/main/java/io/flutter/plugins/firebase/core/FlutterFirebaseCorePlugin.java b/packages/firebase_core/firebase_core/android/src/main/java/io/flutter/plugins/firebase/core/FlutterFirebaseCorePlugin.java index 69a3711211f5..dcf7a0acac22 100644 --- a/packages/firebase_core/firebase_core/android/src/main/java/io/flutter/plugins/firebase/core/FlutterFirebaseCorePlugin.java +++ b/packages/firebase_core/firebase_core/android/src/main/java/io/flutter/plugins/firebase/core/FlutterFirebaseCorePlugin.java @@ -73,39 +73,43 @@ public void onDetachedFromEngine(@NonNull FlutterPluginBinding binding) { applicationContext = null; } - private Task> firebaseAppToMap(FirebaseApp firebaseApp) { - return Tasks.call( - cachedThreadPool, - () -> { - Map appMap = new HashMap<>(); - Map optionsMap = new HashMap<>(); - FirebaseOptions options = firebaseApp.getOptions(); + private Map firebaseOptionsToMap(FirebaseOptions options) { + Map optionsMap = new HashMap<>(); - optionsMap.put(KEY_API_KEY, options.getApiKey()); - optionsMap.put(KEY_APP_ID, options.getApplicationId()); + optionsMap.put(KEY_API_KEY, options.getApiKey()); + optionsMap.put(KEY_APP_ID, options.getApplicationId()); - if (options.getGcmSenderId() != null) { - optionsMap.put(KEY_MESSAGING_SENDER_ID, options.getGcmSenderId()); - } + if (options.getGcmSenderId() != null) { + optionsMap.put(KEY_MESSAGING_SENDER_ID, options.getGcmSenderId()); + } - if (options.getProjectId() != null) { - optionsMap.put(KEY_PROJECT_ID, options.getProjectId()); - } + if (options.getProjectId() != null) { + optionsMap.put(KEY_PROJECT_ID, options.getProjectId()); + } - if (options.getDatabaseUrl() != null) { - optionsMap.put(KEY_DATABASE_URL, options.getDatabaseUrl()); - } + if (options.getDatabaseUrl() != null) { + optionsMap.put(KEY_DATABASE_URL, options.getDatabaseUrl()); + } - if (options.getStorageBucket() != null) { - optionsMap.put(KEY_STORAGE_BUCKET, options.getStorageBucket()); - } + if (options.getStorageBucket() != null) { + optionsMap.put(KEY_STORAGE_BUCKET, options.getStorageBucket()); + } - if (options.getGaTrackingId() != null) { - optionsMap.put(KEY_TRACKING_ID, options.getGaTrackingId()); - } + if (options.getGaTrackingId() != null) { + optionsMap.put(KEY_TRACKING_ID, options.getGaTrackingId()); + } + + return optionsMap; + } + + private Task> firebaseAppToMap(FirebaseApp firebaseApp) { + return Tasks.call( + cachedThreadPool, + () -> { + Map appMap = new HashMap<>(); appMap.put(KEY_NAME, firebaseApp.getName()); - appMap.put(KEY_OPTIONS, optionsMap); + appMap.put(KEY_OPTIONS, firebaseOptionsToMap(firebaseApp.getOptions())); appMap.put( KEY_IS_AUTOMATIC_DATA_COLLECTION_ENABLED, @@ -173,6 +177,16 @@ private Task>> initializeCore() { }); } + private Task> firebaseOptionsFromResource() { + return Tasks.call( + cachedThreadPool, + () -> { + final FirebaseOptions options = FirebaseOptions.fromResource(applicationContext); + if (options == null) return null; + return firebaseOptionsToMap(options); + }); + } + private Task setAutomaticDataCollectionEnabled(Map arguments) { return Tasks.call( cachedThreadPool, @@ -224,6 +238,9 @@ public void onMethodCall(MethodCall call, @NonNull final MethodChannel.Result re case "Firebase#initializeCore": methodCallTask = initializeCore(); break; + case "Firebase#optionsFromResource": + methodCallTask = firebaseOptionsFromResource(); + break; case "FirebaseApp#setAutomaticDataCollectionEnabled": methodCallTask = setAutomaticDataCollectionEnabled(call.arguments()); break; diff --git a/packages/firebase_core/firebase_core/example/android/app/build.gradle b/packages/firebase_core/firebase_core/example/android/app/build.gradle index d92c45688fd2..cf4434ead58e 100644 --- a/packages/firebase_core/firebase_core/example/android/app/build.gradle +++ b/packages/firebase_core/firebase_core/example/android/app/build.gradle @@ -1,61 +1,55 @@ def localProperties = new Properties() def localPropertiesFile = rootProject.file('local.properties') if (localPropertiesFile.exists()) { - localPropertiesFile.withReader('UTF-8') { reader -> - localProperties.load(reader) - } + 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.") + 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' + flutterVersionCode = '1' } def flutterVersionName = localProperties.getProperty('flutter.versionName') if (flutterVersionName == null) { - flutterVersionName = '1.0' + flutterVersionName = '1.0' } apply plugin: 'com.android.application' apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" android { - compileSdkVersion 29 - - lintOptions { - disable 'InvalidPackage' - } - - defaultConfig { - applicationId "io.flutter.plugins.firebasecoreexample" - minSdkVersion 21 - targetSdkVersion 28 - versionCode flutterVersionCode.toInteger() - versionName flutterVersionName - testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" - } - - buildTypes { - release { - // TODO: Add your own signing config for the release build. - // Signing with the debug keys for now, so `flutter run --release` works. - signingConfig signingConfigs.debug - } + compileSdkVersion flutter.compileSdkVersion + + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + + defaultConfig { + applicationId "io.flutter.plugins.firebasecoreexample" + minSdkVersion flutter.minSdkVersion + targetSdkVersion flutter.targetSdkVersion + versionCode flutterVersionCode.toInteger() + versionName flutterVersionName + } + + buildTypes { + release { + signingConfig signingConfigs.debug } + } } flutter { - source '../..' + source '../..' } dependencies { - testImplementation 'junit:junit:4.12' - androidTestImplementation 'androidx.test:runner:1.2.0' - androidTestImplementation 'androidx.test:rules:1.2.0' - androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0' } diff --git a/packages/firebase_core/firebase_core/example/android/app/src/main/AndroidManifest.xml b/packages/firebase_core/firebase_core/example/android/app/src/main/AndroidManifest.xml index 0aa72d970821..8a53ec0757d9 100644 --- a/packages/firebase_core/firebase_core/example/android/app/src/main/AndroidManifest.xml +++ b/packages/firebase_core/firebase_core/example/android/app/src/main/AndroidManifest.xml @@ -6,6 +6,7 @@ + + 406099696497-a12gakvts4epfk5pkio7dphc1anjiggc.apps.googleusercontent.com + https://flutterfire-e2e-tests-default-rtdb.europe-west1.firebasedatabase.app + 406099696497 + AIzaSyCdRjCVZlhrq72RuEklEyyxYlBRCYhI2Sw + 1:406099696497:android:0d4ed619c031c0ac3574d0 + AIzaSyCdRjCVZlhrq72RuEklEyyxYlBRCYhI2Sw + flutterfire-e2e-tests.appspot.com + flutterfire-e2e-tests + diff --git a/packages/firebase_core/firebase_core/example/android/build.gradle b/packages/firebase_core/firebase_core/example/android/build.gradle index 0c7aa9240ce0..041ce013891c 100644 --- a/packages/firebase_core/firebase_core/example/android/build.gradle +++ b/packages/firebase_core/firebase_core/example/android/build.gradle @@ -6,7 +6,6 @@ buildscript { dependencies { classpath 'com.android.tools.build:gradle:4.1.2' - classpath 'com.google.gms:google-services:4.3.4' } } diff --git a/packages/firebase_core/firebase_core/example/android/gradle.properties b/packages/firebase_core/firebase_core/example/android/gradle.properties index d19e73a14147..94adc3a3f97a 100644 --- a/packages/firebase_core/firebase_core/example/android/gradle.properties +++ b/packages/firebase_core/firebase_core/example/android/gradle.properties @@ -1,7 +1,3 @@ org.gradle.jvmargs=-Xmx1536M -android.enableR8=true android.useAndroidX=true android.enableJetifier=true -org.gradle.daemon=true -org.gradle.caching=true -org.gradle.parallel=true diff --git a/packages/firebase_core/firebase_core/example/android/gradle/wrapper/gradle-wrapper.properties b/packages/firebase_core/firebase_core/example/android/gradle/wrapper/gradle-wrapper.properties index 90f271dfdedc..bc6a58afdda2 100644 --- a/packages/firebase_core/firebase_core/example/android/gradle/wrapper/gradle-wrapper.properties +++ b/packages/firebase_core/firebase_core/example/android/gradle/wrapper/gradle-wrapper.properties @@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-6.5-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-6.7-all.zip diff --git a/packages/firebase_core/firebase_core/example/android/settings.gradle b/packages/firebase_core/firebase_core/example/android/settings.gradle index 5a2f14fb18f6..44e62bcf06ae 100644 --- a/packages/firebase_core/firebase_core/example/android/settings.gradle +++ b/packages/firebase_core/firebase_core/example/android/settings.gradle @@ -1,15 +1,11 @@ include ':app' -def flutterProjectRoot = rootProject.projectDir.parentFile.toPath() +def localPropertiesFile = new File(rootProject.projectDir, "local.properties") +def properties = new Properties() -def plugins = new Properties() -def pluginsFile = new File(flutterProjectRoot.toFile(), '.flutter-plugins') -if (pluginsFile.exists()) { - pluginsFile.withReader('UTF-8') { reader -> plugins.load(reader) } -} +assert localPropertiesFile.exists() +localPropertiesFile.withReader("UTF-8") { reader -> properties.load(reader) } -plugins.each { name, path -> - def pluginDirectory = flutterProjectRoot.resolve(path).resolve('android').toFile() - include ":$name" - project(":$name").projectDir = pluginDirectory -} +def flutterSdkPath = properties.getProperty("flutter.sdk") +assert flutterSdkPath != null, "flutter.sdk not set in local.properties" +apply from: "$flutterSdkPath/packages/flutter_tools/gradle/app_plugin_loader.gradle" diff --git a/packages/firebase_core/firebase_core/example/lib/firebase_config.dart b/packages/firebase_core/firebase_core/example/lib/firebase_config.dart deleted file mode 100644 index fe45999096b4..000000000000 --- a/packages/firebase_core/firebase_core/example/lib/firebase_config.dart +++ /dev/null @@ -1,35 +0,0 @@ -import 'dart:io'; - -import 'package:firebase_core/firebase_core.dart'; -import 'package:flutter/foundation.dart'; - -class DefaultFirebaseConfig { - static FirebaseOptions get platformOptions { - if (kIsWeb) { - // Web - return const FirebaseOptions( - appId: '1:448618578101:web:0b650370bb29e29cac3efc', - apiKey: 'AIzaSyAgUhHU8wSJgO5MVNy95tMT07NEjzMOfz0', - projectId: 'react-native-firebase-testing', - messagingSenderId: '448618578101', - ); - } else if (Platform.isIOS || Platform.isMacOS) { - // iOS and MacOS - return const FirebaseOptions( - appId: '1:448618578101:ios:0b650370bb29e29cac3efc', - apiKey: 'AIzaSyAgUhHU8wSJgO5MVNy95tMT07NEjzMOfz0', - projectId: 'react-native-firebase-testing', - messagingSenderId: '448618578101', - iosBundleId: 'io.flutter.plugins.firebasecoreexample', - ); - } else { - // Android - return const FirebaseOptions( - appId: '1:448618578101:android:0446912d5f1476b6ac3efc', - apiKey: 'AIzaSyCuu4tbv9CwwTudNOweMNstzZHIDBhgJxA', - projectId: 'react-native-firebase-testing', - messagingSenderId: '448618578101', - ); - } - } -} diff --git a/packages/firebase_core/firebase_core/example/lib/firebase_options.dart b/packages/firebase_core/firebase_core/example/lib/firebase_options.dart new file mode 100644 index 000000000000..191948500928 --- /dev/null +++ b/packages/firebase_core/firebase_core/example/lib/firebase_options.dart @@ -0,0 +1,87 @@ +// File generated by FlutterFire CLI. +// ignore_for_file: lines_longer_than_80_chars, avoid_classes_with_only_static_members +import 'package:firebase_core/firebase_core.dart' show FirebaseOptions; +import 'package:flutter/foundation.dart' + show defaultTargetPlatform, kIsWeb, TargetPlatform; + +/// Default [FirebaseOptions] for use with your Firebase apps. +/// +/// Example: +/// ```dart +/// import 'firebase_options.dart'; +/// // ... +/// await Firebase.initializeApp( +/// options: DefaultFirebaseOptions.currentPlatform, +/// ); +/// ``` +class DefaultFirebaseOptions { + static FirebaseOptions get currentPlatform { + if (kIsWeb) { + return web; + } + switch (defaultTargetPlatform) { + case TargetPlatform.android: + return android; + case TargetPlatform.iOS: + return ios; + case TargetPlatform.macOS: + return macos; + default: + throw UnsupportedError( + 'DefaultFirebaseOptions are not supported for this platform.', + ); + } + } + + static const FirebaseOptions web = FirebaseOptions( + apiKey: 'AIzaSyB7wZb2tO1-Fs6GbDADUSTs2Qs3w08Hovw', + appId: '1:406099696497:web:87e25e51afe982cd3574d0', + messagingSenderId: '406099696497', + projectId: 'flutterfire-e2e-tests', + authDomain: 'flutterfire-e2e-tests.firebaseapp.com', + databaseURL: + 'https://flutterfire-e2e-tests-default-rtdb.europe-west1.firebasedatabase.app', + storageBucket: 'flutterfire-e2e-tests.appspot.com', + measurementId: 'G-JN95N1JV2E', + ); + + static const FirebaseOptions android = FirebaseOptions( + apiKey: 'AIzaSyCdRjCVZlhrq72RuEklEyyxYlBRCYhI2Sw', + appId: '1:406099696497:android:0d4ed619c031c0ac3574d0', + messagingSenderId: '406099696497', + projectId: 'flutterfire-e2e-tests', + databaseURL: + 'https://flutterfire-e2e-tests-default-rtdb.europe-west1.firebasedatabase.app', + storageBucket: 'flutterfire-e2e-tests.appspot.com', + ); + + static const FirebaseOptions ios = FirebaseOptions( + apiKey: 'AIzaSyDooSUGSf63Ghq02_iIhtnmwMDs4HlWS6c', + appId: '1:406099696497:ios:acd9c8e17b5e620e3574d0', + messagingSenderId: '406099696497', + projectId: 'flutterfire-e2e-tests', + databaseURL: + 'https://flutterfire-e2e-tests-default-rtdb.europe-west1.firebasedatabase.app', + storageBucket: 'flutterfire-e2e-tests.appspot.com', + androidClientId: + '406099696497-tvtvuiqogct1gs1s6lh114jeps7hpjm5.apps.googleusercontent.com', + iosClientId: + '406099696497-taeapvle10rf355ljcvq5dt134mkghmp.apps.googleusercontent.com', + iosBundleId: 'io.flutter.plugins.firebase.tests', + ); + + static const FirebaseOptions macos = FirebaseOptions( + apiKey: 'AIzaSyDooSUGSf63Ghq02_iIhtnmwMDs4HlWS6c', + appId: '1:406099696497:ios:acd9c8e17b5e620e3574d0', + messagingSenderId: '406099696497', + projectId: 'flutterfire-e2e-tests', + databaseURL: + 'https://flutterfire-e2e-tests-default-rtdb.europe-west1.firebasedatabase.app', + storageBucket: 'flutterfire-e2e-tests.appspot.com', + androidClientId: + '406099696497-tvtvuiqogct1gs1s6lh114jeps7hpjm5.apps.googleusercontent.com', + iosClientId: + '406099696497-taeapvle10rf355ljcvq5dt134mkghmp.apps.googleusercontent.com', + iosBundleId: 'io.flutter.plugins.firebase.tests', + ); +} diff --git a/packages/firebase_core/firebase_core/example/lib/main.dart b/packages/firebase_core/firebase_core/example/lib/main.dart index 13ae5fa6a618..78a4243b8085 100644 --- a/packages/firebase_core/firebase_core/example/lib/main.dart +++ b/packages/firebase_core/firebase_core/example/lib/main.dart @@ -3,9 +3,10 @@ // found in the LICENSE file. import 'dart:async'; -import 'package:firebase_core_example/firebase_config.dart'; +import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:firebase_core/firebase_core.dart'; +import 'firebase_options.dart'; void main() => runApp(const MyApp()); @@ -14,23 +15,27 @@ class MyApp extends StatelessWidget { String get name => 'foo'; - FirebaseOptions get firebaseOptions => const FirebaseOptions( - appId: '1:448618578101:ios:0b650370bb29e29cac3efc', - apiKey: 'AIzaSyAgUhHU8wSJgO5MVNy95tMT07NEjzMOfz0', - projectId: 'react-native-firebase-testing', - messagingSenderId: '448618578101', - ); - Future initializeDefault() async { FirebaseApp app = await Firebase.initializeApp( - options: DefaultFirebaseConfig.platformOptions, + options: DefaultFirebaseOptions.currentPlatform, ); print('Initialized default app $app'); } + Future initializeDefaultFromAndroidResource() async { + if (defaultTargetPlatform != TargetPlatform.android || kIsWeb) { + print('Not running on Android, skipping'); + return; + } + FirebaseApp app = await Firebase.initializeApp(); + print('Initialized default app $app from Android resource'); + } + Future initializeSecondary() async { - FirebaseApp app = - await Firebase.initializeApp(name: name, options: firebaseOptions); + FirebaseApp app = await Firebase.initializeApp( + name: name, + options: DefaultFirebaseOptions.currentPlatform, + ); print('Initialized $app'); } @@ -41,9 +46,9 @@ class MyApp extends StatelessWidget { } void options() { - final FirebaseApp app = Firebase.app(name); + final FirebaseApp app = Firebase.app(); final options = app.options; - print('Current options for app $name: $options'); + print('Current options for app ${app.name}: $options'); } Future delete() async { @@ -69,21 +74,28 @@ class MyApp extends StatelessWidget { onPressed: initializeDefault, child: const Text('Initialize default app'), ), + if (defaultTargetPlatform == TargetPlatform.android && !kIsWeb) + ElevatedButton( + onPressed: initializeDefaultFromAndroidResource, + child: const Text( + 'Initialize default app from Android resources', + ), + ), ElevatedButton( onPressed: initializeSecondary, child: const Text('Initialize secondary app'), ), ElevatedButton( onPressed: apps, - child: const Text('Get apps'), + child: const Text('List apps'), ), ElevatedButton( onPressed: options, - child: const Text('List options'), + child: const Text('List default options'), ), ElevatedButton( onPressed: delete, - child: const Text('Delete app'), + child: const Text('Delete secondary app'), ), ], ), diff --git a/packages/firebase_core/firebase_core_platform_interface/lib/src/method_channel/method_channel_firebase.dart b/packages/firebase_core/firebase_core_platform_interface/lib/src/method_channel/method_channel_firebase.dart index c5c9ca33ce17..b281248eeb94 100644 --- a/packages/firebase_core/firebase_core_platform_interface/lib/src/method_channel/method_channel_firebase.dart +++ b/packages/firebase_core/firebase_core_platform_interface/lib/src/method_channel/method_channel_firebase.dart @@ -79,23 +79,37 @@ class MethodChannelFirebase extends FirebasePlatform { if (name == null || name == defaultFirebaseAppName) { MethodChannelFirebaseApp? defaultApp = appInstances[defaultFirebaseAppName]; + FirebaseOptions? _options = options; + // If no default app and no options are provided then + // attempt to read options from native resources on Android, + // e.g. this calls to `FirebaseOptions.fromResource(context)`. + if (defaultTargetPlatform == TargetPlatform.android && + defaultApp == null && + _options == null) { + final optionsMap = await channel.invokeMapMethod( + 'Firebase#optionsFromResource', + ); + if (optionsMap != null) { + _options = FirebaseOptions.fromMap(optionsMap); + } + } - // If options are present & no default app has been setup, the user is - // trying to initialize default from dart - if (defaultApp == null && options != null) { + // If no options are present & no default app has been setup, the user is + // trying to initialize default from Dart + if (defaultApp == null && _options != null) { _initializeFirebaseAppFromMap((await channel.invokeMapMethod( 'Firebase#initializeApp', { 'appName': defaultFirebaseAppName, - 'options': options.asMap + 'options': _options.asMap }, ))!); defaultApp = appInstances[defaultFirebaseAppName]; } - // If there is no native default app and the user didnt provide options to + // If there is no native default app and the user didn't provide options to // create one, throw. - if (defaultApp == null && options == null) { + if (defaultApp == null && _options == null) { throw coreNotInitialized(); } @@ -103,12 +117,12 @@ class MethodChannelFirebase extends FirebasePlatform { // check to see if options are roughly identical (so we don't unnecessarily // throw on minor differences such as platform specific keys missing // e.g. hot reloads/restarts). - if (defaultApp != null && options != null) { - if (options.apiKey != defaultApp.options.apiKey || - (options.databaseURL != null && - options.databaseURL != defaultApp.options.databaseURL) || - (options.storageBucket != null && - options.storageBucket != defaultApp.options.storageBucket)) { + if (defaultApp != null && _options != null) { + if (_options.apiKey != defaultApp.options.apiKey || + (_options.databaseURL != null && + _options.databaseURL != defaultApp.options.databaseURL) || + (_options.storageBucket != null && + _options.storageBucket != defaultApp.options.storageBucket)) { // Options are different; throw. throw duplicateApp(defaultFirebaseAppName); }