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