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.config;
18 
19 import static com.android.launcher3.config.FeatureFlags.FLAGS_PREF_NAME;
20 
21 import android.content.Context;
22 import android.content.SharedPreferences;
23 import android.os.Process;
24 import android.text.Html;
25 import android.util.Log;
26 import android.view.Menu;
27 import android.view.MenuItem;
28 import android.widget.Toast;
29 
30 import androidx.preference.PreferenceDataStore;
31 import androidx.preference.PreferenceFragmentCompat;
32 import androidx.preference.PreferenceGroup;
33 import androidx.preference.SwitchPreference;
34 
35 import com.android.launcher3.R;
36 import com.android.launcher3.config.FeatureFlags.DebugFlag;
37 
38 /**
39  * Dev-build only UI allowing developers to toggle flag settings. See {@link FeatureFlags}.
40  */
41 public final class FlagTogglerPrefUi {
42 
43     private static final String TAG = "FlagTogglerPrefFrag";
44 
45     private final PreferenceFragmentCompat mFragment;
46     private final Context mContext;
47     private final SharedPreferences mSharedPreferences;
48 
49     private final PreferenceDataStore mDataStore = new PreferenceDataStore() {
50 
51         @Override
52         public void putBoolean(String key, boolean value) {
53             for (DebugFlag flag : FeatureFlags.getDebugFlags()) {
54                 if (flag.key.equals(key)) {
55                     SharedPreferences.Editor editor = mContext.getSharedPreferences(
56                             FLAGS_PREF_NAME, Context.MODE_PRIVATE).edit();
57                     if (value == flag.defaultValue) {
58                         editor.remove(key).apply();
59                     } else {
60                         editor.putBoolean(key, value).apply();
61                     }
62                     updateMenu();
63                 }
64             }
65         }
66 
67         @Override
68         public boolean getBoolean(String key, boolean defaultValue) {
69             for (DebugFlag flag : FeatureFlags.getDebugFlags()) {
70                 if (flag.key.equals(key)) {
71                     return mContext.getSharedPreferences(FLAGS_PREF_NAME, Context.MODE_PRIVATE)
72                             .getBoolean(key, flag.defaultValue);
73                 }
74             }
75             return defaultValue;
76         }
77     };
78 
FlagTogglerPrefUi(PreferenceFragmentCompat fragment)79     public FlagTogglerPrefUi(PreferenceFragmentCompat fragment) {
80         mFragment = fragment;
81         mContext = fragment.getActivity();
82         mSharedPreferences = mContext.getSharedPreferences(
83                 FLAGS_PREF_NAME, Context.MODE_PRIVATE);
84     }
85 
applyTo(PreferenceGroup parent)86     public void applyTo(PreferenceGroup parent) {
87         // For flag overrides we only want to store when the engineer chose to override the
88         // flag with a different value than the default. That way, when we flip flags in
89         // future, engineers will pick up the new value immediately. To accomplish this, we use a
90         // custom preference data store.
91         for (DebugFlag flag : FeatureFlags.getDebugFlags()) {
92             SwitchPreference switchPreference = new SwitchPreference(mContext);
93             switchPreference.setKey(flag.key);
94             switchPreference.setDefaultValue(flag.defaultValue);
95             switchPreference.setChecked(getFlagStateFromSharedPrefs(flag));
96             switchPreference.setTitle(flag.key);
97             updateSummary(switchPreference, flag);
98             switchPreference.setPreferenceDataStore(mDataStore);
99             parent.addPreference(switchPreference);
100         }
101         updateMenu();
102     }
103 
104     /**
105      * Updates the summary to show the description and whether the flag overrides the default value.
106      */
updateSummary(SwitchPreference switchPreference, DebugFlag flag)107     private void updateSummary(SwitchPreference switchPreference, DebugFlag flag) {
108         String onWarning = flag.defaultValue ? "" : "<b>OVERRIDDEN</b><br>";
109         String offWarning = flag.defaultValue ? "<b>OVERRIDDEN</b><br>" : "";
110         switchPreference.setSummaryOn(Html.fromHtml(onWarning + flag.description));
111         switchPreference.setSummaryOff(Html.fromHtml(offWarning + flag.description));
112     }
113 
updateMenu()114     private void updateMenu() {
115         mFragment.setHasOptionsMenu(anyChanged());
116         mFragment.getActivity().invalidateOptionsMenu();
117     }
118 
onCreateOptionsMenu(Menu menu)119     public void onCreateOptionsMenu(Menu menu) {
120         if (anyChanged()) {
121             menu.add(0, R.id.menu_apply_flags, 0, "Apply")
122                     .setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS);
123         }
124     }
125 
onOptionsItemSelected(MenuItem item)126     public void onOptionsItemSelected(MenuItem item) {
127         if (item.getItemId() == R.id.menu_apply_flags) {
128             mSharedPreferences.edit().commit();
129             Log.e(TAG,
130                     "Killing launcher process " + Process.myPid() + " to apply new flag values");
131             System.exit(0);
132         }
133     }
134 
onStop()135     public void onStop() {
136         if (anyChanged()) {
137             Toast.makeText(mContext, "Flag won't be applied until you restart launcher",
138                     Toast.LENGTH_LONG).show();
139         }
140     }
141 
getFlagStateFromSharedPrefs(DebugFlag flag)142     private boolean getFlagStateFromSharedPrefs(DebugFlag flag) {
143         return mDataStore.getBoolean(flag.key, flag.defaultValue);
144     }
145 
anyChanged()146     private boolean anyChanged() {
147         for (DebugFlag flag : FeatureFlags.getDebugFlags()) {
148             if (getFlagStateFromSharedPrefs(flag) != flag.get()) {
149                 return true;
150             }
151         }
152         return false;
153     }
154 }