1 /*
2  * Copyright (C) 2015 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 package com.android.systemui.qs.customize;
17 
18 import android.animation.Animator;
19 import android.animation.Animator.AnimatorListener;
20 import android.animation.AnimatorListenerAdapter;
21 import android.content.Context;
22 import android.content.res.Configuration;
23 import android.graphics.Point;
24 import android.os.Bundle;
25 import android.support.v7.widget.DefaultItemAnimator;
26 import android.support.v7.widget.GridLayoutManager;
27 import android.support.v7.widget.RecyclerView;
28 import android.util.AttributeSet;
29 import android.util.TypedValue;
30 import android.view.ContextThemeWrapper;
31 import android.view.LayoutInflater;
32 import android.view.Menu;
33 import android.view.MenuItem;
34 import android.view.View;
35 import android.widget.LinearLayout;
36 import android.widget.Toolbar;
37 import android.widget.Toolbar.OnMenuItemClickListener;
38 
39 import com.android.internal.logging.MetricsLogger;
40 import com.android.internal.logging.nano.MetricsProto;
41 import com.android.settingslib.Utils;
42 import com.android.systemui.Dependency;
43 import com.android.systemui.R;
44 import com.android.systemui.plugins.qs.QS;
45 import com.android.systemui.plugins.qs.QSTile;
46 import com.android.systemui.qs.QSDetailClipper;
47 import com.android.systemui.qs.QSTileHost;
48 import com.android.systemui.statusbar.phone.LightBarController;
49 import com.android.systemui.statusbar.phone.NotificationsQuickSettingsContainer;
50 import com.android.systemui.statusbar.policy.KeyguardMonitor;
51 import com.android.systemui.statusbar.policy.KeyguardMonitor.Callback;
52 
53 import java.util.ArrayList;
54 import java.util.List;
55 
56 /**
57  * Allows full-screen customization of QS, through show() and hide().
58  *
59  * This adds itself to the status bar window, so it can appear on top of quick settings and
60  * *someday* do fancy animations to get into/out of it.
61  */
62 public class QSCustomizer extends LinearLayout implements OnMenuItemClickListener {
63 
64     private static final int MENU_RESET = Menu.FIRST;
65     private static final String EXTRA_QS_CUSTOMIZING = "qs_customizing";
66 
67     private final QSDetailClipper mClipper;
68     private final LightBarController mLightBarController;
69     private final TileQueryHelper mTileQueryHelper;
70 
71     private boolean isShown;
72     private QSTileHost mHost;
73     private RecyclerView mRecyclerView;
74     private TileAdapter mTileAdapter;
75     private Toolbar mToolbar;
76     private boolean mCustomizing;
77     private NotificationsQuickSettingsContainer mNotifQsContainer;
78     private QS mQs;
79     private int mX;
80     private int mY;
81     private boolean mOpening;
82     private boolean mIsShowingNavBackdrop;
83 
QSCustomizer(Context context, AttributeSet attrs)84     public QSCustomizer(Context context, AttributeSet attrs) {
85         super(new ContextThemeWrapper(context, R.style.edit_theme), attrs);
86 
87         LayoutInflater.from(getContext()).inflate(R.layout.qs_customize_panel_content, this);
88         mClipper = new QSDetailClipper(findViewById(R.id.customize_container));
89         mToolbar = findViewById(com.android.internal.R.id.action_bar);
90         TypedValue value = new TypedValue();
91         mContext.getTheme().resolveAttribute(android.R.attr.homeAsUpIndicator, value, true);
92         mToolbar.setNavigationIcon(
93                 getResources().getDrawable(value.resourceId, mContext.getTheme()));
94         mToolbar.setNavigationOnClickListener(new OnClickListener() {
95             @Override
96             public void onClick(View v) {
97                 hide((int) v.getX() + v.getWidth() / 2, (int) v.getY() + v.getHeight() / 2);
98             }
99         });
100         mToolbar.setOnMenuItemClickListener(this);
101         mToolbar.getMenu().add(Menu.NONE, MENU_RESET, 0,
102                 mContext.getString(com.android.internal.R.string.reset));
103         mToolbar.setTitle(R.string.qs_edit);
104         int accentColor = Utils.getColorAttr(context, android.R.attr.colorAccent);
105         mToolbar.setTitleTextColor(accentColor);
106         mToolbar.getNavigationIcon().setTint(accentColor);
107         mToolbar.getOverflowIcon().setTint(accentColor);
108         mRecyclerView = findViewById(android.R.id.list);
109         mTileAdapter = new TileAdapter(getContext());
110         mTileQueryHelper = new TileQueryHelper(context, mTileAdapter);
111         mRecyclerView.setAdapter(mTileAdapter);
112         mTileAdapter.getItemTouchHelper().attachToRecyclerView(mRecyclerView);
113         GridLayoutManager layout = new GridLayoutManager(getContext(), 3);
114         layout.setSpanSizeLookup(mTileAdapter.getSizeLookup());
115         mRecyclerView.setLayoutManager(layout);
116         mRecyclerView.addItemDecoration(mTileAdapter.getItemDecoration());
117         DefaultItemAnimator animator = new DefaultItemAnimator();
118         animator.setMoveDuration(TileAdapter.MOVE_DURATION);
119         mRecyclerView.setItemAnimator(animator);
120         mLightBarController = Dependency.get(LightBarController.class);
121         updateNavBackDrop(getResources().getConfiguration());
122     }
123 
124     @Override
onConfigurationChanged(Configuration newConfig)125     protected void onConfigurationChanged(Configuration newConfig) {
126         super.onConfigurationChanged(newConfig);
127         updateNavBackDrop(newConfig);
128     }
129 
updateNavBackDrop(Configuration newConfig)130     private void updateNavBackDrop(Configuration newConfig) {
131         View navBackdrop = findViewById(R.id.nav_bar_background);
132         mIsShowingNavBackdrop = newConfig.smallestScreenWidthDp >= 600
133                 || newConfig.orientation != Configuration.ORIENTATION_LANDSCAPE;
134         if (navBackdrop != null) {
135             navBackdrop.setVisibility(mIsShowingNavBackdrop ? View.VISIBLE : View.GONE);
136         }
137         updateNavColors();
138     }
139 
updateNavColors()140     private void updateNavColors() {
141         mLightBarController.setQsCustomizing(mIsShowingNavBackdrop && isShown);
142     }
143 
setHost(QSTileHost host)144     public void setHost(QSTileHost host) {
145         mHost = host;
146         mTileAdapter.setHost(host);
147     }
148 
setContainer(NotificationsQuickSettingsContainer notificationsQsContainer)149     public void setContainer(NotificationsQuickSettingsContainer notificationsQsContainer) {
150         mNotifQsContainer = notificationsQsContainer;
151     }
152 
setQs(QS qs)153     public void setQs(QS qs) {
154         mQs = qs;
155     }
156 
show(int x, int y)157     public void show(int x, int y) {
158         if (!isShown) {
159             mX = x;
160             mY = y;
161             MetricsLogger.visible(getContext(), MetricsProto.MetricsEvent.QS_EDIT);
162             isShown = true;
163             mOpening = true;
164             setTileSpecs();
165             setVisibility(View.VISIBLE);
166             mClipper.animateCircularClip(x, y, true, mExpandAnimationListener);
167             queryTiles();
168             mNotifQsContainer.setCustomizerAnimating(true);
169             mNotifQsContainer.setCustomizerShowing(true);
170             Dependency.get(KeyguardMonitor.class).addCallback(mKeyguardCallback);
171             updateNavColors();
172         }
173     }
174 
175 
showImmediately()176     public void showImmediately() {
177         if (!isShown) {
178             setVisibility(VISIBLE);
179             mClipper.showBackground();
180             isShown = true;
181             setTileSpecs();
182             setCustomizing(true);
183             queryTiles();
184             mNotifQsContainer.setCustomizerAnimating(false);
185             mNotifQsContainer.setCustomizerShowing(true);
186             Dependency.get(KeyguardMonitor.class).addCallback(mKeyguardCallback);
187             updateNavColors();
188         }
189     }
190 
queryTiles()191     private void queryTiles() {
192         mTileQueryHelper.queryTiles(mHost);
193     }
194 
hide(int x, int y)195     public void hide(int x, int y) {
196         if (isShown) {
197             MetricsLogger.hidden(getContext(), MetricsProto.MetricsEvent.QS_EDIT);
198             isShown = false;
199             mToolbar.dismissPopupMenus();
200             setCustomizing(false);
201             save();
202             mClipper.animateCircularClip(mX, mY, false, mCollapseAnimationListener);
203             mNotifQsContainer.setCustomizerAnimating(true);
204             mNotifQsContainer.setCustomizerShowing(false);
205             Dependency.get(KeyguardMonitor.class).removeCallback(mKeyguardCallback);
206             updateNavColors();
207         }
208     }
209 
isShown()210     public boolean isShown() {
211         return isShown;
212     }
213 
setCustomizing(boolean customizing)214     private void setCustomizing(boolean customizing) {
215         mCustomizing = customizing;
216         mQs.notifyCustomizeChanged();
217     }
218 
isCustomizing()219     public boolean isCustomizing() {
220         return mCustomizing || mOpening;
221     }
222 
223     @Override
onMenuItemClick(MenuItem item)224     public boolean onMenuItemClick(MenuItem item) {
225         switch (item.getItemId()) {
226             case MENU_RESET:
227                 MetricsLogger.action(getContext(), MetricsProto.MetricsEvent.ACTION_QS_EDIT_RESET);
228                 reset();
229                 break;
230         }
231         return false;
232     }
233 
reset()234     private void reset() {
235         ArrayList<String> tiles = new ArrayList<>();
236         String defTiles = mContext.getString(R.string.quick_settings_tiles_default);
237         for (String tile : defTiles.split(",")) {
238             tiles.add(tile);
239         }
240         mTileAdapter.resetTileSpecs(mHost, tiles);
241     }
242 
setTileSpecs()243     private void setTileSpecs() {
244         List<String> specs = new ArrayList<>();
245         for (QSTile tile : mHost.getTiles()) {
246             specs.add(tile.getTileSpec());
247         }
248         mTileAdapter.setTileSpecs(specs);
249         mRecyclerView.setAdapter(mTileAdapter);
250     }
251 
save()252     private void save() {
253         if (mTileQueryHelper.isFinished()) {
254             mTileAdapter.saveSpecs(mHost);
255         }
256     }
257 
258 
saveInstanceState(Bundle outState)259     public void saveInstanceState(Bundle outState) {
260         if (isShown) {
261             Dependency.get(KeyguardMonitor.class).removeCallback(mKeyguardCallback);
262         }
263         outState.putBoolean(EXTRA_QS_CUSTOMIZING, mCustomizing);
264     }
265 
restoreInstanceState(Bundle savedInstanceState)266     public void restoreInstanceState(Bundle savedInstanceState) {
267         boolean customizing = savedInstanceState.getBoolean(EXTRA_QS_CUSTOMIZING);
268         if (customizing) {
269             setVisibility(VISIBLE);
270             addOnLayoutChangeListener(new OnLayoutChangeListener() {
271                 @Override
272                 public void onLayoutChange(View v, int left, int top, int right, int bottom,
273                         int oldLeft,
274                         int oldTop, int oldRight, int oldBottom) {
275                     removeOnLayoutChangeListener(this);
276                     showImmediately();
277                 }
278             });
279         }
280     }
281 
setEditLocation(int x, int y)282     public void setEditLocation(int x, int y) {
283         mX = x;
284         mY = y;
285     }
286 
287     private final Callback mKeyguardCallback = () -> {
288         if (!isAttachedToWindow()) return;
289         if (Dependency.get(KeyguardMonitor.class).isShowing() && !mOpening) {
290             hide(0, 0);
291         }
292     };
293 
294     private final AnimatorListener mExpandAnimationListener = new AnimatorListenerAdapter() {
295         @Override
296         public void onAnimationEnd(Animator animation) {
297             if (isShown) {
298                 setCustomizing(true);
299             }
300             mOpening = false;
301             mNotifQsContainer.setCustomizerAnimating(false);
302         }
303 
304         @Override
305         public void onAnimationCancel(Animator animation) {
306             mOpening = false;
307             mNotifQsContainer.setCustomizerAnimating(false);
308         }
309     };
310 
311     private final AnimatorListener mCollapseAnimationListener = new AnimatorListenerAdapter() {
312         @Override
313         public void onAnimationEnd(Animator animation) {
314             if (!isShown) {
315                 setVisibility(View.GONE);
316             }
317             mNotifQsContainer.setCustomizerAnimating(false);
318             mRecyclerView.setAdapter(mTileAdapter);
319         }
320 
321         @Override
322         public void onAnimationCancel(Animator animation) {
323             if (!isShown) {
324                 setVisibility(View.GONE);
325             }
326             mNotifQsContainer.setCustomizerAnimating(false);
327         }
328     };
329 }
330