1 /* 2 * Copyright (C) 2017 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file 5 * except in compliance with the License. You may obtain a copy of the License at 6 * 7 * http://www.apache.org/licenses/LICENSE-2.0 8 * 9 * Unless required by applicable law or agreed to in writing, software distributed under the 10 * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 11 * KIND, either express or implied. See the License for the specific language governing 12 * permissions and limitations under the License. 13 */ 14 15 package android.testing; 16 17 import static org.junit.Assert.assertNotNull; 18 19 import android.annotation.Nullable; 20 import android.app.Fragment; 21 import android.app.FragmentController; 22 import android.app.FragmentHostCallback; 23 import android.app.FragmentManagerNonConfig; 24 import android.content.Context; 25 import android.graphics.PixelFormat; 26 import android.os.Bundle; 27 import android.os.Handler; 28 import android.os.Parcelable; 29 import android.view.LayoutInflater; 30 import android.view.View; 31 import android.view.WindowManager; 32 import android.view.WindowManager.LayoutParams; 33 import android.widget.FrameLayout; 34 35 import androidx.test.InstrumentationRegistry; 36 37 import org.junit.After; 38 import org.junit.Before; 39 import org.junit.Rule; 40 import org.junit.Test; 41 42 import java.io.FileDescriptor; 43 import java.io.PrintWriter; 44 45 /** 46 * Base class for fragment class tests. Just adding one for any fragment will push it through 47 * general lifecycle events and ensure no basic leaks are happening. This class also implements 48 * the host for subclasses, so they can push it into desired states and do any unit testing 49 * required. 50 */ 51 public abstract class BaseFragmentTest { 52 53 private static final int VIEW_ID = 42; 54 private final Class<? extends Fragment> mCls; 55 protected Handler mHandler; 56 protected FrameLayout mView; 57 protected FragmentController mFragments; 58 protected Fragment mFragment; 59 60 @Rule 61 public final TestableContext mContext = getContext(); 62 BaseFragmentTest(Class<? extends Fragment> cls)63 public BaseFragmentTest(Class<? extends Fragment> cls) { 64 mCls = cls; 65 } 66 createRootView()67 protected void createRootView() { 68 mView = new FrameLayout(mContext); 69 } 70 71 @Before setupFragment()72 public void setupFragment() throws Exception { 73 createRootView(); 74 mView.setId(VIEW_ID); 75 76 assertNotNull("BaseFragmentTest must be tagged with @RunWithLooper", 77 TestableLooper.get(this)); 78 TestableLooper.get(this).runWithLooper(() -> { 79 mHandler = new Handler(); 80 81 mFragment = instantiate(mContext, mCls.getName(), null); 82 mFragments = FragmentController.createController(new HostCallbacks()); 83 mFragments.attachHost(null); 84 mFragments.getFragmentManager().beginTransaction() 85 .replace(VIEW_ID, mFragment) 86 .commit(); 87 }); 88 } 89 90 /** 91 * Allows tests to sub-class TestableContext if they want to provide any extended functionality 92 * or provide a {@link LeakCheck} to the TestableContext upon instantiation. 93 */ getContext()94 protected TestableContext getContext() { 95 return new TestableContext(InstrumentationRegistry.getContext()); 96 } 97 98 @After tearDown()99 public void tearDown() throws Exception { 100 if (mFragments != null) { 101 // Set mFragments to null to let it know not to destroy. 102 TestableLooper.get(this).runWithLooper(() -> mFragments.dispatchDestroy()); 103 } 104 } 105 106 @Test testCreateDestroy()107 public void testCreateDestroy() { 108 mFragments.dispatchCreate(); 109 processAllMessages(); 110 destroyFragments(); 111 } 112 113 @Test testStartStop()114 public void testStartStop() { 115 mFragments.dispatchStart(); 116 processAllMessages(); 117 mFragments.dispatchStop(); 118 processAllMessages(); 119 } 120 121 @Test testResumePause()122 public void testResumePause() { 123 mFragments.dispatchResume(); 124 processAllMessages(); 125 mFragments.dispatchPause(); 126 processAllMessages(); 127 } 128 129 @Test testAttachDetach()130 public void testAttachDetach() { 131 WindowManager.LayoutParams lp = new WindowManager.LayoutParams( 132 LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT, 133 LayoutParams.TYPE_SYSTEM_ALERT, 134 0, PixelFormat.TRANSLUCENT); 135 mFragments.dispatchResume(); 136 processAllMessages(); 137 attachFragmentToWindow(); 138 detachFragmentToWindow(); 139 mFragments.dispatchPause(); 140 processAllMessages(); 141 } 142 143 @Test testRecreate()144 public void testRecreate() { 145 mFragments.dispatchResume(); 146 processAllMessages(); 147 recreateFragment(); 148 processAllMessages(); 149 } 150 151 @Test testMultipleResumes()152 public void testMultipleResumes() { 153 mFragments.dispatchResume(); 154 processAllMessages(); 155 mFragments.dispatchStop(); 156 processAllMessages(); 157 mFragments.dispatchResume(); 158 processAllMessages(); 159 } 160 recreateFragment()161 protected void recreateFragment() { 162 mFragments.dispatchPause(); 163 Parcelable p = mFragments.saveAllState(); 164 mFragments.dispatchDestroy(); 165 166 mFragments = FragmentController.createController(new HostCallbacks()); 167 mFragments.attachHost(null); 168 mFragments.restoreAllState(p, (FragmentManagerNonConfig) null); 169 mFragments.dispatchResume(); 170 mFragment = mFragments.getFragmentManager().findFragmentById(VIEW_ID); 171 } 172 attachFragmentToWindow()173 protected void attachFragmentToWindow() { 174 ViewUtils.attachView(mView); 175 TestableLooper.get(this).processAllMessages(); 176 } 177 detachFragmentToWindow()178 protected void detachFragmentToWindow() { 179 ViewUtils.detachView(mView); 180 TestableLooper.get(this).processAllMessages(); 181 } 182 destroyFragments()183 protected void destroyFragments() { 184 mFragments.dispatchDestroy(); 185 processAllMessages(); 186 mFragments = null; 187 } 188 processAllMessages()189 protected void processAllMessages() { 190 TestableLooper.get(this).processAllMessages(); 191 } 192 193 /** 194 * Method available for override to replace fragment instantiation. 195 */ instantiate(Context context, String className, @Nullable Bundle arguments)196 protected Fragment instantiate(Context context, String className, @Nullable Bundle arguments) { 197 return Fragment.instantiate(context, className, arguments); 198 } 199 findViewById(int id)200 private View findViewById(int id) { 201 return mView.findViewById(id); 202 } 203 204 private class HostCallbacks extends FragmentHostCallback<BaseFragmentTest> { HostCallbacks()205 public HostCallbacks() { 206 super(mContext, BaseFragmentTest.this.mHandler, 0); 207 } 208 209 @Override onGetHost()210 public BaseFragmentTest onGetHost() { 211 return BaseFragmentTest.this; 212 } 213 214 @Override onDump(String prefix, FileDescriptor fd, PrintWriter writer, String[] args)215 public void onDump(String prefix, FileDescriptor fd, PrintWriter writer, String[] args) { 216 } 217 218 @Override instantiate(Context context, String className, Bundle arguments)219 public Fragment instantiate(Context context, String className, Bundle arguments) { 220 return BaseFragmentTest.this.instantiate(context, className, arguments); 221 } 222 223 @Override onShouldSaveFragmentState(Fragment fragment)224 public boolean onShouldSaveFragmentState(Fragment fragment) { 225 return true; // True for now. 226 } 227 228 @Override onGetLayoutInflater()229 public LayoutInflater onGetLayoutInflater() { 230 return LayoutInflater.from(mContext); 231 } 232 233 @Override onUseFragmentManagerInflaterFactory()234 public boolean onUseFragmentManagerInflaterFactory() { 235 return true; 236 } 237 238 @Override onHasWindowAnimations()239 public boolean onHasWindowAnimations() { 240 return false; 241 } 242 243 @Override onGetWindowAnimations()244 public int onGetWindowAnimations() { 245 return 0; 246 } 247 248 @Override onAttachFragment(Fragment fragment)249 public void onAttachFragment(Fragment fragment) { 250 } 251 252 @Nullable 253 @Override onFindViewById(int id)254 public View onFindViewById(int id) { 255 return BaseFragmentTest.this.findViewById(id); 256 } 257 258 @Override onHasView()259 public boolean onHasView() { 260 return true; 261 } 262 } 263 } 264