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