1 /*
2  * Copyright (C) 2014 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 com.android.systemui.qs;
18 
19 import android.animation.Animator;
20 import android.animation.Animator.AnimatorListener;
21 import android.animation.AnimatorListenerAdapter;
22 import android.content.Context;
23 import android.content.Intent;
24 import android.content.res.Configuration;
25 import android.content.res.Resources;
26 import android.os.Handler;
27 import android.os.Message;
28 import android.util.AttributeSet;
29 import android.util.TypedValue;
30 import android.view.LayoutInflater;
31 import android.view.View;
32 import android.view.ViewGroup;
33 import android.view.accessibility.AccessibilityEvent;
34 import android.widget.ImageView;
35 import android.widget.TextView;
36 
37 import com.android.systemui.FontSizeUtils;
38 import com.android.systemui.R;
39 import com.android.systemui.qs.QSTile.DetailAdapter;
40 import com.android.systemui.settings.BrightnessController;
41 import com.android.systemui.settings.ToggleSlider;
42 import com.android.systemui.statusbar.phone.QSTileHost;
43 import com.android.systemui.statusbar.policy.BrightnessMirrorController;
44 
45 import java.util.ArrayList;
46 import java.util.Collection;
47 
48 /** View that represents the quick settings tile panel. **/
49 public class QSPanel extends ViewGroup {
50     private static final float TILE_ASPECT = 1.2f;
51 
52     private final Context mContext;
53     private final ArrayList<TileRecord> mRecords = new ArrayList<TileRecord>();
54     private final View mDetail;
55     private final ViewGroup mDetailContent;
56     private final TextView mDetailSettingsButton;
57     private final TextView mDetailDoneButton;
58     private final View mBrightnessView;
59     private final QSDetailClipper mClipper;
60     private final H mHandler = new H();
61 
62     private int mColumns;
63     private int mCellWidth;
64     private int mCellHeight;
65     private int mLargeCellWidth;
66     private int mLargeCellHeight;
67     private int mPanelPaddingBottom;
68     private int mDualTileUnderlap;
69     private int mBrightnessPaddingTop;
70     private int mGridHeight;
71     private boolean mExpanded;
72     private boolean mListening;
73     private boolean mClosingDetail;
74 
75     private Record mDetailRecord;
76     private Callback mCallback;
77     private BrightnessController mBrightnessController;
78     private QSTileHost mHost;
79 
80     private QSFooter mFooter;
81     private boolean mGridContentVisible = true;
82 
QSPanel(Context context)83     public QSPanel(Context context) {
84         this(context, null);
85     }
86 
QSPanel(Context context, AttributeSet attrs)87     public QSPanel(Context context, AttributeSet attrs) {
88         super(context, attrs);
89         mContext = context;
90 
91         mDetail = LayoutInflater.from(context).inflate(R.layout.qs_detail, this, false);
92         mDetailContent = (ViewGroup) mDetail.findViewById(android.R.id.content);
93         mDetailSettingsButton = (TextView) mDetail.findViewById(android.R.id.button2);
94         mDetailDoneButton = (TextView) mDetail.findViewById(android.R.id.button1);
95         updateDetailText();
96         mDetail.setVisibility(GONE);
97         mDetail.setClickable(true);
98         mBrightnessView = LayoutInflater.from(context).inflate(
99                 R.layout.quick_settings_brightness_dialog, this, false);
100         mFooter = new QSFooter(this, context);
101         addView(mDetail);
102         addView(mBrightnessView);
103         addView(mFooter.getView());
104         mClipper = new QSDetailClipper(mDetail);
105         updateResources();
106 
107         mBrightnessController = new BrightnessController(getContext(),
108                 (ImageView) findViewById(R.id.brightness_icon),
109                 (ToggleSlider) findViewById(R.id.brightness_slider));
110 
111         mDetailDoneButton.setOnClickListener(new OnClickListener() {
112             @Override
113             public void onClick(View v) {
114                 closeDetail();
115             }
116         });
117     }
118 
updateDetailText()119     private void updateDetailText() {
120         mDetailDoneButton.setText(R.string.quick_settings_done);
121         mDetailSettingsButton.setText(R.string.quick_settings_more_settings);
122     }
123 
setBrightnessMirror(BrightnessMirrorController c)124     public void setBrightnessMirror(BrightnessMirrorController c) {
125         super.onFinishInflate();
126         ToggleSlider brightnessSlider = (ToggleSlider) findViewById(R.id.brightness_slider);
127         ToggleSlider mirror = (ToggleSlider) c.getMirror().findViewById(R.id.brightness_slider);
128         brightnessSlider.setMirror(mirror);
129         brightnessSlider.setMirrorController(c);
130     }
131 
setCallback(Callback callback)132     public void setCallback(Callback callback) {
133         mCallback = callback;
134     }
135 
setHost(QSTileHost host)136     public void setHost(QSTileHost host) {
137         mHost = host;
138         mFooter.setHost(host);
139     }
140 
getHost()141     public QSTileHost getHost() {
142         return mHost;
143     }
144 
updateResources()145     public void updateResources() {
146         final Resources res = mContext.getResources();
147         final int columns = Math.max(1, res.getInteger(R.integer.quick_settings_num_columns));
148         mCellHeight = res.getDimensionPixelSize(R.dimen.qs_tile_height);
149         mCellWidth = (int)(mCellHeight * TILE_ASPECT);
150         mLargeCellHeight = res.getDimensionPixelSize(R.dimen.qs_dual_tile_height);
151         mLargeCellWidth = (int)(mLargeCellHeight * TILE_ASPECT);
152         mPanelPaddingBottom = res.getDimensionPixelSize(R.dimen.qs_panel_padding_bottom);
153         mDualTileUnderlap = res.getDimensionPixelSize(R.dimen.qs_dual_tile_padding_vertical);
154         mBrightnessPaddingTop = res.getDimensionPixelSize(R.dimen.qs_brightness_padding_top);
155         if (mColumns != columns) {
156             mColumns = columns;
157             postInvalidate();
158         }
159         if (mListening) {
160             refreshAllTiles();
161         }
162         updateDetailText();
163     }
164 
165     @Override
onConfigurationChanged(Configuration newConfig)166     protected void onConfigurationChanged(Configuration newConfig) {
167         super.onConfigurationChanged(newConfig);
168         FontSizeUtils.updateFontSize(mDetailDoneButton, R.dimen.qs_detail_button_text_size);
169         FontSizeUtils.updateFontSize(mDetailSettingsButton, R.dimen.qs_detail_button_text_size);
170 
171         // We need to poke the detail views as well as they might not be attached to the view
172         // hierarchy but reused at a later point.
173         int count = mRecords.size();
174         for (int i = 0; i < count; i++) {
175             View detailView = mRecords.get(i).detailView;
176             if (detailView != null) {
177                 detailView.dispatchConfigurationChanged(newConfig);
178             }
179         }
180         mFooter.onConfigurationChanged();
181     }
182 
setExpanded(boolean expanded)183     public void setExpanded(boolean expanded) {
184         if (mExpanded == expanded) return;
185         mExpanded = expanded;
186         if (!mExpanded) {
187             closeDetail();
188         }
189     }
190 
setListening(boolean listening)191     public void setListening(boolean listening) {
192         if (mListening == listening) return;
193         mListening = listening;
194         for (TileRecord r : mRecords) {
195             r.tile.setListening(mListening);
196         }
197         mFooter.setListening(mListening);
198         if (mListening) {
199             refreshAllTiles();
200         }
201         if (listening) {
202             mBrightnessController.registerCallbacks();
203         } else {
204             mBrightnessController.unregisterCallbacks();
205         }
206     }
207 
refreshAllTiles()208     public void refreshAllTiles() {
209         for (TileRecord r : mRecords) {
210             r.tile.refreshState();
211         }
212         mFooter.refreshState();
213     }
214 
showDetailAdapter(boolean show, DetailAdapter adapter)215     public void showDetailAdapter(boolean show, DetailAdapter adapter) {
216         Record r = new Record();
217         r.detailAdapter = adapter;
218         showDetail(show, r);
219     }
220 
showDetail(boolean show, Record r)221     private void showDetail(boolean show, Record r) {
222         mHandler.obtainMessage(H.SHOW_DETAIL, show ? 1 : 0, 0, r).sendToTarget();
223     }
224 
setTileVisibility(View v, int visibility)225     private void setTileVisibility(View v, int visibility) {
226         mHandler.obtainMessage(H.SET_TILE_VISIBILITY, visibility, 0, v).sendToTarget();
227     }
228 
handleSetTileVisibility(View v, int visibility)229     private void handleSetTileVisibility(View v, int visibility) {
230         if (visibility == v.getVisibility()) return;
231         v.setVisibility(visibility);
232     }
233 
setTiles(Collection<QSTile<?>> tiles)234     public void setTiles(Collection<QSTile<?>> tiles) {
235         for (TileRecord record : mRecords) {
236             removeView(record.tileView);
237         }
238         mRecords.clear();
239         for (QSTile<?> tile : tiles) {
240             addTile(tile);
241         }
242         if (isShowingDetail()) {
243             mDetail.bringToFront();
244         }
245     }
246 
addTile(final QSTile<?> tile)247     private void addTile(final QSTile<?> tile) {
248         final TileRecord r = new TileRecord();
249         r.tile = tile;
250         r.tileView = tile.createTileView(mContext);
251         r.tileView.setVisibility(View.GONE);
252         final QSTile.Callback callback = new QSTile.Callback() {
253             @Override
254             public void onStateChanged(QSTile.State state) {
255                 int visibility = state.visible ? VISIBLE : GONE;
256                 if (state.visible && !mGridContentVisible) {
257 
258                     // We don't want to show it if the content is hidden,
259                     // then we just set it to invisible, to ensure that it gets visible again
260                     visibility = INVISIBLE;
261                 }
262                 setTileVisibility(r.tileView, visibility);
263                 r.tileView.onStateChanged(state);
264             }
265             @Override
266             public void onShowDetail(boolean show) {
267                 QSPanel.this.showDetail(show, r);
268             }
269             @Override
270             public void onToggleStateChanged(boolean state) {
271                 if (mDetailRecord == r) {
272                     fireToggleStateChanged(state);
273                 }
274             }
275             @Override
276             public void onScanStateChanged(boolean state) {
277                 r.scanState = state;
278                 if (mDetailRecord == r) {
279                     fireScanStateChanged(r.scanState);
280                 }
281             }
282 
283             @Override
284             public void onAnnouncementRequested(CharSequence announcement) {
285                 announceForAccessibility(announcement);
286             }
287         };
288         r.tile.setCallback(callback);
289         final View.OnClickListener click = new View.OnClickListener() {
290             @Override
291             public void onClick(View v) {
292                 r.tile.click();
293             }
294         };
295         final View.OnClickListener clickSecondary = new View.OnClickListener() {
296             @Override
297             public void onClick(View v) {
298                 r.tile.secondaryClick();
299             }
300         };
301         final View.OnLongClickListener longClick = new View.OnLongClickListener() {
302             @Override
303             public boolean onLongClick(View v) {
304                 r.tile.longClick();
305                 return true;
306             }
307         };
308         r.tileView.init(click, clickSecondary, longClick);
309         r.tile.setListening(mListening);
310         callback.onStateChanged(r.tile.getState());
311         r.tile.refreshState();
312         mRecords.add(r);
313 
314         addView(r.tileView);
315     }
316 
isShowingDetail()317     public boolean isShowingDetail() {
318         return mDetailRecord != null;
319     }
320 
closeDetail()321     public void closeDetail() {
322         showDetail(false, mDetailRecord);
323     }
324 
isClosingDetail()325     public boolean isClosingDetail() {
326         return mClosingDetail;
327     }
328 
getGridHeight()329     public int getGridHeight() {
330         return mGridHeight;
331     }
332 
handleShowDetail(Record r, boolean show)333     private void handleShowDetail(Record r, boolean show) {
334         if (r instanceof TileRecord) {
335             handleShowDetailTile((TileRecord) r, show);
336         } else {
337             handleShowDetailImpl(r, show, getWidth() /* x */, 0/* y */);
338         }
339     }
340 
handleShowDetailTile(TileRecord r, boolean show)341     private void handleShowDetailTile(TileRecord r, boolean show) {
342         if ((mDetailRecord != null) == show) return;
343 
344         if (show) {
345             r.detailAdapter = r.tile.getDetailAdapter();
346             if (r.detailAdapter == null) return;
347         }
348         int x = r.tileView.getLeft() + r.tileView.getWidth() / 2;
349         int y = r.tileView.getTop() + r.tileView.getHeight() / 2;
350         handleShowDetailImpl(r, show, x, y);
351     }
352 
handleShowDetailImpl(Record r, boolean show, int x, int y)353     private void handleShowDetailImpl(Record r, boolean show, int x, int y) {
354         if ((mDetailRecord != null) == show) return;  // already in right state
355         DetailAdapter detailAdapter = null;
356         AnimatorListener listener = null;
357         if (show) {
358             detailAdapter = r.detailAdapter;
359             r.detailView = detailAdapter.createDetailView(mContext, r.detailView, mDetailContent);
360             if (r.detailView == null) throw new IllegalStateException("Must return detail view");
361 
362             final Intent settingsIntent = detailAdapter.getSettingsIntent();
363             mDetailSettingsButton.setVisibility(settingsIntent != null ? VISIBLE : GONE);
364             mDetailSettingsButton.setOnClickListener(new OnClickListener() {
365                 @Override
366                 public void onClick(View v) {
367                     mHost.startSettingsActivity(settingsIntent);
368                 }
369             });
370 
371             mDetailContent.removeAllViews();
372             mDetail.bringToFront();
373             mDetailContent.addView(r.detailView);
374             setDetailRecord(r);
375             listener = mHideGridContentWhenDone;
376         } else {
377             mClosingDetail = true;
378             setGridContentVisibility(true);
379             listener = mTeardownDetailWhenDone;
380             fireScanStateChanged(false);
381         }
382         sendAccessibilityEvent(AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED);
383         fireShowingDetail(show ? detailAdapter : null);
384         mClipper.animateCircularClip(x, y, show, listener);
385     }
386 
setGridContentVisibility(boolean visible)387     private void setGridContentVisibility(boolean visible) {
388         int newVis = visible ? VISIBLE : INVISIBLE;
389         for (int i = 0; i < mRecords.size(); i++) {
390             TileRecord tileRecord = mRecords.get(i);
391             if (tileRecord.tileView.getVisibility() != GONE) {
392                 tileRecord.tileView.setVisibility(newVis);
393             }
394         }
395         mBrightnessView.setVisibility(newVis);
396         mGridContentVisible = visible;
397     }
398 
399     @Override
onMeasure(int widthMeasureSpec, int heightMeasureSpec)400     protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
401         final int width = MeasureSpec.getSize(widthMeasureSpec);
402         mBrightnessView.measure(exactly(width), MeasureSpec.UNSPECIFIED);
403         final int brightnessHeight = mBrightnessView.getMeasuredHeight() + mBrightnessPaddingTop;
404         mFooter.getView().measure(exactly(width), MeasureSpec.UNSPECIFIED);
405         int r = -1;
406         int c = -1;
407         int rows = 0;
408         boolean rowIsDual = false;
409         for (TileRecord record : mRecords) {
410             if (record.tileView.getVisibility() == GONE) continue;
411             // wrap to next column if we've reached the max # of columns
412             // also don't allow dual + single tiles on the same row
413             if (r == -1 || c == (mColumns - 1) || rowIsDual != record.tile.supportsDualTargets()) {
414                 r++;
415                 c = 0;
416                 rowIsDual = record.tile.supportsDualTargets();
417             } else {
418                 c++;
419             }
420             record.row = r;
421             record.col = c;
422             rows = r + 1;
423         }
424 
425         for (TileRecord record : mRecords) {
426             if (record.tileView.setDual(record.tile.supportsDualTargets())) {
427                 record.tileView.handleStateChanged(record.tile.getState());
428             }
429             if (record.tileView.getVisibility() == GONE) continue;
430             final int cw = record.row == 0 ? mLargeCellWidth : mCellWidth;
431             final int ch = record.row == 0 ? mLargeCellHeight : mCellHeight;
432             record.tileView.measure(exactly(cw), exactly(ch));
433         }
434         int h = rows == 0 ? brightnessHeight : (getRowTop(rows) + mPanelPaddingBottom);
435         if (mFooter.hasFooter()) {
436             h += mFooter.getView().getMeasuredHeight();
437         }
438         mDetail.measure(exactly(width), MeasureSpec.UNSPECIFIED);
439         if (mDetail.getMeasuredHeight() < h) {
440             mDetail.measure(exactly(width), exactly(h));
441         }
442         mGridHeight = h;
443         setMeasuredDimension(width, Math.max(h, mDetail.getMeasuredHeight()));
444     }
445 
exactly(int size)446     private static int exactly(int size) {
447         return MeasureSpec.makeMeasureSpec(size, MeasureSpec.EXACTLY);
448     }
449 
450     @Override
onLayout(boolean changed, int l, int t, int r, int b)451     protected void onLayout(boolean changed, int l, int t, int r, int b) {
452         final int w = getWidth();
453         mBrightnessView.layout(0, mBrightnessPaddingTop,
454                 mBrightnessView.getMeasuredWidth(),
455                 mBrightnessPaddingTop + mBrightnessView.getMeasuredHeight());
456         boolean isRtl = getLayoutDirection() == LAYOUT_DIRECTION_RTL;
457         for (TileRecord record : mRecords) {
458             if (record.tileView.getVisibility() == GONE) continue;
459             final int cols = getColumnCount(record.row);
460             final int cw = record.row == 0 ? mLargeCellWidth : mCellWidth;
461             final int extra = (w - cw * cols) / (cols + 1);
462             int left = record.col * cw + (record.col + 1) * extra;
463             final int top = getRowTop(record.row);
464             int right;
465             int tileWith = record.tileView.getMeasuredWidth();
466             if (isRtl) {
467                 right = w - left;
468                 left = right - tileWith;
469             } else {
470                 right = left + tileWith;
471             }
472             record.tileView.layout(left, top, right, top + record.tileView.getMeasuredHeight());
473         }
474         final int dh = Math.max(mDetail.getMeasuredHeight(), getMeasuredHeight());
475         mDetail.layout(0, 0, mDetail.getMeasuredWidth(), dh);
476         if (mFooter.hasFooter()) {
477             View footer = mFooter.getView();
478             footer.layout(0, getMeasuredHeight() - footer.getMeasuredHeight(),
479                     footer.getMeasuredWidth(), getMeasuredHeight());
480         }
481     }
482 
getRowTop(int row)483     private int getRowTop(int row) {
484         if (row <= 0) return mBrightnessView.getMeasuredHeight() + mBrightnessPaddingTop;
485         return mBrightnessView.getMeasuredHeight() + mBrightnessPaddingTop
486                 + mLargeCellHeight - mDualTileUnderlap + (row - 1) * mCellHeight;
487     }
488 
getColumnCount(int row)489     private int getColumnCount(int row) {
490         int cols = 0;
491         for (TileRecord record : mRecords) {
492             if (record.tileView.getVisibility() == GONE) continue;
493             if (record.row == row) cols++;
494         }
495         return cols;
496     }
497 
fireShowingDetail(QSTile.DetailAdapter detail)498     private void fireShowingDetail(QSTile.DetailAdapter detail) {
499         if (mCallback != null) {
500             mCallback.onShowingDetail(detail);
501         }
502     }
503 
fireToggleStateChanged(boolean state)504     private void fireToggleStateChanged(boolean state) {
505         if (mCallback != null) {
506             mCallback.onToggleStateChanged(state);
507         }
508     }
509 
fireScanStateChanged(boolean state)510     private void fireScanStateChanged(boolean state) {
511         if (mCallback != null) {
512             mCallback.onScanStateChanged(state);
513         }
514     }
515 
setDetailRecord(Record r)516     private void setDetailRecord(Record r) {
517         if (r == mDetailRecord) return;
518         mDetailRecord = r;
519         final boolean scanState = mDetailRecord instanceof TileRecord
520                 && ((TileRecord) mDetailRecord).scanState;
521         fireScanStateChanged(scanState);
522     }
523 
524     private class H extends Handler {
525         private static final int SHOW_DETAIL = 1;
526         private static final int SET_TILE_VISIBILITY = 2;
527         @Override
handleMessage(Message msg)528         public void handleMessage(Message msg) {
529             if (msg.what == SHOW_DETAIL) {
530                 handleShowDetail((Record)msg.obj, msg.arg1 != 0);
531             } else if (msg.what == SET_TILE_VISIBILITY) {
532                 handleSetTileVisibility((View)msg.obj, msg.arg1);
533             }
534         }
535     }
536 
537     private static class Record {
538         View detailView;
539         DetailAdapter detailAdapter;
540     }
541 
542     private static final class TileRecord extends Record {
543         QSTile<?> tile;
544         QSTileView tileView;
545         int row;
546         int col;
547         boolean scanState;
548     }
549 
550     private final AnimatorListenerAdapter mTeardownDetailWhenDone = new AnimatorListenerAdapter() {
551         public void onAnimationEnd(Animator animation) {
552             mDetailContent.removeAllViews();
553             setDetailRecord(null);
554             mClosingDetail = false;
555         };
556     };
557 
558     private final AnimatorListenerAdapter mHideGridContentWhenDone = new AnimatorListenerAdapter() {
559         public void onAnimationCancel(Animator animation) {
560             // If we have been cancelled, remove the listener so that onAnimationEnd doesn't get
561             // called, this will avoid accidentally turning off the grid when we don't want to.
562             animation.removeListener(this);
563         };
564 
565         @Override
566         public void onAnimationEnd(Animator animation) {
567             // Only hide content if still in detail state.
568             if (mDetailRecord != null) {
569                 setGridContentVisibility(false);
570             }
571         }
572     };
573 
574     public interface Callback {
onShowingDetail(QSTile.DetailAdapter detail)575         void onShowingDetail(QSTile.DetailAdapter detail);
onToggleStateChanged(boolean state)576         void onToggleStateChanged(boolean state);
onScanStateChanged(boolean state)577         void onScanStateChanged(boolean state);
578     }
579 }
580