1 /*
2  * Copyright (C) 2016 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 com.android.systemui.fragments;
16 
17 import android.annotation.Nullable;
18 import android.app.Fragment;
19 import android.app.FragmentController;
20 import android.app.FragmentHostCallback;
21 import android.app.FragmentManager;
22 import android.app.FragmentManager.FragmentLifecycleCallbacks;
23 import android.app.FragmentManagerNonConfig;
24 import android.content.Context;
25 import android.content.pm.ActivityInfo;
26 import android.content.res.Configuration;
27 import android.os.Bundle;
28 import android.os.Handler;
29 import android.os.Looper;
30 import android.os.Parcelable;
31 import android.util.ArrayMap;
32 import android.view.LayoutInflater;
33 import android.view.View;
34 
35 import androidx.annotation.NonNull;
36 
37 import com.android.settingslib.applications.InterestingConfigChanges;
38 import com.android.systemui.Dependency;
39 import com.android.systemui.plugins.Plugin;
40 import com.android.systemui.util.leak.LeakDetector;
41 
42 import java.io.FileDescriptor;
43 import java.io.PrintWriter;
44 import java.lang.reflect.InvocationTargetException;
45 import java.util.ArrayList;
46 import java.util.HashMap;
47 
48 public class FragmentHostManager {
49 
50     private final Handler mHandler = new Handler(Looper.getMainLooper());
51     private final Context mContext;
52     private final HashMap<String, ArrayList<FragmentListener>> mListeners = new HashMap<>();
53     private final View mRootView;
54     private final InterestingConfigChanges mConfigChanges = new InterestingConfigChanges(
55             ActivityInfo.CONFIG_FONT_SCALE | ActivityInfo.CONFIG_LOCALE
56                 | ActivityInfo.CONFIG_SCREEN_LAYOUT | ActivityInfo.CONFIG_ASSETS_PATHS);
57     private final FragmentService mManager;
58     private final ExtensionFragmentManager mPlugins = new ExtensionFragmentManager();
59 
60     private FragmentController mFragments;
61     private FragmentLifecycleCallbacks mLifecycleCallbacks;
62 
FragmentHostManager(FragmentService manager, View rootView)63     FragmentHostManager(FragmentService manager, View rootView) {
64         mContext = rootView.getContext();
65         mManager = manager;
66         mRootView = rootView;
67         mConfigChanges.applyNewConfig(mContext.getResources());
68         createFragmentHost(null);
69     }
70 
createFragmentHost(Parcelable savedState)71     private void createFragmentHost(Parcelable savedState) {
72         mFragments = FragmentController.createController(new HostCallbacks());
73         mFragments.attachHost(null);
74         mLifecycleCallbacks = new FragmentLifecycleCallbacks() {
75             @Override
76             public void onFragmentViewCreated(FragmentManager fm, Fragment f, View v,
77                     Bundle savedInstanceState) {
78                 FragmentHostManager.this.onFragmentViewCreated(f);
79             }
80 
81             @Override
82             public void onFragmentViewDestroyed(FragmentManager fm, Fragment f) {
83                 FragmentHostManager.this.onFragmentViewDestroyed(f);
84             }
85 
86             @Override
87             public void onFragmentDestroyed(FragmentManager fm, Fragment f) {
88                 Dependency.get(LeakDetector.class).trackGarbage(f);
89             }
90         };
91         mFragments.getFragmentManager().registerFragmentLifecycleCallbacks(mLifecycleCallbacks,
92                 true);
93         if (savedState != null) {
94             mFragments.restoreAllState(savedState, (FragmentManagerNonConfig) null);
95         }
96         // For now just keep all fragments in the resumed state.
97         mFragments.dispatchCreate();
98         mFragments.dispatchStart();
99         mFragments.dispatchResume();
100     }
101 
destroyFragmentHost()102     private Parcelable destroyFragmentHost() {
103         mFragments.dispatchPause();
104         Parcelable p = mFragments.saveAllState();
105         mFragments.dispatchStop();
106         mFragments.dispatchDestroy();
107         mFragments.getFragmentManager().unregisterFragmentLifecycleCallbacks(mLifecycleCallbacks);
108         return p;
109     }
110 
addTagListener(String tag, FragmentListener listener)111     public FragmentHostManager addTagListener(String tag, FragmentListener listener) {
112         ArrayList<FragmentListener> listeners = mListeners.get(tag);
113         if (listeners == null) {
114             listeners = new ArrayList<>();
115             mListeners.put(tag, listeners);
116         }
117         listeners.add(listener);
118         Fragment current = getFragmentManager().findFragmentByTag(tag);
119         if (current != null && current.getView() != null) {
120             listener.onFragmentViewCreated(tag, current);
121         }
122         return this;
123     }
124 
125     // Shouldn't generally be needed, included for completeness sake.
removeTagListener(String tag, FragmentListener listener)126     public void removeTagListener(String tag, FragmentListener listener) {
127         ArrayList<FragmentListener> listeners = mListeners.get(tag);
128         if (listeners != null && listeners.remove(listener) && listeners.size() == 0) {
129             mListeners.remove(tag);
130         }
131     }
132 
onFragmentViewCreated(Fragment fragment)133     private void onFragmentViewCreated(Fragment fragment) {
134         String tag = fragment.getTag();
135 
136         ArrayList<FragmentListener> listeners = mListeners.get(tag);
137         if (listeners != null) {
138             listeners.forEach((listener) -> listener.onFragmentViewCreated(tag, fragment));
139         }
140     }
141 
onFragmentViewDestroyed(Fragment fragment)142     private void onFragmentViewDestroyed(Fragment fragment) {
143         String tag = fragment.getTag();
144 
145         ArrayList<FragmentListener> listeners = mListeners.get(tag);
146         if (listeners != null) {
147             listeners.forEach((listener) -> listener.onFragmentViewDestroyed(tag, fragment));
148         }
149     }
150 
151     /**
152      * Called when the configuration changed, return true if the fragments
153      * should be recreated.
154      */
onConfigurationChanged(Configuration newConfig)155     protected void onConfigurationChanged(Configuration newConfig) {
156         if (mConfigChanges.applyNewConfig(mContext.getResources())) {
157             reloadFragments();
158         } else {
159             mFragments.dispatchConfigurationChanged(newConfig);
160         }
161     }
162 
dump(String prefix, FileDescriptor fd, PrintWriter writer, String[] args)163     private void dump(String prefix, FileDescriptor fd, PrintWriter writer, String[] args) {
164         // TODO: Do something?
165     }
166 
findViewById(int id)167     private <T extends View> T findViewById(int id) {
168         return mRootView.findViewById(id);
169     }
170 
171     /**
172      * Note: Values from this shouldn't be cached as they can change after config changes.
173      */
getFragmentManager()174     public FragmentManager getFragmentManager() {
175         return mFragments.getFragmentManager();
176     }
177 
getExtensionManager()178     ExtensionFragmentManager getExtensionManager() {
179         return mPlugins;
180     }
181 
destroy()182     void destroy() {
183         mFragments.dispatchDestroy();
184     }
185 
186     /**
187      * Creates a fragment that requires injection.
188      */
create(Class<T> fragmentCls)189     public <T> T create(Class<T> fragmentCls) {
190         return (T) mPlugins.instantiate(mContext, fragmentCls.getName(), null);
191     }
192 
193     public interface FragmentListener {
onFragmentViewCreated(String tag, Fragment fragment)194         void onFragmentViewCreated(String tag, Fragment fragment);
195 
196         // The facts of lifecycle
197         // When a fragment is destroyed, you should not talk to it any longer.
onFragmentViewDestroyed(String tag, Fragment fragment)198         default void onFragmentViewDestroyed(String tag, Fragment fragment) {
199         }
200     }
201 
get(View view)202     public static FragmentHostManager get(View view) {
203         try {
204             return Dependency.get(FragmentService.class).getFragmentHostManager(view);
205         } catch (ClassCastException e) {
206             // TODO: Some auto handling here?
207             throw e;
208         }
209     }
210 
removeAndDestroy(View view)211     public static void removeAndDestroy(View view) {
212         Dependency.get(FragmentService.class).removeAndDestroy(view);
213     }
214 
reloadFragments()215     public void reloadFragments() {
216         // Save the old state.
217         Parcelable p = destroyFragmentHost();
218         // Generate a new fragment host and restore its state.
219         createFragmentHost(p);
220     }
221 
222     class HostCallbacks extends FragmentHostCallback<FragmentHostManager> {
HostCallbacks()223         public HostCallbacks() {
224             super(mContext, FragmentHostManager.this.mHandler, 0);
225         }
226 
227         @Override
onGetHost()228         public FragmentHostManager onGetHost() {
229             return FragmentHostManager.this;
230         }
231 
232         @Override
onDump(String prefix, FileDescriptor fd, PrintWriter writer, String[] args)233         public void onDump(String prefix, FileDescriptor fd, PrintWriter writer, String[] args) {
234             FragmentHostManager.this.dump(prefix, fd, writer, args);
235         }
236 
237         @Override
instantiate(Context context, String className, Bundle arguments)238         public Fragment instantiate(Context context, String className, Bundle arguments) {
239             return mPlugins.instantiate(context, className, arguments);
240         }
241 
242         @Override
onShouldSaveFragmentState(Fragment fragment)243         public boolean onShouldSaveFragmentState(Fragment fragment) {
244             return true; // True for now.
245         }
246 
247         @Override
onGetLayoutInflater()248         public LayoutInflater onGetLayoutInflater() {
249             return LayoutInflater.from(mContext);
250         }
251 
252         @Override
onUseFragmentManagerInflaterFactory()253         public boolean onUseFragmentManagerInflaterFactory() {
254             return true;
255         }
256 
257         @Override
onHasWindowAnimations()258         public boolean onHasWindowAnimations() {
259             return false;
260         }
261 
262         @Override
onGetWindowAnimations()263         public int onGetWindowAnimations() {
264             return 0;
265         }
266 
267         @Override
onAttachFragment(Fragment fragment)268         public void onAttachFragment(Fragment fragment) {
269         }
270 
271         @Override
272         @Nullable
onFindViewById(int id)273         public <T extends View> T onFindViewById(int id) {
274             return FragmentHostManager.this.findViewById(id);
275         }
276 
277         @Override
onHasView()278         public boolean onHasView() {
279             return true;
280         }
281     }
282 
283     class ExtensionFragmentManager {
284         private final ArrayMap<String, Context> mExtensionLookup = new ArrayMap<>();
285 
setCurrentExtension(int id, @NonNull String tag, @Nullable String oldClass, @NonNull String currentClass, @Nullable Context context)286         public void setCurrentExtension(int id, @NonNull  String tag, @Nullable String oldClass,
287                 @NonNull String currentClass, @Nullable Context context) {
288             if (oldClass != null) {
289                 mExtensionLookup.remove(oldClass);
290             }
291             mExtensionLookup.put(currentClass, context);
292             getFragmentManager().beginTransaction()
293                     .replace(id, instantiate(context, currentClass, null), tag)
294                     .commit();
295             reloadFragments();
296         }
297 
instantiate(Context context, String className, Bundle arguments)298         Fragment instantiate(Context context, String className, Bundle arguments) {
299             Context extensionContext = mExtensionLookup.get(className);
300             if (extensionContext != null) {
301                 Fragment f = instantiateWithInjections(extensionContext, className, arguments);
302                 if (f instanceof Plugin) {
303                     ((Plugin) f).onCreate(mContext, extensionContext);
304                 }
305                 return f;
306             }
307             return instantiateWithInjections(context, className, arguments);
308         }
309 
instantiateWithInjections( Context context, String className, Bundle args)310         private Fragment instantiateWithInjections(
311                 Context context, String className, Bundle args) {
312             FragmentService.FragmentInstantiationInfo fragmentInstantiationInfo =
313                     mManager.getInjectionMap().get(className);
314             if (fragmentInstantiationInfo != null) {
315                 try {
316                     Fragment f = (Fragment) fragmentInstantiationInfo
317                             .mMethod
318                             .invoke(fragmentInstantiationInfo.mDaggerComponent);
319                     // Setup the args, taken from Fragment#instantiate.
320                     if (args != null) {
321                         args.setClassLoader(f.getClass().getClassLoader());
322                         f.setArguments(args);
323                     }
324                     return f;
325                 } catch (IllegalAccessException | InvocationTargetException e) {
326                     throw new Fragment.InstantiationException("Unable to instantiate " + className,
327                             e);
328                 }
329             }
330             return Fragment.instantiate(context, className, args);
331         }
332     }
333 
334     private static class PluginState {
335         Context mContext;
336         String mCls;
337 
PluginState(String cls, Context context)338         private PluginState(String cls, Context context) {
339             mCls = cls;
340             mContext = context;
341         }
342     }
343 }
344