1/*
2 * Copyright (C) 2017 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file
5 * except in compliance with the License. You may obtain a copy of the License at
6 *
7 *      http://www.apache.org/licenses/LICENSE-2.0
8 *
9 * Unless required by applicable law or agreed to in writing, software distributed under the
10 * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
11 * KIND, either express or implied. See the License for the specific language governing
12 * permissions and limitations under the License.
13 */
14package com.android.systemui.qs.tileimpl;
15
16import static com.android.systemui.qs.tileimpl.QSIconViewImpl.QS_ANIM_LENGTH;
17
18import android.animation.ValueAnimator;
19import android.content.Context;
20import android.content.res.ColorStateList;
21import android.content.res.TypedArray;
22import android.graphics.drawable.Drawable;
23import android.graphics.drawable.RippleDrawable;
24import android.os.Handler;
25import android.os.Looper;
26import android.os.Message;
27import android.service.quicksettings.Tile;
28import android.text.TextUtils;
29import android.util.Log;
30import android.view.Gravity;
31import android.view.View;
32import android.view.ViewGroup;
33import android.view.accessibility.AccessibilityEvent;
34import android.view.accessibility.AccessibilityNodeInfo;
35import android.widget.FrameLayout;
36import android.widget.ImageView;
37import android.widget.ImageView.ScaleType;
38import android.widget.Switch;
39
40import com.android.settingslib.Utils;
41import com.android.systemui.R;
42import com.android.systemui.plugins.qs.QSIconView;
43import com.android.systemui.plugins.qs.QSTile;
44import com.android.systemui.plugins.qs.QSTile.BooleanState;
45
46public class QSTileBaseView extends com.android.systemui.plugins.qs.QSTileView {
47
48    private static final String TAG = "QSTileBaseView";
49    private final H mHandler = new H();
50    private final FrameLayout mIconFrame;
51    protected QSIconView mIcon;
52    protected RippleDrawable mRipple;
53    private Drawable mTileBackground;
54    private String mAccessibilityClass;
55    private boolean mTileState;
56    private boolean mCollapsedView;
57    private boolean mClicked;
58
59    private final ImageView mBg;
60    private final int mColorActive;
61    private final int mColorInactive;
62    private final int mColorDisabled;
63    private int mCircleColor;
64
65    public QSTileBaseView(Context context, QSIconView icon) {
66        this(context, icon, false);
67    }
68
69    public QSTileBaseView(Context context, QSIconView icon, boolean collapsedView) {
70        super(context);
71        // Default to Quick Tile padding, and QSTileView will specify its own padding.
72        int padding = context.getResources().getDimensionPixelSize(R.dimen.qs_quick_tile_padding);
73
74        mIconFrame = new FrameLayout(context);
75        mIconFrame.setForegroundGravity(Gravity.CENTER);
76        int size = context.getResources().getDimensionPixelSize(R.dimen.qs_quick_tile_size);
77        addView(mIconFrame, new LayoutParams(size, size));
78        mBg = new ImageView(getContext());
79        mBg.setScaleType(ScaleType.FIT_CENTER);
80        mBg.setImageResource(R.drawable.ic_qs_circle);
81        mIconFrame.addView(mBg);
82        mIcon = icon;
83        FrameLayout.LayoutParams params = new FrameLayout.LayoutParams(
84                ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
85        params.setMargins(0, padding, 0, padding);
86        mIconFrame.addView(mIcon, params);
87        mIconFrame.setClipChildren(false);
88        mIconFrame.setClipToPadding(false);
89
90        mTileBackground = newTileBackground();
91        if (mTileBackground instanceof RippleDrawable) {
92            setRipple((RippleDrawable) mTileBackground);
93        }
94        setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_YES);
95        setBackground(mTileBackground);
96
97        mColorActive = Utils.getColorAttr(context, android.R.attr.colorAccent);
98        mColorDisabled = Utils.getDisabled(context,
99                Utils.getColorAttr(context, android.R.attr.textColorTertiary));
100        mColorInactive = Utils.getColorAttr(context, android.R.attr.textColorSecondary);
101
102        setPadding(0, 0, 0, 0);
103        setClipChildren(false);
104        setClipToPadding(false);
105        mCollapsedView = collapsedView;
106        setFocusable(true);
107    }
108
109    public View getBgCicle() {
110        return mBg;
111    }
112
113    protected Drawable newTileBackground() {
114        final int[] attrs = new int[]{android.R.attr.selectableItemBackgroundBorderless};
115        final TypedArray ta = getContext().obtainStyledAttributes(attrs);
116        final Drawable d = ta.getDrawable(0);
117        ta.recycle();
118        return d;
119    }
120
121    private void setRipple(RippleDrawable tileBackground) {
122        mRipple = tileBackground;
123        if (getWidth() != 0) {
124            updateRippleSize();
125        }
126    }
127
128    private void updateRippleSize() {
129        // center the touch feedback on the center of the icon, and dial it down a bit
130        final int cx = mIconFrame.getMeasuredWidth() / 2 + mIconFrame.getLeft();
131        final int cy = mIconFrame.getMeasuredHeight() / 2 + mIconFrame.getTop();
132        final int rad = (int) (mIcon.getHeight() * .85f);
133        mRipple.setHotspotBounds(cx - rad, cy - rad, cx + rad, cy + rad);
134    }
135
136    @Override
137    public void init(QSTile tile) {
138        init(v -> tile.click(), v -> tile.secondaryClick(), view -> {
139            tile.longClick();
140            return true;
141        });
142    }
143
144    public void init(OnClickListener click, OnClickListener secondaryClick,
145            OnLongClickListener longClick) {
146        setOnClickListener(click);
147        setOnLongClickListener(longClick);
148    }
149
150    @Override
151    protected void onLayout(boolean changed, int l, int t, int r, int b) {
152        super.onLayout(changed, l, t, r, b);
153        if (mRipple != null) {
154            updateRippleSize();
155        }
156    }
157
158    @Override
159    public boolean hasOverlappingRendering() {
160        // Avoid layers for this layout - we don't need them.
161        return false;
162    }
163
164    /**
165     * Update the accessibility order for this view.
166     *
167     * @param previousView the view which should be before this one
168     * @return the last view in this view which is accessible
169     */
170    public View updateAccessibilityOrder(View previousView) {
171        setAccessibilityTraversalAfter(previousView.getId());
172        return this;
173    }
174
175    public void onStateChanged(QSTile.State state) {
176        mHandler.obtainMessage(H.STATE_CHANGED, state).sendToTarget();
177    }
178
179    protected void handleStateChanged(QSTile.State state) {
180        int circleColor = getCircleColor(state.state);
181        if (circleColor != mCircleColor) {
182            if (mBg.isShown() && animationsEnabled()) {
183                ValueAnimator animator = ValueAnimator.ofArgb(mCircleColor, circleColor)
184                        .setDuration(QS_ANIM_LENGTH);
185                animator.addUpdateListener(animation -> mBg.setImageTintList(ColorStateList.valueOf(
186                        (Integer) animation.getAnimatedValue())));
187                animator.start();
188            } else {
189                QSIconViewImpl.setTint(mBg, circleColor);
190            }
191            mCircleColor = circleColor;
192        }
193
194        setClickable(state.state != Tile.STATE_UNAVAILABLE);
195        mIcon.setIcon(state);
196        setContentDescription(state.contentDescription);
197
198        mAccessibilityClass = state.expandedAccessibilityClassName;
199        if (state instanceof QSTile.BooleanState) {
200            boolean newState = ((BooleanState) state).value;
201            if (mTileState != newState) {
202                mClicked = false;
203                mTileState = newState;
204            }
205        }
206    }
207
208    protected boolean animationsEnabled() {
209        return true;
210    }
211
212    private int getCircleColor(int state) {
213        switch (state) {
214            case Tile.STATE_ACTIVE:
215                return mColorActive;
216            case Tile.STATE_INACTIVE:
217            case Tile.STATE_UNAVAILABLE:
218                return mColorDisabled;
219            default:
220                Log.e(TAG, "Invalid state " + state);
221                return 0;
222        }
223    }
224
225    @Override
226    public void setClickable(boolean clickable) {
227        super.setClickable(clickable);
228        setBackground(clickable ? mRipple : null);
229    }
230
231    @Override
232    public int getDetailY() {
233        return getTop() + getHeight() / 2;
234    }
235
236    public QSIconView getIcon() {
237        return mIcon;
238    }
239
240    public View getIconWithBackground() {
241        return mIconFrame;
242    }
243
244    @Override
245    public boolean performClick() {
246        mClicked = true;
247        return super.performClick();
248    }
249
250    @Override
251    public void onInitializeAccessibilityEvent(AccessibilityEvent event) {
252        super.onInitializeAccessibilityEvent(event);
253        if (!TextUtils.isEmpty(mAccessibilityClass)) {
254            event.setClassName(mAccessibilityClass);
255            if (Switch.class.getName().equals(mAccessibilityClass)) {
256                boolean b = mClicked ? !mTileState : mTileState;
257                String label = getResources()
258                        .getString(b ? R.string.switch_bar_on : R.string.switch_bar_off);
259                event.setContentDescription(label);
260                event.setChecked(b);
261            }
262        }
263    }
264
265    @Override
266    public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) {
267        super.onInitializeAccessibilityNodeInfo(info);
268        if (!TextUtils.isEmpty(mAccessibilityClass)) {
269            info.setClassName(mAccessibilityClass);
270            if (Switch.class.getName().equals(mAccessibilityClass)) {
271                boolean b = mClicked ? !mTileState : mTileState;
272                String label = getResources()
273                        .getString(b ? R.string.switch_bar_on : R.string.switch_bar_off);
274                info.setText(label);
275                info.setChecked(b);
276                info.setCheckable(true);
277                info.addAction(
278                        new AccessibilityNodeInfo.AccessibilityAction(
279                                AccessibilityNodeInfo.AccessibilityAction.ACTION_LONG_CLICK.getId(),
280                                getResources().getString(R.string.accessibility_long_click_tile)));
281            }
282        }
283    }
284
285    private class H extends Handler {
286        private static final int STATE_CHANGED = 1;
287        public H() {
288            super(Looper.getMainLooper());
289        }
290
291        @Override
292        public void handleMessage(Message msg) {
293            if (msg.what == STATE_CHANGED) {
294                handleStateChanged((QSTile.State) msg.obj);
295            }
296        }
297    }
298}
299