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 View mQsDetailHeaderBack; 67 private BaseStatusBarHeader mHeader; 68 private boolean mTriggeredExpand; 69 private int mOpenX; 70 private int mOpenY; 71 QSDetail(Context context, @Nullable AttributeSet attrs)72 public QSDetail(Context context, @Nullable AttributeSet attrs) { 73 super(context, attrs); 74 } 75 76 @Override onConfigurationChanged(Configuration newConfig)77 protected void onConfigurationChanged(Configuration newConfig) { 78 super.onConfigurationChanged(newConfig); 79 FontSizeUtils.updateFontSize(mDetailDoneButton, R.dimen.qs_detail_button_text_size); 80 FontSizeUtils.updateFontSize(mDetailSettingsButton, R.dimen.qs_detail_button_text_size); 81 82 for (int i = 0; i < mDetailViews.size(); i++) { 83 mDetailViews.valueAt(i).dispatchConfigurationChanged(newConfig); 84 } 85 } 86 87 @Override onFinishInflate()88 protected void onFinishInflate() { 89 super.onFinishInflate(); 90 mDetailContent = (ViewGroup) findViewById(android.R.id.content); 91 mDetailSettingsButton = (TextView) findViewById(android.R.id.button2); 92 mDetailDoneButton = (TextView) findViewById(android.R.id.button1); 93 94 mQsDetailHeader = findViewById(R.id.qs_detail_header); 95 mQsDetailHeaderBack = mQsDetailHeader.findViewById(com.android.internal.R.id.up); 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 mQsDetailHeaderBack.setOnClickListener(doneListener); 113 mDetailDoneButton.setOnClickListener(doneListener); 114 } 115 setQsPanel(QSPanel panel, BaseStatusBarHeader header)116 public void setQsPanel(QSPanel panel, BaseStatusBarHeader header) { 117 mQsPanel = panel; 118 mHeader = header; 119 mHeader.setCallback(mQsPanelCallback); 120 mQsPanel.setCallback(mQsPanelCallback); 121 } 122 setHost(QSTileHost host)123 public void setHost(QSTileHost host) { 124 mHost = host; 125 } isShowingDetail()126 public boolean isShowingDetail() { 127 return mDetailAdapter != null; 128 } 129 setFullyExpanded(boolean fullyExpanded)130 public void setFullyExpanded(boolean fullyExpanded) { 131 mFullyExpanded = fullyExpanded; 132 } 133 setExpanded(boolean qsExpanded)134 public void setExpanded(boolean qsExpanded) { 135 if (!qsExpanded) { 136 mTriggeredExpand = false; 137 } 138 } 139 updateDetailText()140 private void updateDetailText() { 141 mDetailDoneButton.setText(R.string.quick_settings_done); 142 mDetailSettingsButton.setText(R.string.quick_settings_more_settings); 143 } 144 updateResources()145 public void updateResources() { 146 updateDetailText(); 147 } 148 isClosingDetail()149 public boolean isClosingDetail() { 150 return mClosingDetail; 151 } 152 handleShowingDetail(final QSTile.DetailAdapter adapter, int x, int y)153 private void handleShowingDetail(final QSTile.DetailAdapter adapter, int x, int y) { 154 final boolean showingDetail = adapter != null; 155 setClickable(showingDetail); 156 if (showingDetail) { 157 mQsDetailHeaderTitle.setText(adapter.getTitle()); 158 final Boolean toggleState = adapter.getToggleState(); 159 if (toggleState == null) { 160 mQsDetailHeaderSwitch.setVisibility(INVISIBLE); 161 mQsDetailHeader.setClickable(false); 162 } else { 163 mQsDetailHeaderSwitch.setVisibility(VISIBLE); 164 mQsDetailHeaderSwitch.setChecked(toggleState); 165 mQsDetailHeader.setClickable(true); 166 mQsDetailHeader.setOnClickListener(new OnClickListener() { 167 @Override 168 public void onClick(View v) { 169 boolean checked = !mQsDetailHeaderSwitch.isChecked(); 170 mQsDetailHeaderSwitch.setChecked(checked); 171 adapter.setToggleState(checked); 172 } 173 }); 174 } 175 if (!mFullyExpanded) { 176 mTriggeredExpand = true; 177 mHost.animateToggleQSExpansion(); 178 } else { 179 mTriggeredExpand = false; 180 } 181 mOpenX = x; 182 mOpenY = y; 183 } else { 184 // Ensure we collapse into the same point we opened from. 185 x = mOpenX; 186 y = mOpenY; 187 if (mTriggeredExpand) { 188 mHost.animateToggleQSExpansion(); 189 mTriggeredExpand = false; 190 } 191 } 192 193 boolean visibleDiff = (mDetailAdapter != null) != (adapter != null); 194 if (!visibleDiff && mDetailAdapter == adapter) return; // already in right state 195 AnimatorListener listener = null; 196 if (adapter != null) { 197 int viewCacheIndex = adapter.getMetricsCategory(); 198 View detailView = adapter.createDetailView(mContext, mDetailViews.get(viewCacheIndex), 199 mDetailContent); 200 if (detailView == null) throw new IllegalStateException("Must return detail view"); 201 202 final Intent settingsIntent = adapter.getSettingsIntent(); 203 mDetailSettingsButton.setVisibility(settingsIntent != null ? VISIBLE : GONE); 204 mDetailSettingsButton.setOnClickListener(new OnClickListener() { 205 @Override 206 public void onClick(View v) { 207 mHost.startActivityDismissingKeyguard(settingsIntent); 208 } 209 }); 210 211 mDetailContent.removeAllViews(); 212 mDetailContent.addView(detailView); 213 mDetailViews.put(viewCacheIndex, detailView); 214 MetricsLogger.visible(mContext, adapter.getMetricsCategory()); 215 announceForAccessibility(mContext.getString( 216 R.string.accessibility_quick_settings_detail, 217 adapter.getTitle())); 218 mDetailAdapter = adapter; 219 listener = mHideGridContentWhenDone; 220 setVisibility(View.VISIBLE); 221 } else { 222 if (mDetailAdapter != null) { 223 MetricsLogger.hidden(mContext, mDetailAdapter.getMetricsCategory()); 224 } 225 mClosingDetail = true; 226 mDetailAdapter = null; 227 listener = mTeardownDetailWhenDone; 228 mHeader.setVisibility(View.VISIBLE); 229 mQsPanel.setGridContentVisibility(true); 230 mQsPanelCallback.onScanStateChanged(false); 231 } 232 sendAccessibilityEvent(AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED); 233 if (visibleDiff) { 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 handleToggleStateChanged(boolean state)246 private void handleToggleStateChanged(boolean state) { 247 mQsDetailHeaderSwitch.setChecked(state); 248 } 249 handleScanStateChanged(boolean state)250 private void handleScanStateChanged(boolean state) { 251 if (mScanState == state) return; 252 mScanState = state; 253 final Animatable anim = (Animatable) mQsDetailHeaderProgress.getDrawable(); 254 if (state) { 255 mQsDetailHeaderProgress.animate().alpha(1f); 256 anim.start(); 257 } else { 258 mQsDetailHeaderProgress.animate().alpha(0f); 259 anim.stop(); 260 } 261 } 262 263 private final QSPanel.Callback mQsPanelCallback = new QSPanel.Callback() { 264 @Override 265 public void onToggleStateChanged(final boolean state) { 266 post(new Runnable() { 267 @Override 268 public void run() { 269 handleToggleStateChanged(state); 270 } 271 }); 272 } 273 274 @Override 275 public void onShowingDetail(final DetailAdapter detail, final int x, final int y) { 276 post(new Runnable() { 277 @Override 278 public void run() { 279 handleShowingDetail(detail, x, y); 280 } 281 }); 282 } 283 284 @Override 285 public void onScanStateChanged(final boolean state) { 286 post(new Runnable() { 287 @Override 288 public void run() { 289 handleScanStateChanged(state); 290 } 291 }); 292 } 293 }; 294 295 private final AnimatorListenerAdapter mHideGridContentWhenDone = new AnimatorListenerAdapter() { 296 public void onAnimationCancel(Animator animation) { 297 // If we have been cancelled, remove the listener so that onAnimationEnd doesn't get 298 // called, this will avoid accidentally turning off the grid when we don't want to. 299 animation.removeListener(this); 300 }; 301 302 @Override 303 public void onAnimationEnd(Animator animation) { 304 // Only hide content if still in detail state. 305 if (mDetailAdapter != null) { 306 mQsPanel.setGridContentVisibility(false); 307 mHeader.setVisibility(View.INVISIBLE); 308 } 309 } 310 }; 311 312 private final AnimatorListenerAdapter mTeardownDetailWhenDone = new AnimatorListenerAdapter() { 313 public void onAnimationEnd(Animator animation) { 314 mDetailContent.removeAllViews(); 315 setVisibility(View.INVISIBLE); 316 mClosingDetail = false; 317 }; 318 }; 319 } 320