1 /*
2  * Copyright (C) 2015 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.settings;
18 
19 import static androidx.core.view.accessibility.AccessibilityNodeInfoCompat.ACTION_ACCESSIBILITY_FOCUS;
20 
21 import static com.android.launcher3.states.RotationHelper.ALLOW_ROTATION_PREFERENCE_KEY;
22 
23 import android.content.Intent;
24 import android.content.SharedPreferences;
25 import android.os.Bundle;
26 import android.text.TextUtils;
27 import android.view.MenuItem;
28 import android.view.View;
29 
30 import androidx.annotation.NonNull;
31 import androidx.annotation.VisibleForTesting;
32 import androidx.core.view.WindowCompat;
33 import androidx.fragment.app.DialogFragment;
34 import androidx.fragment.app.Fragment;
35 import androidx.fragment.app.FragmentActivity;
36 import androidx.fragment.app.FragmentManager;
37 import androidx.preference.Preference;
38 import androidx.preference.PreferenceFragmentCompat;
39 import androidx.preference.PreferenceFragmentCompat.OnPreferenceStartFragmentCallback;
40 import androidx.preference.PreferenceFragmentCompat.OnPreferenceStartScreenCallback;
41 import androidx.preference.PreferenceGroup.PreferencePositionCallback;
42 import androidx.preference.PreferenceScreen;
43 import androidx.recyclerview.widget.RecyclerView;
44 
45 import com.android.launcher3.DeviceProfile;
46 import com.android.launcher3.InvariantDeviceProfile;
47 import com.android.launcher3.LauncherFiles;
48 import com.android.launcher3.R;
49 import com.android.launcher3.Utilities;
50 import com.android.launcher3.config.FeatureFlags;
51 import com.android.launcher3.model.WidgetsModel;
52 import com.android.launcher3.states.RotationHelper;
53 import com.android.launcher3.uioverrides.plugins.PluginManagerWrapper;
54 
55 import java.util.Collections;
56 import java.util.List;
57 
58 /**
59  * Settings activity for Launcher. Currently implements the following setting: Allow rotation
60  */
61 public class SettingsActivity extends FragmentActivity
62         implements OnPreferenceStartFragmentCallback, OnPreferenceStartScreenCallback,
63         SharedPreferences.OnSharedPreferenceChangeListener{
64 
65     /** List of fragments that can be hosted by this activity. */
66     private static final List<String> VALID_PREFERENCE_FRAGMENTS =
67             !Utilities.IS_DEBUG_DEVICE ? Collections.emptyList()
68                     : Collections.singletonList(DeveloperOptionsFragment.class.getName());
69 
70     private static final String DEVELOPER_OPTIONS_KEY = "pref_developer_options";
71     private static final String FLAGS_PREFERENCE_KEY = "flag_toggler";
72 
73     private static final String NOTIFICATION_DOTS_PREFERENCE_KEY = "pref_icon_badging";
74 
75     public static final String EXTRA_FRAGMENT_ARG_KEY = ":settings:fragment_args_key";
76     public static final String EXTRA_SHOW_FRAGMENT_ARGS = ":settings:show_fragment_args";
77     private static final int DELAY_HIGHLIGHT_DURATION_MILLIS = 600;
78     public static final String SAVE_HIGHLIGHTED_KEY = "android:preference_highlighted";
79 
80     @VisibleForTesting
81     static final String EXTRA_FRAGMENT = ":settings:fragment";
82     @VisibleForTesting
83     static final String EXTRA_FRAGMENT_ARGS = ":settings:fragment_args";
84 
85     @Override
onCreate(Bundle savedInstanceState)86     protected void onCreate(Bundle savedInstanceState) {
87         super.onCreate(savedInstanceState);
88         setContentView(R.layout.settings_activity);
89         setActionBar(findViewById(R.id.action_bar));
90         WindowCompat.setDecorFitsSystemWindows(getWindow(), false);
91 
92         Intent intent = getIntent();
93         if (intent.hasExtra(EXTRA_FRAGMENT) || intent.hasExtra(EXTRA_FRAGMENT_ARGS)) {
94             getActionBar().setDisplayHomeAsUpEnabled(true);
95         }
96 
97         if (savedInstanceState == null) {
98             Bundle args = intent.getBundleExtra(EXTRA_FRAGMENT_ARGS);
99             if (args == null) {
100                 args = new Bundle();
101             }
102 
103             String prefKey = intent.getStringExtra(EXTRA_FRAGMENT_ARG_KEY);
104             if (!TextUtils.isEmpty(prefKey)) {
105                 args.putString(EXTRA_FRAGMENT_ARG_KEY, prefKey);
106             }
107 
108             final FragmentManager fm = getSupportFragmentManager();
109             final Fragment f = fm.getFragmentFactory().instantiate(getClassLoader(),
110                     getPreferenceFragment());
111             f.setArguments(args);
112             // Display the fragment as the main content.
113             fm.beginTransaction().replace(R.id.content_frame, f).commit();
114         }
115         Utilities.getPrefs(getApplicationContext()).registerOnSharedPreferenceChangeListener(this);
116     }
117 
118     /**
119      * Obtains the preference fragment to instantiate in this activity.
120      *
121      * @return the preference fragment class
122      * @throws IllegalArgumentException if the fragment is unknown to this activity
123      */
getPreferenceFragment()124     private String getPreferenceFragment() {
125         String preferenceFragment = getIntent().getStringExtra(EXTRA_FRAGMENT);
126         String defaultFragment = getString(R.string.settings_fragment_name);
127 
128         if (TextUtils.isEmpty(preferenceFragment)) {
129             return defaultFragment;
130         } else if (!preferenceFragment.equals(defaultFragment)
131                 && !VALID_PREFERENCE_FRAGMENTS.contains(preferenceFragment)) {
132             throw new IllegalArgumentException(
133                     "Invalid fragment for this activity: " + preferenceFragment);
134         } else {
135             return preferenceFragment;
136         }
137     }
138 
139     @Override
onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key)140     public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) { }
141 
startPreference(String fragment, Bundle args, String key)142     private boolean startPreference(String fragment, Bundle args, String key) {
143         if (Utilities.ATLEAST_P && getSupportFragmentManager().isStateSaved()) {
144             // Sometimes onClick can come after onPause because of being posted on the handler.
145             // Skip starting new preferences in that case.
146             return false;
147         }
148         final FragmentManager fm = getSupportFragmentManager();
149         final Fragment f = fm.getFragmentFactory().instantiate(getClassLoader(), fragment);
150         if (f instanceof DialogFragment) {
151             f.setArguments(args);
152             ((DialogFragment) f).show(fm, key);
153         } else {
154             startActivity(new Intent(this, SettingsActivity.class)
155                     .putExtra(EXTRA_FRAGMENT, fragment)
156                     .putExtra(EXTRA_FRAGMENT_ARGS, args));
157         }
158         return true;
159     }
160 
161     @Override
onPreferenceStartFragment( PreferenceFragmentCompat preferenceFragment, Preference pref)162     public boolean onPreferenceStartFragment(
163             PreferenceFragmentCompat preferenceFragment, Preference pref) {
164         return startPreference(pref.getFragment(), pref.getExtras(), pref.getKey());
165     }
166 
167     @Override
onPreferenceStartScreen(PreferenceFragmentCompat caller, PreferenceScreen pref)168     public boolean onPreferenceStartScreen(PreferenceFragmentCompat caller, PreferenceScreen pref) {
169         Bundle args = new Bundle();
170         args.putString(PreferenceFragmentCompat.ARG_PREFERENCE_ROOT, pref.getKey());
171         return startPreference(getString(R.string.settings_fragment_name), args, pref.getKey());
172     }
173 
174     @Override
onOptionsItemSelected(MenuItem item)175     public boolean onOptionsItemSelected(MenuItem item) {
176         if (item.getItemId() == android.R.id.home) {
177             onBackPressed();
178             return true;
179         }
180         return super.onOptionsItemSelected(item);
181     }
182 
183     /**
184      * This fragment shows the launcher preferences.
185      */
186     public static class LauncherSettingsFragment extends PreferenceFragmentCompat {
187 
188         private String mHighLightKey;
189         private boolean mPreferenceHighlighted = false;
190         private Preference mDeveloperOptionPref;
191 
192         @Override
onCreatePreferences(Bundle savedInstanceState, String rootKey)193         public void onCreatePreferences(Bundle savedInstanceState, String rootKey) {
194             final Bundle args = getArguments();
195             mHighLightKey = args == null ? null : args.getString(EXTRA_FRAGMENT_ARG_KEY);
196             if (rootKey == null && !TextUtils.isEmpty(mHighLightKey)) {
197                 rootKey = getParentKeyForPref(mHighLightKey);
198             }
199 
200             if (savedInstanceState != null) {
201                 mPreferenceHighlighted = savedInstanceState.getBoolean(SAVE_HIGHLIGHTED_KEY);
202             }
203 
204             getPreferenceManager().setSharedPreferencesName(LauncherFiles.SHARED_PREFERENCES_KEY);
205             setPreferencesFromResource(R.xml.launcher_preferences, rootKey);
206 
207             PreferenceScreen screen = getPreferenceScreen();
208             for (int i = screen.getPreferenceCount() - 1; i >= 0; i--) {
209                 Preference preference = screen.getPreference(i);
210                 if (!initPreference(preference)) {
211                     screen.removePreference(preference);
212                 }
213             }
214 
215             if (getActivity() != null && !TextUtils.isEmpty(getPreferenceScreen().getTitle())) {
216                 getActivity().setTitle(getPreferenceScreen().getTitle());
217             }
218         }
219 
220         @Override
onViewCreated(View view, Bundle savedInstanceState)221         public void onViewCreated(View view, Bundle savedInstanceState) {
222             super.onViewCreated(view, savedInstanceState);
223             View listView = getListView();
224             final int bottomPadding = listView.getPaddingBottom();
225             listView.setOnApplyWindowInsetsListener((v, insets) -> {
226                 v.setPadding(
227                         v.getPaddingLeft(),
228                         v.getPaddingTop(),
229                         v.getPaddingRight(),
230                         bottomPadding + insets.getSystemWindowInsetBottom());
231                 return insets.consumeSystemWindowInsets();
232             });
233         }
234 
235         @Override
onSaveInstanceState(Bundle outState)236         public void onSaveInstanceState(Bundle outState) {
237             super.onSaveInstanceState(outState);
238             outState.putBoolean(SAVE_HIGHLIGHTED_KEY, mPreferenceHighlighted);
239         }
240 
getParentKeyForPref(String key)241         protected String getParentKeyForPref(String key) {
242             return null;
243         }
244 
245         /**
246          * Initializes a preference. This is called for every preference. Returning false here
247          * will remove that preference from the list.
248          */
initPreference(Preference preference)249         protected boolean initPreference(Preference preference) {
250             switch (preference.getKey()) {
251                 case NOTIFICATION_DOTS_PREFERENCE_KEY:
252                     return !WidgetsModel.GO_DISABLE_NOTIFICATION_DOTS;
253 
254                 case ALLOW_ROTATION_PREFERENCE_KEY:
255                     DeviceProfile deviceProfile = InvariantDeviceProfile.INSTANCE.get(
256                             getContext()).getDeviceProfile(getContext());
257                     if (deviceProfile.isTablet) {
258                         // Launcher supports rotation by default. No need to show this setting.
259                         return false;
260                     }
261                     // Initialize the UI once
262                     preference.setDefaultValue(
263                             RotationHelper.getAllowRotationDefaultValue(deviceProfile));
264                     return true;
265 
266                 case FLAGS_PREFERENCE_KEY:
267                     // Only show flag toggler UI if this build variant implements that.
268                     return FeatureFlags.showFlagTogglerUi(getContext());
269 
270                 case DEVELOPER_OPTIONS_KEY:
271                     mDeveloperOptionPref = preference;
272                     return updateDeveloperOption();
273             }
274 
275             return true;
276         }
277 
278         /**
279          * Show if plugins are enabled or flag UI is enabled.
280          * @return True if we should show the preference option.
281          */
updateDeveloperOption()282         private boolean updateDeveloperOption() {
283             boolean showPreference = FeatureFlags.showFlagTogglerUi(getContext())
284                     || PluginManagerWrapper.hasPlugins(getContext());
285             if (mDeveloperOptionPref != null) {
286                 mDeveloperOptionPref.setEnabled(showPreference);
287                 if (showPreference) {
288                     getPreferenceScreen().addPreference(mDeveloperOptionPref);
289                 } else {
290                     getPreferenceScreen().removePreference(mDeveloperOptionPref);
291                 }
292             }
293             return showPreference;
294         }
295 
296         @Override
onResume()297         public void onResume() {
298             super.onResume();
299 
300             updateDeveloperOption();
301 
302             if (isAdded() && !mPreferenceHighlighted) {
303                 PreferenceHighlighter highlighter = createHighlighter();
304                 if (highlighter != null) {
305                     getView().postDelayed(highlighter, DELAY_HIGHLIGHT_DURATION_MILLIS);
306                     mPreferenceHighlighted = true;
307                 } else {
308                     requestAccessibilityFocus(getListView());
309                 }
310             }
311         }
312 
createHighlighter()313         private PreferenceHighlighter createHighlighter() {
314             if (TextUtils.isEmpty(mHighLightKey)) {
315                 return null;
316             }
317 
318             PreferenceScreen screen = getPreferenceScreen();
319             if (screen == null) {
320                 return null;
321             }
322 
323             RecyclerView list = getListView();
324             PreferencePositionCallback callback = (PreferencePositionCallback) list.getAdapter();
325             int position = callback.getPreferenceAdapterPosition(mHighLightKey);
326             return position >= 0 ? new PreferenceHighlighter(
327                     list, position, screen.findPreference(mHighLightKey))
328                     : null;
329         }
330 
requestAccessibilityFocus(@onNull final RecyclerView rv)331         private void requestAccessibilityFocus(@NonNull final RecyclerView rv) {
332             rv.post(() -> {
333                 if (!rv.hasFocus() && rv.getChildCount() > 0) {
334                     rv.getChildAt(0)
335                             .performAccessibilityAction(ACTION_ACCESSIBILITY_FOCUS, null);
336                 }
337             });
338         }
339     }
340 }
341