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