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
17package android.widget;
18
19import android.annotation.IdRes;
20import android.content.Context;
21import android.content.res.TypedArray;
22import android.util.AttributeSet;
23import android.util.Log;
24import android.view.View;
25import android.view.ViewGroup;
26import android.view.ViewStructure;
27import android.view.autofill.AutofillManager;
28import android.view.autofill.AutofillValue;
29
30import 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 */
57public 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     */
76    public RadioGroup(Context context) {
77        super(context);
78        setOrientation(VERTICAL);
79        init();
80    }
81
82    /**
83     * {@inheritDoc}
84     */
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
110    private void init() {
111        mChildOnCheckedChangeListener = new CheckedStateTracker();
112        mPassThroughListener = new PassThroughHierarchyChangeListener();
113        super.setOnHierarchyChangeListener(mPassThroughListener);
114    }
115
116    /**
117     * {@inheritDoc}
118     */
119    @Override
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
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
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     */
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
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
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
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     */
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     */
241    public void setOnCheckedChangeListener(OnCheckedChangeListener listener) {
242        mOnCheckedChangeListener = listener;
243    }
244
245    /**
246     * {@inheritDoc}
247     */
248    @Override
249    public LayoutParams generateLayoutParams(AttributeSet attrs) {
250        return new RadioGroup.LayoutParams(getContext(), attrs);
251    }
252
253    /**
254     * {@inheritDoc}
255     */
256    @Override
257    protected boolean checkLayoutParams(ViewGroup.LayoutParams p) {
258        return p instanceof RadioGroup.LayoutParams;
259    }
260
261    @Override
262    protected LinearLayout.LayoutParams generateDefaultLayoutParams() {
263        return new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
264    }
265
266    @Override
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         */
285        public LayoutParams(Context c, AttributeSet attrs) {
286            super(c, attrs);
287        }
288
289        /**
290         * {@inheritDoc}
291         */
292        public LayoutParams(int w, int h) {
293            super(w, h);
294        }
295
296        /**
297         * {@inheritDoc}
298         */
299        public LayoutParams(int w, int h, float initWeight) {
300            super(w, h, initWeight);
301        }
302
303        /**
304         * {@inheritDoc}
305         */
306        public LayoutParams(ViewGroup.LayoutParams p) {
307            super(p);
308        }
309
310        /**
311         * {@inheritDoc}
312         */
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
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         */
357        public void onCheckedChanged(RadioGroup group, @IdRes int checkedId);
358    }
359
360    private class CheckedStateTracker implements CompoundButton.OnCheckedChangeListener {
361        @Override
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
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
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
425    public void onProvideAutofillStructure(ViewStructure structure, int flags) {
426        super.onProvideAutofillStructure(structure, flags);
427        structure.setDataIsSensitive(mCheckedId != mInitialCheckedId);
428    }
429
430    @Override
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
450    public @AutofillType int getAutofillType() {
451        return isEnabled() ? AUTOFILL_TYPE_LIST : AUTOFILL_TYPE_NONE;
452    }
453
454    @Override
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