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