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