1 /* 2 * Copyright 2018 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 androidx.preference; 18 19 import static androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP; 20 21 import android.app.Dialog; 22 import android.content.Context; 23 import android.content.DialogInterface; 24 import android.graphics.Bitmap; 25 import android.graphics.Canvas; 26 import android.graphics.drawable.BitmapDrawable; 27 import android.graphics.drawable.Drawable; 28 import android.os.Bundle; 29 import android.text.TextUtils; 30 import android.view.LayoutInflater; 31 import android.view.View; 32 import android.view.Window; 33 import android.view.WindowManager; 34 import android.widget.TextView; 35 36 import androidx.annotation.LayoutRes; 37 import androidx.annotation.NonNull; 38 import androidx.annotation.RestrictTo; 39 import androidx.appcompat.app.AlertDialog; 40 import androidx.fragment.app.DialogFragment; 41 import androidx.fragment.app.Fragment; 42 43 /** 44 * Abstract base class which presents a dialog associated with a 45 * {@link androidx.preference.DialogPreference}. Since the preference object may 46 * not be available during fragment re-creation, the necessary information for displaying the dialog 47 * is read once during the initial call to {@link #onCreate(Bundle)} and saved/restored in the saved 48 * instance state. Custom subclasses should also follow this pattern. 49 */ 50 public abstract class PreferenceDialogFragmentCompat extends DialogFragment implements 51 DialogInterface.OnClickListener { 52 53 protected static final String ARG_KEY = "key"; 54 55 private static final String SAVE_STATE_TITLE = "PreferenceDialogFragment.title"; 56 private static final String SAVE_STATE_POSITIVE_TEXT = "PreferenceDialogFragment.positiveText"; 57 private static final String SAVE_STATE_NEGATIVE_TEXT = "PreferenceDialogFragment.negativeText"; 58 private static final String SAVE_STATE_MESSAGE = "PreferenceDialogFragment.message"; 59 private static final String SAVE_STATE_LAYOUT = "PreferenceDialogFragment.layout"; 60 private static final String SAVE_STATE_ICON = "PreferenceDialogFragment.icon"; 61 62 private DialogPreference mPreference; 63 64 private CharSequence mDialogTitle; 65 private CharSequence mPositiveButtonText; 66 private CharSequence mNegativeButtonText; 67 private CharSequence mDialogMessage; 68 private @LayoutRes int mDialogLayoutRes; 69 70 private BitmapDrawable mDialogIcon; 71 72 /** Which button was clicked. */ 73 private int mWhichButtonClicked; 74 75 @Override 76 public void onCreate(Bundle savedInstanceState) { 77 super.onCreate(savedInstanceState); 78 79 final Fragment rawFragment = getTargetFragment(); 80 if (!(rawFragment instanceof DialogPreference.TargetFragment)) { 81 throw new IllegalStateException("Target fragment must implement TargetFragment" + 82 " interface"); 83 } 84 85 final DialogPreference.TargetFragment fragment = 86 (DialogPreference.TargetFragment) rawFragment; 87 88 final String key = getArguments().getString(ARG_KEY); 89 if (savedInstanceState == null) { 90 mPreference = (DialogPreference) fragment.findPreference(key); 91 mDialogTitle = mPreference.getDialogTitle(); 92 mPositiveButtonText = mPreference.getPositiveButtonText(); 93 mNegativeButtonText = mPreference.getNegativeButtonText(); 94 mDialogMessage = mPreference.getDialogMessage(); 95 mDialogLayoutRes = mPreference.getDialogLayoutResource(); 96 97 final Drawable icon = mPreference.getDialogIcon(); 98 if (icon == null || icon instanceof BitmapDrawable) { 99 mDialogIcon = (BitmapDrawable) icon; 100 } else { 101 final Bitmap bitmap = Bitmap.createBitmap(icon.getIntrinsicWidth(), 102 icon.getIntrinsicHeight(), Bitmap.Config.ARGB_8888); 103 final Canvas canvas = new Canvas(bitmap); 104 icon.setBounds(0, 0, canvas.getWidth(), canvas.getHeight()); 105 icon.draw(canvas); 106 mDialogIcon = new BitmapDrawable(getResources(), bitmap); 107 } 108 } else { 109 mDialogTitle = savedInstanceState.getCharSequence(SAVE_STATE_TITLE); 110 mPositiveButtonText = savedInstanceState.getCharSequence(SAVE_STATE_POSITIVE_TEXT); 111 mNegativeButtonText = savedInstanceState.getCharSequence(SAVE_STATE_NEGATIVE_TEXT); 112 mDialogMessage = savedInstanceState.getCharSequence(SAVE_STATE_MESSAGE); 113 mDialogLayoutRes = savedInstanceState.getInt(SAVE_STATE_LAYOUT, 0); 114 final Bitmap bitmap = savedInstanceState.getParcelable(SAVE_STATE_ICON); 115 if (bitmap != null) { 116 mDialogIcon = new BitmapDrawable(getResources(), bitmap); 117 } 118 } 119 } 120 121 @Override 122 public void onSaveInstanceState(@NonNull Bundle outState) { 123 super.onSaveInstanceState(outState); 124 125 outState.putCharSequence(SAVE_STATE_TITLE, mDialogTitle); 126 outState.putCharSequence(SAVE_STATE_POSITIVE_TEXT, mPositiveButtonText); 127 outState.putCharSequence(SAVE_STATE_NEGATIVE_TEXT, mNegativeButtonText); 128 outState.putCharSequence(SAVE_STATE_MESSAGE, mDialogMessage); 129 outState.putInt(SAVE_STATE_LAYOUT, mDialogLayoutRes); 130 if (mDialogIcon != null) { 131 outState.putParcelable(SAVE_STATE_ICON, mDialogIcon.getBitmap()); 132 } 133 } 134 135 @Override 136 public @NonNull 137 Dialog onCreateDialog(Bundle savedInstanceState) { 138 final Context context = getActivity(); 139 mWhichButtonClicked = DialogInterface.BUTTON_NEGATIVE; 140 141 final AlertDialog.Builder builder = new AlertDialog.Builder(context) 142 .setTitle(mDialogTitle) 143 .setIcon(mDialogIcon) 144 .setPositiveButton(mPositiveButtonText, this) 145 .setNegativeButton(mNegativeButtonText, this); 146 147 View contentView = onCreateDialogView(context); 148 if (contentView != null) { 149 onBindDialogView(contentView); 150 builder.setView(contentView); 151 } else { 152 builder.setMessage(mDialogMessage); 153 } 154 155 onPrepareDialogBuilder(builder); 156 157 // Create the dialog 158 final Dialog dialog = builder.create(); 159 if (needInputMethod()) { 160 requestInputMethod(dialog); 161 } 162 163 return dialog; 164 } 165 166 /** 167 * Get the preference that requested this dialog. Available after {@link #onCreate(Bundle)} has 168 * been called on the {@link PreferenceFragmentCompat} which launched this dialog. 169 * 170 * @return The {@link DialogPreference} associated with this 171 * dialog. 172 */ 173 public DialogPreference getPreference() { 174 if (mPreference == null) { 175 final String key = getArguments().getString(ARG_KEY); 176 final DialogPreference.TargetFragment fragment = 177 (DialogPreference.TargetFragment) getTargetFragment(); 178 mPreference = (DialogPreference) fragment.findPreference(key); 179 } 180 return mPreference; 181 } 182 183 /** 184 * Prepares the dialog builder to be shown when the preference is clicked. 185 * Use this to set custom properties on the dialog. 186 * <p> 187 * Do not {@link AlertDialog.Builder#create()} or 188 * {@link AlertDialog.Builder#show()}. 189 */ 190 protected void onPrepareDialogBuilder(AlertDialog.Builder builder) {} 191 192 /** 193 * Returns whether the preference needs to display a soft input method when the dialog is 194 * displayed. Default is false. Subclasses should override this method if they need the soft 195 * input method brought up automatically. 196 * 197 * <p>Note: If your application targets P or above, ensure your subclass manually requests 198 * focus (ideally in {@link #onBindDialogView(View)}) for the input field in order to 199 * correctly attach the input method to the field. 200 * 201 * @hide 202 */ 203 @RestrictTo(LIBRARY_GROUP) 204 protected boolean needInputMethod() { 205 return false; 206 } 207 208 /** 209 * Sets the required flags on the dialog window to enable input method window to show up. 210 */ 211 private void requestInputMethod(Dialog dialog) { 212 Window window = dialog.getWindow(); 213 window.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_VISIBLE); 214 } 215 216 /** 217 * Creates the content view for the dialog (if a custom content view is 218 * required). By default, it inflates the dialog layout resource if it is 219 * set. 220 * 221 * @return The content View for the dialog. 222 * @see DialogPreference#setLayoutResource(int) 223 */ 224 protected View onCreateDialogView(Context context) { 225 final int resId = mDialogLayoutRes; 226 if (resId == 0) { 227 return null; 228 } 229 230 LayoutInflater inflater = LayoutInflater.from(context); 231 return inflater.inflate(resId, null); 232 } 233 234 /** 235 * Binds views in the content View of the dialog to data. 236 * <p> 237 * Make sure to call through to the superclass implementation. 238 * 239 * @param view The content View of the dialog, if it is custom. 240 */ 241 protected void onBindDialogView(View view) { 242 View dialogMessageView = view.findViewById(android.R.id.message); 243 244 if (dialogMessageView != null) { 245 final CharSequence message = mDialogMessage; 246 int newVisibility = View.GONE; 247 248 if (!TextUtils.isEmpty(message)) { 249 if (dialogMessageView instanceof TextView) { 250 ((TextView) dialogMessageView).setText(message); 251 } 252 253 newVisibility = View.VISIBLE; 254 } 255 256 if (dialogMessageView.getVisibility() != newVisibility) { 257 dialogMessageView.setVisibility(newVisibility); 258 } 259 } 260 } 261 262 @Override 263 public void onClick(DialogInterface dialog, int which) { 264 mWhichButtonClicked = which; 265 } 266 267 @Override 268 public void onDismiss(DialogInterface dialog) { 269 super.onDismiss(dialog); 270 onDialogClosed(mWhichButtonClicked == DialogInterface.BUTTON_POSITIVE); 271 } 272 273 public abstract void onDialogClosed(boolean positiveResult); 274 } 275