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