1 /* 2 * Copyright 2019 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.car.ui.preference; 18 19 import android.app.AlertDialog; 20 import android.app.Dialog; 21 import android.content.Context; 22 import android.content.DialogInterface; 23 import android.graphics.Bitmap; 24 import android.graphics.drawable.BitmapDrawable; 25 import android.os.Bundle; 26 import android.text.TextUtils; 27 import android.view.LayoutInflater; 28 import android.view.View; 29 import android.view.Window; 30 import android.view.WindowManager; 31 import android.widget.TextView; 32 33 import androidx.annotation.CallSuper; 34 import androidx.annotation.LayoutRes; 35 import androidx.annotation.NonNull; 36 import androidx.fragment.app.DialogFragment; 37 import androidx.preference.DialogPreference; 38 39 /** 40 * Abstract base class which presents a dialog associated with a {@link 41 * androidx.preference.DialogPreference}. Since the preference object may not be available during 42 * fragment re-creation, the necessary information for displaying the dialog is read once during 43 * the initial call to {@link #onCreate(Bundle)} and saved/restored in the saved instance state. 44 * Custom subclasses should also follow this pattern. 45 * 46 * <p>Note: this is borrowed as-is from {@link androidx.preference.PreferenceDialogFragmentCompat} 47 * with updates to formatting to match the project style and the removal of the {@link 48 * DialogPreference.TargetFragment} interface requirement. See {@link PreferenceDialogFragment} 49 * for a version of this class with the check preserved. Automotive applications should use 50 * children of this fragment in order to launch the system themed platform {@link AlertDialog} 51 * instead of the one in the support library. 52 */ 53 54 public abstract class CarUiDialogFragment extends DialogFragment implements 55 DialogInterface.OnClickListener { 56 57 private static final String SAVE_STATE_TITLE = "CarUiDialogFragment.title"; 58 private static final String SAVE_STATE_POSITIVE_TEXT = "CarUiDialogFragment.positiveText"; 59 private static final String SAVE_STATE_NEGATIVE_TEXT = "CarUiDialogFragment.negativeText"; 60 private static final String SAVE_STATE_MESSAGE = "CarUiDialogFragment.message"; 61 private static final String SAVE_STATE_LAYOUT = "CarUiDialogFragment.layout"; 62 private static final String SAVE_STATE_ICON = "CarUiDialogFragment.icon"; 63 64 protected CharSequence mDialogTitle; 65 protected CharSequence mPositiveButtonText; 66 protected CharSequence mNegativeButtonText; 67 protected CharSequence mDialogMessage; 68 @LayoutRes 69 protected int mDialogLayoutRes; 70 71 protected BitmapDrawable mDialogIcon; 72 73 /** Which button was clicked. */ 74 private int mWhichButtonClicked; 75 76 @Override onCreate(Bundle savedInstanceState)77 public void onCreate(Bundle savedInstanceState) { 78 super.onCreate(savedInstanceState); 79 80 if (savedInstanceState != null) { 81 mDialogTitle = savedInstanceState.getCharSequence(SAVE_STATE_TITLE); 82 mPositiveButtonText = savedInstanceState.getCharSequence(SAVE_STATE_POSITIVE_TEXT); 83 mNegativeButtonText = savedInstanceState.getCharSequence(SAVE_STATE_NEGATIVE_TEXT); 84 mDialogMessage = savedInstanceState.getCharSequence(SAVE_STATE_MESSAGE); 85 mDialogLayoutRes = savedInstanceState.getInt(SAVE_STATE_LAYOUT, 0); 86 Bitmap bitmap = savedInstanceState.getParcelable(SAVE_STATE_ICON); 87 if (bitmap != null) { 88 mDialogIcon = new BitmapDrawable(getResources(), bitmap); 89 } 90 } 91 } 92 93 @Override onSaveInstanceState(@onNull Bundle outState)94 public void onSaveInstanceState(@NonNull Bundle outState) { 95 super.onSaveInstanceState(outState); 96 97 outState.putCharSequence(SAVE_STATE_TITLE, mDialogTitle); 98 outState.putCharSequence(SAVE_STATE_POSITIVE_TEXT, mPositiveButtonText); 99 outState.putCharSequence(SAVE_STATE_NEGATIVE_TEXT, mNegativeButtonText); 100 outState.putCharSequence(SAVE_STATE_MESSAGE, mDialogMessage); 101 outState.putInt(SAVE_STATE_LAYOUT, mDialogLayoutRes); 102 if (mDialogIcon != null) { 103 outState.putParcelable(SAVE_STATE_ICON, mDialogIcon.getBitmap()); 104 } 105 } 106 107 @Override 108 @NonNull onCreateDialog(Bundle savedInstanceState)109 public Dialog onCreateDialog(Bundle savedInstanceState) { 110 Context context = getActivity(); 111 mWhichButtonClicked = DialogInterface.BUTTON_NEGATIVE; 112 113 AlertDialog.Builder builder = new AlertDialog.Builder(context) 114 .setTitle(mDialogTitle) 115 .setIcon(mDialogIcon) 116 .setPositiveButton(mPositiveButtonText, this) 117 .setNegativeButton(mNegativeButtonText, this); 118 119 View contentView = onCreateDialogView(context); 120 if (contentView != null) { 121 onBindDialogView(contentView); 122 builder.setView(contentView); 123 } else { 124 builder.setMessage(mDialogMessage); 125 } 126 127 onPrepareDialogBuilder(builder); 128 129 // Create the dialog 130 Dialog dialog = builder.create(); 131 if (needInputMethod()) { 132 // Request input only after the dialog is shown. This is to prevent an issue where the 133 // dialog view collapsed the content on small displays. 134 dialog.setOnShowListener(d -> requestInputMethod(dialog)); 135 } 136 137 return dialog; 138 } 139 140 /** 141 * Prepares the dialog builder to be shown when the preference is clicked. Use this to set 142 * custom properties on the dialog. 143 * 144 * <p>Do not {@link AlertDialog.Builder#create()} or {@link AlertDialog.Builder#show()}. 145 */ onPrepareDialogBuilder(AlertDialog.Builder builder)146 protected void onPrepareDialogBuilder(AlertDialog.Builder builder) { 147 } 148 149 /** 150 * Returns whether the preference needs to display a soft input method when the dialog is 151 * displayed. Default is false. Subclasses should override this method if they need the soft 152 * input method brought up automatically. 153 * 154 * <p>Note: Ensure your subclass manually requests focus (ideally in {@link 155 * #onBindDialogView(View)}) for the input field in order to 156 * correctly attach the input method to the field. 157 */ needInputMethod()158 protected boolean needInputMethod() { 159 return false; 160 } 161 162 /** 163 * Sets the required flags on the dialog window to enable input method window to show up. 164 */ requestInputMethod(Dialog dialog)165 private void requestInputMethod(Dialog dialog) { 166 Window window = dialog.getWindow(); 167 window.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_VISIBLE); 168 } 169 170 /** 171 * Creates the content view for the dialog (if a custom content view is required). By default, 172 * it inflates the dialog layout resource if it is set. 173 * 174 * @return the content View for the dialog. 175 * @see DialogPreference#setLayoutResource(int) 176 */ onCreateDialogView(Context context)177 protected View onCreateDialogView(Context context) { 178 int resId = mDialogLayoutRes; 179 if (resId == 0) { 180 return null; 181 } 182 183 LayoutInflater inflater = LayoutInflater.from(context); 184 return inflater.inflate(resId, null); 185 } 186 187 /** 188 * Binds views in the content View of the dialog to data. 189 * 190 * <p>Make sure to call through to the superclass implementation. 191 * 192 * @param view the content View of the dialog, if it is custom. 193 */ 194 @CallSuper onBindDialogView(View view)195 protected void onBindDialogView(View view) { 196 View dialogMessageView = view.findViewById(android.R.id.message); 197 198 if (dialogMessageView != null) { 199 CharSequence message = mDialogMessage; 200 int newVisibility = View.GONE; 201 202 if (!TextUtils.isEmpty(message)) { 203 if (dialogMessageView instanceof TextView) { 204 ((TextView) dialogMessageView).setText(message); 205 } 206 207 newVisibility = View.VISIBLE; 208 } 209 210 if (dialogMessageView.getVisibility() != newVisibility) { 211 dialogMessageView.setVisibility(newVisibility); 212 } 213 } 214 } 215 216 @Override onClick(DialogInterface dialog, int which)217 public void onClick(DialogInterface dialog, int which) { 218 mWhichButtonClicked = which; 219 } 220 221 @Override onDismiss(DialogInterface dialog)222 public void onDismiss(DialogInterface dialog) { 223 super.onDismiss(dialog); 224 onDialogClosed(mWhichButtonClicked == DialogInterface.BUTTON_POSITIVE); 225 } 226 227 /** 228 * Called when the dialog is dismissed. 229 * 230 * @param positiveResult {@code true} if the dialog was dismissed with {@link 231 * DialogInterface#BUTTON_POSITIVE}. 232 */ onDialogClosed(boolean positiveResult)233 protected abstract void onDialogClosed(boolean positiveResult); 234 } 235