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