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.PreferenceGroupAdapter;
34 import android.support.v7.preference.PreferenceScreen;
35 import android.support.v7.preference.PreferenceViewHolder;
36 import android.support.v7.widget.LinearLayoutManager;
37 import android.support.v7.widget.RecyclerView;
38 import android.text.TextUtils;
39 import android.util.ArrayMap;
40 import android.util.Log;
41 import android.view.LayoutInflater;
42 import android.view.Menu;
43 import android.view.MenuInflater;
44 import android.view.View;
45 import android.view.ViewGroup;
46 import android.widget.Button;
48 import com.android.settings.applications.LayoutPreference;
49 import com.android.settings.core.InstrumentedPreferenceFragment;
50 import com.android.settings.core.instrumentation.Instrumentable;
51 import com.android.settings.core.instrumentation.InstrumentedDialogFragment;
52 import com.android.settings.widget.LoadingViewController;
53 import com.android.settingslib.CustomDialogPreference;
54 import com.android.settingslib.CustomEditTextPreference;
55 import com.android.settingslib.HelpUtils;
56 import com.android.settingslib.widget.FooterPreferenceMixin;
58 import java.util.UUID;
61 * Base class for Settings fragments, with some helper functions and dialog management.
63 public abstract class SettingsPreferenceFragment extends InstrumentedPreferenceFragment
64 implements DialogCreatable {
67 * The Help Uri Resource key. This can be passed as an extra argument when creating the
70 public static final String HELP_URI_RESOURCE_KEY = "help_uri_resource";
72 private static final String TAG = "SettingsPreference";
75 static final int DELAY_HIGHLIGHT_DURATION_MILLIS = 600;
77 private static final String SAVE_HIGHLIGHTED_KEY = "android:preference_highlighted";
79 protected final FooterPreferenceMixin mFooterPreferenceMixin =
80 new FooterPreferenceMixin(this, getLifecycle());
82 private SettingsDialogFragment mDialogFragment;
84 private String mHelpUri;
86 private static final int ORDER_FIRST = -1;
87 private static final int ORDER_LAST = Integer.MAX_VALUE -1;
89 // Cache the content resolver for async callbacks
90 private ContentResolver mContentResolver;
92 private String mPreferenceKey;
94 private RecyclerView.Adapter mCurrentRootAdapter;
95 private boolean mIsDataSetObserverRegistered = false;
96 private RecyclerView.AdapterDataObserver mDataSetObserver =
97 new RecyclerView.AdapterDataObserver() {
99 public void onChanged() {
104 public void onItemRangeChanged(int positionStart, int itemCount) {
109 public void onItemRangeChanged(int positionStart, int itemCount, Object payload) {
114 public void onItemRangeInserted(int positionStart, int itemCount) {
119 public void onItemRangeRemoved(int positionStart, int itemCount) {
124 public void onItemRangeMoved(int fromPosition, int toPosition, int itemCount) {
129 private ViewGroup mPinnedHeaderFrameLayout;
130 private ViewGroup mButtonBar;
132 private LayoutPreference mHeader;
134 private View mEmptyView;
135 private LinearLayoutManager mLayoutManager;
136 private ArrayMap<String, Preference> mPreferenceCache;
137 private boolean mAnimationAllowed;
140 public HighlightablePreferenceGroupAdapter mAdapter;
142 public boolean mPreferenceHighlighted = false;
145 public void onCreate(Bundle icicle) {
146 super.onCreate(icicle);
148 if (icicle != null) {
149 mPreferenceHighlighted = icicle.getBoolean(SAVE_HIGHLIGHTED_KEY);
152 // Prepare help url and enable menu if necessary
153 Bundle arguments = getArguments();
155 if (arguments != null && arguments.containsKey(HELP_URI_RESOURCE_KEY)) {
156 helpResource = arguments.getInt(HELP_URI_RESOURCE_KEY);
158 helpResource = getHelpResource();
160 if (helpResource != 0) {
161 mHelpUri = getResources().getString(helpResource);
166 public View onCreateView(LayoutInflater inflater, ViewGroup container,
167 Bundle savedInstanceState) {
168 final View root = super.onCreateView(inflater, container, savedInstanceState);
169 mPinnedHeaderFrameLayout = (ViewGroup) root.findViewById(R.id.pinned_header);
170 mButtonBar = (ViewGroup) root.findViewById(R.id.button_bar);
175 public void addPreferencesFromResource(@XmlRes int preferencesResId) {
176 super.addPreferencesFromResource(preferencesResId);
177 checkAvailablePrefs(getPreferenceScreen());
180 private void checkAvailablePrefs(PreferenceGroup preferenceGroup) {
181 if (preferenceGroup == null) return;
182 for (int i = 0; i < preferenceGroup.getPreferenceCount(); i++) {
183 Preference pref = preferenceGroup.getPreference(i);
184 if (pref instanceof SelfAvailablePreference
185 && !((SelfAvailablePreference) pref).isAvailable(getContext())) {
186 preferenceGroup.removePreference(pref);
187 } else if (pref instanceof PreferenceGroup) {
188 checkAvailablePrefs((PreferenceGroup) pref);
193 public ViewGroup getButtonBar() {
197 public View setPinnedHeaderView(int layoutResId) {
198 final LayoutInflater inflater = getActivity().getLayoutInflater();
199 final View pinnedHeader =
200 inflater.inflate(layoutResId, mPinnedHeaderFrameLayout, false);
201 setPinnedHeaderView(pinnedHeader);
205 public void setPinnedHeaderView(View pinnedHeader) {
206 mPinnedHeaderFrameLayout.addView(pinnedHeader);
207 mPinnedHeaderFrameLayout.setVisibility(View.VISIBLE);
211 public void onSaveInstanceState(Bundle outState) {
212 super.onSaveInstanceState(outState);
214 outState.putBoolean(SAVE_HIGHLIGHTED_KEY, mPreferenceHighlighted);
218 public void onActivityCreated(Bundle savedInstanceState) {
219 super.onActivityCreated(savedInstanceState);
220 setHasOptionsMenu(true);
224 public void onResume() {
227 final Bundle args = getArguments();
229 mPreferenceKey = args.getString(SettingsActivity.EXTRA_FRAGMENT_ARG_KEY);
230 highlightPreferenceIfNeeded();
235 protected void onBindPreferences() {
236 registerObserverIfNeeded();
240 protected void onUnbindPreferences() {
241 unregisterObserverIfNeeded();
244 public void setLoading(boolean loading, boolean animate) {
245 View loadingContainer = getView().findViewById(R.id.loading_container);
246 LoadingViewController.handleLoadingContainer(loadingContainer, getListView(),
251 public void registerObserverIfNeeded() {
252 if (!mIsDataSetObserverRegistered) {
253 if (mCurrentRootAdapter != null) {
254 mCurrentRootAdapter.unregisterAdapterDataObserver(mDataSetObserver);
256 mCurrentRootAdapter = getListView().getAdapter();
257 mCurrentRootAdapter.registerAdapterDataObserver(mDataSetObserver);
258 mIsDataSetObserverRegistered = true;
263 public void unregisterObserverIfNeeded() {
264 if (mIsDataSetObserverRegistered) {
265 if (mCurrentRootAdapter != null) {
266 mCurrentRootAdapter.unregisterAdapterDataObserver(mDataSetObserver);
267 mCurrentRootAdapter = null;
269 mIsDataSetObserverRegistered = false;
273 public void highlightPreferenceIfNeeded() {
274 if (isAdded() && !mPreferenceHighlighted &&!TextUtils.isEmpty(mPreferenceKey)) {
275 getView().postDelayed(new Runnable() {
278 highlightPreference(mPreferenceKey);
280 }, DELAY_HIGHLIGHT_DURATION_MILLIS);
284 protected void onDataSetChanged() {
285 highlightPreferenceIfNeeded();
289 public LayoutPreference getHeaderView() {
293 protected void setHeaderView(int resource) {
294 mHeader = new LayoutPreference(getPrefContext(), resource);
295 addPreferenceToTop(mHeader);
298 protected void setHeaderView(View view) {
299 mHeader = new LayoutPreference(getPrefContext(), view);
300 addPreferenceToTop(mHeader);
303 private void addPreferenceToTop(LayoutPreference preference) {
304 preference.setOrder(ORDER_FIRST);
305 if (getPreferenceScreen() != null) {
306 getPreferenceScreen().addPreference(preference);
311 public void setPreferenceScreen(PreferenceScreen preferenceScreen) {
312 if (preferenceScreen != null && !preferenceScreen.isAttached()) {
313 // Without ids generated, the RecyclerView won't animate changes to the preferences.
314 preferenceScreen.setShouldUseGeneratedIds(mAnimationAllowed);
316 super.setPreferenceScreen(preferenceScreen);
317 if (preferenceScreen != null) {
318 if (mHeader != null) {
319 preferenceScreen.addPreference(mHeader);
325 void updateEmptyView() {
326 if (mEmptyView == null) return;
327 if (getPreferenceScreen() != null) {
328 final View listContainer = getActivity().findViewById(android.R.id.list_container);
329 boolean show = (getPreferenceScreen().getPreferenceCount()
330 - (mHeader != null ? 1 : 0)
331 - (mFooterPreferenceMixin.hasFooter() ? 1 : 0)) <= 0
332 || (listContainer != null && listContainer.getVisibility() != View.VISIBLE);
333 mEmptyView.setVisibility(show ? View.VISIBLE : View.GONE);
335 mEmptyView.setVisibility(View.VISIBLE);
339 public void setEmptyView(View v) {
340 if (mEmptyView != null) {
341 mEmptyView.setVisibility(View.GONE);
347 public View getEmptyView() {
352 * Return a valid ListView position or -1 if none is found
354 private int canUseListViewForHighLighting(String key) {
355 if (getListView() == null) {
359 RecyclerView listView = getListView();
360 RecyclerView.Adapter adapter = listView.getAdapter();
362 if (adapter != null && adapter instanceof PreferenceGroupAdapter) {
363 return findListPositionFromKey((PreferenceGroupAdapter) adapter, key);
370 public RecyclerView.LayoutManager onCreateLayoutManager() {
371 mLayoutManager = new LinearLayoutManager(getContext());
372 return mLayoutManager;
376 protected RecyclerView.Adapter onCreateAdapter(PreferenceScreen preferenceScreen) {
377 mAdapter = new HighlightablePreferenceGroupAdapter(preferenceScreen);
381 protected void setAnimationAllowed(boolean animationAllowed) {
382 mAnimationAllowed = animationAllowed;
385 protected void cacheRemoveAllPrefs(PreferenceGroup group) {
386 mPreferenceCache = new ArrayMap<String, Preference>();
387 final int N = group.getPreferenceCount();
388 for (int i = 0; i < N; i++) {
389 Preference p = group.getPreference(i);
390 if (TextUtils.isEmpty(p.getKey())) {
393 mPreferenceCache.put(p.getKey(), p);
397 protected Preference getCachedPreference(String key) {
398 return mPreferenceCache != null ? mPreferenceCache.remove(key) : null;
401 protected void removeCachedPrefs(PreferenceGroup group) {
402 for (Preference p : mPreferenceCache.values()) {
403 group.removePreference(p);
405 mPreferenceCache = null;
408 protected int getCachedCount() {
409 return mPreferenceCache != null ? mPreferenceCache.size() : 0;
412 private void highlightPreference(String key) {
413 final int position = canUseListViewForHighLighting(key);
418 mPreferenceHighlighted = true;
419 mLayoutManager.scrollToPosition(position);
420 mAdapter.highlight(position);
423 private int findListPositionFromKey(PreferenceGroupAdapter adapter, String key) {
424 final int count = adapter.getItemCount();
425 for (int n = 0; n < count; n++) {
426 final Preference preference = adapter.getItem(n);
427 final String preferenceKey = preference.getKey();
428 if (preferenceKey != null && preferenceKey.equals(key)) {
435 protected boolean removePreference(String key) {
436 return removePreference(getPreferenceScreen(), key);
440 boolean removePreference(PreferenceGroup group, String key) {
441 final int preferenceCount = group.getPreferenceCount();
442 for (int i = 0; i < preferenceCount; i++) {
443 final Preference preference = group.getPreference(i);
444 final String curKey = preference.getKey();
446 if (TextUtils.equals(curKey, key)) {
447 return group.removePreference(preference);
450 if (preference instanceof PreferenceGroup) {
451 if (removePreference((PreferenceGroup) preference, key)) {
460 * Override this if you want to show a help item in the menu, by returning the resource id.
461 * @return the resource id for the help url
463 protected int getHelpResource() {
464 return R.string.help_uri_default;
468 public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
469 super.onCreateOptionsMenu(menu, inflater);
470 if (mHelpUri != null && getActivity() != null) {
471 HelpUtils.prepareHelpMenuItem(getActivity(), menu, mHelpUri, getClass().getName());
476 * The name is intentionally made different from Activity#finish(), so that
477 * users won't misunderstand its meaning.
479 public final void finishFragment() {
480 getActivity().onBackPressed();
483 // Some helpers for functions used by the settings fragments when they were activities
486 * Returns the ContentResolver from the owning Activity.
488 protected ContentResolver getContentResolver() {
489 Context context = getActivity();
490 if (context != null) {
491 mContentResolver = context.getContentResolver();
493 return mContentResolver;
497 * Returns the specified system service from the owning Activity.
499 protected Object getSystemService(final String name) {
500 return getActivity().getSystemService(name);
504 * Returns the PackageManager from the owning Activity.
506 protected PackageManager getPackageManager() {
507 return getActivity().getPackageManager();
511 public void onDetach() {
513 if (mDialogFragment != null) {
514 mDialogFragment.dismiss();
515 mDialogFragment = null;
523 protected void showDialog(int dialogId) {
524 if (mDialogFragment != null) {
525 Log.e(TAG, "Old dialog fragment not null!");
527 mDialogFragment = new SettingsDialogFragment(this, dialogId);
528 mDialogFragment.show(getChildFragmentManager(), Integer.toString(dialogId));
532 public Dialog onCreateDialog(int dialogId) {
537 public int getDialogMetricsCategory(int dialogId) {
541 protected void removeDialog(int dialogId) {
542 // mDialogFragment may not be visible yet in parent fragment's onResume().
543 // To be able to dismiss dialog at that time, don't check
544 // mDialogFragment.isVisible().
545 if (mDialogFragment != null && mDialogFragment.getDialogId() == dialogId) {
546 mDialogFragment.dismissAllowingStateLoss();
548 mDialogFragment = null;
552 * Sets the OnCancelListener of the dialog shown. This method can only be
553 * called after showDialog(int) and before removeDialog(int). The method
554 * does nothing otherwise.
556 protected void setOnCancelListener(DialogInterface.OnCancelListener listener) {
557 if (mDialogFragment != null) {
558 mDialogFragment.mOnCancelListener = listener;
563 * Sets the OnDismissListener of the dialog shown. This method can only be
564 * called after showDialog(int) and before removeDialog(int). The method
565 * does nothing otherwise.
567 protected void setOnDismissListener(DialogInterface.OnDismissListener listener) {
568 if (mDialogFragment != null) {
569 mDialogFragment.mOnDismissListener = listener;
573 public void onDialogShowing() {
574 // override in subclass to attach a dismiss listener, for instance
578 public void onDisplayPreferenceDialog(Preference preference) {
579 if (preference.getKey() == null) {
580 // Auto-key preferences that don't have a key, so the dialog can find them.
581 preference.setKey(UUID.randomUUID().toString());
583 DialogFragment f = null;
584 if (preference instanceof RestrictedListPreference) {
585 f = RestrictedListPreference.RestrictedListPreferenceDialogFragment
586 .newInstance(preference.getKey());
587 } else if (preference instanceof CustomListPreference) {
588 f = CustomListPreference.CustomListPreferenceDialogFragment
589 .newInstance(preference.getKey());
590 } else if (preference instanceof CustomDialogPreference) {
591 f = CustomDialogPreference.CustomPreferenceDialogFragment
592 .newInstance(preference.getKey());
593 } else if (preference instanceof CustomEditTextPreference) {
594 f = CustomEditTextPreference.CustomPreferenceDialogFragment
595 .newInstance(preference.getKey());
597 super.onDisplayPreferenceDialog(preference);
600 f.setTargetFragment(this, 0);
601 f.show(getFragmentManager(), "dialog_preference");
605 public static class SettingsDialogFragment extends InstrumentedDialogFragment {
606 private static final String KEY_DIALOG_ID = "key_dialog_id";
607 private static final String KEY_PARENT_FRAGMENT_ID = "key_parent_fragment_id";
609 private Fragment mParentFragment;
611 private DialogInterface.OnCancelListener mOnCancelListener;
612 private DialogInterface.OnDismissListener mOnDismissListener;
614 public SettingsDialogFragment() {
618 public SettingsDialogFragment(DialogCreatable fragment, int dialogId) {
619 super(fragment, dialogId);
620 if (!(fragment instanceof Fragment)) {
621 throw new IllegalArgumentException("fragment argument must be an instance of "
622 + Fragment.class.getName());
624 mParentFragment = (Fragment) fragment;
629 public int getMetricsCategory() {
630 if (mDialogCreatable == null) {
631 return Instrumentable.METRICS_CATEGORY_UNKNOWN;
633 final int metricsCategory = mDialogCreatable.getDialogMetricsCategory(mDialogId);
634 if (metricsCategory <= 0) {
635 throw new IllegalStateException("Dialog must provide a metrics category");
637 return metricsCategory;
641 public void onSaveInstanceState(Bundle outState) {
642 super.onSaveInstanceState(outState);
643 if (mParentFragment != null) {
644 outState.putInt(KEY_DIALOG_ID, mDialogId);
645 outState.putInt(KEY_PARENT_FRAGMENT_ID, mParentFragment.getId());
650 public void onStart() {
653 if (mParentFragment != null && mParentFragment instanceof SettingsPreferenceFragment) {
654 ((SettingsPreferenceFragment) mParentFragment).onDialogShowing();
659 public Dialog onCreateDialog(Bundle savedInstanceState) {
660 if (savedInstanceState != null) {
661 mDialogId = savedInstanceState.getInt(KEY_DIALOG_ID, 0);
662 mParentFragment = getParentFragment();
663 int mParentFragmentId = savedInstanceState.getInt(KEY_PARENT_FRAGMENT_ID, -1);
664 if (mParentFragment == null) {
665 mParentFragment = getFragmentManager().findFragmentById(mParentFragmentId);
667 if (!(mParentFragment instanceof DialogCreatable)) {
668 throw new IllegalArgumentException(
669 (mParentFragment != null
670 ? mParentFragment.getClass().getName()
673 + DialogCreatable.class.getName());
675 // This dialog fragment could be created from non-SettingsPreferenceFragment
676 if (mParentFragment instanceof SettingsPreferenceFragment) {
677 // restore mDialogFragment in mParentFragment
678 ((SettingsPreferenceFragment) mParentFragment).mDialogFragment = this;
681 return ((DialogCreatable) mParentFragment).onCreateDialog(mDialogId);
685 public void onCancel(DialogInterface dialog) {
686 super.onCancel(dialog);
687 if (mOnCancelListener != null) {
688 mOnCancelListener.onCancel(dialog);
693 public void onDismiss(DialogInterface dialog) {
694 super.onDismiss(dialog);
695 if (mOnDismissListener != null) {
696 mOnDismissListener.onDismiss(dialog);
700 public int getDialogId() {
705 public void onDetach() {
708 // This dialog fragment could be created from non-SettingsPreferenceFragment
709 if (mParentFragment instanceof SettingsPreferenceFragment) {
710 // in case the dialog is not explicitly removed by removeDialog()
711 if (((SettingsPreferenceFragment) mParentFragment).mDialogFragment == this) {
712 ((SettingsPreferenceFragment) mParentFragment).mDialogFragment = null;
718 protected boolean hasNextButton() {
719 return ((ButtonBarHandler)getActivity()).hasNextButton();
722 protected Button getNextButton() {
723 return ((ButtonBarHandler)getActivity()).getNextButton();
726 public void finish() {
727 Activity activity = getActivity();
728 if (activity == null) return;
729 if (getFragmentManager().getBackStackEntryCount() > 0) {
730 getFragmentManager().popBackStack();
736 protected Intent getIntent() {
737 if (getActivity() == null) {
740 return getActivity().getIntent();
743 protected void setResult(int result, Intent intent) {
744 if (getActivity() == null) {
747 getActivity().setResult(result, intent);
750 protected void setResult(int result) {
751 if (getActivity() == null) {
754 getActivity().setResult(result);
757 public boolean startFragment(Fragment caller, String fragmentClass, int titleRes,
758 int requestCode, Bundle extras) {
759 final Activity activity = getActivity();
760 if (activity instanceof SettingsActivity) {
761 SettingsActivity sa = (SettingsActivity) activity;
762 sa.startPreferencePanel(
763 caller, fragmentClass, extras, titleRes, null, caller, requestCode);
767 "Parent isn't SettingsActivity nor PreferenceActivity, thus there's no way to "
768 + "launch the given Fragment (name: " + fragmentClass
769 + ", requestCode: " + requestCode + ")");
774 public static class HighlightablePreferenceGroupAdapter extends PreferenceGroupAdapter {
776 @VisibleForTesting(otherwise=VisibleForTesting.NONE)
777 int initialHighlightedPosition = -1;
779 private int mHighlightPosition = -1;
781 public HighlightablePreferenceGroupAdapter(PreferenceGroup preferenceGroup) {
782 super(preferenceGroup);
785 public void highlight(int position) {
786 mHighlightPosition = position;
787 initialHighlightedPosition = position;
788 notifyDataSetChanged();
792 public void onBindViewHolder(PreferenceViewHolder holder, int position) {
793 super.onBindViewHolder(holder, position);
794 if (position == mHighlightPosition) {
795 View v = holder.itemView;
797 if (v.getBackground() != null) {
798 final int centerX = v.getWidth() / 2;
799 final int centerY = v.getHeight() / 2;
800 v.getBackground().setHotspot(centerX, centerY);
804 mHighlightPosition = -1;