1 /* 2 * Copyright (C) 2016 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file 5 * except in compliance with the License. You may obtain a copy of the License at 6 * 7 * http://www.apache.org/licenses/LICENSE-2.0 8 * 9 * Unless required by applicable law or agreed to in writing, software distributed under the 10 * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 11 * KIND, either express or implied. See the License for the specific language governing 12 * permissions and limitations under the License. 13 */ 14 15 package com.android.systemui.qs; 16 17 import static com.android.internal.logging.nano.MetricsProto.MetricsEvent.ACTION_QS_MORE_SETTINGS; 18 19 import android.animation.Animator; 20 import android.animation.Animator.AnimatorListener; 21 import android.animation.AnimatorListenerAdapter; 22 import android.annotation.Nullable; 23 import android.content.Context; 24 import android.content.Intent; 25 import android.content.res.Configuration; 26 import android.graphics.drawable.Animatable; 27 import android.util.AttributeSet; 28 import android.util.SparseArray; 29 import android.view.View; 30 import android.view.ViewGroup; 31 import android.view.accessibility.AccessibilityEvent; 32 import android.widget.ImageView; 33 import android.widget.LinearLayout; 34 import android.widget.Switch; 35 import android.widget.TextView; 36 37 import com.android.internal.logging.MetricsLogger; 38 import com.android.systemui.Dependency; 39 import com.android.systemui.FontSizeUtils; 40 import com.android.systemui.R; 41 import com.android.systemui.SysUiServiceProvider; 42 import com.android.systemui.plugins.ActivityStarter; 43 import com.android.systemui.plugins.qs.DetailAdapter; 44 import com.android.systemui.statusbar.CommandQueue; 45 46 public class QSDetail extends LinearLayout { 47 48 private static final String TAG = "QSDetail"; 49 private static final long FADE_DURATION = 300; 50 51 private final SparseArray<View> mDetailViews = new SparseArray<>(); 52 53 private ViewGroup mDetailContent; 54 protected TextView mDetailSettingsButton; 55 protected TextView mDetailDoneButton; 56 private QSDetailClipper mClipper; 57 private DetailAdapter mDetailAdapter; 58 private QSPanel mQsPanel; 59 60 protected View mQsDetailHeader; 61 protected TextView mQsDetailHeaderTitle; 62 protected Switch mQsDetailHeaderSwitch; 63 protected ImageView mQsDetailHeaderProgress; 64 65 protected QSTileHost mHost; 66 67 private boolean mScanState; 68 private boolean mClosingDetail; 69 private boolean mFullyExpanded; 70 private QuickStatusBarHeader mHeader; 71 private boolean mTriggeredExpand; 72 private int mOpenX; 73 private int mOpenY; 74 private boolean mAnimatingOpen; 75 private boolean mSwitchState; 76 private View mFooter; 77 QSDetail(Context context, @Nullable AttributeSet attrs)78 public QSDetail(Context context, @Nullable AttributeSet attrs) { 79 super(context, attrs); 80 } 81 82 @Override onConfigurationChanged(Configuration newConfig)83 protected void onConfigurationChanged(Configuration newConfig) { 84 super.onConfigurationChanged(newConfig); 85 FontSizeUtils.updateFontSize(mDetailDoneButton, R.dimen.qs_detail_button_text_size); 86 FontSizeUtils.updateFontSize(mDetailSettingsButton, R.dimen.qs_detail_button_text_size); 87 88 for (int i = 0; i < mDetailViews.size(); i++) { 89 mDetailViews.valueAt(i).dispatchConfigurationChanged(newConfig); 90 } 91 } 92 93 @Override onFinishInflate()94 protected void onFinishInflate() { 95 super.onFinishInflate(); 96 mDetailContent = findViewById(android.R.id.content); 97 mDetailSettingsButton = findViewById(android.R.id.button2); 98 mDetailDoneButton = findViewById(android.R.id.button1); 99 100 mQsDetailHeader = findViewById(R.id.qs_detail_header); 101 mQsDetailHeaderTitle = (TextView) mQsDetailHeader.findViewById(android.R.id.title); 102 mQsDetailHeaderSwitch = (Switch) mQsDetailHeader.findViewById(android.R.id.toggle); 103 mQsDetailHeaderProgress = findViewById(R.id.qs_detail_header_progress); 104 105 updateDetailText(); 106 107 mClipper = new QSDetailClipper(this); 108 109 final OnClickListener doneListener = new OnClickListener() { 110 @Override 111 public void onClick(View v) { 112 announceForAccessibility( 113 mContext.getString(R.string.accessibility_desc_quick_settings)); 114 mQsPanel.closeDetail(); 115 } 116 }; 117 mDetailDoneButton.setOnClickListener(doneListener); 118 } 119 setQsPanel(QSPanel panel, QuickStatusBarHeader header, View footer)120 public void setQsPanel(QSPanel panel, QuickStatusBarHeader header, View footer) { 121 mQsPanel = panel; 122 mHeader = header; 123 mFooter = footer; 124 mHeader.setCallback(mQsPanelCallback); 125 mQsPanel.setCallback(mQsPanelCallback); 126 } 127 setHost(QSTileHost host)128 public void setHost(QSTileHost host) { 129 mHost = host; 130 } isShowingDetail()131 public boolean isShowingDetail() { 132 return mDetailAdapter != null; 133 } 134 setFullyExpanded(boolean fullyExpanded)135 public void setFullyExpanded(boolean fullyExpanded) { 136 mFullyExpanded = fullyExpanded; 137 } 138 setExpanded(boolean qsExpanded)139 public void setExpanded(boolean qsExpanded) { 140 if (!qsExpanded) { 141 mTriggeredExpand = false; 142 } 143 } 144 updateDetailText()145 private void updateDetailText() { 146 mDetailDoneButton.setText(R.string.quick_settings_done); 147 mDetailSettingsButton.setText(R.string.quick_settings_more_settings); 148 } 149 updateResources()150 public void updateResources() { 151 updateDetailText(); 152 } 153 isClosingDetail()154 public boolean isClosingDetail() { 155 return mClosingDetail; 156 } 157 158 public interface Callback { onShowingDetail(DetailAdapter detail, int x, int y)159 void onShowingDetail(DetailAdapter detail, int x, int y); onToggleStateChanged(boolean state)160 void onToggleStateChanged(boolean state); onScanStateChanged(boolean state)161 void onScanStateChanged(boolean state); 162 } 163 handleShowingDetail(final DetailAdapter adapter, int x, int y, boolean toggleQs)164 public void handleShowingDetail(final DetailAdapter adapter, int x, int y, 165 boolean toggleQs) { 166 final boolean showingDetail = adapter != null; 167 setClickable(showingDetail); 168 if (showingDetail) { 169 setupDetailHeader(adapter); 170 if (toggleQs && !mFullyExpanded) { 171 mTriggeredExpand = true; 172 SysUiServiceProvider.getComponent(mContext, CommandQueue.class) 173 .animateExpandSettingsPanel(null); 174 } else { 175 mTriggeredExpand = false; 176 } 177 mOpenX = x; 178 mOpenY = y; 179 } else { 180 // Ensure we collapse into the same point we opened from. 181 x = mOpenX; 182 y = mOpenY; 183 if (toggleQs && mTriggeredExpand) { 184 SysUiServiceProvider.getComponent(mContext, CommandQueue.class) 185 .animateCollapsePanels(); 186 mTriggeredExpand = false; 187 } 188 } 189 190 boolean visibleDiff = (mDetailAdapter != null) != (adapter != null); 191 if (!visibleDiff && mDetailAdapter == adapter) return; // already in right state 192 AnimatorListener listener = null; 193 if (adapter != null) { 194 int viewCacheIndex = adapter.getMetricsCategory(); 195 View detailView = adapter.createDetailView(mContext, mDetailViews.get(viewCacheIndex), 196 mDetailContent); 197 if (detailView == null) throw new IllegalStateException("Must return detail view"); 198 199 setupDetailFooter(adapter); 200 201 mDetailContent.removeAllViews(); 202 mDetailContent.addView(detailView); 203 mDetailViews.put(viewCacheIndex, detailView); 204 Dependency.get(MetricsLogger.class).visible(adapter.getMetricsCategory()); 205 announceForAccessibility(mContext.getString( 206 R.string.accessibility_quick_settings_detail, 207 adapter.getTitle())); 208 mDetailAdapter = adapter; 209 listener = mHideGridContentWhenDone; 210 setVisibility(View.VISIBLE); 211 } else { 212 if (mDetailAdapter != null) { 213 Dependency.get(MetricsLogger.class).hidden(mDetailAdapter.getMetricsCategory()); 214 } 215 mClosingDetail = true; 216 mDetailAdapter = null; 217 listener = mTeardownDetailWhenDone; 218 mHeader.setVisibility(View.VISIBLE); 219 mFooter.setVisibility(View.VISIBLE); 220 mQsPanel.setGridContentVisibility(true); 221 mQsPanelCallback.onScanStateChanged(false); 222 } 223 sendAccessibilityEvent(AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED); 224 225 animateDetailVisibleDiff(x, y, visibleDiff, listener); 226 } 227 animateDetailVisibleDiff(int x, int y, boolean visibleDiff, AnimatorListener listener)228 protected void animateDetailVisibleDiff(int x, int y, boolean visibleDiff, AnimatorListener listener) { 229 if (visibleDiff) { 230 mAnimatingOpen = mDetailAdapter != null; 231 if (mFullyExpanded || mDetailAdapter != null) { 232 setAlpha(1); 233 mClipper.animateCircularClip(x, y, mDetailAdapter != null, listener); 234 } else { 235 animate().alpha(0) 236 .setDuration(FADE_DURATION) 237 .setListener(listener) 238 .start(); 239 } 240 } 241 } 242 setupDetailFooter(DetailAdapter adapter)243 protected void setupDetailFooter(DetailAdapter adapter) { 244 final Intent settingsIntent = adapter.getSettingsIntent(); 245 mDetailSettingsButton.setVisibility(settingsIntent != null ? VISIBLE : GONE); 246 mDetailSettingsButton.setOnClickListener(v -> { 247 Dependency.get(MetricsLogger.class).action(ACTION_QS_MORE_SETTINGS, 248 adapter.getMetricsCategory()); 249 Dependency.get(ActivityStarter.class) 250 .postStartActivityDismissingKeyguard(settingsIntent, 0); 251 }); 252 } 253 setupDetailHeader(final DetailAdapter adapter)254 protected void setupDetailHeader(final DetailAdapter adapter) { 255 mQsDetailHeaderTitle.setText(adapter.getTitle()); 256 final Boolean toggleState = adapter.getToggleState(); 257 if (toggleState == null) { 258 mQsDetailHeaderSwitch.setVisibility(INVISIBLE); 259 mQsDetailHeader.setClickable(false); 260 } else { 261 mQsDetailHeaderSwitch.setVisibility(VISIBLE); 262 handleToggleStateChanged(toggleState, adapter.getToggleEnabled()); 263 mQsDetailHeader.setClickable(true); 264 mQsDetailHeader.setOnClickListener(new OnClickListener() { 265 @Override 266 public void onClick(View v) { 267 boolean checked = !mQsDetailHeaderSwitch.isChecked(); 268 mQsDetailHeaderSwitch.setChecked(checked); 269 adapter.setToggleState(checked); 270 } 271 }); 272 } 273 } 274 handleToggleStateChanged(boolean state, boolean toggleEnabled)275 private void handleToggleStateChanged(boolean state, boolean toggleEnabled) { 276 mSwitchState = state; 277 if (mAnimatingOpen) { 278 return; 279 } 280 mQsDetailHeaderSwitch.setChecked(state); 281 mQsDetailHeader.setEnabled(toggleEnabled); 282 mQsDetailHeaderSwitch.setEnabled(toggleEnabled); 283 } 284 handleScanStateChanged(boolean state)285 private void handleScanStateChanged(boolean state) { 286 if (mScanState == state) return; 287 mScanState = state; 288 final Animatable anim = (Animatable) mQsDetailHeaderProgress.getDrawable(); 289 if (state) { 290 mQsDetailHeaderProgress.animate().cancel(); 291 mQsDetailHeaderProgress.animate() 292 .alpha(1) 293 .withEndAction(anim::start) 294 .start(); 295 } else { 296 mQsDetailHeaderProgress.animate().cancel(); 297 mQsDetailHeaderProgress.animate() 298 .alpha(0f) 299 .withEndAction(anim::stop) 300 .start(); 301 } 302 } 303 checkPendingAnimations()304 private void checkPendingAnimations() { 305 handleToggleStateChanged(mSwitchState, 306 mDetailAdapter != null && mDetailAdapter.getToggleEnabled()); 307 } 308 309 protected Callback mQsPanelCallback = new Callback() { 310 @Override 311 public void onToggleStateChanged(final boolean state) { 312 post(new Runnable() { 313 @Override 314 public void run() { 315 handleToggleStateChanged(state, 316 mDetailAdapter != null && mDetailAdapter.getToggleEnabled()); 317 } 318 }); 319 } 320 321 @Override 322 public void onShowingDetail(final DetailAdapter detail, final int x, final int y) { 323 post(new Runnable() { 324 @Override 325 public void run() { 326 if (isAttachedToWindow()) { 327 handleShowingDetail(detail, x, y, false /* toggleQs */); 328 } 329 } 330 }); 331 } 332 333 @Override 334 public void onScanStateChanged(final boolean state) { 335 post(new Runnable() { 336 @Override 337 public void run() { 338 handleScanStateChanged(state); 339 } 340 }); 341 } 342 }; 343 344 private final AnimatorListenerAdapter mHideGridContentWhenDone = new AnimatorListenerAdapter() { 345 public void onAnimationCancel(Animator animation) { 346 // If we have been cancelled, remove the listener so that onAnimationEnd doesn't get 347 // called, this will avoid accidentally turning off the grid when we don't want to. 348 animation.removeListener(this); 349 mAnimatingOpen = false; 350 checkPendingAnimations(); 351 }; 352 353 @Override 354 public void onAnimationEnd(Animator animation) { 355 // Only hide content if still in detail state. 356 if (mDetailAdapter != null) { 357 mQsPanel.setGridContentVisibility(false); 358 mHeader.setVisibility(View.INVISIBLE); 359 mFooter.setVisibility(View.INVISIBLE); 360 } 361 mAnimatingOpen = false; 362 checkPendingAnimations(); 363 } 364 }; 365 366 private final AnimatorListenerAdapter mTeardownDetailWhenDone = new AnimatorListenerAdapter() { 367 public void onAnimationEnd(Animator animation) { 368 mDetailContent.removeAllViews(); 369 setVisibility(View.INVISIBLE); 370 mClosingDetail = false; 371 }; 372 }; 373 } 374