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 android.animation.Animator;
18 import android.animation.Animator.AnimatorListener;
19 import android.animation.AnimatorListenerAdapter;
20 import android.annotation.Nullable;
21 import android.content.Context;
22 import android.content.Intent;
23 import android.content.res.Configuration;
24 import android.graphics.drawable.Animatable;
25 import android.util.AttributeSet;
26 import android.util.SparseArray;
27 import android.view.View;
28 import android.view.ViewGroup;
29 import android.view.accessibility.AccessibilityEvent;
30 import android.widget.ImageView;
31 import android.widget.LinearLayout;
32 import android.widget.Switch;
33 import android.widget.TextView;
34 
35 import com.android.internal.logging.MetricsLogger;
36 import com.android.systemui.FontSizeUtils;
37 import com.android.systemui.R;
38 import com.android.systemui.qs.QSTile.DetailAdapter;
39 import com.android.systemui.statusbar.phone.BaseStatusBarHeader;
40 import com.android.systemui.statusbar.phone.QSTileHost;
41 
42 public class QSDetail extends LinearLayout {
43 
44     private static final String TAG = "QSDetail";
45     private static final long FADE_DURATION = 300;
46 
47     private final SparseArray<View> mDetailViews = new SparseArray<>();
48 
49     private ViewGroup mDetailContent;
50     private TextView mDetailSettingsButton;
51     private TextView mDetailDoneButton;
52     private QSDetailClipper mClipper;
53     private DetailAdapter mDetailAdapter;
54     private QSPanel mQsPanel;
55 
56     private View mQsDetailHeader;
57     private TextView mQsDetailHeaderTitle;
58     private Switch mQsDetailHeaderSwitch;
59     private ImageView mQsDetailHeaderProgress;
60 
61     private QSTileHost mHost;
62 
63     private boolean mScanState;
64     private boolean mClosingDetail;
65     private boolean mFullyExpanded;
66     private View mQsDetailHeaderBack;
67     private BaseStatusBarHeader mHeader;
68     private boolean mTriggeredExpand;
69     private int mOpenX;
70     private int mOpenY;
71 
QSDetail(Context context, @Nullable AttributeSet attrs)72     public QSDetail(Context context, @Nullable AttributeSet attrs) {
73         super(context, attrs);
74     }
75 
76     @Override
onConfigurationChanged(Configuration newConfig)77     protected void onConfigurationChanged(Configuration newConfig) {
78         super.onConfigurationChanged(newConfig);
79         FontSizeUtils.updateFontSize(mDetailDoneButton, R.dimen.qs_detail_button_text_size);
80         FontSizeUtils.updateFontSize(mDetailSettingsButton, R.dimen.qs_detail_button_text_size);
81 
82         for (int i = 0; i < mDetailViews.size(); i++) {
83             mDetailViews.valueAt(i).dispatchConfigurationChanged(newConfig);
84         }
85     }
86 
87     @Override
onFinishInflate()88     protected void onFinishInflate() {
89         super.onFinishInflate();
90         mDetailContent = (ViewGroup) findViewById(android.R.id.content);
91         mDetailSettingsButton = (TextView) findViewById(android.R.id.button2);
92         mDetailDoneButton = (TextView) findViewById(android.R.id.button1);
93 
94         mQsDetailHeader = findViewById(R.id.qs_detail_header);
95         mQsDetailHeaderBack = mQsDetailHeader.findViewById(com.android.internal.R.id.up);
96         mQsDetailHeaderTitle = (TextView) mQsDetailHeader.findViewById(android.R.id.title);
97         mQsDetailHeaderSwitch = (Switch) mQsDetailHeader.findViewById(android.R.id.toggle);
98         mQsDetailHeaderProgress = (ImageView) findViewById(R.id.qs_detail_header_progress);
99 
100         updateDetailText();
101 
102         mClipper = new QSDetailClipper(this);
103 
104         final OnClickListener doneListener = new OnClickListener() {
105             @Override
106             public void onClick(View v) {
107                 announceForAccessibility(
108                         mContext.getString(R.string.accessibility_desc_quick_settings));
109                 mQsPanel.closeDetail();
110             }
111         };
112         mQsDetailHeaderBack.setOnClickListener(doneListener);
113         mDetailDoneButton.setOnClickListener(doneListener);
114     }
115 
setQsPanel(QSPanel panel, BaseStatusBarHeader header)116     public void setQsPanel(QSPanel panel, BaseStatusBarHeader header) {
117         mQsPanel = panel;
118         mHeader = header;
119         mHeader.setCallback(mQsPanelCallback);
120         mQsPanel.setCallback(mQsPanelCallback);
121     }
122 
setHost(QSTileHost host)123     public void setHost(QSTileHost host) {
124         mHost = host;
125     }
isShowingDetail()126     public boolean isShowingDetail() {
127         return mDetailAdapter != null;
128     }
129 
setFullyExpanded(boolean fullyExpanded)130     public void setFullyExpanded(boolean fullyExpanded) {
131         mFullyExpanded = fullyExpanded;
132     }
133 
setExpanded(boolean qsExpanded)134     public void setExpanded(boolean qsExpanded) {
135         if (!qsExpanded) {
136             mTriggeredExpand = false;
137         }
138     }
139 
updateDetailText()140     private void updateDetailText() {
141         mDetailDoneButton.setText(R.string.quick_settings_done);
142         mDetailSettingsButton.setText(R.string.quick_settings_more_settings);
143     }
144 
updateResources()145     public void updateResources() {
146         updateDetailText();
147     }
148 
isClosingDetail()149     public boolean isClosingDetail() {
150         return mClosingDetail;
151     }
152 
handleShowingDetail(final QSTile.DetailAdapter adapter, int x, int y)153     private void handleShowingDetail(final QSTile.DetailAdapter adapter, int x, int y) {
154         final boolean showingDetail = adapter != null;
155         setClickable(showingDetail);
156         if (showingDetail) {
157             mQsDetailHeaderTitle.setText(adapter.getTitle());
158             final Boolean toggleState = adapter.getToggleState();
159             if (toggleState == null) {
160                 mQsDetailHeaderSwitch.setVisibility(INVISIBLE);
161                 mQsDetailHeader.setClickable(false);
162             } else {
163                 mQsDetailHeaderSwitch.setVisibility(VISIBLE);
164                 mQsDetailHeaderSwitch.setChecked(toggleState);
165                 mQsDetailHeader.setClickable(true);
166                 mQsDetailHeader.setOnClickListener(new OnClickListener() {
167                     @Override
168                     public void onClick(View v) {
169                         boolean checked = !mQsDetailHeaderSwitch.isChecked();
170                         mQsDetailHeaderSwitch.setChecked(checked);
171                         adapter.setToggleState(checked);
172                     }
173                 });
174             }
175             if (!mFullyExpanded) {
176                 mTriggeredExpand = true;
177                 mHost.animateToggleQSExpansion();
178             } else {
179                 mTriggeredExpand = false;
180             }
181             mOpenX = x;
182             mOpenY = y;
183         } else {
184             // Ensure we collapse into the same point we opened from.
185             x = mOpenX;
186             y = mOpenY;
187             if (mTriggeredExpand) {
188                 mHost.animateToggleQSExpansion();
189                 mTriggeredExpand = false;
190             }
191         }
192 
193         boolean visibleDiff = (mDetailAdapter != null) != (adapter != null);
194         if (!visibleDiff && mDetailAdapter == adapter) return;  // already in right state
195         AnimatorListener listener = null;
196         if (adapter != null) {
197             int viewCacheIndex = adapter.getMetricsCategory();
198             View detailView = adapter.createDetailView(mContext, mDetailViews.get(viewCacheIndex),
199                     mDetailContent);
200             if (detailView == null) throw new IllegalStateException("Must return detail view");
201 
202             final Intent settingsIntent = adapter.getSettingsIntent();
203             mDetailSettingsButton.setVisibility(settingsIntent != null ? VISIBLE : GONE);
204             mDetailSettingsButton.setOnClickListener(new OnClickListener() {
205                 @Override
206                 public void onClick(View v) {
207                     mHost.startActivityDismissingKeyguard(settingsIntent);
208                 }
209             });
210 
211             mDetailContent.removeAllViews();
212             mDetailContent.addView(detailView);
213             mDetailViews.put(viewCacheIndex, detailView);
214             MetricsLogger.visible(mContext, adapter.getMetricsCategory());
215             announceForAccessibility(mContext.getString(
216                     R.string.accessibility_quick_settings_detail,
217                     adapter.getTitle()));
218             mDetailAdapter = adapter;
219             listener = mHideGridContentWhenDone;
220             setVisibility(View.VISIBLE);
221         } else {
222             if (mDetailAdapter != null) {
223                 MetricsLogger.hidden(mContext, mDetailAdapter.getMetricsCategory());
224             }
225             mClosingDetail = true;
226             mDetailAdapter = null;
227             listener = mTeardownDetailWhenDone;
228             mHeader.setVisibility(View.VISIBLE);
229             mQsPanel.setGridContentVisibility(true);
230             mQsPanelCallback.onScanStateChanged(false);
231         }
232         sendAccessibilityEvent(AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED);
233         if (visibleDiff) {
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 
handleToggleStateChanged(boolean state)246     private void handleToggleStateChanged(boolean state) {
247         mQsDetailHeaderSwitch.setChecked(state);
248     }
249 
handleScanStateChanged(boolean state)250     private void handleScanStateChanged(boolean state) {
251         if (mScanState == state) return;
252         mScanState = state;
253         final Animatable anim = (Animatable) mQsDetailHeaderProgress.getDrawable();
254         if (state) {
255             mQsDetailHeaderProgress.animate().alpha(1f);
256             anim.start();
257         } else {
258             mQsDetailHeaderProgress.animate().alpha(0f);
259             anim.stop();
260         }
261     }
262 
263     private final QSPanel.Callback mQsPanelCallback = new QSPanel.Callback() {
264         @Override
265         public void onToggleStateChanged(final boolean state) {
266             post(new Runnable() {
267                 @Override
268                 public void run() {
269                     handleToggleStateChanged(state);
270                 }
271             });
272         }
273 
274         @Override
275         public void onShowingDetail(final DetailAdapter detail, final int x, final int y) {
276             post(new Runnable() {
277                 @Override
278                 public void run() {
279                     handleShowingDetail(detail, x, y);
280                 }
281             });
282         }
283 
284         @Override
285         public void onScanStateChanged(final boolean state) {
286             post(new Runnable() {
287                 @Override
288                 public void run() {
289                     handleScanStateChanged(state);
290                 }
291             });
292         }
293     };
294 
295     private final AnimatorListenerAdapter mHideGridContentWhenDone = new AnimatorListenerAdapter() {
296         public void onAnimationCancel(Animator animation) {
297             // If we have been cancelled, remove the listener so that onAnimationEnd doesn't get
298             // called, this will avoid accidentally turning off the grid when we don't want to.
299             animation.removeListener(this);
300         };
301 
302         @Override
303         public void onAnimationEnd(Animator animation) {
304             // Only hide content if still in detail state.
305             if (mDetailAdapter != null) {
306                 mQsPanel.setGridContentVisibility(false);
307                 mHeader.setVisibility(View.INVISIBLE);
308             }
309         }
310     };
311 
312     private final AnimatorListenerAdapter mTeardownDetailWhenDone = new AnimatorListenerAdapter() {
313         public void onAnimationEnd(Animator animation) {
314             mDetailContent.removeAllViews();
315             setVisibility(View.INVISIBLE);
316             mClosingDetail = false;
317         };
318     };
319 }
320