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.os.Bundle;
24 import android.util.AttributeSet;
25 import android.util.TypedValue;
26 import android.view.ContextThemeWrapper;
27 import android.view.LayoutInflater;
28 import android.view.Menu;
29 import android.view.MenuItem;
30 import android.view.View;
31 import android.widget.LinearLayout;
32 import android.widget.Toolbar;
33 import android.widget.Toolbar.OnMenuItemClickListener;
34 
35 import androidx.recyclerview.widget.DefaultItemAnimator;
36 import androidx.recyclerview.widget.GridLayoutManager;
37 import androidx.recyclerview.widget.RecyclerView;
38 
39 import com.android.internal.logging.MetricsLogger;
40 import com.android.internal.logging.nano.MetricsProto;
41 import com.android.systemui.R;
42 import com.android.systemui.keyguard.ScreenLifecycle;
43 import com.android.systemui.plugins.qs.QS;
44 import com.android.systemui.plugins.qs.QSTile;
45 import com.android.systemui.qs.QSDetailClipper;
46 import com.android.systemui.qs.QSTileHost;
47 import com.android.systemui.statusbar.phone.LightBarController;
48 import com.android.systemui.statusbar.phone.NotificationsQuickSettingsContainer;
49 import com.android.systemui.statusbar.policy.KeyguardMonitor;
50 import com.android.systemui.statusbar.policy.KeyguardMonitor.Callback;
51 
52 import java.util.ArrayList;
53 import java.util.List;
54 
55 import javax.inject.Inject;
56 
57 /**
58  * Allows full-screen customization of QS, through show() and hide().
59  *
60  * This adds itself to the status bar window, so it can appear on top of quick settings and
61  * *someday* do fancy animations to get into/out of it.
62  */
63 public class QSCustomizer extends LinearLayout implements OnMenuItemClickListener {
64 
65     private static final int MENU_RESET = Menu.FIRST;
66     private static final String EXTRA_QS_CUSTOMIZING = "qs_customizing";
67     private static final String TAG = "QSCustomizer";
68 
69     private final QSDetailClipper mClipper;
70     private final LightBarController mLightBarController;
71     private KeyguardMonitor mKeyguardMonitor;
72     private final ScreenLifecycle mScreenLifecycle;
73     private final TileQueryHelper mTileQueryHelper;
74     private final View mTransparentView;
75 
76     private boolean isShown;
77     private QSTileHost mHost;
78     private RecyclerView mRecyclerView;
79     private TileAdapter mTileAdapter;
80     private Toolbar mToolbar;
81     private boolean mCustomizing;
82     private NotificationsQuickSettingsContainer mNotifQsContainer;
83     private QS mQs;
84     private int mX;
85     private int mY;
86     private boolean mOpening;
87     private boolean mIsShowingNavBackdrop;
88 
89     @Inject
QSCustomizer(Context context, AttributeSet attrs, LightBarController lightBarController, KeyguardMonitor keyguardMonitor, ScreenLifecycle screenLifecycle)90     public QSCustomizer(Context context, AttributeSet attrs,
91             LightBarController lightBarController,
92             KeyguardMonitor keyguardMonitor,
93             ScreenLifecycle screenLifecycle) {
94         super(new ContextThemeWrapper(context, R.style.edit_theme), attrs);
95 
96         LayoutInflater.from(getContext()).inflate(R.layout.qs_customize_panel_content, this);
97         mClipper = new QSDetailClipper(findViewById(R.id.customize_container));
98         mToolbar = findViewById(com.android.internal.R.id.action_bar);
99         TypedValue value = new TypedValue();
100         mContext.getTheme().resolveAttribute(android.R.attr.homeAsUpIndicator, value, true);
101         mToolbar.setNavigationIcon(
102                 getResources().getDrawable(value.resourceId, mContext.getTheme()));
103         mToolbar.setNavigationOnClickListener(new OnClickListener() {
104             @Override
105             public void onClick(View v) {
106                 hide();
107             }
108         });
109         mToolbar.setOnMenuItemClickListener(this);
110         mToolbar.getMenu().add(Menu.NONE, MENU_RESET, 0,
111                 mContext.getString(com.android.internal.R.string.reset));
112         mToolbar.setTitle(R.string.qs_edit);
113         mRecyclerView = findViewById(android.R.id.list);
114         mTransparentView = findViewById(R.id.customizer_transparent_view);
115         mTileAdapter = new TileAdapter(getContext());
116         mTileQueryHelper = new TileQueryHelper(context, mTileAdapter);
117         mRecyclerView.setAdapter(mTileAdapter);
118         mTileAdapter.getItemTouchHelper().attachToRecyclerView(mRecyclerView);
119         GridLayoutManager layout = new GridLayoutManager(getContext(), 3);
120         layout.setSpanSizeLookup(mTileAdapter.getSizeLookup());
121         mRecyclerView.setLayoutManager(layout);
122         mRecyclerView.addItemDecoration(mTileAdapter.getItemDecoration());
123         DefaultItemAnimator animator = new DefaultItemAnimator();
124         animator.setMoveDuration(TileAdapter.MOVE_DURATION);
125         mRecyclerView.setItemAnimator(animator);
126         mLightBarController = lightBarController;
127         mKeyguardMonitor = keyguardMonitor;
128         mScreenLifecycle = screenLifecycle;
129         updateNavBackDrop(getResources().getConfiguration());
130     }
131 
132     @Override
onConfigurationChanged(Configuration newConfig)133     protected void onConfigurationChanged(Configuration newConfig) {
134         super.onConfigurationChanged(newConfig);
135         updateNavBackDrop(newConfig);
136         updateResources();
137     }
138 
updateResources()139     private void updateResources() {
140         LayoutParams lp = (LayoutParams) mTransparentView.getLayoutParams();
141         lp.height = mContext.getResources().getDimensionPixelSize(
142                 com.android.internal.R.dimen.quick_qs_offset_height);
143         mTransparentView.setLayoutParams(lp);
144     }
145 
updateNavBackDrop(Configuration newConfig)146     private void updateNavBackDrop(Configuration newConfig) {
147         View navBackdrop = findViewById(R.id.nav_bar_background);
148         mIsShowingNavBackdrop = newConfig.smallestScreenWidthDp >= 600
149                 || newConfig.orientation != Configuration.ORIENTATION_LANDSCAPE;
150         if (navBackdrop != null) {
151             navBackdrop.setVisibility(mIsShowingNavBackdrop ? View.VISIBLE : View.GONE);
152         }
153         updateNavColors();
154     }
155 
updateNavColors()156     private void updateNavColors() {
157         mLightBarController.setQsCustomizing(mIsShowingNavBackdrop && isShown);
158     }
159 
setHost(QSTileHost host)160     public void setHost(QSTileHost host) {
161         mHost = host;
162         mTileAdapter.setHost(host);
163     }
164 
setContainer(NotificationsQuickSettingsContainer notificationsQsContainer)165     public void setContainer(NotificationsQuickSettingsContainer notificationsQsContainer) {
166         mNotifQsContainer = notificationsQsContainer;
167     }
168 
setQs(QS qs)169     public void setQs(QS qs) {
170         mQs = qs;
171     }
172 
173     /** Animate and show QSCustomizer panel.
174      * @param x,y Location on screen of {@code edit} button to determine center of animation.
175      */
show(int x, int y)176     public void show(int x, int y) {
177         if (!isShown) {
178             int containerLocation[] = findViewById(R.id.customize_container).getLocationOnScreen();
179             mX = x - containerLocation[0];
180             mY = y - containerLocation[1];
181             MetricsLogger.visible(getContext(), MetricsProto.MetricsEvent.QS_EDIT);
182             isShown = true;
183             mOpening = true;
184             setTileSpecs();
185             setVisibility(View.VISIBLE);
186             mClipper.animateCircularClip(mX, mY, true, mExpandAnimationListener);
187             queryTiles();
188             mNotifQsContainer.setCustomizerAnimating(true);
189             mNotifQsContainer.setCustomizerShowing(true);
190             mKeyguardMonitor.addCallback(mKeyguardCallback);
191             updateNavColors();
192         }
193     }
194 
195 
showImmediately()196     public void showImmediately() {
197         if (!isShown) {
198             setVisibility(VISIBLE);
199             mClipper.showBackground();
200             isShown = true;
201             setTileSpecs();
202             setCustomizing(true);
203             queryTiles();
204             mNotifQsContainer.setCustomizerAnimating(false);
205             mNotifQsContainer.setCustomizerShowing(true);
206             mKeyguardMonitor.addCallback(mKeyguardCallback);
207             updateNavColors();
208         }
209     }
210 
queryTiles()211     private void queryTiles() {
212         mTileQueryHelper.queryTiles(mHost);
213     }
214 
hide()215     public void hide() {
216         final boolean animate = mScreenLifecycle.getScreenState() != ScreenLifecycle.SCREEN_OFF;
217         if (isShown) {
218             MetricsLogger.hidden(getContext(), MetricsProto.MetricsEvent.QS_EDIT);
219             isShown = false;
220             mToolbar.dismissPopupMenus();
221             setCustomizing(false);
222             save();
223             if (animate) {
224                 mClipper.animateCircularClip(mX, mY, false, mCollapseAnimationListener);
225             } else {
226                 setVisibility(View.GONE);
227             }
228             mNotifQsContainer.setCustomizerAnimating(animate);
229             mNotifQsContainer.setCustomizerShowing(false);
230             mKeyguardMonitor.removeCallback(mKeyguardCallback);
231             updateNavColors();
232         }
233     }
234 
isShown()235     public boolean isShown() {
236         return isShown;
237     }
238 
setCustomizing(boolean customizing)239     private void setCustomizing(boolean customizing) {
240         mCustomizing = customizing;
241         mQs.notifyCustomizeChanged();
242     }
243 
isCustomizing()244     public boolean isCustomizing() {
245         return mCustomizing || mOpening;
246     }
247 
248     @Override
onMenuItemClick(MenuItem item)249     public boolean onMenuItemClick(MenuItem item) {
250         switch (item.getItemId()) {
251             case MENU_RESET:
252                 MetricsLogger.action(getContext(), MetricsProto.MetricsEvent.ACTION_QS_EDIT_RESET);
253                 reset();
254                 break;
255         }
256         return false;
257     }
258 
reset()259     private void reset() {
260         ArrayList<String> tiles = new ArrayList<>();
261         String defTiles = mContext.getString(R.string.quick_settings_tiles_default);
262         for (String tile : defTiles.split(",")) {
263             tiles.add(tile);
264         }
265         mTileAdapter.resetTileSpecs(mHost, tiles);
266     }
267 
setTileSpecs()268     private void setTileSpecs() {
269         List<String> specs = new ArrayList<>();
270         for (QSTile tile : mHost.getTiles()) {
271             specs.add(tile.getTileSpec());
272         }
273         mTileAdapter.setTileSpecs(specs);
274         mRecyclerView.setAdapter(mTileAdapter);
275     }
276 
save()277     private void save() {
278         if (mTileQueryHelper.isFinished()) {
279             mTileAdapter.saveSpecs(mHost);
280         }
281     }
282 
283 
saveInstanceState(Bundle outState)284     public void saveInstanceState(Bundle outState) {
285         if (isShown) {
286             mKeyguardMonitor.removeCallback(mKeyguardCallback);
287         }
288         outState.putBoolean(EXTRA_QS_CUSTOMIZING, mCustomizing);
289     }
290 
restoreInstanceState(Bundle savedInstanceState)291     public void restoreInstanceState(Bundle savedInstanceState) {
292         boolean customizing = savedInstanceState.getBoolean(EXTRA_QS_CUSTOMIZING);
293         if (customizing) {
294             setVisibility(VISIBLE);
295             addOnLayoutChangeListener(new OnLayoutChangeListener() {
296                 @Override
297                 public void onLayoutChange(View v, int left, int top, int right, int bottom,
298                         int oldLeft,
299                         int oldTop, int oldRight, int oldBottom) {
300                     removeOnLayoutChangeListener(this);
301                     showImmediately();
302                 }
303             });
304         }
305     }
306     /** @param x,y Location on screen of animation center.
307      */
setEditLocation(int x, int y)308     public void setEditLocation(int x, int y) {
309         int containerLocation[] = findViewById(R.id.customize_container).getLocationOnScreen();
310         mX = x - containerLocation[0];
311         mY = y - containerLocation[1];
312     }
313 
314     private final Callback mKeyguardCallback = new Callback() {
315         @Override
316         public void onKeyguardShowingChanged() {
317             if (!isAttachedToWindow()) return;
318             if (mKeyguardMonitor.isShowing() && !mOpening) {
319                 hide();
320             }
321         }
322     };
323 
324     private final AnimatorListener mExpandAnimationListener = new AnimatorListenerAdapter() {
325         @Override
326         public void onAnimationEnd(Animator animation) {
327             if (isShown) {
328                 setCustomizing(true);
329             }
330             mOpening = false;
331             mNotifQsContainer.setCustomizerAnimating(false);
332         }
333 
334         @Override
335         public void onAnimationCancel(Animator animation) {
336             mOpening = false;
337             mNotifQsContainer.setCustomizerAnimating(false);
338         }
339     };
340 
341     private final AnimatorListener mCollapseAnimationListener = new AnimatorListenerAdapter() {
342         @Override
343         public void onAnimationEnd(Animator animation) {
344             if (!isShown) {
345                 setVisibility(View.GONE);
346             }
347             mNotifQsContainer.setCustomizerAnimating(false);
348             mRecyclerView.setAdapter(mTileAdapter);
349         }
350 
351         @Override
352         public void onAnimationCancel(Animator animation) {
353             if (!isShown) {
354                 setVisibility(View.GONE);
355             }
356             mNotifQsContainer.setCustomizerAnimating(false);
357         }
358     };
359 }
360