diff --git a/.github/labeler.yml b/.github/labeler.yml index 44c32d452..234f8a80b 100644 --- a/.github/labeler.yml +++ b/.github/labeler.yml @@ -47,6 +47,8 @@ - packages/shared_preferences/**/* 'p: share_plus': - packages/share_plus/**/* +'p: tizen_app_control': + - packages/tizen_app_control/**/* 'p: url_launcher': - packages/url_launcher/**/* 'p: video_player': diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 05d75e836..61630cd94 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -57,6 +57,12 @@ jobs: NativeToolchain-Gcc-9.2 \ WEARABLE-4.0-NativeAppDevelopment \ WEARABLE-5.5-NativeAppDevelopment + - name: Create a Tizen certificate profile + if: ${{ env.HAS_CHANGED_PACKAGES == 'true' }} + run: | + export PATH=$PATH:$HOME/tizen-studio/tools/ide/bin + tizen certificate -a platform -p platform -f platform + tizen security-profiles add -n platform -a $HOME/tizen-studio-data/keystore/author/platform.p12 -p platform - name: Install flutter-tizen if: ${{ env.HAS_CHANGED_PACKAGES == 'true' }} run: | diff --git a/README.md b/README.md index e76002da3..ee8e43b20 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # plugins -[![Build](https://github.com/flutter-tizen/plugins/actions/workflows/build.yml/badge.svg)](https://github.com/flutter-tizen/plugins/actions) +[![Build](https://github.com/flutter-tizen/plugins/actions/workflows/build.yml/badge.svg)](https://github.com/flutter-tizen/plugins/actions/workflows/build.yml) This repo contains Flutter plugins maintained by the flutter-tizen team. We're in process of adding Tizen platform support to existing first and third-party plugins on [pub.dev](https://pub.dev) based on their popularity. If the plugin you're looking for isn't implemented for Tizen yet, consider filing an [issue](../../issues) or creating a package by yourself. (We welcome your pull requests!) @@ -31,6 +31,7 @@ The _"non-endorsed"_ status means that the plugin is not endorsed by the origina | [**sensors_plus_tizen**](packages/sensors_plus) | [sensors_plus](https://github.com/fluttercommunity/plus_plugins/tree/main/packages/sensors_plus) (1st-party) | [![pub package](https://img.shields.io/pub/v/sensors_plus_tizen.svg)](https://pub.dev/packages/sensors_plus_tizen) | No | | [**share_plus_tizen**](packages/share_plus) | [share_plus](https://github.com/fluttercommunity/plus_plugins/tree/main/packages/share_plus) (1st-party) | [![pub package](https://img.shields.io/pub/v/share_plus_tizen.svg)](https://pub.dev/packages/share_plus_tizen) | No | | [**shared_preferences_tizen**](packages/shared_preferences) | [shared_preferences](https://github.com/flutter/plugins/tree/master/packages/shared_preferences) (1st-party) | [![pub package](https://img.shields.io/pub/v/shared_preferences_tizen.svg)](https://pub.dev/packages/shared_preferences_tizen) | No | +| [**tizen_app_control**](packages/tizen_app_control) | (Tizen-only) | [![pub package](https://img.shields.io/pub/v/tizen_app_control.svg)](https://pub.dev/packages/tizen_app_control) | N/A | | [**url_launcher_tizen**](packages/url_launcher) | [url_launcher](https://github.com/flutter/plugins/tree/master/packages/url_launcher) (1st-party) | [![pub package](https://img.shields.io/pub/v/url_launcher_tizen.svg)](https://pub.dev/packages/url_launcher_tizen) | No | | [**video_player_tizen**](packages/video_player) | [video_player](https://github.com/flutter/plugins/tree/master/packages/video_player) (1st-party) | [![pub package](https://img.shields.io/pub/v/video_player_tizen.svg)](https://pub.dev/packages/video_player_tizen) | No | | [**wakelock_tizen**](packages/wakelock) | [wakelock](https://github.com/creativecreatorormaybenot/wakelock) (3rd-party) | [![pub package](https://img.shields.io/pub/v/wakelock_tizen.svg)](https://pub.dev/packages/wakelock_tizen) | No | @@ -72,6 +73,7 @@ The following packages are deprecated. | [**sensors_plus_tizen**](packages/sensors_plus) | ✔️ | ✔️ | ❌ | ❌ | No sensor hardware | | [**share_plus_tizen**](packages/share_plus) | ⚠️ | ⚠️ | ❌ | ❌ | No SMS or e-mail app | | [**shared_preferences_tizen**](packages/shared_preferences) | ✔️ | ✔️ | ✔️ | ✔️ | +| [**tizen_app_control**](packages/tizen_app_control) | ✔️ | ✔️ | ✔️ | ✔️ | | [**url_launcher_tizen**](packages/url_launcher) | ✔️ | ❌ | ✔️ | ❌ | No browser app | | [**video_player_tizen**](packages/video_player) | ✔️ | ✔️ | ✔️ | ❌ | TV emulator issue | | [**wakelock_tizen**](packages/wakelock) | ✔️ | ✔️ | ❌ | ❌ | Cannot override system display setting | diff --git a/packages/tizen_app_control/.gitignore b/packages/tizen_app_control/.gitignore new file mode 100644 index 000000000..a247422ef --- /dev/null +++ b/packages/tizen_app_control/.gitignore @@ -0,0 +1,75 @@ +# 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/ +.dart_tool/ +.flutter-plugins +.flutter-plugins-dependencies +.packages +.pub-cache/ +.pub/ +build/ + +# Android related +**/android/**/gradle-wrapper.jar +**/android/.gradle +**/android/captures/ +**/android/gradlew +**/android/gradlew.bat +**/android/local.properties +**/android/**/GeneratedPluginRegistrant.java + +# iOS/XCode related +**/ios/**/*.mode1v3 +**/ios/**/*.mode2v3 +**/ios/**/*.moved-aside +**/ios/**/*.pbxuser +**/ios/**/*.perspectivev3 +**/ios/**/*sync/ +**/ios/**/.sconsign.dblite +**/ios/**/.tags* +**/ios/**/.vagrant/ +**/ios/**/DerivedData/ +**/ios/**/Icon? +**/ios/**/Pods/ +**/ios/**/.symlinks/ +**/ios/**/profile +**/ios/**/xcuserdata +**/ios/.generated/ +**/ios/Flutter/App.framework +**/ios/Flutter/Flutter.framework +**/ios/Flutter/Flutter.podspec +**/ios/Flutter/Generated.xcconfig +**/ios/Flutter/ephemeral +**/ios/Flutter/app.flx +**/ios/Flutter/app.zip +**/ios/Flutter/flutter_assets/ +**/ios/Flutter/flutter_export_environment.sh +**/ios/ServiceDefinitions.json +**/ios/Runner/GeneratedPluginRegistrant.* + +# Exceptions to above rules. +!**/ios/**/default.mode1v3 +!**/ios/**/default.mode2v3 +!**/ios/**/default.pbxuser +!**/ios/**/default.perspectivev3 diff --git a/packages/tizen_app_control/CHANGELOG.md b/packages/tizen_app_control/CHANGELOG.md new file mode 100644 index 000000000..607323422 --- /dev/null +++ b/packages/tizen_app_control/CHANGELOG.md @@ -0,0 +1,3 @@ +## 0.1.0 + +* Initial release. diff --git a/packages/tizen_app_control/LICENSE b/packages/tizen_app_control/LICENSE new file mode 100644 index 000000000..b8fc05698 --- /dev/null +++ b/packages/tizen_app_control/LICENSE @@ -0,0 +1,25 @@ +Copyright (c) 2021 Samsung Electronics Co., Ltd. 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 name of the copyright holder 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/tizen_app_control/README.md b/packages/tizen_app_control/README.md new file mode 100644 index 000000000..a335df0f2 --- /dev/null +++ b/packages/tizen_app_control/README.md @@ -0,0 +1,69 @@ +# tizen_app_control + +Tizen application control APIs. Used for launching and terminating applications on a Tizen device. + +## Usage + +To use this package, add `tizen_app_control` as a dependency in your `pubspec.yaml` file. + +```yaml +dependencies: + tizen_app_control: ^0.1.0 +``` + +### Sending a launch request + +To send an explicit launch request, create an `AppControl` instance with an application ID as an argument. + +```dart +import 'package:tizen_app_control/app_control.dart'; + +var request = AppControl(appId: 'com.example.app_id'); +await request.sendLaunchRequest(); +``` + +To send an implicit launch request, create an `AppControl` instance and specify necessary conditions, such as operation, URI, and MIME type. For example, if you want to open an image file with an image viewer app on your device, + +```dart +import 'package:tizen_app_control/app_control.dart'; + +var request = AppControl( + operation: 'http://tizen.org/appcontrol/operation/view', + uri: 'file:///image_file_path', + mime: 'image/*', +); +await request.sendLaunchRequest(); +``` + +For detailed information on Tizen application controls, see [Tizen Docs: Application Controls](https://docs.tizen.org/application/native/guides/app-management/app-controls). For a list of common operation types and examples, see [Tizen Docs: Common Application Controls](https://docs.tizen.org/application/native/guides/app-management/common-appcontrols). Operation and data constants, such as `http://tizen.org/appcontrol/operation/view`, are defined in [the native API references](https://docs.tizen.org/application/native/api/wearable/latest/group__CAPI__APP__CONTROL__MODULE.html). + +### Receiving a launch request + +You can subscribe to incoming application controls using `AppControl.onAppControl`. + +```dart +import 'package:tizen_app_control/app_control.dart'; + +var subscription = AppControl.onAppControl.listen((request) async { + if (request.shouldReply) { + var reply = AppControl(); + await request.reply(reply, AppControlReplyResult.succeeded); + } +}); +... +await subscription.cancel(); +``` + +## Required privileges + +Privileges may be required to perform operations requested by your app. Add required privileges in `tizen-manifest.xml` of your application. + +```xml + + http://tizen.org/privilege/appmanager.launch + + http://tizen.org/privilege/appmanager.kill.bgapp + http://tizen.org/privilege/call + http://tizen.org/privilege/download + +``` diff --git a/packages/tizen_app_control/example/.gitignore b/packages/tizen_app_control/example/.gitignore new file mode 100644 index 000000000..0fa6b675c --- /dev/null +++ b/packages/tizen_app_control/example/.gitignore @@ -0,0 +1,46 @@ +# 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 + +# Android Studio will place build artifacts here +/android/app/debug +/android/app/profile +/android/app/release diff --git a/packages/tizen_app_control/example/README.md b/packages/tizen_app_control/example/README.md new file mode 100644 index 000000000..755f6a43d --- /dev/null +++ b/packages/tizen_app_control/example/README.md @@ -0,0 +1,7 @@ +# tizen_app_control_example + +Demonstrates how to use the tizen_app_control 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/tizen_app_control/example/integration_test/tizen_app_control_test.dart b/packages/tizen_app_control/example/integration_test/tizen_app_control_test.dart new file mode 100644 index 000000000..23592aee3 --- /dev/null +++ b/packages/tizen_app_control/example/integration_test/tizen_app_control_test.dart @@ -0,0 +1,131 @@ +// Copyright 2021 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:flutter/services.dart'; +import 'package:flutter/widgets.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:integration_test/integration_test.dart'; +import 'package:tizen_app_control/app_control.dart'; +import 'package:tizen_app_control/app_manager.dart'; + +const String kAppId = 'org.tizen.tizen_app_control_example'; +const String kServiceAppId = 'org.tizen.tizen_app_control_example_service'; +const Timeout kTimeout = Timeout(Duration(seconds: 10)); + +@pragma('vm:entry-point') +void main() { + IntegrationTestWidgetsFlutterBinding.ensureInitialized(); + + testWidgets('Can receive request from platform', (WidgetTester _) async { + // The very first message is a launch request from the platform. + final ReceivedAppControl received = await AppControl.onAppControl.first; + expect(received.appId, kAppId); + expect(received.operation, 'http://tizen.org/appcontrol/operation/default'); + }, timeout: kTimeout); + + testWidgets('Can send and receive request', (WidgetTester _) async { + // Send a request to this app (the test runner itself). + final AppControl request = AppControl( + appId: kAppId, + operation: 'operation_1', + ); + await request.sendLaunchRequest(); + + final ReceivedAppControl received = await AppControl.onAppControl.first; + expect(received.appId, kAppId); + expect(received.operation, 'operation_1'); + expect(received.uri, isNull); + expect(received.mime, isNull); + expect(received.category, isNull); + expect(received.launchMode, LaunchMode.single); + expect(received.extraData, isEmpty); + expect(received.shouldReply, isFalse); + }, timeout: kTimeout); + + testWidgets('Omit invalid extra data', (WidgetTester _) async { + final AppControl request = AppControl( + appId: kAppId, + extraData: { + 'STRING_DATA': 'string', + 'STRING_LIST_DATA': ['string', 'list'], + 'INTEGER_DATA': 1, + }, + ); + await request.sendLaunchRequest(); + + final ReceivedAppControl received = await AppControl.onAppControl.first; + expect(received.extraData.length, 2); + expect(received.extraData['STRING_DATA'], 'string'); + expect(received.extraData['STRING_LIST_DATA'], isNotEmpty); + expect(received.extraData['STRING_LIST_DATA'][0], 'string'); + }, timeout: kTimeout); + + testWidgets('Can send and receive reply', (WidgetTester _) async { + // This time, the request is sent to the service app instead of the test + // runner, because the platform doesn't allow sending a reply back when + // caller = callee. + final AppControl request = AppControl( + appId: kServiceAppId, + operation: 'operation_2', + ); + await request.sendLaunchRequest( + replyCallback: ( + AppControl request, + AppControl reply, + AppControlReplyResult result, + ) { + expect(result, AppControlReplyResult.canceled); + expect(reply.extraData['STRING_DATA'], 'string'); + }, + ); + }, timeout: kTimeout); + + testWidgets('Cannot find target applications', (WidgetTester _) async { + final AppControl request1 = AppControl(appId: 'unknown_app'); + expect( + request1.sendLaunchRequest, + throwsA(isInstanceOf()), + ); + + final AppControl request2 = AppControl(operation: 'unknown_operation'); + expect( + request2.sendLaunchRequest, + throwsA(isInstanceOf()), + ); + }, timeout: kTimeout); + + testWidgets('Can terminate service application', (WidgetTester _) async { + expect(AppManager.isRunning(kServiceAppId), isFalse); + + final AppControl request = AppControl(appId: kServiceAppId); + await request.sendLaunchRequest(); + await Future.delayed(const Duration(seconds: 1)); + expect(AppManager.isRunning(kServiceAppId), isTrue); + + AppManager.terminateBackgroundApplication(kServiceAppId); + await Future.delayed(const Duration(seconds: 1)); + expect(AppManager.isRunning(kServiceAppId), isFalse); + }, timeout: kTimeout); +} + +@pragma('vm:entry-point') +void serviceMain() { + WidgetsFlutterBinding.ensureInitialized(); + + AppControl.onAppControl.listen((ReceivedAppControl request) async { + if (request.shouldReply) { + final AppControl reply = AppControl( + extraData: {'STRING_DATA': 'string'}, + ); + await request.reply(reply, AppControlReplyResult.canceled); + await SystemNavigator.pop(); + } + }); + + Future.delayed(kTimeout.duration!).whenComplete(() async { + await SystemNavigator.pop(); + }); +} diff --git a/packages/tizen_app_control/example/lib/main.dart b/packages/tizen_app_control/example/lib/main.dart new file mode 100644 index 000000000..fee1f6b5f --- /dev/null +++ b/packages/tizen_app_control/example/lib/main.dart @@ -0,0 +1,198 @@ +// Copyright 2021 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:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:messageport_tizen/messageport_tizen.dart'; +import 'package:tizen_app_control/app_control.dart'; +import 'package:tizen_app_control/app_manager.dart'; + +const String _kAppId = 'org.tizen.tizen_app_control_example'; +const String _kServiceAppId = 'org.tizen.tizen_app_control_example_service'; +const String _kPortName = 'service_port'; + +/// The main entry point for the UI app. +void main() { + runApp(const MyApp()); +} + +/// The main entry point for the service app. +@pragma('vm:entry-point') +void serviceMain() { + // This call is required to use platform channels. + WidgetsFlutterBinding.ensureInitialized(); + + // Listen for incoming AppControls. + final StreamSubscription appControlListener = + AppControl.onAppControl.listen((ReceivedAppControl request) async { + if (request.shouldReply) { + final AppControl reply = AppControl(); + await request.reply(reply, AppControlReplyResult.succeeded); + } + }); + + // Connect to the UI app and send messages. + // An exception will be thrown if the UI app is not running. + TizenMessagePort.connectToRemotePort(_kAppId, _kPortName) + .then((RemotePort remotePort) async { + while (true) { + if (await remotePort.check()) { + await remotePort.send(null); + } else { + break; + } + await Future.delayed(const Duration(seconds: 1)); + } + }).whenComplete(() async { + await appControlListener.cancel(); + await SystemNavigator.pop(); + }); +} + +/// The main UI app widget. +class MyApp extends StatefulWidget { + /// The main UI app widget. + const MyApp({Key? key}) : super(key: key); + + @override + _MyAppState createState() => _MyAppState(); +} + +class _MyAppState extends State { + final GlobalKey _messengerKey = + GlobalKey(); + LocalPort? _localPort; + int _messagesCount = 0; + bool _isServiceStarted = false; + + @override + void initState() { + super.initState(); + + // Open a message port to receive messages from the service app. + TizenMessagePort.createLocalPort(_kPortName).then((LocalPort value) { + _localPort = value; + _localPort?.register((dynamic message, [RemotePort? remotePort]) { + setState(() { + _messagesCount++; + }); + }); + }); + } + + @override + void dispose() { + super.dispose(); + + _localPort?.unregister(); + } + + Future _sendSms() async { + final AppControl request = AppControl( + operation: 'http://tizen.org/appcontrol/operation/share_text', + uri: 'sms:', + launchMode: LaunchMode.group, + extraData: { + 'http://tizen.org/appcontrol/data/text': 'Some text', + }, + ); + await request.sendLaunchRequest(); + } + + Future _pickImage() async { + final AppControl request = AppControl( + operation: 'http://tizen.org/appcontrol/operation/pick', + mime: 'image/*', + launchMode: LaunchMode.group, + ); + await request.sendLaunchRequest( + replyCallback: ( + AppControl request, + AppControl reply, + AppControlReplyResult result, + ) { + const String kAppControlDataSelected = + 'http://tizen.org/appcontrol/data/selected'; + String? imagePath; + if (result == AppControlReplyResult.succeeded && + reply.extraData.containsKey(kAppControlDataSelected)) { + imagePath = reply.extraData[kAppControlDataSelected][0] as String; + } + _messengerKey.currentState!.showSnackBar( + SnackBar(content: Text(imagePath ?? 'No image selected.')), + ); + }, + ); + } + + Future _launchService() async { + final AppControl request = AppControl(appId: _kServiceAppId); + await request.sendLaunchRequest( + replyCallback: ( + AppControl request, + AppControl reply, + AppControlReplyResult result, + ) { + if (result == AppControlReplyResult.succeeded) { + setState(() { + _isServiceStarted = true; + }); + } else { + _messengerKey.currentState!.showSnackBar( + const SnackBar(content: Text('Launch failed.')), + ); + } + }, + ); + } + + Future _terminateService() async { + AppManager.terminateBackgroundApplication(_kServiceAppId); + setState(() { + _isServiceStarted = AppManager.isRunning(_kServiceAppId); + }); + } + + @override + Widget build(BuildContext context) { + return MaterialApp( + scaffoldMessengerKey: _messengerKey, + home: Scaffold( + appBar: AppBar(title: const Text('Tizen App Control Example')), + body: Center( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + ElevatedButton( + onPressed: _sendSms, + child: const Text('Send SMS'), + ), + const SizedBox(height: 10), + ElevatedButton( + onPressed: _pickImage, + child: const Text('Pick image'), + ), + const SizedBox(height: 10), + if (_isServiceStarted) + ElevatedButton( + onPressed: _terminateService, + style: ElevatedButton.styleFrom(primary: Colors.redAccent), + child: const Text('Terminate service'), + ) + else + ElevatedButton( + onPressed: _launchService, + child: const Text('Launch service'), + ), + const SizedBox(height: 10), + Text('Received messages: $_messagesCount'), + ], + ), + ), + ), + ); + } +} diff --git a/packages/tizen_app_control/example/pubspec.yaml b/packages/tizen_app_control/example/pubspec.yaml new file mode 100644 index 000000000..338154a2d --- /dev/null +++ b/packages/tizen_app_control/example/pubspec.yaml @@ -0,0 +1,23 @@ +name: tizen_app_control_example +description: Demonstrates how to use the tizen_app_control plugin. +publish_to: "none" + +dependencies: + flutter: + sdk: flutter + messageport_tizen: ^0.1.0 + tizen_app_control: + path: ../ + +dev_dependencies: + flutter_driver: + sdk: flutter + flutter_test: + sdk: flutter + integration_test: + sdk: flutter + integration_test_tizen: + path: ../../integration_test/ + +environment: + sdk: ">=2.13.0 <3.0.0" diff --git a/packages/tizen_app_control/example/test_driver/integration_test.dart b/packages/tizen_app_control/example/test_driver/integration_test.dart new file mode 100644 index 000000000..b38629cca --- /dev/null +++ b/packages/tizen_app_control/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/tizen_app_control/example/tizen/.gitignore b/packages/tizen_app_control/example/tizen/.gitignore new file mode 100644 index 000000000..f68e1be09 --- /dev/null +++ b/packages/tizen_app_control/example/tizen/.gitignore @@ -0,0 +1 @@ +flutter/ diff --git a/packages/tizen_app_control/example/tizen/service/.exportMap b/packages/tizen_app_control/example/tizen/service/.exportMap new file mode 100644 index 000000000..3b97a4f3b --- /dev/null +++ b/packages/tizen_app_control/example/tizen/service/.exportMap @@ -0,0 +1,5 @@ +{ + global: main; + _IO_*; + local: *; +}; diff --git a/packages/tizen_app_control/example/tizen/service/.gitignore b/packages/tizen_app_control/example/tizen/service/.gitignore new file mode 100644 index 000000000..660cb0f67 --- /dev/null +++ b/packages/tizen_app_control/example/tizen/service/.gitignore @@ -0,0 +1,8 @@ +lib/*.so +res/flutter_assets/ +res/icudtl.dat +.cproject +.sign +crash-info/ +Debug/ +Release/ diff --git a/packages/tizen_app_control/example/tizen/service/inc/runner.h b/packages/tizen_app_control/example/tizen/service/inc/runner.h new file mode 100644 index 000000000..a2d45b6ee --- /dev/null +++ b/packages/tizen_app_control/example/tizen/service/inc/runner.h @@ -0,0 +1,6 @@ +#ifndef __RUNNER_H__ +#define __RUNNER_H__ + +#include + +#endif /* __RUNNER_H__ */ diff --git a/packages/tizen_app_control/example/tizen/service/project_def.prop b/packages/tizen_app_control/example/tizen/service/project_def.prop new file mode 100644 index 000000000..b72765c37 --- /dev/null +++ b/packages/tizen_app_control/example/tizen/service/project_def.prop @@ -0,0 +1,30 @@ +# See https://docs.tizen.org/application/tizen-studio/native-tools/project-conversion +# for details. + +APPNAME = runner_service +type = app +profile = common-4.0 + +# Source files +USER_SRCS += src/runner.cc + +# User defines +USER_DEFS = +USER_UNDEFS = +USER_CPP_DEFS = TIZEN_DEPRECATION DEPRECATION_WARNING +USER_CPP_UNDEFS = + +# Compiler/linker flags +USER_CFLAGS_MISC = +USER_CPPFLAGS_MISC = -c -fmessage-length=0 +USER_LFLAGS = -Wl,-E + +# Libraries and objects +USER_LIB_DIRS = lib +USER_LIBS = +USER_OBJS = + +# User includes +USER_INC_DIRS = inc ../flutter src +USER_INC_FILES = +USER_CPP_INC_FILES = diff --git a/packages/tizen_app_control/example/tizen/service/shared/res/ic_launcher.png b/packages/tizen_app_control/example/tizen/service/shared/res/ic_launcher.png new file mode 100644 index 000000000..4d6372eeb Binary files /dev/null and b/packages/tizen_app_control/example/tizen/service/shared/res/ic_launcher.png differ diff --git a/packages/tizen_app_control/example/tizen/service/src/runner.cc b/packages/tizen_app_control/example/tizen/service/src/runner.cc new file mode 100644 index 000000000..b344cf215 --- /dev/null +++ b/packages/tizen_app_control/example/tizen/service/src/runner.cc @@ -0,0 +1,19 @@ +#include "runner.h" + +#include "generated_plugin_registrant.h" + +class App : public FlutterServiceApp { + public: + bool OnCreate() { + if (FlutterServiceApp::OnCreate()) { + RegisterPlugins(this); + } + return IsRunning(); + } +}; + +int main(int argc, char *argv[]) { + App app; + app.SetDartEntrypoint("serviceMain"); + return app.Run(argc, argv); +} diff --git a/packages/tizen_app_control/example/tizen/service/tizen-manifest.xml b/packages/tizen_app_control/example/tizen/service/tizen-manifest.xml new file mode 100644 index 000000000..bd42f8811 --- /dev/null +++ b/packages/tizen_app_control/example/tizen/service/tizen-manifest.xml @@ -0,0 +1,10 @@ + + + + + + ic_launcher.png + + + + diff --git a/packages/tizen_app_control/example/tizen/ui/.exportMap b/packages/tizen_app_control/example/tizen/ui/.exportMap new file mode 100644 index 000000000..3b97a4f3b --- /dev/null +++ b/packages/tizen_app_control/example/tizen/ui/.exportMap @@ -0,0 +1,5 @@ +{ + global: main; + _IO_*; + local: *; +}; diff --git a/packages/tizen_app_control/example/tizen/ui/.gitignore b/packages/tizen_app_control/example/tizen/ui/.gitignore new file mode 100644 index 000000000..660cb0f67 --- /dev/null +++ b/packages/tizen_app_control/example/tizen/ui/.gitignore @@ -0,0 +1,8 @@ +lib/*.so +res/flutter_assets/ +res/icudtl.dat +.cproject +.sign +crash-info/ +Debug/ +Release/ diff --git a/packages/tizen_app_control/example/tizen/ui/inc/runner.h b/packages/tizen_app_control/example/tizen/ui/inc/runner.h new file mode 100644 index 000000000..a2d45b6ee --- /dev/null +++ b/packages/tizen_app_control/example/tizen/ui/inc/runner.h @@ -0,0 +1,6 @@ +#ifndef __RUNNER_H__ +#define __RUNNER_H__ + +#include + +#endif /* __RUNNER_H__ */ diff --git a/packages/tizen_app_control/example/tizen/ui/project_def.prop b/packages/tizen_app_control/example/tizen/ui/project_def.prop new file mode 100644 index 000000000..e810dc649 --- /dev/null +++ b/packages/tizen_app_control/example/tizen/ui/project_def.prop @@ -0,0 +1,30 @@ +# See https://docs.tizen.org/application/tizen-studio/native-tools/project-conversion +# for details. + +APPNAME = runner +type = app +profile = common-4.0 + +# Source files +USER_SRCS += src/runner.cc + +# User defines +USER_DEFS = +USER_UNDEFS = +USER_CPP_DEFS = TIZEN_DEPRECATION DEPRECATION_WARNING +USER_CPP_UNDEFS = + +# Compiler/linker flags +USER_CFLAGS_MISC = +USER_CPPFLAGS_MISC = -c -fmessage-length=0 +USER_LFLAGS = -Wl,-E + +# Libraries and objects +USER_LIB_DIRS = lib +USER_LIBS = +USER_OBJS = + +# User includes +USER_INC_DIRS = inc ../flutter src +USER_INC_FILES = +USER_CPP_INC_FILES = diff --git a/packages/tizen_app_control/example/tizen/ui/shared/res/ic_launcher.png b/packages/tizen_app_control/example/tizen/ui/shared/res/ic_launcher.png new file mode 100644 index 000000000..4d6372eeb Binary files /dev/null and b/packages/tizen_app_control/example/tizen/ui/shared/res/ic_launcher.png differ diff --git a/packages/tizen_app_control/example/tizen/ui/src/runner.cc b/packages/tizen_app_control/example/tizen/ui/src/runner.cc new file mode 100644 index 000000000..74d25472a --- /dev/null +++ b/packages/tizen_app_control/example/tizen/ui/src/runner.cc @@ -0,0 +1,18 @@ +#include "runner.h" + +#include "generated_plugin_registrant.h" + +class App : public FlutterApp { + public: + bool OnCreate() { + if (FlutterApp::OnCreate()) { + RegisterPlugins(this); + } + return IsRunning(); + } +}; + +int main(int argc, char *argv[]) { + App app; + return app.Run(argc, argv); +} diff --git a/packages/tizen_app_control/example/tizen/ui/tizen-manifest.xml b/packages/tizen_app_control/example/tizen/ui/tizen-manifest.xml new file mode 100644 index 000000000..5a6387f0c --- /dev/null +++ b/packages/tizen_app_control/example/tizen/ui/tizen-manifest.xml @@ -0,0 +1,13 @@ + + + + + + ic_launcher.png + + + http://tizen.org/privilege/appmanager.launch + http://tizen.org/privilege/appmanager.kill.bgapp + + + diff --git a/packages/tizen_app_control/lib/app_control.dart b/packages/tizen_app_control/lib/app_control.dart new file mode 100644 index 000000000..535a45538 --- /dev/null +++ b/packages/tizen_app_control/lib/app_control.dart @@ -0,0 +1,251 @@ +// Copyright 2021 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. + +library tizen_app_control; + +import 'dart:async'; + +import 'package:flutter/services.dart'; + +import 'app_manager.dart'; +import 'src/ffi.dart'; +import 'src/utils.dart'; + +/// Enumeration for the application control launch mode. +/// +/// For detailed information on Tizen's launch modes, see: +/// https://docs.tizen.org/application/native/guides/app-management/app-controls/#application-group-management +enum LaunchMode { + /// Prefer to launch the application as a main application in a new group. + single, + + /// Prefer to launch the application as a sub application in the same group. + group, +} + +/// Enumeration for the application control result. +enum AppControlReplyResult { + /// Reserved for platform developers. + appStarted, + + /// The callee application launched successfully. + succeeded, + + /// The launch failed. + failed, + + /// The operation has been canceled. + canceled, +} + +/// Callback to be called when a reply to a launch request is delivered. +/// +/// [request] represents the launch request that has been sent. +/// [reply] represents the reply message sent by the callee application. +/// [result] represents the result of launch. +typedef AppControlReplyCallback = FutureOr Function( + AppControl request, + AppControl reply, + AppControlReplyResult result, +); + +/// Represents a control message exchanged between applications. +/// +/// An explicit or implicit control request can be made by an application to +/// launch another application using this API. For detailed information on +/// Tizen application controls, see: +/// https://docs.tizen.org/application/native/guides/app-management/app-controls +/// +/// For a list of common operation types and examples, see: +/// https://docs.tizen.org/application/native/guides/app-management/common-appcontrols +class AppControl { + /// Creates an instance of [AppControl] with the given parameters. + AppControl({ + this.appId, + this.operation, + this.uri, + this.mime, + this.category, + this.launchMode = LaunchMode.single, + this.extraData = const {}, + }) { + _id = nativeCreateAppControl(this); + if (_id < 0) { + throw Exception('Could not create an instance of AppControl.'); + } + } + + AppControl._fromMap(dynamic map) + : _id = map['id'] as int, + appId = map['appId'] as String?, + operation = map['operation'] as String?, + uri = map['uri'] as String?, + mime = map['mime'] as String?, + category = map['category'] as String?, + launchMode = + enumFromString(LaunchMode.values, map['launchMode'] as String), + extraData = Map.from( + map['extraData'] as Map) { + if (!nativeAttachAppControl(_id, this)) { + throw Exception('Could not find an instance of AppControl with ID $_id.'); + } + } + + /// The ID of the application to handle this request (applicable for explicit + /// requests). + /// + /// Either `appId` or `operation` must be set to non-null before sending a + /// request. + String? appId; + + /// The operation to be performed by the callee application, such as + /// `http://tizen.org/appcontrol/operation/view`. + /// + /// If null, defaults to `http://tizen.org/appcontrol/operation/default`. + String? operation; + + /// The URI of the data to be handled by this request. + /// + /// If the URI points to a file (`file://`) in the caller's data directory, + /// the callee process will be granted a read access to the file temporarily + /// during its lifetime. + String? uri; + + /// The MIME type of the data to be handled by this request. + String? mime; + + /// The type of the application that should handle this request, such as + /// `http://tizen.org/category/homeapp`. + String? category; + + /// The launch mode, either [LaunchMode.single] or [LaunchMode.group]. + /// + /// This value acts as a hint for the platform and cannot override the value + /// set in the callee's manifest file. + LaunchMode launchMode; + + /// Additional information contained by this application control. Each value + /// must be either `String` or non-empty `List`. + Map extraData; + + /// The unique ID internally used for managing application control handles. + late int _id; + + static const MethodChannel _methodChannel = + MethodChannel('tizen/internal/app_control_method'); + + static const EventChannel _eventChannel = + EventChannel('tizen/internal/app_control_event'); + + /// A stream of incoming application controls. + static final Stream onAppControl = _eventChannel + .receiveBroadcastStream() + .map((dynamic event) => ReceivedAppControl._fromMap( + Map.from(event as Map))); + + /// Sends a launch request to an application. + /// + /// The `http://tizen.org/privilege/appmanager.launch` privilege is required + /// to use this API. + /// + /// If [replyCallback] is null, this call returns immediately after sending + /// a request to the platform. + /// + /// If [replyCallback] is non-null, this call will not return until a reply + /// is received from the callee and [replyCallback] is invoked. If the callee + /// doesn't reply to the request or is terminated before replying, this call + /// will never return and [replyCallback] will never be invoked, resulting in + /// a memory leak. + Future sendLaunchRequest({ + AppControlReplyCallback? replyCallback, + }) async { + await _setAppControlData(); + + final Map args = { + 'id': _id, + 'waitForReply': replyCallback != null, + }; + if (replyCallback == null) { + await _methodChannel.invokeMethod('sendLaunchRequest', args); + } else { + final dynamic response = + await _methodChannel.invokeMethod('sendLaunchRequest', args); + final Map responseMap = + Map.from(response as Map); + final AppControlReplyResult result = enumFromString( + AppControlReplyResult.values, + responseMap['result'] as String, + AppControlReplyResult.failed, + ); + final Map replyMap = Map.from( + responseMap['reply'] as Map); + final AppControl reply = AppControl._fromMap(replyMap); + await replyCallback(this, reply, result); + } + } + + /// Sends a terminate request to a running application. + /// + /// This API can be only used to terminate sub applications launched by the + /// caller application as a group. To terminate background applications not + /// launched as a group, use [AppManager.terminateBackgroundApplication] + /// instead. + /// + /// Applications that were launched by the callee application as a group will + /// be terminated by this API as well. + Future sendTerminateRequest() async { + await _setAppControlData(); + + final Map args = { + 'id': _id, + }; + await _methodChannel.invokeMethod('sendTerminateRequest', args); + } + + Future _setAppControlData() async { + final Map args = { + 'id': _id, + 'appId': appId, + 'operation': operation, + 'uri': uri, + 'mime': mime, + 'category': category, + 'launchMode': enumToString(launchMode), + 'extraData': extraData, + }; + await _methodChannel.invokeMethod('setAppControlData', args); + } +} + +/// Represents a received [AppControl] message. +class ReceivedAppControl extends AppControl { + ReceivedAppControl._fromMap(dynamic map) + : callerAppId = map['callerAppId'] as String?, + shouldReply = map['shouldReply'] as bool, + super._fromMap(map); + + /// The caller application ID. + final String? callerAppId; + + /// Whether a reply is requested. + /// + /// This is true when the caller application provided non-null + /// `replyCallback` for [AppControl.sendLaunchRequest]. + final bool shouldReply; + + /// Replies to a launch request. + /// + /// [reply] and [result] are sent back to the caller application and set as + /// arguments of [AppControlReplyCallback]. + Future reply(AppControl reply, AppControlReplyResult result) async { + await reply._setAppControlData(); + + final Map args = { + 'id': _id, + 'replyId': reply._id, + 'result': enumToString(result), + }; + await AppControl._methodChannel.invokeMethod('reply', args); + } +} diff --git a/packages/tizen_app_control/lib/app_manager.dart b/packages/tizen_app_control/lib/app_manager.dart new file mode 100644 index 000000000..3ae180b9d --- /dev/null +++ b/packages/tizen_app_control/lib/app_manager.dart @@ -0,0 +1,46 @@ +// Copyright 2021 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. + +// ignore_for_file: always_specify_types + +library tizen_app_control; + +import 'dart:ffi'; + +import 'package:ffi/ffi.dart'; + +import 'src/ffi.dart'; +import 'src/utils.dart'; + +/// Provides information about installed and running applications. +class AppManager { + const AppManager._(); + + /// Returns true if an application with the given [appId] is running, + /// otherwise false. + static bool isRunning(String appId) { + return using((Arena arena) { + final Pointer running = arena(); + throwOnError( + appManagerIsRunning(appId.toNativeUtf8(allocator: arena), running)); + return running.value > 0; + }); + } + + /// Sends a terminate request to an application with the given [appId]. + /// UI applications that are in a paused state and service applications can + /// be terminated by this API. + /// + /// The `http://tizen.org/privilege/appmanager.kill.bgapp` privilege is + /// required to use this API. + static void terminateBackgroundApplication(String appId) { + using((Arena arena) { + final Pointer appContext = arena(); + throwOnError(appManagerGetAppContext( + appId.toNativeUtf8(allocator: arena), appContext)); + throwOnError(appManagerRequestTerminateBgApp(appContext.value)); + throwOnError(appContextDestroy(appContext.value)); + }); + } +} diff --git a/packages/tizen_app_control/lib/src/ffi.dart b/packages/tizen_app_control/lib/src/ffi.dart new file mode 100644 index 000000000..fad83a8bb --- /dev/null +++ b/packages/tizen_app_control/lib/src/ffi.dart @@ -0,0 +1,105 @@ +// Copyright 2021 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. + +// ignore_for_file: public_member_api_docs +// ignore_for_file: always_specify_types + +import 'dart:ffi'; + +import 'package:ffi/ffi.dart'; + +typedef _InitializeDartApiNative = IntPtr Function(Pointer); +typedef _InitializeDartApi = int Function(Pointer); +typedef _CreateAppControlNative = Uint32 Function(Handle); +typedef _CreateAppControl = int Function(Object); +typedef _AttachAppControlNative = Int8 Function(Int32, Handle); +typedef _AttachAppControl = int Function(int, Object); + +DynamicLibrary? _libEmbedderCache; + +DynamicLibrary get _libEmbedder { + if (_libEmbedderCache == null) { + const List embedderPaths = [ + 'libflutter_tizen.so', + 'libflutter_tizen_common.so', + 'libflutter_tizen_mobile.so', + 'libflutter_tizen_tv.so', + 'libflutter_tizen_wearable.so', + ]; + for (final String path in embedderPaths) { + try { + _libEmbedderCache = DynamicLibrary.open(path); + break; + } on ArgumentError { + continue; + } + } + if (_libEmbedderCache == null) { + throw Exception('Failed to load the embedder library.'); + } + final _InitializeDartApi initFunction = _libEmbedder.lookupFunction< + _InitializeDartApiNative, + _InitializeDartApi>('NativeInitializeDartApi'); + initFunction(NativeApi.initializeApiDLData); + } + return _libEmbedderCache!; +} + +final _CreateAppControl nativeCreateAppControl = + _libEmbedder.lookupFunction<_CreateAppControlNative, _CreateAppControl>( + 'NativeCreateAppControl'); +final _AttachAppControl _nativeAttachAppControl = + _libEmbedder.lookupFunction<_AttachAppControlNative, _AttachAppControl>( + 'NativeAttachAppControl'); + +bool nativeAttachAppControl(int id, Object dartObject) { + return _nativeAttachAppControl(id, dartObject) > 0; +} + +class _AppContext extends Opaque {} + +typedef AppContextHandle = Pointer<_AppContext>; + +typedef _AppContextDestroyNative = Int32 Function(AppContextHandle); +typedef _AppContextDestroy = int Function(AppContextHandle); + +typedef _AppManagerGetAppContextNative = Int32 Function( + Pointer, Pointer); +typedef _AppManagerGetAppContext = int Function( + Pointer, Pointer); +typedef _AppManagerIsRunningNative = Int32 Function( + Pointer, Pointer); +typedef _AppManagerIsRunning = int Function(Pointer, Pointer); +typedef _AppManagerRequestTerminateBgAppNative = Int32 Function( + AppContextHandle); +typedef _AppManagerRequestTerminateBgApp = int Function(AppContextHandle); + +final DynamicLibrary _libAppManager = + DynamicLibrary.open('libcapi-appfw-app-manager.so.0'); + +final _AppManagerGetAppContext appManagerGetAppContext = _libAppManager + .lookupFunction<_AppManagerGetAppContextNative, _AppManagerGetAppContext>( + 'app_manager_get_app_context'); + +final _AppContextDestroy appContextDestroy = + _libAppManager.lookupFunction<_AppContextDestroyNative, _AppContextDestroy>( + 'app_context_destroy'); + +final _AppManagerRequestTerminateBgApp appManagerRequestTerminateBgApp = + _libAppManager.lookupFunction<_AppManagerRequestTerminateBgAppNative, + _AppManagerRequestTerminateBgApp>( + 'app_manager_request_terminate_bg_app'); + +final _AppManagerIsRunning appManagerIsRunning = _libAppManager.lookupFunction< + _AppManagerIsRunningNative, _AppManagerIsRunning>('app_manager_is_running'); + +typedef _GetErrorMessageNative = Pointer Function(Int32); +typedef _GetErrorMessage = Pointer Function(int); + +final DynamicLibrary _libBaseCommon = + DynamicLibrary.open('libcapi-base-common.so.0'); + +final _GetErrorMessage getErrorMessage = + _libBaseCommon.lookupFunction<_GetErrorMessageNative, _GetErrorMessage>( + 'get_error_message'); diff --git a/packages/tizen_app_control/lib/src/utils.dart b/packages/tizen_app_control/lib/src/utils.dart new file mode 100644 index 000000000..6c8f922f1 --- /dev/null +++ b/packages/tizen_app_control/lib/src/utils.dart @@ -0,0 +1,34 @@ +// Copyright 2021 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. + +// ignore_for_file: public_member_api_docs + +import 'package:ffi/ffi.dart'; +import 'package:flutter/services.dart'; + +import 'ffi.dart'; + +String enumToString(E enumValue) { + return enumValue.toString().split('.').last; +} + +E enumFromString( + List enumValues, + String stringValue, [ + E? defaultValue, +]) { + return enumValues.firstWhere( + (E e) => e.toString().split('.').last == stringValue, + orElse: () => defaultValue ?? enumValues.first, + ); +} + +void throwOnError(int ret) { + if (ret != 0) { + throw PlatformException( + code: ret.toString(), + message: getErrorMessage(ret).toDartString(), + ); + } +} diff --git a/packages/tizen_app_control/pubspec.yaml b/packages/tizen_app_control/pubspec.yaml new file mode 100644 index 000000000..7297d53f8 --- /dev/null +++ b/packages/tizen_app_control/pubspec.yaml @@ -0,0 +1,14 @@ +name: tizen_app_control +description: Tizen application control APIs. Used for launching and terminating + applications on a Tizen device. +homepage: https://github.com/flutter-tizen/plugins +version: 0.1.0 + +dependencies: + ffi: ^1.1.2 + flutter: + sdk: flutter + +environment: + sdk: ">=2.13.0 <3.0.0" + flutter: ">=2.2.0"