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