1 /*
2  * Copyright (C) 2020 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 
17 package com.android.systemui.qs.customize;
18 
19 import static com.android.systemui.qs.customize.QSCustomizer.EXTRA_QS_CUSTOMIZING;
20 import static com.android.systemui.qs.customize.QSCustomizer.MENU_RESET;
21 
22 import android.content.res.Configuration;
23 import android.graphics.Rect;
24 import android.os.Bundle;
25 import android.view.MenuItem;
26 import android.view.View;
27 import android.widget.TextView;
28 import android.widget.Toolbar;
29 import android.widget.Toolbar.OnMenuItemClickListener;
30 
31 import androidx.annotation.Nullable;
32 import androidx.core.view.accessibility.AccessibilityNodeInfoCompat;
33 import androidx.recyclerview.widget.GridLayoutManager;
34 import androidx.recyclerview.widget.RecyclerView;
35 
36 import com.android.internal.logging.UiEventLogger;
37 import com.android.systemui.keyguard.ScreenLifecycle;
38 import com.android.systemui.plugins.qs.QS;
39 import com.android.systemui.plugins.qs.QSContainerController;
40 import com.android.systemui.plugins.qs.QSTile;
41 import com.android.systemui.qs.QSEditEvent;
42 import com.android.systemui.qs.QSHost;
43 import com.android.systemui.qs.dagger.QSScope;
44 import com.android.systemui.res.R;
45 import com.android.systemui.scene.shared.flag.SceneContainerFlag;
46 import com.android.systemui.statusbar.phone.LightBarController;
47 import com.android.systemui.statusbar.policy.ConfigurationController;
48 import com.android.systemui.statusbar.policy.ConfigurationController.ConfigurationListener;
49 import com.android.systemui.statusbar.policy.KeyguardStateController;
50 import com.android.systemui.util.ViewController;
51 
52 import java.util.ArrayList;
53 import java.util.List;
54 
55 import javax.inject.Inject;
56 
57 /** {@link ViewController} for {@link QSCustomizer}. */
58 @QSScope
59 public class QSCustomizerController extends ViewController<QSCustomizer> {
60     private final TileQueryHelper mTileQueryHelper;
61     private final QSHost mQsHost;
62     private final TileAdapter mTileAdapter;
63     private final ScreenLifecycle mScreenLifecycle;
64     private final KeyguardStateController mKeyguardStateController;
65     private final LightBarController mLightBarController;
66     private final ConfigurationController mConfigurationController;
67     private final UiEventLogger mUiEventLogger;
68     private final Toolbar mToolbar;
69 
70     private final OnMenuItemClickListener mOnMenuItemClickListener = new OnMenuItemClickListener() {
71         @Override
72         public boolean onMenuItemClick(MenuItem item) {
73             if (item.getItemId() == MENU_RESET) {
74                 mUiEventLogger.log(QSEditEvent.QS_EDIT_RESET);
75                 reset();
76             }
77             return false;
78         }
79     };
80 
81     private final KeyguardStateController.Callback mKeyguardCallback =
82             new KeyguardStateController.Callback() {
83         @Override
84         public void onKeyguardShowingChanged() {
85             if (!mView.isAttachedToWindow()) return;
86             if (mKeyguardStateController.isShowing() && !mView.isOpening()) {
87                 hide();
88             }
89         }
90     };
91 
92     private final ConfigurationListener mConfigurationListener = new ConfigurationListener() {
93         @Override
94         public void onConfigChanged(Configuration newConfig) {
95             mView.updateNavBackDrop(newConfig, mLightBarController);
96             mView.updateResources();
97             if (mTileAdapter.updateNumColumns()) {
98                 RecyclerView.LayoutManager lm = mView.getRecyclerView().getLayoutManager();
99                 if (lm instanceof GridLayoutManager) {
100                     ((GridLayoutManager) lm).setSpanCount(mTileAdapter.getNumColumns());
101                 }
102             }
103         }
104     };
105 
106     @Inject
QSCustomizerController(QSCustomizer view, TileQueryHelper tileQueryHelper, QSHost qsHost, TileAdapter tileAdapter, ScreenLifecycle screenLifecycle, KeyguardStateController keyguardStateController, LightBarController lightBarController, ConfigurationController configurationController, UiEventLogger uiEventLogger)107     protected QSCustomizerController(QSCustomizer view, TileQueryHelper tileQueryHelper,
108             QSHost qsHost, TileAdapter tileAdapter, ScreenLifecycle screenLifecycle,
109             KeyguardStateController keyguardStateController, LightBarController lightBarController,
110             ConfigurationController configurationController, UiEventLogger uiEventLogger) {
111         super(view);
112         mTileQueryHelper = tileQueryHelper;
113         mQsHost = qsHost;
114         mTileAdapter = tileAdapter;
115         mScreenLifecycle = screenLifecycle;
116         mKeyguardStateController = keyguardStateController;
117         mLightBarController = lightBarController;
118         mConfigurationController = configurationController;
119         mUiEventLogger = uiEventLogger;
120         view.setSceneContainerEnabled(SceneContainerFlag.isEnabled());
121 
122         mToolbar = mView.findViewById(com.android.internal.R.id.action_bar);
123     }
124 
applyBottomNavBarSizeToRecyclerViewPadding(int padding)125     public void applyBottomNavBarSizeToRecyclerViewPadding(int padding) {
126         mView.applyBottomNavBarToPadding(padding);
127     }
128 
129     @Override
onViewAttached()130     protected void onViewAttached() {
131         mView.updateNavBackDrop(getResources().getConfiguration(), mLightBarController);
132 
133         mConfigurationController.addCallback(mConfigurationListener);
134 
135         mTileQueryHelper.setListener(mTileAdapter);
136         int halfMargin =
137                 getResources().getDimensionPixelSize(R.dimen.qs_tile_margin_horizontal) / 2;
138         mTileAdapter.changeHalfMargin(halfMargin);
139 
140         RecyclerView recyclerView = mView.getRecyclerView();
141         recyclerView.setAdapter(mTileAdapter);
142         mTileAdapter.getItemTouchHelper().attachToRecyclerView(recyclerView);
143         GridLayoutManager layout =
144                 new GridLayoutManager(getContext(), mTileAdapter.getNumColumns()) {
145             @Override
146             public void onInitializeAccessibilityNodeInfoForItem(RecyclerView.Recycler recycler,
147                     RecyclerView.State state, View host, AccessibilityNodeInfoCompat info) {
148                 // Do not read row and column every time it changes.
149             }
150 
151             public void calculateItemDecorationsForChild(View child, Rect outRect) {
152                 // There's only a single item decoration that cares about the itemOffsets, so
153                 // we just call it manually so they are never cached. This way, it's updated as the
154                 // tiles are moved around.
155                 // It only sets the left and right margin and only cares about tiles (not TextView).
156                 if (!(child instanceof TextView)) {
157                     outRect.setEmpty();
158                     mTileAdapter.getMarginItemDecoration().getItemOffsets(outRect, child,
159                             recyclerView, new RecyclerView.State());
160                     ((LayoutParams) child.getLayoutParams()).leftMargin = outRect.left;
161                     ((LayoutParams) child.getLayoutParams()).rightMargin = outRect.right;
162                 }
163             }
164         };
165         layout.setSpanSizeLookup(mTileAdapter.getSizeLookup());
166         recyclerView.setLayoutManager(layout);
167         recyclerView.addItemDecoration(mTileAdapter.getItemDecoration());
168         recyclerView.addItemDecoration(mTileAdapter.getMarginItemDecoration());
169 
170         mToolbar.setOnMenuItemClickListener(mOnMenuItemClickListener);
171         mToolbar.setNavigationOnClickListener(v -> hide());
172     }
173 
174     @Override
onViewDetached()175     protected void onViewDetached() {
176         mTileQueryHelper.setListener(null);
177         mToolbar.setOnMenuItemClickListener(null);
178         mConfigurationController.removeCallback(mConfigurationListener);
179     }
180 
181 
reset()182     private void reset() {
183         mTileAdapter.resetTileSpecs(QSHost.getDefaultSpecs(getContext().getResources()));
184     }
185 
isCustomizing()186     public boolean isCustomizing() {
187         return mView.isCustomizing();
188     }
189 
190     /** */
show(int x, int y, boolean immediate)191     public void show(int x, int y, boolean immediate) {
192         if (!mView.isShown()) {
193             setTileSpecs();
194             if (immediate) {
195                 mView.showImmediately();
196             } else {
197                 mView.show(x, y, mTileAdapter);
198                 mUiEventLogger.log(QSEditEvent.QS_EDIT_OPEN);
199             }
200             mTileQueryHelper.queryTiles(mQsHost);
201             mKeyguardStateController.addCallback(mKeyguardCallback);
202             mView.updateNavColors(mLightBarController);
203         }
204     }
205 
206     /** */
setQs(@ullable QS qsFragment)207     public void setQs(@Nullable QS qsFragment) {
208         mView.setQs(qsFragment);
209     }
210 
211     /** */
restoreInstanceState(Bundle savedInstanceState)212     public void restoreInstanceState(Bundle savedInstanceState) {
213         boolean customizing = savedInstanceState.getBoolean(EXTRA_QS_CUSTOMIZING);
214         if (customizing) {
215             mView.setVisibility(View.VISIBLE);
216             mView.addOnLayoutChangeListener(new View.OnLayoutChangeListener() {
217                 @Override
218                 public void onLayoutChange(View v, int left, int top, int right, int bottom,
219                         int oldLeft,
220                         int oldTop, int oldRight, int oldBottom) {
221                     mView.removeOnLayoutChangeListener(this);
222                     show(0, 0, true);
223                 }
224             });
225         }
226     }
227 
228     /** */
saveInstanceState(Bundle outState)229     public void saveInstanceState(Bundle outState) {
230         if (mView.isShown()) {
231             mKeyguardStateController.removeCallback(mKeyguardCallback);
232         }
233         outState.putBoolean(EXTRA_QS_CUSTOMIZING, mView.isCustomizing());
234     }
235 
236     /** */
setEditLocation(int x, int y)237     public void setEditLocation(int x, int y) {
238         mView.setEditLocation(x, y);
239     }
240 
241     /** */
setContainerController(QSContainerController controller)242     public void setContainerController(QSContainerController controller) {
243         mView.setContainerController(controller);
244     }
245 
isShown()246     public boolean isShown() {
247         return mView.isShown();
248     }
249 
250     /** Hice the customizer. */
hide()251     public void hide() {
252         hide(true);
253     }
254 
hide(boolean animated)255     public void hide(boolean animated) {
256         final boolean animate = animated
257                 && mScreenLifecycle.getScreenState() != ScreenLifecycle.SCREEN_OFF;
258         if (mView.isShown()) {
259             mUiEventLogger.log(QSEditEvent.QS_EDIT_CLOSED);
260             mToolbar.dismissPopupMenus();
261             mView.setCustomizing(false);
262             save();
263             mView.hide(animate);
264             mView.updateNavColors(mLightBarController);
265             mKeyguardStateController.removeCallback(mKeyguardCallback);
266         }
267     }
268 
save()269     private void save() {
270         if (mTileQueryHelper.isFinished()) {
271             mTileAdapter.saveSpecs(mQsHost);
272         }
273     }
274 
setTileSpecs()275     private void setTileSpecs() {
276         List<String> specs = new ArrayList<>();
277         for (QSTile tile : mQsHost.getTiles()) {
278             specs.add(tile.getTileSpec());
279         }
280         mTileAdapter.setTileSpecs(specs);
281     }
282 }
283