diff --git a/.github/recipe.yaml b/.github/recipe.yaml index e173687..18ba8bd 100644 --- a/.github/recipe.yaml +++ b/.github/recipe.yaml @@ -2,4 +2,4 @@ plugins: firebase_core: ["tv-7.0"] firebase_database: [] firebase_storage: [] - cloud_functions: [] + cloud_functions: ["tv-7.0"] diff --git a/packages/cloud_functions/.clang-format b/packages/cloud_functions/.clang-format new file mode 100644 index 0000000..5ab0945 --- /dev/null +++ b/packages/cloud_functions/.clang-format @@ -0,0 +1,3 @@ +--- +BasedOnStyle: Google +--- diff --git a/packages/cloud_functions/.gitignore b/packages/cloud_functions/.gitignore new file mode 100644 index 0000000..d87f6f0 --- /dev/null +++ b/packages/cloud_functions/.gitignore @@ -0,0 +1,28 @@ +# Miscellaneous +*.class +*.log +*.pyc +*.swp +.DS_Store +.atom/ +.buildlog/ +.history +.svn/ +migrate_working_dir/ + +# IntelliJ related +*.iml +*.ipr +*.iws +.idea/ + +# VS Code related +.vscode/ + +# Flutter/Dart/Pub related +# Libraries should not include pubspec.lock, per https://dart.dev/guides/libraries/private-files#pubspeclock. +/pubspec.lock +**/doc/api/ +.dart_tool/ +.packages +build/ diff --git a/packages/cloud_functions/CHANGELOG.md b/packages/cloud_functions/CHANGELOG.md new file mode 100644 index 0000000..6073234 --- /dev/null +++ b/packages/cloud_functions/CHANGELOG.md @@ -0,0 +1,3 @@ +## 0.1.0 + +* Initial release. diff --git a/packages/cloud_functions/LICENSE b/packages/cloud_functions/LICENSE new file mode 100644 index 0000000..f652d0e --- /dev/null +++ b/packages/cloud_functions/LICENSE @@ -0,0 +1,63 @@ +``` +Copyright (c) 2023 Samsung Electronics Co., Ltd. All rights reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +``` + +``` +Copyright (c) 2013 The Flutter Authors. All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following + disclaimer in the documentation and/or other materials provided + with the distribution. + * Neither the names of the copyright holders nor the names of the + contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +``` + +The externally maintained libraries used by this pluggin is: + +- Firebase C++ Open Source SDK is licensed as follows: + +``` +Copyright 2016 Google LLC + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +``` diff --git a/packages/cloud_functions/README.md b/packages/cloud_functions/README.md new file mode 100644 index 0000000..b15ced5 --- /dev/null +++ b/packages/cloud_functions/README.md @@ -0,0 +1,31 @@ +# cloud_functions_tizen + +The [Firebase Cloud Functions for Flutter](https://pub.dev/packages/cloud_functions) implementation for Tizen. + +It offers experimental features for using Firebase on Flutter for Tizen. It works by wrapping cross-compiled libraries that are based on the [Firebase C++ SDK](https://github.com/firebase/firebase-cpp-sdk) for Linux. + +# Usage + +To use this package, you need to include `cloud_functions_tizen` as a dependency alongside `cloud_functions` and `cloud_functions_platform_interface` in your `pubspec.yaml`. Please note that `cloud_functions_tizen` implementation is not officially endorsed for `cloud_functions`. + +```yaml +dependencies: + cloud_functions: 4.0.7 + cloud_functions_tizen: ^0.1.0 +``` + +Then you can import `cloud_functions` in your Dart code: + +```dart +import 'package:cloud_functions/cloud_functions.dart'; +``` + +# Limitations + +The following features are currently unavailable as they're not supported by the version of Firebase C++ SDK for Linux that this plugin is currently based on. + +- Using `HttpsCallableOptions#timeout` for an HttpsCallable instance's options. + +# Known bugs + +- The code and details of `FirebaseFunctionsException` aren't properly provided. diff --git a/packages/cloud_functions/analysis_options.yaml b/packages/cloud_functions/analysis_options.yaml new file mode 100644 index 0000000..0db44b9 --- /dev/null +++ b/packages/cloud_functions/analysis_options.yaml @@ -0,0 +1,11 @@ +# Copyright 2021 The Chromium Authors. All rights reserved. +# Use of this source code is governed by a BSD-style license that can be +# in the LICENSE file. + +include: ../../analysis_options.yaml + +linter: + rules: + public_member_api_docs: false + depend_on_referenced_packages: false + library_private_types_in_public_api: false diff --git a/packages/cloud_functions/example/.gitignore b/packages/cloud_functions/example/.gitignore new file mode 100644 index 0000000..f980a1b --- /dev/null +++ b/packages/cloud_functions/example/.gitignore @@ -0,0 +1,37 @@ +# Miscellaneous +*.class +*.log +*.pyc +*.swp +.DS_Store +.atom/ +.buildlog/ +.history +.svn/ +migrate_working_dir/ + +# IntelliJ related +*.iml +*.ipr +*.iws +.idea/ + +# VS Code related +.vscode/ + +# Flutter/Dart/Pub related +**/doc/api/ +**/ios/Flutter/.last_build_id +.dart_tool/ +.flutter-plugins +.flutter-plugins-dependencies +.packages +.pub-cache/ +.pub/ +/build/ + +# Symbolication related +app.*.symbols + +# Obfuscation related +app.*.map.json diff --git a/packages/cloud_functions/example/README.md b/packages/cloud_functions/example/README.md new file mode 100644 index 0000000..0302522 --- /dev/null +++ b/packages/cloud_functions/example/README.md @@ -0,0 +1,75 @@ +# Example + +A test app for cloud function plugin (e2e). + +## Prerequisites + +1. Configure [the Firebase Options](https://pub.dev/documentation/firebase_core_platform_interface/latest/firebase_core_platform_interface/FirebaseOptions-class.html) below. + +```dart + // {plugin_example_path}/lib/firebase_options.dart + + static const FirebaseOptions tizen = FirebaseOptions( + apiKey: 'PLACEHOLDER', + appId: 'PLACEHOLDER', + messagingSenderId: 'PLACEHOLDER', + projectId: 'PLACEHOLDER', + databaseURL: 'PLACEHOLDER', + storageBucket: 'PLACEHOLDER', + ); +``` + +2. Configure the Firebase emulator. + +To test Cloud Functions, use the emulator. Run the command below to go to the path where you can run a test app on the emulator. + +```shell +$ cd tools/emulator/functions +``` + +Configure your connection address and projectId. + +```dart +// example/lib/firebase_options.dart +static String get emulatorHost { + return 'XX.XXX.XXX.XXX'; +} +``` + +```json +// tools/emulator/firebase.json +{ + "emulators": { + "functions": { + "port": 5001, + "host": "XX.XXX.XXX.XXX" + } + ... + } +} +``` + +```json +// tools/emulator/.firebaserc +{ + "projects": { + "default": "PLACEHOLDER" + } +} +``` + +3. Run the Firebase emulator for cloud functions. + +```shell +$ cd tools/emulator/functions + +# `npm install` is required at the first time. + +$ npm run serve + +┌───────────┬─────────────────────┬──────────────────────────────────────┐ +│ Emulator │ Host:Port │ View in Emulator UI │ +├───────────┼─────────────────────┼──────────────────────────────────────┤ +│ Functions │ XX.XXX.XXX.XXX:5001 │ http://XX.XXX.XXX.XXX:4000/functions │ +└───────────┴─────────────────────┴──────────────────────────────────────┘ +``` diff --git a/packages/cloud_functions/example/analysis_options.yaml b/packages/cloud_functions/example/analysis_options.yaml new file mode 100644 index 0000000..5e2133e --- /dev/null +++ b/packages/cloud_functions/example/analysis_options.yaml @@ -0,0 +1 @@ +include: ../analysis_options.yaml diff --git a/packages/cloud_functions/example/integration_test/cloud_functions/cloud_functions_e2e_test.dart b/packages/cloud_functions/example/integration_test/cloud_functions/cloud_functions_e2e_test.dart new file mode 100644 index 0000000..766c84b --- /dev/null +++ b/packages/cloud_functions/example/integration_test/cloud_functions/cloud_functions_e2e_test.dart @@ -0,0 +1,208 @@ +// Copyright 2021, the Chromium project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:cloud_functions/cloud_functions.dart'; +import 'package:firebase_core/firebase_core.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:integration_test/integration_test.dart'; +import 'package:tests/firebase_options.dart'; + +import 'sample_data.dart' as data; + +String kTestFunctionDefaultRegion = 'testFunctionDefaultRegion'; +String kTestFunctionCustomRegion = 'testFunctionCustomRegion'; +String kTestFunctionTimeout = 'testFunctionTimeout'; +String kTestMapConvertType = 'testMapConvertType'; +String kEmulatorHost = DefaultFirebaseOptions.emulatorHost; + +void main() { + IntegrationTestWidgetsFlutterBinding.ensureInitialized(); + + group('cloud_functions', () { + late HttpsCallable callable; + + setUpAll(() async { + await Firebase.initializeApp( + options: DefaultFirebaseOptions.currentPlatform, + ); + FirebaseFunctions.instance.useFunctionsEmulator(kEmulatorHost, 5001); + callable = + FirebaseFunctions.instance.httpsCallable(kTestFunctionDefaultRegion); + }); + + group('HttpsCallable', () { + test('returns a [HttpsCallableResult]', () async { + var result = await callable(); + expect(result, isA()); + }); + + test('accepts no arguments', () async { + HttpsCallableResult result = await callable(); + expect(result.data, equals('null')); + }); + + test('accepts `null arguments', () async { + HttpsCallableResult result = await callable(null); + expect(result.data, equals('null')); + }); + + test('accepts a string value', () async { + HttpsCallableResult result = await callable('foo'); + expect(result.data, equals('string')); + }); + + test('accepts a number value', () async { + HttpsCallableResult result = await callable(123); + expect(result.data, equals('number')); + HttpsCallableResult result2 = await callable(12.3); + expect(result2.data, equals('number')); + }); + + test('accepts a boolean value', () async { + HttpsCallableResult result = await callable(true); + expect(result.data, equals('boolean')); + HttpsCallableResult result2 = await callable(false); + expect(result2.data, equals('boolean')); + }); + + test('accepts a [List]', () async { + HttpsCallableResult result = await callable(data.list); + expect(result.data, equals('array')); + }); + + test('accepts a deeply nested [Map]', () async { + HttpsCallableResult result = await callable({ + 'type': 'deepMap', + 'inputData': data.deepMap, + }); + expect(result.data, equals(data.deepMap)); + }); + + test('accepts a deeply nested [List]', () async { + HttpsCallableResult result = await callable({ + 'type': 'deepList', + 'inputData': data.deepList, + }); + expect(result.data, equals(data.deepList)); + }); + + test( + 'accepts raw data as arguments', + () async { + HttpsCallableResult result = await callable({ + 'type': 'rawData', + 'list': Uint8List(100), + 'int': Int32List(39), + 'long': Int64List(45), + 'float': Float32List(23), + 'double': Float64List(1001), + }); + final data = result.data; + expect(data['list'], isA()); + expect(data['int'], isA()); + expect(data['long'], isA()); + expect(data['float'], isA()); + expect(data['double'], isA()); + }, + // Int64List is not supported on Web. + skip: kIsWeb, + ); + + test( + '[HttpsCallableResult.data] should return Map type for returned objects', + () async { + HttpsCallable callable = + FirebaseFunctions.instance.httpsCallable(kTestMapConvertType); + + var result = await callable(); + + expect(result.data, isA>()); + }, + ); + }); + + group('FirebaseFunctionsException', () { + test('HttpsCallable returns a FirebaseFunctionsException on error', + () async { + try { + await callable({}); + fail('Should have thrown'); + } on FirebaseFunctionsException catch (e) { + // FIXME: parse details to meet code message. + // expect(e.code, equals('invalid-argument')); + expect(e.message, equals('Invalid test requested.')); + return; + } catch (e) { + fail('$e'); + } + }); + + test( + 'it returns "details" value as part of the exception', + () async { + try { + await callable({ + 'type': 'deepMap', + 'inputData': data.deepMap, + 'asError': true, + }); + fail('Should have thrown'); + } on FirebaseFunctionsException catch (e) { + expect(e.code, equals('cancelled')); + expect( + e.message, + equals( + 'Response data was requested to be sent as part of an Error payload, so here we are!', + ), + ); + expect(e.details, equals(data.deepMap)); + } catch (e) { + fail('$e'); + } + }, + skip: '"details" is not supported yet', + ); + }); + + group('instanceFor', () { + test('accepts a custom region', () async { + final instance = FirebaseFunctions.instanceFor(region: 'europe-west1'); + instance.useFunctionsEmulator(kEmulatorHost, 5001); + final customRegionCallable = + instance.httpsCallable(kTestFunctionCustomRegion); + final result = await customRegionCallable(); + expect(result.data, equals('europe-west1')); + }); + }); + + group('HttpsCallableOptions', () { + test( + 'times out when the provided timeout option is exceeded', + () async { + final instance = FirebaseFunctions.instance; + instance.useFunctionsEmulator(kEmulatorHost, 5001); + final timeoutCallable = FirebaseFunctions.instance.httpsCallable( + kTestFunctionTimeout, + options: HttpsCallableOptions(timeout: const Duration(seconds: 3)), + ); + try { + await timeoutCallable({ + 'testTimeout': + const Duration(seconds: 6).inMilliseconds.toString(), + }); + fail('Should have thrown'); + } on FirebaseFunctionsException catch (e) { + expect(e.code, equals('deadline-exceeded')); + } catch (e) { + fail('$e'); + } + }, + // Android skip because it's flaky. See: + // https://github.com/firebase/flutterfire/issues/9652 + skip: 'timeout is not supported', + ); + }); + }); +} diff --git a/packages/cloud_functions/example/integration_test/cloud_functions/sample_data.dart b/packages/cloud_functions/example/integration_test/cloud_functions/sample_data.dart new file mode 100644 index 0000000..acf3909 --- /dev/null +++ b/packages/cloud_functions/example/integration_test/cloud_functions/sample_data.dart @@ -0,0 +1,25 @@ +// Copyright 2021, the Chromium project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +Map map = { + 'number': 123, + 'string': 'foo', + 'booleanTrue': true, + 'booleanFalse': false, + 'null': null, +}; + +List list = ['1', 2, true, false]; + +Map deepMap = { + ...map, + 'list': list, + 'map': map, +}; + +List deepList = [ + ...list, + list, + map, +]; diff --git a/packages/cloud_functions/example/lib/firebase_options.dart b/packages/cloud_functions/example/lib/firebase_options.dart new file mode 100644 index 0000000..fb3c146 --- /dev/null +++ b/packages/cloud_functions/example/lib/firebase_options.dart @@ -0,0 +1,46 @@ +/* + * Copyright (c) 2023-present Samsung Electronics Co., Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import 'package:firebase_core/firebase_core.dart' show FirebaseOptions; +import 'package:flutter/foundation.dart' + show defaultTargetPlatform, TargetPlatform; + +class DefaultFirebaseOptions { + static FirebaseOptions get currentPlatform { + switch (defaultTargetPlatform) { + case TargetPlatform.linux: + // Note: To find out if you are using the Tizen platform, refer to the link below. + // https://github.com/flutter-tizen/flutter-tizen/issues/482#issuecomment-1441139704 + return tizen; + default: + throw UnsupportedError( + 'DefaultFirebaseOptions are not supported for this platform.', + ); + } + } + + static String get emulatorHost { + return 'PLACEHOLDER'; + } + + static const FirebaseOptions tizen = FirebaseOptions( + apiKey: 'PLACEHOLDER', + appId: 'PLACEHOLDER', + messagingSenderId: 'PLACEHOLDER', + projectId: 'PLACEHOLDER', + databaseURL: 'PLACEHOLDER', + storageBucket: 'PLACEHOLDER', + ); +} diff --git a/packages/cloud_functions/example/lib/main.dart b/packages/cloud_functions/example/lib/main.dart new file mode 100644 index 0000000..ebadd96 --- /dev/null +++ b/packages/cloud_functions/example/lib/main.dart @@ -0,0 +1,140 @@ +// Copyright 2022, the Chromium project authors. Please see the AUTHORS file +// for details. 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:io'; +import 'package:firebase_core/firebase_core.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'firebase_options.dart'; + +Future main() async { + WidgetsFlutterBinding.ensureInitialized(); + await Firebase.initializeApp( + options: DefaultFirebaseOptions.currentPlatform, + ); + runApp(const MyApp()); +} + +class MyApp extends StatelessWidget { + const MyApp({Key? key}) : super(key: key); + + // This widget is the root of your application. + @override + Widget build(BuildContext context) { + return MaterialApp( + title: 'Flutter Demo', + theme: ThemeData( + // This is the theme of your application. + // + // Try running your application with "flutter run". You'll see the + // application has a blue toolbar. Then, without quitting the app, try + // changing the primarySwatch below to Colors.green and then invoke + // "hot reload" (press "r" in the console where you ran "flutter run", + // or simply save your changes to "hot reload" in a Flutter IDE). + // Notice that the counter didn't reset back to zero; the application + // is not restarted. + primarySwatch: Colors.blue, + ), + home: const MyHomePage(title: 'Flutter Demo Home Page'), + ); + } +} + +class MyHomePage extends StatefulWidget { + const MyHomePage({Key? key, required this.title}) : super(key: key); + + // This widget is the home page of your application. It is stateful, meaning + // that it has a State object (defined below) that contains fields that affect + // how it looks. + + // This class is the configuration for the state. It holds the values (in this + // case the title) provided by the parent (in this case the App widget) and + // used by the build method of the State. Fields in a Widget subclass are + // always marked "final". + + final String title; + + @override + State createState() => _MyHomePageState(); +} + +class _MyHomePageState extends State { + int _counter = 0; + + void _incrementCounter() { + setState(() { + // This call to setState tells the Flutter framework that something has + // changed in this State, which causes it to rerun the build method below + // so that the display can reflect the updated values. If we changed + // _counter without calling setState(), then the build method would not be + // called again, and so nothing would appear to happen. + _counter++; + }); + } + + @override + Widget build(BuildContext context) { + // This method is rerun every time setState is called, for instance as done + // by the _incrementCounter method above. + // + // The Flutter framework has been optimized to make rerunning build methods + // fast, so that you can just rebuild anything that needs updating rather + // than having to individually change instances of widgets. + return Scaffold( + appBar: AppBar( + // Here we take the value from the MyHomePage object that was created by + // the App.build method, and use it to set our appbar title. + title: Text(widget.title), + ), + body: Center( + // Center is a layout widget. It takes a single child and positions it + // in the middle of the parent. + child: Column( + // Column is also a layout widget. It takes a list of children and + // arranges them vertically. By default, it sizes itself to fit its + // children horizontally, and tries to be as tall as its parent. + // + // Invoke "debug painting" (press "p" in the console, choose the + // "Toggle Debug Paint" action from the Flutter Inspector in Android + // Studio, or the "Toggle Debug Paint" command in Visual Studio Code) + // to see the wireframe for each widget. + // + // Column has various properties to control how it sizes itself and + // how it positions its children. Here we use mainAxisAlignment to + // center the children vertically; the main axis here is the vertical + // axis because Columns are vertical (the cross axis would be + // horizontal). + mainAxisAlignment: MainAxisAlignment.center, + children: [ + ElevatedButton( + onPressed: () async { + // Running these APIs manually as they're failing on CI due to required keychain sharing entitlements + // See this issue https://github.com/firebase/flutterfire/issues/9538 + // You will also need to add the keychain sharing entitlements to this test app and sign code with development team for app & tests to successfully run + if (Platform.isMacOS && kDebugMode) { + // ignore_for_file: avoid_print + // Wait a little so we don't get a delete-pending exception + await Future.delayed(const Duration(seconds: 8)); + } + }, + child: const Text('Test macOS tests manually'), + ), + const Text( + 'You have pushed the button this many times:', + ), + Text( + '$_counter', + style: Theme.of(context).textTheme.headline4, + ), + ], + ), + ), + floatingActionButton: FloatingActionButton( + onPressed: _incrementCounter, + tooltip: 'Increment', + child: const Icon(Icons.add), + ), // This trailing comma makes auto-formatting nicer for build methods. + ); + } +} diff --git a/packages/cloud_functions/example/pubspec.yaml b/packages/cloud_functions/example/pubspec.yaml new file mode 100644 index 0000000..b411c2a --- /dev/null +++ b/packages/cloud_functions/example/pubspec.yaml @@ -0,0 +1,29 @@ +name: tests +description: A test app for cloud function plugin (e2e). + +environment: + sdk: ">=2.18.0 <4.0.0" + +dependencies: + cloud_functions: 4.0.7 + cloud_functions_tizen: ^0.1.0 + firebase_core: 2.4.1 + firebase_core_tizen: ^1.0.0 + flutter: + sdk: flutter + +dependency_overrides: + cloud_functions_tizen: + path: .. + firebase_core_tizen: + path: ../../firebase_core + +dev_dependencies: + flutter_test: + sdk: flutter + integration_test: + sdk: flutter + integration_test_tizen: ^2.0.1 + +flutter: + uses-material-design: true diff --git a/packages/cloud_functions/example/test_driver/integration_test.dart b/packages/cloud_functions/example/test_driver/integration_test.dart new file mode 100644 index 0000000..f1ac26f --- /dev/null +++ b/packages/cloud_functions/example/test_driver/integration_test.dart @@ -0,0 +1,7 @@ +// Copyright 2022, the Chromium project authors. Please see the AUTHORS file +// for details. All rights reserved. Use of this source code is governed by a +// BSD-style license that can be found in the LICENSE file. + +import 'package:integration_test/integration_test_driver.dart'; + +Future main() => integrationDriver(); diff --git a/packages/cloud_functions/example/tizen/.gitignore b/packages/cloud_functions/example/tizen/.gitignore new file mode 100644 index 0000000..750f3af --- /dev/null +++ b/packages/cloud_functions/example/tizen/.gitignore @@ -0,0 +1,5 @@ +flutter/ +.vs/ +*.user +bin/ +obj/ diff --git a/packages/cloud_functions/example/tizen/App.cs b/packages/cloud_functions/example/tizen/App.cs new file mode 100644 index 0000000..6dd4a63 --- /dev/null +++ b/packages/cloud_functions/example/tizen/App.cs @@ -0,0 +1,20 @@ +using Tizen.Flutter.Embedding; + +namespace Runner +{ + public class App : FlutterApplication + { + protected override void OnCreate() + { + base.OnCreate(); + + GeneratedPluginRegistrant.RegisterPlugins(this); + } + + static void Main(string[] args) + { + var app = new App(); + app.Run(args); + } + } +} diff --git a/packages/cloud_functions/example/tizen/Runner.csproj b/packages/cloud_functions/example/tizen/Runner.csproj new file mode 100644 index 0000000..f4e369d --- /dev/null +++ b/packages/cloud_functions/example/tizen/Runner.csproj @@ -0,0 +1,19 @@ + + + + Exe + tizen40 + + + + + + + + + + %(RecursiveDir) + + + + diff --git a/packages/cloud_functions/example/tizen/shared/res/ic_launcher.png b/packages/cloud_functions/example/tizen/shared/res/ic_launcher.png new file mode 100644 index 0000000..4d6372e Binary files /dev/null and b/packages/cloud_functions/example/tizen/shared/res/ic_launcher.png differ diff --git a/packages/cloud_functions/example/tizen/tizen-manifest.xml b/packages/cloud_functions/example/tizen/tizen-manifest.xml new file mode 100644 index 0000000..b912296 --- /dev/null +++ b/packages/cloud_functions/example/tizen/tizen-manifest.xml @@ -0,0 +1,16 @@ + + + + + + ic_launcher.png + + + + T-INFOLINK2021-1000 + + + http://tizen.org/privilege/internet + + + diff --git a/packages/cloud_functions/pubspec.yaml b/packages/cloud_functions/pubspec.yaml new file mode 100644 index 0000000..26299f6 --- /dev/null +++ b/packages/cloud_functions/pubspec.yaml @@ -0,0 +1,34 @@ +name: cloud_functions_tizen +description: A Flutter plugin allowing you to use Firebase Cloud Functions. +version: 0.1.0 +homepage: +repository: +publish_to: 'none' + +environment: + sdk: ">=2.18.0 <4.0.0" + flutter: ">=3.3.0" + +dependencies: + firebase_core_tizen: ^1.0.0 + cloud_functions: 4.0.7 + cloud_functions_platform_interface: 5.1.26 + flutter: + sdk: flutter + +dependency_overrides: + firebase_core_tizen: + path: ../firebase_core + +dev_dependencies: + flutter_test: + sdk: flutter + flutter_lints: ^2.0.0 + +flutter: + plugin: + implements: cloud_functions + platforms: + tizen: + pluginClass: CloudFunctionsPlugin + fileName: cloud_functions_plugin.h diff --git a/packages/cloud_functions/tizen/.gitignore b/packages/cloud_functions/tizen/.gitignore new file mode 100644 index 0000000..a2a7d62 --- /dev/null +++ b/packages/cloud_functions/tizen/.gitignore @@ -0,0 +1,5 @@ +.cproject +.sign +crash-info/ +Debug/ +Release/ diff --git a/packages/cloud_functions/tizen/build_def.prop b/packages/cloud_functions/tizen/build_def.prop new file mode 100644 index 0000000..64d7f61 --- /dev/null +++ b/packages/cloud_functions/tizen/build_def.prop @@ -0,0 +1,2 @@ +PREBUILD_COMMAND = ./tar_url.sh \ + https://github.com/daeye0n/gooddaytocode/archive/refs/tags/v10.4.0-draft-4.tar.gz diff --git a/packages/cloud_functions/tizen/dep/conversion.cc b/packages/cloud_functions/tizen/dep/conversion.cc new file mode 100644 index 0000000..44d0035 --- /dev/null +++ b/packages/cloud_functions/tizen/dep/conversion.cc @@ -0,0 +1,122 @@ +/* + * Copyright (c) 2023-present Samsung Electronics Co., Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "common/conversion.h" + +#include + +#include "common/trace.h" // for UNIMPLEMENTED and FATAL +#include "common/utils.h" + +using firebase::Variant; +using flutter::EncodableList; +using flutter::EncodableMap; +using flutter::EncodableValue; + +Variant Conversion::ToFirebaseVariant(const EncodableValue& encodable_value) { + switch (encodable_value.index()) { + case 0: // std::monostate + return Variant(); + case 1: // bool + return Variant(std::get(encodable_value)); + case 2: // int32_t + return Variant(std::get(encodable_value)); + case 3: // int64_t + return Variant(std::get(encodable_value)); + case 4: // double + return Variant(std::get(encodable_value)); + case 5: // std::string + return Variant(std::get(encodable_value)); + case 6: // std::vector + return Variant(std::get>(encodable_value)); + case 7: // std::vector + return Variant(std::get>(encodable_value)); + case 8: // std::vector + return Variant(std::get>(encodable_value)); + case 9: // std::vector + return Variant(std::get>(encodable_value)); + case 10: // EncodableList + return Conversion::ToFirebaseVariant( + std::get(encodable_value)); + case 11: // EncodableMap + return Conversion::ToFirebaseVariant( + std::get(encodable_value)); + case 12: // CustomEncodableValue + UNIMPLEMENTED("Unknown to handle this"); + return Variant(); + case 13: // std::vector + return Variant(std::get>(encodable_value)); + default: + FATAL("Invalid EncodableValue type"); + } + return Variant(); +} + +Variant Conversion::ToFirebaseVariant(const EncodableList& encodable_list) { + std::vector variant_list; + for (const EncodableValue& encodable_value : encodable_list) { + variant_list.push_back(Conversion::ToFirebaseVariant(encodable_value)); + } + return Variant(variant_list); +} + +Variant Conversion::ToFirebaseVariant(const EncodableMap& encodable_map) { + std::map variant_map; + for (const auto& [key, value] : encodable_map) { + variant_map.emplace(Conversion::ToFirebaseVariant(key), + Conversion::ToFirebaseVariant(value)); + } + return Variant(variant_map); +} + +Variant Conversion::ToFirebaseVariant(const EncodableMap* map, + const char* key) { + return Conversion::ToFirebaseVariant(GetEncodableValue(map, key)); +} + +EncodableValue Conversion::ToEncodableValue(const Variant& v) { + switch (v.type()) { + case Variant::kTypeNull: + return EncodableValue(std::monostate()); + case Variant::kTypeInt64: + return EncodableValue(v.int64_value()); + case Variant::kTypeDouble: + return EncodableValue(v.double_value()); + case Variant::kTypeBool: + return EncodableValue(v.bool_value()); + case Variant::kTypeStaticString: + return EncodableValue(std::string(v.string_value())); + case Variant::kTypeMutableString: + return EncodableValue(v.mutable_string()); + case Variant::kTypeVector: { + EncodableList list; + for (const auto& e : v.vector()) { + list.push_back(ToEncodableValue(e)); + } + return EncodableValue(list); + } + case Variant::kTypeMap: { + EncodableMap map; + for (const auto& [key, value] : v.map()) { + map[ToEncodableValue(key)] = ToEncodableValue(value); + } + return EncodableValue(map); + } + default: + FATAL("Unsupported Variant type"); + } + return EncodableValue(); +} diff --git a/packages/cloud_functions/tizen/dep/include/common/conversion.h b/packages/cloud_functions/tizen/dep/include/common/conversion.h new file mode 100644 index 0000000..10a2507 --- /dev/null +++ b/packages/cloud_functions/tizen/dep/include/common/conversion.h @@ -0,0 +1,29 @@ +/* + * Copyright (c) 2023-present Samsung Electronics Co., Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +#pragma once + +#include +#include + +class Conversion { + public: + static firebase::Variant ToFirebaseVariant(const flutter::EncodableValue& v); + static firebase::Variant ToFirebaseVariant(const flutter::EncodableList& l); + static firebase::Variant ToFirebaseVariant(const flutter::EncodableMap& m); + static firebase::Variant ToFirebaseVariant(const flutter::EncodableMap* m, + const char* key); + static flutter::EncodableValue ToEncodableValue(const firebase::Variant& v); +}; diff --git a/packages/cloud_functions/tizen/dep/include/common/logger.h b/packages/cloud_functions/tizen/dep/include/common/logger.h new file mode 100644 index 0000000..95d6b62 --- /dev/null +++ b/packages/cloud_functions/tizen/dep/include/common/logger.h @@ -0,0 +1,168 @@ +/* + * Copyright (c) 2022-present Samsung Electronics Co., Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +#pragma once + +#include +#include +#include +#include +#include +#include + +class LogOption { + public: + static bool isEnabled(const std::string& pattern = ""); + static void setExternalIsEnabled(std::function); + + private: + static std::function externalIsEnabled; +}; + +class Logger { + public: + class Header { + protected: + virtual void writeHeader(std::stringstream&) = 0; + + private: + void write(std::stringstream& stream); + friend class Logger; + }; + + class Output { + public: + virtual ~Output() = default; + virtual void flush(std::stringstream& ss) = 0; + }; + + Logger(std::shared_ptr out = nullptr); + Logger(const std::string& header, std::shared_ptr out = nullptr); + Logger(Header&& header, std::shared_ptr out = nullptr); + virtual ~Logger(); + + template + Logger& operator<<(const T& msg) { + if (!isEnabled()) { + return *this; + } + stream_ << msg; + return *this; + } + + template + Logger& log(const T& v, TArgs... args) { + if (!isEnabled()) { + return *this; + } + stream_ << v << " "; + log(args...); + return *this; + } + template + Logger& log(const T& v) { + if (!isEnabled()) { + return *this; + } + stream_ << v; + return *this; + } + Logger& log() { return *this; } + + template + Logger& print(const char* format, T value, Args... args) { + if (!isEnabled()) { + return *this; + } + + while (*format) { + if (*format == '%' && *(++format) != '%') { + stream_ << value; + + // handle sub-specifiers + if ((*format == 'z')) { + format++; + } else if ((*format == 'l') || (*format == 'h')) { + format++; + if (*format == *(format + 1)) { + format++; + } + } + format++; + + print(format, args...); + return *this; + } + stream_ << *format++; + } + assert(((void)"logical error: should not come here", false)); + return *this; + }; + + Logger& print(const char* string_without_format_specifiers = ""); + Logger& flush(); + bool isEnabled(const std::string& pattern = "") { + return LogOption::isEnabled(pattern); + } + + protected: + std::stringstream stream_; + void initialize(std::shared_ptr out = nullptr); + + private: + std::shared_ptr output_; +}; + +class StdOut : public Logger::Output { + public: + static std::shared_ptr instance() { + static std::shared_ptr output = std::make_shared(); + return output; + } + + void flush(std::stringstream& ss) override; +}; + +// --- Utils --- + +#ifndef __FILE_NAME__ +#define __FILE_NAME__ \ + (strrchr(__FILE__, '/') ? strrchr(__FILE__, '/') + 1 : __FILE__) +#endif + +#define __FUNCTION_NAME__ getPrettyFunctionName(__PRETTY_FUNCTION__) + +#define __CODE_LOCATION__ \ + createCodeLocation(__PRETTY_FUNCTION__, __FILE_NAME__, __LINE__).c_str() + +std::string createCodeLocation(const char* functionName, const char* filename, + const int line, std::string prefixPattern = ""); + +std::string getPrettyFunctionName(const std::string& fullname, + std::string prefixPattern = ""); + +void writeThreadIdentifier(std::ostream& ss); + +class IndentCounter { + public: + IndentCounter(std::string id); + ~IndentCounter(); + static std::string getString(std::string id = ""); + static void indent(std::string id); + static void unIndent(std::string id); + + private: + std::string id_; +}; diff --git a/packages/cloud_functions/tizen/dep/include/common/to_string.h b/packages/cloud_functions/tizen/dep/include/common/to_string.h new file mode 100644 index 0000000..5871289 --- /dev/null +++ b/packages/cloud_functions/tizen/dep/include/common/to_string.h @@ -0,0 +1,71 @@ +/* + * Copyright (c) 2023-present Samsung Electronics Co., Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +#pragma once + +#include +#include + +#include + +std::ostream& operator<<(std::ostream& os, const firebase::Variant& v); +std::ostream& operator<<(std::ostream& os, const flutter::EncodableValue& v); + +#ifdef FIREBASE_DATABASE +#include +#include + +std::ostream& operator<<(std::ostream& os, + const firebase::database::DataSnapshot& d); +std::ostream& operator<<(std::ostream& os, + const firebase::database::MutableData& d); +#endif // FIREBASE_DATABASE + +template +static std::ostream& operator<<(std::ostream& os, const std::vector& v) { + static size_t limit = std::numeric_limits::max(); + + size_t n = v.size(); + size_t end = std::min(n, limit); + os << "["; + for (size_t i = 0; i < end; i++) { + os << v[i]; + if (i < end - 1) { + os << ", "; + } + } + if (end < n) { + os << ", ..."; + } + os << "]"; + return os; +} + +template +static std::ostream& operator<<(std::ostream& os, const std::map& m) { + os << "{ "; + for (const auto& [key, value] : m) { + os << key << ": " << value << ", "; + } + os << "}"; + return os; +} + +template +std::string ToString(const T& value) { + std::stringstream ss; + ss << value; + return ss.str(); +} diff --git a/packages/cloud_functions/tizen/dep/include/common/trace.h b/packages/cloud_functions/tizen/dep/include/common/trace.h new file mode 100644 index 0000000..e8d7b9e --- /dev/null +++ b/packages/cloud_functions/tizen/dep/include/common/trace.h @@ -0,0 +1,102 @@ +/* + * Copyright (c) 2023-present Samsung Electronics Co., Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +#pragma once + +#include "logger.h" + +class Trace : public Logger { + public: + Trace(std::string id); + Trace(std::string id, const char* functionName, const char* filename, + const int line); + template + Trace(std::string id, const char* functionName, const char* filename, + const int line, const T& v, TArgs... args) + : Trace(id, functionName, filename, line) { + if (!isEnabled(id)) { + return; + } + stream_ << v << " "; + log(args...); + } + + class Option { + public: + static void setTag(const char* tagName) { tag_ = tagName; } + static const char* tag() { return tag_.c_str(); } + + private: + static std::string tag_; + }; +}; + +#if defined(NDEBUG) + +#define TRACE(id, ...) +#define TRACE0(id, ...) +#define TRACEF(id, ...) +#define TRACEF0(id, ...) +#define TRACE_SCOPE(id, ...) +#define TRACE_SCOPE0(id, ...) + +#else + +#define TRACE(id, ...) \ + Trace(#id, __PRETTY_FUNCTION__, __FILE_NAME__, __LINE__).log(__VA_ARGS__) + +#define TRACE0(id, ...) Trace(#id).log(__VA_ARGS__) + +#define TRACEF(id, ...) \ + Trace(#id, __PRETTY_FUNCTION__, __FILE_NAME__, __LINE__).print(__VA_ARGS__) + +#define TRACEF0(id, ...) Trace(#id).print(__VA_ARGS__) + +#define TRACE_SCOPE(id, ...) \ + IndentCounter __counter(#id); \ + TRACE(id, __VA_ARGS__) + +#define TRACE_SCOPE0(id, ...) \ + IndentCounter __counter(#id); \ + TRACE(id, __VA_ARGS__); \ + Trace __outter(#id, __PRETTY_FUNCTION__, __FILE_NAME__, __LINE__, \ + "/" __VA_ARGS__) + +#endif + +#ifdef __GNUC__ +#define LIKELY(condition) __builtin_expect(!!(condition), 1) +#define UNLIKELY(condition) __builtin_expect(!!(condition), 0) +#else +#define LIKELY(condition) (condition) +#define UNLIKELY(condition) (condition) +#endif + +void Trace_Fatal(const char* functionName, const char* filename, const int line, + const std::string message); + +#define FATAL(...) \ + Trace_Fatal(__PRETTY_FUNCTION__, __FILE_NAME__, __LINE__, __VA_ARGS__) +#define CHECK_FAILED_HANDLER(message) FATAL("Check failed: " message) +#define CHECK_WITH_MSG(condition, message) \ + do { \ + if (UNLIKELY(!(condition))) { \ + CHECK_FAILED_HANDLER(message); \ + } \ + } while (false) +#define CHECK(condition) CHECK_WITH_MSG(condition, #condition) +#define CHECK_NULL(val) CHECK((val) == nullptr) +#define CHECK_NOT_NULL(val) CHECK((val) != nullptr) +#define UNIMPLEMENTED(...) CHECK_WITH_MSG(false, "[UNIMPLEMENTED] " __VA_ARGS__) diff --git a/packages/cloud_functions/tizen/dep/include/common/utils.h b/packages/cloud_functions/tizen/dep/include/common/utils.h new file mode 100644 index 0000000..647d5d2 --- /dev/null +++ b/packages/cloud_functions/tizen/dep/include/common/utils.h @@ -0,0 +1,72 @@ +/* + * Copyright (c) 2023-present Samsung Electronics Co., Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +#pragma once + +#include + +#include +#include +#include +#include + +#include "trace.h" // UNLIKELY, FATAL + +template +class PointerScope { + public: + PointerScope() = delete; + PointerScope(void* p) { value_ = reinterpret_cast(p); } + virtual ~PointerScope() { delete value_; } + T* operator->() { return value_; } + + private: + T* value_; +}; + +template +class Optional : public std::optional { + public: + using std::optional::optional; + + template + T checked(F&& handler = {}) { + if (UNLIKELY(!this->has_value())) { + if constexpr (std::is_invocable_v) { + std::invoke(std::forward(handler)); + } else { + FATAL("Empty Optional"); + } + } + return this->value(); + } +}; + +// EncodableMap + +template +Optional GetOptionalValue(const flutter::EncodableMap* map, + const char* key) { + const auto& iter = map->find(flutter::EncodableValue(key)); + if (iter != map->end() && !iter->second.IsNull()) { + if (auto* value = std::get_if(&iter->second)) { + return *value; + } + } + return std::nullopt; +} + +flutter::EncodableValue GetEncodableValue(const flutter::EncodableMap* map, + const char* key); diff --git a/packages/cloud_functions/tizen/dep/logger.cc b/packages/cloud_functions/tizen/dep/logger.cc new file mode 100644 index 0000000..665317f --- /dev/null +++ b/packages/cloud_functions/tizen/dep/logger.cc @@ -0,0 +1,203 @@ +/* + * Copyright (c) 2023-present Samsung Electronics Co., Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +#include "include/common/logger.h" + +#include +#include +#include +#include + +// --- Formatter --- + +std::string getPrettyFunctionName(const std::string& fullname, + std::string prefixPattern) { + std::stringstream ss; + if (!prefixPattern.empty()) { + ss << "(?:" << prefixPattern << ")|"; + } + ss << R"((?::\()|([\w:~]+)\()"; + + try { + std::smatch match; + const std::regex re(ss.str()); + + std::stringstream result; + std::string suffix = fullname; + while (std::regex_search(suffix, match, re)) { + result << match[1]; + suffix = match.suffix(); + } + return result.str(); + } catch (std::regex_error& e) { + return ""; + } +} + +std::string createCodeLocation(const char* functionName, const char* filename, + const int line, std::string prefixPattern) { + std::ostringstream oss; + oss << getPrettyFunctionName(functionName, prefixPattern) << " (" << filename + << ":" << line << ")"; + return oss.str(); +} + +void writeThreadIdentifier(std::ostream& os) { + static int id = 0; + static thread_local int thisThreadId = 0; + + if (thisThreadId == 0) { + thisThreadId = ++id; + } + os << "[" << thisThreadId << "] "; +} + +std::function LogOption::externalIsEnabled; + +void LogOption::setExternalIsEnabled( + std::function func) { + externalIsEnabled = func; +} + +// --- LogOption --- + +bool LogOption::isEnabled(const std::string& pattern) { + if (externalIsEnabled == nullptr) { + return false; + } + return externalIsEnabled(pattern); +} + +// --- Logger::Header --- + +void Logger::Header::write(std::stringstream& stream) { + writeThreadIdentifier(stream); + writeHeader(stream); +} + +// --- Logger --- + +Logger::Logger(std::shared_ptr out) { initialize(out); } + +Logger::Logger(const std::string& header, std::shared_ptr out) + : output_(out) { + initialize(output_); + stream_ << header; +} + +Logger::Logger(Header&& header, std::shared_ptr out) : output_(out) { + initialize(output_); + header.write(stream_); +} + +Logger::~Logger() { + if (!isEnabled() || output_ == nullptr) { + return; + } + // stream ends with both reset-styles and endl characters. + stream_ << "\033[0m" << std::endl; + output_->flush(stream_); +} + +void Logger::initialize(std::shared_ptr out) { + static thread_local std::shared_ptr loggerOutput; + + if (out == nullptr) { + if (loggerOutput == nullptr) { + loggerOutput = std::make_shared(); + } + output_ = loggerOutput; + } else { + output_ = out; + } +} + +Logger& Logger::print(const char* string_without_format_specifiers) { + if (output_ == nullptr) { + return *this; + } + + while (*string_without_format_specifiers) { + if (*string_without_format_specifiers == '%' && + *(++string_without_format_specifiers) != '%') { + assert(((void)"runtime error: invalid format-string", false)); + } + stream_ << *string_without_format_specifiers++; + } + return *this; +} + +Logger& Logger::flush() { + if (isEnabled() && output_) { + output_->flush(stream_); + } + stream_.str(""); + return *this; +} +// --- Output --- + +void StdOut::flush(std::stringstream& stream) { std::cout << stream.str(); } + +// --- Utils --- + +thread_local int indentCount = 0; +thread_local int deltaCount = 0; + +void IndentCounter::indent(std::string id) { + if (!LogOption::isEnabled(id)) { + return; + } + deltaCount++; +} + +void IndentCounter::unIndent(std::string id) { + if (!LogOption::isEnabled(id)) { + return; + } + deltaCount--; +} + +IndentCounter::IndentCounter(std::string id) { + id_ = id; + + if (!LogOption::isEnabled(id)) { + return; + } + indentCount++; +} + +IndentCounter::~IndentCounter() { + if (!LogOption::isEnabled(id_)) { + return; + } + indentCount--; +} + +std::string IndentCounter::getString(std::string id) { + assert(indentCount >= 0); + + std::ostringstream oss; + int count = indentCount + deltaCount; + + if (deltaCount > 0) { + oss << deltaCount << " "; + } + + for (int i = 1; i < std::min(30, count); ++i) { + oss << " "; + } + + return oss.str(); +} diff --git a/packages/cloud_functions/tizen/dep/to_string.cc b/packages/cloud_functions/tizen/dep/to_string.cc new file mode 100644 index 0000000..7591227 --- /dev/null +++ b/packages/cloud_functions/tizen/dep/to_string.cc @@ -0,0 +1,159 @@ +/* + * Copyright (c) 2023-present Samsung Electronics Co., Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "include/common/to_string.h" + +using firebase::Variant; +using flutter::EncodableList; +using flutter::EncodableMap; +using flutter::EncodableValue; + +std::ostream& operator<<(std::ostream& os, const Variant& v) { + switch (v.type()) { + case Variant::kTypeNull: + os << "null"; + break; + case Variant::kTypeInt64: + os << v.int64_value(); + break; + case Variant::kTypeDouble: + os << v.double_value(); + break; + case Variant::kTypeBool: + os << std::boolalpha << v.bool_value() << std::noboolalpha; + break; + case Variant::kTypeStaticString: + os << v.string_value(); + break; + case Variant::kTypeMutableString: + os << v.mutable_string(); + break; + case Variant::kTypeVector: + os << v.vector(); + break; + case Variant::kTypeMap: + os << v.map(); + break; + default: + os << ""; + break; + } + return os; +} + +std::ostream& operator<<(std::ostream& os, const EncodableValue& v) { + switch (v.index()) { + case 0: // std::monostate + os << "null"; + break; + case 1: // bool + os << std::boolalpha << std::get(v) << std::noboolalpha; + break; + case 2: // int32_t + os << std::get(v); + break; + case 3: // int64_t + os << std::get(v); + break; + case 4: // double + os << std::get(v); + break; + case 5: // std::string + os << "\"" << std::get(v) << "\""; + break; + case 6: // std::vector + os << std::get>(v); + break; + case 7: // std::vector + os << std::get>(v); + break; + case 8: // std::vector + os << std::get>(v); + break; + case 9: // std::vector + os << std::get>(v); + break; + case 10: { // EncodableList + os << std::get(v); + break; + } + case 11: { // EncodableMap + os << std::get(v); + break; + } + case 12: // CustomEncodableValue + os << ""; + break; + case 13: // std::vector + os << std::get>(v); + break; + default: + os << ""; + break; + } + return os; +} + +#ifdef FIREBASE_DATABASE + +using firebase::database::DataSnapshot; +using firebase::database::MutableData; + +std::ostream& operator<<(std::ostream& os, const DataSnapshot& snapshot) { + static thread_local std::string indent; + + os << indent << "{\n"; + indent += " "; + + os << indent << "key: " << snapshot.key_string() << ", \n" + << indent << "value: " << snapshot.value() << ", \n" + << indent << "priority: " << snapshot.priority() << ", \n" + << indent << "children_count: " << snapshot.children_count() << ", \n"; + + for (const auto& s : snapshot.children()) { + os << s; + } + + indent = indent.substr(0, indent.length() - 2); + os << indent << "},\n"; + return os; +} + +std::ostream& operator<<(std::ostream& os, const MutableData& data) { + static thread_local std::string indent; + + os << indent << "{\n"; + indent += " "; + + // Functions like children_count() or priority() is not marked as const. This + // seems to be a firebase mistake. So we convert it to a const reference. + MutableData& mutable_data = const_cast(data); + + os << indent << "key: " << mutable_data.key_string() << ", \n" + << indent << "value: " << mutable_data.value() << ", \n" + << indent << "priority: " << mutable_data.priority() << ", \n" + << indent << "children_count: " << mutable_data.children_count() << ", \n"; + + for (auto& d : mutable_data.children()) { + os << d; + } + + indent = indent.substr(0, indent.length() - 2); + os << indent << "},\n"; + return os; +} + +#endif // FIREBASE_DATABASE diff --git a/packages/cloud_functions/tizen/dep/trace.cc b/packages/cloud_functions/tizen/dep/trace.cc new file mode 100644 index 0000000..a452bf7 --- /dev/null +++ b/packages/cloud_functions/tizen/dep/trace.cc @@ -0,0 +1,120 @@ +/* + * Copyright (c) 2023-present Samsung Electronics Co., Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +#include "common/trace.h" + +#include // assert +#include // setfill and setw + +#define TYPE_LENGTH_LIMIT 5 +#define TRACE_ID_LENGTH_LIMIT 10 +#define COLOR_RESET "\033[0m" +#define COLOR_DIM "\033[0;2m" +#define LOG_PREFIX_PATTERN ".*Plugin" + +std::string Trace::Option::tag_ = "FirebasePlugin"; + +#if defined(__TIZEN__) || defined(TIZEN) + +#include + +class PriorityLog { + public: + class Debug : public Logger::Output { + public: + void flush(std::stringstream& stream) override { + dlog_print(DLOG_DEBUG, Trace::Option::tag(), "%s", stream.str().c_str()); + } + }; + + class Warn : public Logger::Output { + public: + void flush(std::stringstream& stream) override { + dlog_print(DLOG_WARN, Trace::Option::tag(), "%s", stream.str().c_str()); + } + }; + + class Error : public Logger::Output { + public: + void flush(std::stringstream& stream) override { + dlog_print(DLOG_ERROR, Trace::Option::tag(), "%s", stream.str().c_str()); + } + }; +}; + +class CustomOutput : public PriorityLog::Debug { + public: + static std::shared_ptr instance() { + static std::shared_ptr output = + std::make_shared(); + return output; + } +}; + +// -- Fatal -- +static std::string trim(std::string const& str, + std::string const& whitespace = " \r\n\t\v\f") { + if (str.length() == 0) return ""; + std::size_t start = str.find_first_not_of(whitespace); + std::size_t end = str.find_last_not_of(whitespace); + return str.substr(start, end - start + 1); +} + +void Trace_Fatal(const char* functionName, const char* filename, const int line, + const std::string message) { + std::stringstream stream; + if (message.length() > 0) stream << trim(message) << " "; + stream << createCodeLocation(functionName, filename, line); + PriorityLog::Error().flush(stream); + assert(false); +} + +#else + +class CustomOutput : public StdOut::Output { + public: + static std::shared_ptr instance() { return StdOut::instance(); } +}; +#endif + +static void writeHeader(std::ostream& ss, const std::string& tag, + const std::string& id) { + ss << COLOR_DIM; + ss << std::left << std::setfill(' ') << "(" + << std::setw(TRACE_ID_LENGTH_LIMIT) + << std::string(id).substr(0, TRACE_ID_LENGTH_LIMIT) << ") "; +} + +Trace::Trace(std::string id, const char* functionName, const char* filename, + const int line) + : Logger(CustomOutput::instance()) { + if (!isEnabled(id)) { + return; + } + + writeHeader(stream_, Option::tag(), id); + stream_ << IndentCounter::getString(id) + << createCodeLocation(functionName, filename, line, + LOG_PREFIX_PATTERN) + << " " << COLOR_RESET; +} + +Trace::Trace(std::string id) : Logger(CustomOutput::instance()) { + if (!isEnabled(id)) { + return; + } + + writeHeader(stream_, Option::tag(), id); +} diff --git a/packages/cloud_functions/tizen/dep/utils.cc b/packages/cloud_functions/tizen/dep/utils.cc new file mode 100644 index 0000000..280358c --- /dev/null +++ b/packages/cloud_functions/tizen/dep/utils.cc @@ -0,0 +1,29 @@ +/* + * Copyright (c) 2023-present Samsung Electronics Co., Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "common/utils.h" + +using flutter::EncodableMap; +using flutter::EncodableValue; + +EncodableValue GetEncodableValue(const EncodableMap* map, const char* key) { + const auto& iter = map->find(EncodableValue(key)); + if (iter != map->end()) { + return iter->second; + } + // EncodableMap has no key. + return EncodableValue(); +} diff --git a/packages/cloud_functions/tizen/inc/cloud_functions_plugin.h b/packages/cloud_functions/tizen/inc/cloud_functions_plugin.h new file mode 100644 index 0000000..8f4c091 --- /dev/null +++ b/packages/cloud_functions/tizen/inc/cloud_functions_plugin.h @@ -0,0 +1,23 @@ +#ifndef FLUTTER_PLUGIN_CLOUD_FUNCTIONS_PLUGIN_H_ +#define FLUTTER_PLUGIN_CLOUD_FUNCTIONS_PLUGIN_H_ + +#include + +#ifdef FLUTTER_PLUGIN_IMPL +#define FLUTTER_PLUGIN_EXPORT __attribute__((visibility("default"))) +#else +#define FLUTTER_PLUGIN_EXPORT +#endif + +#if defined(__cplusplus) +extern "C" { +#endif + +FLUTTER_PLUGIN_EXPORT void CloudFunctionsPluginRegisterWithRegistrar( + FlutterDesktopPluginRegistrarRef registrar); + +#if defined(__cplusplus) +} // extern "C" +#endif + +#endif // FLUTTER_PLUGIN_CLOUD_FUNCTIONS_PLUGIN_H_ diff --git a/packages/cloud_functions/tizen/project_def.prop b/packages/cloud_functions/tizen/project_def.prop new file mode 100644 index 0000000..5142c71 --- /dev/null +++ b/packages/cloud_functions/tizen/project_def.prop @@ -0,0 +1,29 @@ +# See https://docs.tizen.org/application/tizen-studio/native-tools/project-conversion +# for details. + +APPNAME = firebase_functions_plugin +type = sharedLib +profile = common-7.0 + +# Source files +USER_SRCS += src/*.cc dep/*.cc + +# User defines +USER_DEFS = +USER_UNDEFS = +USER_CPP_DEFS = FLUTTER_PLUGIN_IMPL TIZEN __TIZEN__ +USER_CPP_UNDEFS = + +FIREBASE_SDK_DIR = $(subst $() ,\ ,$(FLUTTER_BUILD_DIR))/.firebaseSDK +FIREBASE_INC_DIR = $(FIREBASE_SDK_DIR)/inc +FIREBASE_LIB_DIR = $(FIREBASE_SDK_DIR)/lib/$(BUILD_ARCH) + +# User includes +USER_INC_DIRS = inc src dep/include $(FIREBASE_INC_DIR) +USER_INC_FILES = +USER_CPP_INC_FILES = + +# Linker options +USER_LIBS = firebase_app firebase_functions +USER_LIB_DIRS = lib/$(BUILD_ARCH) $(FIREBASE_LIB_DIR) +USER_LFLAGS = -Wl,-rpath='$$ORIGIN' diff --git a/packages/cloud_functions/tizen/src/cloud_functions_plugin.cc b/packages/cloud_functions/tizen/src/cloud_functions_plugin.cc new file mode 100644 index 0000000..ab83f31 --- /dev/null +++ b/packages/cloud_functions/tizen/src/cloud_functions_plugin.cc @@ -0,0 +1,269 @@ +/* + * Copyright (c) 2023-present Samsung Electronics Co., Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +#include "cloud_functions_plugin.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +#include "common/conversion.h" +#include "common/to_string.h" +#include "common/trace.h" +#include "common/utils.h" +#include "queue.hpp" + +using flutter::EncodableMap; +using flutter::EncodableValue; +using flutter::MethodChannel; +using flutter::MethodResult; +using EncodableValuePair = std::pair; + +using firebase::App; +using firebase::Future; +using firebase::FutureStatus; +using firebase::Variant; +using firebase::functions::Error; +using firebase::functions::Functions; +using firebase::functions::HttpsCallableReference; +using firebase::functions::HttpsCallableResult; + +namespace { + +constexpr const char* kMethodChannelName = + "plugins.flutter.io/firebase_functions"; + +enum CloudFunctionsErrorCode { + kOK = 0, + kCancelled = 1, + kUnknown = 2, + kInvalidArgument = 3, + kDeadlineExceeded = 4, + kNotFound = 5, + kAlreadyExists = 6, + kPermissionDenied = 7, + kResourceExhausted = 8, + kFailedPrecondition = 9, + kAborted = 10, + kOutOfRange = 11, + kUnimplemented = 12, + kInternal = 13, + kUnavailable = 14, + kDataLoss = 15, + kUnauthenticated = 16, +}; + +static const char* ToErrorCodeString(const int code) { + switch (code) { + case CloudFunctionsErrorCode::kOK: + return "ok"; + case CloudFunctionsErrorCode::kCancelled: + return "cancelled"; + case CloudFunctionsErrorCode::kUnknown: + return "unknown"; + case CloudFunctionsErrorCode::kInvalidArgument: + return "invalid-argument"; + case CloudFunctionsErrorCode::kDeadlineExceeded: + return "deadline-exceeded"; + case CloudFunctionsErrorCode::kNotFound: + return "not-found"; + case CloudFunctionsErrorCode::kAlreadyExists: + return "already-exists"; + case CloudFunctionsErrorCode::kPermissionDenied: + return "permission-denied"; + case CloudFunctionsErrorCode::kResourceExhausted: + return "resource-exhausted"; + case CloudFunctionsErrorCode::kFailedPrecondition: + return "failed-precondition"; + case CloudFunctionsErrorCode::kAborted: + return "aborted"; + case CloudFunctionsErrorCode::kOutOfRange: + return "out-of-range"; + case CloudFunctionsErrorCode::kUnimplemented: + return "unimplemented"; + case CloudFunctionsErrorCode::kInternal: + return "internal"; + case CloudFunctionsErrorCode::kUnavailable: + return "unavailable"; + case CloudFunctionsErrorCode::kDataLoss: + return "data-loss"; + case CloudFunctionsErrorCode::kUnauthenticated: + return "unauthenticated"; + default: + return "unknown-code"; + } +} + +class CloudFunctionsPlugin : public flutter::Plugin { + public: + static void RegisterWithRegistrar(flutter::PluginRegistrar* registrar) { + auto channel = std::make_unique>( + registrar->messenger(), kMethodChannelName, + &flutter::StandardMethodCodec::GetInstance()); + auto plugin = std::make_unique(); + + channel->SetMethodCallHandler( + [plugin_pointer = plugin.get()](const auto& call, auto result) { + plugin_pointer->HandleMethodCall(call, std::move(result)); + }); + + registrar->AddPlugin(std::move(plugin)); + } + + struct AsyncTask { + std::function handler{nullptr}; + }; + + private: + void HandleMethodCall(const flutter::MethodCall& method_call, + std::unique_ptr> result) { + TRACE_SCOPE0(FUNCTION); + + // 0. Handle remaining AsyncTasks. + + while (async_task_queue_.empty() == false) { + AsyncTask task; + async_task_queue_.pop(task, 0); + if (task.handler) { + task.handler(); + } + } + + // 1. Parse method channel arguments. + + const std::string& method_name = method_call.method_name(); + const auto* arguments = std::get_if(method_call.arguments()); + if (!arguments) { + return result->Error("invalid-argument", "Invalid argument type."); + } + + TRACEF(FUNCTION, "[TIZEN: HANDLE_METHOD_CALL] %s {\n%s\n}", method_name, + ToString(*arguments)); + + CHECK(method_name == "FirebaseFunctions#call"); + + const auto app_name = GetOptionalValue(arguments, "appName") + .value_or("[DEFAULT]"); + const auto region = + GetOptionalValue(arguments, "region").value(); + + App* app = App::GetInstance(app_name.c_str()); + if (app == nullptr) { + return result->Error("unavailable", "No app matched found."); + } + Functions* functions = Functions::GetInstance(app, region.c_str()); + if (functions == nullptr) { + return result->Error("invalid-argument", + "Can't create functions with the given app."); + } + const auto origin = GetOptionalValue(arguments, "origin"); + const auto timeout = GetOptionalValue(arguments, "timeout"); + + if (origin) { + functions->UseFunctionsEmulator(origin.value().c_str()); + } + + if (timeout) { + TRACE(FUNCTION, "[!] timeout isn't supported."); + } + + // 2. Call the function requested. + + struct Param { + Param(std::unique_ptr> _result, + std::function _post_task) + : result(std::move(_result)), post_task(std::move(_post_task)) {} + std::unique_ptr> result; + std::function post_task; + }; + + TRACE(FUNCTION, "reference.Call"); + + auto function_name = + GetOptionalValue(arguments, "functionName"); + + CHECK(function_name); + + std::shared_ptr reference( + new HttpsCallableReference( + functions->GetHttpsCallable(function_name.value().c_str()))); + + EncodableValue parameters = GetEncodableValue(arguments, "parameters"); + + reference->Call(Conversion::ToFirebaseVariant(parameters)) + .OnCompletion( + [](const Future& future, void* data) { + PointerScope param(data); + if (future.status() == FutureStatus::kFutureStatusComplete) { + TRACE(FUNCTION, "FutureStatus::kFutureStatusComplete"); + if (future.error() == Error::kErrorNone) { + TRACE_SCOPE0(FUNCTION, "FutureStatus::Success"); + param->result->Success( + Conversion::ToEncodableValue(future.result()->data())); + } else { + TRACE_SCOPE0( + FUNCTION, "FutureStatus::Error", "code:", future.error(), + "code_string:", ToErrorCodeString(future.error()), + "error_message:", future.error_message()); + + EncodableMap m; + m.insert(EncodableValuePair( + "code", ToErrorCodeString(future.error()))); + m.insert( + EncodableValuePair("message", future.error_message())); + // FIXME: details below should be parsed. (Refs: + // https://github.com/firebase/flutterfire/blob/1e4783d86640509a9bae16c923ed5ccb6f917dd3/packages/cloud_functions/cloud_functions_platform_interface/lib/src/method_channel/utils/exception.dart#L37C55-L37C69) + auto details = EncodableMap{ + {EncodableValue("additionalData"), EncodableValue(m)}, + }; + TRACE(FUNCTION, "details", details); + + param->result->Error(std::to_string(future.error()), + future.error_message(), + EncodableValue(details)); + } + } + param->post_task(); + }, + new Param(std::move(result), [this, reference]() { + // This is for holding the reference: shared_ptr<>. + async_task_queue_.push({.handler = [reference]() {}}); + })); + + TRACE(FUNCTION, "/reference.Call"); + } + + Queue async_task_queue_; +}; + +} // namespace + +void CloudFunctionsPluginRegisterWithRegistrar( + FlutterDesktopPluginRegistrarRef registrar) { + CloudFunctionsPlugin::RegisterWithRegistrar( + flutter::PluginRegistrarManager::GetInstance() + ->GetRegistrar(registrar)); +} diff --git a/packages/cloud_functions/tizen/src/queue.hpp b/packages/cloud_functions/tizen/src/queue.hpp new file mode 100644 index 0000000..19d1c1c --- /dev/null +++ b/packages/cloud_functions/tizen/src/queue.hpp @@ -0,0 +1,60 @@ +/* + * Copyright (c) 2023-present Samsung Electronics Co., Ltd + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +#pragma once + +#include +#include +#include + +template +class Queue { + public: + Queue() = default; + Queue(const Queue&) = delete; + Queue(const Queue&&) = delete; + Queue& operator=(const Queue&) = delete; + Queue& operator=(const Queue&&) = delete; + + void push(const T& item) { + std::unique_lock lock(mutex_); + queue_.push(item); + lock.unlock(); + cv_.notify_one(); + } + + bool empty() { + std::unique_lock lock(mutex_); + return queue_.empty(); + } + + bool pop(T& item, const unsigned int millisecondWaitingPeriod) { + std::unique_lock lock(mutex_); + while (queue_.empty()) { + auto timeout = std::chrono::milliseconds(millisecondWaitingPeriod); + if (cv_.wait_for(lock, timeout) == std::cv_status::timeout) { + return false; + } + } + item = queue_.front(); + queue_.pop(); + return true; + } + + private: + std::queue queue_; + std::mutex mutex_; + std::condition_variable cv_; +}; diff --git a/packages/cloud_functions/tizen/tar_url.sh b/packages/cloud_functions/tizen/tar_url.sh new file mode 100755 index 0000000..8ab36df --- /dev/null +++ b/packages/cloud_functions/tizen/tar_url.sh @@ -0,0 +1,39 @@ +#!/bin/bash +set -e + +USAGE=$(cat << EOF +Usage: $(basename "$0") [DEST_DIR] [STRIP_COMPONENTS_NUMBER] + +Description: +A script to download a tar file from a given URL and extract it. It skips the extraction +processs if the contents of the file have already been extracted for the same URL. + +Arguments: + FILE_URL The URL address of the tar file to download. + DEST_DIR The directory path to the tar file for extraction. (default: \$HOME/.firebaseSDK) + STRIP_COMPONENTS_NUMBER + The number of leading components to strip during the tar extraction. (default: 1) + +Example: + $(basename "$0") https://example.com/file.tar.gz /path/to 0 +EOF +) + +if [ -z "$1" ]; then + echo "$USAGE" + exit 1 +fi + +FILE_URL=$1 +DEST_DIR=${2:-${FLUTTER_BUILD_DIR}/.firebaseSDK} +TAR_SNUM=${3:-1} + +RECORD_FILE="${DEST_DIR}/VERSION" + +if [ -e "$RECORD_FILE" ] && [ "$FILE_URL" = $(head -n 1 "$RECORD_FILE") ]; then + echo "$RECORD_FILE exists with the same URL." +else + [ ! -d "$DEST_DIR" ] && mkdir -v "$DEST_DIR" + curl -L $FILE_URL | tar -xz --strip-components=$TAR_SNUM -C "$DEST_DIR" + echo $FILE_URL > "$RECORD_FILE" +fi