diff --git a/.cirrus.yml b/.cirrus.yml index 8c8cc624d1ee..d5a1ac82f6d5 100644 --- a/.cirrus.yml +++ b/.cirrus.yml @@ -93,7 +93,7 @@ task: - echo "This user does not have permission to run Firebase Test Lab tests." - else - echo $GCLOUD_FIREBASE_TESTLAB_KEY > ${HOME}/gcloud-service-key.json - - ./script/incremental_build.sh firebase-test-lab + - ./script/incremental_build.sh firebase-test-lab --device model=flame,version=29 --device model=starqlteue,version=26 - fi - export CIRRUS_CHANGE_MESSAGE=`cat /tmp/cirrus_change_message.txt` - export CIRRUS_COMMIT_MESSAGE=`cat /tmp/cirrus_commit_message.txt` diff --git a/packages/android_intent/CHANGELOG.md b/packages/android_intent/CHANGELOG.md index 4d63b0cbceab..f06652e71931 100644 --- a/packages/android_intent/CHANGELOG.md +++ b/packages/android_intent/CHANGELOG.md @@ -1,3 +1,7 @@ +## 0.3.7+3 + +* Update the `platform` package dependency to resolve the conflict with the latest flutter. + ## 0.3.7+2 * Declare API stability and compatibility with `1.0.0` (more details at: https://github.com/flutter/flutter/wiki/Package-migration-to-1.0.0). diff --git a/packages/android_intent/pubspec.yaml b/packages/android_intent/pubspec.yaml index 2e786277d869..166a73dfcb4d 100644 --- a/packages/android_intent/pubspec.yaml +++ b/packages/android_intent/pubspec.yaml @@ -4,7 +4,7 @@ homepage: https://github.com/flutter/plugins/tree/master/packages/android_intent # 0.3.y+z is compatible with 1.0.0, if you land a breaking change bump # the version to 2.0.0. # See more details: https://github.com/flutter/flutter/wiki/Package-migration-to-1.0.0 -version: 0.3.7+2 +version: 0.3.7+3 flutter: plugin: @@ -16,7 +16,7 @@ flutter: dependencies: flutter: sdk: flutter - platform: ^2.0.0 + platform: ">=2.0.0 <4.0.0" meta: ^1.0.5 dev_dependencies: test: ^1.3.0 diff --git a/packages/battery/CHANGELOG.md b/packages/battery/battery/CHANGELOG.md similarity index 97% rename from packages/battery/CHANGELOG.md rename to packages/battery/battery/CHANGELOG.md index c47879ce0419..2cf470d17e00 100644 --- a/packages/battery/CHANGELOG.md +++ b/packages/battery/battery/CHANGELOG.md @@ -1,3 +1,11 @@ +## 1.0.4+1 + +* Moved everything from battery to battery/battery + +## 1.0.4 + +* Updated README.md. + ## 1.0.3 * Update package:e2e to use package:integration_test diff --git a/packages/battery/LICENSE b/packages/battery/battery/LICENSE similarity index 100% rename from packages/battery/LICENSE rename to packages/battery/battery/LICENSE diff --git a/packages/battery/README.md b/packages/battery/battery/README.md similarity index 91% rename from packages/battery/README.md rename to packages/battery/battery/README.md index 93f8330db0db..22ce5007acd7 100644 --- a/packages/battery/README.md +++ b/packages/battery/battery/README.md @@ -14,10 +14,10 @@ To use this plugin, add `battery` as a [dependency in your pubspec.yaml file](ht import 'package:battery/battery.dart'; // Instantiate it -var battery = Battery(); +var _battery = Battery(); // Access current battery level -print(await battery.batteryLevel); +print(await _battery.batteryLevel); // Be informed when the state (full, charging, discharging) changes _battery.onBatteryStateChanged.listen((BatteryState state) { diff --git a/packages/battery/android/build.gradle b/packages/battery/battery/android/build.gradle similarity index 100% rename from packages/battery/android/build.gradle rename to packages/battery/battery/android/build.gradle diff --git a/packages/battery/android/gradle.properties b/packages/battery/battery/android/gradle.properties similarity index 100% rename from packages/battery/android/gradle.properties rename to packages/battery/battery/android/gradle.properties diff --git a/packages/battery/android/gradle/wrapper/gradle-wrapper.properties b/packages/battery/battery/android/gradle/wrapper/gradle-wrapper.properties similarity index 100% rename from packages/battery/android/gradle/wrapper/gradle-wrapper.properties rename to packages/battery/battery/android/gradle/wrapper/gradle-wrapper.properties diff --git a/packages/battery/android/settings.gradle b/packages/battery/battery/android/settings.gradle similarity index 100% rename from packages/battery/android/settings.gradle rename to packages/battery/battery/android/settings.gradle diff --git a/packages/battery/android/src/main/AndroidManifest.xml b/packages/battery/battery/android/src/main/AndroidManifest.xml similarity index 100% rename from packages/battery/android/src/main/AndroidManifest.xml rename to packages/battery/battery/android/src/main/AndroidManifest.xml diff --git a/packages/battery/android/src/main/java/io/flutter/plugins/battery/BatteryPlugin.java b/packages/battery/battery/android/src/main/java/io/flutter/plugins/battery/BatteryPlugin.java similarity index 100% rename from packages/battery/android/src/main/java/io/flutter/plugins/battery/BatteryPlugin.java rename to packages/battery/battery/android/src/main/java/io/flutter/plugins/battery/BatteryPlugin.java diff --git a/packages/battery/example/README.md b/packages/battery/battery/example/README.md similarity index 100% rename from packages/battery/example/README.md rename to packages/battery/battery/example/README.md diff --git a/packages/battery/example/android/app/build.gradle b/packages/battery/battery/example/android/app/build.gradle similarity index 100% rename from packages/battery/example/android/app/build.gradle rename to packages/battery/battery/example/android/app/build.gradle diff --git a/packages/battery/example/android/app/gradle/wrapper/gradle-wrapper.properties b/packages/battery/battery/example/android/app/gradle/wrapper/gradle-wrapper.properties similarity index 100% rename from packages/battery/example/android/app/gradle/wrapper/gradle-wrapper.properties rename to packages/battery/battery/example/android/app/gradle/wrapper/gradle-wrapper.properties diff --git a/packages/battery/example/android/app/src/androidTest/java/io/flutter/plugins/battery/EmbedderV1ActivityTest.java b/packages/battery/battery/example/android/app/src/androidTest/java/io/flutter/plugins/battery/EmbedderV1ActivityTest.java similarity index 100% rename from packages/battery/example/android/app/src/androidTest/java/io/flutter/plugins/battery/EmbedderV1ActivityTest.java rename to packages/battery/battery/example/android/app/src/androidTest/java/io/flutter/plugins/battery/EmbedderV1ActivityTest.java diff --git a/packages/battery/example/android/app/src/androidTest/java/io/flutter/plugins/battery/FlutterActivityTest.java b/packages/battery/battery/example/android/app/src/androidTest/java/io/flutter/plugins/battery/FlutterActivityTest.java similarity index 100% rename from packages/battery/example/android/app/src/androidTest/java/io/flutter/plugins/battery/FlutterActivityTest.java rename to packages/battery/battery/example/android/app/src/androidTest/java/io/flutter/plugins/battery/FlutterActivityTest.java diff --git a/packages/battery/example/android/app/src/main/AndroidManifest.xml b/packages/battery/battery/example/android/app/src/main/AndroidManifest.xml similarity index 100% rename from packages/battery/example/android/app/src/main/AndroidManifest.xml rename to packages/battery/battery/example/android/app/src/main/AndroidManifest.xml diff --git a/packages/battery/example/android/app/src/main/java/io/flutter/plugins/batteryexample/EmbedderV1Activity.java b/packages/battery/battery/example/android/app/src/main/java/io/flutter/plugins/batteryexample/EmbedderV1Activity.java similarity index 100% rename from packages/battery/example/android/app/src/main/java/io/flutter/plugins/batteryexample/EmbedderV1Activity.java rename to packages/battery/battery/example/android/app/src/main/java/io/flutter/plugins/batteryexample/EmbedderV1Activity.java diff --git a/packages/battery/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/packages/battery/battery/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png similarity index 100% rename from packages/battery/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png rename to packages/battery/battery/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png diff --git a/packages/battery/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png b/packages/battery/battery/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png similarity index 100% rename from packages/battery/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png rename to packages/battery/battery/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png diff --git a/packages/battery/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/packages/battery/battery/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png similarity index 100% rename from packages/battery/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png rename to packages/battery/battery/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png diff --git a/packages/battery/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/packages/battery/battery/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png similarity index 100% rename from packages/battery/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png rename to packages/battery/battery/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png diff --git a/packages/battery/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/packages/battery/battery/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png similarity index 100% rename from packages/battery/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png rename to packages/battery/battery/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png diff --git a/packages/battery/example/android/build.gradle b/packages/battery/battery/example/android/build.gradle similarity index 100% rename from packages/battery/example/android/build.gradle rename to packages/battery/battery/example/android/build.gradle diff --git a/packages/battery/example/android/gradle.properties b/packages/battery/battery/example/android/gradle.properties similarity index 100% rename from packages/battery/example/android/gradle.properties rename to packages/battery/battery/example/android/gradle.properties diff --git a/packages/battery/example/android/gradle/wrapper/gradle-wrapper.properties b/packages/battery/battery/example/android/gradle/wrapper/gradle-wrapper.properties similarity index 100% rename from packages/battery/example/android/gradle/wrapper/gradle-wrapper.properties rename to packages/battery/battery/example/android/gradle/wrapper/gradle-wrapper.properties diff --git a/packages/battery/example/android/settings.gradle b/packages/battery/battery/example/android/settings.gradle similarity index 100% rename from packages/battery/example/android/settings.gradle rename to packages/battery/battery/example/android/settings.gradle diff --git a/packages/battery/example/ios/Flutter/AppFrameworkInfo.plist b/packages/battery/battery/example/ios/Flutter/AppFrameworkInfo.plist similarity index 100% rename from packages/battery/example/ios/Flutter/AppFrameworkInfo.plist rename to packages/battery/battery/example/ios/Flutter/AppFrameworkInfo.plist diff --git a/packages/battery/example/ios/Flutter/Debug.xcconfig b/packages/battery/battery/example/ios/Flutter/Debug.xcconfig similarity index 100% rename from packages/battery/example/ios/Flutter/Debug.xcconfig rename to packages/battery/battery/example/ios/Flutter/Debug.xcconfig diff --git a/packages/battery/example/ios/Flutter/Release.xcconfig b/packages/battery/battery/example/ios/Flutter/Release.xcconfig similarity index 100% rename from packages/battery/example/ios/Flutter/Release.xcconfig rename to packages/battery/battery/example/ios/Flutter/Release.xcconfig diff --git a/packages/battery/example/ios/Runner.xcodeproj/project.pbxproj b/packages/battery/battery/example/ios/Runner.xcodeproj/project.pbxproj similarity index 100% rename from packages/battery/example/ios/Runner.xcodeproj/project.pbxproj rename to packages/battery/battery/example/ios/Runner.xcodeproj/project.pbxproj diff --git a/packages/battery/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/packages/battery/battery/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata similarity index 100% rename from packages/battery/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata rename to packages/battery/battery/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata diff --git a/packages/battery/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/packages/battery/battery/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme similarity index 100% rename from packages/battery/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme rename to packages/battery/battery/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme diff --git a/packages/battery/example/ios/Runner.xcworkspace/contents.xcworkspacedata b/packages/battery/battery/example/ios/Runner.xcworkspace/contents.xcworkspacedata similarity index 100% rename from packages/battery/example/ios/Runner.xcworkspace/contents.xcworkspacedata rename to packages/battery/battery/example/ios/Runner.xcworkspace/contents.xcworkspacedata diff --git a/packages/battery/example/ios/Runner/AppDelegate.h b/packages/battery/battery/example/ios/Runner/AppDelegate.h similarity index 100% rename from packages/battery/example/ios/Runner/AppDelegate.h rename to packages/battery/battery/example/ios/Runner/AppDelegate.h diff --git a/packages/battery/example/ios/Runner/AppDelegate.m b/packages/battery/battery/example/ios/Runner/AppDelegate.m similarity index 100% rename from packages/battery/example/ios/Runner/AppDelegate.m rename to packages/battery/battery/example/ios/Runner/AppDelegate.m diff --git a/packages/battery/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/packages/battery/battery/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json similarity index 100% rename from packages/battery/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json rename to packages/battery/battery/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json diff --git a/packages/battery/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png b/packages/battery/battery/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png similarity index 100% rename from packages/battery/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png rename to packages/battery/battery/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png diff --git a/packages/battery/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png b/packages/battery/battery/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png similarity index 100% rename from packages/battery/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png rename to packages/battery/battery/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png diff --git a/packages/battery/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png b/packages/battery/battery/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png similarity index 100% rename from packages/battery/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png rename to packages/battery/battery/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png diff --git a/packages/battery/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png b/packages/battery/battery/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png similarity index 100% rename from packages/battery/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png rename to packages/battery/battery/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png diff --git a/packages/battery/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png b/packages/battery/battery/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png similarity index 100% rename from packages/battery/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png rename to packages/battery/battery/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png diff --git a/packages/battery/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png b/packages/battery/battery/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png similarity index 100% rename from packages/battery/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png rename to packages/battery/battery/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png diff --git a/packages/battery/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png b/packages/battery/battery/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png similarity index 100% rename from packages/battery/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png rename to packages/battery/battery/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png diff --git a/packages/battery/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png b/packages/battery/battery/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png similarity index 100% rename from packages/battery/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png rename to packages/battery/battery/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png diff --git a/packages/battery/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png b/packages/battery/battery/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png similarity index 100% rename from packages/battery/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png rename to packages/battery/battery/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png diff --git a/packages/battery/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png b/packages/battery/battery/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png similarity index 100% rename from packages/battery/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png rename to packages/battery/battery/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png diff --git a/packages/battery/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png b/packages/battery/battery/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png similarity index 100% rename from packages/battery/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png rename to packages/battery/battery/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png diff --git a/packages/battery/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png b/packages/battery/battery/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png similarity index 100% rename from packages/battery/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png rename to packages/battery/battery/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png diff --git a/packages/battery/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png b/packages/battery/battery/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png similarity index 100% rename from packages/battery/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png rename to packages/battery/battery/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png diff --git a/packages/battery/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png b/packages/battery/battery/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png similarity index 100% rename from packages/battery/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png rename to packages/battery/battery/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png diff --git a/packages/battery/example/ios/Runner/Base.lproj/LaunchScreen.storyboard b/packages/battery/battery/example/ios/Runner/Base.lproj/LaunchScreen.storyboard similarity index 100% rename from packages/battery/example/ios/Runner/Base.lproj/LaunchScreen.storyboard rename to packages/battery/battery/example/ios/Runner/Base.lproj/LaunchScreen.storyboard diff --git a/packages/battery/example/ios/Runner/Base.lproj/Main.storyboard b/packages/battery/battery/example/ios/Runner/Base.lproj/Main.storyboard similarity index 100% rename from packages/battery/example/ios/Runner/Base.lproj/Main.storyboard rename to packages/battery/battery/example/ios/Runner/Base.lproj/Main.storyboard diff --git a/packages/battery/example/ios/Runner/Info.plist b/packages/battery/battery/example/ios/Runner/Info.plist similarity index 100% rename from packages/battery/example/ios/Runner/Info.plist rename to packages/battery/battery/example/ios/Runner/Info.plist diff --git a/packages/battery/example/ios/Runner/main.m b/packages/battery/battery/example/ios/Runner/main.m similarity index 100% rename from packages/battery/example/ios/Runner/main.m rename to packages/battery/battery/example/ios/Runner/main.m diff --git a/packages/battery/example/lib/main.dart b/packages/battery/battery/example/lib/main.dart similarity index 100% rename from packages/battery/example/lib/main.dart rename to packages/battery/battery/example/lib/main.dart diff --git a/packages/battery/example/pubspec.yaml b/packages/battery/battery/example/pubspec.yaml similarity index 88% rename from packages/battery/example/pubspec.yaml rename to packages/battery/battery/example/pubspec.yaml index 5ec38f6ce5c2..e7a9b1c8b74d 100644 --- a/packages/battery/example/pubspec.yaml +++ b/packages/battery/battery/example/pubspec.yaml @@ -11,7 +11,7 @@ dev_dependencies: flutter_driver: sdk: flutter integration_test: - path: ../../integration_test + path: ../../../integration_test pedantic: ^1.8.0 flutter: diff --git a/packages/battery/ios/Assets/.gitkeep b/packages/battery/battery/ios/Assets/.gitkeep similarity index 100% rename from packages/battery/ios/Assets/.gitkeep rename to packages/battery/battery/ios/Assets/.gitkeep diff --git a/packages/battery/ios/Classes/FLTBatteryPlugin.h b/packages/battery/battery/ios/Classes/FLTBatteryPlugin.h similarity index 100% rename from packages/battery/ios/Classes/FLTBatteryPlugin.h rename to packages/battery/battery/ios/Classes/FLTBatteryPlugin.h diff --git a/packages/battery/ios/Classes/FLTBatteryPlugin.m b/packages/battery/battery/ios/Classes/FLTBatteryPlugin.m similarity index 100% rename from packages/battery/ios/Classes/FLTBatteryPlugin.m rename to packages/battery/battery/ios/Classes/FLTBatteryPlugin.m diff --git a/packages/battery/ios/battery.podspec b/packages/battery/battery/ios/battery.podspec similarity index 100% rename from packages/battery/ios/battery.podspec rename to packages/battery/battery/ios/battery.podspec diff --git a/packages/battery/lib/battery.dart b/packages/battery/battery/lib/battery.dart similarity index 100% rename from packages/battery/lib/battery.dart rename to packages/battery/battery/lib/battery.dart diff --git a/packages/battery/pubspec.yaml b/packages/battery/battery/pubspec.yaml similarity index 90% rename from packages/battery/pubspec.yaml rename to packages/battery/battery/pubspec.yaml index 7a02f37bab85..5e0549a07f97 100644 --- a/packages/battery/pubspec.yaml +++ b/packages/battery/battery/pubspec.yaml @@ -1,8 +1,8 @@ name: battery description: Flutter plugin for accessing information about the battery state (full, charging, discharging) on Android and iOS. -homepage: https://github.com/flutter/plugins/tree/master/packages/battery -version: 1.0.3 +homepage: https://github.com/flutter/plugins/tree/master/packages/battery/battery +version: 1.0.4+1 flutter: plugin: @@ -25,7 +25,7 @@ dev_dependencies: flutter_test: sdk: flutter integration_test: - path: ../integration_test + path: ../../integration_test pedantic: ^1.8.0 environment: diff --git a/packages/battery/test/battery_e2e.dart b/packages/battery/battery/test/battery_e2e.dart similarity index 100% rename from packages/battery/test/battery_e2e.dart rename to packages/battery/battery/test/battery_e2e.dart diff --git a/packages/battery/test/battery_test.dart b/packages/battery/battery/test/battery_test.dart similarity index 100% rename from packages/battery/test/battery_test.dart rename to packages/battery/battery/test/battery_test.dart diff --git a/packages/battery/battery_platform_interface/CHANGELOG.md b/packages/battery/battery_platform_interface/CHANGELOG.md new file mode 100644 index 000000000000..6fadda91b380 --- /dev/null +++ b/packages/battery/battery_platform_interface/CHANGELOG.md @@ -0,0 +1,3 @@ +## 1.0.0 + +- Initial open-source release. diff --git a/packages/battery/battery_platform_interface/LICENSE b/packages/battery/battery_platform_interface/LICENSE new file mode 100644 index 000000000000..c89293372cf3 --- /dev/null +++ b/packages/battery/battery_platform_interface/LICENSE @@ -0,0 +1,27 @@ +// Copyright 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 name of Google Inc. nor the names of its +// 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/battery/battery_platform_interface/README.md b/packages/battery/battery_platform_interface/README.md new file mode 100644 index 000000000000..e1a42571c6b3 --- /dev/null +++ b/packages/battery/battery_platform_interface/README.md @@ -0,0 +1,26 @@ +# battery_platform_interface + +A common platform interface for the [`battery`][1] plugin. + +This interface allows platform-specific implementations of the `battery` +plugin, as well as the plugin itself, to ensure they are supporting the +same interface. + +# Usage + +To implement a new platform-specific implementation of `battery`, extend +[`BatteryPlatform`][2] with an implementation that performs the +platform-specific behavior, and when you register your plugin, set the default +`BatteryPlatform` by calling +`BatteryPlatform.instance = MyPlatformBattery()`. + +# Note on breaking changes + +Strongly prefer non-breaking changes (such as adding a method to the interface) +over breaking changes for this package. + +See https://flutter.dev/go/platform-interface-breaking-changes for a discussion +on why a less-clean interface is preferable to a breaking change. + +[1]: ../battery +[2]: lib/battery_platform_interface.dart diff --git a/packages/battery/battery_platform_interface/lib/battery_platform_interface.dart b/packages/battery/battery_platform_interface/lib/battery_platform_interface.dart new file mode 100644 index 000000000000..f803c7aaa8fd --- /dev/null +++ b/packages/battery/battery_platform_interface/lib/battery_platform_interface.dart @@ -0,0 +1,51 @@ +// 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. + +import 'dart:async'; + +import 'package:plugin_platform_interface/plugin_platform_interface.dart'; + +import 'method_channel/method_channel_battery.dart'; +import 'enums/battery_state.dart'; + +export 'enums/battery_state.dart'; + +/// The interface that implementations of battery must implement. +/// +/// Platform implementations should extend this class rather than implement it as `battery` +/// does not consider newly added methods to be breaking changes. Extending this class +/// (using `extends`) ensures that the subclass will get the default implementation, while +/// platform implementations that `implements` this interface will be broken by newly added +/// [BatteryPlatform] methods. +abstract class BatteryPlatform extends PlatformInterface { + /// Constructs a BatteryPlatform. + BatteryPlatform() : super(token: _token); + + static final Object _token = Object(); + + static BatteryPlatform _instance = MethodChannelBattery(); + + /// The default instance of [BatteryPlatform] to use. + /// + /// Defaults to [MethodChannelBattery]. + static BatteryPlatform get instance => _instance; + + /// Platform-specific plugins should set this with their own platform-specific + /// class that extends [BatteryPlatform] when they register themselves. + static set instance(BatteryPlatform instance) { + PlatformInterface.verifyToken(instance, _token); + _instance = instance; + } + + /// Gets the battery level from device. + Future batteryLevel() { + throw UnimplementedError('batteryLevel() has not been implemented.'); + } + + /// gets battery state from device. + Stream onBatteryStateChanged() { + throw UnimplementedError( + 'onBatteryStateChanged() has not been implemented.'); + } +} diff --git a/packages/battery/battery_platform_interface/lib/enums/battery_state.dart b/packages/battery/battery_platform_interface/lib/enums/battery_state.dart new file mode 100644 index 000000000000..7dd5e400faf2 --- /dev/null +++ b/packages/battery/battery_platform_interface/lib/enums/battery_state.dart @@ -0,0 +1,11 @@ +/// Indicates the current battery state. +enum BatteryState { + /// The battery is completely full of energy. + full, + + /// The battery is currently storing energy. + charging, + + /// The battery is currently losing energy. + discharging +} diff --git a/packages/battery/battery_platform_interface/lib/method_channel/method_channel_battery.dart b/packages/battery/battery_platform_interface/lib/method_channel/method_channel_battery.dart new file mode 100644 index 000000000000..4a3365cc2475 --- /dev/null +++ b/packages/battery/battery_platform_interface/lib/method_channel/method_channel_battery.dart @@ -0,0 +1,51 @@ +import 'dart:async'; + +import 'package:flutter/services.dart'; +import 'package:meta/meta.dart'; + +import 'package:battery_platform_interface/battery_platform_interface.dart'; + +import '../battery_platform_interface.dart'; + +/// An implementation of [BatteryPlatform] that uses method channels. +class MethodChannelBattery extends BatteryPlatform { + /// The method channel used to interact with the native platform. + @visibleForTesting + MethodChannel channel = MethodChannel('plugins.flutter.io/battery'); + + /// The event channel used to interact with the native platform. + @visibleForTesting + EventChannel eventChannel = EventChannel('plugins.flutter.io/charging'); + + /// Method channel for getting battery level. + Future batteryLevel() async { + return (await channel.invokeMethod('getBatteryLevel')).toInt(); + } + + /// Stream variable for storing battery state. + Stream _onBatteryStateChanged; + + /// Event channel for getting battery change state. + Stream onBatteryStateChanged() { + if (_onBatteryStateChanged == null) { + _onBatteryStateChanged = eventChannel + .receiveBroadcastStream() + .map((dynamic event) => _parseBatteryState(event)); + } + return _onBatteryStateChanged; + } +} + +/// Method for parsing battery state. +BatteryState _parseBatteryState(String state) { + switch (state) { + case 'full': + return BatteryState.full; + case 'charging': + return BatteryState.charging; + case 'discharging': + return BatteryState.discharging; + default: + throw ArgumentError('$state is not a valid BatteryState.'); + } +} diff --git a/packages/battery/battery_platform_interface/pubspec.yaml b/packages/battery/battery_platform_interface/pubspec.yaml new file mode 100644 index 000000000000..6c571debc7b0 --- /dev/null +++ b/packages/battery/battery_platform_interface/pubspec.yaml @@ -0,0 +1,22 @@ +name: battery_platform_interface +description: A common platform interface for the battery plugin. +homepage: https://github.com/flutter/plugins/tree/master/packages/battery +# NOTE: We strongly prefer non-breaking changes, even at the expense of a +# less-clean API. See https://flutter.dev/go/platform-interface-breaking-changes +version: 1.0.0 + +dependencies: + flutter: + sdk: flutter + meta: ^1.1.8 + plugin_platform_interface: ^1.0.2 + +dev_dependencies: + flutter_test: + sdk: flutter + mockito: ^4.1.1 + pedantic: ^1.8.0 + +environment: + sdk: ">=2.7.0 <3.0.0" + flutter: ">=1.9.1+hotfix.4 <2.0.0" diff --git a/packages/battery/battery_platform_interface/test/method_channel_battery_test.dart b/packages/battery/battery_platform_interface/test/method_channel_battery_test.dart new file mode 100644 index 000000000000..65323e4044de --- /dev/null +++ b/packages/battery/battery_platform_interface/test/method_channel_battery_test.dart @@ -0,0 +1,63 @@ +// 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. + +import 'package:flutter/services.dart'; +import 'package:flutter_test/flutter_test.dart'; + +import 'package:battery_platform_interface/battery_platform_interface.dart'; + +import 'package:battery_platform_interface/method_channel/method_channel_battery.dart'; + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + + group("$MethodChannelBattery", () { + MethodChannelBattery methodChannelBattery; + + setUp(() async { + methodChannelBattery = MethodChannelBattery(); + + methodChannelBattery.channel + .setMockMethodCallHandler((MethodCall methodCall) async { + switch (methodCall.method) { + case 'getBatteryLevel': + return 90; + default: + return null; + } + }); + + MethodChannel(methodChannelBattery.eventChannel.name) + .setMockMethodCallHandler((MethodCall methodCall) async { + switch (methodCall.method) { + case 'listen': + await ServicesBinding.instance.defaultBinaryMessenger + .handlePlatformMessage( + methodChannelBattery.eventChannel.name, + methodChannelBattery.eventChannel.codec + .encodeSuccessEnvelope('full'), + (_) {}, + ); + break; + case 'cancel': + default: + return null; + } + }); + }); + + /// Test for batetry level call. + test("getBatteryLevel", () async { + final int result = await methodChannelBattery.batteryLevel(); + expect(result, 90); + }); + + /// Test for battery changed state call. + test("onBatteryChanged", () async { + final BatteryState result = + await methodChannelBattery.onBatteryStateChanged().first; + expect(result, BatteryState.full); + }); + }); +} diff --git a/packages/battery/example/android.iml b/packages/battery/example/android.iml deleted file mode 100644 index 462b903e05b6..000000000000 --- a/packages/battery/example/android.iml +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - - - - - - - diff --git a/packages/battery/example/battery_example.iml b/packages/battery/example/battery_example.iml deleted file mode 100644 index 9d5dae19540c..000000000000 --- a/packages/battery/example/battery_example.iml +++ /dev/null @@ -1,15 +0,0 @@ - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/packages/image_picker/image_picker/CHANGELOG.md b/packages/image_picker/image_picker/CHANGELOG.md index 197314292f8f..07447db130ca 100644 --- a/packages/image_picker/image_picker/CHANGELOG.md +++ b/packages/image_picker/image_picker/CHANGELOG.md @@ -1,3 +1,7 @@ +## 0.6.7+7 + +* Updating documentation to use isEmpty check. + ## 0.6.7+6 * Update package:e2e -> package:integration_test diff --git a/packages/image_picker/image_picker/README.md b/packages/image_picker/image_picker/README.md index 1c9503cbe5cb..71d20ea4af2c 100755 --- a/packages/image_picker/image_picker/README.md +++ b/packages/image_picker/image_picker/README.md @@ -77,7 +77,7 @@ Android system -- although very rarely -- sometimes kills the MainActivity after Future retrieveLostData() async { final LostData response = await picker.getLostData(); - if (response == null) { + if (response.isEmpty) { return; } if (response.file != null) { diff --git a/packages/image_picker/image_picker/pubspec.yaml b/packages/image_picker/image_picker/pubspec.yaml index 118faf3712bc..91338f1e22ee 100755 --- a/packages/image_picker/image_picker/pubspec.yaml +++ b/packages/image_picker/image_picker/pubspec.yaml @@ -2,7 +2,7 @@ name: image_picker description: Flutter plugin for selecting images from the Android and iOS image library, and taking new pictures with the camera. homepage: https://github.com/flutter/plugins/tree/master/packages/image_picker/image_picker -version: 0.6.7+6 +version: 0.6.7+7 flutter: plugin: diff --git a/packages/in_app_purchase/CHANGELOG.md b/packages/in_app_purchase/CHANGELOG.md index ad53c7ccf1f5..65f7f86a16e6 100644 --- a/packages/in_app_purchase/CHANGELOG.md +++ b/packages/in_app_purchase/CHANGELOG.md @@ -1,3 +1,7 @@ +## 0.3.4+5 + +* Added necessary README docs for getting started with Android. + ## 0.3.4+4 * Update package:e2e -> package:integration_test diff --git a/packages/in_app_purchase/README.md b/packages/in_app_purchase/README.md index f3a34a667c70..021811842b2f 100644 --- a/packages/in_app_purchase/README.md +++ b/packages/in_app_purchase/README.md @@ -51,6 +51,19 @@ use. ### Initializing the plugin +```dart +void main() { + // Inform the plugin that this app supports pending purchases on Android. + // An error will occur on Android if you access the plugin `instance` + // without this call. + // + // On iOS this is a no-op. + InAppPurchaseConnection.enablePendingPurchases(); + + runApp(MyApp()); +} +``` + ```dart // Subscribe to any incoming purchases at app initialization. These can // propagate from either storefront so it's important to listen as soon as @@ -90,7 +103,7 @@ if (!available) { // Set literals require Dart 2.2. Alternatively, use `Set _kIds = ['product1', 'product2'].toSet()`. const Set _kIds = {'product1', 'product2'}; final ProductDetailsResponse response = await InAppPurchaseConnection.instance.queryProductDetails(_kIds); -if (!response.notFoundIDs.isEmpty) { +if (response.notFoundIDs.isNotEmpty) { // Handle the error. } List products = response.productDetails; diff --git a/packages/in_app_purchase/pubspec.yaml b/packages/in_app_purchase/pubspec.yaml index 0633d154cb8b..42bb0d8f4619 100644 --- a/packages/in_app_purchase/pubspec.yaml +++ b/packages/in_app_purchase/pubspec.yaml @@ -1,7 +1,7 @@ name: in_app_purchase description: A Flutter plugin for in-app purchases. Exposes APIs for making in-app purchases through the App Store and Google Play. homepage: https://github.com/flutter/plugins/tree/master/packages/in_app_purchase -version: 0.3.4+4 +version: 0.3.4+5 dependencies: async: ^2.0.8 diff --git a/packages/integration_test/CHANGELOG.md b/packages/integration_test/CHANGELOG.md index cbd5282028f4..d57819e331ea 100644 --- a/packages/integration_test/CHANGELOG.md +++ b/packages/integration_test/CHANGELOG.md @@ -1,3 +1,12 @@ +## 0.8.2 + +* Add support to get timeline. + +## 0.8.1 + +* Show stack trace of widget test errors on the platform side +* Fix method channel name for iOS + ## 0.8.0 * Rename plugin to integration_test. diff --git a/packages/integration_test/README.md b/packages/integration_test/README.md index bb0bfa2eb1ae..a48210beacf8 100644 --- a/packages/integration_test/README.md +++ b/packages/integration_test/README.md @@ -6,7 +6,7 @@ and native Android instrumentation testing. ## Usage -Add a dependency on the `integration_test` package in the +Add a dependency on the `integration_test` and `flutter_test` package in the `dev_dependencies` section of pubspec.yaml. For plugins, do this in the pubspec.yaml of the example app. @@ -14,6 +14,7 @@ Invoke `IntegrationTestWidgetsFlutterBinding.ensureInitialized()` at the start of a test file, e.g. ```dart +import 'package:flutter_test/flutter_test.dart'; import 'package:integration_test/integration_test.dart'; void main() { @@ -32,7 +33,7 @@ app code, it should go in `example/test/`. It is also acceptable to put integration_test tests in `test_driver/` folder so that they're alongside the runner app (see below). -## Using Flutter driver to run tests +## Using Flutter Driver to Run Tests `IntegrationTestWidgetsTestBinding` supports launching the on-device tests with `flutter drive`. Note that the tests don't use the `FlutterDriver` API, they @@ -47,28 +48,27 @@ import 'dart:async'; import 'package:integration_test/integration_test_driver.dart'; Future main() async => integrationDriver(); - ``` To run a example app test with Flutter driver: -``` +```sh cd example flutter drive test/_integration.dart ``` To test plugin APIs using Flutter driver: -``` +```sh cd example -flutter drive --driver=test_driver/_test.dart test/_e2e.dart +flutter drive --driver=test_driver/_test.dart test/_integration_test.dart ``` You can run tests on web in release or profile mode. First you need to make sure you have downloaded the driver for the browser. -``` +```sh cd example flutter drive -v --target=test_driver/dart -d web-server --release --browser-name=chrome ``` @@ -84,7 +84,7 @@ this test file MainActivityTest.java or another name of your choice. package com.example.myapp; import androidx.test.rule.ActivityTestRule; -import dev.flutter.plugins.e2e.FlutterTestRunner; +import dev.flutter.plugins.integration_test.FlutterTestRunner; import org.junit.Rule; import org.junit.runner.RunWith; @@ -99,7 +99,7 @@ Update your application's **myapp/android/app/build.gradle** to make sure it uses androidx's version of AndroidJUnitRunner and has androidx libraries as a dependency. -``` +```gradle android { ... defaultConfig { @@ -117,10 +117,10 @@ dependencies { } ``` -To e2e test on a local Android device (emulated or physical): +To run a test on a local Android device (emulated or physical): -``` -./gradlew app:connectedAndroidTest -Ptarget=`pwd`/../test_driver/_e2e.dart +```sh +./gradlew app:connectedAndroidTest -Ptarget=`pwd`/../test_driver/_integration_test.dart ``` ## Firebase Test Lab @@ -130,7 +130,7 @@ the guides in the [Firebase test lab documentation](https://firebase.google.com/docs/test-lab/?gclid=EAIaIQobChMIs5qVwqW25QIV8iCtBh3DrwyUEAAYASAAEgLFU_D_BwE) to set up a project. -To run an e2e test on Android devices using Firebase Test Lab, use gradle commands to build an +To run a test on Android devices using Firebase Test Lab, use gradle commands to build an instrumentation test for Android, after creating `androidTest` as suggested in the last section. ```bash @@ -172,10 +172,10 @@ target 'Runner' do end ``` -To e2e test on your iOS device (simulator or real), rebuild your iOS targets with Flutter tool. +To run a test on your iOS device (simulator or real), rebuild your iOS targets with Flutter tool. -``` -flutter build ios -t test_driver/_e2e.dart (--simulator) +```sh +flutter build ios -t test_driver/_integration_test.dart (--simulator) ``` Open Xcode project (by default, it's `ios/Runner.xcodeproj`). Create a test target @@ -184,9 +184,9 @@ change the code. You can change `RunnerTests.m` to the name of your choice. ```objective-c #import -#import +#import -E2E_IOS_RUNNER(RunnerTests) +INTEGRATION_TEST_IOS_RUNNER(RunnerTests) ``` -Now you can start RunnerTests to kick out e2e tests! +Now you can start RunnerTests to kick out integration tests! diff --git a/packages/integration_test/android/src/main/java/dev/flutter/plugins/integration_test/FlutterTestRunner.java b/packages/integration_test/android/src/main/java/dev/flutter/plugins/integration_test/FlutterTestRunner.java index 511fc141a917..fc6b99b30f64 100644 --- a/packages/integration_test/android/src/main/java/dev/flutter/plugins/integration_test/FlutterTestRunner.java +++ b/packages/integration_test/android/src/main/java/dev/flutter/plugins/integration_test/FlutterTestRunner.java @@ -77,7 +77,7 @@ public void run(RunNotifier notifier) { Description d = Description.createTestDescription(testClass, name); notifier.fireTestStarted(d); String outcome = results.get(name); - if (outcome.equals("failed")) { + if (!outcome.equals("success")) { Exception dummyException = new Exception(outcome); notifier.fireTestFailure(new Failure(d, dummyException)); } diff --git a/packages/integration_test/android/src/main/java/dev/flutter/plugins/integration_test/IntegrationTestPlugin.java b/packages/integration_test/android/src/main/java/dev/flutter/plugins/integration_test/IntegrationTestPlugin.java index 2245b33ba4c3..efe1028b6836 100644 --- a/packages/integration_test/android/src/main/java/dev/flutter/plugins/integration_test/IntegrationTestPlugin.java +++ b/packages/integration_test/android/src/main/java/dev/flutter/plugins/integration_test/IntegrationTestPlugin.java @@ -37,8 +37,8 @@ public void onAttachedToEngine(FlutterPluginBinding binding) { onAttachedToEngine(binding.getApplicationContext(), binding.getBinaryMessenger()); } - private void onAttachedToEngine(Context applicationContext, BinaryMessenger messenger) { - methodChannel = new MethodChannel(messenger, "plugins.flutter.io/integration_test"); + private void onAttachedToEngine(Context unusedApplicationContext, BinaryMessenger messenger) { + methodChannel = new MethodChannel(messenger, CHANNEL); methodChannel.setMethodCallHandler(this); } diff --git a/packages/integration_test/example/README.md b/packages/integration_test/example/README.md index 64a5e8780bc2..7c61fbf5c8de 100644 --- a/packages/integration_test/example/README.md +++ b/packages/integration_test/example/README.md @@ -1,6 +1,6 @@ -# e2e_example +# integration_test_example -Demonstrates how to use the e2e plugin. +Demonstrates how to use the `package:integration_test`. ## Getting Started diff --git a/packages/integration_test/example/test_driver/example_integration_io.dart b/packages/integration_test/example/test_driver/example_integration_io.dart index 35fc7271d841..7ed28963c32b 100644 --- a/packages/integration_test/example/test_driver/example_integration_io.dart +++ b/packages/integration_test/example/test_driver/example_integration_io.dart @@ -13,22 +13,29 @@ import 'package:integration_test/integration_test.dart'; import 'package:integration_test_example/main.dart' as app; void main() { - IntegrationTestWidgetsFlutterBinding.ensureInitialized(); + final IntegrationTestWidgetsFlutterBinding binding = + IntegrationTestWidgetsFlutterBinding.ensureInitialized() + as IntegrationTestWidgetsFlutterBinding; testWidgets('verify text', (WidgetTester tester) async { // Build our app and trigger a frame. app.main(); - // Trigger a frame. - await tester.pumpAndSettle(); + // Trace the timeline of the following operation. The timeline result will + // be written to `build/integration_response_data.json` with the key + // `timeline`. + await binding.traceAction(() async { + // Trigger a frame. + await tester.pumpAndSettle(); - // Verify that platform version is retrieved. - expect( - find.byWidgetPredicate( - (Widget widget) => - widget is Text && - widget.data.startsWith('Platform: ${Platform.operatingSystem}'), - ), - findsOneWidget, - ); + // Verify that platform version is retrieved. + expect( + find.byWidgetPredicate( + (Widget widget) => + widget is Text && + widget.data.startsWith('Platform: ${Platform.operatingSystem}'), + ), + findsOneWidget, + ); + }); }); } diff --git a/packages/integration_test/ios/Classes/IntegrationTestIosTest.m b/packages/integration_test/ios/Classes/IntegrationTestIosTest.m index 4243cdd86bc0..1397f547e6f6 100644 --- a/packages/integration_test/ios/Classes/IntegrationTestIosTest.m +++ b/packages/integration_test/ios/Classes/IntegrationTestIosTest.m @@ -26,7 +26,7 @@ - (BOOL)testIntegrationTest:(NSString **)testResult { NSLog(@"%@ passed.", test); [passedTests addObject:test]; } else { - NSLog(@"%@ failed.", test); + NSLog(@"%@ failed: %@", test, result); [failedTests addObject:test]; } } diff --git a/packages/integration_test/ios/Classes/IntegrationTestPlugin.m b/packages/integration_test/ios/Classes/IntegrationTestPlugin.m index 99d0c7fdf888..e7e5a74c01ee 100644 --- a/packages/integration_test/ios/Classes/IntegrationTestPlugin.m +++ b/packages/integration_test/ios/Classes/IntegrationTestPlugin.m @@ -1,6 +1,6 @@ #import "IntegrationTestPlugin.h" -static NSString *const kIntegrationTestPluginChannel = @"plugins.flutter.io/integratoin_test"; +static NSString *const kIntegrationTestPluginChannel = @"plugins.flutter.io/integration_test"; static NSString *const kMethodTestFinished = @"allTestsFinished"; @interface IntegrationTestPlugin () diff --git a/packages/integration_test/lib/common.dart b/packages/integration_test/lib/common.dart index 3363e3c91989..789b1fa54948 100644 --- a/packages/integration_test/lib/common.dart +++ b/packages/integration_test/lib/common.dart @@ -53,7 +53,7 @@ class Response { } } - /// Method for formating the test failures' details. + /// Method for formatting the test failures' details. String formatFailures(List failureDetails) { if (failureDetails.isEmpty) { return ''; @@ -78,7 +78,7 @@ class Response { } _failureDetails.forEach((Failure f) { - list.add(f.toString()); + list.add(f.toJson()); }); return list; @@ -107,14 +107,16 @@ class Failure { Failure(this.methodName, this.details); /// Serializes the object to JSON. - @override - String toString() { + String toJson() { return json.encode({ 'methodName': methodName, 'details': details, }); } + @override + String toString() => toJson(); + /// Decode a JSON string to create a Failure object. static Failure fromJsonString(String jsonString) { Map failure = json.decode(jsonString); diff --git a/packages/integration_test/lib/integration_test.dart b/packages/integration_test/lib/integration_test.dart index 6354ef768939..f6980bc9d6d1 100644 --- a/packages/integration_test/lib/integration_test.dart +++ b/packages/integration_test/lib/integration_test.dart @@ -3,16 +3,21 @@ // found in the LICENSE file. import 'dart:async'; +import 'dart:developer' as developer; import 'package:flutter/rendering.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/services.dart'; import 'package:flutter/widgets.dart'; +import 'package:vm_service/vm_service.dart' as vm; +import 'package:vm_service/vm_service_io.dart' as vm_io; import 'common.dart'; import '_extension_io.dart' if (dart.library.html) '_extension_web.dart'; +const String _success = 'success'; + /// A subclass of [LiveTestWidgetsFlutterBinding] that reports tests results /// on a channel to adapt them to native instrumentation test format. class IntegrationTestWidgetsFlutterBinding @@ -22,7 +27,6 @@ class IntegrationTestWidgetsFlutterBinding IntegrationTestWidgetsFlutterBinding() { // TODO(jackson): Report test results as they arrive tearDownAll(() async { - print('TESTING123: TEARING HER DOWN'); try { // For web integration tests we are not using the // `plugins.flutter.io/integration_test`. Mark the tests as complete @@ -34,7 +38,14 @@ class IntegrationTestWidgetsFlutterBinding } await _channel.invokeMethod( 'allTestsFinished', - {'results': results}, + { + 'results': results.map((name, result) { + if (result is Failure) { + return MapEntry(name, result.details); + } + return MapEntry(name, result); + }) + }, ); } on MissingPluginException { print('Warning: integration_test test plugin was not detected.'); @@ -47,8 +58,7 @@ class IntegrationTestWidgetsFlutterBinding final TestExceptionReporter oldTestExceptionReporter = reportTestException; reportTestException = (FlutterErrorDetails details, String testDescription) { - results[testDescription] = 'failed'; - _failureMethodsDetails.add(Failure(testDescription, details.toString())); + results[testDescription] = Failure(testDescription, details.toString()); if (!_allTestsPassed.isCompleted) { _allTestsPassed.complete(false); } @@ -96,17 +106,11 @@ class IntegrationTestWidgetsFlutterBinding final Completer _allTestsPassed = Completer(); - /// Stores failure details. - /// - /// Failed test method's names used as key. - final List _failureMethodsDetails = List(); - /// Similar to [WidgetsFlutterBinding.ensureInitialized]. /// /// Returns an instance of the [IntegrationTestWidgetsFlutterBinding], creating and /// initializing it if necessary. static WidgetsBinding ensureInitialized() { - print('TESTING123 ensuring init'); if (WidgetsBinding.instance == null) { IntegrationTestWidgetsFlutterBinding(); } @@ -119,10 +123,12 @@ class IntegrationTestWidgetsFlutterBinding /// Test results that will be populated after the tests have completed. /// - /// Keys are the test descriptions, and values are either `success` or - /// `failed`. + /// Keys are the test descriptions, and values are either [_success] or + /// a [Failure]. @visibleForTesting - Map results = {}; + Map results = {}; + + List get _failures => results.values.whereType().toList(); /// The extra data for the reported result. /// @@ -144,7 +150,7 @@ class IntegrationTestWidgetsFlutterBinding 'message': allTestsPassed ? Response.allTestsPassed(data: reportData).toJson() : Response.someTestsFailed( - _failureMethodsDetails, + _failures, data: reportData, ).toJson(), }; @@ -186,6 +192,94 @@ class IntegrationTestWidgetsFlutterBinding description: description, timeout: timeout, ); - results[description] ??= 'success'; + results[description] ??= _success; + } + + vm.VmService _vmService; + + /// Initialize the [vm.VmService] settings for the timeline. + @visibleForTesting + Future enableTimeline({ + List streams = const ['all'], + @visibleForTesting vm.VmService vmService, + }) async { + assert(streams != null); + assert(streams.isNotEmpty); + if (vmService != null) { + _vmService = vmService; + } + if (_vmService == null) { + final developer.ServiceProtocolInfo info = + await developer.Service.getInfo(); + assert(info.serverUri != null); + _vmService = await vm_io.vmServiceConnectUri( + 'ws://localhost:${info.serverUri.port}${info.serverUri.path}ws', + ); + } + await _vmService.setVMTimelineFlags(streams); + } + + /// Runs [action] and returns a [vm.Timeline] trace for it. + /// + /// Waits for the `Future` returned by [action] to complete prior to stopping + /// the trace. + /// + /// The `streams` parameter limits the recorded timeline event streams to only + /// the ones listed. By default, all streams are recorded. + /// See `timeline_streams` in + /// [Dart-SDK/runtime/vm/timeline.cc](https://github.com/dart-lang/sdk/blob/master/runtime/vm/timeline.cc) + /// + /// If [retainPriorEvents] is true, retains events recorded prior to calling + /// [action]. Otherwise, prior events are cleared before calling [action]. By + /// default, prior events are cleared. + Future traceTimeline( + Future action(), { + List streams = const ['all'], + bool retainPriorEvents = false, + }) async { + await enableTimeline(streams: streams); + if (retainPriorEvents) { + await action(); + return await _vmService.getVMTimeline(); + } + + await _vmService.clearVMTimeline(); + final vm.Timestamp startTime = await _vmService.getVMTimelineMicros(); + await action(); + final vm.Timestamp endTime = await _vmService.getVMTimelineMicros(); + return await _vmService.getVMTimeline( + timeOriginMicros: startTime.timestamp, + timeExtentMicros: endTime.timestamp, + ); + } + + /// This is a convenience wrap of [traceTimeline] and send the result back to + /// the host for the [flutter_driver] style tests. + /// + /// This records the timeline during `action` and adds the result to + /// [reportData] with `reportKey`. [reportData] contains the extra information + /// of the test other than test success/fail. It will be passed back to the + /// host and be processed by the [ResponseDataCallback] defined in + /// [integrationDriver]. By default it will be written to + /// `build/integration_response_data.json` with the key `timeline`. + /// + /// For tests with multiple calls of this method, `reportKey` needs to be a + /// unique key, otherwise the later result will override earlier one. + /// + /// The `streams` and `retainPriorEvents` parameters are passed as-is to + /// [traceTimeline]. + Future traceAction( + Future action(), { + List streams = const ['all'], + bool retainPriorEvents = false, + String reportKey = 'timeline', + }) async { + vm.Timeline timeline = await traceTimeline( + action, + streams: streams, + retainPriorEvents: retainPriorEvents, + ); + reportData ??= {}; + reportData[reportKey] = timeline.toJson(); } } diff --git a/packages/integration_test/lib/integration_test_driver.dart b/packages/integration_test/lib/integration_test_driver.dart index e5160b4e8d68..3b0e756000d0 100644 --- a/packages/integration_test/lib/integration_test_driver.dart +++ b/packages/integration_test/lib/integration_test_driver.dart @@ -19,7 +19,8 @@ import 'package:path/path.dart' as path; String testOutputsDirectory = Platform.environment['FLUTTER_TEST_OUTPUTS_DIR'] ?? 'build'; -/// The callback type to handle [integration_test.Response.data] after the test succcess. +/// The callback type to handle [integration_test.Response.data] after the test +/// succeeds. typedef ResponseDataCallback = FutureOr Function(Map); /// Writes a json-serializable json data to to diff --git a/packages/integration_test/pubspec.yaml b/packages/integration_test/pubspec.yaml index b222531044e9..9dd4ade6ce49 100644 --- a/packages/integration_test/pubspec.yaml +++ b/packages/integration_test/pubspec.yaml @@ -1,6 +1,6 @@ name: integration_test description: Runs tests that use the flutter_test API as integration tests. -version: 0.8.0 +version: 0.8.2 homepage: https://github.com/flutter/plugins/tree/master/packages/integration_test environment: @@ -15,9 +15,11 @@ dependencies: flutter_test: sdk: flutter path: ^1.6.4 + vm_service: ^4.2.0 dev_dependencies: pedantic: ^1.8.0 + mockito: ^4.1.1 flutter: plugin: diff --git a/packages/integration_test/test/binding_fail_test.dart b/packages/integration_test/test/binding_fail_test.dart index 020fb9607608..bb5961b18fc7 100644 --- a/packages/integration_test/test/binding_fail_test.dart +++ b/packages/integration_test/test/binding_fail_test.dart @@ -8,6 +8,7 @@ import 'package:flutter_test/flutter_test.dart'; const String _flutterBin = 'flutter'; const String _integrationResultsPrefix = 'IntegrationTestWidgetsFlutterBinding test results:'; +const String _failureExcerpt = 'Expected: \\n Actual: '; void main() async { group('Integration binding result', () { @@ -27,24 +28,20 @@ void main() async { final Map results = await _runTest('test/data/fail_test_script.dart'); + expect(results, hasLength(2)); expect( - results, - equals({ - 'failing test 1': 'failed', - 'failing test 2': 'failed', - })); + results, containsPair('failing test 1', contains(_failureExcerpt))); + expect( + results, containsPair('failing test 2', contains(_failureExcerpt))); }); test('when one test passes, then another fails', () async { final Map results = await _runTest('test/data/pass_then_fail_test_script.dart'); - expect( - results, - equals({ - 'passing test': 'success', - 'failing test': 'failed', - })); + expect(results, hasLength(2)); + expect(results, containsPair('passing test', equals('success'))); + expect(results, containsPair('failing test', contains(_failureExcerpt))); }); }); } diff --git a/packages/integration_test/test/binding_test.dart b/packages/integration_test/test/binding_test.dart index bad365ac59b6..ef4efc59aac0 100644 --- a/packages/integration_test/test/binding_test.dart +++ b/packages/integration_test/test/binding_test.dart @@ -1,8 +1,18 @@ +import 'dart:convert'; + import 'package:flutter/material.dart'; import 'package:integration_test/integration_test.dart'; import 'package:integration_test/common.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:mockito/mockito.dart'; +import 'package:vm_service/vm_service.dart' as vm; + +vm.Timeline _ktimelines = vm.Timeline( + traceEvents: [], + timeOriginMicros: 100, + timeExtentMicros: 200, +); void main() async { Future> request; @@ -14,10 +24,21 @@ void main() async { final IntegrationTestWidgetsFlutterBinding integrationBinding = binding as IntegrationTestWidgetsFlutterBinding; + MockVM mockVM; + List clockTimes = [100, 200]; + setUp(() { request = integrationBinding.callback({ 'command': 'request_data', }); + mockVM = MockVM(); + when(mockVM.getVMTimeline( + timeOriginMicros: anyNamed('timeOriginMicros'), + timeExtentMicros: anyNamed('timeExtentMicros'), + )).thenAnswer((_) => Future.value(_ktimelines)); + when(mockVM.getVMTimelineMicros()).thenAnswer( + (_) => Future.value(vm.Timestamp(timestamp: clockTimes.removeAt(0))), + ); }); testWidgets('Run Integration app', (WidgetTester tester) async { @@ -53,6 +74,17 @@ void main() async { expect(widgetCenter.dx, windowCenterX); expect(widgetCenter.dy, windowCenterY); }); + + testWidgets('Test traceAction', (WidgetTester tester) async { + await integrationBinding.enableTimeline(vmService: mockVM); + await integrationBinding.traceAction(() async {}); + expect(integrationBinding.reportData, isNotNull); + expect(integrationBinding.reportData.containsKey('timeline'), true); + expect( + json.encode(integrationBinding.reportData['timeline']), + json.encode(_ktimelines), + ); + }); }); tearDownAll(() async { @@ -66,3 +98,5 @@ void main() async { assert(result.data['answer'] == 42); }); } + +class MockVM extends Mock implements vm.VmService {} diff --git a/packages/path_provider/path_provider_macos/example/pubspec.yaml b/packages/path_provider/path_provider_macos/example/pubspec.yaml index 1ba991dc8160..aaa6842651b5 100644 --- a/packages/path_provider/path_provider_macos/example/pubspec.yaml +++ b/packages/path_provider/path_provider_macos/example/pubspec.yaml @@ -4,7 +4,11 @@ description: Demonstrates how to use the path_provider plugin. dependencies: flutter: sdk: flutter - path_provider: any + path_provider: ^1.6.14 + +# path_provider_macos is endorsed, so we need to add it to dependency_overrides +# to depend on it from path: +dependency_overrides: path_provider_macos: path: ../ diff --git a/packages/share/CHANGELOG.md b/packages/share/CHANGELOG.md index 8c3814d2f559..c4ee830ed34f 100644 --- a/packages/share/CHANGELOG.md +++ b/packages/share/CHANGELOG.md @@ -1,3 +1,7 @@ +## 0.6.5 + +* Added support for sharing files + ## 0.6.4+5 * Update package:e2e -> package:integration_test diff --git a/packages/share/README.md b/packages/share/README.md index 14be8da7d10e..750fca6a5b18 100644 --- a/packages/share/README.md +++ b/packages/share/README.md @@ -39,3 +39,9 @@ sharing to email. ``` dart Share.share('check out my website https://example.com', subject: 'Look what I made!'); ``` + +To share one or multiple files invoke the static `shareFiles` method anywhere in your Dart code. Optionally you can also pass in `text` and `subject`. +``` dart +Share.shareFiles(['${directory.path}/image.jpg'], text: 'Great picture'); +Share.shareFiles(['${directory.path}/image1.jpg', '${directory.path}/image2.jpg']); +``` \ No newline at end of file diff --git a/packages/share/android/build.gradle b/packages/share/android/build.gradle index e154b068c5dd..7506f4db8261 100644 --- a/packages/share/android/build.gradle +++ b/packages/share/android/build.gradle @@ -31,4 +31,9 @@ android { lintOptions { disable 'InvalidPackage' } + + dependencies { + implementation 'androidx.core:core:1.3.1' + implementation 'androidx.annotation:annotation:1.1.0' + } } diff --git a/packages/share/android/src/main/AndroidManifest.xml b/packages/share/android/src/main/AndroidManifest.xml index 407eae4d8128..c141a5c67928 100644 --- a/packages/share/android/src/main/AndroidManifest.xml +++ b/packages/share/android/src/main/AndroidManifest.xml @@ -1,3 +1,14 @@ + + + + + diff --git a/packages/share/android/src/main/java/io/flutter/plugins/share/MethodCallHandler.java b/packages/share/android/src/main/java/io/flutter/plugins/share/MethodCallHandler.java index f7e4d579e7a2..02841d3a4ae2 100644 --- a/packages/share/android/src/main/java/io/flutter/plugins/share/MethodCallHandler.java +++ b/packages/share/android/src/main/java/io/flutter/plugins/share/MethodCallHandler.java @@ -6,6 +6,8 @@ import io.flutter.plugin.common.MethodCall; import io.flutter.plugin.common.MethodChannel; +import java.io.*; +import java.util.List; import java.util.Map; /** Handles the method calls for the plugin. */ @@ -19,15 +21,37 @@ class MethodCallHandler implements MethodChannel.MethodCallHandler { @Override public void onMethodCall(MethodCall call, MethodChannel.Result result) { - if (call.method.equals("share")) { - if (!(call.arguments instanceof Map)) { - throw new IllegalArgumentException("Map argument expected"); - } - // Android does not support showing the share sheet at a particular point on screen. - share.share((String) call.argument("text"), (String) call.argument("subject")); - result.success(null); - } else { - result.notImplemented(); + switch (call.method) { + case "share": + expectMapArguments(call); + // Android does not support showing the share sheet at a particular point on screen. + share.share((String) call.argument("text"), (String) call.argument("subject")); + result.success(null); + break; + case "shareFiles": + expectMapArguments(call); + + // Android does not support showing the share sheet at a particular point on screen. + try { + share.shareFiles( + (List) call.argument("paths"), + (List) call.argument("mimeTypes"), + (String) call.argument("text"), + (String) call.argument("subject")); + result.success(null); + } catch (IOException e) { + result.error(e.getMessage(), null, null); + } + break; + default: + result.notImplemented(); + break; + } + } + + private void expectMapArguments(MethodCall call) throws IllegalArgumentException { + if (!(call.arguments instanceof Map)) { + throw new IllegalArgumentException("Map argument expected"); } } } diff --git a/packages/share/android/src/main/java/io/flutter/plugins/share/Share.java b/packages/share/android/src/main/java/io/flutter/plugins/share/Share.java index 8c9e833ee9d3..eb856bf572ee 100644 --- a/packages/share/android/src/main/java/io/flutter/plugins/share/Share.java +++ b/packages/share/android/src/main/java/io/flutter/plugins/share/Share.java @@ -5,19 +5,36 @@ package io.flutter.plugins.share; import android.app.Activity; +import android.content.Context; import android.content.Intent; +import android.content.pm.PackageManager; +import android.content.pm.ResolveInfo; +import android.net.Uri; +import android.os.Environment; +import androidx.annotation.NonNull; +import androidx.core.content.FileProvider; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.util.ArrayList; +import java.util.List; /** Handles share intent. */ class Share { + private Context context; private Activity activity; /** - * Constructs a Share object. The {@code activity} is used to start the share intent. It might be - * null when constructing the {@link Share} object and set to non-null when an activity is - * available using {@link #setActivity(Activity)}. + * Constructs a Share object. The {@code context} and {@code activity} are used to start the share + * intent. The {@code activity} might be null when constructing the {@link Share} object and set + * to non-null when an activity is available using {@link #setActivity(Activity)}. */ - Share(Activity activity) { + Share(Context context, Activity activity) { + this.context = context; this.activity = activity; } @@ -40,11 +57,177 @@ void share(String text, String subject) { shareIntent.putExtra(Intent.EXTRA_SUBJECT, subject); shareIntent.setType("text/plain"); Intent chooserIntent = Intent.createChooser(shareIntent, null /* dialog title optional */); + startActivity(chooserIntent); + } + + void shareFiles(List paths, List mimeTypes, String text, String subject) + throws IOException { + if (paths == null || paths.isEmpty()) { + throw new IllegalArgumentException("Non-empty path expected"); + } + + clearExternalShareFolder(); + ArrayList fileUris = getUrisForPaths(paths); + + Intent shareIntent = new Intent(); + if (fileUris.isEmpty()) { + share(text, subject); + return; + } else if (fileUris.size() == 1) { + shareIntent.setAction(Intent.ACTION_SEND); + shareIntent.putExtra(Intent.EXTRA_STREAM, fileUris.get(0)); + shareIntent.setType( + !mimeTypes.isEmpty() && mimeTypes.get(0) != null ? mimeTypes.get(0) : "*/*"); + } else { + shareIntent.setAction(Intent.ACTION_SEND_MULTIPLE); + shareIntent.putParcelableArrayListExtra(Intent.EXTRA_STREAM, fileUris); + shareIntent.setType(reduceMimeTypes(mimeTypes)); + } + if (text != null) shareIntent.putExtra(Intent.EXTRA_TEXT, text); + if (subject != null) shareIntent.putExtra(Intent.EXTRA_SUBJECT, subject); + shareIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); + Intent chooserIntent = Intent.createChooser(shareIntent, null /* dialog title optional */); + + List resInfoList = + getContext() + .getPackageManager() + .queryIntentActivities(chooserIntent, PackageManager.MATCH_DEFAULT_ONLY); + for (ResolveInfo resolveInfo : resInfoList) { + String packageName = resolveInfo.activityInfo.packageName; + for (Uri fileUri : fileUris) { + getContext() + .grantUriPermission( + packageName, + fileUri, + Intent.FLAG_GRANT_WRITE_URI_PERMISSION | Intent.FLAG_GRANT_READ_URI_PERMISSION); + } + } + + startActivity(chooserIntent); + } + + private void startActivity(Intent intent) { if (activity != null) { - activity.startActivity(chooserIntent); + activity.startActivity(intent); + } else if (context != null) { + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + context.startActivity(intent); + } else { + throw new IllegalStateException("Both context and activity are null"); + } + } + + private ArrayList getUrisForPaths(List paths) throws IOException { + ArrayList uris = new ArrayList<>(paths.size()); + for (String path : paths) { + File file = new File(path); + if (!fileIsOnExternal(file)) { + file = copyToExternalShareFolder(file); + } + + uris.add( + FileProvider.getUriForFile( + getContext(), getContext().getPackageName() + ".flutter.share_provider", file)); + } + return uris; + } + + private String reduceMimeTypes(List mimeTypes) { + if (mimeTypes.size() > 1) { + String reducedMimeType = mimeTypes.get(0); + for (int i = 1; i < mimeTypes.size(); i++) { + String mimeType = mimeTypes.get(i); + if (!reducedMimeType.equals(mimeType)) { + if (getMimeTypeBase(mimeType).equals(getMimeTypeBase(reducedMimeType))) { + reducedMimeType = getMimeTypeBase(mimeType) + "/*"; + } else { + reducedMimeType = "*/*"; + break; + } + } + } + return reducedMimeType; + } else if (mimeTypes.size() == 1) { + return mimeTypes.get(0); } else { - chooserIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); - activity.startActivity(chooserIntent); + return "*/*"; + } + } + + @NonNull + private String getMimeTypeBase(String mimeType) { + if (mimeType == null || !mimeType.contains("/")) { + return "*"; + } + + return mimeType.substring(0, mimeType.indexOf("/")); + } + + private boolean fileIsOnExternal(File file) { + try { + String filePath = file.getCanonicalPath(); + File externalDir = Environment.getExternalStorageDirectory(); + return externalDir != null && filePath.startsWith(externalDir.getCanonicalPath()); + } catch (IOException e) { + return false; + } + } + + @SuppressWarnings("ResultOfMethodCallIgnored") + private void clearExternalShareFolder() { + File folder = getExternalShareFolder(); + if (folder.exists()) { + for (File file : folder.listFiles()) { + file.delete(); + } + folder.delete(); + } + } + + @SuppressWarnings("ResultOfMethodCallIgnored") + private File copyToExternalShareFolder(File file) throws IOException { + File folder = getExternalShareFolder(); + if (!folder.exists()) { + folder.mkdirs(); + } + + File newFile = new File(folder, file.getName()); + copy(file, newFile); + return newFile; + } + + @NonNull + private File getExternalShareFolder() { + return new File(getContext().getExternalCacheDir(), "share"); + } + + private Context getContext() { + if (activity != null) { + return activity; + } + if (context != null) { + return context; + } + + throw new IllegalStateException("Both context and activity are null"); + } + + private static void copy(File src, File dst) throws IOException { + InputStream in = new FileInputStream(src); + try { + OutputStream out = new FileOutputStream(dst); + try { + // Transfer bytes from in to out + byte[] buf = new byte[1024]; + int len; + while ((len = in.read(buf)) > 0) { + out.write(buf, 0, len); + } + } finally { + out.close(); + } + } finally { + in.close(); } } } diff --git a/packages/share/android/src/main/java/io/flutter/plugins/share/ShareFileProvider.java b/packages/share/android/src/main/java/io/flutter/plugins/share/ShareFileProvider.java new file mode 100644 index 000000000000..87e4e42a03d4 --- /dev/null +++ b/packages/share/android/src/main/java/io/flutter/plugins/share/ShareFileProvider.java @@ -0,0 +1,14 @@ +// Copyright 2019 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +package io.flutter.plugins.share; + +import androidx.core.content.FileProvider; + +/** + * Providing a custom {@code FileProvider} prevents manifest {@code } name collisions. + * + *

See https://developer.android.com/guide/topics/manifest/provider-element.html for details. + */ +public class ShareFileProvider extends FileProvider {} diff --git a/packages/share/android/src/main/java/io/flutter/plugins/share/SharePlugin.java b/packages/share/android/src/main/java/io/flutter/plugins/share/SharePlugin.java index fdb9dc4fe644..bd7dfc22a3cd 100644 --- a/packages/share/android/src/main/java/io/flutter/plugins/share/SharePlugin.java +++ b/packages/share/android/src/main/java/io/flutter/plugins/share/SharePlugin.java @@ -5,6 +5,7 @@ package io.flutter.plugins.share; import android.app.Activity; +import android.content.Context; import io.flutter.embedding.engine.plugins.FlutterPlugin; import io.flutter.embedding.engine.plugins.activity.ActivityAware; import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding; @@ -22,12 +23,12 @@ public class SharePlugin implements FlutterPlugin, ActivityAware { public static void registerWith(Registrar registrar) { SharePlugin plugin = new SharePlugin(); - plugin.setUpChannel(registrar.activity(), registrar.messenger()); + plugin.setUpChannel(registrar.context(), registrar.activity(), registrar.messenger()); } @Override public void onAttachedToEngine(FlutterPluginBinding binding) { - setUpChannel(null, binding.getBinaryMessenger()); + setUpChannel(binding.getApplicationContext(), null, binding.getBinaryMessenger()); } @Override @@ -57,9 +58,9 @@ public void onDetachedFromActivityForConfigChanges() { onDetachedFromActivity(); } - private void setUpChannel(Activity activity, BinaryMessenger messenger) { + private void setUpChannel(Context context, Activity activity, BinaryMessenger messenger) { methodChannel = new MethodChannel(messenger, CHANNEL); - share = new Share(activity); + share = new Share(context, activity); handler = new MethodCallHandler(share); methodChannel.setMethodCallHandler(handler); } diff --git a/packages/share/android/src/main/res/xml/flutter_share_file_paths.xml b/packages/share/android/src/main/res/xml/flutter_share_file_paths.xml new file mode 100644 index 000000000000..e68bf916a30b --- /dev/null +++ b/packages/share/android/src/main/res/xml/flutter_share_file_paths.xml @@ -0,0 +1,6 @@ + + + + + + diff --git a/packages/share/example/ios/Runner/Info.plist b/packages/share/example/ios/Runner/Info.plist index ac44e05ef845..71656105a1fa 100644 --- a/packages/share/example/ios/Runner/Info.plist +++ b/packages/share/example/ios/Runner/Info.plist @@ -45,5 +45,11 @@ UIViewControllerBasedStatusBarAppearance + NSPhotoLibraryUsageDescription + This app requires access to the photo library for sharing images. + NSMicrophoneUsageDescription + This app does not require access to the microphone for sharing images. + NSCameraUsageDescription + This app requires access to the camera for sharing images. diff --git a/packages/share/example/lib/image_previews.dart b/packages/share/example/lib/image_previews.dart new file mode 100644 index 000000000000..61ecec43bdc7 --- /dev/null +++ b/packages/share/example/lib/image_previews.dart @@ -0,0 +1,75 @@ +import 'dart:io'; + +import 'package:flutter/material.dart'; +import 'package:flutter/widgets.dart'; + +/// Widget for displaying a preview of images +class ImagePreviews extends StatelessWidget { + /// The image paths of the displayed images + final List imagePaths; + + /// Callback when an image should be removed + final Function(int) onDelete; + + /// Creates a widget for preview of images. [imagePaths] can not be empty + /// and all contained paths need to be non empty. + const ImagePreviews(this.imagePaths, {Key key, this.onDelete}) + : super(key: key); + + @override + Widget build(BuildContext context) { + if (imagePaths.isEmpty) { + return Container(); + } + + List imageWidgets = []; + for (int i = 0; i < imagePaths.length; i++) { + imageWidgets.add(_ImagePreview( + imagePaths[i], + onDelete: onDelete != null ? () => onDelete(i) : null, + )); + } + + return SingleChildScrollView( + scrollDirection: Axis.horizontal, + child: Row(children: imageWidgets), + ); + } +} + +class _ImagePreview extends StatelessWidget { + final String imagePath; + final VoidCallback onDelete; + + const _ImagePreview(this.imagePath, {Key key, this.onDelete}) + : super(key: key); + + @override + Widget build(BuildContext context) { + File imageFile = File(imagePath); + return Padding( + padding: const EdgeInsets.all(8.0), + child: Stack( + children: [ + ConstrainedBox( + constraints: BoxConstraints( + maxWidth: 200, + maxHeight: 200, + ), + child: Image.file(imageFile), + ), + Positioned( + right: 0, + child: Padding( + padding: const EdgeInsets.all(8.0), + child: FloatingActionButton( + backgroundColor: Colors.red, + child: Icon(Icons.delete), + onPressed: onDelete), + ), + ), + ], + ), + ); + } +} diff --git a/packages/share/example/lib/main.dart b/packages/share/example/lib/main.dart index b68195cd3507..d6f1a1622b3c 100644 --- a/packages/share/example/lib/main.dart +++ b/packages/share/example/lib/main.dart @@ -5,8 +5,11 @@ // ignore_for_file: public_member_api_docs import 'package:flutter/material.dart'; +import 'package:image_picker/image_picker.dart'; import 'package:share/share.dart'; +import 'image_previews.dart'; + void main() { runApp(DemoApp()); } @@ -19,6 +22,7 @@ class DemoApp extends StatefulWidget { class DemoAppState extends State { String text = ''; String subject = ''; + List imagePaths = []; @override Widget build(BuildContext context) { @@ -28,59 +32,92 @@ class DemoAppState extends State { appBar: AppBar( title: const Text('Share Plugin Demo'), ), - body: Padding( - padding: const EdgeInsets.all(24.0), - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - TextField( - decoration: const InputDecoration( - labelText: 'Share text:', - hintText: 'Enter some text and/or link to share', + body: SingleChildScrollView( + child: Padding( + padding: const EdgeInsets.all(24.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + TextField( + decoration: const InputDecoration( + labelText: 'Share text:', + hintText: 'Enter some text and/or link to share', + ), + maxLines: 2, + onChanged: (String value) => setState(() { + text = value; + }), + ), + TextField( + decoration: const InputDecoration( + labelText: 'Share subject:', + hintText: 'Enter subject to share (optional)', + ), + maxLines: 2, + onChanged: (String value) => setState(() { + subject = value; + }), ), - maxLines: 2, - onChanged: (String value) => setState(() { - text = value; - }), - ), - TextField( - decoration: const InputDecoration( - labelText: 'Share subject:', - hintText: 'Enter subject to share (optional)', + const Padding(padding: EdgeInsets.only(top: 12.0)), + ImagePreviews(imagePaths, onDelete: _onDeleteImage), + ListTile( + leading: Icon(Icons.add), + title: Text("Add image"), + onTap: () async { + final imagePicker = ImagePicker(); + final pickedFile = await imagePicker.getImage( + source: ImageSource.gallery, + ); + if (pickedFile != null) { + setState(() { + imagePaths.add(pickedFile.path); + }); + } + }, ), - maxLines: 2, - onChanged: (String value) => setState(() { - subject = value; - }), - ), - const Padding(padding: EdgeInsets.only(top: 24.0)), - Builder( - builder: (BuildContext context) { - return RaisedButton( - child: const Text('Share'), - onPressed: text.isEmpty - ? null - : () { - // A builder is used to retrieve the context immediately - // surrounding the RaisedButton. - // - // The context's `findRenderObject` returns the first - // RenderObject in its descendent tree when it's not - // a RenderObjectWidget. The RaisedButton's RenderObject - // has its position and size after it's built. - final RenderBox box = context.findRenderObject(); - Share.share(text, - subject: subject, - sharePositionOrigin: - box.localToGlobal(Offset.zero) & - box.size); - }, - ); - }, - ), - ], + const Padding(padding: EdgeInsets.only(top: 12.0)), + Builder( + builder: (BuildContext context) { + return RaisedButton( + child: const Text('Share'), + onPressed: text.isEmpty && imagePaths.isEmpty + ? null + : () => _onShare(context), + ); + }, + ), + ], + ), ), )), ); } + + _onDeleteImage(int position) { + setState(() { + imagePaths.removeAt(position); + }); + } + + _onShare(BuildContext context) async { + // A builder is used to retrieve the context immediately + // surrounding the RaisedButton. + // + // The context's `findRenderObject` returns the first + // RenderObject in its descendent tree when it's not + // a RenderObjectWidget. The RaisedButton's RenderObject + // has its position and size after it's built. + final RenderBox box = context.findRenderObject(); + + if (imagePaths.isNotEmpty) { + await Share.shareFiles(imagePaths, + text: text, + subject: subject, + sharePositionOrigin: box.localToGlobal(Offset.zero) & box.size); + } else { + await Share.share(text, + subject: subject, + sharePositionOrigin: box.localToGlobal(Offset.zero) & box.size); + } + } } diff --git a/packages/share/example/pubspec.yaml b/packages/share/example/pubspec.yaml index 4830b7186019..8b8623910b7a 100644 --- a/packages/share/example/pubspec.yaml +++ b/packages/share/example/pubspec.yaml @@ -6,6 +6,7 @@ dependencies: sdk: flutter share: path: ../ + image_picker: ^0.6.7+4 dev_dependencies: flutter_driver: @@ -20,4 +21,3 @@ flutter: environment: sdk: ">=2.0.0-dev.28.0 <3.0.0" flutter: ">=1.9.1+hotfix.2 <2.0.0" - diff --git a/packages/share/ios/Classes/FLTSharePlugin.m b/packages/share/ios/Classes/FLTSharePlugin.m index 335ba5b819e5..837623a0119a 100644 --- a/packages/share/ios/Classes/FLTSharePlugin.m +++ b/packages/share/ios/Classes/FLTSharePlugin.m @@ -10,8 +10,12 @@ @interface ShareData : NSObject @property(readonly, nonatomic, copy) NSString *subject; @property(readonly, nonatomic, copy) NSString *text; +@property(readonly, nonatomic, copy) NSString *path; +@property(readonly, nonatomic, copy) NSString *mimeType; - (instancetype)initWithSubject:(NSString *)subject text:(NSString *)text NS_DESIGNATED_INITIALIZER; +- (instancetype)initWithFile:(NSString *)path + mimeType:(NSString *)mimeType NS_DESIGNATED_INITIALIZER; - (instancetype)init __attribute__((unavailable("Use initWithSubject:text: instead"))); @@ -27,24 +31,62 @@ - (instancetype)init { - (instancetype)initWithSubject:(NSString *)subject text:(NSString *)text { self = [super init]; if (self) { - _subject = subject; + _subject = [subject isKindOfClass:NSNull.class] ? @"" : subject; _text = text; } return self; } +- (instancetype)initWithFile:(NSString *)path mimeType:(NSString *)mimeType { + self = [super init]; + if (self) { + _path = path; + _mimeType = mimeType; + } + return self; +} + - (id)activityViewControllerPlaceholderItem:(UIActivityViewController *)activityViewController { return @""; } - (id)activityViewController:(UIActivityViewController *)activityViewController itemForActivityType:(UIActivityType)activityType { - return _text; + if (!_path || !_mimeType) { + return _text; + } + + if ([_mimeType hasPrefix:@"image/"]) { + UIImage *image = [UIImage imageWithContentsOfFile:_path]; + return image; + } else { + NSURL *url = [NSURL fileURLWithPath:_path]; + return url; + } } - (NSString *)activityViewController:(UIActivityViewController *)activityViewController subjectForActivityType:(UIActivityType)activityType { - return [_subject isKindOfClass:NSNull.class] ? @"" : _subject; + return _subject; +} + +- (UIImage *)activityViewController:(UIActivityViewController *)activityViewController + thumbnailImageForActivityType:(UIActivityType)activityType + suggestedSize:(CGSize)suggestedSize { + if (!_path || !_mimeType || ![_mimeType hasPrefix:@"image/"]) { + return nil; + } + + UIImage *image = [UIImage imageWithContentsOfFile:_path]; + return [self imageWithImage:image scaledToSize:suggestedSize]; +} + +- (UIImage *)imageWithImage:(UIImage *)image scaledToSize:(CGSize)newSize { + UIGraphicsBeginImageContext(newSize); + [image drawInRect:CGRectMake(0, 0, newSize.width, newSize.height)]; + UIImage *newImage = UIGraphicsGetImageFromCurrentImageContext(); + UIGraphicsEndImageContext(); + return newImage; } @end @@ -57,8 +99,19 @@ + (void)registerWithRegistrar:(NSObject *)registrar { binaryMessenger:registrar.messenger]; [shareChannel setMethodCallHandler:^(FlutterMethodCall *call, FlutterResult result) { + NSDictionary *arguments = [call arguments]; + NSNumber *originX = arguments[@"originX"]; + NSNumber *originY = arguments[@"originY"]; + NSNumber *originWidth = arguments[@"originWidth"]; + NSNumber *originHeight = arguments[@"originHeight"]; + + CGRect originRect = CGRectZero; + if (originX && originY && originWidth && originHeight) { + originRect = CGRectMake([originX doubleValue], [originY doubleValue], + [originWidth doubleValue], [originHeight doubleValue]); + } + if ([@"share" isEqualToString:call.method]) { - NSDictionary *arguments = [call arguments]; NSString *shareText = arguments[@"text"]; NSString *shareSubject = arguments[@"subject"]; @@ -69,19 +122,37 @@ + (void)registerWithRegistrar:(NSObject *)registrar { return; } - NSNumber *originX = arguments[@"originX"]; - NSNumber *originY = arguments[@"originY"]; - NSNumber *originWidth = arguments[@"originWidth"]; - NSNumber *originHeight = arguments[@"originHeight"]; + [self shareText:shareText + subject:shareSubject + withController:[UIApplication sharedApplication].keyWindow.rootViewController + atSource:originRect]; + result(nil); + } else if ([@"shareFiles" isEqualToString:call.method]) { + NSArray *paths = arguments[@"paths"]; + NSArray *mimeTypes = arguments[@"mimeTypes"]; + NSString *subject = arguments[@"subject"]; + NSString *text = arguments[@"text"]; + + if (paths.count == 0) { + result([FlutterError errorWithCode:@"error" + message:@"Non-empty paths expected" + details:nil]); + return; + } - CGRect originRect = CGRectZero; - if (originX != nil && originY != nil && originWidth != nil && originHeight != nil) { - originRect = CGRectMake([originX doubleValue], [originY doubleValue], - [originWidth doubleValue], [originHeight doubleValue]); + for (NSString *path in paths) { + if (path.length == 0) { + result([FlutterError errorWithCode:@"error" + message:@"Each path must not be empty" + details:nil]); + return; + } } - [self share:shareText - subject:shareSubject + [self shareFiles:paths + withMimeType:mimeTypes + withSubject:subject + withText:text withController:[UIApplication sharedApplication].keyWindow.rootViewController atSource:originRect]; result(nil); @@ -91,13 +162,11 @@ + (void)registerWithRegistrar:(NSObject *)registrar { }]; } -+ (void)share:(NSString *)shareText - subject:(NSString *)subject ++ (void)share:(NSArray *)shareItems withController:(UIViewController *)controller atSource:(CGRect)origin { - ShareData *data = [[ShareData alloc] initWithSubject:subject text:shareText]; UIActivityViewController *activityViewController = - [[UIActivityViewController alloc] initWithActivityItems:@[ data ] applicationActivities:nil]; + [[UIActivityViewController alloc] initWithActivityItems:shareItems applicationActivities:nil]; activityViewController.popoverPresentationController.sourceView = controller.view; if (!CGRectIsEmpty(origin)) { activityViewController.popoverPresentationController.sourceRect = origin; @@ -105,4 +174,44 @@ + (void)share:(NSString *)shareText [controller presentViewController:activityViewController animated:YES completion:nil]; } ++ (void)shareText:(NSString *)shareText + subject:(NSString *)subject + withController:(UIViewController *)controller + atSource:(CGRect)origin { + ShareData *data = [[ShareData alloc] initWithSubject:subject text:shareText]; + [self share:@[ data ] withController:controller atSource:origin]; +} + ++ (void)shareFiles:(NSArray *)paths + withMimeType:(NSArray *)mimeTypes + withSubject:(NSString *)subject + withText:(NSString *)text + withController:(UIViewController *)controller + atSource:(CGRect)origin { + NSMutableArray *items = [[NSMutableArray alloc] init]; + + if (text || subject) { + [items addObject:[[ShareData alloc] initWithSubject:subject text:text]]; + } + + for (int i = 0; i < [paths count]; i++) { + NSString *path = paths[i]; + NSString *pathExtension = [path pathExtension]; + NSString *mimeType = mimeTypes[i]; + if ([pathExtension.lowercaseString isEqualToString:@"jpg"] || + [pathExtension.lowercaseString isEqualToString:@"jpeg"] || + [pathExtension.lowercaseString isEqualToString:@"png"] || + [mimeType.lowercaseString isEqualToString:@"image/jpg"] || + [mimeType.lowercaseString isEqualToString:@"image/jpeg"] || + [mimeType.lowercaseString isEqualToString:@"image/png"]) { + UIImage *image = [UIImage imageWithContentsOfFile:path]; + [items addObject:image]; + } else { + [items addObject:[[ShareData alloc] initWithFile:path mimeType:mimeType]]; + } + } + + [self share:items withController:controller atSource:origin]; +} + @end diff --git a/packages/share/lib/share.dart b/packages/share/lib/share.dart index ff20d194f9e5..4a3ff6f1de09 100644 --- a/packages/share/lib/share.dart +++ b/packages/share/lib/share.dart @@ -7,6 +7,7 @@ import 'dart:ui'; import 'package:flutter/services.dart'; import 'package:meta/meta.dart' show visibleForTesting; +import 'package:mime/mime.dart' show lookupMimeType; /// Plugin for summoning a platform share sheet. class Share { @@ -51,4 +52,50 @@ class Share { return channel.invokeMethod('share', params); } + + /// Summons the platform's share sheet to share multiple files. + /// + /// Wraps the platform's native share dialog. Can share a file. + /// It uses the `ACTION_SEND` Intent on Android and `UIActivityViewController` + /// on iOS. + /// + /// The optional `sharePositionOrigin` parameter can be used to specify a global + /// origin rect for the share sheet to popover from on iPads. It has no effect + /// on non-iPads. + /// + /// May throw [PlatformException] or [FormatException] + /// from [MethodChannel]. + static Future shareFiles( + List paths, { + List mimeTypes, + String subject, + String text, + Rect sharePositionOrigin, + }) { + assert(paths != null); + assert(paths.isNotEmpty); + assert(paths.every((element) => element != null && element.isNotEmpty)); + final Map params = { + 'paths': paths, + 'mimeTypes': mimeTypes ?? + paths.map((String path) => _mimeTypeForPath(path)).toList(), + }; + + if (subject != null) params['subject'] = subject; + if (text != null) params['text'] = text; + + if (sharePositionOrigin != null) { + params['originX'] = sharePositionOrigin.left; + params['originY'] = sharePositionOrigin.top; + params['originWidth'] = sharePositionOrigin.width; + params['originHeight'] = sharePositionOrigin.height; + } + + return channel.invokeMethod('shareFiles', params); + } + + static String _mimeTypeForPath(String path) { + assert(path != null); + return lookupMimeType(path) ?? 'application/octet-stream'; + } } diff --git a/packages/share/pubspec.yaml b/packages/share/pubspec.yaml index f5e545ca112e..918087b139ec 100644 --- a/packages/share/pubspec.yaml +++ b/packages/share/pubspec.yaml @@ -5,7 +5,7 @@ homepage: https://github.com/flutter/plugins/tree/master/packages/share # 0.6.y+z is compatible with 1.0.0, if you land a breaking change bump # the version to 2.0.0. # See more details: https://github.com/flutter/flutter/wiki/Package-migration-to-1.0.0 -version: 0.6.4+5 +version: 0.6.5 flutter: plugin: @@ -18,6 +18,7 @@ flutter: dependencies: meta: ^1.0.5 + mime: ^0.9.7 flutter: sdk: flutter diff --git a/packages/share/test/share_test.dart b/packages/share/test/share_test.dart index c03f8fb439df..e862d1baf579 100644 --- a/packages/share/test/share_test.dart +++ b/packages/share/test/share_test.dart @@ -2,6 +2,7 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +import 'dart:io'; import 'dart:ui'; import 'package:flutter_test/flutter_test.dart' show TestWidgetsFlutterBinding; @@ -56,6 +57,52 @@ void main() { 'originHeight': 4.0, })); }); + + test('sharing null file fails', () { + expect( + () => Share.shareFiles([null]), + throwsA(const TypeMatcher()), + ); + verifyZeroInteractions(mockChannel); + }); + + test('sharing empty file fails', () { + expect( + () => Share.shareFiles(['']), + throwsA(const TypeMatcher()), + ); + verifyZeroInteractions(mockChannel); + }); + + test('sharing file sets correct mimeType', () async { + final String path = 'tempfile-83649a.png'; + final File file = File(path); + try { + file.createSync(); + await Share.shareFiles([path]); + verify(mockChannel.invokeMethod('shareFiles', { + 'paths': [path], + 'mimeTypes': ['image/png'], + })); + } finally { + file.deleteSync(); + } + }); + + test('sharing file sets passed mimeType', () async { + final String path = 'tempfile-83649a.png'; + final File file = File(path); + try { + file.createSync(); + await Share.shareFiles([path], mimeTypes: ['*/*']); + verify(mockChannel.invokeMethod('shareFiles', { + 'paths': [file.path], + 'mimeTypes': ['*/*'], + })); + } finally { + file.deleteSync(); + } + }); } class MockMethodChannel extends Mock implements MethodChannel {} diff --git a/packages/shared_preferences/shared_preferences_linux/CHANGELOG.md b/packages/shared_preferences/shared_preferences_linux/CHANGELOG.md index 353a921ff281..5bfd6651edfa 100644 --- a/packages/shared_preferences/shared_preferences_linux/CHANGELOG.md +++ b/packages/shared_preferences/shared_preferences_linux/CHANGELOG.md @@ -1,3 +1,7 @@ +## 0.0.2+2 + +* Bump the `file` package dependency to resolve dep conflicts with `flutter_driver`. + ## 0.0.2+1 * Replace path_provider dependency with path_provider_linux. diff --git a/packages/shared_preferences/shared_preferences_linux/pubspec.yaml b/packages/shared_preferences/shared_preferences_linux/pubspec.yaml index 3d40d39241ac..8a19c46fd531 100644 --- a/packages/shared_preferences/shared_preferences_linux/pubspec.yaml +++ b/packages/shared_preferences/shared_preferences_linux/pubspec.yaml @@ -1,6 +1,6 @@ name: shared_preferences_linux description: Linux implementation of the shared_preferences plugin -version: 0.0.2+1 +version: 0.0.2+2 homepage: https://github.com/flutter/plugins/tree/master/packages/shared_preferences/shared_preferences_linux flutter: @@ -15,7 +15,7 @@ environment: flutter: ">=1.12.8 <2.0.0" dependencies: - file: ^5.1.0 + file: ">=5.1.0 <7.0.0" flutter: sdk: flutter meta: ^1.0.4 diff --git a/packages/url_launcher/url_launcher/CHANGELOG.md b/packages/url_launcher/url_launcher/CHANGELOG.md index a5364726bee3..845fec757163 100644 --- a/packages/url_launcher/url_launcher/CHANGELOG.md +++ b/packages/url_launcher/url_launcher/CHANGELOG.md @@ -1,3 +1,7 @@ +## 5.5.1 + +* Added webOnlyWindowName parameter to launch() + ## 5.5.0 * Support Linux by default. diff --git a/packages/url_launcher/url_launcher/lib/url_launcher.dart b/packages/url_launcher/url_launcher/lib/url_launcher.dart index 2ce725da8642..bc171062093c 100644 --- a/packages/url_launcher/url_launcher/lib/url_launcher.dart +++ b/packages/url_launcher/url_launcher/lib/url_launcher.dart @@ -44,6 +44,9 @@ import 'package:url_launcher_platform_interface/url_launcher_platform_interface. /// [enableDomStorage] is an Android only setting. If true, WebView enable /// DOM storage. /// [headers] is an Android only setting that adds headers to the WebView. +/// [webOnlyWindowName] is an Web only setting . _blank opens the new url in new tab , +/// _self opens the new url in current tab. +/// Default behaviour is to open the url in new tab. /// /// Note that if any of the above are set to true but the URL is not a web URL, /// this will throw a [PlatformException]. @@ -63,6 +66,7 @@ Future launch( bool universalLinksOnly, Map headers, Brightness statusBarBrightness, + String webOnlyWindowName, }) async { assert(urlString != null); final Uri url = Uri.parse(urlString.trimLeft()); @@ -93,6 +97,7 @@ Future launch( enableDomStorage: enableDomStorage ?? false, universalLinksOnly: universalLinksOnly ?? false, headers: headers ?? {}, + webOnlyWindowName: webOnlyWindowName, ); assert(previousAutomaticSystemUiAdjustment != null); if (statusBarBrightness != null) { diff --git a/packages/url_launcher/url_launcher/pubspec.yaml b/packages/url_launcher/url_launcher/pubspec.yaml index 299de938165f..a3955226aed3 100644 --- a/packages/url_launcher/url_launcher/pubspec.yaml +++ b/packages/url_launcher/url_launcher/pubspec.yaml @@ -2,7 +2,7 @@ name: url_launcher description: Flutter plugin for launching a URL on Android and iOS. Supports web, phone, SMS, and email schemes. homepage: https://github.com/flutter/plugins/tree/master/packages/url_launcher/url_launcher -version: 5.5.0 +version: 5.5.1 flutter: plugin: diff --git a/packages/url_launcher/url_launcher_platform_interface/CHANGELOG.md b/packages/url_launcher/url_launcher_platform_interface/CHANGELOG.md index 8766d7a3f239..768042be4cef 100644 --- a/packages/url_launcher/url_launcher_platform_interface/CHANGELOG.md +++ b/packages/url_launcher/url_launcher_platform_interface/CHANGELOG.md @@ -1,3 +1,7 @@ +## 1.0.8 + +* Added webOnlyWindowName parameter + ## 1.0.7 * Update lower bound of dart dependency to 2.1.0. diff --git a/packages/url_launcher/url_launcher_platform_interface/lib/method_channel_url_launcher.dart b/packages/url_launcher/url_launcher_platform_interface/lib/method_channel_url_launcher.dart index 3fbd2ee01843..f87630ee3045 100644 --- a/packages/url_launcher/url_launcher_platform_interface/lib/method_channel_url_launcher.dart +++ b/packages/url_launcher/url_launcher_platform_interface/lib/method_channel_url_launcher.dart @@ -35,6 +35,7 @@ class MethodChannelUrlLauncher extends UrlLauncherPlatform { @required bool enableDomStorage, @required bool universalLinksOnly, @required Map headers, + String webOnlyWindowName, }) { return _channel.invokeMethod( 'launch', diff --git a/packages/url_launcher/url_launcher_platform_interface/lib/url_launcher_platform_interface.dart b/packages/url_launcher/url_launcher_platform_interface/lib/url_launcher_platform_interface.dart index 164555d63e0c..1de5742c1f6f 100644 --- a/packages/url_launcher/url_launcher_platform_interface/lib/url_launcher_platform_interface.dart +++ b/packages/url_launcher/url_launcher_platform_interface/lib/url_launcher_platform_interface.dart @@ -55,6 +55,7 @@ abstract class UrlLauncherPlatform extends PlatformInterface { @required bool enableDomStorage, @required bool universalLinksOnly, @required Map headers, + String webOnlyWindowName, }) { throw UnimplementedError('launch() has not been implemented.'); } diff --git a/packages/url_launcher/url_launcher_platform_interface/pubspec.yaml b/packages/url_launcher/url_launcher_platform_interface/pubspec.yaml index 4486134310c2..0c4096278bcb 100644 --- a/packages/url_launcher/url_launcher_platform_interface/pubspec.yaml +++ b/packages/url_launcher/url_launcher_platform_interface/pubspec.yaml @@ -3,7 +3,7 @@ description: A common platform interface for the url_launcher plugin. homepage: https://github.com/flutter/plugins/tree/master/packages/url_launcher/url_launcher_platform_interface # NOTE: We strongly prefer non-breaking changes, even at the expense of a # less-clean API. See https://flutter.dev/go/platform-interface-breaking-changes -version: 1.0.7 +version: 1.0.8 dependencies: flutter: diff --git a/packages/url_launcher/url_launcher_web/CHANGELOG.md b/packages/url_launcher/url_launcher_web/CHANGELOG.md index ed8014297776..2f33ffbe1d50 100644 --- a/packages/url_launcher/url_launcher_web/CHANGELOG.md +++ b/packages/url_launcher/url_launcher_web/CHANGELOG.md @@ -1,3 +1,7 @@ +# 0.1.3 + +- Added webOnlyWindowName parameter to launch() + # 0.1.2+1 - Update docs diff --git a/packages/url_launcher/url_launcher_web/lib/url_launcher_web.dart b/packages/url_launcher/url_launcher_web/lib/url_launcher_web.dart index 1bac4d524122..da73cd8b6350 100644 --- a/packages/url_launcher/url_launcher_web/lib/url_launcher_web.dart +++ b/packages/url_launcher/url_launcher_web/lib/url_launcher_web.dart @@ -39,15 +39,15 @@ class UrlLauncherPlugin extends UrlLauncherPlatform { bool _isSafariTargetTopScheme(String url) => _safariTargetTopSchemes.contains(_getUrlScheme(url)); - /// Opens the given [url] in a new window. + /// Opens the given [url] in the specified [webOnlyWindowName]. /// /// Returns the newly created window. @visibleForTesting - html.WindowBase openNewWindow(String url) { + html.WindowBase openNewWindow(String url, {String webOnlyWindowName}) { // We need to open mailto, tel and sms urls on the _top window context on safari browsers. // See https://github.com/flutter/flutter/issues/51461 for reference. - final target = - browser.isSafari && _isSafariTargetTopScheme(url) ? '_top' : ''; + final target = webOnlyWindowName ?? + ((browser.isSafari && _isSafariTargetTopScheme(url)) ? '_top' : ''); return _window.open(url, target); } @@ -65,7 +65,9 @@ class UrlLauncherPlugin extends UrlLauncherPlatform { @required bool enableDomStorage, @required bool universalLinksOnly, @required Map headers, + String webOnlyWindowName, }) { - return Future.value(openNewWindow(url) != null); + return Future.value( + openNewWindow(url, webOnlyWindowName: webOnlyWindowName) != null); } } diff --git a/packages/url_launcher/url_launcher_web/pubspec.yaml b/packages/url_launcher/url_launcher_web/pubspec.yaml index 727b396ba2e6..a77692df63e8 100644 --- a/packages/url_launcher/url_launcher_web/pubspec.yaml +++ b/packages/url_launcher/url_launcher_web/pubspec.yaml @@ -4,7 +4,7 @@ homepage: https://github.com/flutter/plugins/tree/master/packages/url_launcher/u # 0.1.y+z is compatible with 1.0.0, if you land a breaking change bump # the version to 2.0.0. # See more details: https://github.com/flutter/flutter/wiki/Package-migration-to-1.0.0 -version: 0.1.2+1 +version: 0.1.3 flutter: plugin: diff --git a/packages/url_launcher/url_launcher_web/test/url_launcher_web_test.dart b/packages/url_launcher/url_launcher_web/test/url_launcher_web_test.dart index b7e107d892cf..9cbaf686069f 100644 --- a/packages/url_launcher/url_launcher_web/test/url_launcher_web_test.dart +++ b/packages/url_launcher/url_launcher_web/test/url_launcher_web_test.dart @@ -145,6 +145,19 @@ void main() { verify(mockWindow.open('sms:+19725551212?body=hello%20there', '')); }); + test('setting oOnlyLinkTarget as _self opens the url in the same tab', + () { + plugin.openNewWindow("https://www.google.com", + webOnlyWindowName: "_self"); + verify(mockWindow.open('https://www.google.com', '_self')); + }); + + test('setting webOnlyLinkTarget as _blank opens the url in a new tab', + () { + plugin.openNewWindow("https://www.google.com", + webOnlyWindowName: "_blank"); + verify(mockWindow.open('https://www.google.com', '_blank')); + }); group('Safari', () { setUp(() { @@ -181,6 +194,13 @@ void main() { verify( mockWindow.open('sms:+19725551212?body=hello%20there', '_top')); }); + test( + 'mailto urls should use _blank if webOnlyWindowName is set as _blank', + () { + plugin.openNewWindow("mailto:name@mydomain.com", + webOnlyWindowName: "_blank"); + verify(mockWindow.open("mailto:name@mydomain.com", "_blank")); + }); }); }); }); diff --git a/packages/video_player/video_player/CHANGELOG.md b/packages/video_player/video_player/CHANGELOG.md index 1fdfad6256f4..35f50419f823 100644 --- a/packages/video_player/video_player/CHANGELOG.md +++ b/packages/video_player/video_player/CHANGELOG.md @@ -1,3 +1,11 @@ +## 0.10.12+2 + +* Fix `setMixWithOthers` test. + +## 0.10.12+1 + +* Depend on the version of `video_player_platform_interface` that contains the new `VideoPlayerOptions` class. + ## 0.10.12 * Introduce VideoPlayerOptions to set the audio mix mode. diff --git a/packages/video_player/video_player/pubspec.yaml b/packages/video_player/video_player/pubspec.yaml index a62f6f09a202..04c7c6ba3ddb 100644 --- a/packages/video_player/video_player/pubspec.yaml +++ b/packages/video_player/video_player/pubspec.yaml @@ -4,7 +4,7 @@ description: Flutter plugin for displaying inline video with other Flutter # 0.10.y+z is compatible with 1.0.0, if you land a breaking change bump # the version to 2.0.0. # See more details: https://github.com/flutter/flutter/wiki/Package-migration-to-1.0.0 -version: 0.10.12 +version: 0.10.12+2 homepage: https://github.com/flutter/plugins/tree/master/packages/video_player/video_player flutter: @@ -19,8 +19,8 @@ flutter: default_package: video_player_web dependencies: - meta: "^1.0.5" - video_player_platform_interface: ^2.0.0 + meta: ^1.0.5 + video_player_platform_interface: ^2.1.0 # The design on https://flutter.dev/go/federated-plugins was to leave # this constraint as "any". We cannot do it right now as it fails pub publish # validation, so we set a ^ constraint. diff --git a/packages/video_player/video_player/test/video_player_test.dart b/packages/video_player/video_player/test/video_player_test.dart index 67722594989c..f2d4c35a1ce8 100644 --- a/packages/video_player/video_player/test/video_player_test.dart +++ b/packages/video_player/video_player/test/video_player_test.dart @@ -579,11 +579,11 @@ void main() { expect(colors.backgroundColor, backgroundColor); }); - test('setMixWithOthers', () { + test('setMixWithOthers', () async { final VideoPlayerController controller = VideoPlayerController.file( File(''), videoPlayerOptions: VideoPlayerOptions(mixWithOthers: true)); - controller.initialize(); + await controller.initialize(); expect(controller.videoPlayerOptions.mixWithOthers, true); }); } @@ -656,6 +656,11 @@ class FakeVideoPlayerPlatform extends VideoPlayerApiTest { void setVolume(VolumeMessage arg) { calls.add('setVolume'); } + + @override + void setMixWithOthers(MixWithOthersMessage arg) { + calls.add('setMixWithOthers'); + } } class FakeVideoEventStream { diff --git a/packages/video_player/video_player_platform_interface/CHANGELOG.md b/packages/video_player/video_player_platform_interface/CHANGELOG.md index 0920582abad0..2af6f01ffe88 100644 --- a/packages/video_player/video_player_platform_interface/CHANGELOG.md +++ b/packages/video_player/video_player_platform_interface/CHANGELOG.md @@ -1,3 +1,7 @@ +## 2.1.1 + +* Fix mixWithOthers test channel. + ## 2.1.0 * Add VideoPlayerOptions with audo mix mode diff --git a/packages/video_player/video_player_platform_interface/lib/messages.dart b/packages/video_player/video_player_platform_interface/lib/messages.dart index a75ece0db850..c5e8cd413b70 100644 --- a/packages/video_player/video_player_platform_interface/lib/messages.dart +++ b/packages/video_player/video_player_platform_interface/lib/messages.dart @@ -134,6 +134,7 @@ abstract class VideoPlayerApiTest { PositionMessage position(TextureMessage arg); void seekTo(PositionMessage arg); void pause(TextureMessage arg); + void setMixWithOthers(MixWithOthersMessage arg); } void VideoPlayerApiTestSetup(VideoPlayerApiTest api) { @@ -225,6 +226,18 @@ void VideoPlayerApiTestSetup(VideoPlayerApiTest api) { return {}; }); } + { + const BasicMessageChannel channel = BasicMessageChannel( + 'dev.flutter.pigeon.VideoPlayerApi.setMixWithOthers', + StandardMessageCodec()); + channel.setMockMessageHandler((dynamic message) async { + final Map mapMessage = message as Map; + final MixWithOthersMessage input = + MixWithOthersMessage._fromMap(mapMessage); + api.setMixWithOthers(input); + return {}; + }); + } } class VideoPlayerApi { diff --git a/packages/video_player/video_player_platform_interface/pubspec.yaml b/packages/video_player/video_player_platform_interface/pubspec.yaml index 4740605d7669..38bc6851f376 100644 --- a/packages/video_player/video_player_platform_interface/pubspec.yaml +++ b/packages/video_player/video_player_platform_interface/pubspec.yaml @@ -3,7 +3,7 @@ description: A common platform interface for the video_player plugin. homepage: https://github.com/flutter/plugins/tree/master/packages/video_player/video_player_platform_interface # NOTE: We strongly prefer non-breaking changes, even at the expense of a # less-clean API. See https://flutter.dev/go/platform-interface-breaking-changes -version: 2.1.0 +version: 2.1.1 dependencies: flutter: diff --git a/packages/video_player/video_player_platform_interface/test/method_channel_video_player_test.dart b/packages/video_player/video_player_platform_interface/test/method_channel_video_player_test.dart index e7bcf26e9d26..185953163350 100644 --- a/packages/video_player/video_player_platform_interface/test/method_channel_video_player_test.dart +++ b/packages/video_player/video_player_platform_interface/test/method_channel_video_player_test.dart @@ -19,6 +19,7 @@ class _ApiLogger implements VideoPlayerApiTest { PositionMessage positionMessage; LoopingMessage loopingMessage; VolumeMessage volumeMessage; + MixWithOthersMessage mixWithOthersMessage; @override TextureMessage create(CreateMessage arg) { @@ -50,6 +51,12 @@ class _ApiLogger implements VideoPlayerApiTest { textureMessage = arg; } + @override + void setMixWithOthers(MixWithOthersMessage arg) { + log.add('setMixWithOthers'); + mixWithOthersMessage = arg; + } + @override PositionMessage position(TextureMessage arg) { log.add('position'); @@ -179,6 +186,16 @@ void main() { expect(log.textureMessage.textureId, 1); }); + test('setMixWithOthers', () async { + await player.setMixWithOthers(true); + expect(log.log.last, 'setMixWithOthers'); + expect(log.mixWithOthersMessage.mixWithOthers, true); + + await player.setMixWithOthers(false); + expect(log.log.last, 'setMixWithOthers'); + expect(log.mixWithOthersMessage.mixWithOthers, false); + }); + test('setVolume', () async { await player.setVolume(1, 0.7); expect(log.log.last, 'setVolume');