1 /*
2  * Copyright (C) 2006 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 android.widget;
18 
19 import android.annotation.IdRes;
20 import android.annotation.NonNull;
21 import android.compat.annotation.UnsupportedAppUsage;
22 import android.content.Context;
23 import android.content.res.TypedArray;
24 import android.util.AttributeSet;
25 import android.util.Log;
26 import android.view.View;
27 import android.view.ViewGroup;
28 import android.view.ViewStructure;
29 import android.view.autofill.AutofillManager;
30 import android.view.autofill.AutofillValue;
31 
32 import com.android.internal.R;
33 
34 
35 /**
36  * <p>This class is used to create a multiple-exclusion scope for a set of radio
37  * buttons. Checking one radio button that belongs to a radio group unchecks
38  * any previously checked radio button within the same group.</p>
39  *
40  * <p>Intially, all of the radio buttons are unchecked. While it is not possible
41  * to uncheck a particular radio button, the radio group can be cleared to
42  * remove the checked state.</p>
43  *
44  * <p>The selection is identified by the unique id of the radio button as defined
45  * in the XML layout file.</p>
46  *
47  * <p><strong>XML Attributes</strong></p>
48  * <p>See {@link android.R.styleable#RadioGroup RadioGroup Attributes},
49  * {@link android.R.styleable#LinearLayout LinearLayout Attributes},
50  * {@link android.R.styleable#ViewGroup ViewGroup Attributes},
51  * {@link android.R.styleable#View View Attributes}</p>
52  * <p>Also see
53  * {@link android.widget.LinearLayout.LayoutParams LinearLayout.LayoutParams}
54  * for layout attributes.</p>
55  *
56  * @see RadioButton
57  *
58  */
59 public class RadioGroup extends LinearLayout {
60     private static final String LOG_TAG = RadioGroup.class.getSimpleName();
61 
62     // holds the checked id; the selection is empty by default
63     private int mCheckedId = -1;
64     // tracks children radio buttons checked state
65     @UnsupportedAppUsage
66     private CompoundButton.OnCheckedChangeListener mChildOnCheckedChangeListener;
67     // when true, mOnCheckedChangeListener discards events
68     private boolean mProtectFromCheckedChange = false;
69     @UnsupportedAppUsage
70     private OnCheckedChangeListener mOnCheckedChangeListener;
71     private PassThroughHierarchyChangeListener mPassThroughListener;
72 
73     // Indicates whether the child was set from resources or dynamically, so it can be used
74     // to sanitize autofill requests.
75     private int mInitialCheckedId = View.NO_ID;
76 
77     /**
78      * {@inheritDoc}
79      */
RadioGroup(Context context)80     public RadioGroup(Context context) {
81         super(context);
82         setOrientation(VERTICAL);
83         init();
84     }
85 
86     /**
87      * {@inheritDoc}
88      */
RadioGroup(Context context, AttributeSet attrs)89     public RadioGroup(Context context, AttributeSet attrs) {
90         super(context, attrs);
91 
92         // RadioGroup is important by default, unless app developer overrode attribute.
93         if (getImportantForAutofill() == IMPORTANT_FOR_AUTOFILL_AUTO) {
94             setImportantForAutofill(IMPORTANT_FOR_AUTOFILL_YES);
95         }
96 
97         // retrieve selected radio button as requested by the user in the
98         // XML layout file
99         TypedArray attributes = context.obtainStyledAttributes(
100                 attrs, com.android.internal.R.styleable.RadioGroup, com.android.internal.R.attr.radioButtonStyle, 0);
101         saveAttributeDataForStyleable(context, com.android.internal.R.styleable.RadioGroup,
102                 attrs, attributes, com.android.internal.R.attr.radioButtonStyle, 0);
103 
104         int value = attributes.getResourceId(R.styleable.RadioGroup_checkedButton, View.NO_ID);
105         if (value != View.NO_ID) {
106             mCheckedId = value;
107             mInitialCheckedId = value;
108         }
109         final int index = attributes.getInt(com.android.internal.R.styleable.RadioGroup_orientation, VERTICAL);
110         setOrientation(index);
111 
112         attributes.recycle();
113         init();
114     }
115 
init()116     private void init() {
117         mChildOnCheckedChangeListener = new CheckedStateTracker();
118         mPassThroughListener = new PassThroughHierarchyChangeListener();
119         super.setOnHierarchyChangeListener(mPassThroughListener);
120     }
121 
122     /**
123      * {@inheritDoc}
124      */
125     @Override
setOnHierarchyChangeListener(OnHierarchyChangeListener listener)126     public void setOnHierarchyChangeListener(OnHierarchyChangeListener listener) {
127         // the user listener is delegated to our pass-through listener
128         mPassThroughListener.mOnHierarchyChangeListener = listener;
129     }
130 
131     /**
132      * {@inheritDoc}
133      */
134     @Override
onFinishInflate()135     protected void onFinishInflate() {
136         super.onFinishInflate();
137 
138         // checks the appropriate radio button as requested in the XML file
139         if (mCheckedId != -1) {
140             mProtectFromCheckedChange = true;
141             setCheckedStateForView(mCheckedId, true);
142             mProtectFromCheckedChange = false;
143             setCheckedId(mCheckedId);
144         }
145     }
146 
147     @Override
addView(View child, int index, ViewGroup.LayoutParams params)148     public void addView(View child, int index, ViewGroup.LayoutParams params) {
149         if (child instanceof RadioButton) {
150             final RadioButton button = (RadioButton) child;
151             if (button.isChecked()) {
152                 mProtectFromCheckedChange = true;
153                 if (mCheckedId != -1) {
154                     setCheckedStateForView(mCheckedId, false);
155                 }
156                 mProtectFromCheckedChange = false;
157                 setCheckedId(button.getId());
158             }
159         }
160 
161         super.addView(child, index, params);
162     }
163 
164     /**
165      * <p>Sets the selection to the radio button whose identifier is passed in
166      * parameter. Using -1 as the selection identifier clears the selection;
167      * such an operation is equivalent to invoking {@link #clearCheck()}.</p>
168      *
169      * @param id the unique id of the radio button to select in this group
170      *
171      * @see #getCheckedRadioButtonId()
172      * @see #clearCheck()
173      */
check(@dRes int id)174     public void check(@IdRes int id) {
175         // don't even bother
176         if (id != -1 && (id == mCheckedId)) {
177             return;
178         }
179 
180         if (mCheckedId != -1) {
181             setCheckedStateForView(mCheckedId, false);
182         }
183 
184         if (id != -1) {
185             setCheckedStateForView(id, true);
186         }
187 
188         setCheckedId(id);
189     }
190 
setCheckedId(@dRes int id)191     private void setCheckedId(@IdRes int id) {
192         boolean changed = id != mCheckedId;
193         mCheckedId = id;
194 
195         if (mOnCheckedChangeListener != null) {
196             mOnCheckedChangeListener.onCheckedChanged(this, mCheckedId);
197         }
198         if (changed) {
199             final AutofillManager afm = mContext.getSystemService(AutofillManager.class);
200             if (afm != null) {
201                 afm.notifyValueChanged(this);
202             }
203         }
204     }
205 
setCheckedStateForView(int viewId, boolean checked)206     private void setCheckedStateForView(int viewId, boolean checked) {
207         View checkedView = findViewById(viewId);
208         if (checkedView != null && checkedView instanceof RadioButton) {
209             ((RadioButton) checkedView).setChecked(checked);
210         }
211     }
212 
213     /**
214      * <p>Returns the identifier of the selected radio button in this group.
215      * Upon empty selection, the returned value is -1.</p>
216      *
217      * @return the unique id of the selected radio button in this group
218      *
219      * @see #check(int)
220      * @see #clearCheck()
221      *
222      * @attr ref android.R.styleable#RadioGroup_checkedButton
223      */
224     @IdRes
getCheckedRadioButtonId()225     public int getCheckedRadioButtonId() {
226         return mCheckedId;
227     }
228 
229     /**
230      * <p>Clears the selection. When the selection is cleared, no radio button
231      * in this group is selected and {@link #getCheckedRadioButtonId()} returns
232      * null.</p>
233      *
234      * @see #check(int)
235      * @see #getCheckedRadioButtonId()
236      */
clearCheck()237     public void clearCheck() {
238         check(-1);
239     }
240 
241     /**
242      * <p>Register a callback to be invoked when the checked radio button
243      * changes in this group.</p>
244      *
245      * @param listener the callback to call on checked state change
246      */
setOnCheckedChangeListener(OnCheckedChangeListener listener)247     public void setOnCheckedChangeListener(OnCheckedChangeListener listener) {
248         mOnCheckedChangeListener = listener;
249     }
250 
251     /**
252      * {@inheritDoc}
253      */
254     @Override
generateLayoutParams(AttributeSet attrs)255     public LayoutParams generateLayoutParams(AttributeSet attrs) {
256         return new RadioGroup.LayoutParams(getContext(), attrs);
257     }
258 
259     /**
260      * {@inheritDoc}
261      */
262     @Override
checkLayoutParams(ViewGroup.LayoutParams p)263     protected boolean checkLayoutParams(ViewGroup.LayoutParams p) {
264         return p instanceof RadioGroup.LayoutParams;
265     }
266 
267     @Override
generateDefaultLayoutParams()268     protected LinearLayout.LayoutParams generateDefaultLayoutParams() {
269         return new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
270     }
271 
272     @Override
getAccessibilityClassName()273     public CharSequence getAccessibilityClassName() {
274         return RadioGroup.class.getName();
275     }
276 
277     /**
278      * <p>This set of layout parameters defaults the width and the height of
279      * the children to {@link #WRAP_CONTENT} when they are not specified in the
280      * XML file. Otherwise, this class ussed the value read from the XML file.</p>
281      *
282      * <p>See
283      * {@link android.R.styleable#LinearLayout_Layout LinearLayout Attributes}
284      * for a list of all child view attributes that this class supports.</p>
285      *
286      */
287     public static class LayoutParams extends LinearLayout.LayoutParams {
288         /**
289          * {@inheritDoc}
290          */
LayoutParams(Context c, AttributeSet attrs)291         public LayoutParams(Context c, AttributeSet attrs) {
292             super(c, attrs);
293         }
294 
295         /**
296          * {@inheritDoc}
297          */
LayoutParams(int w, int h)298         public LayoutParams(int w, int h) {
299             super(w, h);
300         }
301 
302         /**
303          * {@inheritDoc}
304          */
LayoutParams(int w, int h, float initWeight)305         public LayoutParams(int w, int h, float initWeight) {
306             super(w, h, initWeight);
307         }
308 
309         /**
310          * {@inheritDoc}
311          */
LayoutParams(ViewGroup.LayoutParams p)312         public LayoutParams(ViewGroup.LayoutParams p) {
313             super(p);
314         }
315 
316         /**
317          * {@inheritDoc}
318          */
LayoutParams(MarginLayoutParams source)319         public LayoutParams(MarginLayoutParams source) {
320             super(source);
321         }
322 
323         /**
324          * <p>Fixes the child's width to
325          * {@link android.view.ViewGroup.LayoutParams#WRAP_CONTENT} and the child's
326          * height to  {@link android.view.ViewGroup.LayoutParams#WRAP_CONTENT}
327          * when not specified in the XML file.</p>
328          *
329          * @param a the styled attributes set
330          * @param widthAttr the width attribute to fetch
331          * @param heightAttr the height attribute to fetch
332          */
333         @Override
setBaseAttributes(TypedArray a, int widthAttr, int heightAttr)334         protected void setBaseAttributes(TypedArray a,
335                 int widthAttr, int heightAttr) {
336 
337             if (a.hasValue(widthAttr)) {
338                 width = a.getLayoutDimension(widthAttr, "layout_width");
339             } else {
340                 width = WRAP_CONTENT;
341             }
342 
343             if (a.hasValue(heightAttr)) {
344                 height = a.getLayoutDimension(heightAttr, "layout_height");
345             } else {
346                 height = WRAP_CONTENT;
347             }
348         }
349     }
350 
351     /**
352      * <p>Interface definition for a callback to be invoked when the checked
353      * radio button changed in this group.</p>
354      */
355     public interface OnCheckedChangeListener {
356         /**
357          * <p>Called when the checked radio button has changed. When the
358          * selection is cleared, checkedId is -1.</p>
359          *
360          * @param group the group in which the checked radio button has changed
361          * @param checkedId the unique identifier of the newly checked radio button
362          */
onCheckedChanged(RadioGroup group, @IdRes int checkedId)363         public void onCheckedChanged(RadioGroup group, @IdRes int checkedId);
364     }
365 
366     private class CheckedStateTracker implements CompoundButton.OnCheckedChangeListener {
367         @Override
onCheckedChanged(CompoundButton buttonView, boolean isChecked)368         public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
369             // prevents from infinite recursion
370             if (mProtectFromCheckedChange) {
371                 return;
372             }
373 
374             mProtectFromCheckedChange = true;
375             if (mCheckedId != -1) {
376                 setCheckedStateForView(mCheckedId, false);
377             }
378             mProtectFromCheckedChange = false;
379 
380             int id = buttonView.getId();
381             setCheckedId(id);
382         }
383     }
384 
385     /**
386      * <p>A pass-through listener acts upon the events and dispatches them
387      * to another listener. This allows the table layout to set its own internal
388      * hierarchy change listener without preventing the user to setup his.</p>
389      */
390     private class PassThroughHierarchyChangeListener implements
391             ViewGroup.OnHierarchyChangeListener {
392         private ViewGroup.OnHierarchyChangeListener mOnHierarchyChangeListener;
393 
394         /**
395          * {@inheritDoc}
396          */
397         @Override
onChildViewAdded(View parent, View child)398         public void onChildViewAdded(View parent, View child) {
399             if (parent == RadioGroup.this && child instanceof RadioButton) {
400                 int id = child.getId();
401                 // generates an id if it's missing
402                 if (id == View.NO_ID) {
403                     id = View.generateViewId();
404                     child.setId(id);
405                 }
406                 ((RadioButton) child).setOnCheckedChangeWidgetListener(
407                         mChildOnCheckedChangeListener);
408             }
409 
410             if (mOnHierarchyChangeListener != null) {
411                 mOnHierarchyChangeListener.onChildViewAdded(parent, child);
412             }
413         }
414 
415         /**
416          * {@inheritDoc}
417          */
418         @Override
onChildViewRemoved(View parent, View child)419         public void onChildViewRemoved(View parent, View child) {
420             if (parent == RadioGroup.this && child instanceof RadioButton) {
421                 ((RadioButton) child).setOnCheckedChangeWidgetListener(null);
422             }
423 
424             if (mOnHierarchyChangeListener != null) {
425                 mOnHierarchyChangeListener.onChildViewRemoved(parent, child);
426             }
427         }
428     }
429 
430     /** @hide */
431     @Override
onProvideStructure(@onNull ViewStructure structure, @ViewStructureType int viewFor, int flags)432     protected void onProvideStructure(@NonNull ViewStructure structure,
433             @ViewStructureType int viewFor, int flags) {
434         super.onProvideStructure(structure, viewFor, flags);
435 
436         if (viewFor == VIEW_STRUCTURE_FOR_AUTOFILL) {
437             structure.setDataIsSensitive(mCheckedId != mInitialCheckedId);
438         }
439     }
440 
441     @Override
autofill(AutofillValue value)442     public void autofill(AutofillValue value) {
443         if (!isEnabled()) return;
444 
445         if (!value.isList()) {
446             Log.w(LOG_TAG, value + " could not be autofilled into " + this);
447             return;
448         }
449 
450         final int index = value.getListValue();
451         final View child = getChildAt(index);
452         if (child == null) {
453             Log.w(VIEW_LOG_TAG, "RadioGroup.autoFill(): no child with index " + index);
454             return;
455         }
456 
457         check(child.getId());
458     }
459 
460     @Override
getAutofillType()461     public @AutofillType int getAutofillType() {
462         return isEnabled() ? AUTOFILL_TYPE_LIST : AUTOFILL_TYPE_NONE;
463     }
464 
465     @Override
getAutofillValue()466     public AutofillValue getAutofillValue() {
467         if (!isEnabled()) return null;
468 
469         final int count = getChildCount();
470         for (int i = 0; i < count; i++) {
471             final View child = getChildAt(i);
472             if (child.getId() == mCheckedId) {
473                 return AutofillValue.forList(i);
474             }
475         }
476         return null;
477     }
478 }
479