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