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