1 /* 2 * Copyright (C) 2023 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 17 package com.example.android.vdmdemo.host; 18 19 import static android.Manifest.permission.ADD_ALWAYS_UNLOCKED_DISPLAY; 20 import static android.Manifest.permission.ADD_TRUSTED_DISPLAY; 21 import static android.os.Build.VERSION.SDK_INT; 22 import static android.os.Build.VERSION_CODES.BAKLAVA; 23 import static android.os.Build.VERSION_CODES.TIRAMISU; 24 import static android.os.Build.VERSION_CODES.UPSIDE_DOWN_CAKE; 25 import static android.os.Build.VERSION_CODES.VANILLA_ICE_CREAM; 26 27 import android.companion.AssociationRequest; 28 import android.companion.virtualdevice.flags.Flags; 29 import android.content.Context; 30 import android.content.SharedPreferences; 31 import android.content.pm.PackageManager; 32 import android.util.ArrayMap; 33 import android.util.Log; 34 35 import androidx.annotation.StringRes; 36 import androidx.preference.Preference; 37 import androidx.preference.PreferenceManager; 38 39 import dagger.hilt.android.qualifiers.ApplicationContext; 40 41 import java.util.Arrays; 42 import java.util.Map; 43 import java.util.Objects; 44 import java.util.Set; 45 import java.util.function.BooleanSupplier; 46 import java.util.function.Consumer; 47 48 import javax.inject.Inject; 49 import javax.inject.Singleton; 50 51 /** 52 * Manages the VDM Demo Host application settings and feature switches. 53 * 54 * <p>Upon creation, it will automatically update the preference values based on the current SDK 55 * version and the relevant feature flags.</p> 56 */ 57 @Singleton 58 final class PreferenceController { 59 60 private static final String TAG = PreferenceController.class.getSimpleName(); 61 62 // LINT.IfChange 63 private static final Set<PrefRule<?>> RULES = Set.of( 64 65 // Exposed in the settings page 66 67 new StringRule(R.string.pref_device_profile, UPSIDE_DOWN_CAKE) 68 .withDefaultValue(AssociationRequest.DEVICE_PROFILE_APP_STREAMING), 69 70 new BoolRule(R.string.pref_hide_from_recents, UPSIDE_DOWN_CAKE) 71 .withRequiredPermissions(ADD_TRUSTED_DISPLAY), 72 73 new BoolRule(R.string.pref_enable_cross_device_clipboard, VANILLA_ICE_CREAM) 74 .withRequiredPermissions(ADD_TRUSTED_DISPLAY), 75 76 new BoolRule(R.string.pref_enable_custom_activity_policy, BAKLAVA, 77 Flags::activityControlApi), 78 79 new BoolRule(R.string.pref_enable_client_camera, VANILLA_ICE_CREAM), 80 81 new BoolRule(R.string.pref_enable_client_sensors, UPSIDE_DOWN_CAKE), 82 83 new BoolRule(R.string.pref_enable_client_audio, UPSIDE_DOWN_CAKE), 84 85 new BoolRule(R.string.pref_enable_display_rotation, VANILLA_ICE_CREAM) 86 .withDefaultValue(true), 87 88 new BoolRule(R.string.pref_enable_display_category, UPSIDE_DOWN_CAKE), 89 90 new BoolRule(R.string.pref_always_unlocked_device, TIRAMISU) 91 .withRequiredPermissions(ADD_ALWAYS_UNLOCKED_DISPLAY), 92 93 new BoolRule(R.string.pref_show_pointer_icon, TIRAMISU) 94 .withRequiredPermissions(ADD_TRUSTED_DISPLAY), 95 96 new BoolRule(R.string.pref_enable_custom_home, VANILLA_ICE_CREAM) 97 .withRequiredPermissions(ADD_TRUSTED_DISPLAY), 98 99 new BoolRule(R.string.pref_enable_custom_status_bar, BAKLAVA, Flags::statusBarAndInsets) 100 .withRequiredPermissions(ADD_TRUSTED_DISPLAY), 101 102 new StringRule(R.string.pref_display_timeout, BAKLAVA, 103 Flags::deviceAwareDisplayPower, Flags::displayPowerManagerApis) 104 .withDefaultValue(String.valueOf(0)), 105 106 new StringRule(R.string.pref_enable_client_brightness, BAKLAVA, 107 Flags::deviceAwareDisplayPower, Flags::displayPowerManagerApis), 108 109 new StringRule(R.string.pref_display_ime_policy, VANILLA_ICE_CREAM) 110 .withRequiredPermissions(ADD_TRUSTED_DISPLAY) 111 .withDefaultValue(String.valueOf(0)), 112 113 new BoolRule(R.string.pref_enable_client_native_ime, VANILLA_ICE_CREAM) 114 .withRequiredPermissions(ADD_TRUSTED_DISPLAY), 115 116 new BoolRule(R.string.pref_standalone_host_demo, TIRAMISU), 117 118 new BoolRule(R.string.pref_record_encoder_output, TIRAMISU), 119 120 new StringRule(R.string.pref_network_channel, TIRAMISU) 121 .withDefaultValue(String.valueOf(0)), 122 123 new BoolRule(R.string.pref_enable_update_audio_policy_mixes, VANILLA_ICE_CREAM) 124 .withDefaultValue(true), 125 126 // Internal-only switches not exposed in the settings page. 127 // All of these are booleans acting as switches, while the above ones may be any type. 128 129 new InternalBoolRule(R.string.internal_pref_home_displays_supported, TIRAMISU) 130 .withRequiredPermissions(ADD_TRUSTED_DISPLAY), 131 132 new InternalBoolRule(R.string.internal_pref_mirror_displays_supported, 133 VANILLA_ICE_CREAM), 134 135 new InternalBoolRule(R.string.internal_pref_virtual_stylus_supported, 136 VANILLA_ICE_CREAM), 137 138 new InternalBoolRule(R.string.internal_pref_virtual_rotary_supported, BAKLAVA, 139 Flags::virtualRotary), 140 141 new InternalBoolRule(R.string.internal_pref_display_rotation_supported, BAKLAVA, 142 Flags::virtualDisplayRotationApi) 143 ); 144 // LINT.ThenChange(/samples/VirtualDeviceManager/README.md:host_options) 145 146 private final ArrayMap<Object, Map<String, Consumer<Object>>> mObservers = new ArrayMap<>(); 147 private final SharedPreferences.OnSharedPreferenceChangeListener mPreferenceChangeListener = 148 this::onPreferencesChanged; 149 150 private final Context mContext; 151 private final SharedPreferences mSharedPreferences; 152 153 @Inject PreferenceController(@pplicationContext Context context)154 PreferenceController(@ApplicationContext Context context) { 155 mContext = context; 156 mSharedPreferences = PreferenceManager.getDefaultSharedPreferences(mContext); 157 evaluate(); 158 mSharedPreferences.registerOnSharedPreferenceChangeListener(mPreferenceChangeListener); 159 } 160 161 /** 162 * Adds an observer for preference changes. 163 * 164 * @param key an object used only for bookkeeping. 165 * @param preferenceObserver a map from resource ID corresponding to the preference string key 166 * to the function that should be executed when that preference changes. 167 */ addPreferenceObserver(Object key, Map<Integer, Consumer<Object>> preferenceObserver)168 void addPreferenceObserver(Object key, Map<Integer, Consumer<Object>> preferenceObserver) { 169 ArrayMap<String, Consumer<Object>> stringObserver = new ArrayMap<>(); 170 for (int resId : preferenceObserver.keySet()) { 171 stringObserver.put( 172 Objects.requireNonNull(mContext.getString(resId)), 173 preferenceObserver.get(resId)); 174 } 175 mObservers.put(key, stringObserver); 176 } 177 178 /** Removes a previously added preference observer for the given key. */ removePreferenceObserver(Object key)179 void removePreferenceObserver(Object key) { 180 mObservers.remove(key); 181 } 182 183 /** 184 * Disables any {@link androidx.preference.Preference}, which is not satisfied by the current 185 * SDK version or the relevant feature flags. 186 * 187 * <p>This doesn't change any of the preference values, only disables the relevant UI elements 188 * in the preference screen.</p> 189 */ evaluate(PreferenceManager preferenceManager)190 void evaluate(PreferenceManager preferenceManager) { 191 RULES.forEach(r -> r.evaluate(mContext, preferenceManager)); 192 } 193 evaluate()194 void evaluate() { 195 SharedPreferences.Editor editor = mSharedPreferences.edit(); 196 RULES.forEach(r -> r.evaluate(mContext, mSharedPreferences, editor)); 197 editor.commit(); 198 } 199 getBoolean(@tringRes int resId)200 boolean getBoolean(@StringRes int resId) { 201 return mSharedPreferences.getBoolean(mContext.getString(resId), false); 202 } 203 getString(@tringRes int resId)204 String getString(@StringRes int resId) { 205 return Objects.requireNonNull( 206 mSharedPreferences.getString(mContext.getString(resId), null)); 207 } 208 getInt(@tringRes int resId)209 int getInt(@StringRes int resId) { 210 return Integer.valueOf(getString(resId)); 211 } 212 onPreferencesChanged(SharedPreferences sharedPreferences, String key)213 private void onPreferencesChanged(SharedPreferences sharedPreferences, String key) { 214 Map<String, ?> currentPreferences = sharedPreferences.getAll(); 215 for (Map<String, Consumer<Object>> observer : mObservers.values()) { 216 Consumer<Object> consumer = observer.get(key); 217 if (consumer != null) { 218 consumer.accept(currentPreferences.get(key)); 219 } 220 } 221 } 222 223 private abstract static class PrefRule<T> { 224 final @StringRes int mKey; 225 final int mMinSdk; 226 final BooleanSupplier[] mRequiredFlags; 227 228 protected String[] mRequiredPermissions = null; 229 protected T mDefaultValue; 230 PrefRule(@tringRes int key, T defaultValue, int minSdk, BooleanSupplier... requiredFlags)231 PrefRule(@StringRes int key, T defaultValue, int minSdk, BooleanSupplier... requiredFlags) { 232 mKey = key; 233 mMinSdk = minSdk; 234 mRequiredFlags = requiredFlags; 235 mDefaultValue = defaultValue; 236 } 237 evaluate(Context context, SharedPreferences prefs, SharedPreferences.Editor editor)238 void evaluate(Context context, SharedPreferences prefs, SharedPreferences.Editor editor) { 239 String preferenceName = context.getString(mKey); 240 if (!prefs.contains(preferenceName) || !isSatisfied(context, preferenceName)) { 241 reset(context, editor); 242 } 243 } 244 evaluate(Context context, PreferenceManager preferenceManager)245 void evaluate(Context context, PreferenceManager preferenceManager) { 246 String preferenceName = context.getString(mKey); 247 Preference preference = preferenceManager.findPreference(preferenceName); 248 if (preference != null) { 249 boolean enabled = isSatisfied(context, preferenceName); 250 if (preference.isEnabled() != enabled) { 251 preference.setEnabled(enabled); 252 } 253 } 254 } 255 reset(Context context, SharedPreferences.Editor editor)256 protected abstract void reset(Context context, SharedPreferences.Editor editor); 257 isSatisfied(Context context, String preferenceName)258 protected boolean isSatisfied(Context context, String preferenceName) { 259 if (mRequiredPermissions != null) { 260 for (String requiredPermission : mRequiredPermissions) { 261 if (context.checkCallingOrSelfPermission(requiredPermission) 262 != PackageManager.PERMISSION_GRANTED) { 263 return false; 264 } 265 } 266 } 267 try { 268 return isSdkVersionSatisfied() 269 && Arrays.stream(mRequiredFlags).allMatch(BooleanSupplier::getAsBoolean); 270 } catch (NoSuchMethodError e) { 271 Log.w(TAG, "Missing at least one required flag for feature: " + preferenceName, e); 272 return false; 273 } 274 } 275 isSdkVersionSatisfied()276 private boolean isSdkVersionSatisfied() { 277 return mMinSdk <= SDK_INT || (mMinSdk == BAKLAVA && VdmCompat.isAtLeastB()); 278 } 279 withDefaultValue(T defaultValue)280 PrefRule<T> withDefaultValue(T defaultValue) { 281 mDefaultValue = defaultValue; 282 return this; 283 } 284 withRequiredPermissions(String... permissions)285 PrefRule<T> withRequiredPermissions(String... permissions) { 286 mRequiredPermissions = permissions; 287 return this; 288 } 289 } 290 291 private static class BoolRule extends PrefRule<Boolean> { BoolRule(@tringRes int key, int minSdk, BooleanSupplier... requiredFlags)292 BoolRule(@StringRes int key, int minSdk, BooleanSupplier... requiredFlags) { 293 super(key, false, minSdk, requiredFlags); 294 } 295 296 @Override reset(Context context, SharedPreferences.Editor editor)297 protected void reset(Context context, SharedPreferences.Editor editor) { 298 editor.putBoolean(context.getString(mKey), mDefaultValue); 299 } 300 } 301 302 private static class InternalBoolRule extends BoolRule { InternalBoolRule(@tringRes int key, int minSdk, BooleanSupplier... requiredFlags)303 InternalBoolRule(@StringRes int key, int minSdk, BooleanSupplier... requiredFlags) { 304 super(key, minSdk, requiredFlags); 305 } 306 307 @Override evaluate(Context context, SharedPreferences prefs, SharedPreferences.Editor editor)308 void evaluate(Context context, SharedPreferences prefs, SharedPreferences.Editor editor) { 309 String preferenceName = context.getString(mKey); 310 editor.putBoolean(preferenceName, isSatisfied(context, preferenceName)); 311 } 312 } 313 314 private static class StringRule extends PrefRule<String> { StringRule(@tringRes int key, int minSdk, BooleanSupplier... requiredFlags)315 StringRule(@StringRes int key, int minSdk, BooleanSupplier... requiredFlags) { 316 super(key, null, minSdk, requiredFlags); 317 } 318 319 @Override reset(Context context, SharedPreferences.Editor editor)320 protected void reset(Context context, SharedPreferences.Editor editor) { 321 editor.putString(context.getString(mKey), mDefaultValue); 322 } 323 } 324 } 325