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.MATCH_DISABLED_COMPONENTS; 19 20 import static com.android.launcher3.uioverrides.plugins.PluginManagerWrapper.PLUGIN_CHANGED; 21 import static com.android.launcher3.uioverrides.plugins.PluginManagerWrapper.pluginEnabledKey; 22 23 import android.annotation.TargetApi; 24 import android.content.BroadcastReceiver; 25 import android.content.ComponentName; 26 import android.content.Context; 27 import android.content.Intent; 28 import android.content.IntentFilter; 29 import android.content.pm.ApplicationInfo; 30 import android.content.pm.PackageManager; 31 import android.content.pm.ResolveInfo; 32 import android.content.pm.ServiceInfo; 33 import android.net.Uri; 34 import android.os.Build; 35 import android.os.Bundle; 36 import android.provider.Settings; 37 import android.util.ArrayMap; 38 import android.util.Pair; 39 import android.view.Menu; 40 import android.view.MenuInflater; 41 import android.view.MenuItem; 42 import android.view.View; 43 44 import androidx.preference.Preference; 45 import androidx.preference.PreferenceCategory; 46 import androidx.preference.PreferenceDataStore; 47 import androidx.preference.PreferenceFragmentCompat; 48 import androidx.preference.PreferenceScreen; 49 import androidx.preference.PreferenceViewHolder; 50 import androidx.preference.SwitchPreference; 51 52 import com.android.launcher3.R; 53 import com.android.launcher3.config.FeatureFlags; 54 import com.android.launcher3.config.FlagTogglerPrefUi; 55 import com.android.launcher3.uioverrides.plugins.PluginManagerWrapper; 56 57 import java.util.ArrayList; 58 import java.util.List; 59 import java.util.Set; 60 import java.util.stream.Collectors; 61 62 /** 63 * Dev-build only UI allowing developers to toggle flag settings and plugins. 64 * See {@link FeatureFlags}. 65 */ 66 @TargetApi(Build.VERSION_CODES.O) 67 public class DeveloperOptionsFragment extends PreferenceFragmentCompat { 68 69 private static final String ACTION_PLUGIN_SETTINGS = "com.android.systemui.action.PLUGIN_SETTINGS"; 70 private static final String PLUGIN_PERMISSION = "com.android.systemui.permission.PLUGIN"; 71 72 private final BroadcastReceiver mPluginReceiver = new BroadcastReceiver() { 73 @Override 74 public void onReceive(Context context, Intent intent) { 75 loadPluginPrefs(); 76 } 77 }; 78 79 private PreferenceScreen mPreferenceScreen; 80 81 private PreferenceCategory mPluginsCategory; 82 private FlagTogglerPrefUi mFlagTogglerPrefUi; 83 84 @Override onCreatePreferences(Bundle savedInstanceState, String rootKey)85 public void onCreatePreferences(Bundle savedInstanceState, String rootKey) { 86 IntentFilter filter = new IntentFilter(Intent.ACTION_PACKAGE_ADDED); 87 filter.addAction(Intent.ACTION_PACKAGE_CHANGED); 88 filter.addAction(Intent.ACTION_PACKAGE_REMOVED); 89 filter.addDataScheme("package"); 90 getContext().registerReceiver(mPluginReceiver, filter); 91 getContext().registerReceiver(mPluginReceiver, 92 new IntentFilter(Intent.ACTION_USER_UNLOCKED)); 93 94 mPreferenceScreen = getPreferenceManager().createPreferenceScreen(getContext()); 95 setPreferenceScreen(mPreferenceScreen); 96 97 initFlags(); 98 loadPluginPrefs(); 99 maybeAddSandboxCategory(); 100 } 101 102 @Override onDestroy()103 public void onDestroy() { 104 super.onDestroy(); 105 getContext().unregisterReceiver(mPluginReceiver); 106 } 107 newCategory(String title)108 private PreferenceCategory newCategory(String title) { 109 PreferenceCategory category = new PreferenceCategory(getContext()); 110 category.setOrder(Preference.DEFAULT_ORDER); 111 category.setTitle(title); 112 mPreferenceScreen.addPreference(category); 113 return category; 114 } 115 initFlags()116 private void initFlags() { 117 if (!FeatureFlags.showFlagTogglerUi(getContext())) { 118 return; 119 } 120 121 mFlagTogglerPrefUi = new FlagTogglerPrefUi(this); 122 mFlagTogglerPrefUi.applyTo(newCategory("Feature flags")); 123 } 124 125 @Override onCreateOptionsMenu(Menu menu, MenuInflater inflater)126 public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { 127 if (mFlagTogglerPrefUi != null) { 128 mFlagTogglerPrefUi.onCreateOptionsMenu(menu); 129 } 130 } 131 132 @Override onOptionsItemSelected(MenuItem item)133 public boolean onOptionsItemSelected(MenuItem item) { 134 if (mFlagTogglerPrefUi != null) { 135 mFlagTogglerPrefUi.onOptionsItemSelected(item); 136 } 137 return super.onOptionsItemSelected(item); 138 } 139 140 @Override onStop()141 public void onStop() { 142 if (mFlagTogglerPrefUi != null) { 143 mFlagTogglerPrefUi.onStop(); 144 } 145 super.onStop(); 146 } 147 loadPluginPrefs()148 private void loadPluginPrefs() { 149 if (mPluginsCategory != null) { 150 mPreferenceScreen.removePreference(mPluginsCategory); 151 } 152 if (!PluginManagerWrapper.hasPlugins(getActivity())) { 153 mPluginsCategory = null; 154 return; 155 } 156 mPluginsCategory = newCategory("Plugins"); 157 158 PluginManagerWrapper manager = PluginManagerWrapper.INSTANCE.get(getContext()); 159 Context prefContext = getContext(); 160 PackageManager pm = getContext().getPackageManager(); 161 162 Set<String> pluginActions = manager.getPluginActions(); 163 164 ArrayMap<Pair<String, String>, ArrayList<Pair<String, ServiceInfo>>> plugins = 165 new ArrayMap<>(); 166 167 Set<String> pluginPermissionApps = pm.getPackagesHoldingPermissions( 168 new String[]{PLUGIN_PERMISSION}, MATCH_DISABLED_COMPONENTS) 169 .stream() 170 .map(pi -> pi.packageName) 171 .collect(Collectors.toSet()); 172 173 for (String action : pluginActions) { 174 String name = toName(action); 175 List<ResolveInfo> result = pm.queryIntentServices( 176 new Intent(action), MATCH_DISABLED_COMPONENTS); 177 for (ResolveInfo info : result) { 178 String packageName = info.serviceInfo.packageName; 179 if (!pluginPermissionApps.contains(packageName)) { 180 continue; 181 } 182 183 Pair<String, String> key = Pair.create(packageName, info.serviceInfo.processName); 184 if (!plugins.containsKey(key)) { 185 plugins.put(key, new ArrayList<>()); 186 } 187 plugins.get(key).add(Pair.create(name, info.serviceInfo)); 188 } 189 } 190 191 PreferenceDataStore enabler = manager.getPluginEnabler(); 192 plugins.forEach((key, si) -> { 193 String packageName = key.first; 194 List<ComponentName> componentNames = si.stream() 195 .map(p -> new ComponentName(packageName, p.second.name)) 196 .collect(Collectors.toList()); 197 if (!componentNames.isEmpty()) { 198 SwitchPreference pref = new PluginPreference( 199 prefContext, si.get(0).second.applicationInfo, enabler, componentNames); 200 pref.setSummary("Plugins: " 201 + si.stream().map(p -> p.first).collect(Collectors.joining(", "))); 202 mPluginsCategory.addPreference(pref); 203 } 204 }); 205 } 206 maybeAddSandboxCategory()207 private void maybeAddSandboxCategory() { 208 Context context = getContext(); 209 if (context == null) { 210 return; 211 } 212 Intent launchSandboxIntent = 213 new Intent("com.android.quickstep.action.GESTURE_SANDBOX") 214 .setPackage(context.getPackageName()) 215 .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); 216 if (launchSandboxIntent.resolveActivity(context.getPackageManager()) == null) { 217 return; 218 } 219 PreferenceCategory sandboxCategory = newCategory("Gesture Navigation Sandbox"); 220 sandboxCategory.setSummary("Learn and practice navigation gestures"); 221 Preference launchBackTutorialPreference = new Preference(context); 222 launchBackTutorialPreference.setKey("launchBackTutorial"); 223 launchBackTutorialPreference.setTitle("Launch Back Tutorial"); 224 launchBackTutorialPreference.setSummary("Learn how to use the Back gesture"); 225 launchBackTutorialPreference.setOnPreferenceClickListener(preference -> { 226 startActivity(launchSandboxIntent.putExtra( 227 "tutorial_type", "RIGHT_EDGE_BACK_NAVIGATION")); 228 return true; 229 }); 230 sandboxCategory.addPreference(launchBackTutorialPreference); 231 Preference launchHomeTutorialPreference = new Preference(context); 232 launchHomeTutorialPreference.setKey("launchHomeTutorial"); 233 launchHomeTutorialPreference.setTitle("Launch Home Tutorial"); 234 launchHomeTutorialPreference.setSummary("Learn how to use the Home gesture"); 235 launchHomeTutorialPreference.setOnPreferenceClickListener(preference -> { 236 startActivity(launchSandboxIntent.putExtra("tutorial_type", "HOME_NAVIGATION")); 237 return true; 238 }); 239 sandboxCategory.addPreference(launchHomeTutorialPreference); 240 Preference launchOverviewTutorialPreference = new Preference(context); 241 launchOverviewTutorialPreference.setKey("launchOverviewTutorial"); 242 launchOverviewTutorialPreference.setTitle("Launch Overview Tutorial"); 243 launchOverviewTutorialPreference.setSummary("Learn how to use the Overview gesture"); 244 launchOverviewTutorialPreference.setOnPreferenceClickListener(preference -> { 245 startActivity(launchSandboxIntent.putExtra("tutorial_type", "OVERVIEW_NAVIGATION")); 246 return true; 247 }); 248 sandboxCategory.addPreference(launchOverviewTutorialPreference); 249 Preference launchAssistantTutorialPreference = new Preference(context); 250 launchAssistantTutorialPreference.setKey("launchAssistantTutorial"); 251 launchAssistantTutorialPreference.setTitle("Launch Assistant Tutorial"); 252 launchAssistantTutorialPreference.setSummary("Learn how to use the Assistant gesture"); 253 launchAssistantTutorialPreference.setOnPreferenceClickListener(preference -> { 254 startActivity(launchSandboxIntent.putExtra("tutorial_type", "ASSISTANT")); 255 return true; 256 }); 257 sandboxCategory.addPreference(launchAssistantTutorialPreference); 258 } 259 toName(String action)260 private String toName(String action) { 261 String str = action.replace("com.android.systemui.action.PLUGIN_", "") 262 .replace("com.android.launcher3.action.PLUGIN_", ""); 263 StringBuilder b = new StringBuilder(); 264 for (String s : str.split("_")) { 265 if (b.length() != 0) { 266 b.append(' '); 267 } 268 b.append(s.substring(0, 1)); 269 b.append(s.substring(1).toLowerCase()); 270 } 271 return b.toString(); 272 } 273 274 private static class PluginPreference extends SwitchPreference { 275 private final boolean mHasSettings; 276 private final PreferenceDataStore mPluginEnabler; 277 private final String mPackageName; 278 private final List<ComponentName> mComponentNames; 279 PluginPreference(Context prefContext, ApplicationInfo info, PreferenceDataStore pluginEnabler, List<ComponentName> componentNames)280 PluginPreference(Context prefContext, ApplicationInfo info, 281 PreferenceDataStore pluginEnabler, List<ComponentName> componentNames) { 282 super(prefContext); 283 PackageManager pm = prefContext.getPackageManager(); 284 mHasSettings = pm.resolveActivity(new Intent(ACTION_PLUGIN_SETTINGS) 285 .setPackage(info.packageName), 0) != null; 286 mPackageName = info.packageName; 287 mComponentNames = componentNames; 288 mPluginEnabler = pluginEnabler; 289 setTitle(info.loadLabel(pm)); 290 setChecked(isPluginEnabled()); 291 setWidgetLayoutResource(R.layout.switch_preference_with_settings); 292 } 293 isEnabled(ComponentName cn)294 private boolean isEnabled(ComponentName cn) { 295 return mPluginEnabler.getBoolean(pluginEnabledKey(cn), true); 296 297 } 298 isPluginEnabled()299 private boolean isPluginEnabled() { 300 for (ComponentName componentName : mComponentNames) { 301 if (!isEnabled(componentName)) { 302 return false; 303 } 304 } 305 return true; 306 } 307 308 @Override persistBoolean(boolean isEnabled)309 protected boolean persistBoolean(boolean isEnabled) { 310 boolean shouldSendBroadcast = false; 311 for (ComponentName componentName : mComponentNames) { 312 if (isEnabled(componentName) != isEnabled) { 313 mPluginEnabler.putBoolean(pluginEnabledKey(componentName), isEnabled); 314 shouldSendBroadcast = true; 315 } 316 } 317 if (shouldSendBroadcast) { 318 final String pkg = mPackageName; 319 final Intent intent = new Intent(PLUGIN_CHANGED, 320 pkg != null ? Uri.fromParts("package", pkg, null) : null); 321 getContext().sendBroadcast(intent); 322 } 323 setChecked(isEnabled); 324 return true; 325 } 326 327 @Override onBindViewHolder(PreferenceViewHolder holder)328 public void onBindViewHolder(PreferenceViewHolder holder) { 329 super.onBindViewHolder(holder); 330 holder.findViewById(R.id.settings).setVisibility(mHasSettings ? View.VISIBLE 331 : View.GONE); 332 holder.findViewById(R.id.divider).setVisibility(mHasSettings ? View.VISIBLE 333 : View.GONE); 334 holder.findViewById(R.id.settings).setOnClickListener(v -> { 335 ResolveInfo result = v.getContext().getPackageManager().resolveActivity( 336 new Intent(ACTION_PLUGIN_SETTINGS).setPackage(mPackageName), 0); 337 if (result != null) { 338 v.getContext().startActivity(new Intent().setComponent( 339 new ComponentName(result.activityInfo.packageName, 340 result.activityInfo.name))); 341 } 342 }); 343 holder.itemView.setOnLongClickListener(v -> { 344 Intent intent = new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS); 345 intent.setData(Uri.fromParts("package", mPackageName, null)); 346 getContext().startActivity(intent); 347 return true; 348 }); 349 } 350 } 351 } 352