blob: 4a2d2fbc30cf56c0bd2bd8987c1233d873e6e55c [file] [log] [blame]
Brad Stenning19f236a2018-12-11 14:12:30 -08001/*
2 * Copyright (C) 2018 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.notifications;
18
Priyank Singh6e07e3a2019-02-20 23:30:52 +000019import android.app.ActivityManager;
Brad Stenningc622f1d2019-01-29 11:24:11 -080020import android.animation.Animator;
21import android.animation.AnimatorListenerAdapter;
22import android.animation.ValueAnimator;
23import android.annotation.SuppressLint;
Brad Stenning19f236a2018-12-11 14:12:30 -080024import android.car.Car;
25import android.car.CarNotConnectedException;
26import android.car.drivingstate.CarUxRestrictionsManager;
27import android.content.ComponentName;
28import android.content.Context;
29import android.content.ServiceConnection;
Brad Stenningc622f1d2019-01-29 11:24:11 -080030import android.content.res.Configuration;
Brad Stenning19f236a2018-12-11 14:12:30 -080031import android.graphics.PixelFormat;
32import android.os.IBinder;
Brad Stenning2b484372018-11-20 13:04:55 -080033import android.os.ServiceManager;
Brad Stenning19f236a2018-12-11 14:12:30 -080034import android.util.Log;
Brad Stenningc622f1d2019-01-29 11:24:11 -080035import android.view.ContextThemeWrapper;
36import android.view.GestureDetector;
37import android.view.MotionEvent;
Brad Stenning19f236a2018-12-11 14:12:30 -080038import android.view.View;
39import android.view.ViewGroup;
Brad Stenningc622f1d2019-01-29 11:24:11 -080040import android.view.ViewTreeObserver;
Brad Stenning19f236a2018-12-11 14:12:30 -080041import android.view.WindowManager;
42
Brad Stenningc622f1d2019-01-29 11:24:11 -080043import androidx.annotation.NonNull;
44import androidx.recyclerview.widget.RecyclerView;
45
Brad Stenning19f236a2018-12-11 14:12:30 -080046import com.android.car.notification.CarNotificationListener;
Brad Stenningc622f1d2019-01-29 11:24:11 -080047import com.android.car.notification.CarNotificationView;
Brad Stenning19f236a2018-12-11 14:12:30 -080048import com.android.car.notification.CarUxRestrictionManagerWrapper;
Brad Stenning2b484372018-11-20 13:04:55 -080049import com.android.car.notification.NotificationClickHandlerFactory;
Brad Stenning19f236a2018-12-11 14:12:30 -080050import com.android.car.notification.NotificationViewController;
51import com.android.car.notification.PreprocessingManager;
Brad Stenning2b484372018-11-20 13:04:55 -080052import com.android.internal.statusbar.IStatusBarService;
Brad Stenningc622f1d2019-01-29 11:24:11 -080053import com.android.systemui.Dependency;
Brad Stenning19f236a2018-12-11 14:12:30 -080054import com.android.systemui.R;
55import com.android.systemui.SystemUI;
Brad Stenningc622f1d2019-01-29 11:24:11 -080056import com.android.systemui.statusbar.FlingAnimationUtils;
57import com.android.systemui.statusbar.policy.ConfigurationController;
Brad Stenning19f236a2018-12-11 14:12:30 -080058
59/**
60 * Standalone SystemUI for displaying Notifications that have been designed to be used in the car
61 */
Brad Stenningc622f1d2019-01-29 11:24:11 -080062public class NotificationsUI extends SystemUI
63 implements ConfigurationController.ConfigurationListener {
Brad Stenning19f236a2018-12-11 14:12:30 -080064
65 private static final String TAG = "NotificationsUI";
Brad Stenningc622f1d2019-01-29 11:24:11 -080066 // used to calculate how fast to open or close the window
67 private static final float DEFAULT_FLING_VELOCITY = 0;
68 // max time a fling animation takes
69 private static final float FLING_ANIMATION_MAX_TIME = 0.5f;
70 // acceleration rate for the fling animation
71 private static final float FLING_SPEED_UP_FACTOR = 0.6f;
Brad Stenning19f236a2018-12-11 14:12:30 -080072 private CarNotificationListener mCarNotificationListener;
73 private CarUxRestrictionsManager mCarUxRestrictionsManager;
Brad Stenning2b484372018-11-20 13:04:55 -080074 private NotificationClickHandlerFactory mClickHandlerFactory;
Brad Stenning19f236a2018-12-11 14:12:30 -080075 private Car mCar;
76 private ViewGroup mCarNotificationWindow;
77 private NotificationViewController mNotificationViewController;
78 private boolean mIsShowing;
Brad Stenningc622f1d2019-01-29 11:24:11 -080079 private boolean mIsTracking;
80 private boolean mNotificationListAtBottom;
81 private boolean mNotificationListAtBottomAtTimeOfTouch;
Brad Stenning19f236a2018-12-11 14:12:30 -080082 private CarUxRestrictionManagerWrapper mCarUxRestrictionManagerWrapper =
83 new CarUxRestrictionManagerWrapper();
Brad Stenningc622f1d2019-01-29 11:24:11 -080084 // Used in the Notification panel touch listener
85 private GestureDetector mGestureDetector;
86 // Used in scrollable content of the notifications
87 private GestureDetector mScrollUpDetector;
88 private View mContent;
89 private View.OnTouchListener mOnTouchListener;
90 private FlingAnimationUtils mFlingAnimationUtils;
91 private static int sSettleOpenPercentage;
92 private static int sSettleClosePercentage;
Brad Stenning19f236a2018-12-11 14:12:30 -080093
94 /**
95 * Inits the window that hosts the notifications and establishes the connections
96 * to the car related services.
97 */
98 @Override
99 public void start() {
Brad Stenningc622f1d2019-01-29 11:24:11 -0800100 sSettleOpenPercentage = mContext.getResources().getInteger(
101 R.integer.notification_settle_open_percentage);
102 sSettleClosePercentage = mContext.getResources().getInteger(
103 R.integer.notification_settle_close_percentage);
Brad Stenning19f236a2018-12-11 14:12:30 -0800104 WindowManager windowManager =
105 (WindowManager) mContext.getSystemService(Context.WINDOW_SERVICE);
Brad Stenningc622f1d2019-01-29 11:24:11 -0800106 mFlingAnimationUtils = new FlingAnimationUtils(mContext,
107 FLING_ANIMATION_MAX_TIME, FLING_SPEED_UP_FACTOR);
Brad Stenning19f236a2018-12-11 14:12:30 -0800108 mCarNotificationListener = new CarNotificationListener();
Brad Stenningc622f1d2019-01-29 11:24:11 -0800109 // create a notification click handler that closes the notification ui if the an activity
110 // is launched successfully
Brad Stenning2b484372018-11-20 13:04:55 -0800111 mClickHandlerFactory = new NotificationClickHandlerFactory(
112 IStatusBarService.Stub.asInterface(
113 ServiceManager.getService(Context.STATUS_BAR_SERVICE)),
114 launchResult -> {
115 if (launchResult == ActivityManager.START_TASK_TO_FRONT
Priyank Singh6e07e3a2019-02-20 23:30:52 +0000116 || launchResult == ActivityManager.START_SUCCESS){
Brad Stenningc622f1d2019-01-29 11:24:11 -0800117 closeCarNotifications(DEFAULT_FLING_VELOCITY);
Brad Stenning2b484372018-11-20 13:04:55 -0800118 }
119 });
Priyank Singh6e07e3a2019-02-20 23:30:52 +0000120 mCarNotificationListener.registerAsSystemService(mContext, mCarUxRestrictionManagerWrapper,
121 mClickHandlerFactory);
Brad Stenning19f236a2018-12-11 14:12:30 -0800122 mCar = Car.createCar(mContext, mCarConnectionListener);
123 mCar.connect();
Brad Stenningc622f1d2019-01-29 11:24:11 -0800124 NotificationGestureListener gestureListener = new NotificationGestureListener();
125 mGestureDetector = new GestureDetector(mContext, gestureListener);
126 mScrollUpDetector = new GestureDetector(mContext, new ScrollUpDetector());
127 mOnTouchListener = new NotificationPanelTouchListener();
128 mCarNotificationWindow = (ViewGroup) View.inflate(new ContextThemeWrapper(mContext,
129 R.style.Theme_Notification),
Brad Stenning19f236a2018-12-11 14:12:30 -0800130 R.layout.navigation_bar_window, null);
Brad Stenningc622f1d2019-01-29 11:24:11 -0800131 mCarNotificationWindow
132 .setBackgroundColor(mContext.getColor(R.color.notification_shade_background_color));
Priyank Singh6e07e3a2019-02-20 23:30:52 +0000133 inflateNotificationContent();
134
Brad Stenning19f236a2018-12-11 14:12:30 -0800135 WindowManager.LayoutParams layoutParams = new WindowManager.LayoutParams(
136 ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT,
137 WindowManager.LayoutParams.TYPE_DISPLAY_OVERLAY,
138 WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
139 | WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL
140 | WindowManager.LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH
141 | WindowManager.LayoutParams.FLAG_SPLIT_TOUCH,
142 PixelFormat.TRANSLUCENT);
143 layoutParams.setTitle("Car Notification Window");
144 // start in the hidden state
145 mCarNotificationWindow.setVisibility(View.GONE);
146 windowManager.addView(mCarNotificationWindow, layoutParams);
Brad Stenningc622f1d2019-01-29 11:24:11 -0800147
148 // Add this object to the SystemUI component registry such that the status bar
149 // can get a reference to it.
150 putComponent(NotificationsUI.class, this);
151 Dependency.get(ConfigurationController.class).addCallback(this);
152 }
153
154 @SuppressLint("ClickableViewAccessibility")
155 private void inflateNotificationContent() {
156 if (mNotificationViewController != null) {
157 mNotificationViewController.disable();
158 }
159 mCarNotificationWindow.removeAllViews();
160
161 mContent = View.inflate(new ContextThemeWrapper(mContext,
162 com.android.car.notification.R.style.Theme_Notification),
163 R.layout.notification_center_activity,
164 mCarNotificationWindow);
165 // set the click handler such that we can dismiss the UI when a notification is clicked
166 CarNotificationView noteView = mCarNotificationWindow.findViewById(R.id.notification_view);
167 noteView.setClickHandlerFactory(mClickHandlerFactory);
168
169 mContent.setOnTouchListener(mOnTouchListener);
170 // set initial translation after size is calculated
171 mContent.getViewTreeObserver().addOnGlobalLayoutListener(
172 new ViewTreeObserver.OnGlobalLayoutListener() {
173 @Override
174 public void onGlobalLayout() {
175 mContent.getViewTreeObserver().removeOnGlobalLayoutListener(this);
176 if (!mIsShowing && !mIsTracking) {
177 mContent.setTranslationY(mContent.getHeight() * -1);
178 }
179 }
180 });
181
182 RecyclerView notificationList = mCarNotificationWindow
183 .findViewById(com.android.car.notification.R.id.recycler_view);
184 // register a scroll listener so we can figure out if we are at the bottom of the
185 // list of notifications
186 notificationList.addOnScrollListener(new RecyclerView.OnScrollListener() {
187 @Override
188 public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy) {
189 super.onScrolled(recyclerView, dx, dy);
190 if (!notificationList.canScrollVertically(1)) {
191 mNotificationListAtBottom = true;
192 return;
193 }
194 mNotificationListAtBottom = false;
195 mNotificationListAtBottomAtTimeOfTouch = false;
196 }
197 });
198 // add a touch listener such that when the user scrolls up and they are at the bottom
199 // of the list we can start the closing of the view.
200 notificationList.setOnTouchListener(new NotificationListTouchListener());
201
202 // There's a view installed at a higher z-order such that we can intercept the ACTION_DOWN
203 // to set the initial click state.
204 mCarNotificationWindow.findViewById(R.id.glass_pane).setOnTouchListener((v, event) -> {
Priyank Singh6e07e3a2019-02-20 23:30:52 +0000205 if (event.getActionMasked() == MotionEvent.ACTION_UP ) {
Brad Stenningc622f1d2019-01-29 11:24:11 -0800206 mNotificationListAtBottomAtTimeOfTouch = false;
207 }
208 if (event.getActionMasked() == MotionEvent.ACTION_DOWN) {
209 mNotificationListAtBottomAtTimeOfTouch = mNotificationListAtBottom;
210 // register the down event with the gesture detectors so then know where the down
211 // started. This is needed because at this point we don't know which listener
212 // is going to handle scroll and fling events.
213 mGestureDetector.onTouchEvent(event);
214 mScrollUpDetector.onTouchEvent(event);
215 }
216 return false;
217 });
218
Brad Stenning19f236a2018-12-11 14:12:30 -0800219 mNotificationViewController = new NotificationViewController(
220 mCarNotificationWindow
221 .findViewById(com.android.car.notification.R.id.notification_view),
222 PreprocessingManager.getInstance(mContext),
223 mCarNotificationListener,
Brad Stenningc622f1d2019-01-29 11:24:11 -0800224 mCarUxRestrictionManagerWrapper);
225 mNotificationViewController.enable();
226 }
227
228 // allows for day night switch
229 @Override
230 public void onConfigChanged(Configuration newConfig) {
231 inflateNotificationContent();
232 }
233
234 public View.OnTouchListener getDragDownListener() {
235 return mOnTouchListener;
236 }
237
238 /**
239 * This listener is attached to the notification list UI to intercept gestures if the user
240 * is scrolling up when the notification list is at the bottom
241 */
242 private class ScrollUpDetector extends GestureDetector.SimpleOnGestureListener {
243
244 @Override
245 public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
246 return distanceY > 0;
247 }
Priyank Singha4fa2912019-02-28 16:00:16 -0800248
249 @Override
250 public boolean onSingleTapUp(MotionEvent motionEvent) {
251 closeCarNotifications(DEFAULT_FLING_VELOCITY);
252 return false;
253 }
Brad Stenningc622f1d2019-01-29 11:24:11 -0800254 }
255
256 private class NotificationListTouchListener implements View.OnTouchListener {
257
258 @Override
259 public boolean onTouch(View v, MotionEvent event) {
260 // reset mNotificationListAtBottomAtTimeOfTouch here since the "glass pane" will not
261 // get the up event
Priyank Singh6e07e3a2019-02-20 23:30:52 +0000262 if (event.getActionMasked() == MotionEvent.ACTION_UP ) {
Brad Stenningc622f1d2019-01-29 11:24:11 -0800263 mNotificationListAtBottomAtTimeOfTouch = false;
264 }
265 boolean wasScrolledUp = mScrollUpDetector.onTouchEvent(event);
266
267 if (mIsTracking
268 || (mNotificationListAtBottomAtTimeOfTouch && mNotificationListAtBottom
269 && wasScrolledUp)) {
270 mOnTouchListener.onTouch(v, event);
271 // touch event should not be propagated further
272 return true;
273 }
274 return false;
275 }
276 }
277
278 /**
279 * Touch listener installed on the notification panel. It is also used by the Nav and StatusBar
280 */
281 private class NotificationPanelTouchListener implements View.OnTouchListener {
282
283 @Override
284 public boolean onTouch(View v, MotionEvent event) {
285 boolean consumed = mGestureDetector.onTouchEvent(event);
286 if (consumed) {
287 return true;
288 }
289 if (!mIsTracking || event.getActionMasked() != MotionEvent.ACTION_UP) {
290 return false;
291 }
292
293 float percentFromBottom =
294 Math.abs(mContent.getTranslationY() / mContent.getHeight()) * 100;
295 if (mIsShowing) {
296 if (percentFromBottom < sSettleOpenPercentage) {
297 // panel started to close but did not cross minimum threshold thus we open
298 // it back up
299 openCarNotifications(DEFAULT_FLING_VELOCITY);
300 return true;
301 }
302 // panel was lifted more than the threshold thus we close it the rest of the way
303 closeCarNotifications(DEFAULT_FLING_VELOCITY);
304 return true;
305 }
306
307 if (percentFromBottom > sSettleClosePercentage) {
308 // panel was only peeked at thus close it back up
309 closeCarNotifications(DEFAULT_FLING_VELOCITY);
310 return true;
311 }
312 // panel has been open more than threshold thus open it the rest of the way
313 openCarNotifications(DEFAULT_FLING_VELOCITY);
314 return true;
315
316 }
317 }
318
319 /**
320 * Listener called by mGestureDetector. This will be initiated from the
321 * NotificationPanelTouchListener
322 */
323 private class NotificationGestureListener extends GestureDetector.SimpleOnGestureListener {
324 private static final int SWIPE_UP_MIN_DISTANCE = 75;
325 private static final int SWIPE_DOWN_MIN_DISTANCE = 25;
326 private static final int SWIPE_MAX_OFF_PATH = 75;
327 private static final int SWIPE_THRESHOLD_VELOCITY = 200;
328
329 @Override
330 public boolean onScroll(MotionEvent event1, MotionEvent event2, float distanceX,
331 float distanceY) {
Priyank Singha4fa2912019-02-28 16:00:16 -0800332 boolean isDown = event1.getY() - event2.getY() < 0;
333 // CarStatusBar and NavigationBar are identical so avoid the touch if it
334 // starts from NavigationBar to open.
335 if (event1.getRawY() > mCarNotificationWindow.getHeight() && isDown
336 && mCarNotificationWindow.getVisibility() == View.GONE) {
337 mIsTracking = false;
338 return true;
339 }
Brad Stenningc622f1d2019-01-29 11:24:11 -0800340 mIsTracking = true;
341 mCarNotificationWindow.setVisibility(View.VISIBLE);
342
343 mContent.setTranslationY(Math.min(mContent.getTranslationY() - distanceY, 0));
344 if (mContent.getTranslationY() == 0) {
345 mIsTracking = false;
346 }
347 return true;
348 }
349
350 @Override
351 public boolean onFling(MotionEvent event1, MotionEvent event2,
352 float velocityX, float velocityY) {
353 if (Math.abs(event1.getX() - event2.getX()) > SWIPE_MAX_OFF_PATH
Priyank Singh6e07e3a2019-02-20 23:30:52 +0000354 || Math.abs(velocityY) < SWIPE_THRESHOLD_VELOCITY){
Brad Stenningc622f1d2019-01-29 11:24:11 -0800355 // swipe was not vertical or was not fast enough
356 return false;
357 }
358
359 boolean isUp = velocityY < 0;
360 float distanceDelta = Math.abs(event1.getY() - event2.getY());
Brad Stenningc622f1d2019-01-29 11:24:11 -0800361 if (isUp && distanceDelta > SWIPE_UP_MIN_DISTANCE) {
362 // fling up
363 mIsTracking = false;
364 closeCarNotifications(Math.abs(velocityY));
365 return true;
366
Priyank Singha4fa2912019-02-28 16:00:16 -0800367 } else if (!isUp && distanceDelta > SWIPE_DOWN_MIN_DISTANCE
368 && (event1.getRawY() < mCarNotificationWindow.getHeight()
369 || mCarNotificationWindow.getVisibility() == View.VISIBLE)) {
Brad Stenningc622f1d2019-01-29 11:24:11 -0800370 // fling down
371 mIsTracking = false;
372 openCarNotifications(velocityY);
373 return true;
374 }
375
376 return false;
377 }
Brad Stenning19f236a2018-12-11 14:12:30 -0800378 }
379
380 /**
381 * Connection callback to establish UX Restrictions
382 */
383 private ServiceConnection mCarConnectionListener = new ServiceConnection() {
384 @Override
385 public void onServiceConnected(ComponentName name, IBinder service) {
386 try {
387 mCarUxRestrictionsManager = (CarUxRestrictionsManager) mCar.getCarManager(
388 Car.CAR_UX_RESTRICTION_SERVICE);
389 mCarUxRestrictionManagerWrapper
390 .setCarUxRestrictionsManager(mCarUxRestrictionsManager);
391 PreprocessingManager preprocessingManager = PreprocessingManager.getInstance(
392 mContext);
393 preprocessingManager
394 .setCarUxRestrictionManagerWrapper(mCarUxRestrictionManagerWrapper);
395 } catch (CarNotConnectedException e) {
396 Log.e(TAG, "Car not connected in CarConnectionListener", e);
397 }
398 }
399
400 @Override
401 public void onServiceDisconnected(ComponentName name) {
402 Log.e(TAG, "Car service disconnected unexpectedly");
403 }
404 };
405
406 /**
Brad Stenningc622f1d2019-01-29 11:24:11 -0800407 * Toggles the visibility of the notifications
Brad Stenning19f236a2018-12-11 14:12:30 -0800408 */
409 public void toggleShowingCarNotifications() {
410 if (mCarNotificationWindow.getVisibility() == View.VISIBLE) {
Brad Stenningc622f1d2019-01-29 11:24:11 -0800411 closeCarNotifications(DEFAULT_FLING_VELOCITY);
Brad Stenning19f236a2018-12-11 14:12:30 -0800412 return;
413 }
Brad Stenningc622f1d2019-01-29 11:24:11 -0800414 openCarNotifications(DEFAULT_FLING_VELOCITY);
Brad Stenning19f236a2018-12-11 14:12:30 -0800415 }
416
417 /**
418 * Hides the notifications
419 */
Brad Stenningc622f1d2019-01-29 11:24:11 -0800420 public void closeCarNotifications(float velocityY) {
421 float closedTranslation = mContent.getHeight() * -1;
422 ValueAnimator animator =
423 ValueAnimator.ofFloat(mContent.getTranslationY(), closedTranslation);
424 animator.addUpdateListener(
425 animation -> mContent.setTranslationY((Float) animation.getAnimatedValue()));
426 mFlingAnimationUtils.apply(
427 animator, mContent.getTranslationY(), closedTranslation, velocityY);
428 animator.addListener(new AnimatorListenerAdapter() {
429 @Override
430 public void onAnimationEnd(Animator animation) {
431 mCarNotificationWindow.setVisibility(View.GONE);
432 }
433 });
434 animator.start();
Brad Stenning19f236a2018-12-11 14:12:30 -0800435 mNotificationViewController.disable();
436 mIsShowing = false;
Brad Stenningc622f1d2019-01-29 11:24:11 -0800437 mIsTracking = false;
438 RecyclerView notificationListView = mCarNotificationWindow.findViewById(
439 com.android.car.notification.R.id.recycler_view);
440 notificationListView.scrollToPosition(0);
Brad Stenning19f236a2018-12-11 14:12:30 -0800441 }
442
443 /**
444 * Sets the notifications to visible
445 */
Brad Stenningc622f1d2019-01-29 11:24:11 -0800446 public void openCarNotifications(float velocityY) {
Brad Stenning19f236a2018-12-11 14:12:30 -0800447 mCarNotificationWindow.setVisibility(View.VISIBLE);
Brad Stenningc622f1d2019-01-29 11:24:11 -0800448
449 ValueAnimator animator = ValueAnimator.ofFloat(mContent.getTranslationY(), 0);
450 animator.addUpdateListener(
451 animation -> mContent.setTranslationY((Float) animation.getAnimatedValue()));
452 mFlingAnimationUtils.apply(animator, mContent.getTranslationY(), 0, velocityY);
453 animator.start();
454
Brad Stenning19f236a2018-12-11 14:12:30 -0800455 mNotificationViewController.enable();
456 mIsShowing = true;
Brad Stenningc622f1d2019-01-29 11:24:11 -0800457 mIsTracking = false;
Brad Stenning19f236a2018-12-11 14:12:30 -0800458 }
459}