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.FIELD_FLAGS;
22 import static com.android.systemui.flags.FlagManager.FIELD_ID;
23 import static com.android.systemui.flags.FlagManager.FIELD_VALUE;
24 
25 import android.content.BroadcastReceiver;
26 import android.content.Context;
27 import android.content.Intent;
28 import android.content.IntentFilter;
29 import android.content.res.Resources;
30 import android.os.Bundle;
31 import android.util.Log;
32 
33 import androidx.annotation.BoolRes;
34 import androidx.annotation.NonNull;
35 
36 import com.android.systemui.Dumpable;
37 import com.android.systemui.dagger.SysUISingleton;
38 import com.android.systemui.dagger.qualifiers.Main;
39 import com.android.systemui.dump.DumpManager;
40 import com.android.systemui.util.settings.SecureSettings;
41 
42 import org.json.JSONException;
43 import org.json.JSONObject;
44 
45 import java.io.FileDescriptor;
46 import java.io.PrintWriter;
47 import java.util.ArrayList;
48 import java.util.HashMap;
49 import java.util.Map;
50 
51 import javax.inject.Inject;
52 
53 /**
54  * Concrete implementation of the a Flag manager that returns default values for debug builds
55  *
56  * Flags can be set (or unset) via the following adb command:
57  *
58  *   adb shell am broadcast -a com.android.systemui.action.SET_FLAG --ei id <id> [--ez value <0|1>]
59  *
60  * To restore a flag back to its default, leave the `--ez value <0|1>` off of the command.
61  */
62 @SysUISingleton
63 public class FeatureFlagManager implements FlagReader, FlagWriter, Dumpable {
64     private static final String TAG = "SysUIFlags";
65 
66     private final FlagManager mFlagManager;
67     private final SecureSettings mSecureSettings;
68     private final Resources mResources;
69     private final Map<Integer, Boolean> mBooleanFlagCache = new HashMap<>();
70 
71     @Inject
FeatureFlagManager( FlagManager flagManager, Context context, SecureSettings secureSettings, @Main Resources resources, DumpManager dumpManager)72     public FeatureFlagManager(
73             FlagManager flagManager,
74             Context context,
75             SecureSettings secureSettings,
76             @Main Resources resources,
77             DumpManager dumpManager) {
78         mFlagManager = flagManager;
79         mSecureSettings = secureSettings;
80         mResources = resources;
81         IntentFilter filter = new IntentFilter();
82         filter.addAction(ACTION_SET_FLAG);
83         filter.addAction(ACTION_GET_FLAGS);
84         context.registerReceiver(mReceiver, filter, null, null);
85         dumpManager.registerDumpable(TAG, this);
86     }
87 
88     @Override
isEnabled(BooleanFlag flag)89     public boolean isEnabled(BooleanFlag flag) {
90         int id = flag.getId();
91         if (!mBooleanFlagCache.containsKey(id)) {
92             boolean def = flag.getDefault();
93             if (flag.hasResourceOverride()) {
94                 try {
95                     def = isEnabledInOverlay(flag.getResourceOverride());
96                 } catch (Resources.NotFoundException e) {
97                     // no-op
98                 }
99             }
100 
101             mBooleanFlagCache.put(id, isEnabled(id, def));
102         }
103 
104         return mBooleanFlagCache.get(id);
105     }
106 
107     /** Return a {@link BooleanFlag}'s value. */
108     @Override
isEnabled(int id, boolean defaultValue)109     public boolean isEnabled(int id, boolean defaultValue) {
110         Boolean result = isEnabledInternal(id);
111         return result == null ? defaultValue : result;
112     }
113 
114     /** Returns the stored value or null if not set. */
isEnabledInternal(int id)115     private Boolean isEnabledInternal(int id) {
116         try {
117             return mFlagManager.isEnabled(id);
118         } catch (Exception e) {
119             eraseInternal(id);
120         }
121         return null;
122     }
123 
isEnabledInOverlay(@oolRes int resId)124     private boolean isEnabledInOverlay(@BoolRes int resId) {
125         return mResources.getBoolean(resId);
126     }
127 
128     /** Set whether a given {@link BooleanFlag} is enabled or not. */
129     @Override
setEnabled(int id, boolean value)130     public void setEnabled(int id, boolean value) {
131         Boolean currentValue = isEnabledInternal(id);
132         if (currentValue != null && currentValue == value) {
133             return;
134         }
135 
136         JSONObject json = new JSONObject();
137         try {
138             json.put(FlagManager.FIELD_TYPE, FlagManager.TYPE_BOOLEAN);
139             json.put(FIELD_VALUE, value);
140             mSecureSettings.putString(mFlagManager.keyToSettingsPrefix(id), json.toString());
141             Log.i(TAG, "Set id " + id + " to " + value);
142             restartSystemUI();
143         } catch (JSONException e) {
144             // no-op
145         }
146     }
147 
148     /** Erase a flag's overridden value if there is one. */
eraseFlag(int id)149     public void eraseFlag(int id) {
150         eraseInternal(id);
151         restartSystemUI();
152     }
153 
154     /** Works just like {@link #eraseFlag(int)} except that it doesn't restart SystemUI. */
eraseInternal(int id)155     private void eraseInternal(int id) {
156         // We can't actually "erase" things from sysprops, but we can set them to empty!
157         mSecureSettings.putString(mFlagManager.keyToSettingsPrefix(id), "");
158         Log.i(TAG, "Erase id " + id);
159     }
160 
161     @Override
addListener(Listener run)162     public void addListener(Listener run) {
163         mFlagManager.addListener(run);
164     }
165 
166     @Override
removeListener(Listener run)167     public void removeListener(Listener run) {
168         mFlagManager.removeListener(run);
169     }
170 
restartSystemUI()171     private void restartSystemUI() {
172         Log.i(TAG, "Restarting SystemUI");
173         // SysUI starts back when up exited. Is there a better way to do this?
174         System.exit(0);
175     }
176 
177     private final BroadcastReceiver mReceiver = new BroadcastReceiver() {
178         @Override
179         public void onReceive(Context context, Intent intent) {
180             String action = intent.getAction();
181             if (action == null) {
182                 return;
183             }
184             if (ACTION_SET_FLAG.equals(action)) {
185                 handleSetFlag(intent.getExtras());
186             } else if (ACTION_GET_FLAGS.equals(action)) {
187                 Map<Integer, Flag<?>> knownFlagMap = Flags.collectFlags();
188                 ArrayList<Flag<?>> flags = new ArrayList<>(knownFlagMap.values());
189                 Bundle extras =  getResultExtras(true);
190                 if (extras != null) {
191                     extras.putParcelableArrayList(FIELD_FLAGS, flags);
192                 }
193             }
194         }
195 
196         private void handleSetFlag(Bundle extras) {
197             int id = extras.getInt(FIELD_ID);
198             if (id <= 0) {
199                 Log.w(TAG, "ID not set or less than  or equal to 0: " + id);
200                 return;
201             }
202 
203             Map<Integer, Flag<?>> flagMap = Flags.collectFlags();
204             if (!flagMap.containsKey(id)) {
205                 Log.w(TAG, "Tried to set unknown id: " + id);
206                 return;
207             }
208             Flag<?> flag = flagMap.get(id);
209 
210             if (!extras.containsKey(FIELD_VALUE)) {
211                 eraseFlag(id);
212                 return;
213             }
214 
215             if (flag instanceof BooleanFlag) {
216                 setEnabled(id, extras.getBoolean(FIELD_VALUE));
217             }
218         }
219     };
220 
221     @Override
dump(@onNull FileDescriptor fd, @NonNull PrintWriter pw, @NonNull String[] args)222     public void dump(@NonNull FileDescriptor fd, @NonNull PrintWriter pw, @NonNull String[] args) {
223         pw.println("can override: true");
224         ArrayList<String> flagStrings = new ArrayList<>(mBooleanFlagCache.size());
225         for (Map.Entry<Integer, Boolean> entry : mBooleanFlagCache.entrySet()) {
226             flagStrings.add("  sysui_flag_" + entry.getKey() + ": " + entry.getValue());
227         }
228         flagStrings.sort(String.CASE_INSENSITIVE_ORDER);
229         for (String flagString : flagStrings) {
230             pw.println(flagString);
231         }
232     }
233 }
234