• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 android.content.pm.PackageManager.MATCH_DISABLED_COMPONENTS;
19 
20 import static com.android.launcher3.uioverrides.plugins.PluginManagerWrapper.PLUGIN_CHANGED;
21 import static com.android.launcher3.uioverrides.plugins.PluginManagerWrapper.pluginEnabledKey;
22 
23 import android.annotation.TargetApi;
24 import android.content.BroadcastReceiver;
25 import android.content.ComponentName;
26 import android.content.Context;
27 import android.content.Intent;
28 import android.content.IntentFilter;
29 import android.content.pm.ApplicationInfo;
30 import android.content.pm.PackageManager;
31 import android.content.pm.ResolveInfo;
32 import android.content.pm.ServiceInfo;
33 import android.net.Uri;
34 import android.os.Build;
35 import android.os.Bundle;
36 import android.provider.Settings;
37 import android.util.ArrayMap;
38 import android.util.Pair;
39 import android.view.Menu;
40 import android.view.MenuInflater;
41 import android.view.MenuItem;
42 import android.view.View;
43 
44 import androidx.preference.Preference;
45 import androidx.preference.PreferenceCategory;
46 import androidx.preference.PreferenceDataStore;
47 import androidx.preference.PreferenceFragmentCompat;
48 import androidx.preference.PreferenceScreen;
49 import androidx.preference.PreferenceViewHolder;
50 import androidx.preference.SwitchPreference;
51 
52 import com.android.launcher3.R;
53 import com.android.launcher3.config.FeatureFlags;
54 import com.android.launcher3.config.FlagTogglerPrefUi;
55 import com.android.launcher3.uioverrides.plugins.PluginManagerWrapper;
56 
57 import java.util.ArrayList;
58 import java.util.List;
59 import java.util.Set;
60 import java.util.stream.Collectors;
61 
62 /**
63  * Dev-build only UI allowing developers to toggle flag settings and plugins.
64  * See {@link FeatureFlags}.
65  */
66 @TargetApi(Build.VERSION_CODES.O)
67 public class DeveloperOptionsFragment extends PreferenceFragmentCompat {
68 
69     private static final String ACTION_PLUGIN_SETTINGS = "com.android.systemui.action.PLUGIN_SETTINGS";
70     private static final String PLUGIN_PERMISSION = "com.android.systemui.permission.PLUGIN";
71 
72     private final BroadcastReceiver mPluginReceiver = new BroadcastReceiver() {
73         @Override
74         public void onReceive(Context context, Intent intent) {
75             loadPluginPrefs();
76         }
77     };
78 
79     private PreferenceScreen mPreferenceScreen;
80 
81     private PreferenceCategory mPluginsCategory;
82     private FlagTogglerPrefUi mFlagTogglerPrefUi;
83 
84     @Override
onCreatePreferences(Bundle savedInstanceState, String rootKey)85     public void onCreatePreferences(Bundle savedInstanceState, String rootKey) {
86         IntentFilter filter = new IntentFilter(Intent.ACTION_PACKAGE_ADDED);
87         filter.addAction(Intent.ACTION_PACKAGE_CHANGED);
88         filter.addAction(Intent.ACTION_PACKAGE_REMOVED);
89         filter.addDataScheme("package");
90         getContext().registerReceiver(mPluginReceiver, filter);
91         getContext().registerReceiver(mPluginReceiver,
92                 new IntentFilter(Intent.ACTION_USER_UNLOCKED));
93 
94         mPreferenceScreen = getPreferenceManager().createPreferenceScreen(getContext());
95         setPreferenceScreen(mPreferenceScreen);
96 
97         initFlags();
98         loadPluginPrefs();
99         maybeAddSandboxCategory();
100     }
101 
102     @Override
onDestroy()103     public void onDestroy() {
104         super.onDestroy();
105         getContext().unregisterReceiver(mPluginReceiver);
106     }
107 
newCategory(String title)108     private PreferenceCategory newCategory(String title) {
109         PreferenceCategory category = new PreferenceCategory(getContext());
110         category.setOrder(Preference.DEFAULT_ORDER);
111         category.setTitle(title);
112         mPreferenceScreen.addPreference(category);
113         return category;
114     }
115 
initFlags()116     private void initFlags() {
117         if (!FeatureFlags.showFlagTogglerUi(getContext())) {
118             return;
119         }
120 
121         mFlagTogglerPrefUi = new FlagTogglerPrefUi(this);
122         mFlagTogglerPrefUi.applyTo(newCategory("Feature flags"));
123     }
124 
125     @Override
onCreateOptionsMenu(Menu menu, MenuInflater inflater)126     public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
127         if (mFlagTogglerPrefUi != null) {
128             mFlagTogglerPrefUi.onCreateOptionsMenu(menu);
129         }
130     }
131 
132     @Override
onOptionsItemSelected(MenuItem item)133     public boolean onOptionsItemSelected(MenuItem item) {
134         if (mFlagTogglerPrefUi != null) {
135             mFlagTogglerPrefUi.onOptionsItemSelected(item);
136         }
137         return super.onOptionsItemSelected(item);
138     }
139 
140     @Override
onStop()141     public void onStop() {
142         if (mFlagTogglerPrefUi != null) {
143             mFlagTogglerPrefUi.onStop();
144         }
145         super.onStop();
146     }
147 
loadPluginPrefs()148     private void loadPluginPrefs() {
149         if (mPluginsCategory != null) {
150             mPreferenceScreen.removePreference(mPluginsCategory);
151         }
152         if (!PluginManagerWrapper.hasPlugins(getActivity())) {
153             mPluginsCategory = null;
154             return;
155         }
156         mPluginsCategory = newCategory("Plugins");
157 
158         PluginManagerWrapper manager = PluginManagerWrapper.INSTANCE.get(getContext());
159         Context prefContext = getContext();
160         PackageManager pm = getContext().getPackageManager();
161 
162         Set<String> pluginActions = manager.getPluginActions();
163 
164         ArrayMap<Pair<String, String>, ArrayList<Pair<String, ServiceInfo>>> plugins =
165                 new ArrayMap<>();
166 
167         Set<String> pluginPermissionApps = pm.getPackagesHoldingPermissions(
168                 new String[]{PLUGIN_PERMISSION}, MATCH_DISABLED_COMPONENTS)
169                 .stream()
170                 .map(pi -> pi.packageName)
171                 .collect(Collectors.toSet());
172 
173         for (String action : pluginActions) {
174             String name = toName(action);
175             List<ResolveInfo> result = pm.queryIntentServices(
176                     new Intent(action), MATCH_DISABLED_COMPONENTS);
177             for (ResolveInfo info : result) {
178                 String packageName = info.serviceInfo.packageName;
179                 if (!pluginPermissionApps.contains(packageName)) {
180                     continue;
181                 }
182 
183                 Pair<String, String> key = Pair.create(packageName, info.serviceInfo.processName);
184                 if (!plugins.containsKey(key)) {
185                     plugins.put(key, new ArrayList<>());
186                 }
187                 plugins.get(key).add(Pair.create(name, info.serviceInfo));
188             }
189         }
190 
191         PreferenceDataStore enabler = manager.getPluginEnabler();
192         plugins.forEach((key, si) -> {
193             String packageName = key.first;
194             List<ComponentName> componentNames = si.stream()
195                     .map(p -> new ComponentName(packageName, p.second.name))
196                     .collect(Collectors.toList());
197             if (!componentNames.isEmpty()) {
198                 SwitchPreference pref = new PluginPreference(
199                         prefContext, si.get(0).second.applicationInfo, enabler, componentNames);
200                 pref.setSummary("Plugins: "
201                         + si.stream().map(p -> p.first).collect(Collectors.joining(", ")));
202                 mPluginsCategory.addPreference(pref);
203             }
204         });
205     }
206 
maybeAddSandboxCategory()207     private void maybeAddSandboxCategory() {
208         Context context = getContext();
209         if (context == null) {
210             return;
211         }
212         Intent launchSandboxIntent =
213                 new Intent("com.android.quickstep.action.GESTURE_SANDBOX")
214                         .setPackage(context.getPackageName())
215                         .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
216         if (launchSandboxIntent.resolveActivity(context.getPackageManager()) == null) {
217             return;
218         }
219         PreferenceCategory sandboxCategory = newCategory("Gesture Navigation Sandbox");
220         sandboxCategory.setSummary("Learn and practice navigation gestures");
221         Preference launchBackTutorialPreference = new Preference(context);
222         launchBackTutorialPreference.setKey("launchBackTutorial");
223         launchBackTutorialPreference.setTitle("Launch Back Tutorial");
224         launchBackTutorialPreference.setSummary("Learn how to use the Back gesture");
225         launchBackTutorialPreference.setOnPreferenceClickListener(preference -> {
226             startActivity(launchSandboxIntent.putExtra(
227                     "tutorial_type", "RIGHT_EDGE_BACK_NAVIGATION"));
228             return true;
229         });
230         sandboxCategory.addPreference(launchBackTutorialPreference);
231         Preference launchHomeTutorialPreference = new Preference(context);
232         launchHomeTutorialPreference.setKey("launchHomeTutorial");
233         launchHomeTutorialPreference.setTitle("Launch Home Tutorial");
234         launchHomeTutorialPreference.setSummary("Learn how to use the Home gesture");
235         launchHomeTutorialPreference.setOnPreferenceClickListener(preference -> {
236             startActivity(launchSandboxIntent.putExtra("tutorial_type", "HOME_NAVIGATION"));
237             return true;
238         });
239         sandboxCategory.addPreference(launchHomeTutorialPreference);
240         Preference launchOverviewTutorialPreference = new Preference(context);
241         launchOverviewTutorialPreference.setKey("launchOverviewTutorial");
242         launchOverviewTutorialPreference.setTitle("Launch Overview Tutorial");
243         launchOverviewTutorialPreference.setSummary("Learn how to use the Overview gesture");
244         launchOverviewTutorialPreference.setOnPreferenceClickListener(preference -> {
245             startActivity(launchSandboxIntent.putExtra("tutorial_type", "OVERVIEW_NAVIGATION"));
246             return true;
247         });
248         sandboxCategory.addPreference(launchOverviewTutorialPreference);
249         Preference launchAssistantTutorialPreference = new Preference(context);
250         launchAssistantTutorialPreference.setKey("launchAssistantTutorial");
251         launchAssistantTutorialPreference.setTitle("Launch Assistant Tutorial");
252         launchAssistantTutorialPreference.setSummary("Learn how to use the Assistant gesture");
253         launchAssistantTutorialPreference.setOnPreferenceClickListener(preference -> {
254             startActivity(launchSandboxIntent.putExtra("tutorial_type", "ASSISTANT"));
255             return true;
256         });
257         sandboxCategory.addPreference(launchAssistantTutorialPreference);
258     }
259 
toName(String action)260     private String toName(String action) {
261         String str = action.replace("com.android.systemui.action.PLUGIN_", "")
262                 .replace("com.android.launcher3.action.PLUGIN_", "");
263         StringBuilder b = new StringBuilder();
264         for (String s : str.split("_")) {
265             if (b.length() != 0) {
266                 b.append(' ');
267             }
268             b.append(s.substring(0, 1));
269             b.append(s.substring(1).toLowerCase());
270         }
271         return b.toString();
272     }
273 
274     private static class PluginPreference extends SwitchPreference {
275         private final boolean mHasSettings;
276         private final PreferenceDataStore mPluginEnabler;
277         private final String mPackageName;
278         private final List<ComponentName> mComponentNames;
279 
PluginPreference(Context prefContext, ApplicationInfo info, PreferenceDataStore pluginEnabler, List<ComponentName> componentNames)280         PluginPreference(Context prefContext, ApplicationInfo info,
281                 PreferenceDataStore pluginEnabler, List<ComponentName> componentNames) {
282             super(prefContext);
283             PackageManager pm = prefContext.getPackageManager();
284             mHasSettings = pm.resolveActivity(new Intent(ACTION_PLUGIN_SETTINGS)
285                     .setPackage(info.packageName), 0) != null;
286             mPackageName = info.packageName;
287             mComponentNames = componentNames;
288             mPluginEnabler = pluginEnabler;
289             setTitle(info.loadLabel(pm));
290             setChecked(isPluginEnabled());
291             setWidgetLayoutResource(R.layout.switch_preference_with_settings);
292         }
293 
isEnabled(ComponentName cn)294         private boolean isEnabled(ComponentName cn) {
295             return mPluginEnabler.getBoolean(pluginEnabledKey(cn), true);
296 
297         }
298 
isPluginEnabled()299         private boolean isPluginEnabled() {
300             for (ComponentName componentName : mComponentNames) {
301                 if (!isEnabled(componentName)) {
302                     return false;
303                 }
304             }
305             return true;
306         }
307 
308         @Override
persistBoolean(boolean isEnabled)309         protected boolean persistBoolean(boolean isEnabled) {
310             boolean shouldSendBroadcast = false;
311             for (ComponentName componentName : mComponentNames) {
312                 if (isEnabled(componentName) != isEnabled) {
313                     mPluginEnabler.putBoolean(pluginEnabledKey(componentName), isEnabled);
314                     shouldSendBroadcast = true;
315                 }
316             }
317             if (shouldSendBroadcast) {
318                 final String pkg = mPackageName;
319                 final Intent intent = new Intent(PLUGIN_CHANGED,
320                         pkg != null ? Uri.fromParts("package", pkg, null) : null);
321                 getContext().sendBroadcast(intent);
322             }
323             setChecked(isEnabled);
324             return true;
325         }
326 
327         @Override
onBindViewHolder(PreferenceViewHolder holder)328         public void onBindViewHolder(PreferenceViewHolder holder) {
329             super.onBindViewHolder(holder);
330             holder.findViewById(R.id.settings).setVisibility(mHasSettings ? View.VISIBLE
331                     : View.GONE);
332             holder.findViewById(R.id.divider).setVisibility(mHasSettings ? View.VISIBLE
333                     : View.GONE);
334             holder.findViewById(R.id.settings).setOnClickListener(v -> {
335                 ResolveInfo result = v.getContext().getPackageManager().resolveActivity(
336                         new Intent(ACTION_PLUGIN_SETTINGS).setPackage(mPackageName), 0);
337                 if (result != null) {
338                     v.getContext().startActivity(new Intent().setComponent(
339                             new ComponentName(result.activityInfo.packageName,
340                                     result.activityInfo.name)));
341                 }
342             });
343             holder.itemView.setOnLongClickListener(v -> {
344                 Intent intent = new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS);
345                 intent.setData(Uri.fromParts("package", mPackageName, null));
346                 getContext().startActivity(intent);
347                 return true;
348             });
349         }
350     }
351 }
352