• 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.uioverrides.flags;
17 
18 import static android.content.Intent.ACTION_PACKAGE_ADDED;
19 import static android.content.Intent.ACTION_PACKAGE_CHANGED;
20 import static android.content.Intent.ACTION_PACKAGE_REMOVED;
21 import static android.content.pm.PackageManager.GET_RESOLVED_FILTER;
22 import static android.content.pm.PackageManager.MATCH_DISABLED_COMPONENTS;
23 import static android.view.View.GONE;
24 import static android.view.View.VISIBLE;
25 
26 import static com.android.launcher3.LauncherPrefs.ALL_APPS_OVERVIEW_THRESHOLD;
27 import static com.android.launcher3.settings.SettingsActivity.EXTRA_FRAGMENT_ARG_KEY;
28 import static com.android.launcher3.uioverrides.plugins.PluginManagerWrapper.PLUGIN_CHANGED;
29 import static com.android.launcher3.uioverrides.plugins.PluginManagerWrapper.pluginEnabledKey;
30 
31 import android.annotation.TargetApi;
32 import android.content.ComponentName;
33 import android.content.Context;
34 import android.content.Intent;
35 import android.content.SharedPreferences;
36 import android.content.pm.PackageManager;
37 import android.content.pm.ResolveInfo;
38 import android.net.Uri;
39 import android.os.Build;
40 import android.os.Bundle;
41 import android.provider.Settings;
42 import android.text.Editable;
43 import android.text.TextUtils;
44 import android.text.TextWatcher;
45 import android.util.ArrayMap;
46 import android.util.Pair;
47 import android.view.Menu;
48 import android.view.MenuInflater;
49 import android.view.MenuItem;
50 import android.view.View;
51 import android.widget.EditText;
52 import android.widget.Toast;
53 
54 import androidx.annotation.NonNull;
55 import androidx.annotation.Nullable;
56 import androidx.preference.Preference;
57 import androidx.preference.PreferenceCategory;
58 import androidx.preference.PreferenceDataStore;
59 import androidx.preference.PreferenceFragmentCompat;
60 import androidx.preference.PreferenceGroup;
61 import androidx.preference.PreferenceScreen;
62 import androidx.preference.PreferenceViewHolder;
63 import androidx.preference.SeekBarPreference;
64 import androidx.preference.SwitchPreference;
65 
66 import com.android.launcher3.LauncherPrefs;
67 import com.android.launcher3.R;
68 import com.android.launcher3.config.FeatureFlags;
69 import com.android.launcher3.secondarydisplay.SecondaryDisplayLauncher;
70 import com.android.launcher3.uioverrides.plugins.PluginManagerWrapper;
71 import com.android.launcher3.util.OnboardingPrefs;
72 import com.android.launcher3.util.SimpleBroadcastReceiver;
73 
74 import java.util.ArrayList;
75 import java.util.List;
76 import java.util.Map;
77 import java.util.Set;
78 import java.util.stream.Collectors;
79 
80 /**
81  * Dev-build only UI allowing developers to toggle flag settings and plugins.
82  * See {@link FeatureFlags}.
83  */
84 @TargetApi(Build.VERSION_CODES.O)
85 public class DeveloperOptionsFragment extends PreferenceFragmentCompat {
86 
87     private static final String ACTION_PLUGIN_SETTINGS = "com.android.systemui.action.PLUGIN_SETTINGS";
88     private static final String PLUGIN_PERMISSION = "com.android.systemui.permission.PLUGIN";
89 
90     private final SimpleBroadcastReceiver mPluginReceiver =
91             new SimpleBroadcastReceiver(i -> loadPluginPrefs());
92 
93     private PreferenceScreen mPreferenceScreen;
94 
95     private PreferenceCategory mPluginsCategory;
96     private FlagTogglerPrefUi mFlagTogglerPrefUi;
97 
98     @Override
onCreatePreferences(Bundle savedInstanceState, String rootKey)99     public void onCreatePreferences(Bundle savedInstanceState, String rootKey) {
100         mPluginReceiver.registerPkgActions(getContext(), null,
101                 ACTION_PACKAGE_ADDED, ACTION_PACKAGE_CHANGED, ACTION_PACKAGE_REMOVED);
102         mPluginReceiver.register(getContext(), Intent.ACTION_USER_UNLOCKED);
103 
104         mPreferenceScreen = getPreferenceManager().createPreferenceScreen(getContext());
105         setPreferenceScreen(mPreferenceScreen);
106 
107         initFlags();
108         loadPluginPrefs();
109         maybeAddSandboxCategory();
110         addOnboardingPrefsCatergory();
111         if (FeatureFlags.ENABLE_ALL_APPS_FROM_OVERVIEW.get()) {
112             addAllAppsFromOverviewCatergory();
113         }
114 
115         if (getActivity() != null) {
116             getActivity().setTitle("Developer Options");
117         }
118     }
119 
filterPreferences(String query, PreferenceGroup pg)120     private void filterPreferences(String query, PreferenceGroup pg) {
121         int count = pg.getPreferenceCount();
122         int hidden = 0;
123         for (int i = 0; i < count; i++) {
124             Preference preference = pg.getPreference(i);
125             if (preference instanceof PreferenceGroup) {
126                 filterPreferences(query, (PreferenceGroup) preference);
127             } else {
128                 String title = preference.getTitle().toString().toLowerCase().replace("_", " ");
129                 if (query.isEmpty() || title.contains(query)) {
130                     preference.setVisible(true);
131                 } else {
132                     preference.setVisible(false);
133                     hidden++;
134                 }
135             }
136         }
137         pg.setVisible(hidden != count);
138     }
139 
140     @Override
onViewCreated(@onNull View view, @Nullable Bundle savedInstanceState)141     public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
142         super.onViewCreated(view, savedInstanceState);
143         EditText filterBox = view.findViewById(R.id.filter_box);
144         filterBox.setVisibility(VISIBLE);
145         filterBox.addTextChangedListener(new TextWatcher() {
146             @Override
147             public void beforeTextChanged(CharSequence charSequence, int i, int i1, int i2) {
148 
149             }
150 
151             @Override
152             public void onTextChanged(CharSequence charSequence, int i, int i1, int i2) {
153 
154             }
155 
156             @Override
157             public void afterTextChanged(Editable editable) {
158                 String query = editable.toString().toLowerCase().replace("_", " ");
159                 filterPreferences(query, mPreferenceScreen);
160             }
161         });
162 
163         if (getArguments() != null) {
164             String filter = getArguments().getString(EXTRA_FRAGMENT_ARG_KEY);
165             // Normally EXTRA_FRAGMENT_ARG_KEY is used to highlight the preference with the given
166             // key. This is a slight variation where we instead filter by the human-readable titles.
167             if (filter != null) {
168                 filterBox.setText(filter);
169             }
170         }
171 
172         View listView = getListView();
173         final int bottomPadding = listView.getPaddingBottom();
174         listView.setOnApplyWindowInsetsListener((v, insets) -> {
175             v.setPadding(
176                     v.getPaddingLeft(),
177                     v.getPaddingTop(),
178                     v.getPaddingRight(),
179                     bottomPadding + insets.getSystemWindowInsetBottom());
180             return insets.consumeSystemWindowInsets();
181         });
182     }
183 
184     @Override
onDestroy()185     public void onDestroy() {
186         super.onDestroy();
187         mPluginReceiver.unregisterReceiverSafely(getContext());
188     }
189 
newCategory(String title)190     private PreferenceCategory newCategory(String title) {
191         return newCategory(title, null);
192     }
193 
newCategory(String title, @Nullable String summary)194     private PreferenceCategory newCategory(String title, @Nullable String summary) {
195         PreferenceCategory category = new PreferenceCategory(getContext());
196         category.setOrder(Preference.DEFAULT_ORDER);
197         category.setTitle(title);
198         if (!TextUtils.isEmpty(summary)) {
199             category.setSummary(summary);
200         }
201         mPreferenceScreen.addPreference(category);
202         return category;
203     }
204 
initFlags()205     private void initFlags() {
206         if (!FeatureFlags.showFlagTogglerUi(getContext())) {
207             return;
208         }
209 
210         mFlagTogglerPrefUi = new FlagTogglerPrefUi(this);
211         mFlagTogglerPrefUi.applyTo(newCategory("Feature flags", "Long press to reset"));
212     }
213 
214     @Override
onCreateOptionsMenu(Menu menu, MenuInflater inflater)215     public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
216         if (mFlagTogglerPrefUi != null) {
217             mFlagTogglerPrefUi.onCreateOptionsMenu(menu);
218         }
219     }
220 
221     @Override
onOptionsItemSelected(MenuItem item)222     public boolean onOptionsItemSelected(MenuItem item) {
223         if (mFlagTogglerPrefUi != null) {
224             mFlagTogglerPrefUi.onOptionsItemSelected(item);
225         }
226         return super.onOptionsItemSelected(item);
227     }
228 
229     @Override
onStop()230     public void onStop() {
231         if (mFlagTogglerPrefUi != null) {
232             mFlagTogglerPrefUi.onStop();
233         }
234         super.onStop();
235     }
236 
loadPluginPrefs()237     private void loadPluginPrefs() {
238         if (mPluginsCategory != null) {
239             mPreferenceScreen.removePreference(mPluginsCategory);
240         }
241         if (!PluginManagerWrapper.hasPlugins(getActivity())) {
242             mPluginsCategory = null;
243             return;
244         }
245         mPluginsCategory = newCategory("Plugins");
246 
247         PluginManagerWrapper manager = PluginManagerWrapper.INSTANCE.get(getContext());
248         Context prefContext = getContext();
249         PackageManager pm = getContext().getPackageManager();
250 
251         Set<String> pluginActions = manager.getPluginActions();
252 
253         ArrayMap<Pair<String, String>, ArrayList<Pair<String, ResolveInfo>>> plugins =
254                 new ArrayMap<>();
255 
256         Set<String> pluginPermissionApps = pm.getPackagesHoldingPermissions(
257                 new String[]{PLUGIN_PERMISSION}, MATCH_DISABLED_COMPONENTS)
258                 .stream()
259                 .map(pi -> pi.packageName)
260                 .collect(Collectors.toSet());
261 
262         for (String action : pluginActions) {
263             String name = toName(action);
264             List<ResolveInfo> result = pm.queryIntentServices(
265                     new Intent(action), MATCH_DISABLED_COMPONENTS | GET_RESOLVED_FILTER);
266             for (ResolveInfo info : result) {
267                 String packageName = info.serviceInfo.packageName;
268                 if (!pluginPermissionApps.contains(packageName)) {
269                     continue;
270                 }
271 
272                 Pair<String, String> key = Pair.create(packageName, info.serviceInfo.processName);
273                 if (!plugins.containsKey(key)) {
274                     plugins.put(key, new ArrayList<>());
275                 }
276                 plugins.get(key).add(Pair.create(name, info));
277             }
278         }
279 
280         PreferenceDataStore enabler = manager.getPluginEnabler();
281         plugins.forEach((key, si) -> {
282             String packageName = key.first;
283             List<ComponentName> componentNames = si.stream()
284                     .map(p -> new ComponentName(packageName, p.second.serviceInfo.name))
285                     .collect(Collectors.toList());
286             if (!componentNames.isEmpty()) {
287                 SwitchPreference pref = new PluginPreference(
288                         prefContext, si.get(0).second, enabler, componentNames);
289                 pref.setSummary("Plugins: "
290                         + si.stream().map(p -> p.first).collect(Collectors.joining(", ")));
291                 mPluginsCategory.addPreference(pref);
292             }
293         });
294     }
295 
maybeAddSandboxCategory()296     private void maybeAddSandboxCategory() {
297         Context context = getContext();
298         if (context == null) {
299             return;
300         }
301         Intent launchSandboxIntent =
302                 new Intent("com.android.quickstep.action.GESTURE_SANDBOX")
303                         .setPackage(context.getPackageName())
304                         .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
305         if (launchSandboxIntent.resolveActivity(context.getPackageManager()) == null) {
306             return;
307         }
308         PreferenceCategory sandboxCategory = newCategory("Gesture Navigation Sandbox");
309         sandboxCategory.setSummary("Learn and practice navigation gestures");
310         Preference launchTutorialStepMenuPreference = new Preference(context);
311         launchTutorialStepMenuPreference.setKey("launchTutorialStepMenu");
312         launchTutorialStepMenuPreference.setTitle("Launch Gesture Tutorial Steps menu");
313         launchTutorialStepMenuPreference.setSummary("Select a gesture tutorial step.");
314         launchTutorialStepMenuPreference.setOnPreferenceClickListener(preference -> {
315             startActivity(launchSandboxIntent.putExtra("use_tutorial_menu", true));
316             return true;
317         });
318         sandboxCategory.addPreference(launchTutorialStepMenuPreference);
319         Preference launchOnboardingTutorialPreference = new Preference(context);
320         launchOnboardingTutorialPreference.setKey("launchOnboardingTutorial");
321         launchOnboardingTutorialPreference.setTitle("Launch Onboarding Tutorial");
322         launchOnboardingTutorialPreference.setSummary("Learn the basic navigation gestures.");
323         launchOnboardingTutorialPreference.setOnPreferenceClickListener(preference -> {
324             startActivity(launchSandboxIntent
325                     .putExtra("use_tutorial_menu", false)
326                     .putExtra("tutorial_steps",
327                             new String[] {
328                                     "HOME_NAVIGATION",
329                                     "BACK_NAVIGATION",
330                                     "OVERVIEW_NAVIGATION"}));
331             return true;
332         });
333         sandboxCategory.addPreference(launchOnboardingTutorialPreference);
334         Preference launchBackTutorialPreference = new Preference(context);
335         launchBackTutorialPreference.setKey("launchBackTutorial");
336         launchBackTutorialPreference.setTitle("Launch Back Tutorial");
337         launchBackTutorialPreference.setSummary("Learn how to use the Back gesture");
338         launchBackTutorialPreference.setOnPreferenceClickListener(preference -> {
339             startActivity(launchSandboxIntent
340                     .putExtra("use_tutorial_menu", false)
341                     .putExtra("tutorial_steps", new String[] {"BACK_NAVIGATION"}));
342             return true;
343         });
344         sandboxCategory.addPreference(launchBackTutorialPreference);
345         Preference launchHomeTutorialPreference = new Preference(context);
346         launchHomeTutorialPreference.setKey("launchHomeTutorial");
347         launchHomeTutorialPreference.setTitle("Launch Home Tutorial");
348         launchHomeTutorialPreference.setSummary("Learn how to use the Home gesture");
349         launchHomeTutorialPreference.setOnPreferenceClickListener(preference -> {
350             startActivity(launchSandboxIntent
351                     .putExtra("use_tutorial_menu", false)
352                     .putExtra("tutorial_steps", new String[] {"HOME_NAVIGATION"}));
353             return true;
354         });
355         sandboxCategory.addPreference(launchHomeTutorialPreference);
356         Preference launchOverviewTutorialPreference = new Preference(context);
357         launchOverviewTutorialPreference.setKey("launchOverviewTutorial");
358         launchOverviewTutorialPreference.setTitle("Launch Overview Tutorial");
359         launchOverviewTutorialPreference.setSummary("Learn how to use the Overview gesture");
360         launchOverviewTutorialPreference.setOnPreferenceClickListener(preference -> {
361             startActivity(launchSandboxIntent
362                     .putExtra("use_tutorial_menu", false)
363                     .putExtra("tutorial_steps", new String[] {"OVERVIEW_NAVIGATION"}));
364             return true;
365         });
366         sandboxCategory.addPreference(launchOverviewTutorialPreference);
367         Preference launchSecondaryDisplayPreference = new Preference(context);
368         launchSecondaryDisplayPreference.setKey("launchSecondaryDisplay");
369         launchSecondaryDisplayPreference.setTitle("Launch Secondary Display");
370         launchSecondaryDisplayPreference.setSummary("Launch secondary display activity");
371         launchSecondaryDisplayPreference.setOnPreferenceClickListener(preference -> {
372             startActivity(new Intent(context, SecondaryDisplayLauncher.class));
373             return true;
374         });
375         sandboxCategory.addPreference(launchSecondaryDisplayPreference);
376     }
377 
addOnboardingPrefsCatergory()378     private void addOnboardingPrefsCatergory() {
379         PreferenceCategory onboardingCategory = newCategory("Onboarding Flows");
380         onboardingCategory.setSummary("Reset these if you want to see the education again.");
381         for (Map.Entry<String, String[]> titleAndKeys : OnboardingPrefs.ALL_PREF_KEYS.entrySet()) {
382             String title = titleAndKeys.getKey();
383             String[] keys = titleAndKeys.getValue();
384             Preference onboardingPref = new Preference(getContext());
385             onboardingPref.setTitle(title);
386             onboardingPref.setSummary("Tap to reset");
387             onboardingPref.setOnPreferenceClickListener(preference -> {
388                 SharedPreferences.Editor sharedPrefsEdit = LauncherPrefs.getPrefs(getContext())
389                         .edit();
390                 for (String key : keys) {
391                     sharedPrefsEdit.remove(key);
392                 }
393                 sharedPrefsEdit.apply();
394                 Toast.makeText(getContext(), "Reset " + title, Toast.LENGTH_SHORT).show();
395                 return true;
396             });
397             onboardingCategory.addPreference(onboardingPref);
398         }
399     }
400 
addAllAppsFromOverviewCatergory()401     private void addAllAppsFromOverviewCatergory() {
402         PreferenceCategory category = newCategory("All Apps from Overview Config");
403 
404         SeekBarPreference thresholdPref = new SeekBarPreference(getContext());
405         thresholdPref.setTitle("Threshold to open All Apps from Overview");
406         thresholdPref.setSingleLineTitle(false);
407 
408         // These values are 100x swipe up shift value (100 = where overview sits).
409         thresholdPref.setMax(500);
410         thresholdPref.setMin(105);
411         thresholdPref.setUpdatesContinuously(true);
412         thresholdPref.setIconSpaceReserved(false);
413         // Don't directly save to shared prefs, use LauncherPrefs instead.
414         thresholdPref.setPersistent(false);
415         thresholdPref.setOnPreferenceChangeListener((preference, newValue) -> {
416             LauncherPrefs.get(getContext()).put(ALL_APPS_OVERVIEW_THRESHOLD, newValue);
417             preference.setSummary(String.valueOf((int) newValue / 100f));
418             return true;
419         });
420         int value = LauncherPrefs.get(getContext()).get(ALL_APPS_OVERVIEW_THRESHOLD);
421         thresholdPref.setValue(value);
422         // For some reason the initial value is not triggering the summary update, so call manually.
423         thresholdPref.getOnPreferenceChangeListener().onPreferenceChange(thresholdPref, value);
424 
425         category.addPreference(thresholdPref);
426     }
427 
toName(String action)428     private String toName(String action) {
429         String str = action.replace("com.android.systemui.action.PLUGIN_", "")
430                 .replace("com.android.launcher3.action.PLUGIN_", "");
431         StringBuilder b = new StringBuilder();
432         for (String s : str.split("_")) {
433             if (b.length() != 0) {
434                 b.append(' ');
435             }
436             b.append(s.substring(0, 1));
437             b.append(s.substring(1).toLowerCase());
438         }
439         return b.toString();
440     }
441 
442     private static class PluginPreference extends SwitchPreference {
443         private final String mPackageName;
444         private final ResolveInfo mSettingsInfo;
445         private final PreferenceDataStore mPluginEnabler;
446         private final List<ComponentName> mComponentNames;
447 
PluginPreference(Context prefContext, ResolveInfo pluginInfo, PreferenceDataStore pluginEnabler, List<ComponentName> componentNames)448         PluginPreference(Context prefContext, ResolveInfo pluginInfo,
449                 PreferenceDataStore pluginEnabler, List<ComponentName> componentNames) {
450             super(prefContext);
451             PackageManager pm = prefContext.getPackageManager();
452             mPackageName = pluginInfo.serviceInfo.applicationInfo.packageName;
453             Intent settingsIntent = new Intent(ACTION_PLUGIN_SETTINGS).setPackage(mPackageName);
454             // If any Settings activity in app has category filters, set plugin action as category.
455             List<ResolveInfo> settingsInfos =
456                     pm.queryIntentActivities(settingsIntent, GET_RESOLVED_FILTER);
457             if (pluginInfo.filter != null) {
458                 for (ResolveInfo settingsInfo : settingsInfos) {
459                     if (settingsInfo.filter != null && settingsInfo.filter.countCategories() > 0) {
460                         settingsIntent.addCategory(pluginInfo.filter.getAction(0));
461                         break;
462                     }
463                 }
464             }
465 
466             mSettingsInfo = pm.resolveActivity(settingsIntent, 0);
467             mPluginEnabler = pluginEnabler;
468             mComponentNames = componentNames;
469             setTitle(pluginInfo.loadLabel(pm));
470             setChecked(isPluginEnabled());
471             setWidgetLayoutResource(R.layout.switch_preference_with_settings);
472         }
473 
isEnabled(ComponentName cn)474         private boolean isEnabled(ComponentName cn) {
475             return mPluginEnabler.getBoolean(pluginEnabledKey(cn), true);
476 
477         }
478 
isPluginEnabled()479         private boolean isPluginEnabled() {
480             for (ComponentName componentName : mComponentNames) {
481                 if (!isEnabled(componentName)) {
482                     return false;
483                 }
484             }
485             return true;
486         }
487 
488         @Override
persistBoolean(boolean isEnabled)489         protected boolean persistBoolean(boolean isEnabled) {
490             boolean shouldSendBroadcast = false;
491             for (ComponentName componentName : mComponentNames) {
492                 if (isEnabled(componentName) != isEnabled) {
493                     mPluginEnabler.putBoolean(pluginEnabledKey(componentName), isEnabled);
494                     shouldSendBroadcast = true;
495                 }
496             }
497             if (shouldSendBroadcast) {
498                 final String pkg = mPackageName;
499                 final Intent intent = new Intent(PLUGIN_CHANGED,
500                         pkg != null ? Uri.fromParts("package", pkg, null) : null);
501                 getContext().sendBroadcast(intent);
502             }
503             setChecked(isEnabled);
504             return true;
505         }
506 
507         @Override
onBindViewHolder(PreferenceViewHolder holder)508         public void onBindViewHolder(PreferenceViewHolder holder) {
509             super.onBindViewHolder(holder);
510             boolean hasSettings = mSettingsInfo != null;
511             holder.findViewById(R.id.settings).setVisibility(hasSettings ? VISIBLE : GONE);
512             holder.findViewById(R.id.divider).setVisibility(hasSettings ? VISIBLE : GONE);
513             holder.findViewById(R.id.settings).setOnClickListener(v -> {
514                 if (hasSettings) {
515                     v.getContext().startActivity(new Intent().setComponent(
516                             new ComponentName(mSettingsInfo.activityInfo.packageName,
517                                     mSettingsInfo.activityInfo.name)));
518                 }
519             });
520             holder.itemView.setOnLongClickListener(v -> {
521                 Intent intent = new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS);
522                 intent.setData(Uri.fromParts("package", mPackageName, null));
523                 getContext().startActivity(intent);
524                 return true;
525             });
526         }
527     }
528 }
529