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.Handler;
24 import android.os.Looper;
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.systemui.Dependency;
42 import com.android.systemui.R;
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.NotificationsQuickSettingsContainer;
48 import com.android.systemui.statusbar.policy.KeyguardMonitor;
49 import com.android.systemui.statusbar.policy.KeyguardMonitor.Callback;
50 
51 import java.util.ArrayList;
52 import java.util.List;
53 
54 /**
55  * Allows full-screen customization of QS, through show() and hide().
56  *
57  * This adds itself to the status bar window, so it can appear on top of quick settings and
58  * *someday* do fancy animations to get into/out of it.
59  */
60 public class QSCustomizer extends LinearLayout implements OnMenuItemClickListener {
61 
62     private static final int MENU_RESET = Menu.FIRST;
63 
64     private final QSDetailClipper mClipper;
65 
66     private boolean isShown;
67     private QSTileHost mHost;
68     private RecyclerView mRecyclerView;
69     private TileAdapter mTileAdapter;
70     private Toolbar mToolbar;
71     private boolean mCustomizing;
72     private NotificationsQuickSettingsContainer mNotifQsContainer;
73     private QS mQs;
74     private boolean mFinishedFetchingTiles = false;
75     private int mX;
76     private int mY;
77     private boolean mOpening;
78 
QSCustomizer(Context context, AttributeSet attrs)79     public QSCustomizer(Context context, AttributeSet attrs) {
80         super(new ContextThemeWrapper(context, R.style.edit_theme), attrs);
81         mClipper = new QSDetailClipper(this);
82 
83         LayoutInflater.from(getContext()).inflate(R.layout.qs_customize_panel_content, this);
84 
85         mToolbar = findViewById(com.android.internal.R.id.action_bar);
86         TypedValue value = new TypedValue();
87         mContext.getTheme().resolveAttribute(android.R.attr.homeAsUpIndicator, value, true);
88         mToolbar.setNavigationIcon(
89                 getResources().getDrawable(value.resourceId, mContext.getTheme()));
90         mToolbar.setNavigationOnClickListener(new OnClickListener() {
91             @Override
92             public void onClick(View v) {
93                 hide((int) v.getX() + v.getWidth() / 2, (int) v.getY() + v.getHeight() / 2);
94             }
95         });
96         mToolbar.setOnMenuItemClickListener(this);
97         mToolbar.getMenu().add(Menu.NONE, MENU_RESET, 0,
98                 mContext.getString(com.android.internal.R.string.reset));
99         mToolbar.setTitle(R.string.qs_edit);
100 
101         mRecyclerView = findViewById(android.R.id.list);
102         mTileAdapter = new TileAdapter(getContext());
103         mRecyclerView.setAdapter(mTileAdapter);
104         mTileAdapter.getItemTouchHelper().attachToRecyclerView(mRecyclerView);
105         GridLayoutManager layout = new GridLayoutManager(getContext(), 3);
106         layout.setSpanSizeLookup(mTileAdapter.getSizeLookup());
107         mRecyclerView.setLayoutManager(layout);
108         mRecyclerView.addItemDecoration(mTileAdapter.getItemDecoration());
109         DefaultItemAnimator animator = new DefaultItemAnimator();
110         animator.setMoveDuration(TileAdapter.MOVE_DURATION);
111         mRecyclerView.setItemAnimator(animator);
112     }
113 
114     @Override
onConfigurationChanged(Configuration newConfig)115     protected void onConfigurationChanged(Configuration newConfig) {
116         super.onConfigurationChanged(newConfig);
117         View navBackdrop = findViewById(R.id.nav_bar_background);
118         if (navBackdrop != null) {
119             boolean shouldShow = newConfig.smallestScreenWidthDp >= 600
120                     || newConfig.orientation != Configuration.ORIENTATION_LANDSCAPE;
121             navBackdrop.setVisibility(shouldShow ? View.VISIBLE : View.GONE);
122         }
123     }
124 
setHost(QSTileHost host)125     public void setHost(QSTileHost host) {
126         mHost = host;
127         mTileAdapter.setHost(host);
128     }
129 
setContainer(NotificationsQuickSettingsContainer notificationsQsContainer)130     public void setContainer(NotificationsQuickSettingsContainer notificationsQsContainer) {
131         mNotifQsContainer = notificationsQsContainer;
132     }
133 
setQs(QS qs)134     public void setQs(QS qs) {
135         mQs = qs;
136     }
137 
show(int x, int y)138     public void show(int x, int y) {
139         if (!isShown) {
140             mX = x;
141             mY = y;
142             MetricsLogger.visible(getContext(), MetricsProto.MetricsEvent.QS_EDIT);
143             isShown = true;
144             mOpening = true;
145             setTileSpecs();
146             setVisibility(View.VISIBLE);
147             mClipper.animateCircularClip(x, y, true, mExpandAnimationListener);
148             queryTiles();
149             mNotifQsContainer.setCustomizerAnimating(true);
150             mNotifQsContainer.setCustomizerShowing(true);
151             announceForAccessibility(mContext.getString(
152                     R.string.accessibility_desc_quick_settings_edit));
153             Dependency.get(KeyguardMonitor.class).addCallback(mKeyguardCallback);
154         }
155     }
156 
queryTiles()157     private void queryTiles() {
158         mFinishedFetchingTiles = false;
159         Runnable tileQueryFetchCompletion = () -> {
160             Handler mainHandler = new Handler(Looper.getMainLooper());
161             mainHandler.post(() -> mFinishedFetchingTiles = true);
162         };
163         new TileQueryHelper(mContext, mHost, mTileAdapter, tileQueryFetchCompletion);
164     }
165 
hide(int x, int y)166     public void hide(int x, int y) {
167         if (isShown) {
168             MetricsLogger.hidden(getContext(), MetricsProto.MetricsEvent.QS_EDIT);
169             isShown = false;
170             mToolbar.dismissPopupMenus();
171             setCustomizing(false);
172             save();
173             mClipper.animateCircularClip(mX, mY, false, mCollapseAnimationListener);
174             mNotifQsContainer.setCustomizerAnimating(true);
175             mNotifQsContainer.setCustomizerShowing(false);
176             announceForAccessibility(mContext.getString(
177                     R.string.accessibility_desc_quick_settings));
178             Dependency.get(KeyguardMonitor.class).removeCallback(mKeyguardCallback);
179         }
180     }
181 
isShown()182     public boolean isShown() {
183         return isShown;
184     }
185 
setCustomizing(boolean customizing)186     private void setCustomizing(boolean customizing) {
187         mCustomizing = customizing;
188         mQs.notifyCustomizeChanged();
189     }
190 
isCustomizing()191     public boolean isCustomizing() {
192         return mCustomizing;
193     }
194 
195     @Override
onMenuItemClick(MenuItem item)196     public boolean onMenuItemClick(MenuItem item) {
197         switch (item.getItemId()) {
198             case MENU_RESET:
199                 MetricsLogger.action(getContext(), MetricsProto.MetricsEvent.ACTION_QS_EDIT_RESET);
200                 reset();
201                 break;
202         }
203         return false;
204     }
205 
reset()206     private void reset() {
207         ArrayList<String> tiles = new ArrayList<>();
208         String defTiles = mContext.getString(R.string.quick_settings_tiles_default);
209         for (String tile : defTiles.split(",")) {
210             tiles.add(tile);
211         }
212         mTileAdapter.resetTileSpecs(mHost, tiles);
213     }
214 
setTileSpecs()215     private void setTileSpecs() {
216         List<String> specs = new ArrayList<>();
217         for (QSTile tile : mHost.getTiles()) {
218             specs.add(tile.getTileSpec());
219         }
220         mTileAdapter.setTileSpecs(specs);
221         mRecyclerView.setAdapter(mTileAdapter);
222     }
223 
save()224     private void save() {
225         if (mFinishedFetchingTiles) {
226             mTileAdapter.saveSpecs(mHost);
227         }
228     }
229 
230     private final Callback mKeyguardCallback = () -> {
231         if (!isAttachedToWindow()) return;
232         if (Dependency.get(KeyguardMonitor.class).isShowing() && !mOpening) {
233             hide(0, 0);
234         }
235     };
236 
237     private final AnimatorListener mExpandAnimationListener = new AnimatorListenerAdapter() {
238         @Override
239         public void onAnimationEnd(Animator animation) {
240             if (isShown) {
241                 setCustomizing(true);
242             }
243             mOpening = false;
244             mNotifQsContainer.setCustomizerAnimating(false);
245         }
246 
247         @Override
248         public void onAnimationCancel(Animator animation) {
249             mOpening = false;
250             mNotifQsContainer.setCustomizerAnimating(false);
251         }
252     };
253 
254     private final AnimatorListener mCollapseAnimationListener = new AnimatorListenerAdapter() {
255         @Override
256         public void onAnimationEnd(Animator animation) {
257             if (!isShown) {
258                 setVisibility(View.GONE);
259             }
260             mNotifQsContainer.setCustomizerAnimating(false);
261             mRecyclerView.setAdapter(mTileAdapter);
262         }
263 
264         @Override
265         public void onAnimationCancel(Animator animation) {
266             if (!isShown) {
267                 setVisibility(View.GONE);
268             }
269             mNotifQsContainer.setCustomizerAnimating(false);
270         }
271     };
272 }
273