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