// 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.app;

import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.app.Activity;
import android.app.Application;
import android.content.Context;
import android.content.Intent;
import android.content.pm.ActivityInfo;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageManager;
import android.content.pm.PackageManager.NameNotFoundException;
import android.content.res.Configuration;
import android.content.res.Resources.NotFoundException;
import android.graphics.drawable.Drawable;
import android.os.Build;
import android.os.Bundle;
import android.util.TypedValue;
import android.view.View;
import android.view.ViewGroup;
import android.view.Window;
import android.view.WindowManager.LayoutParams;
import io.flutter.Log;
import io.flutter.plugin.common.PluginRegistry;
import io.flutter.plugin.platform.PlatformPlugin;
import io.flutter.util.Preconditions;
import io.flutter.view.FlutterMain;
import io.flutter.view.FlutterNativeView;
import io.flutter.view.FlutterRunArguments;
import io.flutter.view.FlutterView;
import java.util.ArrayList;

/**
 * Deprecated class that performs the actual work of tying Android {@link android.app.Activity}
 * instances to Flutter.
 *
 * <p>This exists as a dedicated class (as opposed to being integrated directly into {@link
 * FlutterActivity}) to facilitate applications that don't wish to subclass {@code FlutterActivity}.
 * The most obvious example of when this may come in handy is if an application wishes to subclass
 * the Android v4 support library's {@code FragmentActivity}.
 *
 * <p><b>Usage:</b>
 *
 * <p>To wire this class up to your activity, simply forward the events defined in {@link
 * FlutterActivityEvents} from your activity to an instance of this class. Optionally, you can make
 * your activity implement {@link PluginRegistry} and/or {@link
 * io.flutter.view.FlutterView.Provider} and forward those methods to this class as well.
 *
 * @deprecated {@link io.flutter.embedding.android.FlutterActivity} is the new API that now replaces
 *     this class and {@link io.flutter.app.FlutterActivity}. See
 *     https://flutter.dev/go/android-project-migration for more migration details.
 */
@Deprecated
public final class FlutterActivityDelegate
    implements FlutterActivityEvents, FlutterView.Provider, PluginRegistry {
  private static final String SPLASH_SCREEN_META_DATA_KEY =
      "io.flutter.app.android.SplashScreenUntilFirstFrame";
  private static final String TAG = "FlutterActivityDelegate";
  private static final LayoutParams matchParent =
      new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT);

  /**
   * Specifies the mechanism by which Flutter views are created during the operation of a {@code
   * FlutterActivityDelegate}.
   *
   * <p>A delegate's view factory will be consulted during {@link #onCreate(Bundle)}. If it returns
   * {@code null}, then the delegate will fall back to instantiating a new full-screen {@code
   * FlutterView}.
   *
   * <p>A delegate's native view factory will be consulted during {@link #onCreate(Bundle)}. If it
   * returns {@code null}, then the delegate will fall back to instantiating a new {@code
   * FlutterNativeView}. This is useful for applications to override to reuse the FlutterNativeView
   * held e.g. by a pre-existing background service.
   */
  public interface ViewFactory {
    FlutterView createFlutterView(Context context);

    FlutterNativeView createFlutterNativeView();

    /**
     * Hook for subclasses to indicate that the {@code FlutterNativeView} returned by {@link
     * #createFlutterNativeView()} should not be destroyed when this activity is destroyed.
     *
     * @return Whether the FlutterNativeView is retained.
     */
    boolean retainFlutterNativeView();
  }

  private final Activity activity;
  private final ViewFactory viewFactory;
  private FlutterView flutterView;
  private View launchView;

  public FlutterActivityDelegate(Activity activity, ViewFactory viewFactory) {
    this.activity = Preconditions.checkNotNull(activity);
    this.viewFactory = Preconditions.checkNotNull(viewFactory);
  }

  @Override
  public FlutterView getFlutterView() {
    return flutterView;
  }

  // The implementation of PluginRegistry forwards to flutterView.
  @Override
  public boolean hasPlugin(String key) {
    return flutterView.getPluginRegistry().hasPlugin(key);
  }

  @Override
  @SuppressWarnings("unchecked")
  public <T> T valuePublishedByPlugin(String pluginKey) {
    return (T) flutterView.getPluginRegistry().valuePublishedByPlugin(pluginKey);
  }

  @Override
  public Registrar registrarFor(String pluginKey) {
    return flutterView.getPluginRegistry().registrarFor(pluginKey);
  }

  @Override
  public boolean onRequestPermissionsResult(
      int requestCode, String[] permissions, int[] grantResults) {
    return flutterView
        .getPluginRegistry()
        .onRequestPermissionsResult(requestCode, permissions, grantResults);
  }

  @Override
  public boolean onActivityResult(int requestCode, int resultCode, Intent data) {
    return flutterView.getPluginRegistry().onActivityResult(requestCode, resultCode, data);
  }

  @Override
  public void onCreate(Bundle savedInstanceState) {
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
      Window window = activity.getWindow();
      window.addFlags(LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS);
      window.setStatusBarColor(0x40000000);
      window.getDecorView().setSystemUiVisibility(PlatformPlugin.DEFAULT_SYSTEM_UI);
    }

    String[] args = getArgsFromIntent(activity.getIntent());
    FlutterMain.ensureInitializationComplete(activity.getApplicationContext(), args);

    flutterView = viewFactory.createFlutterView(activity);
    if (flutterView == null) {
      FlutterNativeView nativeView = viewFactory.createFlutterNativeView();
      flutterView = new FlutterView(activity, null, nativeView);
      flutterView.setLayoutParams(matchParent);
      activity.setContentView(flutterView);
      launchView = createLaunchView();
      if (launchView != null) {
        addLaunchView();
      }
    }

    if (loadIntent(activity.getIntent())) {
      return;
    }

    String appBundlePath = FlutterMain.findAppBundlePath();
    if (appBundlePath != null) {
      runBundle(appBundlePath);
    }
  }

  @Override
  public void onNewIntent(Intent intent) {
    // Only attempt to reload the Flutter Dart code during development. Use
    // the debuggable flag as an indicator that we are in development mode.
    if (!isDebuggable() || !loadIntent(intent)) {
      flutterView.getPluginRegistry().onNewIntent(intent);
    }
  }

  private boolean isDebuggable() {
    return (activity.getApplicationInfo().flags & ApplicationInfo.FLAG_DEBUGGABLE) != 0;
  }

  @Override
  public void onPause() {
    Application app = (Application) activity.getApplicationContext();
    if (app instanceof FlutterApplication) {
      FlutterApplication flutterApp = (FlutterApplication) app;
      if (activity.equals(flutterApp.getCurrentActivity())) {
        flutterApp.setCurrentActivity(null);
      }
    }
    if (flutterView != null) {
      flutterView.onPause();
    }
  }

  @Override
  public void onStart() {
    if (flutterView != null) {
      flutterView.onStart();
    }
  }

  @Override
  public void onResume() {
    Application app = (Application) activity.getApplicationContext();
    if (app instanceof FlutterApplication) {
      FlutterApplication flutterApp = (FlutterApplication) app;
      flutterApp.setCurrentActivity(activity);
    }
  }

  @Override
  public void onStop() {
    flutterView.onStop();
  }

  @Override
  public void onPostResume() {
    if (flutterView != null) {
      flutterView.onPostResume();
    }
  }

  @Override
  public void onDestroy() {
    Application app = (Application) activity.getApplicationContext();
    if (app instanceof FlutterApplication) {
      FlutterApplication flutterApp = (FlutterApplication) app;
      if (activity.equals(flutterApp.getCurrentActivity())) {
        flutterApp.setCurrentActivity(null);
      }
    }
    if (flutterView != null) {
      final boolean detach =
          flutterView.getPluginRegistry().onViewDestroy(flutterView.getFlutterNativeView());
      if (detach || viewFactory.retainFlutterNativeView()) {
        // Detach, but do not destroy the FlutterView if a plugin
        // expressed interest in its FlutterNativeView.
        flutterView.detach();
      } else {
        flutterView.destroy();
      }
    }
  }

  @Override
  public boolean onBackPressed() {
    if (flutterView != null) {
      flutterView.popRoute();
      return true;
    }
    return false;
  }

  @Override
  public void onUserLeaveHint() {
    flutterView.getPluginRegistry().onUserLeaveHint();
  }

  @Override
  public void onWindowFocusChanged(boolean hasFocus) {
    flutterView.getPluginRegistry().onWindowFocusChanged(hasFocus);
  }

  @Override
  public void onTrimMemory(int level) {
    // Use a trim level delivered while the application is running so the
    // framework has a chance to react to the notification.
    if (level == TRIM_MEMORY_RUNNING_LOW) {
      flutterView.onMemoryPressure();
    }
  }

  @Override
  public void onLowMemory() {
    flutterView.onMemoryPressure();
  }

  @Override
  public void onConfigurationChanged(Configuration newConfig) {}

  private static String[] getArgsFromIntent(Intent intent) {
    // Before adding more entries to this list, consider that arbitrary
    // Android applications can generate intents with extra data and that
    // there are many security-sensitive args in the binary.
    ArrayList<String> args = new ArrayList<>();
    if (intent.getBooleanExtra("trace-startup", false)) {
      args.add("--trace-startup");
    }
    if (intent.getBooleanExtra("start-paused", false)) {
      args.add("--start-paused");
    }
    if (intent.getBooleanExtra("disable-service-auth-codes", false)) {
      args.add("--disable-service-auth-codes");
    }
    if (intent.getBooleanExtra("use-test-fonts", false)) {
      args.add("--use-test-fonts");
    }
    if (intent.getBooleanExtra("enable-dart-profiling", false)) {
      args.add("--enable-dart-profiling");
    }
    if (intent.getBooleanExtra("enable-software-rendering", false)) {
      args.add("--enable-software-rendering");
    }
    if (intent.getBooleanExtra("skia-deterministic-rendering", false)) {
      args.add("--skia-deterministic-rendering");
    }
    if (intent.getBooleanExtra("trace-skia", false)) {
      args.add("--trace-skia");
    }
    if (intent.getBooleanExtra("trace-systrace", false)) {
      args.add("--trace-systrace");
    }
    if (intent.getBooleanExtra("dump-skp-on-shader-compilation", false)) {
      args.add("--dump-skp-on-shader-compilation");
    }
    if (intent.getBooleanExtra("cache-sksl", false)) {
      args.add("--cache-sksl");
    }
    if (intent.getBooleanExtra("purge-persistent-cache", false)) {
      args.add("--purge-persistent-cache");
    }
    if (intent.getBooleanExtra("verbose-logging", false)) {
      args.add("--verbose-logging");
    }
    int vmServicePort = intent.getIntExtra("vm-service-port", 0);
    if (vmServicePort > 0) {
      args.add("--vm-service-port=" + Integer.toString(vmServicePort));
    } else {
      // TODO(bkonyi): remove once flutter_tools no longer uses this option.
      // See https://github.com/dart-lang/sdk/issues/50233
      vmServicePort = intent.getIntExtra("observatory-port", 0);
      if (vmServicePort > 0) {
        args.add("--vm-service-port=" + Integer.toString(vmServicePort));
      }
    }
    if (intent.getBooleanExtra("endless-trace-buffer", false)) {
      args.add("--endless-trace-buffer");
    }
    // NOTE: all flags provided with this argument are subject to filtering
    // based on a list of allowed flags in shell/common/switches.cc. If any
    // flag provided is not allowed, the process will immediately terminate.
    if (intent.hasExtra("dart-flags")) {
      args.add("--dart-flags=" + intent.getStringExtra("dart-flags"));
    }
    if (!args.isEmpty()) {
      String[] argsArray = new String[args.size()];
      return args.toArray(argsArray);
    }
    return null;
  }

  private boolean loadIntent(Intent intent) {
    String action = intent.getAction();
    if (Intent.ACTION_RUN.equals(action)) {
      String route = intent.getStringExtra("route");
      String appBundlePath = intent.getDataString();
      if (appBundlePath == null) {
        // Fall back to the installation path if no bundle path was specified.
        appBundlePath = FlutterMain.findAppBundlePath();
      }
      if (route != null) {
        flutterView.setInitialRoute(route);
      }

      runBundle(appBundlePath);
      return true;
    }

    return false;
  }

  private void runBundle(String appBundlePath) {
    if (!flutterView.getFlutterNativeView().isApplicationRunning()) {
      FlutterRunArguments args = new FlutterRunArguments();
      args.bundlePath = appBundlePath;
      args.entrypoint = "main";
      flutterView.runFromBundle(args);
    }
  }

  /**
   * Creates a {@link View} containing the same {@link Drawable} as the one set as the {@code
   * windowBackground} of the parent activity for use as a launch splash view.
   *
   * <p>Returns null if no {@code windowBackground} is set for the activity.
   */
  private View createLaunchView() {
    if (!showSplashScreenUntilFirstFrame()) {
      return null;
    }
    final Drawable launchScreenDrawable = getLaunchScreenDrawableFromActivityTheme();
    if (launchScreenDrawable == null) {
      return null;
    }
    final View view = new View(activity);
    view.setLayoutParams(matchParent);
    view.setBackground(launchScreenDrawable);
    return view;
  }

  /**
   * Extracts a {@link Drawable} from the parent activity's {@code windowBackground}.
   *
   * <p>{@code android:windowBackground} is specifically reused instead of a other attributes
   * because the Android framework can display it fast enough when launching the app as opposed to
   * anything defined in the Activity subclass.
   *
   * <p>Returns null if no {@code windowBackground} is set for the activity.
   */
  @SuppressWarnings("deprecation")
  private Drawable getLaunchScreenDrawableFromActivityTheme() {
    TypedValue typedValue = new TypedValue();
    if (!activity.getTheme().resolveAttribute(android.R.attr.windowBackground, typedValue, true)) {
      return null;
    }
    if (typedValue.resourceId == 0) {
      return null;
    }
    try {
      return activity.getResources().getDrawable(typedValue.resourceId);
    } catch (NotFoundException e) {
      Log.e(TAG, "Referenced launch screen windowBackground resource does not exist");
      return null;
    }
  }

  /**
   * Let the user specify whether the activity's {@code windowBackground} is a launch screen and
   * should be shown until the first frame via a <meta-data> tag in the activity.
   */
  private Boolean showSplashScreenUntilFirstFrame() {
    try {
      ActivityInfo activityInfo =
          activity
              .getPackageManager()
              .getActivityInfo(activity.getComponentName(), PackageManager.GET_META_DATA);
      Bundle metadata = activityInfo.metaData;
      return metadata != null && metadata.getBoolean(SPLASH_SCREEN_META_DATA_KEY);
    } catch (NameNotFoundException e) {
      return false;
    }
  }

  /**
   * Show and then automatically animate out the launch view.
   *
   * <p>If a launch screen is defined in the user application's AndroidManifest.xml as the
   * activity's {@code windowBackground}, display it on top of the {@link FlutterView} and remove
   * the activity's {@code windowBackground}.
   *
   * <p>Fade it out and remove it when the {@link FlutterView} renders its first frame.
   */
  private void addLaunchView() {
    if (launchView == null) {
      return;
    }

    activity.addContentView(launchView, matchParent);
    flutterView.addFirstFrameListener(
        new FlutterView.FirstFrameListener() {
          @Override
          public void onFirstFrame() {
            FlutterActivityDelegate.this
                .launchView
                .animate()
                .alpha(0f)
                // Use Android's default animation duration.
                .setListener(
                    new AnimatorListenerAdapter() {
                      @Override
                      public void onAnimationEnd(Animator animation) {
                        // Views added to an Activity's addContentView is always added to its
                        // root FrameLayout.
                        ((ViewGroup) FlutterActivityDelegate.this.launchView.getParent())
                            .removeView(FlutterActivityDelegate.this.launchView);
                        FlutterActivityDelegate.this.launchView = null;
                      }
                    });

            FlutterActivityDelegate.this.flutterView.removeFirstFrameListener(this);
          }
        });

    // Resets the activity theme from the one containing the launch screen in the window
    // background to a blank one since the launch screen is now in a view in front of the
    // FlutterView.
    //
    // We can make this configurable if users want it.
    activity.setTheme(android.R.style.Theme_Black_NoTitleBar);
  }
}
