Home | History | Annotate | Download | only in qs
      1 /*
      2  * Copyright (C) 2016 The Android Open Source Project
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file
      5  * except in compliance with the License. You may obtain a copy of the License at
      6  *
      7  *      http://www.apache.org/licenses/LICENSE-2.0
      8  *
      9  * Unless required by applicable law or agreed to in writing, software distributed under the
     10  * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
     11  * KIND, either express or implied. See the License for the specific language governing
     12  * permissions and limitations under the License.
     13  */
     14 
     15 package com.android.systemui.qs;
     16 
     17 import static com.android.internal.logging.nano.MetricsProto.MetricsEvent.ACTION_QS_MORE_SETTINGS;
     18 
     19 import android.animation.Animator;
     20 import android.animation.Animator.AnimatorListener;
     21 import android.animation.AnimatorListenerAdapter;
     22 import android.annotation.Nullable;
     23 import android.content.Context;
     24 import android.content.Intent;
     25 import android.content.res.Configuration;
     26 import android.graphics.drawable.Animatable;
     27 import android.util.AttributeSet;
     28 import android.util.SparseArray;
     29 import android.view.View;
     30 import android.view.ViewGroup;
     31 import android.view.accessibility.AccessibilityEvent;
     32 import android.widget.ImageView;
     33 import android.widget.LinearLayout;
     34 import android.widget.Switch;
     35 import android.widget.TextView;
     36 
     37 import com.android.internal.logging.MetricsLogger;
     38 import com.android.systemui.Dependency;
     39 import com.android.systemui.FontSizeUtils;
     40 import com.android.systemui.R;
     41 import com.android.systemui.SysUiServiceProvider;
     42 import com.android.systemui.plugins.ActivityStarter;
     43 import com.android.systemui.plugins.qs.DetailAdapter;
     44 import com.android.systemui.statusbar.CommandQueue;
     45 
     46 public class QSDetail extends LinearLayout {
     47 
     48     private static final String TAG = "QSDetail";
     49     private static final long FADE_DURATION = 300;
     50 
     51     private final SparseArray<View> mDetailViews = new SparseArray<>();
     52 
     53     private ViewGroup mDetailContent;
     54     protected TextView mDetailSettingsButton;
     55     protected TextView mDetailDoneButton;
     56     private QSDetailClipper mClipper;
     57     private DetailAdapter mDetailAdapter;
     58     private QSPanel mQsPanel;
     59 
     60     protected View mQsDetailHeader;
     61     protected TextView mQsDetailHeaderTitle;
     62     protected Switch mQsDetailHeaderSwitch;
     63     protected ImageView mQsDetailHeaderProgress;
     64 
     65     protected QSTileHost mHost;
     66 
     67     private boolean mScanState;
     68     private boolean mClosingDetail;
     69     private boolean mFullyExpanded;
     70     private QuickStatusBarHeader mHeader;
     71     private boolean mTriggeredExpand;
     72     private int mOpenX;
     73     private int mOpenY;
     74     private boolean mAnimatingOpen;
     75     private boolean mSwitchState;
     76     private View mFooter;
     77 
     78     public QSDetail(Context context, @Nullable AttributeSet attrs) {
     79         super(context, attrs);
     80     }
     81 
     82     @Override
     83     protected void onConfigurationChanged(Configuration newConfig) {
     84         super.onConfigurationChanged(newConfig);
     85         FontSizeUtils.updateFontSize(mDetailDoneButton, R.dimen.qs_detail_button_text_size);
     86         FontSizeUtils.updateFontSize(mDetailSettingsButton, R.dimen.qs_detail_button_text_size);
     87 
     88         for (int i = 0; i < mDetailViews.size(); i++) {
     89             mDetailViews.valueAt(i).dispatchConfigurationChanged(newConfig);
     90         }
     91     }
     92 
     93     @Override
     94     protected void onFinishInflate() {
     95         super.onFinishInflate();
     96         mDetailContent = findViewById(android.R.id.content);
     97         mDetailSettingsButton = findViewById(android.R.id.button2);
     98         mDetailDoneButton = findViewById(android.R.id.button1);
     99 
    100         mQsDetailHeader = findViewById(R.id.qs_detail_header);
    101         mQsDetailHeaderTitle = (TextView) mQsDetailHeader.findViewById(android.R.id.title);
    102         mQsDetailHeaderSwitch = (Switch) mQsDetailHeader.findViewById(android.R.id.toggle);
    103         mQsDetailHeaderProgress = findViewById(R.id.qs_detail_header_progress);
    104 
    105         updateDetailText();
    106 
    107         mClipper = new QSDetailClipper(this);
    108 
    109         final OnClickListener doneListener = new OnClickListener() {
    110             @Override
    111             public void onClick(View v) {
    112                 announceForAccessibility(
    113                         mContext.getString(R.string.accessibility_desc_quick_settings));
    114                 mQsPanel.closeDetail();
    115             }
    116         };
    117         mDetailDoneButton.setOnClickListener(doneListener);
    118     }
    119 
    120     public void setQsPanel(QSPanel panel, QuickStatusBarHeader header, View footer) {
    121         mQsPanel = panel;
    122         mHeader = header;
    123         mFooter = footer;
    124         mHeader.setCallback(mQsPanelCallback);
    125         mQsPanel.setCallback(mQsPanelCallback);
    126     }
    127 
    128     public void setHost(QSTileHost host) {
    129         mHost = host;
    130     }
    131     public boolean isShowingDetail() {
    132         return mDetailAdapter != null;
    133     }
    134 
    135     public void setFullyExpanded(boolean fullyExpanded) {
    136         mFullyExpanded = fullyExpanded;
    137     }
    138 
    139     public void setExpanded(boolean qsExpanded) {
    140         if (!qsExpanded) {
    141             mTriggeredExpand = false;
    142         }
    143     }
    144 
    145     private void updateDetailText() {
    146         mDetailDoneButton.setText(R.string.quick_settings_done);
    147         mDetailSettingsButton.setText(R.string.quick_settings_more_settings);
    148     }
    149 
    150     public void updateResources() {
    151         updateDetailText();
    152     }
    153 
    154     public boolean isClosingDetail() {
    155         return mClosingDetail;
    156     }
    157 
    158     public interface Callback {
    159         void onShowingDetail(DetailAdapter detail, int x, int y);
    160         void onToggleStateChanged(boolean state);
    161         void onScanStateChanged(boolean state);
    162     }
    163 
    164     public void handleShowingDetail(final DetailAdapter adapter, int x, int y,
    165             boolean toggleQs) {
    166         final boolean showingDetail = adapter != null;
    167         setClickable(showingDetail);
    168         if (showingDetail) {
    169             setupDetailHeader(adapter);
    170             if (toggleQs && !mFullyExpanded) {
    171                 mTriggeredExpand = true;
    172                 SysUiServiceProvider.getComponent(mContext, CommandQueue.class)
    173                         .animateExpandSettingsPanel(null);
    174             } else {
    175                 mTriggeredExpand = false;
    176             }
    177             mOpenX = x;
    178             mOpenY = y;
    179         } else {
    180             // Ensure we collapse into the same point we opened from.
    181             x = mOpenX;
    182             y = mOpenY;
    183             if (toggleQs && mTriggeredExpand) {
    184                 SysUiServiceProvider.getComponent(mContext, CommandQueue.class)
    185                         .animateCollapsePanels();
    186                 mTriggeredExpand = false;
    187             }
    188         }
    189 
    190         boolean visibleDiff = (mDetailAdapter != null) != (adapter != null);
    191         if (!visibleDiff && mDetailAdapter == adapter) return;  // already in right state
    192         AnimatorListener listener = null;
    193         if (adapter != null) {
    194             int viewCacheIndex = adapter.getMetricsCategory();
    195             View detailView = adapter.createDetailView(mContext, mDetailViews.get(viewCacheIndex),
    196                     mDetailContent);
    197             if (detailView == null) throw new IllegalStateException("Must return detail view");
    198 
    199             setupDetailFooter(adapter);
    200 
    201             mDetailContent.removeAllViews();
    202             mDetailContent.addView(detailView);
    203             mDetailViews.put(viewCacheIndex, detailView);
    204             Dependency.get(MetricsLogger.class).visible(adapter.getMetricsCategory());
    205             announceForAccessibility(mContext.getString(
    206                     R.string.accessibility_quick_settings_detail,
    207                     adapter.getTitle()));
    208             mDetailAdapter = adapter;
    209             listener = mHideGridContentWhenDone;
    210             setVisibility(View.VISIBLE);
    211         } else {
    212             if (mDetailAdapter != null) {
    213                 Dependency.get(MetricsLogger.class).hidden(mDetailAdapter.getMetricsCategory());
    214             }
    215             mClosingDetail = true;
    216             mDetailAdapter = null;
    217             listener = mTeardownDetailWhenDone;
    218             mHeader.setVisibility(View.VISIBLE);
    219             mFooter.setVisibility(View.VISIBLE);
    220             mQsPanel.setGridContentVisibility(true);
    221             mQsPanelCallback.onScanStateChanged(false);
    222         }
    223         sendAccessibilityEvent(AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED);
    224 
    225         animateDetailVisibleDiff(x, y, visibleDiff, listener);
    226     }
    227 
    228     protected void animateDetailVisibleDiff(int x, int y, boolean visibleDiff, AnimatorListener listener) {
    229         if (visibleDiff) {
    230             mAnimatingOpen = mDetailAdapter != null;
    231             if (mFullyExpanded || mDetailAdapter != null) {
    232                 setAlpha(1);
    233                 mClipper.animateCircularClip(x, y, mDetailAdapter != null, listener);
    234             } else {
    235                 animate().alpha(0)
    236                         .setDuration(FADE_DURATION)
    237                         .setListener(listener)
    238                         .start();
    239             }
    240         }
    241     }
    242 
    243     protected void setupDetailFooter(DetailAdapter adapter) {
    244         final Intent settingsIntent = adapter.getSettingsIntent();
    245         mDetailSettingsButton.setVisibility(settingsIntent != null ? VISIBLE : GONE);
    246         mDetailSettingsButton.setOnClickListener(v -> {
    247             Dependency.get(MetricsLogger.class).action(ACTION_QS_MORE_SETTINGS,
    248                     adapter.getMetricsCategory());
    249             Dependency.get(ActivityStarter.class)
    250                     .postStartActivityDismissingKeyguard(settingsIntent, 0);
    251         });
    252     }
    253 
    254     protected void setupDetailHeader(final DetailAdapter adapter) {
    255         mQsDetailHeaderTitle.setText(adapter.getTitle());
    256         final Boolean toggleState = adapter.getToggleState();
    257         if (toggleState == null) {
    258             mQsDetailHeaderSwitch.setVisibility(INVISIBLE);
    259             mQsDetailHeader.setClickable(false);
    260         } else {
    261             mQsDetailHeaderSwitch.setVisibility(VISIBLE);
    262             handleToggleStateChanged(toggleState, adapter.getToggleEnabled());
    263             mQsDetailHeader.setClickable(true);
    264             mQsDetailHeader.setOnClickListener(new OnClickListener() {
    265                 @Override
    266                 public void onClick(View v) {
    267                     boolean checked = !mQsDetailHeaderSwitch.isChecked();
    268                     mQsDetailHeaderSwitch.setChecked(checked);
    269                     adapter.setToggleState(checked);
    270                 }
    271             });
    272         }
    273     }
    274 
    275     private void handleToggleStateChanged(boolean state, boolean toggleEnabled) {
    276         mSwitchState = state;
    277         if (mAnimatingOpen) {
    278             return;
    279         }
    280         mQsDetailHeaderSwitch.setChecked(state);
    281         mQsDetailHeader.setEnabled(toggleEnabled);
    282         mQsDetailHeaderSwitch.setEnabled(toggleEnabled);
    283     }
    284 
    285     private void handleScanStateChanged(boolean state) {
    286         if (mScanState == state) return;
    287         mScanState = state;
    288         final Animatable anim = (Animatable) mQsDetailHeaderProgress.getDrawable();
    289         if (state) {
    290             mQsDetailHeaderProgress.animate().cancel();
    291             mQsDetailHeaderProgress.animate()
    292                     .alpha(1)
    293                     .withEndAction(anim::start)
    294                     .start();
    295         } else {
    296             mQsDetailHeaderProgress.animate().cancel();
    297             mQsDetailHeaderProgress.animate()
    298                     .alpha(0f)
    299                     .withEndAction(anim::stop)
    300                     .start();
    301         }
    302     }
    303 
    304     private void checkPendingAnimations() {
    305         handleToggleStateChanged(mSwitchState,
    306                             mDetailAdapter != null && mDetailAdapter.getToggleEnabled());
    307     }
    308 
    309     protected Callback mQsPanelCallback = new Callback() {
    310         @Override
    311         public void onToggleStateChanged(final boolean state) {
    312             post(new Runnable() {
    313                 @Override
    314                 public void run() {
    315                     handleToggleStateChanged(state,
    316                             mDetailAdapter != null && mDetailAdapter.getToggleEnabled());
    317                 }
    318             });
    319         }
    320 
    321         @Override
    322         public void onShowingDetail(final DetailAdapter detail, final int x, final int y) {
    323             post(new Runnable() {
    324                 @Override
    325                 public void run() {
    326                     handleShowingDetail(detail, x, y, false /* toggleQs */);
    327                 }
    328             });
    329         }
    330 
    331         @Override
    332         public void onScanStateChanged(final boolean state) {
    333             post(new Runnable() {
    334                 @Override
    335                 public void run() {
    336                     handleScanStateChanged(state);
    337                 }
    338             });
    339         }
    340     };
    341 
    342     private final AnimatorListenerAdapter mHideGridContentWhenDone = new AnimatorListenerAdapter() {
    343         public void onAnimationCancel(Animator animation) {
    344             // If we have been cancelled, remove the listener so that onAnimationEnd doesn't get
    345             // called, this will avoid accidentally turning off the grid when we don't want to.
    346             animation.removeListener(this);
    347             mAnimatingOpen = false;
    348             checkPendingAnimations();
    349         };
    350 
    351         @Override
    352         public void onAnimationEnd(Animator animation) {
    353             // Only hide content if still in detail state.
    354             if (mDetailAdapter != null) {
    355                 mQsPanel.setGridContentVisibility(false);
    356                 mHeader.setVisibility(View.INVISIBLE);
    357                 mFooter.setVisibility(View.INVISIBLE);
    358             }
    359             mAnimatingOpen = false;
    360             checkPendingAnimations();
    361         }
    362     };
    363 
    364     private final AnimatorListenerAdapter mTeardownDetailWhenDone = new AnimatorListenerAdapter() {
    365         public void onAnimationEnd(Animator animation) {
    366             mDetailContent.removeAllViews();
    367             setVisibility(View.INVISIBLE);
    368             mClosingDetail = false;
    369         };
    370     };
    371 }
    372