1 /* 2 * Copyright (C) 2018 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.launcher3.uioverrides.flags; 18 19 import static com.android.launcher3.config.FeatureFlags.FlagState.TEAMFOOD; 20 import static com.android.launcher3.uioverrides.flags.FlagsFactory.TEAMFOOD_FLAG; 21 22 import android.content.Context; 23 import android.os.Handler; 24 import android.os.Process; 25 import android.text.Html; 26 import android.text.TextUtils; 27 import android.util.Log; 28 import android.view.Menu; 29 import android.view.MenuItem; 30 import android.widget.Toast; 31 32 import androidx.preference.PreferenceDataStore; 33 import androidx.preference.PreferenceFragmentCompat; 34 import androidx.preference.PreferenceGroup; 35 import androidx.preference.PreferenceViewHolder; 36 import androidx.preference.SwitchPreference; 37 38 import com.android.launcher3.R; 39 import com.android.launcher3.config.FeatureFlags; 40 41 import java.util.List; 42 import java.util.Set; 43 44 /** 45 * Dev-build only UI allowing developers to toggle flag settings. See {@link FeatureFlags}. 46 */ 47 public final class FlagTogglerPrefUi { 48 49 private static final String TAG = "FlagTogglerPrefFrag"; 50 51 private final PreferenceFragmentCompat mFragment; 52 private final Context mContext; 53 private final PreferenceDataStore mDataStore = new PreferenceDataStore() { 54 55 @Override 56 public void putBoolean(String key, boolean value) { 57 FlagsFactory.getSharedPreferences().edit().putBoolean(key, value).apply(); 58 updateMenu(); 59 } 60 61 @Override 62 public boolean getBoolean(String key, boolean defaultValue) { 63 return FlagsFactory.getSharedPreferences().getBoolean(key, defaultValue); 64 } 65 }; 66 FlagTogglerPrefUi(PreferenceFragmentCompat fragment)67 public FlagTogglerPrefUi(PreferenceFragmentCompat fragment) { 68 mFragment = fragment; 69 mContext = fragment.getActivity(); 70 } 71 applyTo(PreferenceGroup parent)72 public void applyTo(PreferenceGroup parent) { 73 Set<String> modifiedPrefs = FlagsFactory.getSharedPreferences().getAll().keySet(); 74 List<DebugFlag> flags = FlagsFactory.getDebugFlags(); 75 flags.sort((f1, f2) -> { 76 // Sort first by any prefs that the user has changed, then alphabetically. 77 int changeComparison = Boolean.compare( 78 modifiedPrefs.contains(f2.key), modifiedPrefs.contains(f1.key)); 79 return changeComparison != 0 80 ? changeComparison 81 : f1.key.compareToIgnoreCase(f2.key); 82 }); 83 84 // Ensure that teamfood flag comes on the top 85 if (flags.remove(TEAMFOOD_FLAG)) { 86 flags.add(0, (DebugFlag) TEAMFOOD_FLAG); 87 } 88 89 // For flag overrides we only want to store when the engineer chose to override the 90 // flag with a different value than the default. That way, when we flip flags in 91 // future, engineers will pick up the new value immediately. To accomplish this, we use a 92 // custom preference data store. 93 for (DebugFlag flag : flags) { 94 SwitchPreference switchPreference = new SwitchPreference(mContext) { 95 @Override 96 public void onBindViewHolder(PreferenceViewHolder holder) { 97 super.onBindViewHolder(holder); 98 holder.itemView.setOnLongClickListener(v -> { 99 FlagsFactory.getSharedPreferences().edit().remove(flag.key).apply(); 100 setChecked(getFlagStateFromSharedPrefs(flag)); 101 updateSummary(this, flag); 102 updateMenu(); 103 return true; 104 }); 105 } 106 }; 107 switchPreference.setKey(flag.key); 108 switchPreference.setDefaultValue(FlagsFactory.getEnabledValue(flag.defaultValue)); 109 switchPreference.setChecked(getFlagStateFromSharedPrefs(flag)); 110 switchPreference.setTitle(flag.key); 111 updateSummary(switchPreference, flag); 112 switchPreference.setPreferenceDataStore(mDataStore); 113 switchPreference.setOnPreferenceChangeListener((p, v) -> { 114 new Handler().post(() -> updateSummary(switchPreference, flag)); 115 return true; 116 }); 117 118 119 parent.addPreference(switchPreference); 120 } 121 updateMenu(); 122 } 123 124 /** 125 * Updates the summary to show the description and whether the flag overrides the default value. 126 */ updateSummary(SwitchPreference switchPreference, DebugFlag flag)127 private void updateSummary(SwitchPreference switchPreference, DebugFlag flag) { 128 String summary = flag.defaultValue == TEAMFOOD 129 ? "<font color='blue'><b>[TEAMFOOD]</b> </font>" : ""; 130 if (FlagsFactory.getSharedPreferences().contains(flag.key)) { 131 summary += "<font color='red'><b>[OVERRIDDEN]</b> </font>"; 132 } 133 if (!TextUtils.isEmpty(summary)) { 134 summary += "<br>"; 135 } 136 switchPreference.setSummary(Html.fromHtml(summary + flag.description)); 137 } 138 updateMenu()139 private void updateMenu() { 140 mFragment.setHasOptionsMenu(anyChanged()); 141 mFragment.getActivity().invalidateOptionsMenu(); 142 } 143 onCreateOptionsMenu(Menu menu)144 public void onCreateOptionsMenu(Menu menu) { 145 if (anyChanged()) { 146 menu.add(0, R.id.menu_apply_flags, 0, "Apply") 147 .setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS); 148 } 149 } 150 onOptionsItemSelected(MenuItem item)151 public void onOptionsItemSelected(MenuItem item) { 152 if (item.getItemId() == R.id.menu_apply_flags) { 153 FlagsFactory.getSharedPreferences().edit().commit(); 154 Log.e(TAG, 155 "Killing launcher process " + Process.myPid() + " to apply new flag values"); 156 System.exit(0); 157 } 158 } 159 onStop()160 public void onStop() { 161 if (anyChanged()) { 162 Toast.makeText(mContext, "Flag won't be applied until you restart launcher", 163 Toast.LENGTH_LONG).show(); 164 } 165 } 166 getFlagStateFromSharedPrefs(DebugFlag flag)167 private boolean getFlagStateFromSharedPrefs(DebugFlag flag) { 168 boolean defaultValue = FlagsFactory.getEnabledValue(flag.defaultValue); 169 return mDataStore.getBoolean(flag.key, defaultValue); 170 } 171 anyChanged()172 private boolean anyChanged() { 173 for (DebugFlag flag : FlagsFactory.getDebugFlags()) { 174 if (getFlagStateFromSharedPrefs(flag) != flag.get()) { 175 return true; 176 } 177 } 178 return false; 179 } 180 } 181