• 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.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