Home | History | Annotate | Download | only in qs
      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 
     83     public QSPanel(Context context) {
     84         this(context, null);
     85     }
     86 
     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 
    119     private void updateDetailText() {
    120         mDetailDoneButton.setText(R.string.quick_settings_done);
    121         mDetailSettingsButton.setText(R.string.quick_settings_more_settings);
    122     }
    123 
    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 
    132     public void setCallback(Callback callback) {
    133         mCallback = callback;
    134     }
    135 
    136     public void setHost(QSTileHost host) {
    137         mHost = host;
    138         mFooter.setHost(host);
    139     }
    140 
    141     public QSTileHost getHost() {
    142         return mHost;
    143     }
    144 
    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
    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 
    183     public void setExpanded(boolean expanded) {
    184         if (mExpanded == expanded) return;
    185         mExpanded = expanded;
    186         if (!mExpanded) {
    187             closeDetail();
    188         }
    189     }
    190 
    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 
    208     public void refreshAllTiles() {
    209         for (TileRecord r : mRecords) {
    210             r.tile.refreshState();
    211         }
    212         mFooter.refreshState();
    213     }
    214 
    215     public void showDetailAdapter(boolean show, DetailAdapter adapter) {
    216         Record r = new Record();
    217         r.detailAdapter = adapter;
    218         showDetail(show, r);
    219     }
    220 
    221     private void showDetail(boolean show, Record r) {
    222         mHandler.obtainMessage(H.SHOW_DETAIL, show ? 1 : 0, 0, r).sendToTarget();
    223     }
    224 
    225     private void setTileVisibility(View v, int visibility) {
    226         mHandler.obtainMessage(H.SET_TILE_VISIBILITY, visibility, 0, v).sendToTarget();
    227     }
    228 
    229     private void handleSetTileVisibility(View v, int visibility) {
    230         if (visibility == v.getVisibility()) return;
    231         v.setVisibility(visibility);
    232     }
    233 
    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 
    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 
    317     public boolean isShowingDetail() {
    318         return mDetailRecord != null;
    319     }
    320 
    321     public void closeDetail() {
    322         showDetail(false, mDetailRecord);
    323     }
    324 
    325     public boolean isClosingDetail() {
    326         return mClosingDetail;
    327     }
    328 
    329     public int getGridHeight() {
    330         return mGridHeight;
    331     }
    332 
    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 
    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 
    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 
    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
    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 
    446     private static int exactly(int size) {
    447         return MeasureSpec.makeMeasureSpec(size, MeasureSpec.EXACTLY);
    448     }
    449 
    450     @Override
    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 
    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 
    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 
    498     private void fireShowingDetail(QSTile.DetailAdapter detail) {
    499         if (mCallback != null) {
    500             mCallback.onShowingDetail(detail);
    501         }
    502     }
    503 
    504     private void fireToggleStateChanged(boolean state) {
    505         if (mCallback != null) {
    506             mCallback.onToggleStateChanged(state);
    507         }
    508     }
    509 
    510     private void fireScanStateChanged(boolean state) {
    511         if (mCallback != null) {
    512             mCallback.onScanStateChanged(state);
    513         }
    514     }
    515 
    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
    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 {
    575         void onShowingDetail(QSTile.DetailAdapter detail);
    576         void onToggleStateChanged(boolean state);
    577         void onScanStateChanged(boolean state);
    578     }
    579 }
    580