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