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
17package com.android.systemui.qs;
18
19import static com.android.systemui.qs.tileimpl.QSTileImpl.getColorForState;
20
21import android.content.ComponentName;
22import android.content.Context;
23import android.content.res.Configuration;
24import android.content.res.Resources;
25import android.metrics.LogMaker;
26import android.os.Handler;
27import android.os.Message;
28import android.service.quicksettings.Tile;
29import android.util.AttributeSet;
30import android.view.LayoutInflater;
31import android.view.View;
32import android.widget.ImageView;
33import android.widget.LinearLayout;
34
35import com.android.internal.logging.MetricsLogger;
36import com.android.internal.logging.nano.MetricsProto.MetricsEvent;
37import com.android.settingslib.Utils;
38import com.android.systemui.Dependency;
39import com.android.systemui.R;
40import com.android.systemui.plugins.qs.DetailAdapter;
41import com.android.systemui.plugins.qs.QSTile;
42import com.android.systemui.plugins.qs.QSTileView;
43import com.android.systemui.qs.QSHost.Callback;
44import com.android.systemui.qs.customize.QSCustomizer;
45import com.android.systemui.qs.external.CustomTile;
46import com.android.systemui.settings.BrightnessController;
47import com.android.systemui.settings.ToggleSliderView;
48import com.android.systemui.statusbar.policy.BrightnessMirrorController;
49import com.android.systemui.tuner.TunerService;
50import com.android.systemui.tuner.TunerService.Tunable;
51
52import java.util.ArrayList;
53import java.util.Collection;
54
55/** View that represents the quick settings tile panel. **/
56public 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
87    public QSPanel(Context context) {
88        this(context, null);
89    }
90
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
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
129    public View getDivider() {
130        return mDivider;
131    }
132
133    public View getPageIndicator() {
134        return mPageIndicator;
135    }
136
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
144    public boolean isShowingCustomize() {
145        return mCustomizePanel != null && mCustomizePanel.isCustomizing();
146    }
147
148    @Override
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
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
170    public void onTilesChanged() {
171        setTiles(mHost.getTiles());
172    }
173
174    @Override
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
182    public void openDetails(String subPanel) {
183        QSTile tile = getTile(subPanel);
184        showDetailAdapter(true, tile.getDetailAdapter(), new int[]{getWidth() / 2, 0});
185    }
186
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
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
205    View getBrightnessView() {
206        return mBrightnessView;
207    }
208
209    public void setCallback(QSDetail.Callback callback) {
210        mCallback = callback;
211    }
212
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
224    public QSTileHost getHost() {
225        return mHost;
226    }
227
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
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
255    public void onCollapse() {
256        if (mCustomizePanel != null && mCustomizePanel.isShown()) {
257            mCustomizePanel.hide(mCustomizePanel.getWidth() / 2, mCustomizePanel.getHeight() / 2);
258        }
259    }
260
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
275    public boolean isExpanded() {
276        return mExpanded;
277    }
278
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
298    public void refreshAllTiles() {
299        for (TileRecord r : mRecords) {
300            r.tile.refreshState();
301        }
302        mFooter.refreshState();
303    }
304
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
321    protected void showDetail(boolean show, Record r) {
322        mHandler.obtainMessage(H.SHOW_DETAIL, show ? 1 : 0, 0, r).sendToTarget();
323    }
324
325    public void setTiles(Collection<QSTile> tiles) {
326        setTiles(tiles, false);
327    }
328
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
340    protected void drawTile(TileRecord r, QSTile.State state) {
341        r.tileView.onStateChanged(state);
342    }
343
344    protected QSTileView createTileView(QSTile tile, boolean collapsedView) {
345        return mHost.createTileView(tile, collapsedView);
346    }
347
348    protected boolean shouldShowDetail() {
349        return mExpanded;
350    }
351
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
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
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
435    public int getGridHeight() {
436        return getMeasuredHeight();
437    }
438
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
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
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
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
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
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
496    private void fireShowingDetail(DetailAdapter detail, int x, int y) {
497        if (mCallback != null) {
498            mCallback.onShowingDetail(detail, x, y);
499        }
500    }
501
502    private void fireToggleStateChanged(boolean state) {
503        if (mCallback != null) {
504            mCallback.onToggleStateChanged(state);
505        }
506    }
507
508    private void fireScanStateChanged(boolean state) {
509        if (mCallback != null) {
510            mCallback.onScanStateChanged(state);
511        }
512    }
513
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
525    QSTileLayout getTileLayout() {
526        return mTileLayout;
527    }
528
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
538    public QSSecurityFooter getFooter() {
539        return mFooter;
540    }
541
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
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 {
575        void addTile(TileRecord tile);
576
577        void removeTile(TileRecord tile);
578
579        int getOffsetTop(TileRecord tile);
580
581        boolean updateResources();
582
583        void setListening(boolean listening);
584    }
585}
586