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.os.Build.VERSION.SDK_INT; 20 import static android.os.Build.VERSION_CODES.TIRAMISU; 21 import static android.os.Build.VERSION_CODES.UPSIDE_DOWN_CAKE; 22 import static android.os.Build.VERSION_CODES.VANILLA_ICE_CREAM; 23 24 import android.companion.AssociationRequest; 25 import android.companion.virtual.flags.Flags; 26 import android.content.Context; 27 import android.content.SharedPreferences; 28 import android.util.ArrayMap; 29 30 import androidx.annotation.StringRes; 31 import androidx.core.os.BuildCompat; 32 import androidx.preference.Preference; 33 import androidx.preference.PreferenceManager; 34 35 import dagger.hilt.android.qualifiers.ApplicationContext; 36 37 import java.util.Arrays; 38 import java.util.Map; 39 import java.util.Objects; 40 import java.util.Set; 41 import java.util.function.BooleanSupplier; 42 import java.util.function.Consumer; 43 44 import javax.inject.Inject; 45 import javax.inject.Singleton; 46 47 /** 48 * Manages the VDM Demo Host application settings and feature switches. 49 * 50 * <p>Upon creation, it will automatically update the preference values based on the current SDK 51 * version and the relevant feature flags.</p> 52 */ 53 @Singleton 54 final class PreferenceController { 55 56 // LINT.IfChange 57 private static final Set<PrefRule<?>> RULES = Set.of( 58 59 // Exposed in the settings page 60 61 new StringRule(R.string.pref_device_profile, UPSIDE_DOWN_CAKE) 62 .withDefaultValue(AssociationRequest.DEVICE_PROFILE_APP_STREAMING), 63 64 new BoolRule(R.string.pref_hide_from_recents, UPSIDE_DOWN_CAKE), 65 66 new BoolRule(R.string.pref_enable_cross_device_clipboard, 67 VANILLA_ICE_CREAM, Flags::crossDeviceClipboard), 68 69 new BoolRule(R.string.pref_enable_client_camera, VANILLA_ICE_CREAM, 70 Flags::virtualCamera), 71 72 new BoolRule(R.string.pref_enable_client_sensors, UPSIDE_DOWN_CAKE), 73 74 new BoolRule(R.string.pref_enable_client_audio, UPSIDE_DOWN_CAKE), 75 76 new BoolRule(R.string.pref_enable_display_rotation, 77 VANILLA_ICE_CREAM, Flags::consistentDisplayFlags) 78 .withDefaultValue(true), 79 80 new BoolRule(R.string.pref_always_unlocked_device, TIRAMISU), 81 82 new BoolRule(R.string.pref_show_pointer_icon, TIRAMISU), 83 84 new BoolRule(R.string.pref_enable_custom_home, VANILLA_ICE_CREAM, Flags::vdmCustomHome), 85 86 new StringRule(R.string.pref_display_ime_policy, VANILLA_ICE_CREAM, Flags::vdmCustomIme) 87 .withDefaultValue(String.valueOf(0)), 88 89 new BoolRule(R.string.pref_enable_client_native_ime, 90 VANILLA_ICE_CREAM, Flags::vdmCustomIme), 91 92 new BoolRule(R.string.pref_record_encoder_output, TIRAMISU), 93 94 95 // Internal-only switches not exposed in the settings page. 96 // All of these are booleans acting as switches, while the above ones may be any type. 97 98 new InternalBoolRule(R.string.internal_pref_home_displays_supported, TIRAMISU), 99 100 new InternalBoolRule(R.string.internal_pref_mirror_displays_supported, 101 VANILLA_ICE_CREAM, 102 Flags::consistentDisplayFlags, Flags::interactiveScreenMirror), 103 104 new InternalBoolRule(R.string.internal_pref_virtual_stylus_supported, 105 VANILLA_ICE_CREAM, Flags::virtualStylus) 106 ); 107 // LINT.ThenChange(/samples/VirtualDeviceManager/README.md:host_options) 108 109 private final ArrayMap<Object, Map<String, Consumer<Object>>> mObservers = new ArrayMap<>(); 110 private final SharedPreferences.OnSharedPreferenceChangeListener mPreferenceChangeListener = 111 this::onPreferencesChanged; 112 113 private final Context mContext; 114 private final SharedPreferences mSharedPreferences; 115 116 @Inject PreferenceController(@pplicationContext Context context)117 PreferenceController(@ApplicationContext Context context) { 118 mContext = context; 119 mSharedPreferences = PreferenceManager.getDefaultSharedPreferences(mContext); 120 121 SharedPreferences.Editor editor = mSharedPreferences.edit(); 122 RULES.forEach(r -> r.evaluate(mContext, mSharedPreferences, editor)); 123 editor.commit(); 124 125 mSharedPreferences.registerOnSharedPreferenceChangeListener(mPreferenceChangeListener); 126 } 127 128 /** 129 * Adds an observer for preference changes. 130 * 131 * @param key an object used only for bookkeeping. 132 * @param preferenceObserver a map from resource ID corresponding to the preference string key 133 * to the function that should be executed when that preference changes. 134 */ addPreferenceObserver(Object key, Map<Integer, Consumer<Object>> preferenceObserver)135 void addPreferenceObserver(Object key, Map<Integer, Consumer<Object>> preferenceObserver) { 136 ArrayMap<String, Consumer<Object>> stringObserver = new ArrayMap<>(); 137 for (int resId : preferenceObserver.keySet()) { 138 stringObserver.put( 139 Objects.requireNonNull(mContext.getString(resId)), 140 preferenceObserver.get(resId)); 141 } 142 mObservers.put(key, stringObserver); 143 } 144 145 /** Removes a previously added preference observer for the given key. */ removePreferenceObserver(Object key)146 void removePreferenceObserver(Object key) { 147 mObservers.remove(key); 148 } 149 150 /** 151 * Disables any {@link androidx.preference.Preference}, which is not satisfied by the current 152 * SDK version or the relevant feature flags. 153 * 154 * <p>This doesn't change any of the preference values, only disables the relevant UI elements 155 * in the preference screen.</p> 156 */ evaluate(PreferenceManager preferenceManager)157 void evaluate(PreferenceManager preferenceManager) { 158 RULES.forEach(r -> r.evaluate(mContext, preferenceManager)); 159 } 160 getBoolean(@tringRes int resId)161 boolean getBoolean(@StringRes int resId) { 162 return mSharedPreferences.getBoolean(mContext.getString(resId), false); 163 } 164 getString(@tringRes int resId)165 String getString(@StringRes int resId) { 166 return Objects.requireNonNull( 167 mSharedPreferences.getString(mContext.getString(resId), null)); 168 } 169 getInt(@tringRes int resId)170 int getInt(@StringRes int resId) { 171 return Integer.valueOf(getString(resId)); 172 } 173 onPreferencesChanged(SharedPreferences sharedPreferences, String key)174 private void onPreferencesChanged(SharedPreferences sharedPreferences, String key) { 175 Map<String, ?> currentPreferences = sharedPreferences.getAll(); 176 for (Map<String, Consumer<Object>> observer : mObservers.values()) { 177 Consumer<Object> consumer = observer.get(key); 178 if (consumer != null) { 179 consumer.accept(currentPreferences.get(key)); 180 } 181 } 182 } 183 184 private abstract static class PrefRule<T> { 185 final @StringRes int mKey; 186 final int mMinSdk; 187 final BooleanSupplier[] mRequiredFlags; 188 189 protected T mDefaultValue; 190 PrefRule(@tringRes int key, T defaultValue, int minSdk, BooleanSupplier... requiredFlags)191 PrefRule(@StringRes int key, T defaultValue, int minSdk, BooleanSupplier... requiredFlags) { 192 mKey = key; 193 mMinSdk = minSdk; 194 mRequiredFlags = requiredFlags; 195 mDefaultValue = defaultValue; 196 } 197 evaluate(Context context, SharedPreferences prefs, SharedPreferences.Editor editor)198 void evaluate(Context context, SharedPreferences prefs, SharedPreferences.Editor editor) { 199 if (!prefs.contains(context.getString(mKey)) || !isSatisfied()) { 200 reset(context, editor); 201 } 202 } 203 evaluate(Context context, PreferenceManager preferenceManager)204 void evaluate(Context context, PreferenceManager preferenceManager) { 205 Preference preference = preferenceManager.findPreference(context.getString(mKey)); 206 if (preference != null) { 207 boolean enabled = isSatisfied(); 208 if (preference.isEnabled() != enabled) { 209 preference.setEnabled(enabled); 210 } 211 } 212 } 213 reset(Context context, SharedPreferences.Editor editor)214 protected abstract void reset(Context context, SharedPreferences.Editor editor); 215 isSatisfied()216 protected boolean isSatisfied() { 217 return isSdkVersionSatisfied() 218 && Arrays.stream(mRequiredFlags).allMatch(BooleanSupplier::getAsBoolean); 219 } 220 isSdkVersionSatisfied()221 private boolean isSdkVersionSatisfied() { 222 return mMinSdk <= SDK_INT || (mMinSdk == VANILLA_ICE_CREAM && BuildCompat.isAtLeastV()); 223 } 224 withDefaultValue(T defaultValue)225 PrefRule<T> withDefaultValue(T defaultValue) { 226 mDefaultValue = defaultValue; 227 return this; 228 } 229 } 230 231 private static class BoolRule extends PrefRule<Boolean> { BoolRule(@tringRes int key, int minSdk, BooleanSupplier... requiredFlags)232 BoolRule(@StringRes int key, int minSdk, BooleanSupplier... requiredFlags) { 233 super(key, false, minSdk, requiredFlags); 234 } 235 236 @Override reset(Context context, SharedPreferences.Editor editor)237 protected void reset(Context context, SharedPreferences.Editor editor) { 238 editor.putBoolean(context.getString(mKey), mDefaultValue); 239 } 240 } 241 242 private static class InternalBoolRule extends BoolRule { InternalBoolRule(@tringRes int key, int minSdk, BooleanSupplier... requiredFlags)243 InternalBoolRule(@StringRes int key, int minSdk, BooleanSupplier... requiredFlags) { 244 super(key, minSdk, requiredFlags); 245 } 246 247 @Override evaluate(Context context, SharedPreferences prefs, SharedPreferences.Editor editor)248 void evaluate(Context context, SharedPreferences prefs, SharedPreferences.Editor editor) { 249 editor.putBoolean(context.getString(mKey), isSatisfied()); 250 } 251 } 252 253 private static class StringRule extends PrefRule<String> { StringRule(@tringRes int key, int minSdk, BooleanSupplier... requiredFlags)254 StringRule(@StringRes int key, int minSdk, BooleanSupplier... requiredFlags) { 255 super(key, null, minSdk, requiredFlags); 256 } 257 258 @Override reset(Context context, SharedPreferences.Editor editor)259 protected void reset(Context context, SharedPreferences.Editor editor) { 260 editor.putString(context.getString(mKey), mDefaultValue); 261 } 262 } 263 } 264