/* * 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 android.testing; import static org.junit.Assert.assertNotNull; import android.annotation.Nullable; import android.app.Fragment; import android.app.FragmentController; import android.app.FragmentHostCallback; import android.app.FragmentManagerNonConfig; import android.content.Context; import android.graphics.PixelFormat; import android.os.Bundle; import android.os.Handler; import android.os.Parcelable; import android.view.LayoutInflater; import android.view.View; import android.view.WindowManager; import android.view.WindowManager.LayoutParams; import android.widget.FrameLayout; import androidx.test.InstrumentationRegistry; import org.junit.After; import org.junit.Before; import org.junit.Rule; import org.junit.Test; import java.io.FileDescriptor; import java.io.PrintWriter; /** * Base class for fragment class tests. Just adding one for any fragment will push it through * general lifecycle events and ensure no basic leaks are happening. This class also implements * the host for subclasses, so they can push it into desired states and do any unit testing * required. */ public abstract class BaseFragmentTest { private static final int VIEW_ID = 42; private final Class<? extends Fragment> mCls; protected Handler mHandler; protected FrameLayout mView; protected FragmentController mFragments; protected Fragment mFragment; @Rule public final TestableContext mContext = getContext(); public BaseFragmentTest(Class<? extends Fragment> cls) { mCls = cls; } protected void createRootView() { mView = new FrameLayout(mContext); } @Before public void setupFragment() throws Exception { createRootView(); mView.setId(VIEW_ID); assertNotNull("BaseFragmentTest must be tagged with @RunWithLooper", TestableLooper.get(this)); TestableLooper.get(this).runWithLooper(() -> { mHandler = new Handler(); mFragment = instantiate(mContext, mCls.getName(), null); mFragments = FragmentController.createController(new HostCallbacks()); mFragments.attachHost(null); mFragments.getFragmentManager().beginTransaction() .replace(VIEW_ID, mFragment) .commit(); }); } /** * Allows tests to sub-class TestableContext if they want to provide any extended functionality * or provide a {@link LeakCheck} to the TestableContext upon instantiation. */ protected TestableContext getContext() { return new TestableContext(InstrumentationRegistry.getContext()); } @After public void tearDown() throws Exception { if (mFragments != null) { // Set mFragments to null to let it know not to destroy. TestableLooper.get(this).runWithLooper(() -> mFragments.dispatchDestroy()); } } @Test public void testCreateDestroy() { mFragments.dispatchCreate(); processAllMessages(); destroyFragments(); } @Test public void testStartStop() { mFragments.dispatchStart(); processAllMessages(); mFragments.dispatchStop(); processAllMessages(); } @Test public void testResumePause() { mFragments.dispatchResume(); processAllMessages(); mFragments.dispatchPause(); processAllMessages(); } @Test public void testAttachDetach() { WindowManager.LayoutParams lp = new WindowManager.LayoutParams( LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT, LayoutParams.TYPE_SYSTEM_ALERT, 0, PixelFormat.TRANSLUCENT); mFragments.dispatchResume(); processAllMessages(); attachFragmentToWindow(); detachFragmentToWindow(); mFragments.dispatchPause(); processAllMessages(); } @Test public void testRecreate() { mFragments.dispatchResume(); processAllMessages(); recreateFragment(); processAllMessages(); } @Test public void testMultipleResumes() { mFragments.dispatchResume(); processAllMessages(); mFragments.dispatchStop(); processAllMessages(); mFragments.dispatchResume(); processAllMessages(); } protected void recreateFragment() { mFragments.dispatchPause(); Parcelable p = mFragments.saveAllState(); mFragments.dispatchDestroy(); mFragments = FragmentController.createController(new HostCallbacks()); mFragments.attachHost(null); mFragments.restoreAllState(p, (FragmentManagerNonConfig) null); mFragments.dispatchResume(); mFragment = mFragments.getFragmentManager().findFragmentById(VIEW_ID); } protected void attachFragmentToWindow() { ViewUtils.attachView(mView); TestableLooper.get(this).processAllMessages(); } protected void detachFragmentToWindow() { ViewUtils.detachView(mView); TestableLooper.get(this).processAllMessages(); } protected void destroyFragments() { mFragments.dispatchDestroy(); processAllMessages(); mFragments = null; } protected void processAllMessages() { TestableLooper.get(this).processAllMessages(); } /** * Method available for override to replace fragment instantiation. */ protected Fragment instantiate(Context context, String className, @Nullable Bundle arguments) { return Fragment.instantiate(context, className, arguments); } private View findViewById(int id) { return mView.findViewById(id); } private class HostCallbacks extends FragmentHostCallback<BaseFragmentTest> { public HostCallbacks() { super(mContext, BaseFragmentTest.this.mHandler, 0); } @Override public BaseFragmentTest onGetHost() { return BaseFragmentTest.this; } @Override public void onDump(String prefix, FileDescriptor fd, PrintWriter writer, String[] args) { } @Override public Fragment instantiate(Context context, String className, Bundle arguments) { return BaseFragmentTest.this.instantiate(context, className, arguments); } @Override public boolean onShouldSaveFragmentState(Fragment fragment) { return true; // True for now. } @Override public LayoutInflater onGetLayoutInflater() { return LayoutInflater.from(mContext); } @Override public boolean onUseFragmentManagerInflaterFactory() { return true; } @Override public boolean onHasWindowAnimations() { return false; } @Override public int onGetWindowAnimations() { return 0; } @Override public void onAttachFragment(Fragment fragment) { } @Nullable @Override public View onFindViewById(int id) { return BaseFragmentTest.this.findViewById(id); } @Override public boolean onHasView() { return true; } } }