1/*
2 * Copyright (C) 2010 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
17package com.android.settings;
18
19import android.app.Activity;
20import android.app.Dialog;
21import android.app.DialogFragment;
22import android.app.Fragment;
23import android.content.ContentResolver;
24import android.content.Context;
25import android.content.DialogInterface;
26import android.content.pm.PackageManager;
27import android.database.DataSetObserver;
28import android.graphics.drawable.Drawable;
29import android.os.Bundle;
30import android.preference.Preference;
31import android.preference.PreferenceActivity;
32import android.preference.PreferenceGroupAdapter;
33import android.text.TextUtils;
34import android.util.Log;
35import android.view.LayoutInflater;
36import android.view.Menu;
37import android.view.MenuInflater;
38import android.view.MenuItem;
39import android.view.View;
40import android.view.ViewGroup;
41import android.widget.Button;
42import android.widget.ListAdapter;
43import android.widget.ListView;
44
45import com.android.settings.widget.FloatingActionButton;
46
47/**
48 * Base class for Settings fragments, with some helper functions and dialog management.
49 */
50public abstract class SettingsPreferenceFragment extends InstrumentedPreferenceFragment
51        implements DialogCreatable {
52
53    private static final String TAG = "SettingsPreferenceFragment";
54
55    private static final int DELAY_HIGHLIGHT_DURATION_MILLIS = 600;
56
57    private static final String SAVE_HIGHLIGHTED_KEY = "android:preference_highlighted";
58
59    private SettingsDialogFragment mDialogFragment;
60
61    private String mHelpUri;
62
63    // Cache the content resolver for async callbacks
64    private ContentResolver mContentResolver;
65
66    private String mPreferenceKey;
67    private boolean mPreferenceHighlighted = false;
68    private Drawable mHighlightDrawable;
69
70    private ListAdapter mCurrentRootAdapter;
71    private boolean mIsDataSetObserverRegistered = false;
72    private DataSetObserver mDataSetObserver = new DataSetObserver() {
73        @Override
74        public void onChanged() {
75            highlightPreferenceIfNeeded();
76        }
77
78        @Override
79        public void onInvalidated() {
80            highlightPreferenceIfNeeded();
81        }
82    };
83
84    private ViewGroup mPinnedHeaderFrameLayout;
85    private FloatingActionButton mFloatingActionButton;
86
87    @Override
88    public void onCreate(Bundle icicle) {
89        super.onCreate(icicle);
90
91        if (icicle != null) {
92            mPreferenceHighlighted = icicle.getBoolean(SAVE_HIGHLIGHTED_KEY);
93        }
94
95        // Prepare help url and enable menu if necessary
96        int helpResource = getHelpResource();
97        if (helpResource != 0) {
98            mHelpUri = getResources().getString(helpResource);
99        }
100    }
101
102    @Override
103    public View onCreateView(LayoutInflater inflater, ViewGroup container,
104            Bundle savedInstanceState) {
105        final View root = super.onCreateView(inflater, container, savedInstanceState);
106        mPinnedHeaderFrameLayout = (ViewGroup) root.findViewById(R.id.pinned_header);
107        mFloatingActionButton = (FloatingActionButton) root.findViewById(R.id.fab);
108        return root;
109    }
110
111    public FloatingActionButton getFloatingActionButton() {
112        return mFloatingActionButton;
113    }
114
115    public View setPinnedHeaderView(int layoutResId) {
116        final LayoutInflater inflater = getActivity().getLayoutInflater();
117        final View pinnedHeader =
118                inflater.inflate(layoutResId, mPinnedHeaderFrameLayout, false);
119        setPinnedHeaderView(pinnedHeader);
120        return pinnedHeader;
121    }
122
123    public void setPinnedHeaderView(View pinnedHeader) {
124        mPinnedHeaderFrameLayout.addView(pinnedHeader);
125        mPinnedHeaderFrameLayout.setVisibility(View.VISIBLE);
126    }
127
128    @Override
129    public void onSaveInstanceState(Bundle outState) {
130        super.onSaveInstanceState(outState);
131
132        outState.putBoolean(SAVE_HIGHLIGHTED_KEY, mPreferenceHighlighted);
133    }
134
135    @Override
136    public void onActivityCreated(Bundle savedInstanceState) {
137        super.onActivityCreated(savedInstanceState);
138        if (!TextUtils.isEmpty(mHelpUri)) {
139            setHasOptionsMenu(true);
140        }
141    }
142
143    @Override
144    public void onResume() {
145        super.onResume();
146
147        final Bundle args = getArguments();
148        if (args != null) {
149            mPreferenceKey = args.getString(SettingsActivity.EXTRA_FRAGMENT_ARG_KEY);
150            highlightPreferenceIfNeeded();
151        }
152    }
153
154    @Override
155    protected void onBindPreferences() {
156        registerObserverIfNeeded();
157    }
158
159    @Override
160    protected void onUnbindPreferences() {
161        unregisterObserverIfNeeded();
162    }
163
164    @Override
165    public void onStop() {
166        super.onStop();
167
168        unregisterObserverIfNeeded();
169    }
170
171    public void showLoadingWhenEmpty() {
172        View loading = getView().findViewById(R.id.loading_container);
173        getListView().setEmptyView(loading);
174    }
175
176    public void registerObserverIfNeeded() {
177        if (!mIsDataSetObserverRegistered) {
178            if (mCurrentRootAdapter != null) {
179                mCurrentRootAdapter.unregisterDataSetObserver(mDataSetObserver);
180            }
181            mCurrentRootAdapter = getPreferenceScreen().getRootAdapter();
182            mCurrentRootAdapter.registerDataSetObserver(mDataSetObserver);
183            mIsDataSetObserverRegistered = true;
184        }
185    }
186
187    public void unregisterObserverIfNeeded() {
188        if (mIsDataSetObserverRegistered) {
189            if (mCurrentRootAdapter != null) {
190                mCurrentRootAdapter.unregisterDataSetObserver(mDataSetObserver);
191                mCurrentRootAdapter = null;
192            }
193            mIsDataSetObserverRegistered = false;
194        }
195    }
196
197    public void highlightPreferenceIfNeeded() {
198        if (isAdded() && !mPreferenceHighlighted &&!TextUtils.isEmpty(mPreferenceKey)) {
199            highlightPreference(mPreferenceKey);
200        }
201    }
202
203    private Drawable getHighlightDrawable() {
204        if (mHighlightDrawable == null) {
205            mHighlightDrawable = getActivity().getDrawable(R.drawable.preference_highlight);
206        }
207        return mHighlightDrawable;
208    }
209
210    /**
211     * Return a valid ListView position or -1 if none is found
212     */
213    private int canUseListViewForHighLighting(String key) {
214        if (!hasListView()) {
215            return -1;
216        }
217
218        ListView listView = getListView();
219        ListAdapter adapter = listView.getAdapter();
220
221        if (adapter != null && adapter instanceof PreferenceGroupAdapter) {
222            return findListPositionFromKey(adapter, key);
223        }
224
225        return -1;
226    }
227
228    private void highlightPreference(String key) {
229        final Drawable highlight = getHighlightDrawable();
230
231        final int position = canUseListViewForHighLighting(key);
232        if (position >= 0) {
233            mPreferenceHighlighted = true;
234
235            final ListView listView = getListView();
236            final ListAdapter adapter = listView.getAdapter();
237
238            ((PreferenceGroupAdapter) adapter).setHighlightedDrawable(highlight);
239            ((PreferenceGroupAdapter) adapter).setHighlighted(position);
240
241            listView.post(new Runnable() {
242                @Override
243                public void run() {
244                    listView.setSelection(position);
245                    listView.postDelayed(new Runnable() {
246                        @Override
247                        public void run() {
248                            final int index = position - listView.getFirstVisiblePosition();
249                            if (index >= 0 && index < listView.getChildCount()) {
250                                final View v = listView.getChildAt(index);
251                                final int centerX = v.getWidth() / 2;
252                                final int centerY = v.getHeight() / 2;
253                                highlight.setHotspot(centerX, centerY);
254                                v.setPressed(true);
255                                v.setPressed(false);
256                            }
257                        }
258                    }, DELAY_HIGHLIGHT_DURATION_MILLIS);
259                }
260            });
261        }
262    }
263
264    private int findListPositionFromKey(ListAdapter adapter, String key) {
265        final int count = adapter.getCount();
266        for (int n = 0; n < count; n++) {
267            final Object item = adapter.getItem(n);
268            if (item instanceof Preference) {
269                Preference preference = (Preference) item;
270                final String preferenceKey = preference.getKey();
271                if (preferenceKey != null && preferenceKey.equals(key)) {
272                    return n;
273                }
274            }
275        }
276        return -1;
277    }
278
279    protected void removePreference(String key) {
280        Preference pref = findPreference(key);
281        if (pref != null) {
282            getPreferenceScreen().removePreference(pref);
283        }
284    }
285
286    /**
287     * Override this if you want to show a help item in the menu, by returning the resource id.
288     * @return the resource id for the help url
289     */
290    protected int getHelpResource() {
291        return R.string.help_uri_default;
292    }
293
294    @Override
295    public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
296        if (mHelpUri != null && getActivity() != null) {
297            HelpUtils.prepareHelpMenuItem(getActivity(), menu, mHelpUri, getClass().getName());
298        }
299    }
300
301    /*
302     * The name is intentionally made different from Activity#finish(), so that
303     * users won't misunderstand its meaning.
304     */
305    public final void finishFragment() {
306        getActivity().onBackPressed();
307    }
308
309    // Some helpers for functions used by the settings fragments when they were activities
310
311    /**
312     * Returns the ContentResolver from the owning Activity.
313     */
314    protected ContentResolver getContentResolver() {
315        Context context = getActivity();
316        if (context != null) {
317            mContentResolver = context.getContentResolver();
318        }
319        return mContentResolver;
320    }
321
322    /**
323     * Returns the specified system service from the owning Activity.
324     */
325    protected Object getSystemService(final String name) {
326        return getActivity().getSystemService(name);
327    }
328
329    /**
330     * Returns the PackageManager from the owning Activity.
331     */
332    protected PackageManager getPackageManager() {
333        return getActivity().getPackageManager();
334    }
335
336    @Override
337    public void onDetach() {
338        if (isRemoving()) {
339            if (mDialogFragment != null) {
340                mDialogFragment.dismiss();
341                mDialogFragment = null;
342            }
343        }
344        super.onDetach();
345    }
346
347    // Dialog management
348
349    protected void showDialog(int dialogId) {
350        if (mDialogFragment != null) {
351            Log.e(TAG, "Old dialog fragment not null!");
352        }
353        mDialogFragment = new SettingsDialogFragment(this, dialogId);
354        mDialogFragment.show(getChildFragmentManager(), Integer.toString(dialogId));
355    }
356
357    public Dialog onCreateDialog(int dialogId) {
358        return null;
359    }
360
361    protected void removeDialog(int dialogId) {
362        // mDialogFragment may not be visible yet in parent fragment's onResume().
363        // To be able to dismiss dialog at that time, don't check
364        // mDialogFragment.isVisible().
365        if (mDialogFragment != null && mDialogFragment.getDialogId() == dialogId) {
366            mDialogFragment.dismiss();
367        }
368        mDialogFragment = null;
369    }
370
371    /**
372     * Sets the OnCancelListener of the dialog shown. This method can only be
373     * called after showDialog(int) and before removeDialog(int). The method
374     * does nothing otherwise.
375     */
376    protected void setOnCancelListener(DialogInterface.OnCancelListener listener) {
377        if (mDialogFragment != null) {
378            mDialogFragment.mOnCancelListener = listener;
379        }
380    }
381
382    /**
383     * Sets the OnDismissListener of the dialog shown. This method can only be
384     * called after showDialog(int) and before removeDialog(int). The method
385     * does nothing otherwise.
386     */
387    protected void setOnDismissListener(DialogInterface.OnDismissListener listener) {
388        if (mDialogFragment != null) {
389            mDialogFragment.mOnDismissListener = listener;
390        }
391    }
392
393    public void onDialogShowing() {
394        // override in subclass to attach a dismiss listener, for instance
395    }
396
397    public static class SettingsDialogFragment extends DialogFragment {
398        private static final String KEY_DIALOG_ID = "key_dialog_id";
399        private static final String KEY_PARENT_FRAGMENT_ID = "key_parent_fragment_id";
400
401        private int mDialogId;
402
403        private Fragment mParentFragment;
404
405        private DialogInterface.OnCancelListener mOnCancelListener;
406        private DialogInterface.OnDismissListener mOnDismissListener;
407
408        public SettingsDialogFragment() {
409            /* do nothing */
410        }
411
412        public SettingsDialogFragment(DialogCreatable fragment, int dialogId) {
413            mDialogId = dialogId;
414            if (!(fragment instanceof Fragment)) {
415                throw new IllegalArgumentException("fragment argument must be an instance of "
416                        + Fragment.class.getName());
417            }
418            mParentFragment = (Fragment) fragment;
419        }
420
421        @Override
422        public void onSaveInstanceState(Bundle outState) {
423            super.onSaveInstanceState(outState);
424            if (mParentFragment != null) {
425                outState.putInt(KEY_DIALOG_ID, mDialogId);
426                outState.putInt(KEY_PARENT_FRAGMENT_ID, mParentFragment.getId());
427            }
428        }
429
430        @Override
431        public void onStart() {
432            super.onStart();
433
434            if (mParentFragment != null && mParentFragment instanceof SettingsPreferenceFragment) {
435                ((SettingsPreferenceFragment) mParentFragment).onDialogShowing();
436            }
437        }
438
439        @Override
440        public Dialog onCreateDialog(Bundle savedInstanceState) {
441            if (savedInstanceState != null) {
442                mDialogId = savedInstanceState.getInt(KEY_DIALOG_ID, 0);
443                mParentFragment = getParentFragment();
444                int mParentFragmentId = savedInstanceState.getInt(KEY_PARENT_FRAGMENT_ID, -1);
445                if (mParentFragment == null) {
446                    mParentFragment = getFragmentManager().findFragmentById(mParentFragmentId);
447                }
448                if (!(mParentFragment instanceof DialogCreatable)) {
449                    throw new IllegalArgumentException(
450                            (mParentFragment != null
451                                    ? mParentFragment.getClass().getName()
452                                    : mParentFragmentId)
453                                    + " must implement "
454                                    + DialogCreatable.class.getName());
455                }
456                // This dialog fragment could be created from non-SettingsPreferenceFragment
457                if (mParentFragment instanceof SettingsPreferenceFragment) {
458                    // restore mDialogFragment in mParentFragment
459                    ((SettingsPreferenceFragment) mParentFragment).mDialogFragment = this;
460                }
461            }
462            return ((DialogCreatable) mParentFragment).onCreateDialog(mDialogId);
463        }
464
465        @Override
466        public void onCancel(DialogInterface dialog) {
467            super.onCancel(dialog);
468            if (mOnCancelListener != null) {
469                mOnCancelListener.onCancel(dialog);
470            }
471        }
472
473        @Override
474        public void onDismiss(DialogInterface dialog) {
475            super.onDismiss(dialog);
476            if (mOnDismissListener != null) {
477                mOnDismissListener.onDismiss(dialog);
478            }
479        }
480
481        public int getDialogId() {
482            return mDialogId;
483        }
484
485        @Override
486        public void onDetach() {
487            super.onDetach();
488
489            // This dialog fragment could be created from non-SettingsPreferenceFragment
490            if (mParentFragment instanceof SettingsPreferenceFragment) {
491                // in case the dialog is not explicitly removed by removeDialog()
492                if (((SettingsPreferenceFragment) mParentFragment).mDialogFragment == this) {
493                    ((SettingsPreferenceFragment) mParentFragment).mDialogFragment = null;
494                }
495            }
496        }
497    }
498
499    protected boolean hasNextButton() {
500        return ((ButtonBarHandler)getActivity()).hasNextButton();
501    }
502
503    protected Button getNextButton() {
504        return ((ButtonBarHandler)getActivity()).getNextButton();
505    }
506
507    public void finish() {
508        getActivity().onBackPressed();
509    }
510
511    public boolean startFragment(Fragment caller, String fragmentClass, int titleRes,
512            int requestCode, Bundle extras) {
513        final Activity activity = getActivity();
514        if (activity instanceof SettingsActivity) {
515            SettingsActivity sa = (SettingsActivity) activity;
516            sa.startPreferencePanel(fragmentClass, extras, titleRes, null, caller, requestCode);
517            return true;
518        } else if (activity instanceof PreferenceActivity) {
519            PreferenceActivity sa = (PreferenceActivity) activity;
520            sa.startPreferencePanel(fragmentClass, extras, titleRes, null, caller, requestCode);
521            return true;
522        } else {
523            Log.w(TAG,
524                    "Parent isn't SettingsActivity nor PreferenceActivity, thus there's no way to "
525                    + "launch the given Fragment (name: " + fragmentClass
526                    + ", requestCode: " + requestCode + ")");
527            return false;
528        }
529    }
530}
531