1 /* 2 * Copyright (C) 2017 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 package com.android.systemui.statusbar; 17 18 import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN_OR_SPLIT_SCREEN_SECONDARY; 19 20 import android.app.ActivityManager; 21 import android.app.PendingIntent; 22 import android.app.RemoteInput; 23 import android.content.Context; 24 import android.content.Intent; 25 import android.os.RemoteException; 26 import android.os.ServiceManager; 27 import android.os.SystemClock; 28 import android.os.SystemProperties; 29 import android.os.UserManager; 30 import android.service.notification.StatusBarNotification; 31 import android.util.ArraySet; 32 import android.util.Log; 33 import android.view.MotionEvent; 34 import android.view.View; 35 import android.view.ViewGroup; 36 import android.view.ViewParent; 37 import android.widget.RemoteViews; 38 import android.widget.TextView; 39 40 import com.android.internal.annotations.VisibleForTesting; 41 import com.android.internal.statusbar.IStatusBarService; 42 import com.android.internal.statusbar.NotificationVisibility; 43 import com.android.systemui.Dependency; 44 import com.android.systemui.Dumpable; 45 import com.android.systemui.statusbar.policy.RemoteInputView; 46 47 import java.io.FileDescriptor; 48 import java.io.PrintWriter; 49 import java.util.Set; 50 51 /** 52 * Class for handling remote input state over a set of notifications. This class handles things 53 * like keeping notifications temporarily that were cancelled as a response to a remote input 54 * interaction, keeping track of notifications to remove when NotificationPresenter is collapsed, 55 * and handling clicks on remote views. 56 */ 57 public class NotificationRemoteInputManager implements Dumpable { 58 public static final boolean ENABLE_REMOTE_INPUT = 59 SystemProperties.getBoolean("debug.enable_remote_input", true); 60 public static final boolean FORCE_REMOTE_INPUT_HISTORY = 61 SystemProperties.getBoolean("debug.force_remoteinput_history", true); 62 private static final boolean DEBUG = false; 63 private static final String TAG = "NotificationRemoteInputManager"; 64 65 /** 66 * How long to wait before auto-dismissing a notification that was kept for remote input, and 67 * has now sent a remote input. We auto-dismiss, because the app may not see a reason to cancel 68 * these given that they technically don't exist anymore. We wait a bit in case the app issues 69 * an update. 70 */ 71 private static final int REMOTE_INPUT_KEPT_ENTRY_AUTO_CANCEL_DELAY = 200; 72 73 protected final ArraySet<NotificationData.Entry> mRemoteInputEntriesToRemoveOnCollapse = 74 new ArraySet<>(); 75 76 // Dependencies: 77 protected final NotificationLockscreenUserManager mLockscreenUserManager = 78 Dependency.get(NotificationLockscreenUserManager.class); 79 80 protected final Context mContext; 81 private final UserManager mUserManager; 82 83 protected RemoteInputController mRemoteInputController; 84 protected NotificationPresenter mPresenter; 85 protected NotificationEntryManager mEntryManager; 86 protected IStatusBarService mBarService; 87 protected Callback mCallback; 88 89 private final RemoteViews.OnClickHandler mOnClickHandler = new RemoteViews.OnClickHandler() { 90 91 @Override 92 public boolean onClickHandler( 93 final View view, final PendingIntent pendingIntent, final Intent fillInIntent) { 94 mPresenter.wakeUpIfDozing(SystemClock.uptimeMillis(), view); 95 96 if (handleRemoteInput(view, pendingIntent)) { 97 return true; 98 } 99 100 if (DEBUG) { 101 Log.v(TAG, "Notification click handler invoked for intent: " + pendingIntent); 102 } 103 logActionClick(view); 104 // The intent we are sending is for the application, which 105 // won't have permission to immediately start an activity after 106 // the user switches to home. We know it is safe to do at this 107 // point, so make sure new activity switches are now allowed. 108 try { 109 ActivityManager.getService().resumeAppSwitches(); 110 } catch (RemoteException e) { 111 } 112 return mCallback.handleRemoteViewClick(view, pendingIntent, fillInIntent, 113 () -> superOnClickHandler(view, pendingIntent, fillInIntent)); 114 } 115 116 private void logActionClick(View view) { 117 ViewParent parent = view.getParent(); 118 String key = getNotificationKeyForParent(parent); 119 if (key == null) { 120 Log.w(TAG, "Couldn't determine notification for click."); 121 return; 122 } 123 int index = -1; 124 // If this is a default template, determine the index of the button. 125 if (view.getId() == com.android.internal.R.id.action0 && 126 parent != null && parent instanceof ViewGroup) { 127 ViewGroup actionGroup = (ViewGroup) parent; 128 index = actionGroup.indexOfChild(view); 129 } 130 final int count = mEntryManager.getNotificationData().getActiveNotifications().size(); 131 final int rank = mEntryManager.getNotificationData().getRank(key); 132 final NotificationVisibility nv = NotificationVisibility.obtain(key, rank, count, true); 133 try { 134 mBarService.onNotificationActionClick(key, index, nv); 135 } catch (RemoteException e) { 136 // Ignore 137 } 138 } 139 140 private String getNotificationKeyForParent(ViewParent parent) { 141 while (parent != null) { 142 if (parent instanceof ExpandableNotificationRow) { 143 return ((ExpandableNotificationRow) parent) 144 .getStatusBarNotification().getKey(); 145 } 146 parent = parent.getParent(); 147 } 148 return null; 149 } 150 151 private boolean superOnClickHandler(View view, PendingIntent pendingIntent, 152 Intent fillInIntent) { 153 return super.onClickHandler(view, pendingIntent, fillInIntent, 154 WINDOWING_MODE_FULLSCREEN_OR_SPLIT_SCREEN_SECONDARY); 155 } 156 157 private boolean handleRemoteInput(View view, PendingIntent pendingIntent) { 158 if (mCallback.shouldHandleRemoteInput(view, pendingIntent)) { 159 return true; 160 } 161 162 Object tag = view.getTag(com.android.internal.R.id.remote_input_tag); 163 RemoteInput[] inputs = null; 164 if (tag instanceof RemoteInput[]) { 165 inputs = (RemoteInput[]) tag; 166 } 167 168 if (inputs == null) { 169 return false; 170 } 171 172 RemoteInput input = null; 173 174 for (RemoteInput i : inputs) { 175 if (i.getAllowFreeFormInput()) { 176 input = i; 177 } 178 } 179 180 if (input == null) { 181 return false; 182 } 183 184 ViewParent p = view.getParent(); 185 RemoteInputView riv = null; 186 while (p != null) { 187 if (p instanceof View) { 188 View pv = (View) p; 189 if (pv.isRootNamespace()) { 190 riv = findRemoteInputView(pv); 191 break; 192 } 193 } 194 p = p.getParent(); 195 } 196 ExpandableNotificationRow row = null; 197 while (p != null) { 198 if (p instanceof ExpandableNotificationRow) { 199 row = (ExpandableNotificationRow) p; 200 break; 201 } 202 p = p.getParent(); 203 } 204 205 if (row == null) { 206 return false; 207 } 208 209 row.setUserExpanded(true); 210 211 if (!mLockscreenUserManager.shouldAllowLockscreenRemoteInput()) { 212 final int userId = pendingIntent.getCreatorUserHandle().getIdentifier(); 213 if (mLockscreenUserManager.isLockscreenPublicMode(userId)) { 214 mCallback.onLockedRemoteInput(row, view); 215 return true; 216 } 217 if (mUserManager.getUserInfo(userId).isManagedProfile() 218 && mPresenter.isDeviceLocked(userId)) { 219 mCallback.onLockedWorkRemoteInput(userId, row, view); 220 return true; 221 } 222 } 223 224 if (riv == null) { 225 riv = findRemoteInputView(row.getPrivateLayout().getExpandedChild()); 226 if (riv == null) { 227 return false; 228 } 229 if (!row.getPrivateLayout().getExpandedChild().isShown()) { 230 mCallback.onMakeExpandedVisibleForRemoteInput(row, view); 231 return true; 232 } 233 } 234 235 int width = view.getWidth(); 236 if (view instanceof TextView) { 237 // Center the reveal on the text which might be off-center from the TextView 238 TextView tv = (TextView) view; 239 if (tv.getLayout() != null) { 240 int innerWidth = (int) tv.getLayout().getLineWidth(0); 241 innerWidth += tv.getCompoundPaddingLeft() + tv.getCompoundPaddingRight(); 242 width = Math.min(width, innerWidth); 243 } 244 } 245 int cx = view.getLeft() + width / 2; 246 int cy = view.getTop() + view.getHeight() / 2; 247 int w = riv.getWidth(); 248 int h = riv.getHeight(); 249 int r = Math.max( 250 Math.max(cx + cy, cx + (h - cy)), 251 Math.max((w - cx) + cy, (w - cx) + (h - cy))); 252 253 riv.setRevealParameters(cx, cy, r); 254 riv.setPendingIntent(pendingIntent); 255 riv.setRemoteInput(inputs, input); 256 riv.focusAnimated(); 257 258 return true; 259 } 260 261 private RemoteInputView findRemoteInputView(View v) { 262 if (v == null) { 263 return null; 264 } 265 return (RemoteInputView) v.findViewWithTag(RemoteInputView.VIEW_TAG); 266 } 267 }; 268 NotificationRemoteInputManager(Context context)269 public NotificationRemoteInputManager(Context context) { 270 mContext = context; 271 mBarService = IStatusBarService.Stub.asInterface( 272 ServiceManager.getService(Context.STATUS_BAR_SERVICE)); 273 mUserManager = (UserManager) mContext.getSystemService(Context.USER_SERVICE); 274 } 275 setUpWithPresenter(NotificationPresenter presenter, NotificationEntryManager entryManager, Callback callback, RemoteInputController.Delegate delegate)276 public void setUpWithPresenter(NotificationPresenter presenter, 277 NotificationEntryManager entryManager, 278 Callback callback, 279 RemoteInputController.Delegate delegate) { 280 mPresenter = presenter; 281 mEntryManager = entryManager; 282 mCallback = callback; 283 mRemoteInputController = new RemoteInputController(delegate); 284 mRemoteInputController.addCallback(new RemoteInputController.Callback() { 285 @Override 286 public void onRemoteInputSent(NotificationData.Entry entry) { 287 if (FORCE_REMOTE_INPUT_HISTORY 288 && mEntryManager.isNotificationKeptForRemoteInput(entry.key)) { 289 mEntryManager.removeNotification(entry.key, null); 290 } else if (mRemoteInputEntriesToRemoveOnCollapse.contains(entry)) { 291 // We're currently holding onto this notification, but from the apps point of 292 // view it is already canceled, so we'll need to cancel it on the apps behalf 293 // after sending - unless the app posts an update in the mean time, so wait a 294 // bit. 295 mPresenter.getHandler().postDelayed(() -> { 296 if (mRemoteInputEntriesToRemoveOnCollapse.remove(entry)) { 297 mEntryManager.removeNotification(entry.key, null); 298 } 299 }, REMOTE_INPUT_KEPT_ENTRY_AUTO_CANCEL_DELAY); 300 } 301 try { 302 mBarService.onNotificationDirectReplied(entry.notification.getKey()); 303 } catch (RemoteException e) { 304 // Nothing to do, system going down 305 } 306 } 307 }); 308 309 } 310 getController()311 public RemoteInputController getController() { 312 return mRemoteInputController; 313 } 314 onUpdateNotification(NotificationData.Entry entry)315 public void onUpdateNotification(NotificationData.Entry entry) { 316 mRemoteInputEntriesToRemoveOnCollapse.remove(entry); 317 } 318 319 /** 320 * Returns true if NotificationRemoteInputManager wants to keep this notification around. 321 * 322 * @param entry notification being removed 323 */ onRemoveNotification(NotificationData.Entry entry)324 public boolean onRemoveNotification(NotificationData.Entry entry) { 325 if (entry != null && mRemoteInputController.isRemoteInputActive(entry) 326 && (entry.row != null && !entry.row.isDismissed())) { 327 mRemoteInputEntriesToRemoveOnCollapse.add(entry); 328 return true; 329 } 330 return false; 331 } 332 onPerformRemoveNotification(StatusBarNotification n, NotificationData.Entry entry)333 public void onPerformRemoveNotification(StatusBarNotification n, 334 NotificationData.Entry entry) { 335 if (mRemoteInputController.isRemoteInputActive(entry)) { 336 mRemoteInputController.removeRemoteInput(entry, null); 337 } 338 } 339 removeRemoteInputEntriesKeptUntilCollapsed()340 public void removeRemoteInputEntriesKeptUntilCollapsed() { 341 for (int i = 0; i < mRemoteInputEntriesToRemoveOnCollapse.size(); i++) { 342 NotificationData.Entry entry = mRemoteInputEntriesToRemoveOnCollapse.valueAt(i); 343 mRemoteInputController.removeRemoteInput(entry, null); 344 mEntryManager.removeNotification(entry.key, mEntryManager.getLatestRankingMap()); 345 } 346 mRemoteInputEntriesToRemoveOnCollapse.clear(); 347 } 348 checkRemoteInputOutside(MotionEvent event)349 public void checkRemoteInputOutside(MotionEvent event) { 350 if (event.getAction() == MotionEvent.ACTION_OUTSIDE // touch outside the source bar 351 && event.getX() == 0 && event.getY() == 0 // a touch outside both bars 352 && mRemoteInputController.isRemoteInputActive()) { 353 mRemoteInputController.closeRemoteInputs(); 354 } 355 } 356 357 @Override dump(FileDescriptor fd, PrintWriter pw, String[] args)358 public void dump(FileDescriptor fd, PrintWriter pw, String[] args) { 359 pw.println("NotificationRemoteInputManager state:"); 360 pw.print(" mRemoteInputEntriesToRemoveOnCollapse: "); 361 pw.println(mRemoteInputEntriesToRemoveOnCollapse); 362 } 363 bindRow(ExpandableNotificationRow row)364 public void bindRow(ExpandableNotificationRow row) { 365 row.setRemoteInputController(mRemoteInputController); 366 row.setRemoteViewClickHandler(mOnClickHandler); 367 } 368 369 @VisibleForTesting getRemoteInputEntriesToRemoveOnCollapse()370 public Set<NotificationData.Entry> getRemoteInputEntriesToRemoveOnCollapse() { 371 return mRemoteInputEntriesToRemoveOnCollapse; 372 } 373 374 /** 375 * Callback for various remote input related events, or for providing information that 376 * NotificationRemoteInputManager needs to know to decide what to do. 377 */ 378 public interface Callback { 379 380 /** 381 * Called when remote input was activated but the device is locked. 382 * 383 * @param row 384 * @param clicked 385 */ onLockedRemoteInput(ExpandableNotificationRow row, View clicked)386 void onLockedRemoteInput(ExpandableNotificationRow row, View clicked); 387 388 /** 389 * Called when remote input was activated but the device is locked and in a managed profile. 390 * 391 * @param userId 392 * @param row 393 * @param clicked 394 */ onLockedWorkRemoteInput(int userId, ExpandableNotificationRow row, View clicked)395 void onLockedWorkRemoteInput(int userId, ExpandableNotificationRow row, View clicked); 396 397 /** 398 * Called when a row should be made expanded for the purposes of remote input. 399 * 400 * @param row 401 * @param clickedView 402 */ onMakeExpandedVisibleForRemoteInput(ExpandableNotificationRow row, View clickedView)403 void onMakeExpandedVisibleForRemoteInput(ExpandableNotificationRow row, View clickedView); 404 405 /** 406 * Return whether or not remote input should be handled for this view. 407 * 408 * @param view 409 * @param pendingIntent 410 * @return true iff the remote input should be handled 411 */ shouldHandleRemoteInput(View view, PendingIntent pendingIntent)412 boolean shouldHandleRemoteInput(View view, PendingIntent pendingIntent); 413 414 /** 415 * Performs any special handling for a remote view click. The default behaviour can be 416 * called through the defaultHandler parameter. 417 * 418 * @param view 419 * @param pendingIntent 420 * @param fillInIntent 421 * @param defaultHandler 422 * @return true iff the click was handled 423 */ handleRemoteViewClick(View view, PendingIntent pendingIntent, Intent fillInIntent, ClickHandler defaultHandler)424 boolean handleRemoteViewClick(View view, PendingIntent pendingIntent, Intent fillInIntent, 425 ClickHandler defaultHandler); 426 } 427 428 /** 429 * Helper interface meant for passing the default on click behaviour to NotificationPresenter, 430 * so it may do its own handling before invoking the default behaviour. 431 */ 432 public interface ClickHandler { 433 /** 434 * Tries to handle a click on a remote view. 435 * 436 * @return true iff the click was handled 437 */ handleClick()438 boolean handleClick(); 439 } 440 } 441