1 /*
2  * Copyright (C) 2018 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 package com.android.launcher3.settings;
17 
18 import static com.android.launcher3.uioverrides.plugins.PluginManagerWrapper.PLUGIN_CHANGED;
19 import static com.android.launcher3.uioverrides.plugins.PluginManagerWrapper.pluginEnabledKey;
20 
21 import android.annotation.TargetApi;
22 import android.content.BroadcastReceiver;
23 import android.content.ComponentName;
24 import android.content.Context;
25 import android.content.Intent;
26 import android.content.IntentFilter;
27 import android.content.pm.PackageInfo;
28 import android.content.pm.PackageManager;
29 import android.content.pm.ResolveInfo;
30 import android.net.Uri;
31 import android.os.Build;
32 import android.os.Bundle;
33 import android.provider.Settings;
34 import android.util.ArrayMap;
35 import android.util.ArraySet;
36 import android.view.Menu;
37 import android.view.MenuInflater;
38 import android.view.MenuItem;
39 import android.view.View;
40 
41 import com.android.launcher3.R;
42 import com.android.launcher3.config.FeatureFlags;
43 import com.android.launcher3.config.FlagTogglerPrefUi;
44 import com.android.launcher3.uioverrides.plugins.PluginManagerWrapper;
45 
46 import java.util.List;
47 import java.util.Set;
48 
49 import androidx.preference.Preference;
50 import androidx.preference.PreferenceCategory;
51 import androidx.preference.PreferenceDataStore;
52 import androidx.preference.PreferenceFragment;
53 import androidx.preference.PreferenceScreen;
54 import androidx.preference.PreferenceViewHolder;
55 import androidx.preference.SwitchPreference;
56 
57 /**
58  * Dev-build only UI allowing developers to toggle flag settings and plugins.
59  * See {@link FeatureFlags}.
60  */
61 @TargetApi(Build.VERSION_CODES.O)
62 public class DeveloperOptionsFragment extends PreferenceFragment {
63 
64     private static final String ACTION_PLUGIN_SETTINGS = "com.android.systemui.action.PLUGIN_SETTINGS";
65     private static final String PLUGIN_PERMISSION = "com.android.systemui.permission.PLUGIN";
66 
67     private final BroadcastReceiver mPluginReceiver = new BroadcastReceiver() {
68         @Override
69         public void onReceive(Context context, Intent intent) {
70             loadPluginPrefs();
71         }
72     };
73 
74     private PreferenceScreen mPreferenceScreen;
75 
76     private PreferenceCategory mPluginsCategory;
77     private FlagTogglerPrefUi mFlagTogglerPrefUi;
78 
79     @Override
onCreatePreferences(Bundle savedInstanceState, String rootKey)80     public void onCreatePreferences(Bundle savedInstanceState, String rootKey) {
81         IntentFilter filter = new IntentFilter(Intent.ACTION_PACKAGE_ADDED);
82         filter.addAction(Intent.ACTION_PACKAGE_CHANGED);
83         filter.addAction(Intent.ACTION_PACKAGE_REMOVED);
84         filter.addDataScheme("package");
85         getContext().registerReceiver(mPluginReceiver, filter);
86         getContext().registerReceiver(mPluginReceiver,
87                 new IntentFilter(Intent.ACTION_USER_UNLOCKED));
88 
89         mPreferenceScreen = getPreferenceManager().createPreferenceScreen(getContext());
90         setPreferenceScreen(mPreferenceScreen);
91 
92         initFlags();
93         loadPluginPrefs();
94     }
95 
96     @Override
onDestroy()97     public void onDestroy() {
98         super.onDestroy();
99         getContext().unregisterReceiver(mPluginReceiver);
100     }
101 
newCategory(String title)102     private PreferenceCategory newCategory(String title) {
103         PreferenceCategory category = new PreferenceCategory(getContext());
104         category.setOrder(Preference.DEFAULT_ORDER);
105         category.setTitle(title);
106         mPreferenceScreen.addPreference(category);
107         return category;
108     }
109 
initFlags()110     private void initFlags() {
111         if (!FeatureFlags.showFlagTogglerUi(getContext())) {
112             return;
113         }
114 
115         mFlagTogglerPrefUi = new FlagTogglerPrefUi(this);
116         mFlagTogglerPrefUi.applyTo(newCategory("Feature flags"));
117     }
118 
119     @Override
onCreateOptionsMenu(Menu menu, MenuInflater inflater)120     public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
121         if (mFlagTogglerPrefUi != null) {
122             mFlagTogglerPrefUi.onCreateOptionsMenu(menu);
123         }
124     }
125 
126     @Override
onOptionsItemSelected(MenuItem item)127     public boolean onOptionsItemSelected(MenuItem item) {
128         if (mFlagTogglerPrefUi != null) {
129             mFlagTogglerPrefUi.onOptionsItemSelected(item);
130         }
131         return super.onOptionsItemSelected(item);
132     }
133 
134     @Override
onStop()135     public void onStop() {
136         if (mFlagTogglerPrefUi != null) {
137             mFlagTogglerPrefUi.onStop();
138         }
139         super.onStop();
140     }
141 
loadPluginPrefs()142     private void loadPluginPrefs() {
143         if (mPluginsCategory != null) {
144             mPreferenceScreen.removePreference(mPluginsCategory);
145         }
146         if (!PluginManagerWrapper.hasPlugins(getActivity())) {
147             mPluginsCategory = null;
148             return;
149         }
150         mPluginsCategory = newCategory("Plugins");
151 
152         PluginManagerWrapper manager = PluginManagerWrapper.INSTANCE.get(getContext());
153         Context prefContext = getContext();
154         PackageManager pm = getContext().getPackageManager();
155 
156         Set<String> pluginActions = manager.getPluginActions();
157         ArrayMap<String, ArraySet<String>> plugins = new ArrayMap<>();
158         for (String action : pluginActions) {
159             String name = toName(action);
160             List<ResolveInfo> result = pm.queryIntentServices(
161                     new Intent(action), PackageManager.MATCH_DISABLED_COMPONENTS);
162             for (ResolveInfo info : result) {
163                 String packageName = info.serviceInfo.packageName;
164                 if (!plugins.containsKey(packageName)) {
165                     plugins.put(packageName, new ArraySet<>());
166                 }
167                 plugins.get(packageName).add(name);
168             }
169         }
170 
171         List<PackageInfo> apps = pm.getPackagesHoldingPermissions(new String[]{PLUGIN_PERMISSION},
172                 PackageManager.MATCH_DISABLED_COMPONENTS | PackageManager.GET_SERVICES);
173         PreferenceDataStore enabled = manager.getPluginEnabler();
174         apps.forEach(app -> {
175             if (!plugins.containsKey(app.packageName)) return;
176             SwitchPreference pref = new PluginPreference(prefContext, app, enabled);
177             pref.setSummary("Plugins: " + toString(plugins.get(app.packageName)));
178             mPluginsCategory.addPreference(pref);
179         });
180     }
181 
toString(ArraySet<String> plugins)182     private String toString(ArraySet<String> plugins) {
183         StringBuilder b = new StringBuilder();
184         for (String string : plugins) {
185             if (b.length() != 0) {
186                 b.append(", ");
187             }
188             b.append(string);
189         }
190         return b.toString();
191     }
192 
toName(String action)193     private String toName(String action) {
194         String str = action.replace("com.android.systemui.action.PLUGIN_", "");
195         StringBuilder b = new StringBuilder();
196         for (String s : str.split("_")) {
197             if (b.length() != 0) {
198                 b.append(' ');
199             }
200             b.append(s.substring(0, 1));
201             b.append(s.substring(1).toLowerCase());
202         }
203         return b.toString();
204     }
205 
206     private static class PluginPreference extends SwitchPreference {
207         private final boolean mHasSettings;
208         private final PackageInfo mInfo;
209         private final PreferenceDataStore mPluginEnabler;
210 
PluginPreference(Context prefContext, PackageInfo info, PreferenceDataStore pluginEnabler)211         public PluginPreference(Context prefContext, PackageInfo info,
212                 PreferenceDataStore pluginEnabler) {
213             super(prefContext);
214             PackageManager pm = prefContext.getPackageManager();
215             mHasSettings = pm.resolveActivity(new Intent(ACTION_PLUGIN_SETTINGS)
216                     .setPackage(info.packageName), 0) != null;
217             mInfo = info;
218             mPluginEnabler = pluginEnabler;
219             setTitle(info.applicationInfo.loadLabel(pm));
220             setChecked(isPluginEnabled());
221             setWidgetLayoutResource(R.layout.switch_preference_with_settings);
222         }
223 
isEnabled(ComponentName cn)224         private boolean isEnabled(ComponentName cn) {
225             return mPluginEnabler.getBoolean(pluginEnabledKey(cn), true);
226 
227         }
228 
isPluginEnabled()229         private boolean isPluginEnabled() {
230             for (int i = 0; i < mInfo.services.length; i++) {
231                 ComponentName componentName = new ComponentName(mInfo.packageName,
232                         mInfo.services[i].name);
233                 if (!isEnabled(componentName)) {
234                     return false;
235                 }
236             }
237             return true;
238         }
239 
240         @Override
persistBoolean(boolean isEnabled)241         protected boolean persistBoolean(boolean isEnabled) {
242             boolean shouldSendBroadcast = false;
243             for (int i = 0; i < mInfo.services.length; i++) {
244                 ComponentName componentName = new ComponentName(mInfo.packageName,
245                         mInfo.services[i].name);
246 
247                 if (isEnabled(componentName) != isEnabled) {
248                     mPluginEnabler.putBoolean(pluginEnabledKey(componentName), isEnabled);
249                     shouldSendBroadcast = true;
250                 }
251             }
252             if (shouldSendBroadcast) {
253                 final String pkg = mInfo.packageName;
254                 final Intent intent = new Intent(PLUGIN_CHANGED,
255                         pkg != null ? Uri.fromParts("package", pkg, null) : null);
256                 getContext().sendBroadcast(intent);
257             }
258             setChecked(isEnabled);
259             return true;
260         }
261 
262         @Override
onBindViewHolder(PreferenceViewHolder holder)263         public void onBindViewHolder(PreferenceViewHolder holder) {
264             super.onBindViewHolder(holder);
265             holder.findViewById(R.id.settings).setVisibility(mHasSettings ? View.VISIBLE
266                     : View.GONE);
267             holder.findViewById(R.id.divider).setVisibility(mHasSettings ? View.VISIBLE
268                     : View.GONE);
269             holder.findViewById(R.id.settings).setOnClickListener(v -> {
270                 ResolveInfo result = v.getContext().getPackageManager().resolveActivity(
271                         new Intent(ACTION_PLUGIN_SETTINGS).setPackage(
272                                 mInfo.packageName), 0);
273                 if (result != null) {
274                     v.getContext().startActivity(new Intent().setComponent(
275                             new ComponentName(result.activityInfo.packageName,
276                                     result.activityInfo.name)));
277                 }
278             });
279             holder.itemView.setOnLongClickListener(v -> {
280                 Intent intent = new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS);
281                 intent.setData(Uri.fromParts("package", mInfo.packageName, null));
282                 getContext().startActivity(intent);
283                 return true;
284             });
285         }
286     }
287 }
288