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 static com.android.systemui.qs.tileimpl.QSTileImpl.getColorForState;
20 
21 import android.content.ComponentName;
22 import android.content.Context;
23 import android.content.res.Configuration;
24 import android.content.res.Resources;
25 import android.metrics.LogMaker;
26 import android.os.Handler;
27 import android.os.Message;
28 import android.service.quicksettings.Tile;
29 import android.util.AttributeSet;
30 import android.view.LayoutInflater;
31 import android.view.View;
32 import android.widget.ImageView;
33 import android.widget.LinearLayout;
34 
35 import com.android.internal.logging.MetricsLogger;
36 import com.android.internal.logging.nano.MetricsProto.MetricsEvent;
37 import com.android.settingslib.Utils;
38 import com.android.systemui.Dependency;
39 import com.android.systemui.R;
40 import com.android.systemui.plugins.qs.DetailAdapter;
41 import com.android.systemui.plugins.qs.QSTile;
42 import com.android.systemui.plugins.qs.QSTileView;
43 import com.android.systemui.qs.QSHost.Callback;
44 import com.android.systemui.qs.customize.QSCustomizer;
45 import com.android.systemui.qs.external.CustomTile;
46 import com.android.systemui.settings.BrightnessController;
47 import com.android.systemui.settings.ToggleSliderView;
48 import com.android.systemui.statusbar.policy.BrightnessMirrorController;
49 import com.android.systemui.tuner.TunerService;
50 import com.android.systemui.tuner.TunerService.Tunable;
51 
52 import java.util.ArrayList;
53 import java.util.Collection;
54 
55 /** View that represents the quick settings tile panel. **/
56 public class QSPanel extends LinearLayout implements Tunable, Callback {
57 
58     public static final String QS_SHOW_BRIGHTNESS = "qs_show_brightness";
59 
60     protected final Context mContext;
61     protected final ArrayList<TileRecord> mRecords = new ArrayList<TileRecord>();
62     protected final View mBrightnessView;
63     private final H mHandler = new H();
64     private final View mPageIndicator;
65     private final MetricsLogger mMetricsLogger = Dependency.get(MetricsLogger.class);
66 
67     private int mPanelPaddingBottom;
68     private int mBrightnessPaddingTop;
69     protected boolean mExpanded;
70     protected boolean mListening;
71 
72     private QSDetail.Callback mCallback;
73     private BrightnessController mBrightnessController;
74     protected QSTileHost mHost;
75 
76     protected QSSecurityFooter mFooter;
77     private boolean mGridContentVisible = true;
78 
79     protected QSTileLayout mTileLayout;
80 
81     private QSCustomizer mCustomizePanel;
82     private Record mDetailRecord;
83 
84     private BrightnessMirrorController mBrightnessMirrorController;
85     private View mDivider;
86 
QSPanel(Context context)87     public QSPanel(Context context) {
88         this(context, null);
89     }
90 
QSPanel(Context context, AttributeSet attrs)91     public QSPanel(Context context, AttributeSet attrs) {
92         super(context, attrs);
93         mContext = context;
94 
95         setOrientation(VERTICAL);
96 
97         mBrightnessView = LayoutInflater.from(context).inflate(
98                 R.layout.quick_settings_brightness_dialog, this, false);
99         addView(mBrightnessView);
100 
101         setupTileLayout();
102 
103         mPageIndicator = LayoutInflater.from(context).inflate(
104                 R.layout.qs_page_indicator, this, false);
105         addView(mPageIndicator);
106         if (mTileLayout instanceof PagedTileLayout) {
107             ((PagedTileLayout) mTileLayout).setPageIndicator((PageIndicator) mPageIndicator);
108         }
109 
110         addDivider();
111 
112         mFooter = new QSSecurityFooter(this, context);
113         addView(mFooter.getView());
114 
115         updateResources();
116 
117         mBrightnessController = new BrightnessController(getContext(),
118                 findViewById(R.id.brightness_icon),
119                 findViewById(R.id.brightness_slider));
120     }
121 
addDivider()122     protected void addDivider() {
123         mDivider = LayoutInflater.from(mContext).inflate(R.layout.qs_divider, this, false);
124         mDivider.setBackgroundColor(Utils.applyAlpha(mDivider.getAlpha(),
125                 getColorForState(mContext, Tile.STATE_ACTIVE)));
126         addView(mDivider);
127     }
128 
getDivider()129     public View getDivider() {
130         return mDivider;
131     }
132 
getPageIndicator()133     public View getPageIndicator() {
134         return mPageIndicator;
135     }
136 
setupTileLayout()137     protected void setupTileLayout() {
138         mTileLayout = (QSTileLayout) LayoutInflater.from(mContext).inflate(
139                 R.layout.qs_paged_tile_layout, this, false);
140         mTileLayout.setListening(mListening);
141         addView((View) mTileLayout);
142     }
143 
isShowingCustomize()144     public boolean isShowingCustomize() {
145         return mCustomizePanel != null && mCustomizePanel.isCustomizing();
146     }
147 
148     @Override
onAttachedToWindow()149     protected void onAttachedToWindow() {
150         super.onAttachedToWindow();
151         Dependency.get(TunerService.class).addTunable(this, QS_SHOW_BRIGHTNESS);
152         if (mHost != null) {
153             setTiles(mHost.getTiles());
154         }
155     }
156 
157     @Override
onDetachedFromWindow()158     protected void onDetachedFromWindow() {
159         Dependency.get(TunerService.class).removeTunable(this);
160         if (mHost != null) {
161             mHost.removeCallback(this);
162         }
163         for (TileRecord record : mRecords) {
164             record.tile.removeCallbacks();
165         }
166         super.onDetachedFromWindow();
167     }
168 
169     @Override
onTilesChanged()170     public void onTilesChanged() {
171         setTiles(mHost.getTiles());
172     }
173 
174     @Override
onTuningChanged(String key, String newValue)175     public void onTuningChanged(String key, String newValue) {
176         if (QS_SHOW_BRIGHTNESS.equals(key)) {
177             mBrightnessView.setVisibility(newValue == null || Integer.parseInt(newValue) != 0
178                     ? VISIBLE : GONE);
179         }
180     }
181 
openDetails(String subPanel)182     public void openDetails(String subPanel) {
183         QSTile tile = getTile(subPanel);
184         showDetailAdapter(true, tile.getDetailAdapter(), new int[]{getWidth() / 2, 0});
185     }
186 
getTile(String subPanel)187     private QSTile getTile(String subPanel) {
188         for (int i = 0; i < mRecords.size(); i++) {
189             if (subPanel.equals(mRecords.get(i).tile.getTileSpec())) {
190                 return mRecords.get(i).tile;
191             }
192         }
193         return mHost.createTile(subPanel);
194     }
195 
setBrightnessMirror(BrightnessMirrorController c)196     public void setBrightnessMirror(BrightnessMirrorController c) {
197         mBrightnessMirrorController = c;
198         ToggleSliderView brightnessSlider = findViewById(R.id.brightness_slider);
199         ToggleSliderView mirror = c.getMirror().findViewById(
200                 R.id.brightness_slider);
201         brightnessSlider.setMirror(mirror);
202         brightnessSlider.setMirrorController(c);
203     }
204 
getBrightnessView()205     View getBrightnessView() {
206         return mBrightnessView;
207     }
208 
setCallback(QSDetail.Callback callback)209     public void setCallback(QSDetail.Callback callback) {
210         mCallback = callback;
211     }
212 
setHost(QSTileHost host, QSCustomizer customizer)213     public void setHost(QSTileHost host, QSCustomizer customizer) {
214         mHost = host;
215         mHost.addCallback(this);
216         setTiles(mHost.getTiles());
217         mFooter.setHostEnvironment(host);
218         mCustomizePanel = customizer;
219         if (mCustomizePanel != null) {
220             mCustomizePanel.setHost(mHost);
221         }
222     }
223 
getHost()224     public QSTileHost getHost() {
225         return mHost;
226     }
227 
updateResources()228     public void updateResources() {
229         final Resources res = mContext.getResources();
230         mPanelPaddingBottom = res.getDimensionPixelSize(R.dimen.qs_panel_padding_bottom);
231         mBrightnessPaddingTop = res.getDimensionPixelSize(R.dimen.qs_brightness_padding_top);
232         setPadding(0, mBrightnessPaddingTop, 0, mPanelPaddingBottom);
233         for (TileRecord r : mRecords) {
234             r.tile.clearState();
235         }
236         if (mListening) {
237             refreshAllTiles();
238         }
239         if (mTileLayout != null) {
240             mTileLayout.updateResources();
241         }
242     }
243 
244     @Override
onConfigurationChanged(Configuration newConfig)245     protected void onConfigurationChanged(Configuration newConfig) {
246         super.onConfigurationChanged(newConfig);
247         mFooter.onConfigurationChanged();
248 
249         if (mBrightnessMirrorController != null) {
250             // Reload the mirror in case it got reinflated but we didn't.
251             setBrightnessMirror(mBrightnessMirrorController);
252         }
253     }
254 
onCollapse()255     public void onCollapse() {
256         if (mCustomizePanel != null && mCustomizePanel.isShown()) {
257             mCustomizePanel.hide(mCustomizePanel.getWidth() / 2, mCustomizePanel.getHeight() / 2);
258         }
259     }
260 
setExpanded(boolean expanded)261     public void setExpanded(boolean expanded) {
262         if (mExpanded == expanded) return;
263         mExpanded = expanded;
264         if (!mExpanded && mTileLayout instanceof PagedTileLayout) {
265             ((PagedTileLayout) mTileLayout).setCurrentItem(0, false);
266         }
267         mMetricsLogger.visibility(MetricsEvent.QS_PANEL, mExpanded);
268         if (!mExpanded) {
269             closeDetail();
270         } else {
271             logTiles();
272         }
273     }
274 
isExpanded()275     public boolean isExpanded() {
276         return mExpanded;
277     }
278 
setListening(boolean listening)279     public void setListening(boolean listening) {
280         if (mListening == listening) return;
281         mListening = listening;
282         if (mTileLayout != null) {
283             mTileLayout.setListening(listening);
284         }
285         mFooter.setListening(mListening);
286         if (mListening) {
287             refreshAllTiles();
288         }
289         if (mBrightnessView.getVisibility() == View.VISIBLE) {
290             if (listening) {
291                 mBrightnessController.registerCallbacks();
292             } else {
293                 mBrightnessController.unregisterCallbacks();
294             }
295         }
296     }
297 
refreshAllTiles()298     public void refreshAllTiles() {
299         for (TileRecord r : mRecords) {
300             r.tile.refreshState();
301         }
302         mFooter.refreshState();
303     }
304 
showDetailAdapter(boolean show, DetailAdapter adapter, int[] locationInWindow)305     public void showDetailAdapter(boolean show, DetailAdapter adapter, int[] locationInWindow) {
306         int xInWindow = locationInWindow[0];
307         int yInWindow = locationInWindow[1];
308         ((View) getParent()).getLocationInWindow(locationInWindow);
309 
310         Record r = new Record();
311         r.detailAdapter = adapter;
312         r.x = xInWindow - locationInWindow[0];
313         r.y = yInWindow - locationInWindow[1];
314 
315         locationInWindow[0] = xInWindow;
316         locationInWindow[1] = yInWindow;
317 
318         showDetail(show, r);
319     }
320 
showDetail(boolean show, Record r)321     protected void showDetail(boolean show, Record r) {
322         mHandler.obtainMessage(H.SHOW_DETAIL, show ? 1 : 0, 0, r).sendToTarget();
323     }
324 
setTiles(Collection<QSTile> tiles)325     public void setTiles(Collection<QSTile> tiles) {
326         setTiles(tiles, false);
327     }
328 
setTiles(Collection<QSTile> tiles, boolean collapsedView)329     public void setTiles(Collection<QSTile> tiles, boolean collapsedView) {
330         for (TileRecord record : mRecords) {
331             mTileLayout.removeTile(record);
332             record.tile.removeCallback(record.callback);
333         }
334         mRecords.clear();
335         for (QSTile tile : tiles) {
336             addTile(tile, collapsedView);
337         }
338     }
339 
drawTile(TileRecord r, QSTile.State state)340     protected void drawTile(TileRecord r, QSTile.State state) {
341         r.tileView.onStateChanged(state);
342     }
343 
createTileView(QSTile tile, boolean collapsedView)344     protected QSTileView createTileView(QSTile tile, boolean collapsedView) {
345         return mHost.createTileView(tile, collapsedView);
346     }
347 
shouldShowDetail()348     protected boolean shouldShowDetail() {
349         return mExpanded;
350     }
351 
addTile(final QSTile tile, boolean collapsedView)352     protected TileRecord addTile(final QSTile tile, boolean collapsedView) {
353         final TileRecord r = new TileRecord();
354         r.tile = tile;
355         r.tileView = createTileView(tile, collapsedView);
356         final QSTile.Callback callback = new QSTile.Callback() {
357             @Override
358             public void onStateChanged(QSTile.State state) {
359                 drawTile(r, state);
360             }
361 
362             @Override
363             public void onShowDetail(boolean show) {
364                 // Both the collapsed and full QS panels get this callback, this check determines
365                 // which one should handle showing the detail.
366                 if (shouldShowDetail()) {
367                     QSPanel.this.showDetail(show, r);
368                 }
369             }
370 
371             @Override
372             public void onToggleStateChanged(boolean state) {
373                 if (mDetailRecord == r) {
374                     fireToggleStateChanged(state);
375                 }
376             }
377 
378             @Override
379             public void onScanStateChanged(boolean state) {
380                 r.scanState = state;
381                 if (mDetailRecord == r) {
382                     fireScanStateChanged(r.scanState);
383                 }
384             }
385 
386             @Override
387             public void onAnnouncementRequested(CharSequence announcement) {
388                 if (announcement != null) {
389                     mHandler.obtainMessage(H.ANNOUNCE_FOR_ACCESSIBILITY, announcement)
390                             .sendToTarget();
391                 }
392             }
393         };
394         r.tile.addCallback(callback);
395         r.callback = callback;
396         r.tileView.init(r.tile);
397         r.tile.refreshState();
398         mRecords.add(r);
399 
400         if (mTileLayout != null) {
401             mTileLayout.addTile(r);
402         }
403 
404         return r;
405     }
406 
407 
showEdit(final View v)408     public void showEdit(final View v) {
409         v.post(new Runnable() {
410             @Override
411             public void run() {
412                 if (mCustomizePanel != null) {
413                     if (!mCustomizePanel.isCustomizing()) {
414                         int[] loc = new int[2];
415                         v.getLocationInWindow(loc);
416                         int x = loc[0] + v.getWidth() / 2;
417                         int y = loc[1] + v.getHeight() / 2;
418                         mCustomizePanel.show(x, y);
419                     }
420                 }
421 
422             }
423         });
424     }
425 
closeDetail()426     public void closeDetail() {
427         if (mCustomizePanel != null && mCustomizePanel.isShown()) {
428             // Treat this as a detail panel for now, to make things easy.
429             mCustomizePanel.hide(mCustomizePanel.getWidth() / 2, mCustomizePanel.getHeight() / 2);
430             return;
431         }
432         showDetail(false, mDetailRecord);
433     }
434 
getGridHeight()435     public int getGridHeight() {
436         return getMeasuredHeight();
437     }
438 
handleShowDetail(Record r, boolean show)439     protected void handleShowDetail(Record r, boolean show) {
440         if (r instanceof TileRecord) {
441             handleShowDetailTile((TileRecord) r, show);
442         } else {
443             int x = 0;
444             int y = 0;
445             if (r != null) {
446                 x = r.x;
447                 y = r.y;
448             }
449             handleShowDetailImpl(r, show, x, y);
450         }
451     }
452 
handleShowDetailTile(TileRecord r, boolean show)453     private void handleShowDetailTile(TileRecord r, boolean show) {
454         if ((mDetailRecord != null) == show && mDetailRecord == r) return;
455 
456         if (show) {
457             r.detailAdapter = r.tile.getDetailAdapter();
458             if (r.detailAdapter == null) return;
459         }
460         r.tile.setDetailListening(show);
461         int x = r.tileView.getLeft() + r.tileView.getWidth() / 2;
462         int y = r.tileView.getDetailY() + mTileLayout.getOffsetTop(r) + getTop();
463         handleShowDetailImpl(r, show, x, y);
464     }
465 
handleShowDetailImpl(Record r, boolean show, int x, int y)466     private void handleShowDetailImpl(Record r, boolean show, int x, int y) {
467         setDetailRecord(show ? r : null);
468         fireShowingDetail(show ? r.detailAdapter : null, x, y);
469     }
470 
setDetailRecord(Record r)471     protected void setDetailRecord(Record r) {
472         if (r == mDetailRecord) return;
473         mDetailRecord = r;
474         final boolean scanState = mDetailRecord instanceof TileRecord
475                 && ((TileRecord) mDetailRecord).scanState;
476         fireScanStateChanged(scanState);
477     }
478 
setGridContentVisibility(boolean visible)479     void setGridContentVisibility(boolean visible) {
480         int newVis = visible ? VISIBLE : INVISIBLE;
481         setVisibility(newVis);
482         if (mGridContentVisible != visible) {
483             mMetricsLogger.visibility(MetricsEvent.QS_PANEL, newVis);
484         }
485         mGridContentVisible = visible;
486     }
487 
logTiles()488     private void logTiles() {
489         for (int i = 0; i < mRecords.size(); i++) {
490             QSTile tile = mRecords.get(i).tile;
491             mMetricsLogger.write(tile.populate(new LogMaker(tile.getMetricsCategory())
492                     .setType(MetricsEvent.TYPE_OPEN)));
493         }
494     }
495 
fireShowingDetail(DetailAdapter detail, int x, int y)496     private void fireShowingDetail(DetailAdapter detail, int x, int y) {
497         if (mCallback != null) {
498             mCallback.onShowingDetail(detail, x, y);
499         }
500     }
501 
fireToggleStateChanged(boolean state)502     private void fireToggleStateChanged(boolean state) {
503         if (mCallback != null) {
504             mCallback.onToggleStateChanged(state);
505         }
506     }
507 
fireScanStateChanged(boolean state)508     private void fireScanStateChanged(boolean state) {
509         if (mCallback != null) {
510             mCallback.onScanStateChanged(state);
511         }
512     }
513 
clickTile(ComponentName tile)514     public void clickTile(ComponentName tile) {
515         final String spec = CustomTile.toSpec(tile);
516         final int N = mRecords.size();
517         for (int i = 0; i < N; i++) {
518             if (mRecords.get(i).tile.getTileSpec().equals(spec)) {
519                 mRecords.get(i).tile.click();
520                 break;
521             }
522         }
523     }
524 
getTileLayout()525     QSTileLayout getTileLayout() {
526         return mTileLayout;
527     }
528 
getTileView(QSTile tile)529     QSTileView getTileView(QSTile tile) {
530         for (TileRecord r : mRecords) {
531             if (r.tile == tile) {
532                 return r.tileView;
533             }
534         }
535         return null;
536     }
537 
getFooter()538     public QSSecurityFooter getFooter() {
539         return mFooter;
540     }
541 
showDeviceMonitoringDialog()542     public void showDeviceMonitoringDialog() {
543         mFooter.showDeviceMonitoringDialog();
544     }
545 
546     private class H extends Handler {
547         private static final int SHOW_DETAIL = 1;
548         private static final int SET_TILE_VISIBILITY = 2;
549         private static final int ANNOUNCE_FOR_ACCESSIBILITY = 3;
550 
551         @Override
handleMessage(Message msg)552         public void handleMessage(Message msg) {
553             if (msg.what == SHOW_DETAIL) {
554                 handleShowDetail((Record) msg.obj, msg.arg1 != 0);
555             } else if (msg.what == ANNOUNCE_FOR_ACCESSIBILITY) {
556                 announceForAccessibility((CharSequence) msg.obj);
557             }
558         }
559     }
560 
561     protected static class Record {
562         DetailAdapter detailAdapter;
563         int x;
564         int y;
565     }
566 
567     public static final class TileRecord extends Record {
568         public QSTile tile;
569         public com.android.systemui.plugins.qs.QSTileView tileView;
570         public boolean scanState;
571         public QSTile.Callback callback;
572     }
573 
574     public interface QSTileLayout {
addTile(TileRecord tile)575         void addTile(TileRecord tile);
576 
removeTile(TileRecord tile)577         void removeTile(TileRecord tile);
578 
getOffsetTop(TileRecord tile)579         int getOffsetTop(TileRecord tile);
580 
updateResources()581         boolean updateResources();
582 
setListening(boolean listening)583         void setListening(boolean listening);
584     }
585 }
586