• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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