• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2021 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.android.systemui.flags;
18 
19 import static com.android.systemui.Flags.exampleFlag;
20 import static com.android.systemui.Flags.classicFlagsMultiUser;
21 import static com.android.systemui.Flags.sysuiTeamfood;
22 import static com.android.systemui.flags.FlagManager.ACTION_GET_FLAGS;
23 import static com.android.systemui.flags.FlagManager.ACTION_SET_FLAG;
24 import static com.android.systemui.flags.FlagManager.EXTRA_FLAGS;
25 import static com.android.systemui.flags.FlagManager.EXTRA_NAME;
26 import static com.android.systemui.flags.FlagManager.EXTRA_VALUE;
27 import static com.android.systemui.flags.FlagsCommonModule.ALL_FLAGS;
28 import static com.android.systemui.shared.Flags.exampleSharedFlag;
29 
30 import static java.util.Objects.requireNonNull;
31 
32 import android.content.BroadcastReceiver;
33 import android.content.Context;
34 import android.content.Intent;
35 import android.content.IntentFilter;
36 import android.content.res.Resources;
37 import android.os.Bundle;
38 import android.util.Log;
39 
40 import androidx.annotation.NonNull;
41 import androidx.annotation.Nullable;
42 
43 import com.android.systemui.dagger.SysUISingleton;
44 import com.android.systemui.dagger.qualifiers.Main;
45 import com.android.systemui.settings.UserTracker;
46 import com.android.systemui.util.settings.GlobalSettings;
47 
48 import java.io.PrintWriter;
49 import java.util.ArrayList;
50 import java.util.Map;
51 import java.util.Objects;
52 import java.util.TreeMap;
53 import java.util.concurrent.ConcurrentHashMap;
54 import java.util.concurrent.Executor;
55 import java.util.function.Consumer;
56 
57 import javax.inject.Inject;
58 import javax.inject.Named;
59 
60 /**
61  * Concrete implementation of the a Flag manager that returns default values for debug builds
62  * <p>
63  * Flags can be set (or unset) via the following adb command:
64  * <p>
65  * adb shell cmd statusbar flag <id> <on|off|toggle|erase>
66  * <p>
67  * Alternatively, you can change flags via a broadcast intent:
68  * <p>
69  * adb shell am broadcast -a com.android.systemui.action.SET_FLAG --ei id <id> [--ez value <0|1>]
70  * <p>
71  * To restore a flag back to its default, leave the `--ez value <0|1>` off of the command.
72  */
73 @SysUISingleton
74 public class FeatureFlagsClassicDebug implements FeatureFlagsClassic {
75     static final String TAG = "SysUIFlags";
76 
77     private final FlagManager mFlagManager;
78     private final GlobalSettings mGlobalSettings;
79     private final Resources mResources;
80     private final SystemPropertiesHelper mSystemProperties;
81     private final ServerFlagReader mServerFlagReader;
82     private final Map<String, Flag<?>> mAllFlags;
83     private final Map<String, Boolean> mBooleanFlagCache = new ConcurrentHashMap<>();
84     private final Map<String, String> mStringFlagCache = new ConcurrentHashMap<>();
85     private final Map<String, Integer> mIntFlagCache = new ConcurrentHashMap<>();
86     private final Restarter mRestarter;
87     private final UserTracker mUserTracker;
88     private final Executor mMainExecutor;
89 
90     private final ServerFlagReader.ChangeListener mOnPropertiesChanged =
91             new ServerFlagReader.ChangeListener() {
92                 @Override
93                 public void onChange(Flag<?> flag, String value) {
94                     boolean shouldRestart = false;
95                     if (mBooleanFlagCache.containsKey(flag.getName())) {
96                         boolean newValue = value == null ? false : Boolean.parseBoolean(value);
97                         if (mBooleanFlagCache.get(flag.getName()) != newValue) {
98                             shouldRestart = true;
99                         }
100                     } else if (mStringFlagCache.containsKey(flag.getName())) {
101                         if (!mStringFlagCache.get(flag.getName()).equals(value)) {
102                             shouldRestart = true;
103                         }
104                     } else if (mIntFlagCache.containsKey(flag.getName())) {
105                         int newValue = 0;
106                         try {
107                             newValue = value == null ? 0 : Integer.parseInt(value);
108                         } catch (NumberFormatException e) {
109                         }
110                         if (mIntFlagCache.get(flag.getName()) != newValue) {
111                             shouldRestart = true;
112                         }
113                     }
114                     if (shouldRestart) {
115                         mRestarter.restartSystemUI(
116                                 "Server flag change: " + flag.getNamespace() + "."
117                                         + flag.getName());
118 
119                     }
120                 }
121             };
122 
123     private final UserTracker.Callback mUserChangedCallback =
124             new UserTracker.Callback() {
125                 @Override
126                 public void onUserChanged(int newUser, @NonNull Context userContext) {
127                     mContext.unregisterReceiver(mReceiver);
128                     mContext = userContext;
129                     registerReceiver();
130                 }
131             };
132 
133     private Context mContext;
134 
135     @Inject
FeatureFlagsClassicDebug( FlagManager flagManager, Context context, GlobalSettings globalSettings, SystemPropertiesHelper systemProperties, @Main Resources resources, ServerFlagReader serverFlagReader, @Named(ALL_FLAGS) Map<String, Flag<?>> allFlags, Restarter restarter, UserTracker userTracker, @Main Executor executor)136     public FeatureFlagsClassicDebug(
137             FlagManager flagManager,
138             Context context,
139             GlobalSettings globalSettings,
140             SystemPropertiesHelper systemProperties,
141             @Main Resources resources,
142             ServerFlagReader serverFlagReader,
143             @Named(ALL_FLAGS) Map<String, Flag<?>> allFlags,
144             Restarter restarter,
145             UserTracker userTracker,
146             @Main Executor executor) {
147         mFlagManager = flagManager;
148         if (classicFlagsMultiUser()) {
149             mContext = userTracker.createCurrentUserContext(context);
150         } else {
151             mContext = context;
152         }
153         mGlobalSettings = globalSettings;
154         mResources = resources;
155         mSystemProperties = systemProperties;
156         mServerFlagReader = serverFlagReader;
157         mAllFlags = allFlags;
158         mRestarter = restarter;
159         mUserTracker = userTracker;
160         mMainExecutor = executor;
161     }
162 
163     /** Call after construction to setup listeners. */
init()164     void init() {
165         mFlagManager.setOnSettingsChangedAction(
166                 suppressRestart -> restartSystemUI(suppressRestart, "Settings changed"));
167         mFlagManager.setClearCacheAction(this::removeFromCache);
168         registerReceiver();
169         if (classicFlagsMultiUser()) {
170             mUserTracker.addCallback(mUserChangedCallback, mMainExecutor);
171         }
172         mServerFlagReader.listenForChanges(mAllFlags.values(), mOnPropertiesChanged);
173     }
174 
registerReceiver()175     void registerReceiver() {
176         IntentFilter filter = new IntentFilter();
177         filter.addAction(ACTION_SET_FLAG);
178         filter.addAction(ACTION_GET_FLAGS);
179         mContext.registerReceiver(mReceiver, filter, null, null,
180                 Context.RECEIVER_EXPORTED_UNAUDITED);
181     }
182 
183     @Override
isEnabled(@onNull UnreleasedFlag flag)184     public boolean isEnabled(@NonNull UnreleasedFlag flag) {
185         return isEnabledInternal(flag);
186     }
187 
188     @Override
isEnabled(@onNull ReleasedFlag flag)189     public boolean isEnabled(@NonNull ReleasedFlag flag) {
190         return isEnabledInternal(flag);
191     }
192 
isEnabledInternal(@onNull BooleanFlag flag)193     private boolean isEnabledInternal(@NonNull BooleanFlag flag) {
194         String name = flag.getName();
195 
196         Boolean value = mBooleanFlagCache.get(name);
197         if (value == null) {
198             value = readBooleanFlagInternal(flag, flag.getDefault());
199             mBooleanFlagCache.put(name, value);
200         }
201 
202         return value;
203     }
204 
205     @Override
isEnabled(@onNull ResourceBooleanFlag flag)206     public boolean isEnabled(@NonNull ResourceBooleanFlag flag) {
207         String name = flag.getName();
208         Boolean value = mBooleanFlagCache.get(name);
209         if (value == null) {
210             value = readBooleanFlagInternal(flag, mResources.getBoolean(flag.getResourceId()));
211             mBooleanFlagCache.put(name, value);
212         }
213 
214         return value;
215     }
216 
217     @Override
isEnabled(@onNull SysPropBooleanFlag flag)218     public boolean isEnabled(@NonNull SysPropBooleanFlag flag) {
219         String name = flag.getName();
220         Boolean value = mBooleanFlagCache.get(name);
221         if (value == null) {
222             value = readBooleanFlagInternal(flag,
223                     mSystemProperties.getBoolean(
224                             flag.getName(),
225                             readBooleanFlagInternal(flag, flag.getDefault())));
226             mBooleanFlagCache.put(name, value);
227         }
228         return value;
229     }
230 
231     @NonNull
232     @Override
getString(@onNull StringFlag flag)233     public String getString(@NonNull StringFlag flag) {
234         String name = flag.getName();
235         String value = mStringFlagCache.get(name);
236         if (value == null) {
237             value = readFlagValueInternal(name, flag.getDefault(), StringFlagSerializer.INSTANCE);
238             mStringFlagCache.put(name, value);
239         }
240 
241         return value;
242     }
243 
244     @NonNull
245     @Override
getString(@onNull ResourceStringFlag flag)246     public String getString(@NonNull ResourceStringFlag flag) {
247         String name = flag.getName();
248         String value = mStringFlagCache.get(name);
249         if (value == null) {
250             value = readFlagValueInternal(
251                     name,
252                     mResources.getString(flag.getResourceId()),
253                     StringFlagSerializer.INSTANCE);
254             mStringFlagCache.put(name, value);
255         }
256         return value;
257     }
258 
259     @Override
getInt(@onNull IntFlag flag)260     public int getInt(@NonNull IntFlag flag) {
261         String name = flag.getName();
262         Integer value = mIntFlagCache.get(name);
263         if (value == null) {
264             value = readFlagValueInternal(name, flag.getDefault(), IntFlagSerializer.INSTANCE);
265             mIntFlagCache.put(name, value);
266         }
267 
268         return value;
269     }
270 
271     @Override
getInt(@onNull ResourceIntFlag flag)272     public int getInt(@NonNull ResourceIntFlag flag) {
273         String name = flag.getName();
274         Integer value = mIntFlagCache.get(name);
275         if (value == null) {
276             value = readFlagValueInternal(
277                     name, mResources.getInteger(flag.getResourceId()), IntFlagSerializer.INSTANCE);
278             mIntFlagCache.put(name, value);
279         }
280         return value;
281     }
282 
283     /** Specific override for Boolean flags that checks against the teamfood list.*/
readBooleanFlagInternal(Flag<Boolean> flag, boolean defaultValue)284     private boolean readBooleanFlagInternal(Flag<Boolean> flag, boolean defaultValue) {
285         Boolean result = readBooleanFlagOverride(flag.getName());
286         boolean hasServerOverride = mServerFlagReader.hasOverride(
287                 flag.getNamespace(), flag.getName());
288 
289         // Only check for teamfood if the default is false
290         // and there is no server override.
291         if (!hasServerOverride
292                 && !defaultValue
293                 && result == null
294                 && flag.getTeamfood()) {
295             return sysuiTeamfood();
296         }
297 
298         return result == null ? mServerFlagReader.readServerOverride(
299                 flag.getNamespace(), flag.getName(), defaultValue) : result;
300     }
301 
302 
readBooleanFlagOverride(String name)303     private Boolean readBooleanFlagOverride(String name) {
304         return readFlagValueInternal(name, BooleanFlagSerializer.INSTANCE);
305     }
306 
307     @NonNull
readFlagValueInternal( String name, @NonNull T defaultValue, FlagSerializer<T> serializer)308     private <T> T readFlagValueInternal(
309             String name, @NonNull T defaultValue, FlagSerializer<T> serializer) {
310         requireNonNull(defaultValue, "defaultValue");
311         T resultForName = readFlagValueInternal(name, serializer);
312         if (resultForName == null) {
313             return defaultValue;
314         }
315         return resultForName;
316     }
317 
318     /** Returns the stored value or null if not set. */
319     @Nullable
readFlagValueInternal(String name, FlagSerializer<T> serializer)320     private <T> T readFlagValueInternal(String name, FlagSerializer<T> serializer) {
321         try {
322             return mFlagManager.readFlagValue(name, serializer);
323         } catch (Exception e) {
324             eraseInternal(name);
325         }
326         return null;
327     }
328 
setFlagValue(String name, @NonNull T value, FlagSerializer<T> serializer)329     private <T> void setFlagValue(String name, @NonNull T value, FlagSerializer<T> serializer) {
330         requireNonNull(value, "Cannot set a null value");
331         T currentValue = readFlagValueInternal(name, serializer);
332         if (Objects.equals(currentValue, value)) {
333             Log.i(TAG, "Flag \"" + name + "\" is already " + value);
334             return;
335         }
336         setFlagValueInternal(name, value, serializer);
337         Log.i(TAG, "Set flag \"" + name + "\" to " + value);
338         removeFromCache(name);
339         mFlagManager.dispatchListenersAndMaybeRestart(
340                 name,
341                 suppressRestart -> restartSystemUI(
342                         suppressRestart, "Flag \"" + name + "\" changed to " + value));
343     }
344 
setFlagValueInternal( String name, @NonNull T value, FlagSerializer<T> serializer)345     private <T> void setFlagValueInternal(
346             String name, @NonNull T value, FlagSerializer<T> serializer) {
347         final String data = serializer.toSettingsData(value);
348         if (data == null) {
349             Log.w(TAG, "Failed to set flag " + name + " to " + value);
350             return;
351         }
352         mGlobalSettings.putString(mFlagManager.nameToSettingsKey(name), data);
353     }
354 
eraseFlag(Flag<T> flag)355     <T> void eraseFlag(Flag<T> flag) {
356         if (flag instanceof SysPropFlag) {
357             mSystemProperties.erase(flag.getName());
358             dispatchListenersAndMaybeRestart(
359                     flag.getName(),
360                     suppressRestart -> restartSystemUI(
361                             suppressRestart,
362                             "SysProp Flag \"" + flag.getNamespace() + "."
363                                     + flag.getName() + "\" reset to default."));
364         } else {
365             eraseFlag(flag.getName());
366         }
367     }
368 
369     /** Erase a flag's overridden value if there is one. */
eraseFlag(String name)370     private void eraseFlag(String name) {
371         eraseInternal(name);
372         removeFromCache(name);
373         dispatchListenersAndMaybeRestart(
374                 name,
375                 suppressRestart -> restartSystemUI(
376                         suppressRestart, "Flag \"" + name + "\" reset to default"));
377     }
378 
dispatchListenersAndMaybeRestart(String name, Consumer<Boolean> restartAction)379     private void dispatchListenersAndMaybeRestart(String name, Consumer<Boolean> restartAction) {
380         mFlagManager.dispatchListenersAndMaybeRestart(name, restartAction);
381     }
382 
383     /** Works just like {@link #eraseFlag(String)} except that it doesn't restart SystemUI. */
eraseInternal(String name)384     private void eraseInternal(String name) {
385         // We can't actually "erase" things from settings, but we can set them to empty!
386         mGlobalSettings.putString(mFlagManager.nameToSettingsKey(name), "");
387         Log.i(TAG, "Erase name " + name);
388     }
389 
390     @Override
addListener(@onNull Flag<?> flag, @NonNull Listener listener)391     public void addListener(@NonNull Flag<?> flag, @NonNull Listener listener) {
392         mFlagManager.addListener(flag, listener);
393     }
394 
395     @Override
removeListener(@onNull Listener listener)396     public void removeListener(@NonNull Listener listener) {
397         mFlagManager.removeListener(listener);
398     }
399 
restartSystemUI(boolean requestSuppress, String reason)400     private void restartSystemUI(boolean requestSuppress, String reason) {
401         if (requestSuppress) {
402             Log.i(TAG, "SystemUI Restart Suppressed");
403             return;
404         }
405         mRestarter.restartSystemUI(reason);
406     }
407 
restartAndroid(boolean requestSuppress, String reason)408     private void restartAndroid(boolean requestSuppress, String reason) {
409         if (requestSuppress) {
410             Log.i(TAG, "Android Restart Suppressed");
411             return;
412         }
413         mRestarter.restartAndroid(reason);
414     }
415 
setBooleanFlagInternal(Flag<?> flag, boolean value)416     void setBooleanFlagInternal(Flag<?> flag, boolean value) {
417         if (flag instanceof BooleanFlag) {
418             setFlagValue(flag.getName(), value, BooleanFlagSerializer.INSTANCE);
419         } else if (flag instanceof ResourceBooleanFlag) {
420             setFlagValue(flag.getName(), value, BooleanFlagSerializer.INSTANCE);
421         } else if (flag instanceof SysPropBooleanFlag) {
422             // Store SysProp flags in SystemProperties where they can read by outside parties.
423             mSystemProperties.setBoolean(flag.getName(), value);
424             dispatchListenersAndMaybeRestart(
425                     flag.getName(),
426                     suppressRestart -> restartSystemUI(
427                             suppressRestart,
428                             "Flag \"" + flag.getName() + "\" changed to " + value));
429         } else {
430             throw new IllegalArgumentException("Unknown flag type");
431         }
432     }
433 
setStringFlagInternal(Flag<?> flag, String value)434     void setStringFlagInternal(Flag<?> flag, String value) {
435         if (flag instanceof StringFlag) {
436             setFlagValue(flag.getName(), value, StringFlagSerializer.INSTANCE);
437         } else if (flag instanceof ResourceStringFlag) {
438             setFlagValue(flag.getName(), value, StringFlagSerializer.INSTANCE);
439         } else {
440             throw new IllegalArgumentException("Unknown flag type");
441         }
442     }
443 
setIntFlagInternal(Flag<?> flag, int value)444     void setIntFlagInternal(Flag<?> flag, int value) {
445         if (flag instanceof IntFlag) {
446             setFlagValue(flag.getName(), value, IntFlagSerializer.INSTANCE);
447         } else if (flag instanceof ResourceIntFlag) {
448             setFlagValue(flag.getName(), value, IntFlagSerializer.INSTANCE);
449         } else {
450             throw new IllegalArgumentException("Unknown flag type");
451         }
452     }
453 
454     private final BroadcastReceiver mReceiver = new BroadcastReceiver() {
455         @Override
456         public void onReceive(Context context, Intent intent) {
457             String action = intent == null ? null : intent.getAction();
458             if (action == null) {
459                 return;
460             }
461             if (ACTION_SET_FLAG.equals(action)) {
462                 handleSetFlag(intent.getExtras());
463             } else if (ACTION_GET_FLAGS.equals(action)) {
464                 ArrayList<Flag<?>> flags = new ArrayList<>(mAllFlags.values());
465 
466                 // Convert all flags to parcelable flags.
467                 ArrayList<ParcelableFlag<?>> pFlags = new ArrayList<>();
468                 for (Flag<?> f : flags) {
469                     ParcelableFlag<?> pf = toParcelableFlag(f);
470                     if (pf != null) {
471                         pFlags.add(pf);
472                     }
473                 }
474 
475                 Bundle extras = getResultExtras(true);
476                 if (extras != null) {
477                     extras.putParcelableArrayList(EXTRA_FLAGS, pFlags);
478                 }
479             }
480         }
481 
482         private void handleSetFlag(Bundle extras) {
483             if (extras == null) {
484                 Log.w(TAG, "No extras");
485                 return;
486             }
487             String name = extras.getString(EXTRA_NAME);
488             if (name == null || name.isEmpty()) {
489                 Log.w(TAG, "NAME not set or is empty: " + name);
490                 return;
491             }
492 
493             if (!mAllFlags.containsKey(name)) {
494                 Log.w(TAG, "Tried to set unknown name: " + name);
495                 return;
496             }
497             Flag<?> flag = mAllFlags.get(name);
498 
499             if (!extras.containsKey(EXTRA_VALUE)) {
500                 eraseFlag(flag);
501                 return;
502             }
503 
504             Object value = extras.get(EXTRA_VALUE);
505 
506             try {
507                 if (value instanceof Boolean) {
508                     setBooleanFlagInternal(flag, (Boolean) value);
509                 } else if (value instanceof String) {
510                     setStringFlagInternal(flag, (String) value);
511                 } else {
512                     throw new IllegalArgumentException("Unknown value type");
513                 }
514             } catch (IllegalArgumentException e) {
515                 Log.w(TAG,
516                         "Unable to set " + flag.getName() + " of type " + flag.getClass()
517                                 + " to value of type " + (value == null ? null : value.getClass()));
518             }
519         }
520 
521         /**
522          * Ensures that the data we send to the app reflects the current state of the flags.
523          *
524          * Also converts an non-parcelable versions of the flags to their parcelable versions.
525          */
526         @Nullable
527         private ParcelableFlag<?> toParcelableFlag(Flag<?> f) {
528             boolean enabled;
529             boolean teamfood = f.getTeamfood();
530             boolean overridden;
531 
532             if (f instanceof ReleasedFlag) {
533                 enabled = isEnabled((ReleasedFlag) f);
534                 overridden = readBooleanFlagOverride(f.getName()) != null;
535             } else if (f instanceof UnreleasedFlag) {
536                 enabled = isEnabled((UnreleasedFlag) f);
537                 overridden = readBooleanFlagOverride(f.getName()) != null;
538             } else if (f instanceof ResourceBooleanFlag) {
539                 enabled = isEnabled((ResourceBooleanFlag) f);
540                 overridden = readBooleanFlagOverride(f.getName()) != null;
541             } else if (f instanceof SysPropBooleanFlag) {
542                 enabled = isEnabled((SysPropBooleanFlag) f);
543                 overridden = !mSystemProperties.get(f.getName()).isEmpty();
544             } else {
545                 // TODO: add support for other flag types.
546                 Log.w(TAG, "Unsupported Flag Type. Please file a bug.");
547                 return null;
548             }
549 
550             if (enabled) {
551                 return new ReleasedFlag(f.getName(), f.getNamespace(), overridden);
552             } else {
553                 return new UnreleasedFlag(f.getName(), f.getNamespace(), teamfood, overridden);
554             }
555         }
556     };
557 
removeFromCache(String name)558     private void removeFromCache(String name) {
559         mBooleanFlagCache.remove(name);
560         mStringFlagCache.remove(name);
561     }
562 
563     @Override
dump(@onNull PrintWriter pw, @NonNull String[] args)564     public void dump(@NonNull PrintWriter pw, @NonNull String[] args) {
565         pw.println("can override: true");
566         pw.println("teamfood: " + sysuiTeamfood());
567         pw.println("booleans: " + mBooleanFlagCache.size());
568         pw.println("example_flag: " + exampleFlag());
569         pw.println("example_shared_flag: " + exampleSharedFlag());
570         // Sort our flags for dumping
571         TreeMap<String, Boolean> dumpBooleanMap = new TreeMap<>(mBooleanFlagCache);
572         dumpBooleanMap.forEach((key, value) -> pw.println("  sysui_flag_" + key + ": " + value));
573 
574         pw.println("Strings: " + mStringFlagCache.size());
575         // Sort our flags for dumping
576         TreeMap<String, String> dumpStringMap = new TreeMap<>(mStringFlagCache);
577         dumpStringMap.forEach((key, value) -> pw.println("  sysui_flag_" + key
578                 + ": [length=" + value.length() + "] \"" + value + "\""));
579 
580         pw.println("Integers: " + mIntFlagCache.size());
581         // Sort our flags for dumping
582         TreeMap<String, Integer> dumpIntMap = new TreeMap<>(mIntFlagCache);
583         dumpIntMap.forEach((key, value) -> pw.println("  sysui_flag_" + key + ": " + value));
584     }
585 
586 }
587