HeadsUpNotificationView.java revision a6d4fb60ed241002210f27c94fbf363430152fe7
1/* 2 * Copyright (C) 2011 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 17package com.android.systemui.statusbar.policy; 18 19import android.content.Context; 20import android.content.res.Configuration; 21import android.content.res.Resources; 22import android.database.ContentObserver; 23import android.graphics.Outline; 24import android.graphics.Rect; 25import android.os.SystemClock; 26import android.provider.Settings; 27import android.util.ArrayMap; 28import android.util.AttributeSet; 29import android.util.Log; 30import android.view.MotionEvent; 31import android.view.View; 32import android.view.ViewConfiguration; 33import android.view.ViewGroup; 34import android.view.ViewOutlineProvider; 35import android.view.ViewTreeObserver; 36import android.view.accessibility.AccessibilityEvent; 37import android.widget.FrameLayout; 38 39import com.android.systemui.ExpandHelper; 40import com.android.systemui.Gefingerpoken; 41import com.android.systemui.R; 42import com.android.systemui.SwipeHelper; 43import com.android.systemui.statusbar.ExpandableView; 44import com.android.systemui.statusbar.NotificationData; 45import com.android.systemui.statusbar.phone.PhoneStatusBar; 46 47import java.util.ArrayList; 48 49public class HeadsUpNotificationView extends FrameLayout implements SwipeHelper.Callback, ExpandHelper.Callback, 50 ViewTreeObserver.OnComputeInternalInsetsListener { 51 private static final String TAG = "HeadsUpNotificationView"; 52 private static final boolean DEBUG = false; 53 private static final boolean SPEW = DEBUG; 54 private static final String SETTING_HEADS_UP_SNOOZE_LENGTH_MS = "heads_up_snooze_length_ms"; 55 56 Rect mTmpRect = new Rect(); 57 int[] mTmpTwoArray = new int[2]; 58 59 private final int mTouchSensitivityDelay; 60 private final float mMaxAlpha = 1f; 61 private final ArrayMap<String, Long> mSnoozedPackages; 62 private final int mDefaultSnoozeLengthMs; 63 64 private SwipeHelper mSwipeHelper; 65 private EdgeSwipeHelper mEdgeSwipeHelper; 66 67 private PhoneStatusBar mBar; 68 69 private long mStartTouchTime; 70 private ViewGroup mContentHolder; 71 private int mSnoozeLengthMs; 72 private ContentObserver mSettingsObserver; 73 74 private NotificationData.Entry mHeadsUp; 75 private int mUser; 76 77 public HeadsUpNotificationView(Context context, AttributeSet attrs) { 78 this(context, attrs, 0); 79 } 80 81 public HeadsUpNotificationView(Context context, AttributeSet attrs, int defStyle) { 82 super(context, attrs, defStyle); 83 Resources resources = context.getResources(); 84 mTouchSensitivityDelay = resources.getInteger(R.integer.heads_up_sensitivity_delay); 85 if (DEBUG) Log.v(TAG, "create() " + mTouchSensitivityDelay); 86 mSnoozedPackages = new ArrayMap<>(); 87 mDefaultSnoozeLengthMs = resources.getInteger(R.integer.heads_up_default_snooze_length_ms); 88 mSnoozeLengthMs = mDefaultSnoozeLengthMs; 89 } 90 91 public void updateResources() { 92 if (mContentHolder != null) { 93 final LayoutParams lp = (LayoutParams) mContentHolder.getLayoutParams(); 94 lp.width = getResources().getDimensionPixelSize(R.dimen.notification_panel_width); 95 lp.gravity = getResources().getInteger(R.integer.notification_panel_layout_gravity); 96 mContentHolder.setLayoutParams(lp); 97 } 98 } 99 100 public void setBar(PhoneStatusBar bar) { 101 mBar = bar; 102 } 103 104 public ViewGroup getHolder() { 105 return mContentHolder; 106 } 107 108 public boolean showNotification(NotificationData.Entry headsUp) { 109 if (mHeadsUp != null && headsUp != null && !mHeadsUp.key.equals(headsUp.key)) { 110 // bump any previous heads up back to the shade 111 release(); 112 } 113 114 mHeadsUp = headsUp; 115 if (mContentHolder != null) { 116 mContentHolder.removeAllViews(); 117 } 118 119 if (mHeadsUp != null) { 120 mHeadsUp.row.setSystemExpanded(true); 121 mHeadsUp.row.setSensitive(false); 122 mHeadsUp.row.setHeadsUp(true); 123 mHeadsUp.row.setHideSensitive( 124 false, false /* animated */, 0 /* delay */, 0 /* duration */); 125 if (mContentHolder == null) { 126 // too soon! 127 return false; 128 } 129 mContentHolder.setX(0); 130 mContentHolder.setVisibility(View.VISIBLE); 131 mContentHolder.setAlpha(mMaxAlpha); 132 mContentHolder.addView(mHeadsUp.row); 133 sendAccessibilityEvent(AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED); 134 135 mSwipeHelper.snapChild(mContentHolder, 1f); 136 mStartTouchTime = SystemClock.elapsedRealtime() + mTouchSensitivityDelay; 137 138 mHeadsUp.setInterruption(); 139 140 // 2. Animate mHeadsUpNotificationView in 141 mBar.scheduleHeadsUpOpen(); 142 143 // 3. Set alarm to age the notification off 144 mBar.resetHeadsUpDecayTimer(); 145 } 146 return true; 147 } 148 149 @Override 150 protected void onVisibilityChanged(View changedView, int visibility) { 151 super.onVisibilityChanged(changedView, visibility); 152 if (changedView.getVisibility() == VISIBLE) { 153 sendAccessibilityEvent(AccessibilityEvent.TYPE_WINDOW_CONTENT_CHANGED); 154 } 155 } 156 157 public boolean isShowing(String key) { 158 return mHeadsUp != null && mHeadsUp.key.equals(key); 159 } 160 161 /** Discard the Heads Up notification. */ 162 public void clear() { 163 mHeadsUp = null; 164 mBar.scheduleHeadsUpClose(); 165 } 166 167 /** Respond to dismissal of the Heads Up window. */ 168 public void dismiss() { 169 if (mHeadsUp == null) return; 170 if (mHeadsUp.notification.isClearable()) { 171 mBar.onNotificationClear(mHeadsUp.notification); 172 } else { 173 release(); 174 } 175 mHeadsUp = null; 176 mBar.scheduleHeadsUpClose(); 177 } 178 179 /** Push any current Heads Up notification down into the shade. */ 180 public void release() { 181 if (mHeadsUp != null) { 182 mBar.displayNotificationFromHeadsUp(mHeadsUp.notification); 183 } 184 mHeadsUp = null; 185 } 186 187 public boolean isSnoozed(String packageName) { 188 final String key = snoozeKey(packageName, mUser); 189 Long snoozedUntil = mSnoozedPackages.get(key); 190 if (snoozedUntil != null) { 191 if (snoozedUntil > SystemClock.elapsedRealtime()) { 192 if (DEBUG) Log.v(TAG, key + " snoozed"); 193 return true; 194 } 195 mSnoozedPackages.remove(packageName); 196 } 197 return false; 198 } 199 200 private void snooze() { 201 mSnoozedPackages.put(snoozeKey(mHeadsUp.notification.getPackageName(), mUser), 202 SystemClock.elapsedRealtime() + mSnoozeLengthMs); 203 releaseAndClose(); 204 } 205 206 private static String snoozeKey(String packageName, int user) { 207 return user + "," + packageName; 208 } 209 210 public void releaseAndClose() { 211 release(); 212 mBar.scheduleHeadsUpClose(); 213 } 214 215 public NotificationData.Entry getEntry() { 216 return mHeadsUp; 217 } 218 219 public boolean isClearable() { 220 return mHeadsUp == null || mHeadsUp.notification.isClearable(); 221 } 222 223 // ViewGroup methods 224 225 private static final ViewOutlineProvider CONTENT_HOLDER_OUTLINE_PROVIDER = 226 new ViewOutlineProvider() { 227 @Override 228 public void getOutline(View view, Outline outline) { 229 int outlineLeft = view.getPaddingLeft(); 230 int outlineTop = view.getPaddingTop(); 231 232 // Apply padding to shadow. 233 outline.setRect(outlineLeft, outlineTop, 234 view.getWidth() - outlineLeft - view.getPaddingRight(), 235 view.getHeight() - outlineTop - view.getPaddingBottom()); 236 } 237 }; 238 239 @Override 240 public void onAttachedToWindow() { 241 final ViewConfiguration viewConfiguration = ViewConfiguration.get(getContext()); 242 float touchSlop = viewConfiguration.getScaledTouchSlop(); 243 mSwipeHelper = new SwipeHelper(SwipeHelper.X, this, getContext()); 244 mSwipeHelper.setMaxSwipeProgress(mMaxAlpha); 245 mEdgeSwipeHelper = new EdgeSwipeHelper(touchSlop); 246 247 int minHeight = getResources().getDimensionPixelSize(R.dimen.notification_min_height); 248 int maxHeight = getResources().getDimensionPixelSize(R.dimen.notification_max_height); 249 250 mContentHolder = (ViewGroup) findViewById(R.id.content_holder); 251 mContentHolder.setOutlineProvider(CONTENT_HOLDER_OUTLINE_PROVIDER); 252 253 mSnoozeLengthMs = Settings.Global.getInt(mContext.getContentResolver(), 254 SETTING_HEADS_UP_SNOOZE_LENGTH_MS, mDefaultSnoozeLengthMs); 255 mSettingsObserver = new ContentObserver(getHandler()) { 256 @Override 257 public void onChange(boolean selfChange) { 258 final int packageSnoozeLengthMs = Settings.Global.getInt( 259 mContext.getContentResolver(), SETTING_HEADS_UP_SNOOZE_LENGTH_MS, -1); 260 if (packageSnoozeLengthMs > -1 && packageSnoozeLengthMs != mSnoozeLengthMs) { 261 mSnoozeLengthMs = packageSnoozeLengthMs; 262 if (DEBUG) Log.v(TAG, "mSnoozeLengthMs = " + mSnoozeLengthMs); 263 } 264 } 265 }; 266 mContext.getContentResolver().registerContentObserver( 267 Settings.Global.getUriFor(SETTING_HEADS_UP_SNOOZE_LENGTH_MS), false, 268 mSettingsObserver); 269 if (DEBUG) Log.v(TAG, "mSnoozeLengthMs = " + mSnoozeLengthMs); 270 271 if (mHeadsUp != null) { 272 // whoops, we're on already! 273 showNotification(mHeadsUp); 274 } 275 276 getViewTreeObserver().addOnComputeInternalInsetsListener(this); 277 } 278 279 @Override 280 protected void onDetachedFromWindow() { 281 mContext.getContentResolver().unregisterContentObserver(mSettingsObserver); 282 } 283 284 @Override 285 public boolean onInterceptTouchEvent(MotionEvent ev) { 286 if (DEBUG) Log.v(TAG, "onInterceptTouchEvent()"); 287 if (SystemClock.elapsedRealtime() < mStartTouchTime) { 288 return true; 289 } 290 return mEdgeSwipeHelper.onInterceptTouchEvent(ev) 291 || mSwipeHelper.onInterceptTouchEvent(ev) 292 || super.onInterceptTouchEvent(ev); 293 } 294 295 // View methods 296 297 @Override 298 public void onDraw(android.graphics.Canvas c) { 299 super.onDraw(c); 300 if (DEBUG) { 301 //Log.d(TAG, "onDraw: canvas height: " + c.getHeight() + "px; measured height: " 302 // + getMeasuredHeight() + "px"); 303 c.save(); 304 c.clipRect(6, 6, c.getWidth() - 6, getMeasuredHeight() - 6, 305 android.graphics.Region.Op.DIFFERENCE); 306 c.drawColor(0xFFcc00cc); 307 c.restore(); 308 } 309 } 310 311 @Override 312 public boolean onTouchEvent(MotionEvent ev) { 313 if (SystemClock.elapsedRealtime() < mStartTouchTime) { 314 return false; 315 } 316 mBar.resetHeadsUpDecayTimer(); 317 return mEdgeSwipeHelper.onTouchEvent(ev) 318 || mSwipeHelper.onTouchEvent(ev) 319 || super.onTouchEvent(ev); 320 } 321 322 @Override 323 protected void onConfigurationChanged(Configuration newConfig) { 324 super.onConfigurationChanged(newConfig); 325 float densityScale = getResources().getDisplayMetrics().density; 326 mSwipeHelper.setDensityScale(densityScale); 327 float pagingTouchSlop = ViewConfiguration.get(getContext()).getScaledPagingTouchSlop(); 328 mSwipeHelper.setPagingTouchSlop(pagingTouchSlop); 329 } 330 331 // ExpandHelper.Callback methods 332 333 @Override 334 public ExpandableView getChildAtRawPosition(float x, float y) { 335 return getChildAtPosition(x, y); 336 } 337 338 @Override 339 public ExpandableView getChildAtPosition(float x, float y) { 340 return mHeadsUp == null ? null : mHeadsUp.row; 341 } 342 343 @Override 344 public boolean canChildBeExpanded(View v) { 345 return mHeadsUp != null && mHeadsUp.row == v && mHeadsUp.row.isExpandable(); 346 } 347 348 @Override 349 public void setUserExpandedChild(View v, boolean userExpanded) { 350 if (mHeadsUp != null && mHeadsUp.row == v) { 351 mHeadsUp.row.setUserExpanded(userExpanded); 352 } 353 } 354 355 @Override 356 public void setUserLockedChild(View v, boolean userLocked) { 357 if (mHeadsUp != null && mHeadsUp.row == v) { 358 mHeadsUp.row.setUserLocked(userLocked); 359 } 360 } 361 362 @Override 363 public void expansionStateChanged(boolean isExpanding) { 364 365 } 366 367 // SwipeHelper.Callback methods 368 369 @Override 370 public boolean canChildBeDismissed(View v) { 371 return true; 372 } 373 374 @Override 375 public boolean isAntiFalsingNeeded() { 376 return false; 377 } 378 379 @Override 380 public float getFalsingThresholdFactor() { 381 return 1.0f; 382 } 383 384 @Override 385 public void onChildDismissed(View v) { 386 Log.v(TAG, "User swiped heads up to dismiss"); 387 mBar.onHeadsUpDismissed(); 388 } 389 390 @Override 391 public void onBeginDrag(View v) { 392 } 393 394 @Override 395 public void onDragCancelled(View v) { 396 mContentHolder.setAlpha(mMaxAlpha); // sometimes this isn't quite reset 397 } 398 399 @Override 400 public void onChildSnappedBack(View animView) { 401 } 402 403 @Override 404 public boolean updateSwipeProgress(View animView, boolean dismissable, float swipeProgress) { 405 getBackground().setAlpha((int) (255 * swipeProgress)); 406 return false; 407 } 408 409 @Override 410 public View getChildAtPosition(MotionEvent ev) { 411 return mContentHolder; 412 } 413 414 @Override 415 public View getChildContentView(View v) { 416 return mContentHolder; 417 } 418 419 @Override 420 public void onComputeInternalInsets(ViewTreeObserver.InternalInsetsInfo info) { 421 mContentHolder.getLocationOnScreen(mTmpTwoArray); 422 423 info.setTouchableInsets(ViewTreeObserver.InternalInsetsInfo.TOUCHABLE_INSETS_REGION); 424 info.touchableRegion.set(mTmpTwoArray[0], mTmpTwoArray[1], 425 mTmpTwoArray[0] + mContentHolder.getWidth(), 426 mTmpTwoArray[1] + mContentHolder.getHeight()); 427 } 428 429 public void escalate() { 430 mBar.scheduleHeadsUpEscalation(); 431 } 432 433 public String getKey() { 434 return mHeadsUp == null ? null : mHeadsUp.notification.getKey(); 435 } 436 437 public void setUser(int user) { 438 mUser = user; 439 } 440 441 private class EdgeSwipeHelper implements Gefingerpoken { 442 private static final boolean DEBUG_EDGE_SWIPE = false; 443 private final float mTouchSlop; 444 private boolean mConsuming; 445 private float mFirstY; 446 private float mFirstX; 447 448 public EdgeSwipeHelper(float touchSlop) { 449 mTouchSlop = touchSlop; 450 } 451 452 @Override 453 public boolean onInterceptTouchEvent(MotionEvent ev) { 454 switch (ev.getActionMasked()) { 455 case MotionEvent.ACTION_DOWN: 456 if (DEBUG_EDGE_SWIPE) Log.d(TAG, "action down " + ev.getY()); 457 mFirstX = ev.getX(); 458 mFirstY = ev.getY(); 459 mConsuming = false; 460 break; 461 462 case MotionEvent.ACTION_MOVE: 463 if (DEBUG_EDGE_SWIPE) Log.d(TAG, "action move " + ev.getY()); 464 final float dY = ev.getY() - mFirstY; 465 final float daX = Math.abs(ev.getX() - mFirstX); 466 final float daY = Math.abs(dY); 467 if (!mConsuming && daX < daY && daY > mTouchSlop) { 468 snooze(); 469 if (dY > 0) { 470 if (DEBUG_EDGE_SWIPE) Log.d(TAG, "found an open"); 471 mBar.animateExpandNotificationsPanel(); 472 } 473 mConsuming = true; 474 } 475 break; 476 477 case MotionEvent.ACTION_UP: 478 case MotionEvent.ACTION_CANCEL: 479 if (DEBUG_EDGE_SWIPE) Log.d(TAG, "action done" ); 480 mConsuming = false; 481 break; 482 } 483 return mConsuming; 484 } 485 486 @Override 487 public boolean onTouchEvent(MotionEvent ev) { 488 return mConsuming; 489 } 490 } 491} 492