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 android.content.Context;
17import android.content.res.TypedArray;
18import android.graphics.drawable.Drawable;
19import android.graphics.drawable.RippleDrawable;
20import android.os.Handler;
21import android.os.Looper;
22import android.os.Message;
23import android.service.quicksettings.Tile;
24import android.text.TextUtils;
25import android.view.Gravity;
26import android.view.View;
27import android.view.ViewGroup;
28import android.view.accessibility.AccessibilityEvent;
29import android.view.accessibility.AccessibilityNodeInfo;
30import android.widget.FrameLayout;
31import android.widget.Switch;
32
33import com.android.systemui.R;
34import com.android.systemui.plugins.qs.*;
35import com.android.systemui.plugins.qs.QSTile.BooleanState;
36
37public class QSTileBaseView extends com.android.systemui.plugins.qs.QSTileView {
38
39    private static final String TAG = "QSTileBaseView";
40    private final H mHandler = new H();
41    private final FrameLayout mIconFrame;
42    protected QSIconView mIcon;
43    protected RippleDrawable mRipple;
44    private Drawable mTileBackground;
45    private String mAccessibilityClass;
46    private boolean mTileState;
47    private boolean mCollapsedView;
48    private boolean mClicked;
49
50    public QSTileBaseView(Context context, QSIconView icon) {
51        this(context, icon, false);
52    }
53
54    public QSTileBaseView(Context context, QSIconView icon, boolean collapsedView) {
55        super(context);
56        // Default to Quick Tile padding, and QSTileView will specify its own padding.
57        int padding = context.getResources().getDimensionPixelSize(R.dimen.qs_quick_tile_padding);
58
59        mIconFrame = new FrameLayout(context);
60        mIconFrame.setForegroundGravity(Gravity.CENTER);
61        int size = context.getResources().getDimensionPixelSize(R.dimen.qs_quick_tile_size);
62        addView(mIconFrame, new LayoutParams(size, size));
63        mIcon = icon;
64        FrameLayout.LayoutParams params = new FrameLayout.LayoutParams(
65                ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
66        params.setMargins(0, padding, 0, padding);
67        mIconFrame.addView(mIcon, params);
68
69        mTileBackground = newTileBackground();
70        if (mTileBackground instanceof RippleDrawable) {
71            setRipple((RippleDrawable) mTileBackground);
72        }
73        setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_YES);
74        setBackground(mTileBackground);
75
76        setPadding(0, 0, 0, 0);
77        setClipChildren(false);
78        setClipToPadding(false);
79        mCollapsedView = collapsedView;
80        setFocusable(true);
81    }
82
83    protected Drawable newTileBackground() {
84        final int[] attrs = new int[]{android.R.attr.selectableItemBackgroundBorderless};
85        final TypedArray ta = getContext().obtainStyledAttributes(attrs);
86        final Drawable d = ta.getDrawable(0);
87        ta.recycle();
88        return d;
89    }
90
91    private void setRipple(RippleDrawable tileBackground) {
92        mRipple = tileBackground;
93        if (getWidth() != 0) {
94            updateRippleSize(getWidth(), getHeight());
95        }
96    }
97
98    private void updateRippleSize(int width, int height) {
99        // center the touch feedback on the center of the icon, and dial it down a bit
100        final int cx = width / 2;
101        final int cy = mIconFrame.getMeasuredHeight() / 2;
102        final int rad = (int) (mIcon.getHeight() * .85f);
103        mRipple.setHotspotBounds(cx - rad, cy - rad, cx + rad, cy + rad);
104    }
105
106    @Override
107    public void init(QSTile tile) {
108        init(v -> tile.click(), v -> tile.secondaryClick(), view -> {
109            tile.longClick();
110            return true;
111        });
112    }
113
114    public void init(OnClickListener click, OnClickListener secondaryClick,
115            OnLongClickListener longClick) {
116        setOnClickListener(click);
117        setOnLongClickListener(longClick);
118    }
119
120    @Override
121    protected void onLayout(boolean changed, int l, int t, int r, int b) {
122        super.onLayout(changed, l, t, r, b);
123        final int w = getMeasuredWidth();
124        final int h = getMeasuredHeight();
125
126        if (mRipple != null) {
127            updateRippleSize(w, h);
128        }
129    }
130
131    @Override
132    public boolean hasOverlappingRendering() {
133        // Avoid layers for this layout - we don't need them.
134        return false;
135    }
136
137    /**
138     * Update the accessibility order for this view.
139     *
140     * @param previousView the view which should be before this one
141     * @return the last view in this view which is accessible
142     */
143    public View updateAccessibilityOrder(View previousView) {
144        setAccessibilityTraversalAfter(previousView.getId());
145        return this;
146    }
147
148    public void onStateChanged(QSTile.State state) {
149        mHandler.obtainMessage(H.STATE_CHANGED, state).sendToTarget();
150    }
151
152    protected void handleStateChanged(QSTile.State state) {
153        setClickable(state.state != Tile.STATE_UNAVAILABLE);
154        mIcon.setIcon(state);
155        setContentDescription(state.contentDescription);
156        mAccessibilityClass = state.expandedAccessibilityClassName;
157        if (state instanceof QSTile.BooleanState) {
158            boolean newState = ((BooleanState) state).value;
159            if (mTileState != newState) {
160                mClicked = false;
161                mTileState = newState;
162            }
163        }
164    }
165
166    @Override
167    public void setClickable(boolean clickable) {
168        super.setClickable(clickable);
169        setBackground(clickable ? mRipple : null);
170    }
171
172    @Override
173    public int getDetailY() {
174        return getTop() + getHeight() / 2;
175    }
176
177    public QSIconView getIcon() {
178        return mIcon;
179    }
180
181    @Override
182    public boolean performClick() {
183        mClicked = true;
184        return super.performClick();
185    }
186
187    @Override
188    public void onInitializeAccessibilityEvent(AccessibilityEvent event) {
189        super.onInitializeAccessibilityEvent(event);
190        if (!TextUtils.isEmpty(mAccessibilityClass)) {
191            event.setClassName(mAccessibilityClass);
192            if (Switch.class.getName().equals(mAccessibilityClass)) {
193                boolean b = mClicked ? !mTileState : mTileState;
194                String label = getResources()
195                        .getString(b ? R.string.switch_bar_on : R.string.switch_bar_off);
196                event.setContentDescription(label);
197                event.setChecked(b);
198            }
199        }
200    }
201
202    @Override
203    public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) {
204        super.onInitializeAccessibilityNodeInfo(info);
205        if (!TextUtils.isEmpty(mAccessibilityClass)) {
206            info.setClassName(mAccessibilityClass);
207            if (Switch.class.getName().equals(mAccessibilityClass)) {
208                boolean b = mClicked ? !mTileState : mTileState;
209                String label = getResources()
210                        .getString(b ? R.string.switch_bar_on : R.string.switch_bar_off);
211                info.setText(label);
212                info.setChecked(b);
213                info.setCheckable(true);
214            }
215        }
216    }
217
218    private class H extends Handler {
219        private static final int STATE_CHANGED = 1;
220        public H() {
221            super(Looper.getMainLooper());
222        }
223
224        @Override
225        public void handleMessage(Message msg) {
226            if (msg.what == STATE_CHANGED) {
227                handleStateChanged((QSTile.State) msg.obj);
228            }
229        }
230    }
231}
232