2 * Copyright (C) 2010 The Android Open Source Project
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
8 * http://www.apache.org/licenses/LICENSE-2.0
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.
17 package com.android.settings;
19 import android.app.Activity;
20 import android.app.Dialog;
21 import android.app.DialogFragment;
22 import android.app.Fragment;
23 import android.content.ContentResolver;
24 import android.content.Context;
25 import android.content.DialogInterface;
26 import android.content.Intent;
27 import android.content.pm.PackageManager;
28 import android.os.Bundle;
29 import android.support.annotation.VisibleForTesting;
30 import android.support.annotation.XmlRes;
31 import android.support.v7.preference.Preference;
32 import android.support.v7.preference.PreferenceGroup;
33 import android.support.v7.preference.PreferenceScreen;
34 import android.support.v7.widget.LinearLayoutManager;
35 import android.support.v7.widget.RecyclerView;
36 import android.text.TextUtils;
37 import android.util.ArrayMap;
38 import android.util.Log;
39 import android.view.LayoutInflater;
40 import android.view.View;
41 import android.view.ViewGroup;
42 import android.widget.Button;
44 import com.android.settings.applications.LayoutPreference;
45 import com.android.settings.core.InstrumentedPreferenceFragment;
46 import com.android.settings.core.instrumentation.InstrumentedDialogFragment;
47 import com.android.settings.search.actionbar.SearchMenuController;
48 import com.android.settings.support.actionbar.HelpMenuController;
49 import com.android.settings.support.actionbar.HelpResourceProvider;
50 import com.android.settings.widget.HighlightablePreferenceGroupAdapter;
51 import com.android.settings.widget.LoadingViewController;
52 import com.android.settingslib.CustomDialogPreference;
53 import com.android.settingslib.CustomEditTextPreference;
54 import com.android.settingslib.core.instrumentation.Instrumentable;
55 import com.android.settingslib.widget.FooterPreferenceMixin;
57 import java.util.UUID;
60 * Base class for Settings fragments, with some helper functions and dialog management.
62 public abstract class SettingsPreferenceFragment extends InstrumentedPreferenceFragment
63 implements DialogCreatable, HelpResourceProvider {
65 private static final String TAG = "SettingsPreference";
67 private static final String SAVE_HIGHLIGHTED_KEY = "android:preference_highlighted";
69 protected final FooterPreferenceMixin mFooterPreferenceMixin =
70 new FooterPreferenceMixin(this, getLifecycle());
73 private static final int ORDER_FIRST = -1;
75 private SettingsDialogFragment mDialogFragment;
76 // Cache the content resolver for async callbacks
77 private ContentResolver mContentResolver;
79 private RecyclerView.Adapter mCurrentRootAdapter;
80 private boolean mIsDataSetObserverRegistered = false;
81 private RecyclerView.AdapterDataObserver mDataSetObserver =
82 new RecyclerView.AdapterDataObserver() {
84 public void onChanged() {
89 public void onItemRangeChanged(int positionStart, int itemCount) {
94 public void onItemRangeChanged(int positionStart, int itemCount, Object payload) {
99 public void onItemRangeInserted(int positionStart, int itemCount) {
104 public void onItemRangeRemoved(int positionStart, int itemCount) {
109 public void onItemRangeMoved(int fromPosition, int toPosition, int itemCount) {
114 private ViewGroup mPinnedHeaderFrameLayout;
115 private ViewGroup mButtonBar;
117 private LayoutPreference mHeader;
119 private View mEmptyView;
120 private LinearLayoutManager mLayoutManager;
121 private ArrayMap<String, Preference> mPreferenceCache;
122 private boolean mAnimationAllowed;
125 public HighlightablePreferenceGroupAdapter mAdapter;
127 public boolean mPreferenceHighlighted = false;
130 public void onCreate(Bundle icicle) {
131 super.onCreate(icicle);
132 SearchMenuController.init(this /* host */);
133 HelpMenuController.init(this /* host */);
135 if (icicle != null) {
136 mPreferenceHighlighted = icicle.getBoolean(SAVE_HIGHLIGHTED_KEY);
138 final Bundle arguments = getArguments();
140 // Check if we should keep the preferences expanded.
141 if (arguments != null) {
142 final String highlightKey =
143 arguments.getString(SettingsActivity.EXTRA_FRAGMENT_ARG_KEY);
144 if (!TextUtils.isEmpty(highlightKey)) {
145 final PreferenceScreen screen = getPreferenceScreen();
146 if (screen != null) {
147 screen.setInitialExpandedChildrenCount(Integer.MAX_VALUE);
154 public View onCreateView(LayoutInflater inflater, ViewGroup container,
155 Bundle savedInstanceState) {
156 final View root = super.onCreateView(inflater, container, savedInstanceState);
157 mPinnedHeaderFrameLayout = root.findViewById(R.id.pinned_header);
158 mButtonBar = root.findViewById(R.id.button_bar);
163 public void addPreferencesFromResource(@XmlRes int preferencesResId) {
164 super.addPreferencesFromResource(preferencesResId);
165 checkAvailablePrefs(getPreferenceScreen());
168 private void checkAvailablePrefs(PreferenceGroup preferenceGroup) {
169 if (preferenceGroup == null) return;
170 for (int i = 0; i < preferenceGroup.getPreferenceCount(); i++) {
171 Preference pref = preferenceGroup.getPreference(i);
172 if (pref instanceof SelfAvailablePreference
173 && !((SelfAvailablePreference) pref).isAvailable(getContext())) {
174 preferenceGroup.removePreference(pref);
175 } else if (pref instanceof PreferenceGroup) {
176 checkAvailablePrefs((PreferenceGroup) pref);
181 public ViewGroup getButtonBar() {
185 public View setPinnedHeaderView(int layoutResId) {
186 final LayoutInflater inflater = getActivity().getLayoutInflater();
187 final View pinnedHeader =
188 inflater.inflate(layoutResId, mPinnedHeaderFrameLayout, false);
189 setPinnedHeaderView(pinnedHeader);
193 public void setPinnedHeaderView(View pinnedHeader) {
194 mPinnedHeaderFrameLayout.addView(pinnedHeader);
195 mPinnedHeaderFrameLayout.setVisibility(View.VISIBLE);
199 public void onSaveInstanceState(Bundle outState) {
200 super.onSaveInstanceState(outState);
202 if (mAdapter != null) {
203 outState.putBoolean(SAVE_HIGHLIGHTED_KEY, mAdapter.isHighlightRequested());
208 public void onActivityCreated(Bundle savedInstanceState) {
209 super.onActivityCreated(savedInstanceState);
210 setHasOptionsMenu(true);
214 public void onResume() {
216 highlightPreferenceIfNeeded();
220 protected void onBindPreferences() {
221 registerObserverIfNeeded();
225 protected void onUnbindPreferences() {
226 unregisterObserverIfNeeded();
229 public void setLoading(boolean loading, boolean animate) {
230 View loadingContainer = getView().findViewById(R.id.loading_container);
231 LoadingViewController.handleLoadingContainer(loadingContainer, getListView(),
236 public void registerObserverIfNeeded() {
237 if (!mIsDataSetObserverRegistered) {
238 if (mCurrentRootAdapter != null) {
239 mCurrentRootAdapter.unregisterAdapterDataObserver(mDataSetObserver);
241 mCurrentRootAdapter = getListView().getAdapter();
242 mCurrentRootAdapter.registerAdapterDataObserver(mDataSetObserver);
243 mIsDataSetObserverRegistered = true;
248 public void unregisterObserverIfNeeded() {
249 if (mIsDataSetObserverRegistered) {
250 if (mCurrentRootAdapter != null) {
251 mCurrentRootAdapter.unregisterAdapterDataObserver(mDataSetObserver);
252 mCurrentRootAdapter = null;
254 mIsDataSetObserverRegistered = false;
258 public void highlightPreferenceIfNeeded() {
262 if (mAdapter != null) {
263 mAdapter.requestHighlight(getView(), getListView());
267 protected void onDataSetChanged() {
268 highlightPreferenceIfNeeded();
272 public LayoutPreference getHeaderView() {
276 protected void setHeaderView(int resource) {
277 mHeader = new LayoutPreference(getPrefContext(), resource);
278 addPreferenceToTop(mHeader);
281 protected void setHeaderView(View view) {
282 mHeader = new LayoutPreference(getPrefContext(), view);
283 addPreferenceToTop(mHeader);
286 private void addPreferenceToTop(LayoutPreference preference) {
287 preference.setOrder(ORDER_FIRST);
288 if (getPreferenceScreen() != null) {
289 getPreferenceScreen().addPreference(preference);
294 public void setPreferenceScreen(PreferenceScreen preferenceScreen) {
295 if (preferenceScreen != null && !preferenceScreen.isAttached()) {
296 // Without ids generated, the RecyclerView won't animate changes to the preferences.
297 preferenceScreen.setShouldUseGeneratedIds(mAnimationAllowed);
299 super.setPreferenceScreen(preferenceScreen);
300 if (preferenceScreen != null) {
301 if (mHeader != null) {
302 preferenceScreen.addPreference(mHeader);
308 void updateEmptyView() {
309 if (mEmptyView == null) return;
310 if (getPreferenceScreen() != null) {
311 final View listContainer = getActivity().findViewById(android.R.id.list_container);
312 boolean show = (getPreferenceScreen().getPreferenceCount()
313 - (mHeader != null ? 1 : 0)
314 - (mFooterPreferenceMixin.hasFooter() ? 1 : 0)) <= 0
315 || (listContainer != null && listContainer.getVisibility() != View.VISIBLE);
316 mEmptyView.setVisibility(show ? View.VISIBLE : View.GONE);
318 mEmptyView.setVisibility(View.VISIBLE);
322 public void setEmptyView(View v) {
323 if (mEmptyView != null) {
324 mEmptyView.setVisibility(View.GONE);
330 public View getEmptyView() {
335 public RecyclerView.LayoutManager onCreateLayoutManager() {
336 mLayoutManager = new LinearLayoutManager(getContext());
337 return mLayoutManager;
341 protected RecyclerView.Adapter onCreateAdapter(PreferenceScreen preferenceScreen) {
342 mAdapter = new HighlightablePreferenceGroupAdapter(preferenceScreen,
343 getArguments().getString(SettingsActivity.EXTRA_FRAGMENT_ARG_KEY),
344 mPreferenceHighlighted);
348 protected void setAnimationAllowed(boolean animationAllowed) {
349 mAnimationAllowed = animationAllowed;
352 protected void cacheRemoveAllPrefs(PreferenceGroup group) {
353 mPreferenceCache = new ArrayMap<>();
354 final int N = group.getPreferenceCount();
355 for (int i = 0; i < N; i++) {
356 Preference p = group.getPreference(i);
357 if (TextUtils.isEmpty(p.getKey())) {
360 mPreferenceCache.put(p.getKey(), p);
364 protected Preference getCachedPreference(String key) {
365 return mPreferenceCache != null ? mPreferenceCache.remove(key) : null;
368 protected void removeCachedPrefs(PreferenceGroup group) {
369 for (Preference p : mPreferenceCache.values()) {
370 group.removePreference(p);
372 mPreferenceCache = null;
375 protected int getCachedCount() {
376 return mPreferenceCache != null ? mPreferenceCache.size() : 0;
379 protected boolean removePreference(String key) {
380 return removePreference(getPreferenceScreen(), key);
384 boolean removePreference(PreferenceGroup group, String key) {
385 final int preferenceCount = group.getPreferenceCount();
386 for (int i = 0; i < preferenceCount; i++) {
387 final Preference preference = group.getPreference(i);
388 final String curKey = preference.getKey();
390 if (TextUtils.equals(curKey, key)) {
391 return group.removePreference(preference);
394 if (preference instanceof PreferenceGroup) {
395 if (removePreference((PreferenceGroup) preference, key)) {
404 * The name is intentionally made different from Activity#finish(), so that
405 * users won't misunderstand its meaning.
407 public final void finishFragment() {
408 getActivity().onBackPressed();
411 // Some helpers for functions used by the settings fragments when they were activities
414 * Returns the ContentResolver from the owning Activity.
416 protected ContentResolver getContentResolver() {
417 Context context = getActivity();
418 if (context != null) {
419 mContentResolver = context.getContentResolver();
421 return mContentResolver;
425 * Returns the specified system service from the owning Activity.
427 protected Object getSystemService(final String name) {
428 return getActivity().getSystemService(name);
432 * Returns the PackageManager from the owning Activity.
434 protected PackageManager getPackageManager() {
435 return getActivity().getPackageManager();
439 public void onDetach() {
441 if (mDialogFragment != null) {
442 mDialogFragment.dismiss();
443 mDialogFragment = null;
451 protected void showDialog(int dialogId) {
452 if (mDialogFragment != null) {
453 Log.e(TAG, "Old dialog fragment not null!");
455 mDialogFragment = new SettingsDialogFragment(this, dialogId);
456 mDialogFragment.show(getChildFragmentManager(), Integer.toString(dialogId));
460 public Dialog onCreateDialog(int dialogId) {
465 public int getDialogMetricsCategory(int dialogId) {
469 protected void removeDialog(int dialogId) {
470 // mDialogFragment may not be visible yet in parent fragment's onResume().
471 // To be able to dismiss dialog at that time, don't check
472 // mDialogFragment.isVisible().
473 if (mDialogFragment != null && mDialogFragment.getDialogId() == dialogId) {
474 mDialogFragment.dismissAllowingStateLoss();
476 mDialogFragment = null;
480 * Sets the OnCancelListener of the dialog shown. This method can only be
481 * called after showDialog(int) and before removeDialog(int). The method
482 * does nothing otherwise.
484 protected void setOnCancelListener(DialogInterface.OnCancelListener listener) {
485 if (mDialogFragment != null) {
486 mDialogFragment.mOnCancelListener = listener;
491 * Sets the OnDismissListener of the dialog shown. This method can only be
492 * called after showDialog(int) and before removeDialog(int). The method
493 * does nothing otherwise.
495 protected void setOnDismissListener(DialogInterface.OnDismissListener listener) {
496 if (mDialogFragment != null) {
497 mDialogFragment.mOnDismissListener = listener;
501 public void onDialogShowing() {
502 // override in subclass to attach a dismiss listener, for instance
506 public void onDisplayPreferenceDialog(Preference preference) {
507 if (preference.getKey() == null) {
508 // Auto-key preferences that don't have a key, so the dialog can find them.
509 preference.setKey(UUID.randomUUID().toString());
511 DialogFragment f = null;
512 if (preference instanceof RestrictedListPreference) {
513 f = RestrictedListPreference.RestrictedListPreferenceDialogFragment
514 .newInstance(preference.getKey());
515 } else if (preference instanceof CustomListPreference) {
516 f = CustomListPreference.CustomListPreferenceDialogFragment
517 .newInstance(preference.getKey());
518 } else if (preference instanceof CustomDialogPreference) {
519 f = CustomDialogPreference.CustomPreferenceDialogFragment
520 .newInstance(preference.getKey());
521 } else if (preference instanceof CustomEditTextPreference) {
522 f = CustomEditTextPreference.CustomPreferenceDialogFragment
523 .newInstance(preference.getKey());
525 super.onDisplayPreferenceDialog(preference);
528 f.setTargetFragment(this, 0);
529 f.show(getFragmentManager(), "dialog_preference");
533 public static class SettingsDialogFragment extends InstrumentedDialogFragment {
534 private static final String KEY_DIALOG_ID = "key_dialog_id";
535 private static final String KEY_PARENT_FRAGMENT_ID = "key_parent_fragment_id";
537 private Fragment mParentFragment;
539 private DialogInterface.OnCancelListener mOnCancelListener;
540 private DialogInterface.OnDismissListener mOnDismissListener;
542 public SettingsDialogFragment() {
546 public SettingsDialogFragment(DialogCreatable fragment, int dialogId) {
547 super(fragment, dialogId);
548 if (!(fragment instanceof Fragment)) {
549 throw new IllegalArgumentException("fragment argument must be an instance of "
550 + Fragment.class.getName());
552 mParentFragment = (Fragment) fragment;
557 public int getMetricsCategory() {
558 if (mDialogCreatable == null) {
559 return Instrumentable.METRICS_CATEGORY_UNKNOWN;
561 final int metricsCategory = mDialogCreatable.getDialogMetricsCategory(mDialogId);
562 if (metricsCategory <= 0) {
563 throw new IllegalStateException("Dialog must provide a metrics category");
565 return metricsCategory;
569 public void onSaveInstanceState(Bundle outState) {
570 super.onSaveInstanceState(outState);
571 if (mParentFragment != null) {
572 outState.putInt(KEY_DIALOG_ID, mDialogId);
573 outState.putInt(KEY_PARENT_FRAGMENT_ID, mParentFragment.getId());
578 public void onStart() {
581 if (mParentFragment != null && mParentFragment instanceof SettingsPreferenceFragment) {
582 ((SettingsPreferenceFragment) mParentFragment).onDialogShowing();
587 public Dialog onCreateDialog(Bundle savedInstanceState) {
588 if (savedInstanceState != null) {
589 mDialogId = savedInstanceState.getInt(KEY_DIALOG_ID, 0);
590 mParentFragment = getParentFragment();
591 int mParentFragmentId = savedInstanceState.getInt(KEY_PARENT_FRAGMENT_ID, -1);
592 if (mParentFragment == null) {
593 mParentFragment = getFragmentManager().findFragmentById(mParentFragmentId);
595 if (!(mParentFragment instanceof DialogCreatable)) {
596 throw new IllegalArgumentException(
597 (mParentFragment != null
598 ? mParentFragment.getClass().getName()
601 + DialogCreatable.class.getName());
603 // This dialog fragment could be created from non-SettingsPreferenceFragment
604 if (mParentFragment instanceof SettingsPreferenceFragment) {
605 // restore mDialogFragment in mParentFragment
606 ((SettingsPreferenceFragment) mParentFragment).mDialogFragment = this;
609 return ((DialogCreatable) mParentFragment).onCreateDialog(mDialogId);
613 public void onCancel(DialogInterface dialog) {
614 super.onCancel(dialog);
615 if (mOnCancelListener != null) {
616 mOnCancelListener.onCancel(dialog);
621 public void onDismiss(DialogInterface dialog) {
622 super.onDismiss(dialog);
623 if (mOnDismissListener != null) {
624 mOnDismissListener.onDismiss(dialog);
628 public int getDialogId() {
633 public void onDetach() {
636 // This dialog fragment could be created from non-SettingsPreferenceFragment
637 if (mParentFragment instanceof SettingsPreferenceFragment) {
638 // in case the dialog is not explicitly removed by removeDialog()
639 if (((SettingsPreferenceFragment) mParentFragment).mDialogFragment == this) {
640 ((SettingsPreferenceFragment) mParentFragment).mDialogFragment = null;
646 protected boolean hasNextButton() {
647 return ((ButtonBarHandler) getActivity()).hasNextButton();
650 protected Button getNextButton() {
651 return ((ButtonBarHandler) getActivity()).getNextButton();
654 public void finish() {
655 Activity activity = getActivity();
656 if (activity == null) return;
657 if (getFragmentManager().getBackStackEntryCount() > 0) {
658 getFragmentManager().popBackStack();
664 protected Intent getIntent() {
665 if (getActivity() == null) {
668 return getActivity().getIntent();
671 protected void setResult(int result, Intent intent) {
672 if (getActivity() == null) {
675 getActivity().setResult(result, intent);
678 protected void setResult(int result) {
679 if (getActivity() == null) {
682 getActivity().setResult(result);
685 public boolean startFragment(Fragment caller, String fragmentClass, int titleRes,
686 int requestCode, Bundle extras) {
687 final Activity activity = getActivity();
688 if (activity instanceof SettingsActivity) {
689 SettingsActivity sa = (SettingsActivity) activity;
690 sa.startPreferencePanel(
691 caller, fragmentClass, extras, titleRes, null, caller, requestCode);
695 "Parent isn't SettingsActivity nor PreferenceActivity, thus there's no way to "
696 + "launch the given Fragment (name: " + fragmentClass
697 + ", requestCode: " + requestCode + ")");