content: package de.test.antennapod;
  
  import android.content.Context;
  import android.content.Intent;
  import androidx.annotation.IdRes;
  import androidx.annotation.NonNull;
  import androidx.annotation.StringRes;
  import androidx.preference.PreferenceManager;
  import androidx.test.espresso.NoMatchingViewException;
  import androidx.test.platform.app.InstrumentationRegistry;
  import androidx.test.espresso.PerformException;
  import androidx.test.espresso.UiController;
  import androidx.test.espresso.ViewAction;
  import androidx.test.espresso.ViewInteraction;
  import androidx.test.espresso.contrib.DrawerActions;
  import androidx.test.espresso.contrib.RecyclerViewActions;
  import androidx.test.espresso.util.HumanReadables;
  import androidx.test.espresso.util.TreeIterables;
  import android.view.View;
  
  import de.danoeh.antennapod.playback.service.PlaybackService;
  import de.danoeh.antennapod.storage.database.PodDBAdapter;
  import junit.framework.AssertionFailedError;
  
  import de.danoeh.antennapod.R;
  import de.danoeh.antennapod.activity.MainActivity;
  import de.danoeh.antennapod.storage.preferences.UserPreferences;
  import de.danoeh.antennapod.ui.screen.drawer.NavDrawerFragment;
  import org.awaitility.Awaitility;
  import org.awaitility.core.ConditionTimeoutException;
  import org.hamcrest.Matcher;
  
  import java.io.File;
  import java.util.concurrent.TimeUnit;
  import java.util.concurrent.TimeoutException;
  
  import static androidx.test.espresso.Espresso.onView;
  import static androidx.test.espresso.action.ViewActions.click;
  import static androidx.test.espresso.assertion.ViewAssertions.doesNotExist;
  import static androidx.test.espresso.assertion.ViewAssertions.matches;
  import static androidx.test.espresso.matcher.ViewMatchers.hasDescendant;
  import static androidx.test.espresso.matcher.ViewMatchers.isDisplayed;
  import static androidx.test.espresso.matcher.ViewMatchers.isRoot;
  import static androidx.test.espresso.matcher.ViewMatchers.withContentDescription;
  import static androidx.test.espresso.matcher.ViewMatchers.withId;
  import static androidx.test.espresso.matcher.ViewMatchers.withText;
  import static org.hamcrest.Matchers.allOf;
  import static org.hamcrest.Matchers.not;
  
  public class EspressoTestUtils {
      /**
       * Perform action of waiting for a specific view id.
       * https://stackoverflow.com/a/49814995/
       * @param viewMatcher The view to wait for.
       * @param millis The timeout of until when to wait for.
       */
      public static ViewAction waitForView(final Matcher<View> viewMatcher, final long millis) {
          return new ViewAction() {
              @Override
              public Matcher<View> getConstraints() {
                  return isRoot();
              }
  
              @Override
              public String getDescription() {
                  return "wait for a specific view for " + millis + " millis.";
              }
  
              @Override
              public void perform(final UiController uiController, final View view) {
                  uiController.loopMainThreadUntilIdle();
                  final long startTime = System.currentTimeMillis();
                  final long endTime = startTime + millis;
  
                  do {
                      for (View child : TreeIterables.breadthFirstViewTraversal(view)) {
                          // found view with required ID
                          if (viewMatcher.matches(child)) {
                              return;
                          }
                      }
  
                      uiController.loopMainThreadForAtLeast(50);
                  } while (System.currentTimeMillis() < endTime);
  
                  // timeout happens
                  throw new PerformException.Builder()
                          .withActionDescription(this.getDescription())
                          .withViewDescription(HumanReadables.describe(view))
                          .withCause(new TimeoutException())
                          .build();
              }
          };
      }
  
      /**
       * Wait until a certain view becomes visible, but at the longest until the timeout.
       * Unlike {@link #waitForView(Matcher, long)} it doesn't stick to the initial root view.
       *
       * @param viewMatcher The view to wait for.
       * @param timeoutMillis Maximum waiting period in milliseconds.
       * @throws Exception Throws an Exception in case of a timeout.
       */
      public static void waitForViewGlobally(@NonNull Matcher<View> viewMatcher, long timeoutMillis) throws Exception {
          long startTime = System.currentTimeMillis();
          long endTime = startTime + timeoutMillis;
  
          do {
              try {
                  onView(viewMatcher).check(matches(isDisplayed()));
                  // no Exception thrown -> check successful
                  return;
              } catch (NoMatchingViewException | AssertionFailedError ignore) {
                  // check was not successful "not found" -> continue waiting
              }
              //noinspection BusyWait
              Thread.sleep(50);
          } while (System.currentTimeMillis() < endTime);
  
          throw new Exception("Timeout after " + timeoutMillis + " ms");
      }
  
      /**
       * Perform action of waiting for a specific view id.
       * https://stackoverflow.com/a/30338665/
       * @param id The id of the child to click.
       */
      public static ViewAction clickChildViewWithId(final @IdRes int id) {
          return new ViewAction() {
              @Override
              public Matcher<View> getConstraints() {
                  return null;
              }
  
              @Override
              public String getDescription() {
                  return "Click on a child view with specified id.";
              }
  
              @Override
              public void perform(UiController uiController, View view) {
                  View v = view.findViewById(id);
                  v.performClick();
              }
          };
      }
  
      public static void waitForViewToDisappear(Matcher<? super View> matcher, long maxWaitingTimeMs) {
          long endTime = System.currentTimeMillis() + maxWaitingTimeMs;
          while (System.currentTimeMillis() <= endTime) {
              try {
                  onView(allOf(matcher, isDisplayed())).check(matches(not(doesNotExist())));
                  Thread.sleep(100);
              } catch (NoMatchingViewException ex) {
                  return; // view has disappeared
              } catch (InterruptedException e) {
                  throw new RuntimeException(e);
              }
          }
          throw new RuntimeException("timeout exceeded"); // or whatever exception you want
      }
      
      /**
       * Clear all app databases.
       */
      public static void clearPreferences() {
          File root = InstrumentationRegistry.getInstrumentation().getTargetContext().getFilesDir().getParentFile();
          String[] sharedPreferencesFileNames = new File(root, "shared_prefs").list();
          for (String fileName : sharedPreferencesFileNames) {
              System.out.println("Cleared database: " + fileName);
              InstrumentationRegistry.getInstrumentation().getTargetContext().getSharedPreferences(
                      fileName.replace(".xml", ""), Context.MODE_PRIVATE).edit().clear().commit();
          }
  
          InstrumentationRegistry.getInstrumentation().getTargetContext()
                  .getSharedPreferences(MainActivity.PREF_NAME, Context.MODE_PRIVATE)
                  .edit()
                  .putBoolean(MainActivity.PREF_IS_FIRST_LAUNCH, false)
                  .commit();
  
          PreferenceManager.getDefaultSharedPreferences(InstrumentationRegistry.getInstrumentation().getTargetContext())
                  .edit()
                  .putString(UserPreferences.PREF_UPDATE_INTERVAL_MINUTES, "0")
                  .commit();
      }
  
      public static void setLaunchScreen(String tag) {
          InstrumentationRegistry.getInstrumentation().getTargetContext()
                  .getSharedPreferences(NavDrawerFragment.PREF_NAME, Context.MODE_PRIVATE)
                  .edit()
                  .putString(NavDrawerFragment.PREF_LAST_FRAGMENT_TAG, tag)
                  .commit();
          PreferenceManager.getDefaultSharedPreferences(InstrumentationRegistry.getInstrumentation().getTargetContext())
                  .edit()
                  .putString(UserPreferences.PREF_DEFAULT_PAGE, UserPreferences.DEFAULT_PAGE_REMEMBER)
                  .commit();
      }
  
      public static void clearDatabase() {
          PodDBAdapter.init(InstrumentationRegistry.getInstrumentation().getTargetContext());
          PodDBAdapter.deleteDatabase();
          PodDBAdapter adapter = PodDBAdapter.getInstance();
          adapter.open();
          adapter.close();
      }
  
      public static void clickPreference(@StringRes int title) {
          onView(withId(R.id.recycler_view)).perform(
                  RecyclerViewActions.actionOnItem(
                          allOf(hasDescendant(withText(title)),
                                  hasDescendant(withId(android.R.id.widget_frame))),
                          click()));
      }
  
      public static void openNavDrawer() {
          onView(isRoot()).perform(waitForView(withId(R.id.drawer_layout), 1000));
          onView(withId(R.id.drawer_layout)).perform(DrawerActions.open());
      }
  
      public static ViewInteraction onDrawerItem(Matcher<View> viewMatcher) {
          return onView(allOf(viewMatcher, withId(R.id.txtvTitle)));
      }
  
      public static void tryKillPlaybackService() {
          Context context = InstrumentationRegistry.getInstrumentation().getTargetContext();
          context.stopService(new Intent(context, PlaybackService.class));
          try {
              // Android has no reliable way to stop a service instantly.
              // Calling stopSelf marks allows the system to destroy the service but the actual call
              // to onDestroy takes until the next GC of the system, which we can not influence.
              // Try to wait for the service at least a bit.
              Awaitility.await().atMost(10, TimeUnit.SECONDS).until(() -> !PlaybackService.isRunning);
          } catch (ConditionTimeoutException e) {
              e.printStackTrace();
          }
          InstrumentationRegistry.getInstrumentation().waitForIdleSync();
      }
  
      public static Matcher<View> actionBarOverflow() {
          return allOf(isDisplayed(), withContentDescription("More options"));
      }
  }
  