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