In case of a test that crosses multiple activities, is there a way to get current activity?
getActivtiy() method just gives one activity that was used to start the test.
I tried something like below,
public Activity getCurrentActivity() {
Activity activity = null;
ActivityManager am = (ActivityManager) this.getActivity().getSystemService(Context.ACTIVITY_SERVICE);
List<ActivityManager.RunningTaskInfo> taskInfo = am.getRunningTasks(1);
try {
Class<?> myClass = taskInfo.get(0).topActivity.getClass();
activity = (Activity) myClass.newInstance();
}
catch (Exception e) {
}
return activity;
}
but I get null object.
In Espresso, you can use ActivityLifecycleMonitorRegistry but it is not officially supported, so it may not work in future versions.
Here is how it works:
Activity getCurrentActivity() throws Throwable {
getInstrumentation().waitForIdleSync();
final Activity[] activity = new Activity[1];
runTestOnUiThread(new Runnable() {
#Override
public void run() {
java.util.Collection<Activity> activities = ActivityLifecycleMonitorRegistry.getInstance().getActivitiesInStage(Stage.RESUMED);
activity[0] = Iterables.getOnlyElement(activities);
}});
return activity[0];
}
If all you need is to make the check against current Activity, use may get along with native Espresso one-liner to check that expected intent was launched:
intended(hasComponent(new ComponentName(getTargetContext(), ExpectedActivity.class)));
Espresso will also show you the intents fired in the meanwhile if not matching yours.
The only setup you need is to replace ActivityTestRule with IntentsTestRule in the test to let it keep track of the intents launching. And make sure this library is in your build.gradle dependencies:
androidTestCompile 'com.android.support.test.espresso:espresso-intents:2.2.1'
I like #Ryan's version as it doesn't use undocumented internals, but you can write this even shorter:
private Activity getCurrentActivity() {
final Activity[] activity = new Activity[1];
onView(isRoot()).check(new ViewAssertion() {
#Override
public void check(View view, NoMatchingViewException noViewFoundException) {
activity[0] = (Activity) view.getContext();
}
});
return activity[0];
}
Please be aware, though that this will not work when running your tests in Firebase Test Lab. That fails with
java.lang.ClassCastException: com.android.internal.policy.DecorContext cannot be cast to android.app.Activity
The Android team has replaced ActivityTestRule with ActivityScenario. We could do activityTestRule.getActivity() with ActivityTestRule but not with ActivityScenario. Here is my work around solution for getting an Activity from ActivityScenario (inspired by #Ryan and #Fabian solutions)
#get:Rule
var activityRule = ActivityScenarioRule(MainActivity::class.java)
...
private fun getActivity(): Activity? {
var activity: Activity? = null
activityRule.scenario.onActivity {
activity = it
}
return activity
}
I couldn't get any of the other solutions to work, so I ended up having to do this:
Declare your ActivityTestRule:
#Rule
public ActivityTestRule<MainActivity> mainActivityTestRule =
new ActivityTestRule<>(MainActivity.class);
Declare a final Activity array to store your activities:
private final Activity[] currentActivity = new Activity[1];
Add a helper method to register with the application context to get lifecycle updates:
private void monitorCurrentActivity() {
mainActivityTestRule.getActivity().getApplication()
.registerActivityLifecycleCallbacks(new Application.ActivityLifecycleCallbacks() {
#Override
public void onActivityCreated(final Activity activity, final Bundle savedInstanceState) { }
#Override
public void onActivityStarted(final Activity activity) { }
#Override
public void onActivityResumed(final Activity activity) {
currentActivity[0] = activity;
}
#Override
public void onActivityPaused(final Activity activity) { }
#Override
public void onActivityStopped(final Activity activity) { }
#Override
public void onActivitySaveInstanceState(final Activity activity, final Bundle outState) { }
#Override
public void onActivityDestroyed(final Activity activity) { }
});
}
Add a helper method to get the current activity
private Activity getCurrentActivity() {
return currentActivity[0];
}
So, once you've launched your first activity, just call monitorCurrentActivity() and then whenever you need a reference to the current activity you just call getCurrentActivity()
public static Activity getActivity() {
final Activity[] currentActivity = new Activity[1];
Espresso.onView(AllOf.allOf(ViewMatchers.withId(android.R.id.content), isDisplayed())).perform(new ViewAction() {
#Override
public Matcher<View> getConstraints() {
return isAssignableFrom(View.class);
}
#Override
public String getDescription() {
return "getting text from a TextView";
}
#Override
public void perform(UiController uiController, View view) {
if (view.getContext() instanceof Activity) {
Activity activity1 = ((Activity)view.getContext());
currentActivity[0] = activity1;
}
}
});
return currentActivity[0];
}
I improved #Fabian Streitel answer so you can use this method without ClassCastException
public static Activity getCurrentActivity() {
final Activity[] activity = new Activity[1];
onView(isRoot()).check((view, noViewFoundException) -> {
View checkedView = view;
while (checkedView instanceof ViewGroup && ((ViewGroup) checkedView).getChildCount() > 0) {
checkedView = ((ViewGroup) checkedView).getChildAt(0);
if (checkedView.getContext() instanceof Activity) {
activity[0] = (Activity) checkedView.getContext();
return;
}
}
});
return activity[0];
}
Based on https://stackoverflow.com/a/50762439/6007104 here's a Kotlin version of a generic util for accessing current Activity:
class CurrentActivityDelegate(application: Application) {
private var cachedActivity: Activity? = null
init {
monitorCurrentActivity(application)
}
fun getCurrentActivity() = cachedActivity
private fun monitorCurrentActivity(application: Application) {
application.registerActivityLifecycleCallbacks(
object : Application.ActivityLifecycleCallbacks {
override fun onActivityResumed(activity: Activity) {
cachedActivity = activity
Log.i(TAG, "Current activity updated: ${activity::class.simpleName}")
}
override fun onActivityCreated(activity: Activity?, savedInstanceState: Bundle?) {}
override fun onActivityStarted(activity: Activity?) {}
override fun onActivityPaused(activity: Activity?) {}
override fun onActivityStopped(activity: Activity?) {}
override fun onActivitySaveInstanceState(activity: Activity?, outState: Bundle?) {}
override fun onActivityDestroyed(activity: Activity?) {}
})
}
}
And then simply use like so:
#Before
fun setup() {
currentActivityDelegate = CurrentActivityDelegate(activityTestRule.activity.application)
}
If you have the only Activity in your test case, you can do:
1. declare you test Rule
#Rule
public ActivityTestRule<TestActivity> mActivityTestRule = new ActivityTestRule<>(TestActivity.class);
2. get you Activity:
mActivityTestRule.getActivity()
That's a piece of pie!
Solution proposed by #lacton didn't work for me, probably because activity was not in a state that was reported by ActivityLifecycleMonitorRegistry.
I even tried Stage.PRE_ON_CREATE still didn't get any activity.
Note: I could not use the ActivityTestRule or IntentTestRule because I was starting my activity using activitiy-alias and didn't make any sense to use the actual class in the tests when I want to test to see if the alias works.
My solution to this was subscribing to lifecycle changes through ActivityLifecycleMonitorRegistry and blocking the test thread until activity is launched:
// NOTE: make sure this is a strong reference (move up as a class field) otherwise will be GCed and you will not stably receive updates.
ActivityLifecycleCallback lifeCycleCallback = new ActivityLifecycleCallback() {
#Override
public void onActivityLifecycleChanged(Activity activity, Stage stage) {
classHolder.setValue(((MyActivity) activity).getClass());
// release the test thread
lock.countDown();
}
};
// used to block the test thread until activity is launched
final CountDownLatch lock = new CountDownLatch(1);
final Holder<Class<? extends MyActivity>> classHolder = new Holder<>();
instrumentation.runOnMainSync(new Runnable() {
#Override
public void run() {
ActivityLifecycleMonitorRegistry.getInstance().addLifecycleCallback(lifeCycleCallback);
}
});
// start the Activity
intent.setClassName(context, MyApp.class.getPackage().getName() + ".MyActivityAlias");
context.startActivity(intent);
// wait for activity to start
lock.await();
// continue with the tests
assertTrue(classHolder.hasValue());
assertTrue(classHolder.getValue().isAssignableFrom(MyActivity.class));
Holder is basically a wrapper object. You can use an array or anything else to capture a value inside the anonymous class.
The accepted answer may not work in many espresso tests. The following works with espresso version 2.2.2 and Android compile/target SDK 27 running on API 25 devices:
#Nullable
private Activity getActivity() {
Activity currentActivity = null;
Collection resumedActivities = ActivityLifecycleMonitorRegistry.getInstance().getActivitiesInStage(RESUMED);
if (resumedActivities.iterator().hasNext()){
currentActivity = (Activity) resumedActivities.iterator().next();
}
return currentActivity;
}
Related
I have activity and two fragments which opened in this activity (for example FragmentFirst, FragmentSecond). In my activity I register BroadcastReceiver. And when i receive any event (inside onReceive) I need to display the received text in the TextView of second fragment.
I could register the BroadcastReceiver in the second fragment, however, the event may come at the moment when my second fragment opens and then I do not have time to register the BroadcastReceiver and I lose the event.
So my code inside the onReceive of activity looks like this:
#Override
public void onReceive(Context context, Intent intent) {
String receivedMessage = intent.getStringExtra(MY_MESSAGE);
Fragment currentFragment = getSupportFragmentManager().findFragmentById(R.id.container);
if (currentFragment instanceof FragmentSecond) {
((FragmentSecond) currentFragment).initMessage(receivedMessage);
}
}
And now I have a question, can there be such a situation that the current fragment is FragmentSecond, but the view of this fragment has not yet been created. In this case, I can get a NPE when calling a initMessage of my FragmentSecond that sets the receivedMessage for the TextView.
If this is really possible, then I will have to add some kind of checks inside the initMessage:
public void initMessage(String receivedMessage) {
if (isViewCreated) { // flag to detect when view was created
tvMessage.setText(receivedMessage);
savedMessage = "";
} else {
savedMessage = receivedMessage; // save message to display it when view will be created
}
}
In practice, I was not able to reproduce such a situation, but it seems to me that this is possible. Please tell me if this is possible?
P.S. I get the feeling that the onReceive controls the life cycle and is called exactly when the fragment view is created. Because I tried to send a broadcast until the moment when the FragmentSecond view was created, but until the onViewCreated of my FragmentSecond was called, onReceive was not called. However, I may be wrong
You can store the message in the Activity and use it the fragment WHEN fragment is ready.
Activity
String message = null;
#Override
public void onReceive(Context context, Intent intent) {
message = intent.getStringExtra(MY_MESSAGE);
}
You can "touch" the Activity in onAttach(context) method of the fragment.
Fragment
private String message = null;
void onAttach(Context: context) {
super.onAttach(context)
if (context instanceof MainActivity) {
MainActivity mainActivity = (MainActivity) context;
message = mainActivity.message;
}
}
That is for grabbing the latest value.
Since you are operating in Java is it a bit more verbose but for updates you can use Observer pattern(create listener interface which the fragments will implement and register themselves to the activity).
interface MessageObserver {
void onMessageChange(String message);
}
Use WeakReference for the Activity so it doesn't leak the context.
Fragment
private WeakReference<MainActivity> mWeakRefActivity;
#Override
public void onAttach(#NonNull Context context) {
super.onAttach(context);
if (context instanceof MainActivity) {
MainActivity mainActivity = (MainActivity) context;
mWeakRefActivity = new WeakReference<MainActivity>(mainActivity);
mainActivity.addObserver(this);
}
}
#Override
public void onDetach() {
// Clean up after yourself
mWeakRefActivity.get().removeObserver(this);
super.onDetach();
}
Activity
ArrayList<MessageObserver> mObservers = new ArrayList<MessageObserver>();
void addObserver(MessageObserver observer) {
mObservers.add(observer);
}
void removeObserver(MessageObserver observer) {
mObservers.remove(observer);
}
public void onReceive(Context context, Intent intent) {
message = intent.getStringExtra(MY_MESSAGE);
for (MessageObserver observer : mObservers) {
observer.onMessageChange(message);
}
}
Android Studio 3.2 Canary 18
kotlin_version = 1.2.50
I have a simple app that uses a recyclerview and adapter. When the app starts is load all the data.
However, when I click the back button and start the app again. It won't display the data (blank).
If I clear the app from memory and start the app. The data will load as normal.
I am loading the data from sqlite and the data is loaded each time. as it populates the insectDataModelList.
After going into the RecyclerView.java source code the reason is the mAdapter is null. However, I have
checked that the adapter is correct when I set it to the recyclerview.
void dispatchLayout() {
if (mAdapter == null) {
Log.e(TAG, "No adapter attached; skipping layout");
// leave the state in START
return;
}
...
}
My MainActivity.java is Java
public class MainActivity extends AppCompatActivity {
private RecyclerView rvInsects;
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
setSupportActionBar(toolbar);
rvInsects = (RecyclerView)findViewById(R.id.recycler_view);
DatabaseManager databaseManager = DatabaseManager.getInstance(this);
databaseManager.queryAllInsects("friendlyName");
}
private void setupAdapter(List<InsectDataModel> insectDataModelList) {
final LayoutManager layoutManager = new LinearLayoutManager(
this, LinearLayoutManager.VERTICAL, false);
rvInsects.setLayoutManager(layoutManager);
rvInsects.setHasFixedSize(true);
final InsectAdapter insectAdapter = new InsectAdapter(insectDataModelList);
rvInsects.setAdapter(insectAdapter);
insectAdapter.notifyDataSetChanged();
}
/* Callback from database */
public void loadAllInsects(final Cursor cursor) {
InsectInteractorMapper insectInteractorMapper = new InsectInteractorMapperImp();
final List<InsectDataModel> insectDataModelList = insectInteractorMapper.map(cursor);
/* data loaded with 24 items */
setupAdapter(insectDataModelList);
}
}
InsectAdapter.kt is Kotlin.
class InsectAdapter(private val insectList: MutableList<InsectDataModel>)
: RecyclerView.Adapter<InsectAdapter.CustomInsectHolder>() {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): CustomInsectHolder {
val view = LayoutInflater.from(parent.context)
.inflate(R.layout.insect_row_item, parent, false)
return CustomInsectHolder(view)
}
override fun onBindViewHolder(holder: CustomInsectHolder, position: Int) {
holder.tvFriendlyName.text = insectList[position].friendlyName
holder.tvScientificName.text = insectList[position].scientificName
}
override fun getItemCount(): Int {
return insectList.size
}
class CustomInsectHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
val ivDangerLevel: DangerLevelView = itemView.findViewById(R.id.ivDangerLevel)
val tvFriendlyName: TextView = itemView.findViewById(R.id.tvFriendlyName)
val tvScientificName: TextView = itemView.findViewById(R.id.tvScientificName)
}
}
The database I use rxjava2 to do the query
public class DatabaseManager {
private static DatabaseManager sInstance;
private MainActivity mainActivity;
private BugsDbHelper mBugsDbHelper;
public static synchronized DatabaseManager getInstance(MainActivity context) {
if (sInstance == null) {
sInstance = new DatabaseManager(context);
}
return sInstance;
}
private DatabaseManager(MainActivity context) {
mBugsDbHelper = new BugsDbHelper(context);
mainActivity = context;
}
#SuppressLint("CheckResult")
public void queryAllInsects(String sortOrder) {
final InsectStorageInteractorImp insectStorageInteractorImp
= new InsectStorageInteractorImp(new InsectStorageImp(mBugsDbHelper.getReadableDatabase()));
insectStorageInteractorImp.getAllSortedInsects(sortOrder)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(new SingleObserver<Cursor>() {
Disposable disposable;
#Override
public void onSubscribe(Disposable d) {
disposable = d;
}
#Override
public void onSuccess(Cursor cursor) {
mainActivity.loadAllInsects(cursor);
disposable.dispose();
}
#Override
public void onError(Throwable e) {
disposable.dispose();
}
});
}
}
Everything works as expected when the apps installs for the first time. And if you clear it out of memory.
However, its only when you click the back button, and then try and start the app it will not load any data
because of the mAdapter being null in the RecyclerView class.
When I click the back button and then start the app again. All I get is a blank screen i.e.
Updated DatabaseManager class that removes the singleton and used a weakreference to ensure that the MainActivity instance is garbage collected.
public class DatabaseManager {
private WeakReference<MainActivity> mainActivity;
private BugsDbHelper mBugsDbHelper;
public DatabaseManager(MainActivity context) {
mBugsDbHelper = new BugsDbHelper(context);
mainActivity = new WeakReference<>(context);
}
#SuppressLint("CheckResult")
public void queryAllInsects(String sortOrder) {
final InsectStorageInteractorImp insectStorageInteractorImp
= new InsectStorageInteractorImp(new InsectStorageImp(mBugsDbHelper.getReadableDatabase()));
insectStorageInteractorImp.getAllSortedInsects(sortOrder)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(new SingleObserver<Cursor>() {
Disposable disposable;
#Override
public void onSubscribe(Disposable d) {
disposable = d;
}
#Override
public void onSuccess(Cursor cursor) {
mainActivity.loadAllInsects(cursor);
disposable.dispose();
}
#Override
public void onError(Throwable e) {
disposable.dispose();
}
});
}
}
Many thanks for any suggestions,
When you click the back button and relaunch the app, a new instance of MainActivity is started.
At the same time, your DatabaseManager is a singleton. Its reference is stored as a static variable. It survives the activity recreation. It will live until the process is killed.
So, when you run queryAllInsects for the second time, the callback is sent to the old instance of MainActivity, which is not visible anymore.
You should not keep a reference to MainActivity in DatabaseManager. It's a memory leak, because it cannot be garbage collected.
The issue is most likely that you are loading the data in your onCreate() and not in onResume(). When you press back to "close the app" you are not necessarily clearing the UI stack from memory. That's why when you go back into the app, it doesn't invoke onCreate() again, and doesn't load your data again.
Keep everything the same, just move your data loading from onCreate() to onResume(). That way, whenever the screen is shown to the user, the data will load.
Few observations:
You are still passing the MainActivity to the BugsDbHelper class, take care of the reference there.
It's probably a good idea to include a "cleaning method" in Singleton classes, which should be called in onStop() or onDestroy() of an activity. onStop() is preferred since onDestroy() is not guaranteed to be called immediately.
The "cleaning method" in Singleton class should do the following:
a) Nullify any references to the parameters, objects, context or callbacks you have asked as a dependency in the constructor or otherwise.
b) If the Singleton class has created "new" objects with context dependencies, make sure to include similar cleaning methods in these classes too.
To avoid crashes and memory leakage in fragment/activities, make sure you are cleaning up your recycler view/adapter in onStop(). The callbacks can be received anytime, and if that happens while your activity is in the background, you are bound to get a "force close" fortune cookie.
Keep an eye on the activity/fragment lifecycle. A lot of issues are just because of ignoring the lifecycle callbacks. These are there for a reason, utilize them.
Put this 2 lines in onResume() and remove from onCreate() and try it.
DatabaseManager databaseManager = DatabaseManager.getInstance(this);
databaseManager.queryAllInsects("friendlyName");
I suggest the following changes:
MainActivity, the less code you write in the activity the better, move all the data retrieval part to the DatabaseManager. Also setup the RecyclerView once and only update the dataset when appropriate:
public class MainActivity extends AppCompatActivity {
private List<InsectDataModel> insectDataModelList = new ArrayList<>();
private Disposable disposable;
private RecyclerView rvInsects;
private InsectAdapter insectAdapter;
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
setSupportActionBar(toolbar);
setupAdapter();
//Request Data, take advantage of RxJava to load data asynchronously
DatabaseManager.getInstance(this)
.queryAllInsects("friendlyName")
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(new SingleObserver<List<InsectDataModel>>() {
#Override
public void onSubscribe(Disposable d) {
disposable = d;
}
#Override
public void onSuccess(List<InsectDataModel> response) {
insectDataModelList.clear();
insectDataModelList.addAll(response);
insectAdapter.notifyDatasetChanged();
}
#Override
public void onError(Throwable e) {
Toast.makeText(this, e.getMessage(), Toast.LENGTH_SHORT).show();
}
});;
}
private void setupAdapter() {
//Setup RecyclerView Only need to be called once
rvInsects = (RecyclerView)findViewById(R.id.recycler_view);
LayoutManager layoutManager = new LinearLayoutManager(this); // LinearLayoutManager is Vertical by default
rvInsects.setLayoutManager(layoutManager); // You don't event have to define it as RecyclerView use LinearLayoutManager.Vertical by default
rvInsects.setHasFixedSize(true);
insectAdapter = new InsectAdapter(insectDataModelList);
rvInsects.setAdapter(insectAdapter);
}
#Override
protected void onDestroy() {
//Dispose observer if activity is destroyed to prevent memory leak
if(disposable != null && !disposable.isDisposed())
disposable.dispose();
super.onDestroy();
}
}
And in DatabaseManager, instead of observing the data source(Cursor) and notify the requester(Activity) via callback, we get the data stream and pass it the caller to observe:
public class DatabaseManager {
private static DatabaseManager sInstance;
private BugsDbHelper mBugsDbHelper;
public static synchronized DatabaseManager getInstance() {
if (sInstance == null) {
sInstance = new DatabaseManager();
}
return sInstance;
}
private DatabaseManager() {
// Move the actualy database initiation to application class or singleton
mBugsDbHelper = BugsDbHelper.getInstance(); // or ApplicationController.getDbHelper();
}
#SuppressLint("CheckResult")
public SingleObserver<List<InsectDataModel>> queryAllInsects(String sortOrder) {
final InsectStorageInteractorImp insectStorageInteractorImp
= new InsectStorageInteractorImp(new InsectStorageImp(mBugsDbHelper.getReadableDatabase()));
insectStorageInteractorImp.getAllSortedInsects(sortOrder)
.map(new Function<Cursor, List<Object>>() {
#Override
public List<Object> apply(Cursor cursor) throws Exception {
InsectInteractorMapper insectInteractorMapper = new InsectInteractorMapperImp();
return insectInteractorMapper.map(cursor);
}
});
}
}
Now the solution here is to rely on the RxJava to change the callback pattern to the observer pattern. So instead of passing the activity (callback) and waiting to be called, we get the data steram (observable) and observe it for the response. This eliminate the leak problem all together and enhance the readability and maintainability.
Also don't forget to move the Database initialization to the Application class or a Singleton instance to prevent multiple instantiation. The easier solution would be like:
public class ApplicationController extends Application {
private BugsDbHelper mBugsDbHelper;
#Override
public void onCreate() {
super.onCreate();
mBugsDbHelper = new BugsDbHelper(this);
}
public BugsDbHelper getDbHelper(){
return mBugsDbHelper ;
}
}
I would like to check that my app shows an error message when the device it is running on has no camera. I have tried passing in a mock context but mockito gives an error when I try to mock the CameraManager class as it is declared final. Surely android has a simple solution for this? Here's my attempt:
public class CreateNewIdentityActivityUnitTest extends ActivityUnitTestCase<CreateNewIdentityActivity> {
public CreateNewIdentityActivityUnitTest() {
super(CreateNewIdentityActivity.class);
}
public void testErrorMessageDisplayedWhenNoCamerasExist() throws Exception {
// Create the mock cameramanager
// THIS LINE FAILS BECAUSE CAMERAMANAGER IS FINAL
CameraManager mockCameraManager = mock(CameraManager.class);
String[] cameraIdList = {};
when(mockCameraManager.getCameraIdList()).thenReturn(cameraIdList);
// Create the mock context
Context mockContext = mock(Context.class);
when(mockContext.getSystemService(Context.CAMERA_SERVICE)).thenReturn(mockCameraManager);
// Start the activity
Intent intent = new Intent(mockContext, CreateNewIdentityActivity.class);
Activity activity = startActivity(intent, null, null);
// Verify that the error message was made visible
TextView errorTextView = (TextView)activity.findViewById(R.id.ErrorTextView);
assertNotNull(errorTextView);
assertEquals(View.VISIBLE, errorTextView.getVisibility());
}
}
Unfortunately, you can't mock final class.
There're few options/hacks:
Try to add Robolectric library and write test with it's ShadowCamera
Move logic related to CameraManager into a separate class and inject it in Activity. Then in the Test project, you can override this injection.
Pretty similar idea - create an interface OnCameraManagerInterface
public interface OnCameraManagerInterface {
String[] getListOfCameras() throws CameraAccessException;
}
Then implement it in the Activity:
public class CreateNewIdentityActivity extends AppCompatActivity
implements OnCameraManagerInterface {
.......
#TargetApi(Build.VERSION_CODES.LOLLIPOP)
#Override
public String[] getListOfCameras() throws CameraAccessException {
return ((CameraManager)getSystemService(Context.CAMERA_SERVICE)).
getCameraIdList();
}
}
And in the code, where you check camera existence - call: if (getListOfCameras().length == 0) {}
Now, add new TestCreateNewIdentityActivity to override your CreateNewIdentityActivity:
public class TestCreateNewIdentityActivity extends CreateNewIdentityActivity {
#Override
public String[] getListOfCameras() throws CameraAccessException {
return new String[0];
}
}
In Manifest:
<activity android:name=".TestCreateNewIdentityActivity"
android:theme="#style/AppTheme.NoActionBar"/>
And test will look like:
public class CreateNewIdentityActivityUnitTest extends ActivityUnitTestCase<TestCreateNewIdentityActivity> {
public CreateNewIdentityActivityUnitTest() {
super(TestCreateNewIdentityActivity.class);
}
public void testErrorMessageDisplayedWhenNoCamerasExist() throws Exception {
// Verify that the error message was made visible
TextView errorTextView = (TextView)getActivity().findViewById(R.id.ErrorTextView);
assertNotNull(errorTextView);
assertEquals(View.VISIBLE, errorTextView.getVisibility());
}
}
I'm pretty sure, it doable even without adding the TestActivity into the main source code and to manifest(to keep it in androidTest, though I didn't look)
Hybrid variant without creation of new activity:
public class ActivityCameraManager {
private boolean isTest = false;
private CameraManager cameraManager;
public ActivityCameraManager(CameraManager cameraManager) {
this.cameraManager = cameraManager;
}
#TargetApi(Build.VERSION_CODES.LOLLIPOP)
public String[] getListOfCameras() throws CameraAccessException {
if (isTest) {
return new String[0];
}
return cameraManager.getCameraIdList();
}
public void setTestMode() {
isTest = true;
}
}
Then your activity is gonna look like:
public class MainActivity extends AppCompatActivity {
ActivityCameraManager activityCameraManager;
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
...
activityCameraManager = new ActivityCameraManager((CameraManager) getSystemService(Context.CAMERA_SERVICE));
}
public void setActivityCameraManagerForTest() {
activityCameraManager.setTestMode();
}
}
And in test just call getActivity().setActivityCameraManagerForTest();
I hope, it helps
The answer is late but here is a mock camera example for Android.
You can set the VideoFileInputSource to mock camera from video file
textureVideoInputSource = new VideoFileInputSource(this, "mock_input_video.mp4");
or you can enable hardware camera for video stream.
textureVideoInputSource = new CameraTextureVideoInputSource(this);
You can refer the answer here.
https://stackoverflow.com/a/38456189/1053097
I can easily detect when Fragments are attached to Activity via Activity.onAttachFragment()
But how can I detect in Activity that some Fragment is detached from activity?
There is no Activity.onDetachFragment()
Is subcclasing Fragment and write some code to notify Activity about that state is the only solution?
you can use interface for communicating between Fragment and Activity
something like this :
public Class MyFragment extends Fragment {
FragmentCommunicator communicator;
public void setCommunicator(FragmentCommunicator communicator) {
this.communicator = communicator;
}
#Override
public void OnDetach() {
communicator.fragmentDetached();
}
...
public Interface FragmentCommunicator {
public void fragmentDetached();
}
}
and in your activity :
public Class MyActivity extends Activity Implements FragmentCommunicator {
...
MyFragment fragment = new MyFragment();
fragment.setCommunicator(this);
...
#Override
public void fragmentDetached() {
//Do what you want!
}
}
Edit:
the new approach is setting interface instance in onAttach.
public void onAttach(Activity activity) {
if (activity instanceof FragmentCommunicator) {
communicator = activity;
} else {
throw new RuntimeException("activity must implement FragmentCommunicator");
}
}
now there is no need to have setCommunicator method.
Mohammad's original answer is close to I would do. He has since updated it to leverage a mechanism provided by Android - Fragment.onAttach(Context context).In that approach, the fragment grabs components (ie, the activity) from the system and calls into it. This breaks inversion of control.
Here is my preferred approach:
public class MyActivity extends AppCompatActivity {
#Override
public void onAttachFragment(Fragment fragment) {
super.onAttachFragment(fragment);
if (fragment instanceof MyFragment) {
((MyFragment) fragment).setListener(mMyFragmentListener);
}
}
private final MyFragment.Listener mMyFragmentListener = new MyFragment.Listener() {
#Override
public void onDetached(MyFragment fragment) {
fragment.setListener(null);
}
// implement other worker methods.
};
}
public class MyFragment extends Fragment {
#Nullable
private Listener mListener;
public void setListener(#Nullable Listener listener) {
mListener = listener;
}
public interface Listener {
void onDetached(MyFragment fragment);
// declare more worker methods here that leverage the connection.
}
#Override
public void onDetach() {
super.onDetach();
if (mListener != null) {
mListener.onDetached(this);
}
}
}
In this solution, the fragment doesn't dictate it's surroundings. Some control is given the to fragment in that it breaks the connection itself. We also already don't own the detaching of the fragment anyways, so clearing the listener is really just cleanup.
Here is an alternative approach that is more explicit, less prone to developer error, but also creates extra boiler plate (I prefer the previous approach because the goodbye handshake feels like an unnecessary distraction):
public static class MyActivity extends AppCompatActivity {
#Override
public void onAttachFragment(Fragment fragment) {
super.onAttachFragment(fragment);
if (fragment instanceof MyFragment) {
((MyFragment) fragment).setListener(mMyFragmentListener);
}
}
private final MyFragment.Listener mMyFragmentListener = new MyFragment.Listener() {
#Override
public void onDetached(MyFragment fragment) {
fragment.setListener(null);
}
// implement other worker methods.
};
}
public static class MyFragment extends Fragment {
#Nullable
private Listener mListener;
public void setListener(#Nullable Listener listener) {
mListener = listener;
}
public interface Listener {
void onDetached(MyFragment fragment);
// declare more worker methods here that leverage the connection.
}
#Override
public void onDetach() {
super.onDetach();
if (mListener != null) {
mListener.onDetached(this);
}
}
}
You have a callback in the fragment life cycle. onDetach() is called when fragment is no longer attached to activity.
An alternative would be:
mFragmentManager.findFragmentByTag("Tag").getView()
If the view is null the fragment must be detached.
You can use ViewModel for update host activity. Shared ViewModel could be better choice than across the old listener based polymorphism model. You can follow the official documentation.
Data, fragment lifecyle etc. can be observable with shared viewmodel.
sealed class FragmentStates {
object Attached : FragmentStates()
object Started : FragmentStates()
object Stopped : FragmentStates()
object DeAtached : FragmentStates()
}
class FragmentStateViewModel : ViewModel() {
private val _fragmentState = MutableLiveData<FragmentStates>()
val fragmentStates: LiveData<FragmentStates> get() = _fragmentState
fun fragmentAttached() {
_fragmentState.value = FragmentStates.Attached
}
fun fragmentDeAtached() {
_fragmentState.value = FragmentStates.DeAtached
}
}
class HostActivity : AppCompatActivity() {
private val fragmentStateViewModel: FragmentStateViewModel by viewModels()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
fragmentStateViewModel.fragmentStates.observe(this, Observer {
when(it) {
FragmentStates.Attached -> {}
FragmentStates.Started -> {}
FragmentStates.Stopped -> {}
FragmentStates.DeAtached -> {}
}
})
}
}
class MyFragment: Fragment() {
private val fragmentStateViewModel: FragmentStateViewModel by activityViewModels()
override fun onAttach(context: Context) {
super.onAttach(context)
fragmentStateViewModel.fragmentAttached()
}
override fun onDetach() {
super.onDetach()
fragmentStateViewModel.fragmentDeAtached()
}
}
I have a firstActivity that launches the secondActivity, where in the secondActivity I have a loading Dialog (not AsyncTask), and I need to make Espresso wait until the dialog disappears before it continues with the test.
Where do I have to implement the IdlingResource? How can I make it wait for the dismissDialog() function?
Here is what I've tried to do:
class DocumentLoadingIdlingResource implements IdlingResource {
private ResourceCallback callback;
#Override
public String getName() {
return "Documnet loading idling resource";
}
#Override
public boolean isIdleNow() {
Activity activity;
try {
activity = getCurrentActivity();
} catch (Throwable e) {
return false;
}
if(activity.getClass().getName().equals(EditorActivity.class.getName())
&& activity.loadingDialogShowing() == false) {
return false;
}
return true;
}
#Override
public void registerIdleTransitionCallback(ResourceCallback callback) {
this.callback = callback;
}
}
Activity getCurrentActivity() throws Throwable {
getInstrumentation().waitForIdleSync();
final Activity[] activity = new Activity[1];
runTestOnUiThread(new Runnable() {
#Override
public void run() {
java.util.Collection<Activity> activites = ActivityLifecycleMonitorRegistry.getInstance().getActivitiesInStage(Stage.RESUMED);
activity[0] = com.google.common.collect.Iterables.getOnlyElement(activites);
}});
return activity[0];
}
This class is implemented in the test class.
There are a few problems here:
Your isIdleNow() calls getCurrentActivity() which calls waitForIdleSync() and runTestOnUiThread(). isIdleNow Javadoc says: "Espresso will always call this method from the main thread, therefore it should be non-blocking and return immediately." So this won't work as is, but you could call getActivitiesInStage directly from isIdleNow.
Your other issue is that you store the reference to ResourceCallback but never invoke onTransitionToIdle, also you should allow for the possibility of more than one ResourceCallback being registered and call onTransitionToIdle on all of the callbacks.
You can do the following:
Copy/Paste IdlingResource into your app as com.mycompany.IdlingResource.
Then have your Activity implement that interface and make sure to call onTransitionToIdle when the dialog goes away and make sure isIdleNow returns false iff the dialog is showing.
In your test code, write a "IdlingResourceAdapter" that wraps com.mycompany.IdlingResource and turns it into an Espresso IdlingResource and register that with Espresso.
This will be simpler once this issue is implemented: https://code.google.com/p/android-test-kit/issues/detail?id=71
I stumbled upon this question in my search for a similar answer. Using concepts from Stefano Dacchille's article on IdlingResources, I built the following idling resource that waits for a specific Activity to be active before firing. In my case, I know the dialog is showing when a fragment with a specific tag exists. This isn't the same as the OP's test, but the concepts should translate well.
public class BusyWhenFragmentExistsInActivityIdlingResource implements IdlingResource {
private FragmentActivity activity = null;
private final String fragmentTag;
private ResourceCallback resourceCallback;
private boolean wasIdleLastTime = true; // Start off as idle
private final String name;
// Need this strong reference because ActivityLifecycleMonitorRegistry won't hold one
private final ActivityLifecycleCallback activityLifecycleCallback;
public BusyWhenFragmentExistsInActivityIdlingResource(
final Class<? extends FragmentActivity> clazz,
final String fragmentTag
){
name = BusyWhenFragmentExistsInActivityIdlingResource.class.getSimpleName()+" "+clazz.getSimpleName();
this.fragmentTag = fragmentTag;
activityLifecycleCallback = new ActivityLifecycleCallback() {
#Override
public void onActivityLifecycleChanged(Activity activity, Stage stage) {
if (!FragmentActivity.class.isAssignableFrom(activity.getClass())) {
return;
}
FragmentActivity fragmentActivity = (FragmentActivity) activity;
if (!clazz.isAssignableFrom(fragmentActivity.getClass())) {
return;
}
switch (stage){
case RESUMED:
BusyWhenFragmentExistsInActivityIdlingResource.this.activity = fragmentActivity;
break;
case STOPPED:
BusyWhenFragmentExistsInActivityIdlingResource.this.activity = null;
break;
}
}
};
ActivityLifecycleMonitorRegistry.getInstance()
.addLifecycleCallback(activityLifecycleCallback);
}
#Override
public String getName() {
return name;
}
#Override
public boolean isIdleNow() {
if (activity==null) {
return wasIdleLastTime = true;
}
boolean isIdleThisTime = activity
.getSupportFragmentManager()
.findFragmentByTag(fragmentTag)==null;
if (!wasIdleLastTime && isIdleThisTime && resourceCallback!=null){
resourceCallback.onTransitionToIdle();
}
return wasIdleLastTime = isIdleThisTime;
}
#Override
public void registerIdleTransitionCallback(ResourceCallback resourceCallback) {
this.resourceCallback = resourceCallback;
}
}
To use it, add something similar to this to your test:
#Before
public void setUp() throws Exception {
registerIdlingResources(new BusyWhenFragmentExistsInActivityIdlingResource(
SomeOtherActivity.class,
BaseActivity.LOADING_DIALOG
));
}