/* * Copyright 2016 Kejun Xia * * 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.shipdream.lib.android.mvc; import android.annotation.SuppressLint; import android.content.Intent; import android.os.Build; import android.provider.Settings; import android.support.test.InstrumentationRegistry; import android.support.test.runner.AndroidJUnit4; import android.test.ActivityInstrumentationTestCase2; import android.test.suitebuilder.annotation.LargeTest; import android.util.Log; import com.shipdream.lib.android.mvc.event.bus.EventBus; import com.shipdream.lib.android.mvc.event.bus.annotation.EventBusV; import com.shipdream.lib.android.mvc.view.LifeCycleValidator; import com.shipdream.lib.android.mvc.view.help.LifeCycleMonitor; import com.shipdream.lib.android.mvc.view.help.LifeCycleMonitorA; import com.shipdream.lib.android.mvc.view.help.LifeCycleMonitorB; import com.shipdream.lib.android.mvc.view.help.LifeCycleMonitorC; import com.shipdream.lib.android.mvc.view.help.LifeCycleMonitorD; import com.shipdream.lib.poke.Component; import com.shipdream.lib.poke.Provides; import org.junit.After; import org.junit.Before; import org.junit.BeforeClass; import org.junit.runner.RunWith; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.util.Map; import java.util.UUID; import java.util.concurrent.ConcurrentHashMap; import javax.inject.Inject; import ch.qos.logback.classic.Level; import ch.qos.logback.classic.LoggerContext; import ch.qos.logback.classic.android.LogcatAppender; import ch.qos.logback.classic.encoder.PatternLayoutEncoder; import static org.mockito.Mockito.mock; @RunWith(AndroidJUnit4.class) @LargeTest public abstract class BaseTestCase<T extends TestActivity> extends ActivityInstrumentationTestCase2<T> { protected LifeCycleValidator lifeCycleValidator; protected LifeCycleMonitor lifeCycleMonitorMock; protected LifeCycleMonitorA lifeCycleMonitorMockA; protected LifeCycleValidator lifeCycleValidatorA; protected LifeCycleMonitorB lifeCycleMonitorMockB; protected LifeCycleValidator lifeCycleValidatorB; protected LifeCycleMonitorC lifeCycleMonitorMockC; protected LifeCycleValidator lifeCycleValidatorC; protected LifeCycleMonitorD lifeCycleMonitorMockD; protected LifeCycleValidator lifeCycleValidatorD; @Inject @EventBusV private EventBus eventBusV; protected abstract Class<T> getActivityClass(); private static class Waiter { boolean skip = false; private String name; private long timeout; public Waiter(String name) { this (name, 1000); } public Waiter(String name, long timeout) { this.name = name; this.timeout = timeout; } private void skip() { skip = true; } private void waitNow() { long start = System.currentTimeMillis(); while (true) { long elapsed = System.currentTimeMillis() - start; if (elapsed > timeout) { Log.w("TrackLifeSync", name + " times out by " + timeout + "ms"); break; } if (skip) { break; } try { Thread.sleep(10); } catch (InterruptedException e) { e.printStackTrace(); } } } } @Inject protected NavigationManager navigationManager; protected T activity; protected android.app.Instrumentation instrumentation; public BaseTestCase(Class<T> activityClass) { super(activityClass); } protected MvcComponent component; @BeforeClass public static void beforeClass() { configureLogbackDirectly(); } private static void configureLogbackDirectly() { // reset the default context (which may already have been initialized) // since we want to reconfigure it LoggerContext lc = (LoggerContext) LoggerFactory.getILoggerFactory(); lc.reset(); // setup LogcatAppender PatternLayoutEncoder encoder2 = new PatternLayoutEncoder(); encoder2.setContext(lc); encoder2.setPattern("[%thread] %msg%n"); encoder2.start(); LogcatAppender logcatAppender = new LogcatAppender(); logcatAppender.setContext(lc); logcatAppender.setEncoder(encoder2); logcatAppender.start(); // backup the newly created appenders to the root logger; // qualify Logger to disambiguate from org.slf4j.Logger ch.qos.logback.classic.Logger root = (ch.qos.logback.classic.Logger) LoggerFactory.getLogger(Logger.ROOT_LOGGER_NAME); root.addAppender(logcatAppender); root.setLevel(Level.ALL); } @Before public void setUp() throws Exception { super.setUp(); lifeCycleMonitorMock = mock(LifeCycleMonitor.class); lifeCycleValidator = new LifeCycleValidator(lifeCycleMonitorMock); lifeCycleMonitorMockA = mock(LifeCycleMonitorA.class); lifeCycleValidatorA = new LifeCycleValidator(lifeCycleMonitorMockA); lifeCycleMonitorMockB = mock(LifeCycleMonitorB.class); lifeCycleValidatorB = new LifeCycleValidator(lifeCycleMonitorMockB); lifeCycleMonitorMockC = mock(LifeCycleMonitorC.class); lifeCycleValidatorC = new LifeCycleValidator(lifeCycleMonitorMockC); lifeCycleMonitorMockD = mock(LifeCycleMonitorD.class); lifeCycleValidatorD = new LifeCycleValidator(lifeCycleMonitorMockD); component = new MvcComponent("UnitTestComponent"); component.register(new Object() { @Provides public LifeCycleMonitor provideLifeCycleMonitor() { return lifeCycleMonitorMock; } @Provides public LifeCycleMonitorA provideLifeCycleMonitorA() { return lifeCycleMonitorMockA; } @Provides public LifeCycleMonitorB provideLifeCycleMonitorB() { return lifeCycleMonitorMockB; } @Provides public LifeCycleMonitorC provideLifeCycleMonitorC() { return lifeCycleMonitorMockC; } @Provides public LifeCycleMonitorD provideLifeCycleMonitorD() { return lifeCycleMonitorMockD; } }); prepareDependencies(component); Mvc.graph().getRootComponent().attach(component, true); instrumentation = InstrumentationRegistry.getInstrumentation(); injectInstrumentation(instrumentation); activity = getActivity(); activity.runOnUiThread(new Runnable() { @Override public void run() { Mvc.graph().inject(BaseTestCase.this); eventBusV.register(BaseTestCase.this); } }); instrumentation.waitForIdleSync(); } protected void prepareDependencies(MvcComponent testComponent) throws Exception { } @After public void tearDown() throws Exception { navigationManager.navigate(this).back(null); navigationManager.navigate(this).back(); try { Mvc.graph().getRootComponent().getCache().clear(); Mvc.graph().getRootComponent().detach(component); } catch (Component.MismatchDetachException e) { e.printStackTrace(); } super.tearDown(); } protected void navTo(final Class cls) { navTo(cls, null); } protected void navTo(final Class cls, final Forwarder forwarder) { final Waiter waiter = new Waiter("NavTo " + cls.getSimpleName() + " ", 200); navigationManager.navigate(this).onSettled(new Navigator.OnSettled() { @Override public void run() { waiter.skip(); Log.v("TrackLifeSync:NavTo", "skip"); } }).to(cls, forwarder); waiter.waitNow(); Log.v("TrackLifeSync:NavTo", "finish"); } protected void navigateBackByFragment() throws InterruptedException { final Waiter waiter = new Waiter("NavBack"); navigationManager.navigate(this).onSettled(new Navigator.OnSettled() { @Override public void run() { waiter.skip(); } }).back(); waiter.waitNow(); waitTest(); } protected String pressHome() { String ticket = "PressHome: " + UUID.randomUUID(); TestActivity.ticket = ticket; final Intent startMain = new Intent(Intent.ACTION_MAIN); startMain.addCategory(Intent.CATEGORY_HOME); startMain.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); final Waiter waiter = new Waiter("PressHome", 500); TestActivity.Proxy proxy = new TestActivity.Proxy() { @Override protected void onPause() { waiter.skip(); Log.v("TrackLifeSync:Home", "Pause and skip"); activity.removeProxy(this); } }; activity.addProxy(proxy); activity.startActivity(startMain); Log.v("TrackLifeSync:Home", "Start home activity"); TestActivity.State state = activity.getState(); if (state != null && state.ordinal() >= TestActivity.State.PAUSE.ordinal()) { //ready } else { Log.v("TrackLifeSync:Home", "Start wait"); waiter.waitNow(); } activity.removeProxy(proxy); Log.v("TrackLifeSync:Home", "Finish"); try { waitTest(); } catch (InterruptedException e) { e.printStackTrace(); } return ticket; } private Map<String, Waiter> bringBackWaiters = new ConcurrentHashMap<>(); private void onEvent(TestActivity.Event.OnFragmentsResumed event) { Waiter waiter = bringBackWaiters.get(event.sender); if (waiter != null) { waiter.skip(); Log.v("TrackLifeSync:BringBack", "Skip waiting bringBack " + event.sender); bringBackWaiters.remove(event.sender); } } protected void bringBack(String ticket) { final Intent i = new Intent(activity, activity.getClass()); i.setFlags(Intent.FLAG_ACTIVITY_REORDER_TO_FRONT); boolean kill = false; try { kill = isDontKeepActivities(); } catch (Settings.SettingNotFoundException e) { e.printStackTrace(); } if (kill) { final Waiter waiter = new Waiter("BringBack", 2000); bringBackWaiters.put(ticket, waiter); Log.v("TrackLifeSync:BringBack", "Ticket: " + ticket); activity.startActivity(i); waiter.waitNow(); bringBackWaiters.remove(ticket); } else { final Waiter waiter = new Waiter("BringBack"); TestActivity.Proxy proxy = new TestActivity.Proxy() { @Override protected void onResumeFragments() { waiter.skip(); activity.removeProxy(this); } }; activity.addProxy(proxy); activity.startActivity(i); TestActivity.State state = activity.getState(); if (state != null && state.ordinal() >= TestActivity.State.RESUME_FRAGMENTS.ordinal()) { //ready activity.removeProxy(proxy); } else { waiter.waitNow(); } } try { waitTest(); } catch (InterruptedException e) { e.printStackTrace(); } } protected void waitActivityResume(final TestActivity activity) { final Waiter waiter = new Waiter("ActivityResume"); TestActivity.Proxy proxy = new TestActivity.Proxy() { @Override protected void onResume() { waiter.skip(); } }; activity.addProxy(proxy); waiter.waitNow(); activity.removeProxy(proxy); } protected void startActivity(Intent intent) { activity.startActivity(intent); try { Thread.sleep(300); } catch (InterruptedException e) { e.printStackTrace(); } } protected void rotateMainActivity(final int orientation) { final Waiter waiter = new Waiter("Rotate", 500); activity.delegateFragment.registerOnViewReadyListener(new Runnable() { @Override public void run() { waiter.skip(); activity.delegateFragment.unregisterOnViewReadyListener(this); } }); activity.setRequestedOrientation(orientation); waiter.waitNow(); } @SuppressLint("NewApi") @SuppressWarnings("deprecation") protected boolean isDontKeepActivities() throws Settings.SettingNotFoundException { try { int val; if (Build.VERSION.SDK_INT > 16) { val = Settings.System.getInt(activity.getContentResolver(), Settings.Global.ALWAYS_FINISH_ACTIVITIES); } else { val = Settings.System.getInt(activity.getContentResolver(), Settings.System.ALWAYS_FINISH_ACTIVITIES); } return val != 0; } catch (Exception e) { return false; } } /** * Thread sleeps for 0 ms by default * * @throws InterruptedException */ protected void waitTest() throws InterruptedException { getInstrumentation().waitForIdleSync(); } }