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
15package com.android.systemui.qs;
16
17import static com.android.internal.logging.nano.MetricsProto.MetricsEvent.ACTION_QS_MORE_SETTINGS;
18
19import android.animation.Animator;
20import android.animation.Animator.AnimatorListener;
21import android.animation.AnimatorListenerAdapter;
22import android.annotation.Nullable;
23import android.content.Context;
24import android.content.Intent;
25import android.content.res.Configuration;
26import android.graphics.drawable.Animatable;
27import android.util.AttributeSet;
28import android.util.SparseArray;
29import android.view.View;
30import android.view.ViewGroup;
31import android.view.accessibility.AccessibilityEvent;
32import android.widget.ImageView;
33import android.widget.LinearLayout;
34import android.widget.Switch;
35import android.widget.TextView;
36
37import com.android.internal.logging.MetricsLogger;
38import com.android.systemui.Dependency;
39import com.android.systemui.FontSizeUtils;
40import com.android.systemui.R;
41import com.android.systemui.SysUiServiceProvider;
42import com.android.systemui.plugins.ActivityStarter;
43import com.android.systemui.plugins.qs.DetailAdapter;
44import com.android.systemui.statusbar.CommandQueue;
45
46public 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().alpha(1f);
291            anim.start();
292        } else {
293            mQsDetailHeaderProgress.animate().alpha(0f);
294            anim.stop();
295        }
296    }
297
298    private void checkPendingAnimations() {
299        handleToggleStateChanged(mSwitchState,
300                            mDetailAdapter != null && mDetailAdapter.getToggleEnabled());
301    }
302
303    protected Callback mQsPanelCallback = new Callback() {
304        @Override
305        public void onToggleStateChanged(final boolean state) {
306            post(new Runnable() {
307                @Override
308                public void run() {
309                    handleToggleStateChanged(state,
310                            mDetailAdapter != null && mDetailAdapter.getToggleEnabled());
311                }
312            });
313        }
314
315        @Override
316        public void onShowingDetail(final DetailAdapter detail, final int x, final int y) {
317            post(new Runnable() {
318                @Override
319                public void run() {
320                    handleShowingDetail(detail, x, y, true /* toggleQs */);
321                }
322            });
323        }
324
325        @Override
326        public void onScanStateChanged(final boolean state) {
327            post(new Runnable() {
328                @Override
329                public void run() {
330                    handleScanStateChanged(state);
331                }
332            });
333        }
334    };
335
336    private final AnimatorListenerAdapter mHideGridContentWhenDone = new AnimatorListenerAdapter() {
337        public void onAnimationCancel(Animator animation) {
338            // If we have been cancelled, remove the listener so that onAnimationEnd doesn't get
339            // called, this will avoid accidentally turning off the grid when we don't want to.
340            animation.removeListener(this);
341            mAnimatingOpen = false;
342            checkPendingAnimations();
343        };
344
345        @Override
346        public void onAnimationEnd(Animator animation) {
347            // Only hide content if still in detail state.
348            if (mDetailAdapter != null) {
349                mQsPanel.setGridContentVisibility(false);
350                mHeader.setVisibility(View.INVISIBLE);
351                mFooter.setVisibility(View.INVISIBLE);
352            }
353            mAnimatingOpen = false;
354            checkPendingAnimations();
355        }
356    };
357
358    private final AnimatorListenerAdapter mTeardownDetailWhenDone = new AnimatorListenerAdapter() {
359        public void onAnimationEnd(Animator animation) {
360            mDetailContent.removeAllViews();
361            setVisibility(View.INVISIBLE);
362            mClosingDetail = false;
363        };
364    };
365}
366