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