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