• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 
QSDetail(Context context, @Nullable AttributeSet attrs)78     public QSDetail(Context context, @Nullable AttributeSet attrs) {
79         super(context, attrs);
80     }
81 
82     @Override
onConfigurationChanged(Configuration newConfig)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
onFinishInflate()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 
setQsPanel(QSPanel panel, QuickStatusBarHeader header, View footer)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 
setHost(QSTileHost host)128     public void setHost(QSTileHost host) {
129         mHost = host;
130     }
isShowingDetail()131     public boolean isShowingDetail() {
132         return mDetailAdapter != null;
133     }
134 
setFullyExpanded(boolean fullyExpanded)135     public void setFullyExpanded(boolean fullyExpanded) {
136         mFullyExpanded = fullyExpanded;
137     }
138 
setExpanded(boolean qsExpanded)139     public void setExpanded(boolean qsExpanded) {
140         if (!qsExpanded) {
141             mTriggeredExpand = false;
142         }
143     }
144 
updateDetailText()145     private void updateDetailText() {
146         mDetailDoneButton.setText(R.string.quick_settings_done);
147         mDetailSettingsButton.setText(R.string.quick_settings_more_settings);
148     }
149 
updateResources()150     public void updateResources() {
151         updateDetailText();
152     }
153 
isClosingDetail()154     public boolean isClosingDetail() {
155         return mClosingDetail;
156     }
157 
158     public interface Callback {
onShowingDetail(DetailAdapter detail, int x, int y)159         void onShowingDetail(DetailAdapter detail, int x, int y);
onToggleStateChanged(boolean state)160         void onToggleStateChanged(boolean state);
onScanStateChanged(boolean state)161         void onScanStateChanged(boolean state);
162     }
163 
handleShowingDetail(final DetailAdapter adapter, int x, int y, boolean toggleQs)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 
animateDetailVisibleDiff(int x, int y, boolean visibleDiff, AnimatorListener listener)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 
setupDetailFooter(DetailAdapter adapter)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 
setupDetailHeader(final DetailAdapter adapter)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 
handleToggleStateChanged(boolean state, boolean toggleEnabled)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 
handleScanStateChanged(boolean state)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 
checkPendingAnimations()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                     if (isAttachedToWindow()) {
327                         handleShowingDetail(detail, x, y, false /* toggleQs */);
328                     }
329                 }
330             });
331         }
332 
333         @Override
334         public void onScanStateChanged(final boolean state) {
335             post(new Runnable() {
336                 @Override
337                 public void run() {
338                     handleScanStateChanged(state);
339                 }
340             });
341         }
342     };
343 
344     private final AnimatorListenerAdapter mHideGridContentWhenDone = new AnimatorListenerAdapter() {
345         public void onAnimationCancel(Animator animation) {
346             // If we have been cancelled, remove the listener so that onAnimationEnd doesn't get
347             // called, this will avoid accidentally turning off the grid when we don't want to.
348             animation.removeListener(this);
349             mAnimatingOpen = false;
350             checkPendingAnimations();
351         };
352 
353         @Override
354         public void onAnimationEnd(Animator animation) {
355             // Only hide content if still in detail state.
356             if (mDetailAdapter != null) {
357                 mQsPanel.setGridContentVisibility(false);
358                 mHeader.setVisibility(View.INVISIBLE);
359                 mFooter.setVisibility(View.INVISIBLE);
360             }
361             mAnimatingOpen = false;
362             checkPendingAnimations();
363         }
364     };
365 
366     private final AnimatorListenerAdapter mTeardownDetailWhenDone = new AnimatorListenerAdapter() {
367         public void onAnimationEnd(Animator animation) {
368             mDetailContent.removeAllViews();
369             setVisibility(View.INVISIBLE);
370             mClosingDetail = false;
371         };
372     };
373 }
374