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