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