[image_picker] support android V2 embedding (#2430)
diff --git a/packages/image_picker/CHANGELOG.md b/packages/image_picker/CHANGELOG.md
index 6592c9e..bd0c289 100644
--- a/packages/image_picker/CHANGELOG.md
+++ b/packages/image_picker/CHANGELOG.md
@@ -1,3 +1,8 @@
+## 0.6.3
+
+* Support Android V2 embedding.
+* Migrate to using the new e2e test binding.
+
## 0.6.2+3
* Remove the deprecated `author:` field from pubspec.yaml
* Migrate the plugin to the pubspec platforms manifest.
diff --git a/packages/image_picker/android/src/main/java/io/flutter/plugins/imagepicker/ImagePickerDelegate.java b/packages/image_picker/android/src/main/java/io/flutter/plugins/imagepicker/ImagePickerDelegate.java
index e34a3b5..f2a0b02 100644
--- a/packages/image_picker/android/src/main/java/io/flutter/plugins/imagepicker/ImagePickerDelegate.java
+++ b/packages/image_picker/android/src/main/java/io/flutter/plugins/imagepicker/ImagePickerDelegate.java
@@ -199,6 +199,7 @@
this.cache = cache;
}
+ // Save the state of the image picker so it can be retrieved with `retrieveLostImage`.
void saveStateBeforeResult() {
if (methodCall == null) {
return;
diff --git a/packages/image_picker/android/src/main/java/io/flutter/plugins/imagepicker/ImagePickerPlugin.java b/packages/image_picker/android/src/main/java/io/flutter/plugins/imagepicker/ImagePickerPlugin.java
index b495a8e..950304c 100644
--- a/packages/image_picker/android/src/main/java/io/flutter/plugins/imagepicker/ImagePickerPlugin.java
+++ b/packages/image_picker/android/src/main/java/io/flutter/plugins/imagepicker/ImagePickerPlugin.java
@@ -10,13 +10,86 @@
import android.os.Environment;
import android.os.Handler;
import android.os.Looper;
+import androidx.annotation.NonNull;
import androidx.annotation.VisibleForTesting;
+import androidx.lifecycle.DefaultLifecycleObserver;
+import androidx.lifecycle.Lifecycle;
+import androidx.lifecycle.LifecycleOwner;
+import io.flutter.embedding.engine.plugins.FlutterPlugin;
+import io.flutter.embedding.engine.plugins.activity.ActivityAware;
+import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding;
+import io.flutter.embedding.engine.plugins.lifecycle.FlutterLifecycleAdapter;
+import io.flutter.plugin.common.BinaryMessenger;
import io.flutter.plugin.common.MethodCall;
import io.flutter.plugin.common.MethodChannel;
import io.flutter.plugin.common.PluginRegistry;
import java.io.File;
-public class ImagePickerPlugin implements MethodChannel.MethodCallHandler {
+@SuppressWarnings("deprecation")
+public class ImagePickerPlugin
+ implements MethodChannel.MethodCallHandler, FlutterPlugin, ActivityAware {
+
+ private class LifeCycleObserver
+ implements Application.ActivityLifecycleCallbacks, DefaultLifecycleObserver {
+ private final Activity thisActivity;
+
+ LifeCycleObserver(Activity activity) {
+ this.thisActivity = activity;
+ }
+
+ @Override
+ public void onCreate(@NonNull LifecycleOwner owner) {}
+
+ @Override
+ public void onStart(@NonNull LifecycleOwner owner) {}
+
+ @Override
+ public void onResume(@NonNull LifecycleOwner owner) {}
+
+ @Override
+ public void onPause(@NonNull LifecycleOwner owner) {}
+
+ @Override
+ public void onStop(@NonNull LifecycleOwner owner) {
+ onActivityStopped(thisActivity);
+ }
+
+ @Override
+ public void onDestroy(@NonNull LifecycleOwner owner) {
+ onActivityDestroyed(thisActivity);
+ }
+
+ @Override
+ public void onActivityCreated(Activity activity, Bundle savedInstanceState) {}
+
+ @Override
+ public void onActivityStarted(Activity activity) {}
+
+ @Override
+ public void onActivityResumed(Activity activity) {}
+
+ @Override
+ public void onActivityPaused(Activity activity) {}
+
+ @Override
+ public void onActivitySaveInstanceState(Activity activity, Bundle outState) {}
+
+ @Override
+ public void onActivityDestroyed(Activity activity) {
+ if (thisActivity == activity && activity.getApplicationContext() != null) {
+ ((Application) activity.getApplicationContext())
+ .unregisterActivityLifecycleCallbacks(
+ this); // Use getApplicationContext() to avoid casting failures
+ }
+ }
+
+ @Override
+ public void onActivityStopped(Activity activity) {
+ if (thisActivity == activity) {
+ delegate.saveStateBeforeResult();
+ }
+ }
+ }
static final String METHOD_CALL_IMAGE = "pickImage";
static final String METHOD_CALL_VIDEO = "pickVideo";
@@ -27,9 +100,15 @@
private static final int SOURCE_CAMERA = 0;
private static final int SOURCE_GALLERY = 1;
- private final PluginRegistry.Registrar registrar;
+ private MethodChannel channel;
private ImagePickerDelegate delegate;
- private Application.ActivityLifecycleCallbacks activityLifecycleCallbacks;
+ private FlutterPluginBinding pluginBinding;
+ private ActivityPluginBinding activityBinding;
+ private Application application;
+ private Activity activity;
+ // This is null when not using v2 embedding;
+ private Lifecycle lifecycle;
+ private LifeCycleObserver observer;
public static void registerWith(PluginRegistry.Registrar registrar) {
if (registrar.activity() == null) {
@@ -37,72 +116,114 @@
// we stop the registering process immediately because the ImagePicker requires an activity.
return;
}
- final ImagePickerCache cache = new ImagePickerCache(registrar.activity());
-
- final MethodChannel channel = new MethodChannel(registrar.messenger(), CHANNEL);
-
- final File externalFilesDirectory =
- registrar.activity().getExternalFilesDir(Environment.DIRECTORY_PICTURES);
- final ExifDataCopier exifDataCopier = new ExifDataCopier();
- final ImageResizer imageResizer = new ImageResizer(externalFilesDirectory, exifDataCopier);
- final ImagePickerDelegate delegate =
- new ImagePickerDelegate(registrar.activity(), externalFilesDirectory, imageResizer, cache);
-
- registrar.addActivityResultListener(delegate);
- registrar.addRequestPermissionsResultListener(delegate);
- final ImagePickerPlugin instance = new ImagePickerPlugin(registrar, delegate);
- channel.setMethodCallHandler(instance);
+ Activity activity = registrar.activity();
+ Application application = null;
+ if (registrar.context() != null) {
+ application = (Application) (registrar.context().getApplicationContext());
+ }
+ ImagePickerPlugin plugin = new ImagePickerPlugin();
+ plugin.setup(registrar.messenger(), application, activity, registrar, null);
}
+ /**
+ * Default constructor for the plugin.
+ *
+ * <p>Use this constructor for production code.
+ */
+ // See also: * {@link #ImagePickerPlugin(ImagePickerDelegate, Activity)} for testing.
+ public ImagePickerPlugin() {}
+
@VisibleForTesting
- ImagePickerPlugin(final PluginRegistry.Registrar registrar, final ImagePickerDelegate delegate) {
- this.registrar = registrar;
+ ImagePickerPlugin(final ImagePickerDelegate delegate, final Activity activity) {
this.delegate = delegate;
- this.activityLifecycleCallbacks =
- new Application.ActivityLifecycleCallbacks() {
- @Override
- public void onActivityCreated(Activity activity, Bundle savedInstanceState) {}
+ this.activity = activity;
+ }
- @Override
- public void onActivityStarted(Activity activity) {}
+ @Override
+ public void onAttachedToEngine(FlutterPluginBinding binding) {
+ pluginBinding = binding;
+ }
- @Override
- public void onActivityResumed(Activity activity) {}
+ @Override
+ public void onDetachedFromEngine(FlutterPluginBinding binding) {
+ pluginBinding = null;
+ }
- @Override
- public void onActivityPaused(Activity activity) {}
+ @Override
+ public void onAttachedToActivity(ActivityPluginBinding binding) {
+ activityBinding = binding;
+ setup(
+ pluginBinding.getBinaryMessenger(),
+ (Application) pluginBinding.getApplicationContext(),
+ activityBinding.getActivity(),
+ null,
+ activityBinding);
+ }
- @Override
- public void onActivitySaveInstanceState(Activity activity, Bundle outState) {
- if (activity == registrar.activity()) {
- delegate.saveStateBeforeResult();
- }
- }
+ @Override
+ public void onDetachedFromActivity() {
+ tearDown();
+ }
- @Override
- public void onActivityDestroyed(Activity activity) {
- if (activity == registrar.activity()
- && registrar.activity().getApplicationContext() != null) {
- ((Application) registrar.activity().getApplicationContext())
- .unregisterActivityLifecycleCallbacks(
- this); // Use getApplicationContext() to avoid casting failures
- }
- }
+ @Override
+ public void onDetachedFromActivityForConfigChanges() {
+ onDetachedFromActivity();
+ }
- @Override
- public void onActivityStopped(Activity activity) {}
- };
+ @Override
+ public void onReattachedToActivityForConfigChanges(ActivityPluginBinding binding) {
+ onAttachedToActivity(binding);
+ }
- if (this.registrar != null
- && this.registrar.context() != null
- && this.registrar.context().getApplicationContext() != null) {
- ((Application) this.registrar.context().getApplicationContext())
- .registerActivityLifecycleCallbacks(
- this
- .activityLifecycleCallbacks); // Use getApplicationContext() to avoid casting failures.
+ private void setup(
+ final BinaryMessenger messenger,
+ final Application application,
+ final Activity activity,
+ final PluginRegistry.Registrar registrar,
+ final ActivityPluginBinding activityBinding) {
+ this.activity = activity;
+ this.application = application;
+ this.delegate = constructDelegate(activity);
+ channel = new MethodChannel(messenger, CHANNEL);
+ channel.setMethodCallHandler(this);
+ observer = new LifeCycleObserver(activity);
+ if (registrar != null) {
+ // V1 embedding setup for activity listeners.
+ application.registerActivityLifecycleCallbacks(observer);
+ registrar.addActivityResultListener(delegate);
+ registrar.addRequestPermissionsResultListener(delegate);
+ } else {
+ // V2 embedding setup for activity listeners.
+ activityBinding.addActivityResultListener(delegate);
+ activityBinding.addRequestPermissionsResultListener(delegate);
+ lifecycle = FlutterLifecycleAdapter.getActivityLifecycle(activityBinding);
+ lifecycle.addObserver(observer);
}
}
+ private void tearDown() {
+ activityBinding.removeActivityResultListener(delegate);
+ activityBinding.removeRequestPermissionsResultListener(delegate);
+ activityBinding = null;
+ lifecycle.removeObserver(observer);
+ lifecycle = null;
+ delegate = null;
+ channel.setMethodCallHandler(null);
+ channel = null;
+ application.unregisterActivityLifecycleCallbacks(observer);
+ application = null;
+ }
+
+ private final ImagePickerDelegate constructDelegate(final Activity setupActivity) {
+ final ImagePickerCache cache = new ImagePickerCache(setupActivity);
+
+ final File externalFilesDirectory =
+ setupActivity.getExternalFilesDir(Environment.DIRECTORY_PICTURES);
+ final ExifDataCopier exifDataCopier = new ExifDataCopier();
+ final ImageResizer imageResizer = new ImageResizer(externalFilesDirectory, exifDataCopier);
+ return new ImagePickerDelegate(setupActivity, externalFilesDirectory, imageResizer, cache);
+ }
+
// MethodChannel.Result wrapper that responds on the platform thread.
private static class MethodResultWrapper implements MethodChannel.Result {
private MethodChannel.Result methodResult;
@@ -150,7 +271,7 @@
@Override
public void onMethodCall(MethodCall call, MethodChannel.Result rawResult) {
- if (registrar.activity() == null) {
+ if (activity == null) {
rawResult.error("no_activity", "image_picker plugin requires a foreground activity.", null);
return;
}
diff --git a/packages/image_picker/example/android/app/src/main/AndroidManifest.xml b/packages/image_picker/example/android/app/src/main/AndroidManifest.xml
index fa2b500..5de9f04 100755
--- a/packages/image_picker/example/android/app/src/main/AndroidManifest.xml
+++ b/packages/image_picker/example/android/app/src/main/AndroidManifest.xml
@@ -4,8 +4,7 @@
<uses-permission android:name="android.permission.INTERNET"/>
<application android:name="io.flutter.app.FlutterApplication" android:label="Image Picker Example" android:icon="@mipmap/ic_launcher">
- <activity android:name=".MainActivity"
- android:launchMode="singleTop"
+ <activity android:name="io.flutter.embedding.android.FlutterActivity"
android:theme="@android:style/Theme.Black.NoTitleBar"
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|locale|layoutDirection"
android:hardwareAccelerated="true"
@@ -15,5 +14,12 @@
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>
+ <activity
+ android:name=".EmbeddingV1Activity"
+ android:theme="@android:style/Theme.Black.NoTitleBar"
+ android:configChanges="orientation|keyboardHidden|keyboard|screenSize|locale|layoutDirection"
+ android:hardwareAccelerated="true"
+ android:windowSoftInputMode="adjustResize">
+ </activity>
</application>
</manifest>
diff --git a/packages/image_picker/example/android/app/src/main/java/io/flutter/plugins/imagepickerexample/MainActivity.java b/packages/image_picker/example/android/app/src/main/java/io/flutter/plugins/imagepickerexample/EmbeddingV1Activity.java
similarity index 78%
rename from packages/image_picker/example/android/app/src/main/java/io/flutter/plugins/imagepickerexample/MainActivity.java
rename to packages/image_picker/example/android/app/src/main/java/io/flutter/plugins/imagepickerexample/EmbeddingV1Activity.java
index 4690ebc..79c1ca6 100644
--- a/packages/image_picker/example/android/app/src/main/java/io/flutter/plugins/imagepickerexample/MainActivity.java
+++ b/packages/image_picker/example/android/app/src/main/java/io/flutter/plugins/imagepickerexample/EmbeddingV1Activity.java
@@ -1,4 +1,4 @@
-// Copyright 2017 The Chromium Authors. All rights reserved.
+// Copyright 2019 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.
@@ -8,8 +8,7 @@
import io.flutter.app.FlutterActivity;
import io.flutter.plugins.GeneratedPluginRegistrant;
-public class MainActivity extends FlutterActivity {
-
+public class EmbeddingV1Activity extends FlutterActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
diff --git a/packages/image_picker/example/android/app/src/main/java/io/flutter/plugins/imagepickerexample/EmbeddingV1ActivityTest.java b/packages/image_picker/example/android/app/src/main/java/io/flutter/plugins/imagepickerexample/EmbeddingV1ActivityTest.java
new file mode 100644
index 0000000..924cf2f
--- /dev/null
+++ b/packages/image_picker/example/android/app/src/main/java/io/flutter/plugins/imagepickerexample/EmbeddingV1ActivityTest.java
@@ -0,0 +1,17 @@
+// Copyright 2019 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.
+
+package io.flutter.plugins.imagepickerexample;
+
+import androidx.test.rule.ActivityTestRule;
+import dev.flutter.plugins.e2e.FlutterRunner;
+import org.junit.Rule;
+import org.junit.runner.RunWith;
+
+@RunWith(FlutterRunner.class)
+public class EmbeddingV1ActivityTest {
+ @Rule
+ public ActivityTestRule<EmbeddingV1Activity> rule =
+ new ActivityTestRule<>(EmbeddingV1Activity.class);
+}
diff --git a/packages/image_picker/example/android/app/src/main/java/io/flutter/plugins/imagepickerexample/FlutterActivityTest.java b/packages/image_picker/example/android/app/src/main/java/io/flutter/plugins/imagepickerexample/FlutterActivityTest.java
new file mode 100644
index 0000000..df9794c
--- /dev/null
+++ b/packages/image_picker/example/android/app/src/main/java/io/flutter/plugins/imagepickerexample/FlutterActivityTest.java
@@ -0,0 +1,13 @@
+package io.flutter.plugins.imagepickerexample;
+
+import androidx.test.rule.ActivityTestRule;
+import dev.flutter.plugins.e2e.FlutterRunner;
+import io.flutter.embedding.android.FlutterActivity;
+import org.junit.Rule;
+import org.junit.runner.RunWith;
+
+@RunWith(FlutterRunner.class)
+public class FlutterActivityTest {
+ @Rule
+ public ActivityTestRule<FlutterActivity> rule = new ActivityTestRule<>(FlutterActivity.class);
+}
diff --git a/packages/image_picker/example/android/app/src/test/java/io/flutter/plugins/imagepicker/ImagePickerPluginTest.java b/packages/image_picker/example/android/app/src/test/java/io/flutter/plugins/imagepicker/ImagePickerPluginTest.java
index 94a81d3..acfd064 100644
--- a/packages/image_picker/example/android/app/src/test/java/io/flutter/plugins/imagepicker/ImagePickerPluginTest.java
+++ b/packages/image_picker/example/android/app/src/test/java/io/flutter/plugins/imagepicker/ImagePickerPluginTest.java
@@ -40,16 +40,15 @@
MockitoAnnotations.initMocks(this);
when(mockRegistrar.context()).thenReturn(mockApplication);
- plugin = new ImagePickerPlugin(mockRegistrar, mockImagePickerDelegate);
+ plugin = new ImagePickerPlugin(mockImagePickerDelegate, mockActivity);
}
@Test
public void onMethodCall_WhenActivityIsNull_FinishesWithForegroundActivityRequiredError() {
- when(mockRegistrar.activity()).thenReturn(null);
MethodCall call = buildMethodCall(SOURCE_GALLERY);
-
- plugin.onMethodCall(call, mockResult);
-
+ ImagePickerPlugin imagePickerPluginWithNullActivity =
+ new ImagePickerPlugin(mockImagePickerDelegate, null);
+ imagePickerPluginWithNullActivity.onMethodCall(call, mockResult);
verify(mockResult)
.error("no_activity", "image_picker plugin requires a foreground activity.", null);
verifyZeroInteractions(mockImagePickerDelegate);
@@ -57,46 +56,34 @@
@Test
public void onMethodCall_WhenCalledWithUnknownMethod_ThrowsException() {
- when(mockRegistrar.activity()).thenReturn(mockActivity);
exception.expect(IllegalArgumentException.class);
exception.expectMessage("Unknown method test");
-
plugin.onMethodCall(new MethodCall("test", null), mockResult);
-
verifyZeroInteractions(mockImagePickerDelegate);
verifyZeroInteractions(mockResult);
}
@Test
public void onMethodCall_WhenCalledWithUnknownImageSource_ThrowsException() {
- when(mockRegistrar.activity()).thenReturn(mockActivity);
exception.expect(IllegalArgumentException.class);
exception.expectMessage("Invalid image source: -1");
-
plugin.onMethodCall(buildMethodCall(-1), mockResult);
-
verifyZeroInteractions(mockImagePickerDelegate);
verifyZeroInteractions(mockResult);
}
@Test
public void onMethodCall_WhenSourceIsGallery_InvokesChooseImageFromGallery() {
- when(mockRegistrar.activity()).thenReturn(mockActivity);
MethodCall call = buildMethodCall(SOURCE_GALLERY);
-
plugin.onMethodCall(call, mockResult);
-
verify(mockImagePickerDelegate).chooseImageFromGallery(eq(call), any());
verifyZeroInteractions(mockResult);
}
@Test
public void onMethodCall_WhenSourceIsCamera_InvokesTakeImageWithCamera() {
- when(mockRegistrar.activity()).thenReturn(mockActivity);
MethodCall call = buildMethodCall(SOURCE_CAMERA);
-
plugin.onMethodCall(call, mockResult);
-
verify(mockImagePickerDelegate).takeImageWithCamera(eq(call), any());
verifyZeroInteractions(mockResult);
}
@@ -111,8 +98,7 @@
@Test
public void onConstructor_WhenContextTypeIsActivity_ShouldNotCrash() {
- when(mockRegistrar.context()).thenReturn(mockActivity);
- new ImagePickerPlugin(mockRegistrar, mockImagePickerDelegate);
+ new ImagePickerPlugin(mockImagePickerDelegate, mockActivity);
assertTrue(
"No exception thrown when ImagePickerPlugin() ran with context instanceof Activity", true);
}
diff --git a/packages/image_picker/example/lib/main.dart b/packages/image_picker/example/lib/main.dart
index 3ece64b..919c838 100755
--- a/packages/image_picker/example/lib/main.dart
+++ b/packages/image_picker/example/lib/main.dart
@@ -305,13 +305,13 @@
FlatButton(
child: const Text('PICK'),
onPressed: () {
- double width = maxWidthController.text.length > 0
+ double width = maxWidthController.text.isNotEmpty
? double.parse(maxWidthController.text)
: null;
- double height = maxHeightController.text.length > 0
+ double height = maxHeightController.text.isNotEmpty
? double.parse(maxHeightController.text)
: null;
- int quality = qualityController.text.length > 0
+ int quality = qualityController.text.isNotEmpty
? int.parse(qualityController.text)
: null;
onPick(width, height, quality);
diff --git a/packages/image_picker/example/pubspec.yaml b/packages/image_picker/example/pubspec.yaml
index 1f793d7..b84ee9f 100755
--- a/packages/image_picker/example/pubspec.yaml
+++ b/packages/image_picker/example/pubspec.yaml
@@ -3,12 +3,22 @@
author: Flutter Team <flutter-dev@googlegroups.com>
dependencies:
- video_player: 0.10.1+5
+ video_player: ^0.10.3
flutter:
sdk: flutter
image_picker:
path: ../
+ flutter_plugin_android_lifecycle: ^1.0.2
+
+dev_dependencies:
+ flutter_driver:
+ sdk: flutter
+ e2e: ^0.2.1
flutter:
uses-material-design: true
+environment:
+ sdk: ">=2.0.0-dev.28.0 <3.0.0"
+ flutter: ">=1.10.0 <2.0.0"
+
diff --git a/packages/image_picker/example/test_driver/test/image_picker_e2e_test.dart b/packages/image_picker/example/test_driver/test/image_picker_e2e_test.dart
new file mode 100644
index 0000000..f3aa9e2
--- /dev/null
+++ b/packages/image_picker/example/test_driver/test/image_picker_e2e_test.dart
@@ -0,0 +1,15 @@
+// Copyright 2019, the Chromium project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+import 'dart:async';
+import 'dart:io';
+import 'package:flutter_driver/flutter_driver.dart';
+
+Future<void> main() async {
+ final FlutterDriver driver = await FlutterDriver.connect();
+ final String result =
+ await driver.requestData(null, timeout: const Duration(minutes: 1));
+ await driver.close();
+ exit(result == 'pass' ? 0 : 1);
+}
diff --git a/packages/image_picker/pubspec.yaml b/packages/image_picker/pubspec.yaml
index 217764c..5b88c71 100755
--- a/packages/image_picker/pubspec.yaml
+++ b/packages/image_picker/pubspec.yaml
@@ -2,7 +2,7 @@
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
-version: 0.6.2+3
+version: 0.6.3
flutter:
plugin:
@@ -16,11 +16,13 @@
dependencies:
flutter:
sdk: flutter
+ flutter_plugin_android_lifecycle: ^1.0.2
dev_dependencies:
- video_player: 0.10.1+5
+ video_player: ^0.10.3
flutter_test:
sdk: flutter
+ e2e: ^0.2.1
environment:
sdk: ">=2.0.0-dev.28.0 <3.0.0"
diff --git a/packages/image_picker/test/image_picker_e2e.dart b/packages/image_picker/test/image_picker_e2e.dart
new file mode 100644
index 0000000..b19e37d
--- /dev/null
+++ b/packages/image_picker/test/image_picker_e2e.dart
@@ -0,0 +1,5 @@
+import 'package:e2e/e2e.dart';
+
+void main() {
+ E2EWidgetsFlutterBinding.ensureInitialized();
+}