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