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