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