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