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