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