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.PreferenceGroupAdapter; 33 import android.text.TextUtils; 34 import android.util.Log; 35 import android.view.LayoutInflater; 36 import android.view.Menu; 37 import android.view.MenuInflater; 38 import android.view.MenuItem; 39 import android.view.View; 40 import android.view.ViewGroup; 41 import android.widget.Button; 42 import android.widget.ListAdapter; 43 import android.widget.ListView; 44 45 import com.android.settings.widget.FloatingActionButton; 46 47 /** 48 * Base class for Settings fragments, with some helper functions and dialog management. 49 */ 50 public 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 onCreate(Bundle icicle)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 onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState)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 getFloatingActionButton()111 public FloatingActionButton getFloatingActionButton() { 112 return mFloatingActionButton; 113 } 114 setPinnedHeaderView(int layoutResId)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 setPinnedHeaderView(View pinnedHeader)123 public void setPinnedHeaderView(View pinnedHeader) { 124 mPinnedHeaderFrameLayout.addView(pinnedHeader); 125 mPinnedHeaderFrameLayout.setVisibility(View.VISIBLE); 126 } 127 128 @Override onSaveInstanceState(Bundle outState)129 public void onSaveInstanceState(Bundle outState) { 130 super.onSaveInstanceState(outState); 131 132 outState.putBoolean(SAVE_HIGHLIGHTED_KEY, mPreferenceHighlighted); 133 } 134 135 @Override onActivityCreated(Bundle savedInstanceState)136 public void onActivityCreated(Bundle savedInstanceState) { 137 super.onActivityCreated(savedInstanceState); 138 if (!TextUtils.isEmpty(mHelpUri)) { 139 setHasOptionsMenu(true); 140 } 141 } 142 143 @Override onResume()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 onBindPreferences()155 protected void onBindPreferences() { 156 registerObserverIfNeeded(); 157 } 158 159 @Override onUnbindPreferences()160 protected void onUnbindPreferences() { 161 unregisterObserverIfNeeded(); 162 } 163 164 @Override onStop()165 public void onStop() { 166 super.onStop(); 167 168 unregisterObserverIfNeeded(); 169 } 170 showLoadingWhenEmpty()171 public void showLoadingWhenEmpty() { 172 View loading = getView().findViewById(R.id.loading_container); 173 getListView().setEmptyView(loading); 174 } 175 registerObserverIfNeeded()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 unregisterObserverIfNeeded()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 highlightPreferenceIfNeeded()197 public void highlightPreferenceIfNeeded() { 198 if (isAdded() && !mPreferenceHighlighted &&!TextUtils.isEmpty(mPreferenceKey)) { 199 highlightPreference(mPreferenceKey); 200 } 201 } 202 getHighlightDrawable()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 */ canUseListViewForHighLighting(String key)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 highlightPreference(String key)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 findListPositionFromKey(ListAdapter adapter, String key)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 removePreference(String key)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 */ getHelpResource()290 protected int getHelpResource() { 291 return R.string.help_uri_default; 292 } 293 294 @Override onCreateOptionsMenu(Menu menu, MenuInflater inflater)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 */ finishFragment()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 */ getContentResolver()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 */ getSystemService(final String name)325 protected Object getSystemService(final String name) { 326 return getActivity().getSystemService(name); 327 } 328 329 /** 330 * Returns the PackageManager from the owning Activity. 331 */ getPackageManager()332 protected PackageManager getPackageManager() { 333 return getActivity().getPackageManager(); 334 } 335 336 @Override onDetach()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 showDialog(int dialogId)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 onCreateDialog(int dialogId)357 public Dialog onCreateDialog(int dialogId) { 358 return null; 359 } 360 removeDialog(int dialogId)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 */ setOnCancelListener(DialogInterface.OnCancelListener listener)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 */ setOnDismissListener(DialogInterface.OnDismissListener listener)387 protected void setOnDismissListener(DialogInterface.OnDismissListener listener) { 388 if (mDialogFragment != null) { 389 mDialogFragment.mOnDismissListener = listener; 390 } 391 } 392 onDialogShowing()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 SettingsDialogFragment()408 public SettingsDialogFragment() { 409 /* do nothing */ 410 } 411 SettingsDialogFragment(DialogCreatable fragment, int dialogId)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 onSaveInstanceState(Bundle outState)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 onStart()431 public void onStart() { 432 super.onStart(); 433 434 if (mParentFragment != null && mParentFragment instanceof SettingsPreferenceFragment) { 435 ((SettingsPreferenceFragment) mParentFragment).onDialogShowing(); 436 } 437 } 438 439 @Override onCreateDialog(Bundle savedInstanceState)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 onCancel(DialogInterface dialog)466 public void onCancel(DialogInterface dialog) { 467 super.onCancel(dialog); 468 if (mOnCancelListener != null) { 469 mOnCancelListener.onCancel(dialog); 470 } 471 } 472 473 @Override onDismiss(DialogInterface dialog)474 public void onDismiss(DialogInterface dialog) { 475 super.onDismiss(dialog); 476 if (mOnDismissListener != null) { 477 mOnDismissListener.onDismiss(dialog); 478 } 479 } 480 getDialogId()481 public int getDialogId() { 482 return mDialogId; 483 } 484 485 @Override onDetach()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 hasNextButton()499 protected boolean hasNextButton() { 500 return ((ButtonBarHandler)getActivity()).hasNextButton(); 501 } 502 getNextButton()503 protected Button getNextButton() { 504 return ((ButtonBarHandler)getActivity()).getNextButton(); 505 } 506 finish()507 public void finish() { 508 Activity activity = getActivity(); 509 if (activity != null) { 510 activity.onBackPressed(); 511 } 512 } 513 startFragment(Fragment caller, String fragmentClass, int titleRes, int requestCode, Bundle extras)514 public boolean startFragment(Fragment caller, String fragmentClass, int titleRes, 515 int requestCode, Bundle extras) { 516 final Activity activity = getActivity(); 517 if (activity instanceof SettingsActivity) { 518 SettingsActivity sa = (SettingsActivity) activity; 519 sa.startPreferencePanel(fragmentClass, extras, titleRes, null, caller, requestCode); 520 return true; 521 } else if (activity instanceof PreferenceActivity) { 522 PreferenceActivity sa = (PreferenceActivity) activity; 523 sa.startPreferencePanel(fragmentClass, extras, titleRes, null, caller, requestCode); 524 return true; 525 } else { 526 Log.w(TAG, 527 "Parent isn't SettingsActivity nor PreferenceActivity, thus there's no way to " 528 + "launch the given Fragment (name: " + fragmentClass 529 + ", requestCode: " + requestCode + ")"); 530 return false; 531 } 532 } 533 } 534