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 final Bundle arguments = getArguments();
343 mAdapter = new HighlightablePreferenceGroupAdapter(preferenceScreen,
345 ? null : arguments.getString(SettingsActivity.EXTRA_FRAGMENT_ARG_KEY),
346 mPreferenceHighlighted);
350 protected void setAnimationAllowed(boolean animationAllowed) {
351 mAnimationAllowed = animationAllowed;
354 protected void cacheRemoveAllPrefs(PreferenceGroup group) {
355 mPreferenceCache = new ArrayMap<>();
356 final int N = group.getPreferenceCount();
357 for (int i = 0; i < N; i++) {
358 Preference p = group.getPreference(i);
359 if (TextUtils.isEmpty(p.getKey())) {
362 mPreferenceCache.put(p.getKey(), p);
366 protected Preference getCachedPreference(String key) {
367 return mPreferenceCache != null ? mPreferenceCache.remove(key) : null;
370 protected void removeCachedPrefs(PreferenceGroup group) {
371 for (Preference p : mPreferenceCache.values()) {
372 group.removePreference(p);
374 mPreferenceCache = null;
377 protected int getCachedCount() {
378 return mPreferenceCache != null ? mPreferenceCache.size() : 0;
381 protected boolean removePreference(String key) {
382 return removePreference(getPreferenceScreen(), key);
386 boolean removePreference(PreferenceGroup group, String key) {
387 final int preferenceCount = group.getPreferenceCount();
388 for (int i = 0; i < preferenceCount; i++) {
389 final Preference preference = group.getPreference(i);
390 final String curKey = preference.getKey();
392 if (TextUtils.equals(curKey, key)) {
393 return group.removePreference(preference);
396 if (preference instanceof PreferenceGroup) {
397 if (removePreference((PreferenceGroup) preference, key)) {
406 * The name is intentionally made different from Activity#finish(), so that
407 * users won't misunderstand its meaning.
409 public final void finishFragment() {
410 getActivity().onBackPressed();
413 // Some helpers for functions used by the settings fragments when they were activities
416 * Returns the ContentResolver from the owning Activity.
418 protected ContentResolver getContentResolver() {
419 Context context = getActivity();
420 if (context != null) {
421 mContentResolver = context.getContentResolver();
423 return mContentResolver;
427 * Returns the specified system service from the owning Activity.
429 protected Object getSystemService(final String name) {
430 return getActivity().getSystemService(name);
434 * Returns the PackageManager from the owning Activity.
436 protected PackageManager getPackageManager() {
437 return getActivity().getPackageManager();
441 public void onDetach() {
443 if (mDialogFragment != null) {
444 mDialogFragment.dismiss();
445 mDialogFragment = null;
453 protected void showDialog(int dialogId) {
454 if (mDialogFragment != null) {
455 Log.e(TAG, "Old dialog fragment not null!");
457 mDialogFragment = new SettingsDialogFragment(this, dialogId);
458 mDialogFragment.show(getChildFragmentManager(), Integer.toString(dialogId));
462 public Dialog onCreateDialog(int dialogId) {
467 public int getDialogMetricsCategory(int dialogId) {
471 protected void removeDialog(int dialogId) {
472 // mDialogFragment may not be visible yet in parent fragment's onResume().
473 // To be able to dismiss dialog at that time, don't check
474 // mDialogFragment.isVisible().
475 if (mDialogFragment != null && mDialogFragment.getDialogId() == dialogId) {
476 mDialogFragment.dismissAllowingStateLoss();
478 mDialogFragment = null;
482 * Sets the OnCancelListener of the dialog shown. This method can only be
483 * called after showDialog(int) and before removeDialog(int). The method
484 * does nothing otherwise.
486 protected void setOnCancelListener(DialogInterface.OnCancelListener listener) {
487 if (mDialogFragment != null) {
488 mDialogFragment.mOnCancelListener = listener;
493 * Sets the OnDismissListener of the dialog shown. This method can only be
494 * called after showDialog(int) and before removeDialog(int). The method
495 * does nothing otherwise.
497 protected void setOnDismissListener(DialogInterface.OnDismissListener listener) {
498 if (mDialogFragment != null) {
499 mDialogFragment.mOnDismissListener = listener;
503 public void onDialogShowing() {
504 // override in subclass to attach a dismiss listener, for instance
508 public void onDisplayPreferenceDialog(Preference preference) {
509 if (preference.getKey() == null) {
510 // Auto-key preferences that don't have a key, so the dialog can find them.
511 preference.setKey(UUID.randomUUID().toString());
513 DialogFragment f = null;
514 if (preference instanceof RestrictedListPreference) {
515 f = RestrictedListPreference.RestrictedListPreferenceDialogFragment
516 .newInstance(preference.getKey());
517 } else if (preference instanceof CustomListPreference) {
518 f = CustomListPreference.CustomListPreferenceDialogFragment
519 .newInstance(preference.getKey());
520 } else if (preference instanceof CustomDialogPreference) {
521 f = CustomDialogPreference.CustomPreferenceDialogFragment
522 .newInstance(preference.getKey());
523 } else if (preference instanceof CustomEditTextPreference) {
524 f = CustomEditTextPreference.CustomPreferenceDialogFragment
525 .newInstance(preference.getKey());
527 super.onDisplayPreferenceDialog(preference);
530 f.setTargetFragment(this, 0);
531 f.show(getFragmentManager(), "dialog_preference");
535 public static class SettingsDialogFragment extends InstrumentedDialogFragment {
536 private static final String KEY_DIALOG_ID = "key_dialog_id";
537 private static final String KEY_PARENT_FRAGMENT_ID = "key_parent_fragment_id";
539 private Fragment mParentFragment;
541 private DialogInterface.OnCancelListener mOnCancelListener;
542 private DialogInterface.OnDismissListener mOnDismissListener;
544 public SettingsDialogFragment() {
548 public SettingsDialogFragment(DialogCreatable fragment, int dialogId) {
549 super(fragment, dialogId);
550 if (!(fragment instanceof Fragment)) {
551 throw new IllegalArgumentException("fragment argument must be an instance of "
552 + Fragment.class.getName());
554 mParentFragment = (Fragment) fragment;
559 public int getMetricsCategory() {
560 if (mDialogCreatable == null) {
561 return Instrumentable.METRICS_CATEGORY_UNKNOWN;
563 final int metricsCategory = mDialogCreatable.getDialogMetricsCategory(mDialogId);
564 if (metricsCategory <= 0) {
565 throw new IllegalStateException("Dialog must provide a metrics category");
567 return metricsCategory;
571 public void onSaveInstanceState(Bundle outState) {
572 super.onSaveInstanceState(outState);
573 if (mParentFragment != null) {
574 outState.putInt(KEY_DIALOG_ID, mDialogId);
575 outState.putInt(KEY_PARENT_FRAGMENT_ID, mParentFragment.getId());
580 public void onStart() {
583 if (mParentFragment != null && mParentFragment instanceof SettingsPreferenceFragment) {
584 ((SettingsPreferenceFragment) mParentFragment).onDialogShowing();
589 public Dialog onCreateDialog(Bundle savedInstanceState) {
590 if (savedInstanceState != null) {
591 mDialogId = savedInstanceState.getInt(KEY_DIALOG_ID, 0);
592 mParentFragment = getParentFragment();
593 int mParentFragmentId = savedInstanceState.getInt(KEY_PARENT_FRAGMENT_ID, -1);
594 if (mParentFragment == null) {
595 mParentFragment = getFragmentManager().findFragmentById(mParentFragmentId);
597 if (!(mParentFragment instanceof DialogCreatable)) {
598 throw new IllegalArgumentException(
599 (mParentFragment != null
600 ? mParentFragment.getClass().getName()
603 + DialogCreatable.class.getName());
605 // This dialog fragment could be created from non-SettingsPreferenceFragment
606 if (mParentFragment instanceof SettingsPreferenceFragment) {
607 // restore mDialogFragment in mParentFragment
608 ((SettingsPreferenceFragment) mParentFragment).mDialogFragment = this;
611 return ((DialogCreatable) mParentFragment).onCreateDialog(mDialogId);
615 public void onCancel(DialogInterface dialog) {
616 super.onCancel(dialog);
617 if (mOnCancelListener != null) {
618 mOnCancelListener.onCancel(dialog);
623 public void onDismiss(DialogInterface dialog) {
624 super.onDismiss(dialog);
625 if (mOnDismissListener != null) {
626 mOnDismissListener.onDismiss(dialog);
630 public int getDialogId() {
635 public void onDetach() {
638 // This dialog fragment could be created from non-SettingsPreferenceFragment
639 if (mParentFragment instanceof SettingsPreferenceFragment) {
640 // in case the dialog is not explicitly removed by removeDialog()
641 if (((SettingsPreferenceFragment) mParentFragment).mDialogFragment == this) {
642 ((SettingsPreferenceFragment) mParentFragment).mDialogFragment = null;
648 protected boolean hasNextButton() {
649 return ((ButtonBarHandler) getActivity()).hasNextButton();
652 protected Button getNextButton() {
653 return ((ButtonBarHandler) getActivity()).getNextButton();
656 public void finish() {
657 Activity activity = getActivity();
658 if (activity == null) return;
659 if (getFragmentManager().getBackStackEntryCount() > 0) {
660 getFragmentManager().popBackStack();
666 protected Intent getIntent() {
667 if (getActivity() == null) {
670 return getActivity().getIntent();
673 protected void setResult(int result, Intent intent) {
674 if (getActivity() == null) {
677 getActivity().setResult(result, intent);
680 protected void setResult(int result) {
681 if (getActivity() == null) {
684 getActivity().setResult(result);
687 public boolean startFragment(Fragment caller, String fragmentClass, int titleRes,
688 int requestCode, Bundle extras) {
689 final Activity activity = getActivity();
690 if (activity instanceof SettingsActivity) {
691 SettingsActivity sa = (SettingsActivity) activity;
692 sa.startPreferencePanel(
693 caller, fragmentClass, extras, titleRes, null, caller, requestCode);
697 "Parent isn't SettingsActivity nor PreferenceActivity, thus there's no way to "
698 + "launch the given Fragment (name: " + fragmentClass
699 + ", requestCode: " + requestCode + ")");