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