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