diff --git a/packages/url_launcher/CHANGELOG.md b/packages/url_launcher/CHANGELOG.md new file mode 100644 index 000000000..0d8803f93 --- /dev/null +++ b/packages/url_launcher/CHANGELOG.md @@ -0,0 +1,3 @@ +## 1.0.0 + +* Initial release. diff --git a/packages/url_launcher/LICENSE b/packages/url_launcher/LICENSE new file mode 100644 index 000000000..4e5cfe14e --- /dev/null +++ b/packages/url_launcher/LICENSE @@ -0,0 +1,26 @@ +Copyright (c) 2020 Samsung Electronics Co., Ltd. All rights reserved. +Copyright (c) 2017 The Chromium 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. diff --git a/packages/url_launcher/README.md b/packages/url_launcher/README.md new file mode 100644 index 000000000..60c07cd2c --- /dev/null +++ b/packages/url_launcher/README.md @@ -0,0 +1,35 @@ +# url_launcher_tizen + +The Tizen implementation of [`url_launcher`](https://github.com/flutter/plugins/tree/master/packages/url_launcher). + +## Usage + +This package is not an _endorsed_ implementation of `url_launcher`. Therefore, you have to include `url_launcher_tizen` alongside `url_launcher` as dependencies in your `pubspec.yaml` file. + +```yaml +dependencies: + url_launcher: ^5.7.5 + url_launcher_tizen: ^1.0.0 +``` + +Then you can import `url_launcher` in your Dart code: + +```dart +import 'package:url_launcher/url_launcher.dart'; +``` + +For detailed usage, see https://github.com/flutter/plugins/tree/master/packages/url_launcher/url_launcher#usage. + +An `AppControlException` is raised if no application on the device can launch the requested URL. + +## Required privileges + +To use this plugin in a Tizen application, the application manager privilege is required. Add below lines under the `` section in your `tizen-manifest.xml` file. + +```xml + + http://tizen.org/privilege/appmanager.launch + +``` + +For details, see [Security and API Privileges](https://docs.tizen.org/application/dotnet/tutorials/sec-privileges). diff --git a/packages/url_launcher/example/.gitignore b/packages/url_launcher/example/.gitignore new file mode 100644 index 000000000..9d532b18a --- /dev/null +++ b/packages/url_launcher/example/.gitignore @@ -0,0 +1,41 @@ +# Miscellaneous +*.class +*.log +*.pyc +*.swp +.DS_Store +.atom/ +.buildlog/ +.history +.svn/ + +# IntelliJ related +*.iml +*.ipr +*.iws +.idea/ + +# The .vscode folder contains launch configuration and tasks you configure in +# VS Code which you may wish to be included in version control, so this line +# is commented out by default. +#.vscode/ + +# Flutter/Dart/Pub related +**/doc/api/ +**/ios/Flutter/.last_build_id +.dart_tool/ +.flutter-plugins +.flutter-plugins-dependencies +.packages +.pub-cache/ +.pub/ +/build/ + +# Web related +lib/generated_plugin_registrant.dart + +# Symbolication related +app.*.symbols + +# Obfuscation related +app.*.map.json diff --git a/packages/url_launcher/example/README.md b/packages/url_launcher/example/README.md new file mode 100644 index 000000000..a8c367898 --- /dev/null +++ b/packages/url_launcher/example/README.md @@ -0,0 +1,7 @@ +# url_launcher_tizen_example + +Demonstrates how to use the url_launcher_tizen plugin. + +## Getting Started + +To run this app on your Tizen device, use [flutter-tizen](https://github.com/flutter-tizen/flutter-tizen). diff --git a/packages/url_launcher/example/integration_test/url_launcher_test.dart b/packages/url_launcher/example/integration_test/url_launcher_test.dart new file mode 100644 index 000000000..785b58c6c --- /dev/null +++ b/packages/url_launcher/example/integration_test/url_launcher_test.dart @@ -0,0 +1,29 @@ +// Copyright 2019 the Chromium project authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:io' show Platform; + +import 'package:flutter/foundation.dart' show kIsWeb; +import 'package:flutter_test/flutter_test.dart'; +import 'package:integration_test/integration_test.dart'; +import 'package:url_launcher/url_launcher.dart'; + +void main() { + IntegrationTestWidgetsFlutterBinding.ensureInitialized(); + + test('canLaunch', () async { + expect(await canLaunch('randomstring'), false); + + // Generally all devices should have some default browser. + expect(await canLaunch('http://flutter.dev'), true); + + // SMS handling is available by default on most platforms. + if (kIsWeb || !(Platform.isLinux || Platform.isWindows)) { + expect(await canLaunch('sms:5555555555'), true); + } + + // tel: and mailto: links may not be openable on every device. iOS + // simulators notably can't open these link types. + }); +} diff --git a/packages/url_launcher/example/lib/main.dart b/packages/url_launcher/example/lib/main.dart new file mode 100644 index 000000000..f7d90c4be --- /dev/null +++ b/packages/url_launcher/example/lib/main.dart @@ -0,0 +1,206 @@ +// Copyright 2017 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +// ignore_for_file: public_member_api_docs + +import 'dart:async'; + +import 'package:flutter/material.dart'; +import 'package:url_launcher/url_launcher.dart'; + +void main() { + runApp(MyApp()); +} + +class MyApp extends StatelessWidget { + @override + Widget build(BuildContext context) { + return MaterialApp( + title: 'URL Launcher', + theme: ThemeData( + primarySwatch: Colors.blue, + ), + home: MyHomePage(title: 'URL Launcher'), + ); + } +} + +class MyHomePage extends StatefulWidget { + MyHomePage({Key key, this.title}) : super(key: key); + final String title; + + @override + _MyHomePageState createState() => _MyHomePageState(); +} + +class _MyHomePageState extends State { + Future _launched; + String _phone = ''; + + Future _launchInBrowser(String url) async { + if (await canLaunch(url)) { + await launch( + url, + forceSafariVC: false, + forceWebView: false, + headers: {'my_header_key': 'my_header_value'}, + ); + } else { + throw 'Could not launch $url'; + } + } + + Future _launchInWebViewOrVC(String url) async { + if (await canLaunch(url)) { + await launch( + url, + forceSafariVC: true, + forceWebView: true, + headers: {'my_header_key': 'my_header_value'}, + ); + } else { + throw 'Could not launch $url'; + } + } + + Future _launchInWebViewWithJavaScript(String url) async { + if (await canLaunch(url)) { + await launch( + url, + forceSafariVC: true, + forceWebView: true, + enableJavaScript: true, + ); + } else { + throw 'Could not launch $url'; + } + } + + Future _launchInWebViewWithDomStorage(String url) async { + if (await canLaunch(url)) { + await launch( + url, + forceSafariVC: true, + forceWebView: true, + enableDomStorage: true, + ); + } else { + throw 'Could not launch $url'; + } + } + + Future _launchUniversalLinkIos(String url) async { + if (await canLaunch(url)) { + final bool nativeAppLaunchSucceeded = await launch( + url, + forceSafariVC: false, + universalLinksOnly: true, + ); + if (!nativeAppLaunchSucceeded) { + await launch( + url, + forceSafariVC: true, + ); + } + } + } + + Widget _launchStatus(BuildContext context, AsyncSnapshot snapshot) { + if (snapshot.hasError) { + return Text('Error: ${snapshot.error}'); + } else { + return const Text(''); + } + } + + Future _makePhoneCall(String url) async { + if (await canLaunch(url)) { + await launch(url); + } else { + throw 'Could not launch $url'; + } + } + + @override + Widget build(BuildContext context) { + const String toLaunch = 'https://www.cylog.org/headers/'; + return Scaffold( + appBar: AppBar( + title: Text(widget.title), + ), + body: ListView( + children: [ + Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Padding( + padding: const EdgeInsets.all(16.0), + child: TextField( + onChanged: (String text) => _phone = text, + decoration: const InputDecoration( + hintText: 'Input the phone number to launch')), + ), + RaisedButton( + onPressed: () => setState(() { + _launched = _makePhoneCall('tel:$_phone'); + }), + child: const Text('Make phone call'), + ), + const Padding( + padding: EdgeInsets.all(16.0), + child: Text(toLaunch), + ), + RaisedButton( + onPressed: () => setState(() { + _launched = _launchInBrowser(toLaunch); + }), + child: const Text('Launch in browser'), + ), + const Padding(padding: EdgeInsets.all(16.0)), + RaisedButton( + onPressed: () => setState(() { + _launched = _launchInWebViewOrVC(toLaunch); + }), + child: const Text('Launch in app'), + ), + RaisedButton( + onPressed: () => setState(() { + _launched = _launchInWebViewWithJavaScript(toLaunch); + }), + child: const Text('Launch in app(JavaScript ON)'), + ), + RaisedButton( + onPressed: () => setState(() { + _launched = _launchInWebViewWithDomStorage(toLaunch); + }), + child: const Text('Launch in app(DOM storage ON)'), + ), + const Padding(padding: EdgeInsets.all(16.0)), + RaisedButton( + onPressed: () => setState(() { + _launched = _launchUniversalLinkIos(toLaunch); + }), + child: const Text( + 'Launch a universal link in a native app, fallback to Safari.(Youtube)'), + ), + const Padding(padding: EdgeInsets.all(16.0)), + RaisedButton( + onPressed: () => setState(() { + _launched = _launchInWebViewOrVC(toLaunch); + Timer(const Duration(seconds: 5), () { + print('Closing WebView after 5 seconds...'); + closeWebView(); + }); + }), + child: const Text('Launch in app + close after 5 seconds'), + ), + const Padding(padding: EdgeInsets.all(16.0)), + FutureBuilder(future: _launched, builder: _launchStatus), + ], + ), + ], + ), + ); + } +} diff --git a/packages/url_launcher/example/pubspec.yaml b/packages/url_launcher/example/pubspec.yaml new file mode 100644 index 000000000..3ef422f24 --- /dev/null +++ b/packages/url_launcher/example/pubspec.yaml @@ -0,0 +1,25 @@ +name: url_launcher_example +description: Demonstrates how to use the url_launcher_tizen plugin. + +dependencies: + flutter: + sdk: flutter + url_launcher: ^5.7.5 + url_launcher_tizen: + path: ../ + +dev_dependencies: + integration_test: ^0.9.2 + integration_test_tizen: + path: ../../integration_test/ + flutter_driver: + sdk: flutter + pedantic: ^1.8.0 + plugin_platform_interface: ^1.0.0 + +flutter: + uses-material-design: true + +environment: + sdk: ">=2.1.0 <3.0.0" + flutter: ">=1.12.13+hotfix.5 <2.0.0" diff --git a/packages/url_launcher/example/test_driver/integration_test.dart b/packages/url_launcher/example/test_driver/integration_test.dart new file mode 100644 index 000000000..b38629cca --- /dev/null +++ b/packages/url_launcher/example/test_driver/integration_test.dart @@ -0,0 +1,3 @@ +import 'package:integration_test/integration_test_driver.dart'; + +Future main() => integrationDriver(); diff --git a/packages/url_launcher/example/tizen/.gitignore b/packages/url_launcher/example/tizen/.gitignore new file mode 100644 index 000000000..750f3af1b --- /dev/null +++ b/packages/url_launcher/example/tizen/.gitignore @@ -0,0 +1,5 @@ +flutter/ +.vs/ +*.user +bin/ +obj/ diff --git a/packages/url_launcher/example/tizen/App.cs b/packages/url_launcher/example/tizen/App.cs new file mode 100644 index 000000000..6dd4a6356 --- /dev/null +++ b/packages/url_launcher/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/url_launcher/example/tizen/NuGet.Config b/packages/url_launcher/example/tizen/NuGet.Config new file mode 100644 index 000000000..c4ea70c17 --- /dev/null +++ b/packages/url_launcher/example/tizen/NuGet.Config @@ -0,0 +1,7 @@ + + + + + + + diff --git a/packages/url_launcher/example/tizen/Runner.csproj b/packages/url_launcher/example/tizen/Runner.csproj new file mode 100644 index 000000000..8ebc2abdf --- /dev/null +++ b/packages/url_launcher/example/tizen/Runner.csproj @@ -0,0 +1,26 @@ + + + + Exe + tizen60 + + + + portable + + + none + + + + + + + + + + %(RecursiveDir) + + + + diff --git a/packages/url_launcher/example/tizen/shared/res/ic_launcher.png b/packages/url_launcher/example/tizen/shared/res/ic_launcher.png new file mode 100644 index 000000000..4d6372eeb Binary files /dev/null and b/packages/url_launcher/example/tizen/shared/res/ic_launcher.png differ diff --git a/packages/url_launcher/example/tizen/tizen-manifest.xml b/packages/url_launcher/example/tizen/tizen-manifest.xml new file mode 100644 index 000000000..0579c9400 --- /dev/null +++ b/packages/url_launcher/example/tizen/tizen-manifest.xml @@ -0,0 +1,13 @@ + + + + + + ic_launcher.png + + + + + http://tizen.org/privilege/appmanager.launch + + diff --git a/packages/url_launcher/lib/app_control.dart b/packages/url_launcher/lib/app_control.dart new file mode 100644 index 000000000..9e3e0c0d0 --- /dev/null +++ b/packages/url_launcher/lib/app_control.dart @@ -0,0 +1,153 @@ +// Copyright 2020 Samsung Electronics Co., Ltd. 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:ffi'; +import 'package:ffi/ffi.dart'; + +// Native function signatures +typedef app_control_create = Int32 Function(Pointer>); +typedef app_control_set_operation = Int32 Function( + Pointer<_AppControl>, Pointer); +typedef app_control_set_uri = Int32 Function( + Pointer<_AppControl>, Pointer); +typedef app_control_send_launch_request = Int32 Function( + Pointer<_AppControl>, Pointer, Pointer); +typedef app_control_destroy = Int32 Function(Pointer<_AppControl>); + +// Constants from [app_control_result_e] in `app_control.h` +const int APP_CONTROL_RESULT_APP_STARTED = 1; +const int APP_CONTROL_RESULT_SUCCEEDED = 0; +const int APP_CONTROL_RESULT_FAILED = -1; +const int APP_CONTROL_RESULT_CANCELED = -2; + +// Constant from `app_control.h` +const String APP_CONTROL_OPERATION_VIEW = + 'http://tizen.org/appcontrol/operation/view'; + +// Constants from `tizen_error.h` +const int TIZEN_ERROR_NONE = 0; +const int TIZEN_ERROR_INVALID_PARAMETER = -22; +const int TIZEN_ERROR_OUT_OF_MEMORY = -12; +const int TIZEN_ERROR_APPLICATION = -0x01100000; +const int TIZEN_ERROR_KEY_NOT_AVAILABLE = -126; +const int TIZEN_ERROR_KEY_REJECTED = -129; +const int TIZEN_ERROR_PERMISSION_DENIED = -13; +const int TIZEN_ERROR_TIMED_OUT = -1073741824 + 1; +const int TIZEN_ERROR_IO_ERROR = -5; + +class _AppControl extends Struct {} + +/// A class representing non-zero errors from the platform. +class AppControlException { + const AppControlException(this.returnCode, this.message); + + final int returnCode; + final String message; + + // Constants from [app_control_error_e] in `app_control.h` + static const Map errorCodes = { + TIZEN_ERROR_NONE: 'APP_CONTROL_ERROR_NONE', + TIZEN_ERROR_INVALID_PARAMETER: 'APP_CONTROL_ERROR_INVALID_PARAMETER', + TIZEN_ERROR_OUT_OF_MEMORY: 'APP_CONTROL_ERROR_OUT_OF_MEMORY', + TIZEN_ERROR_APPLICATION | 0x21: 'APP_CONTROL_ERROR_APP_NOT_FOUND', + TIZEN_ERROR_KEY_NOT_AVAILABLE: 'APP_CONTROL_ERROR_KEY_NOT_FOUND', + TIZEN_ERROR_KEY_REJECTED: 'APP_CONTROL_ERROR_KEY_REJECTED', + TIZEN_ERROR_APPLICATION | 0x22: 'APP_CONTROL_ERROR_INVALID_DATA_TYPE', + TIZEN_ERROR_APPLICATION | 0x23: 'APP_CONTROL_ERROR_LAUNCH_REJECTED', + TIZEN_ERROR_PERMISSION_DENIED: 'APP_CONTROL_ERROR_PERMISSION_DENIED', + TIZEN_ERROR_APPLICATION | 0x24: 'APP_CONTROL_ERROR_LAUNCH_FAILED', + TIZEN_ERROR_TIMED_OUT: 'APP_CONTROL_ERROR_TIMED_OUT', + TIZEN_ERROR_IO_ERROR: 'APP_CONTROL_ERROR_IO_ERROR', + }; + + @override + String toString() => '$message (${errorCodes[returnCode] ?? returnCode})'; +} + +/// A wrapper class of the native App Control API. +/// Not all functions or values are supported. +class AppControl { + AppControl() { + final DynamicLibrary lib = + DynamicLibrary.open('libcapi-appfw-app-control.so.0'); + _create = lib + .lookup>('app_control_create') + .asFunction(); + _setOperation = lib + .lookup>( + 'app_control_set_operation') + .asFunction(); + _setUri = lib + .lookup>('app_control_set_uri') + .asFunction(); + _sendLaunchRequest = lib + .lookup>( + 'app_control_send_launch_request') + .asFunction(); + _destroy = lib + .lookup>('app_control_destroy') + .asFunction(); + } + + // Bindings + int Function(Pointer>) _create; + int Function(Pointer<_AppControl>, Pointer) _setOperation; + int Function(Pointer<_AppControl>, Pointer) _setUri; + int Function(Pointer<_AppControl>, Pointer, Pointer) + _sendLaunchRequest; + int Function(Pointer<_AppControl>) _destroy; + + Pointer<_AppControl> _handle; + + bool get isValid => _handle != null; + + void create() { + final Pointer> pHandle = allocate(); + final int ret = _create(pHandle); + _handle = pHandle.value; + free(pHandle); + + if (ret != 0) { + throw AppControlException(ret, 'Failed to execute app_control_create.'); + } + } + + void setOperation(String operation) { + assert(isValid); + + final int ret = _setOperation(_handle, Utf8.toUtf8(operation)); + if (ret != 0) { + throw AppControlException( + ret, 'Failed to execute app_control_set_operation.'); + } + } + + void setUri(String uri) { + assert(isValid); + + final int ret = _setUri(_handle, Utf8.toUtf8(uri)); + if (ret != 0) { + throw AppControlException(ret, 'Failed to execute app_control_set_uri.'); + } + } + + void sendLaunchRequest() { + assert(isValid); + + final int ret = _sendLaunchRequest(_handle, nullptr, nullptr); + if (ret != 0) { + throw AppControlException( + ret, 'Failed to execute app_control_send_launch_request.'); + } + } + + /// This method must be called after use to release the underlying handle. + /// `dart:ffi` doesn't support finalizers (the disposal pattern) as of now. + void destroy() { + if (isValid) { + _destroy(_handle); + _handle = null; + } + } +} diff --git a/packages/url_launcher/lib/url_launcher_tizen.dart b/packages/url_launcher/lib/url_launcher_tizen.dart new file mode 100644 index 000000000..0eb5d2760 --- /dev/null +++ b/packages/url_launcher/lib/url_launcher_tizen.dart @@ -0,0 +1,55 @@ +// Copyright 2020 Samsung Electronics Co., Ltd. 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:async'; + +import 'package:meta/meta.dart'; +import 'package:url_launcher_platform_interface/url_launcher_platform_interface.dart'; + +import 'app_control.dart'; + +class UrlLauncherPlugin extends UrlLauncherPlatform { + static final _supportedSchemes = { + 'file', + 'http', + 'https', + }; + + /// Registers this class as the default instance of [UrlLauncherPlatform]. + static void register() { + UrlLauncherPlatform.instance = UrlLauncherPlugin(); + } + + String _getUrlScheme(String url) => Uri.tryParse(url)?.scheme; + + @override + Future canLaunch(String url) async { + return _supportedSchemes.contains(_getUrlScheme(url)); + } + + @override + Future launch( + String url, { + // None of these options are used in Tizen. + @required bool useSafariVC, + @required bool useWebView, + @required bool enableJavaScript, + @required bool enableDomStorage, + @required bool universalLinksOnly, + @required Map headers, + String webOnlyWindowName, + }) async { + AppControl appControl; + try { + appControl = AppControl() + ..create() + ..setOperation(APP_CONTROL_OPERATION_VIEW) + ..setUri(url) + ..sendLaunchRequest(); + return true; + } finally { + appControl?.destroy(); + } + } +} diff --git a/packages/url_launcher/pubspec.yaml b/packages/url_launcher/pubspec.yaml new file mode 100644 index 000000000..8e795a900 --- /dev/null +++ b/packages/url_launcher/pubspec.yaml @@ -0,0 +1,31 @@ +name: url_launcher_tizen +description: Tizen implementation of the url_launcher plugin +homepage: https://github.com/flutter-tizen/plugins +version: 1.0.0 + +flutter: + plugin: + platforms: + tizen: + dartPluginClass: UrlLauncherPlugin + fileName: url_launcher_tizen.dart + +dependencies: + flutter: + sdk: flutter + url_launcher_platform_interface: ^1.0.8 + ffi: ^0.1.3 + meta: ^1.1.7 + +dev_dependencies: + test: ^1.3.0 + flutter_test: + sdk: flutter + mockito: ^4.1.1 + plugin_platform_interface: ^1.0.0 + pedantic: ^1.8.0 + url_launcher: ^5.7.5 + +environment: + sdk: ">=2.1.0 <3.0.0" + flutter: ">=1.12.13+hotfix.5 <2.0.0"