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