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