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