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