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