// 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/*");
  }
}
