blob: 41ffa23853c7c08f48f1cb5ae4b4701f58a9e1b5 [file] [log] [blame]
// Copyright 2013 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.plugin.platform;
import android.annotation.TargetApi;
import android.app.Activity;
import android.app.ActivityManager.TaskDescription;
import android.content.ClipData;
import android.content.ClipDescription;
import android.content.ClipboardManager;
import android.content.Context;
import android.os.Build;
import android.view.HapticFeedbackConstants;
import android.view.SoundEffectConstants;
import android.view.View;
import android.view.Window;
import android.view.WindowManager;
import androidx.activity.OnBackPressedDispatcherOwner;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
import androidx.core.view.WindowInsetsControllerCompat;
import io.flutter.Log;
import io.flutter.embedding.engine.systemchannels.PlatformChannel;
import java.io.FileNotFoundException;
import java.util.List;
/** Android implementation of the platform plugin. */
public class PlatformPlugin {
public static final int DEFAULT_SYSTEM_UI =
View.SYSTEM_UI_FLAG_LAYOUT_STABLE | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN;
private final Activity activity;
private final PlatformChannel platformChannel;
private final PlatformPluginDelegate platformPluginDelegate;
private PlatformChannel.SystemChromeStyle currentTheme;
private int mEnabledOverlays;
private static final String TAG = "PlatformPlugin";
/**
* The {@link PlatformPlugin} generally has default behaviors implemented for platform
* functionalities requested by the Flutter framework. However, functionalities exposed through
* this interface could be customized by the more public-facing APIs that implement this interface
* such as the {@link io.flutter.embedding.android.FlutterActivity} or the {@link
* io.flutter.embedding.android.FlutterFragment}.
*/
public interface PlatformPluginDelegate {
/**
* Allow implementer to customize the behavior needed when the Flutter framework calls to pop
* the Android-side navigation stack.
*
* @return true if the implementation consumed the pop signal. If false, a default behavior of
* finishing the activity or sending the signal to {@link
* androidx.activity.OnBackPressedDispatcher} will be executed.
*/
boolean popSystemNavigator();
/**
* The Flutter application would or would not like to handle navigation pop events itself.
*
* <p>Relevant for registering and unregistering the app's OnBackInvokedCallback for the
* Predictive Back feature, for example as in {@link
* io.flutter.embedding.android.FlutterActivity}.
*/
default void setFrameworkHandlesBack(boolean frameworkHandlesBack) {}
}
@VisibleForTesting
final PlatformChannel.PlatformMessageHandler mPlatformMessageHandler =
new PlatformChannel.PlatformMessageHandler() {
@Override
public void playSystemSound(@NonNull PlatformChannel.SoundType soundType) {
PlatformPlugin.this.playSystemSound(soundType);
}
@Override
public void vibrateHapticFeedback(
@NonNull PlatformChannel.HapticFeedbackType feedbackType) {
PlatformPlugin.this.vibrateHapticFeedback(feedbackType);
}
@Override
public void setPreferredOrientations(int androidOrientation) {
setSystemChromePreferredOrientations(androidOrientation);
}
@Override
public void setApplicationSwitcherDescription(
@NonNull PlatformChannel.AppSwitcherDescription description) {
setSystemChromeApplicationSwitcherDescription(description);
}
@Override
public void showSystemOverlays(@NonNull List<PlatformChannel.SystemUiOverlay> overlays) {
setSystemChromeEnabledSystemUIOverlays(overlays);
}
@Override
public void showSystemUiMode(@NonNull PlatformChannel.SystemUiMode mode) {
setSystemChromeEnabledSystemUIMode(mode);
}
@Override
public void setSystemUiChangeListener() {
setSystemChromeChangeListener();
}
@Override
public void restoreSystemUiOverlays() {
restoreSystemChromeSystemUIOverlays();
}
@Override
public void setSystemUiOverlayStyle(
@NonNull PlatformChannel.SystemChromeStyle systemUiOverlayStyle) {
setSystemChromeSystemUIOverlayStyle(systemUiOverlayStyle);
}
@Override
public void setFrameworkHandlesBack(boolean frameworkHandlesBack) {
PlatformPlugin.this.setFrameworkHandlesBack(frameworkHandlesBack);
}
@Override
public void popSystemNavigator() {
PlatformPlugin.this.popSystemNavigator();
}
@Override
public CharSequence getClipboardData(
@Nullable PlatformChannel.ClipboardContentFormat format) {
return PlatformPlugin.this.getClipboardData(format);
}
@Override
public void setClipboardData(@NonNull String text) {
PlatformPlugin.this.setClipboardData(text);
}
@Override
public boolean clipboardHasStrings() {
return PlatformPlugin.this.clipboardHasStrings();
}
};
public PlatformPlugin(@NonNull Activity activity, @NonNull PlatformChannel platformChannel) {
this(activity, platformChannel, null);
}
public PlatformPlugin(
@NonNull Activity activity,
@NonNull PlatformChannel platformChannel,
@NonNull PlatformPluginDelegate delegate) {
this.activity = activity;
this.platformChannel = platformChannel;
this.platformChannel.setPlatformMessageHandler(mPlatformMessageHandler);
this.platformPluginDelegate = delegate;
mEnabledOverlays = DEFAULT_SYSTEM_UI;
}
/**
* Releases all resources held by this {@code PlatformPlugin}.
*
* <p>Do not invoke any methods on a {@code PlatformPlugin} after invoking this method.
*/
public void destroy() {
this.platformChannel.setPlatformMessageHandler(null);
}
private void playSystemSound(@NonNull PlatformChannel.SoundType soundType) {
if (soundType == PlatformChannel.SoundType.CLICK) {
View view = activity.getWindow().getDecorView();
view.playSoundEffect(SoundEffectConstants.CLICK);
}
}
@VisibleForTesting
/* package */ void vibrateHapticFeedback(
@NonNull PlatformChannel.HapticFeedbackType feedbackType) {
View view = activity.getWindow().getDecorView();
switch (feedbackType) {
case STANDARD:
view.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS);
break;
case LIGHT_IMPACT:
view.performHapticFeedback(HapticFeedbackConstants.VIRTUAL_KEY);
break;
case MEDIUM_IMPACT:
view.performHapticFeedback(HapticFeedbackConstants.KEYBOARD_TAP);
break;
case HEAVY_IMPACT:
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
view.performHapticFeedback(HapticFeedbackConstants.CONTEXT_CLICK);
}
break;
case SELECTION_CLICK:
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
view.performHapticFeedback(HapticFeedbackConstants.CLOCK_TICK);
}
break;
}
}
private void setSystemChromePreferredOrientations(int androidOrientation) {
activity.setRequestedOrientation(androidOrientation);
}
@SuppressWarnings("deprecation")
private void setSystemChromeApplicationSwitcherDescription(
PlatformChannel.AppSwitcherDescription description) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
return;
}
// Linter refuses to believe we're only executing this code in API 28 unless we
// use distinct if
// blocks and
// hardcode the API 28 constant.
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.P
&& Build.VERSION.SDK_INT > Build.VERSION_CODES.LOLLIPOP) {
activity.setTaskDescription(
new TaskDescription(description.label, /* icon= */ null, description.color));
}
if (Build.VERSION.SDK_INT >= 28) {
TaskDescription taskDescription =
new TaskDescription(description.label, 0, description.color);
activity.setTaskDescription(taskDescription);
}
}
private void setSystemChromeChangeListener() {
// Set up a listener to notify the framework when the system ui has changed.
View decorView = activity.getWindow().getDecorView();
decorView.setOnSystemUiVisibilityChangeListener(
new View.OnSystemUiVisibilityChangeListener() {
@Override
public void onSystemUiVisibilityChange(int visibility) {
// `platformChannel.systemChromeChanged` may trigger a callback that eventually results
// in a call to `setSystemUiVisibility`.
// `setSystemUiVisibility` must not be called in the same frame as when
// `onSystemUiVisibilityChange` is received though.
//
// As such, post `platformChannel.systemChromeChanged` to the view handler to ensure
// that downstream callbacks are trigged on the next frame.
decorView.post(
() -> {
if ((visibility & View.SYSTEM_UI_FLAG_FULLSCREEN) == 0) {
// The system bars are visible. Make any desired adjustments to
// your UI, such as showing the action bar or other navigational
// controls. Another common action is to set a timer to dismiss
// the system bars and restore the fullscreen mode that was
// previously enabled.
platformChannel.systemChromeChanged(true);
} else {
// The system bars are NOT visible. Make any desired adjustments
// to your UI, such as hiding the action bar or other
// navigational controls.
platformChannel.systemChromeChanged(false);
}
});
}
});
}
private void setSystemChromeEnabledSystemUIMode(PlatformChannel.SystemUiMode systemUiMode) {
int enabledOverlays;
if (systemUiMode == PlatformChannel.SystemUiMode.LEAN_BACK) {
// LEAN BACK
// Available starting at SDK 16
// Should not show overlays, tap to reveal overlays, needs onChange callback
// When the overlays come in on tap, the app does not receive the gesture and does not know
// the system overlay has changed. The overlays cannot be dismissed, so adding the callback
// support will allow users to restore the system ui and dismiss the overlays.
// Not compatible with top/bottom overlays enabled.
enabledOverlays =
View.SYSTEM_UI_FLAG_LAYOUT_STABLE
| View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
| View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
| View.SYSTEM_UI_FLAG_HIDE_NAVIGATION
| View.SYSTEM_UI_FLAG_FULLSCREEN;
} else if (systemUiMode == PlatformChannel.SystemUiMode.IMMERSIVE
&& Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
// IMMERSIVE
// Available starting at 19
// Should not show overlays, swipe from edges to reveal overlays, needs onChange callback
// When the overlays come in on swipe, the app does not receive the gesture and does not know
// the system overlay has changed. The overlays cannot be dismissed, so adding callback
// support will allow users to restore the system ui and dismiss the overlays.
// Not compatible with top/bottom overlays enabled.
enabledOverlays =
View.SYSTEM_UI_FLAG_IMMERSIVE
| View.SYSTEM_UI_FLAG_LAYOUT_STABLE
| View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
| View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
| View.SYSTEM_UI_FLAG_HIDE_NAVIGATION
| View.SYSTEM_UI_FLAG_FULLSCREEN;
} else if (systemUiMode == PlatformChannel.SystemUiMode.IMMERSIVE_STICKY
&& Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
// STICKY IMMERSIVE
// Available starting at 19
// Should not show overlays, swipe from edges to reveal overlays. The app will also receive
// the swipe gesture. The overlays cannot be dismissed, so adding callback support will
// allow users to restore the system ui and dismiss the overlays.
// Not compatible with top/bottom overlays enabled.
enabledOverlays =
View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY
| View.SYSTEM_UI_FLAG_LAYOUT_STABLE
| View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
| View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
| View.SYSTEM_UI_FLAG_HIDE_NAVIGATION
| View.SYSTEM_UI_FLAG_FULLSCREEN;
} else if (systemUiMode == PlatformChannel.SystemUiMode.EDGE_TO_EDGE
&& Build.VERSION.SDK_INT >= 29) {
// EDGE TO EDGE
// Available starting at 29
// SDK 29 and up will apply a translucent body scrim behind 2/3 button navigation bars
// to ensure contrast with buttons on the nav and status bars, unless the contrast is not
// enforced in the overlay styling.
enabledOverlays =
View.SYSTEM_UI_FLAG_LAYOUT_STABLE
| View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
| View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN;
} else {
// When none of the conditions are matched, return without updating the system UI overlays.
return;
}
mEnabledOverlays = enabledOverlays;
updateSystemUiOverlays();
}
private void setSystemChromeEnabledSystemUIOverlays(
List<PlatformChannel.SystemUiOverlay> overlaysToShow) {
// Start by assuming we want to hide all system overlays (like an immersive
// game).
int enabledOverlays =
DEFAULT_SYSTEM_UI
| View.SYSTEM_UI_FLAG_FULLSCREEN
| View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
| View.SYSTEM_UI_FLAG_HIDE_NAVIGATION;
// The SYSTEM_UI_FLAG_IMMERSIVE_STICKY flag was introduced in API 19, so we
// apply it
// if desired, and if the current Android version is 19 or greater.
if (overlaysToShow.size() == 0 && Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
enabledOverlays |= View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY;
}
// Re-add any desired system overlays.
for (int i = 0; i < overlaysToShow.size(); ++i) {
PlatformChannel.SystemUiOverlay overlayToShow = overlaysToShow.get(i);
switch (overlayToShow) {
case TOP_OVERLAYS:
enabledOverlays &= ~View.SYSTEM_UI_FLAG_FULLSCREEN;
break;
case BOTTOM_OVERLAYS:
enabledOverlays &= ~View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION;
enabledOverlays &= ~View.SYSTEM_UI_FLAG_HIDE_NAVIGATION;
break;
}
}
mEnabledOverlays = enabledOverlays;
updateSystemUiOverlays();
}
/**
* Refreshes Android's window system UI (AKA system chrome) to match Flutter's desired {@link
* PlatformChannel.SystemChromeStyle}.
*
* <p>Updating the system UI Overlays is accomplished by altering the decor view of the {@link
* Window} associated with the {@link android.app.Activity} that was provided to this {@code
* PlatformPlugin}.
*/
public void updateSystemUiOverlays() {
activity.getWindow().getDecorView().setSystemUiVisibility(mEnabledOverlays);
if (currentTheme != null) {
setSystemChromeSystemUIOverlayStyle(currentTheme);
}
}
private void restoreSystemChromeSystemUIOverlays() {
updateSystemUiOverlays();
}
@SuppressWarnings("deprecation")
@TargetApi(21)
private void setSystemChromeSystemUIOverlayStyle(
PlatformChannel.SystemChromeStyle systemChromeStyle) {
Window window = activity.getWindow();
View view = window.getDecorView();
WindowInsetsControllerCompat windowInsetsControllerCompat =
new WindowInsetsControllerCompat(window, view);
if (Build.VERSION.SDK_INT < 30) {
// Flag set to specify that this window is responsible for drawing the background for the
// system bars. Must be set for all operations on API < 30 excluding enforcing system
// bar contrasts. Deprecated in API 30.
window.addFlags(WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS);
// Flag set to dismiss any requests for translucent system bars to be provided in lieu of what
// is specified by systemChromeStyle. Must be set for all operations on API < 30 operations
// excluding enforcing system bar contrasts. Deprecated in API 30.
window.clearFlags(
WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS
| WindowManager.LayoutParams.FLAG_TRANSLUCENT_NAVIGATION);
}
// SYSTEM STATUS BAR -------------------------------------------------------------------
// You can't change the color of the system status bar until SDK 21, and you can't change the
// color of the status icons until SDK 23. We only allow both starting at 23 to ensure buttons
// and icons can be visible when changing the background color.
// If transparent, SDK 29 and higher may apply a translucent scrim behind the bar to ensure
// proper contrast. This can be overridden with
// SystemChromeStyle.systemStatusBarContrastEnforced.
if (Build.VERSION.SDK_INT >= 23) {
if (systemChromeStyle.statusBarIconBrightness != null) {
switch (systemChromeStyle.statusBarIconBrightness) {
case DARK:
// Dark status bar icon brightness.
// Light status bar appearance.
windowInsetsControllerCompat.setAppearanceLightStatusBars(true);
break;
case LIGHT:
// Light status bar icon brightness.
// Dark status bar appearance.
windowInsetsControllerCompat.setAppearanceLightStatusBars(false);
break;
}
}
if (systemChromeStyle.statusBarColor != null) {
window.setStatusBarColor(systemChromeStyle.statusBarColor);
}
}
// You can't override the enforced contrast for a transparent status bar until SDK 29.
// This overrides the translucent scrim that may be placed behind the bar on SDK 29+ to ensure
// contrast is appropriate when using full screen layout modes like Edge to Edge.
if (systemChromeStyle.systemStatusBarContrastEnforced != null && Build.VERSION.SDK_INT >= 29) {
window.setStatusBarContrastEnforced(systemChromeStyle.systemStatusBarContrastEnforced);
}
// SYSTEM NAVIGATION BAR --------------------------------------------------------------
// You can't change the color of the system navigation bar until SDK 21, and you can't change
// the color of the navigation buttons until SDK 26. We only allow both starting at 26 to
// ensure buttons can be visible when changing the background color.
// If transparent, SDK 29 and higher may apply a translucent scrim behind 2/3 button navigation
// bars to ensure proper contrast. This can be overridden with
// SystemChromeStyle.systemNavigationBarContrastEnforced.
if (Build.VERSION.SDK_INT >= 26) {
if (systemChromeStyle.systemNavigationBarIconBrightness != null) {
switch (systemChromeStyle.systemNavigationBarIconBrightness) {
case DARK:
// Dark navigation bar icon brightness.
// Light navigation bar appearance.
windowInsetsControllerCompat.setAppearanceLightNavigationBars(true);
break;
case LIGHT:
// Light navigation bar icon brightness.
// Dark navigation bar appearance.
windowInsetsControllerCompat.setAppearanceLightNavigationBars(false);
break;
}
}
if (systemChromeStyle.systemNavigationBarColor != null) {
window.setNavigationBarColor(systemChromeStyle.systemNavigationBarColor);
}
}
// You can't change the color of the navigation bar divider color until SDK 28.
if (systemChromeStyle.systemNavigationBarDividerColor != null && Build.VERSION.SDK_INT >= 28) {
window.setNavigationBarDividerColor(systemChromeStyle.systemNavigationBarDividerColor);
}
// You can't override the enforced contrast for a transparent navigation bar until SDK 29.
// This overrides the translucent scrim that may be placed behind 2/3 button navigation bars on
// SDK 29+ to ensure contrast is appropriate when using full screen layout modes like
// Edge to Edge.
if (systemChromeStyle.systemNavigationBarContrastEnforced != null
&& Build.VERSION.SDK_INT >= 29) {
window.setNavigationBarContrastEnforced(
systemChromeStyle.systemNavigationBarContrastEnforced);
}
currentTheme = systemChromeStyle;
}
private void setFrameworkHandlesBack(boolean frameworkHandlesBack) {
platformPluginDelegate.setFrameworkHandlesBack(frameworkHandlesBack);
}
private void popSystemNavigator() {
if (platformPluginDelegate != null && platformPluginDelegate.popSystemNavigator()) {
// A custom behavior was executed by the delegate. Don't execute default behavior.
return;
}
if (activity instanceof OnBackPressedDispatcherOwner) {
((OnBackPressedDispatcherOwner) activity).getOnBackPressedDispatcher().onBackPressed();
} else {
activity.finish();
}
}
private CharSequence getClipboardData(PlatformChannel.ClipboardContentFormat format) {
ClipboardManager clipboard =
(ClipboardManager) activity.getSystemService(Context.CLIPBOARD_SERVICE);
if (!clipboard.hasPrimaryClip()) return null;
try {
ClipData clip = clipboard.getPrimaryClip();
if (clip == null) return null;
if (format == null || format == PlatformChannel.ClipboardContentFormat.PLAIN_TEXT) {
ClipData.Item item = clip.getItemAt(0);
if (item.getUri() != null)
activity.getContentResolver().openTypedAssetFileDescriptor(item.getUri(), "text/*", null);
return item.coerceToText(activity);
}
} catch (SecurityException e) {
Log.w(
TAG,
"Attempted to get clipboard data that requires additional permission(s).\n"
+ "See the exception details for which permission(s) are required, and consider adding them to your Android Manifest as described in:\n"
+ "https://developer.android.com/guide/topics/permissions/overview",
e);
return null;
} catch (FileNotFoundException e) {
return null;
}
return null;
}
private void setClipboardData(String text) {
ClipboardManager clipboard =
(ClipboardManager) activity.getSystemService(Context.CLIPBOARD_SERVICE);
ClipData clip = ClipData.newPlainText("text label?", text);
clipboard.setPrimaryClip(clip);
}
private boolean clipboardHasStrings() {
ClipboardManager clipboard =
(ClipboardManager) activity.getSystemService(Context.CLIPBOARD_SERVICE);
// Android 12 introduces a toast message that appears when an app reads the clipboard. To avoid
// unintended access, call the appropriate APIs to receive information about the current content
// that's on the clipboard (rather than the actual content itself).
if (!clipboard.hasPrimaryClip()) {
return false;
}
ClipDescription description = clipboard.getPrimaryClipDescription();
if (description == null) {
return false;
}
return description.hasMimeType("text/*");
}
}