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        mCheckedId = id;
187        if (mOnCheckedChangeListener != null) {
188            mOnCheckedChangeListener.onCheckedChanged(this, mCheckedId);
189        }
190        final AutofillManager afm = mContext.getSystemService(AutofillManager.class);
191        if (afm != null) {
192            afm.notifyValueChanged(this);
193        }
194    }
195
196    private void setCheckedStateForView(int viewId, boolean checked) {
197        View checkedView = findViewById(viewId);
198        if (checkedView != null && checkedView instanceof RadioButton) {
199            ((RadioButton) checkedView).setChecked(checked);
200        }
201    }
202
203    /**
204     * <p>Returns the identifier of the selected radio button in this group.
205     * Upon empty selection, the returned value is -1.</p>
206     *
207     * @return the unique id of the selected radio button in this group
208     *
209     * @see #check(int)
210     * @see #clearCheck()
211     *
212     * @attr ref android.R.styleable#RadioGroup_checkedButton
213     */
214    @IdRes
215    public int getCheckedRadioButtonId() {
216        return mCheckedId;
217    }
218
219    /**
220     * <p>Clears the selection. When the selection is cleared, no radio button
221     * in this group is selected and {@link #getCheckedRadioButtonId()} returns
222     * null.</p>
223     *
224     * @see #check(int)
225     * @see #getCheckedRadioButtonId()
226     */
227    public void clearCheck() {
228        check(-1);
229    }
230
231    /**
232     * <p>Register a callback to be invoked when the checked radio button
233     * changes in this group.</p>
234     *
235     * @param listener the callback to call on checked state change
236     */
237    public void setOnCheckedChangeListener(OnCheckedChangeListener listener) {
238        mOnCheckedChangeListener = listener;
239    }
240
241    /**
242     * {@inheritDoc}
243     */
244    @Override
245    public LayoutParams generateLayoutParams(AttributeSet attrs) {
246        return new RadioGroup.LayoutParams(getContext(), attrs);
247    }
248
249    /**
250     * {@inheritDoc}
251     */
252    @Override
253    protected boolean checkLayoutParams(ViewGroup.LayoutParams p) {
254        return p instanceof RadioGroup.LayoutParams;
255    }
256
257    @Override
258    protected LinearLayout.LayoutParams generateDefaultLayoutParams() {
259        return new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
260    }
261
262    @Override
263    public CharSequence getAccessibilityClassName() {
264        return RadioGroup.class.getName();
265    }
266
267    /**
268     * <p>This set of layout parameters defaults the width and the height of
269     * the children to {@link #WRAP_CONTENT} when they are not specified in the
270     * XML file. Otherwise, this class ussed the value read from the XML file.</p>
271     *
272     * <p>See
273     * {@link android.R.styleable#LinearLayout_Layout LinearLayout Attributes}
274     * for a list of all child view attributes that this class supports.</p>
275     *
276     */
277    public static class LayoutParams extends LinearLayout.LayoutParams {
278        /**
279         * {@inheritDoc}
280         */
281        public LayoutParams(Context c, AttributeSet attrs) {
282            super(c, attrs);
283        }
284
285        /**
286         * {@inheritDoc}
287         */
288        public LayoutParams(int w, int h) {
289            super(w, h);
290        }
291
292        /**
293         * {@inheritDoc}
294         */
295        public LayoutParams(int w, int h, float initWeight) {
296            super(w, h, initWeight);
297        }
298
299        /**
300         * {@inheritDoc}
301         */
302        public LayoutParams(ViewGroup.LayoutParams p) {
303            super(p);
304        }
305
306        /**
307         * {@inheritDoc}
308         */
309        public LayoutParams(MarginLayoutParams source) {
310            super(source);
311        }
312
313        /**
314         * <p>Fixes the child's width to
315         * {@link android.view.ViewGroup.LayoutParams#WRAP_CONTENT} and the child's
316         * height to  {@link android.view.ViewGroup.LayoutParams#WRAP_CONTENT}
317         * when not specified in the XML file.</p>
318         *
319         * @param a the styled attributes set
320         * @param widthAttr the width attribute to fetch
321         * @param heightAttr the height attribute to fetch
322         */
323        @Override
324        protected void setBaseAttributes(TypedArray a,
325                int widthAttr, int heightAttr) {
326
327            if (a.hasValue(widthAttr)) {
328                width = a.getLayoutDimension(widthAttr, "layout_width");
329            } else {
330                width = WRAP_CONTENT;
331            }
332
333            if (a.hasValue(heightAttr)) {
334                height = a.getLayoutDimension(heightAttr, "layout_height");
335            } else {
336                height = WRAP_CONTENT;
337            }
338        }
339    }
340
341    /**
342     * <p>Interface definition for a callback to be invoked when the checked
343     * radio button changed in this group.</p>
344     */
345    public interface OnCheckedChangeListener {
346        /**
347         * <p>Called when the checked radio button has changed. When the
348         * selection is cleared, checkedId is -1.</p>
349         *
350         * @param group the group in which the checked radio button has changed
351         * @param checkedId the unique identifier of the newly checked radio button
352         */
353        public void onCheckedChanged(RadioGroup group, @IdRes int checkedId);
354    }
355
356    private class CheckedStateTracker implements CompoundButton.OnCheckedChangeListener {
357        @Override
358        public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
359            // prevents from infinite recursion
360            if (mProtectFromCheckedChange) {
361                return;
362            }
363
364            mProtectFromCheckedChange = true;
365            if (mCheckedId != -1) {
366                setCheckedStateForView(mCheckedId, false);
367            }
368            mProtectFromCheckedChange = false;
369
370            int id = buttonView.getId();
371            setCheckedId(id);
372        }
373    }
374
375    /**
376     * <p>A pass-through listener acts upon the events and dispatches them
377     * to another listener. This allows the table layout to set its own internal
378     * hierarchy change listener without preventing the user to setup his.</p>
379     */
380    private class PassThroughHierarchyChangeListener implements
381            ViewGroup.OnHierarchyChangeListener {
382        private ViewGroup.OnHierarchyChangeListener mOnHierarchyChangeListener;
383
384        /**
385         * {@inheritDoc}
386         */
387        @Override
388        public void onChildViewAdded(View parent, View child) {
389            if (parent == RadioGroup.this && child instanceof RadioButton) {
390                int id = child.getId();
391                // generates an id if it's missing
392                if (id == View.NO_ID) {
393                    id = View.generateViewId();
394                    child.setId(id);
395                }
396                ((RadioButton) child).setOnCheckedChangeWidgetListener(
397                        mChildOnCheckedChangeListener);
398            }
399
400            if (mOnHierarchyChangeListener != null) {
401                mOnHierarchyChangeListener.onChildViewAdded(parent, child);
402            }
403        }
404
405        /**
406         * {@inheritDoc}
407         */
408        @Override
409        public void onChildViewRemoved(View parent, View child) {
410            if (parent == RadioGroup.this && child instanceof RadioButton) {
411                ((RadioButton) child).setOnCheckedChangeWidgetListener(null);
412            }
413
414            if (mOnHierarchyChangeListener != null) {
415                mOnHierarchyChangeListener.onChildViewRemoved(parent, child);
416            }
417        }
418    }
419
420    @Override
421    public void onProvideAutofillStructure(ViewStructure structure, int flags) {
422        super.onProvideAutofillStructure(structure, flags);
423        structure.setDataIsSensitive(mCheckedId != mInitialCheckedId);
424    }
425
426    @Override
427    public void autofill(AutofillValue value) {
428        if (!isEnabled()) return;
429
430        if (!value.isList()) {
431            Log.w(LOG_TAG, value + " could not be autofilled into " + this);
432            return;
433        }
434
435        final int index = value.getListValue();
436        final View child = getChildAt(index);
437        if (child == null) {
438            Log.w(VIEW_LOG_TAG, "RadioGroup.autoFill(): no child with index " + index);
439            return;
440        }
441
442        check(child.getId());
443    }
444
445    @Override
446    public @AutofillType int getAutofillType() {
447        return isEnabled() ? AUTOFILL_TYPE_LIST : AUTOFILL_TYPE_NONE;
448    }
449
450    @Override
451    public AutofillValue getAutofillValue() {
452        if (!isEnabled()) return null;
453
454        final int count = getChildCount();
455        for (int i = 0; i < count; i++) {
456            final View child = getChildAt(i);
457            if (child.getId() == mCheckedId) {
458                return AutofillValue.forList(i);
459            }
460        }
461        return null;
462    }
463}
464