content: /*
   * Copyright (C) 2017 The Android Open Source Project
   *
   * Licensed under the Apache License, Version 2.0 (the "License"); you may not
   * use this file except in compliance with the License. You may obtain a copy of
   * the License at
   *
   * http://www.apache.org/licenses/LICENSE-2.0
   *
   * Unless required by applicable law or agreed to in writing, software
   * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
   * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
   * License for the specific language governing permissions and limitations under
   * the License.
   */
  package com.android.launcher3.ui;
  
  import static com.android.launcher3.tapl.LauncherInstrumentation.ContainerType;
  import static com.android.launcher3.ui.TaplTestsLauncher3.getAppPackageName;
  import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
  
  import static org.junit.Assert.assertTrue;
  
  import static java.lang.System.exit;
  
  import static androidx.test.InstrumentationRegistry.getInstrumentation;
  
  import android.content.BroadcastReceiver;
  import android.content.ComponentName;
  import android.content.Context;
  import android.content.Intent;
  import android.content.IntentFilter;
  import android.content.pm.LauncherActivityInfo;
  import android.content.pm.PackageManager;
  import android.os.Process;
  import android.os.RemoteException;
  import android.util.Log;
  
  import androidx.test.InstrumentationRegistry;
  import androidx.test.uiautomator.By;
  import androidx.test.uiautomator.BySelector;
  import androidx.test.uiautomator.UiDevice;
  import androidx.test.uiautomator.Until;
  
  import com.android.launcher3.Launcher;
  import com.android.launcher3.LauncherAppState;
  import com.android.launcher3.LauncherModel;
  import com.android.launcher3.LauncherSettings;
  import com.android.launcher3.LauncherState;
  import com.android.launcher3.LauncherStateManager;
  import com.android.launcher3.Utilities;
  import com.android.launcher3.compat.LauncherAppsCompat;
  import com.android.launcher3.model.AppLaunchTracker;
  import com.android.launcher3.tapl.LauncherInstrumentation;
  import com.android.launcher3.tapl.TestHelpers;
  import com.android.launcher3.testcomponent.TestCommandReceiver;
  import com.android.launcher3.testing.TestProtocol;
  import com.android.launcher3.util.LooperExecutor;
  import com.android.launcher3.util.PackageManagerHelper;
  import com.android.launcher3.util.Wait;
  import com.android.launcher3.util.rule.FailureWatcher;
  import com.android.launcher3.util.rule.LauncherActivityRule;
  import com.android.launcher3.util.rule.ShellCommandRule;
  import com.android.launcher3.util.rule.TestStabilityRule;
  
  import org.junit.After;
  import org.junit.Before;
  import org.junit.Rule;
  import org.junit.rules.RuleChain;
  import org.junit.rules.TestRule;
  
  import java.io.IOException;
  import java.lang.annotation.ElementType;
  import java.lang.annotation.Retention;
  import java.lang.annotation.RetentionPolicy;
  import java.lang.annotation.Target;
  import java.util.concurrent.Callable;
  import java.util.concurrent.CountDownLatch;
  import java.util.concurrent.TimeUnit;
  import java.util.function.Consumer;
  import java.util.function.Function;
  
  /**
   * Base class for all instrumentation tests providing various utility methods.
   */
  public abstract class AbstractLauncherUiTest {
  
      public static final long DEFAULT_ACTIVITY_TIMEOUT = TimeUnit.SECONDS.toMillis(10);
      public static final long DEFAULT_BROADCAST_TIMEOUT_SECS = 5;
  
      public static final long DEFAULT_UI_TIMEOUT = 60000; // b/136278866
      private static final String TAG = "AbstractLauncherUiTest";
  
      protected LooperExecutor mMainThreadExecutor = MAIN_EXECUTOR;
      protected final UiDevice mDevice = UiDevice.getInstance(getInstrumentation());
      protected final LauncherInstrumentation mLauncher = new LauncherInstrumentation();
      protected Context mTargetContext;
      protected String mTargetPackage;
  
      protected AbstractLauncherUiTest() {
          try {
              mDevice.setOrientationNatural();
          } catch (RemoteException e) {
              throw new RuntimeException(e);
          }
          if (TestHelpers.isInLauncherProcess()) {
              Utilities.enableRunningInTestHarnessForTests();
              mLauncher.setSystemHealthSupplier(startTime -> TestCommandReceiver.callCommand(
                      TestCommandReceiver.GET_SYSTEM_HEALTH_MESSAGE, startTime.toString()).
                      getString("result"));
              mLauncher.setOnSettledStateAction(
                      containerType -> executeOnLauncher(
                              launcher ->
                                      checkLauncherIntegrity(launcher, containerType)));
          }
          mLauncher.enableDebugTracing();
      }
  
      protected final LauncherActivityRule mActivityMonitor = new LauncherActivityRule();
  
      @Rule
      public ShellCommandRule mDefaultLauncherRule =
              TestHelpers.isInLauncherProcess() ? ShellCommandRule.setDefaultLauncher() : null;
  
      @Rule
      public ShellCommandRule mDisableHeadsUpNotification =
              ShellCommandRule.disableHeadsUpNotification();
  
      protected void clearPackageData(String pkg) throws IOException, InterruptedException {
          final CountDownLatch count = new CountDownLatch(2);
          final BroadcastReceiver broadcastReceiver = new BroadcastReceiver() {
              @Override
              public void onReceive(Context context, Intent intent) {
                  count.countDown();
              }
          };
          mTargetContext.registerReceiver(broadcastReceiver,
                  PackageManagerHelper.getPackageFilter(pkg,
                          Intent.ACTION_PACKAGE_RESTARTED, Intent.ACTION_PACKAGE_DATA_CLEARED));
  
          mDevice.executeShellCommand("pm clear " + pkg);
          assertTrue(pkg + " didn't restart", count.await(10, TimeUnit.SECONDS));
          mTargetContext.unregisterReceiver(broadcastReceiver);
      }
  
      // Annotation for tests that need to be run in portrait and landscape modes.
      @Retention(RetentionPolicy.RUNTIME)
      @Target(ElementType.METHOD)
      protected @interface PortraitLandscape {
      }
  
      protected TestRule getRulesInsideActivityMonitor() {
          return RuleChain.
                  outerRule(new PortraitLandscapeRunner(this)).
                  around(new FailureWatcher(mDevice));
      }
  
      @Rule
      public TestRule mOrderSensitiveRules = RuleChain.
              outerRule(new TestStabilityRule()).
              around(mActivityMonitor).
              around(getRulesInsideActivityMonitor());
  
      public UiDevice getDevice() {
          return mDevice;
      }
  
      @Before
      public void setUp() throws Exception {
          // Disable app tracker
          AppLaunchTracker.INSTANCE.initializeForTesting(new AppLaunchTracker());
  
          mTargetContext = InstrumentationRegistry.getTargetContext();
          mTargetPackage = mTargetContext.getPackageName();
      }
  
      @After
      public void verifyLauncherState() {
          try {
              // Limits UI tests affecting tests running after them.
              waitForModelLoaded();
          } catch (Throwable t) {
              Log.e(TAG,
                      "Couldn't deinit after a test, exiting tests, see logs for failures that "
                              + "could have caused this",
                      t);
              exit(1);
          }
      }
  
      protected void clearLauncherData() throws IOException, InterruptedException {
          if (TestHelpers.isInLauncherProcess()) {
              LauncherSettings.Settings.call(mTargetContext.getContentResolver(),
                      LauncherSettings.Settings.METHOD_CREATE_EMPTY_DB);
              resetLoaderState();
          } else {
              clearPackageData(mDevice.getLauncherPackageName());
              mLauncher.enableDebugTracing();
          }
      }
  
      /**
       * Removes all icons from homescreen and hotseat.
       */
      public void clearHomescreen() throws Throwable {
          LauncherSettings.Settings.call(mTargetContext.getContentResolver(),
                  LauncherSettings.Settings.METHOD_CREATE_EMPTY_DB);
          LauncherSettings.Settings.call(mTargetContext.getContentResolver(),
                  LauncherSettings.Settings.METHOD_CLEAR_EMPTY_DB_FLAG);
          resetLoaderState();
      }
  
      protected void resetLoaderState() {
          try {
              mMainThreadExecutor.execute(
                      () -> LauncherAppState.getInstance(mTargetContext).getModel().forceReload());
          } catch (Throwable t) {
              throw new IllegalArgumentException(t);
          }
          waitForModelLoaded();
      }
  
      protected void waitForModelLoaded() {
          waitForLauncherCondition("Launcher model didn't load", launcher -> {
              final LauncherModel model = LauncherAppState.getInstance(mTargetContext).getModel();
              return model.getCallback() == null || model.isModelLoaded();
          });
      }
  
      /**
       * Runs the callback on the UI thread and returns the result.
       */
      protected <T> T getOnUiThread(final Callable<T> callback) {
          try {
              return mMainThreadExecutor.submit(callback).get();
          } catch (Exception e) {
              throw new RuntimeException(e);
          }
      }
  
      protected <T> T getFromLauncher(Function<Launcher, T> f) {
          if (!TestHelpers.isInLauncherProcess()) return null;
          return getOnUiThread(() -> f.apply(mActivityMonitor.getActivity()));
      }
  
      protected void executeOnLauncher(Consumer<Launcher> f) {
          getFromLauncher(launcher -> {
              f.accept(launcher);
              return null;
          });
      }
  
      // Cannot be used in TaplTests between a Tapl call injecting a gesture and a tapl call expecting
      // the results of that gesture because the wait can hide flakeness.
      protected void waitForState(String message, LauncherState state) {
          waitForLauncherCondition(message,
                  launcher -> launcher.getStateManager().getCurrentStableState() == state);
      }
  
      protected void waitForResumed(String message) {
          waitForLauncherCondition(message, launcher -> launcher.hasBeenResumed());
      }
  
      // Cannot be used in TaplTests after injecting any gesture using Tapl because this can hide
      // flakiness.
      protected void waitForLauncherCondition(String message, Function<Launcher, Boolean> condition) {
          waitForLauncherCondition(message, condition, DEFAULT_ACTIVITY_TIMEOUT);
      }
  
      // Cannot be used in TaplTests after injecting any gesture using Tapl because this can hide
      // flakiness.
      protected <T> T getOnceNotNull(String message, Function<Launcher, T> f) {
          return getOnceNotNull(message, f, DEFAULT_ACTIVITY_TIMEOUT);
      }
  
      // Cannot be used in TaplTests after injecting any gesture using Tapl because this can hide
      // flakiness.
      protected void waitForLauncherCondition(
              String message, Function<Launcher, Boolean> condition, long timeout) {
          if (!TestHelpers.isInLauncherProcess()) return;
          Wait.atMost(message, () -> getFromLauncher(condition), timeout);
      }
  
      // Cannot be used in TaplTests after injecting any gesture using Tapl because this can hide
      // flakiness.
      protected <T> T getOnceNotNull(String message, Function<Launcher, T> f, long timeout) {
          if (!TestHelpers.isInLauncherProcess()) return null;
  
          final Object[] output = new Object[1];
          Wait.atMost(message, () -> {
              final Object fromLauncher = getFromLauncher(f);
              output[0] = fromLauncher;
              return fromLauncher != null;
          }, timeout);
          return (T) output[0];
      }
  
      // Cannot be used in TaplTests after injecting any gesture using Tapl because this can hide
      // flakiness.
      protected void waitForLauncherCondition(
              String message,
              Runnable testThreadAction, Function<Launcher, Boolean> condition,
              long timeout) {
          if (!TestHelpers.isInLauncherProcess()) return;
          Wait.atMost(message, () -> {
              testThreadAction.run();
              return getFromLauncher(condition);
          }, timeout);
      }
  
      protected LauncherActivityInfo getSettingsApp() {
          return LauncherAppsCompat.getInstance(mTargetContext)
                  .getActivityList("com.android.settings",
                          Process.myUserHandle()).get(0);
      }
  
      /**
       * Broadcast receiver which blocks until the result is received.
       */
      public class BlockingBroadcastReceiver extends BroadcastReceiver {
  
          private final CountDownLatch latch = new CountDownLatch(1);
          private Intent mIntent;
  
          public BlockingBroadcastReceiver(String action) {
              mTargetContext.registerReceiver(this, new IntentFilter(action));
          }
  
          @Override
          public void onReceive(Context context, Intent intent) {
              mIntent = intent;
              latch.countDown();
          }
  
          public Intent blockingGetIntent() throws InterruptedException {
              latch.await(DEFAULT_BROADCAST_TIMEOUT_SECS, TimeUnit.SECONDS);
              mTargetContext.unregisterReceiver(this);
              return mIntent;
          }
  
          public Intent blockingGetExtraIntent() throws InterruptedException {
              Intent intent = blockingGetIntent();
              return intent == null ? null : (Intent) intent.getParcelableExtra(Intent.EXTRA_INTENT);
          }
      }
  
      protected void startAppFast(String packageName) {
          startIntent(
                  getInstrumentation().getContext().getPackageManager().getLaunchIntentForPackage(
                          packageName),
                  By.pkg(packageName).depth(0),
                  true /* newTask */);
      }
  
      protected void startTestActivity(int activityNumber) {
          final String packageName = getAppPackageName();
          final Intent intent = getInstrumentation().getContext().getPackageManager().
                  getLaunchIntentForPackage(packageName);
          intent.setComponent(new ComponentName(packageName,
                  "com.android.launcher3.tests.Activity" + activityNumber));
          startIntent(intent, By.pkg(packageName).text("TestActivity" + activityNumber),
                  false /* newTask */);
      }
  
      private void startIntent(Intent intent, BySelector selector, boolean newTask) {
          intent.addCategory(Intent.CATEGORY_LAUNCHER);
          if (newTask) {
              intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
          } else {
              intent.addFlags(Intent.FLAG_ACTIVITY_MULTIPLE_TASK | Intent.FLAG_ACTIVITY_NEW_DOCUMENT);
          }
          getInstrumentation().getTargetContext().startActivity(intent);
          assertTrue("App didn't start: " + selector,
                  mDevice.wait(Until.hasObject(selector), DEFAULT_UI_TIMEOUT));
      }
  
      public static String resolveSystemApp(String category) {
          return getInstrumentation().getContext().getPackageManager().resolveActivity(
                  new Intent(Intent.ACTION_MAIN).addCategory(category),
                  PackageManager.MATCH_SYSTEM_ONLY).
                  activityInfo.packageName;
      }
  
      protected void closeLauncherActivity() {
          // Destroy Launcher activity.
          executeOnLauncher(launcher -> {
              if (launcher != null) {
                  launcher.finish();
              }
          });
          waitForLauncherCondition(
                  "Launcher still active", launcher -> launcher == null, DEFAULT_UI_TIMEOUT);
      }
  
      protected boolean isInBackground(Launcher launcher) {
          return !launcher.hasBeenResumed();
      }
  
      protected boolean isInState(LauncherState state) {
          if (!TestHelpers.isInLauncherProcess()) return true;
          return getFromLauncher(launcher -> launcher.getStateManager().getState() == state);
      }
  
      protected int getAllAppsScroll(Launcher launcher) {
          return launcher.getAppsView().getActiveRecyclerView().getCurrentScrollY();
      }
  
      private static void checkLauncherIntegrity(
              Launcher launcher, ContainerType expectedContainerType) {
          if (launcher != null) {
              final LauncherStateManager stateManager = launcher.getStateManager();
              final LauncherState stableState = stateManager.getCurrentStableState();
  
              assertTrue("Stable state != state: " + stableState.getClass().getSimpleName() + ", "
                              + stateManager.getState().getClass().getSimpleName(),
                      stableState == stateManager.getState());
  
              final boolean isResumed = launcher.hasBeenResumed();
              assertTrue("hasBeenResumed() != isStarted(), hasBeenResumed(): " + isResumed,
                      isResumed == launcher.isStarted());
              assertTrue("hasBeenResumed() != isUserActive(), hasBeenResumed(): " + isResumed,
                      isResumed == launcher.isUserActive());
  
              final int ordinal = stableState.ordinal;
  
              switch (expectedContainerType) {
                  case WORKSPACE:
                  case WIDGETS: {
                      assertTrue(
                              "Launcher is not resumed in state: " + expectedContainerType,
                              isResumed);
                      assertTrue(TestProtocol.stateOrdinalToString(ordinal),
                              ordinal == TestProtocol.NORMAL_STATE_ORDINAL);
                      break;
                  }
                  case ALL_APPS: {
                      assertTrue(
                              "Launcher is not resumed in state: " + expectedContainerType,
                              isResumed);
                      assertTrue(TestProtocol.stateOrdinalToString(ordinal),
                              ordinal == TestProtocol.ALL_APPS_STATE_ORDINAL);
                      break;
                  }
                  case OVERVIEW: {
                      assertTrue(
                              "Launcher is not resumed in state: " + expectedContainerType,
                              isResumed);
                      assertTrue(TestProtocol.stateOrdinalToString(ordinal),
                              ordinal == TestProtocol.OVERVIEW_STATE_ORDINAL);
                      break;
                  }
                  case BACKGROUND: {
                      assertTrue("Launcher is resumed in state: " + expectedContainerType,
                              !isResumed);
                      assertTrue(TestProtocol.stateOrdinalToString(ordinal),
                              ordinal == TestProtocol.NORMAL_STATE_ORDINAL);
                      break;
                  }
                  default:
                      throw new IllegalArgumentException(
                              "Illegal container: " + expectedContainerType);
              }
          } else {
              assertTrue(
                      "Container type is not BACKGROUND or FALLBACK_OVERVIEW: "
                              + expectedContainerType,
                      expectedContainerType == ContainerType.BACKGROUND ||
                              expectedContainerType == ContainerType.FALLBACK_OVERVIEW);
          }
      }
  }
  