1 /* 2 * Copyright (C) 2015 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.animation.Animator; 20 import android.animation.AnimatorListenerAdapter; 21 import android.app.Notification; 22 import android.app.PendingIntent; 23 import android.app.RemoteInput; 24 import android.content.Context; 25 import android.content.Intent; 26 import android.content.pm.ShortcutManager; 27 import android.graphics.Rect; 28 import android.graphics.drawable.Drawable; 29 import android.os.Bundle; 30 import android.text.Editable; 31 import android.text.TextWatcher; 32 import android.util.AttributeSet; 33 import android.util.Log; 34 import android.view.KeyEvent; 35 import android.view.LayoutInflater; 36 import android.view.MotionEvent; 37 import android.view.View; 38 import android.view.ViewAnimationUtils; 39 import android.view.ViewGroup; 40 import android.view.ViewParent; 41 import android.view.accessibility.AccessibilityEvent; 42 import android.view.inputmethod.CompletionInfo; 43 import android.view.inputmethod.EditorInfo; 44 import android.view.inputmethod.InputConnection; 45 import android.view.inputmethod.InputMethodManager; 46 import android.widget.EditText; 47 import android.widget.ImageButton; 48 import android.widget.LinearLayout; 49 import android.widget.ProgressBar; 50 import android.widget.TextView; 51 52 import com.android.internal.logging.MetricsLogger; 53 import com.android.internal.logging.nano.MetricsProto; 54 import com.android.systemui.Interpolators; 55 import com.android.systemui.R; 56 import com.android.systemui.statusbar.ExpandableView; 57 import com.android.systemui.statusbar.NotificationData; 58 import com.android.systemui.statusbar.RemoteInputController; 59 import com.android.systemui.statusbar.notification.NotificationViewWrapper; 60 import com.android.systemui.statusbar.stack.ScrollContainer; 61 import com.android.systemui.statusbar.stack.StackStateAnimator; 62 63 /** 64 * Host for the remote input. 65 */ 66 public class RemoteInputView extends LinearLayout implements View.OnClickListener, TextWatcher { 67 68 private static final String TAG = "RemoteInput"; 69 70 // A marker object that let's us easily find views of this class. 71 public static final Object VIEW_TAG = new Object(); 72 73 public final Object mToken = new Object(); 74 75 private RemoteEditText mEditText; 76 private ImageButton mSendButton; 77 private ProgressBar mProgressBar; 78 private PendingIntent mPendingIntent; 79 private RemoteInput[] mRemoteInputs; 80 private RemoteInput mRemoteInput; 81 private RemoteInputController mController; 82 83 private NotificationData.Entry mEntry; 84 85 private ScrollContainer mScrollContainer; 86 private View mScrollContainerChild; 87 private boolean mRemoved; 88 89 private int mRevealCx; 90 private int mRevealCy; 91 private int mRevealR; 92 93 private boolean mResetting; 94 private NotificationViewWrapper mWrapper; 95 RemoteInputView(Context context, AttributeSet attrs)96 public RemoteInputView(Context context, AttributeSet attrs) { 97 super(context, attrs); 98 } 99 100 @Override onFinishInflate()101 protected void onFinishInflate() { 102 super.onFinishInflate(); 103 104 mProgressBar = findViewById(R.id.remote_input_progress); 105 106 mSendButton = findViewById(R.id.remote_input_send); 107 mSendButton.setOnClickListener(this); 108 109 mEditText = (RemoteEditText) getChildAt(0); 110 mEditText.setOnEditorActionListener(new TextView.OnEditorActionListener() { 111 @Override 112 public boolean onEditorAction(TextView v, int actionId, KeyEvent event) { 113 final boolean isSoftImeEvent = event == null 114 && (actionId == EditorInfo.IME_ACTION_DONE 115 || actionId == EditorInfo.IME_ACTION_NEXT 116 || actionId == EditorInfo.IME_ACTION_SEND); 117 final boolean isKeyboardEnterKey = event != null 118 && KeyEvent.isConfirmKey(event.getKeyCode()) 119 && event.getAction() == KeyEvent.ACTION_DOWN; 120 121 if (isSoftImeEvent || isKeyboardEnterKey) { 122 if (mEditText.length() > 0) { 123 sendRemoteInput(); 124 } 125 // Consume action to prevent IME from closing. 126 return true; 127 } 128 return false; 129 } 130 }); 131 mEditText.addTextChangedListener(this); 132 mEditText.setInnerFocusable(false); 133 mEditText.mRemoteInputView = this; 134 } 135 sendRemoteInput()136 private void sendRemoteInput() { 137 Bundle results = new Bundle(); 138 results.putString(mRemoteInput.getResultKey(), mEditText.getText().toString()); 139 Intent fillInIntent = new Intent().addFlags(Intent.FLAG_RECEIVER_FOREGROUND); 140 RemoteInput.addResultsToIntent(mRemoteInputs, fillInIntent, 141 results); 142 143 mEditText.setEnabled(false); 144 mSendButton.setVisibility(INVISIBLE); 145 mProgressBar.setVisibility(VISIBLE); 146 mEntry.remoteInputText = mEditText.getText(); 147 mController.addSpinning(mEntry.key, mToken); 148 mController.removeRemoteInput(mEntry, mToken); 149 mEditText.mShowImeOnInputConnection = false; 150 mController.remoteInputSent(mEntry); 151 152 // Tell ShortcutManager that this package has been "activated". ShortcutManager 153 // will reset the throttling for this package. 154 // Strictly speaking, the intent receiver may be different from the notification publisher, 155 // but that's an edge case, and also because we can't always know which package will receive 156 // an intent, so we just reset for the publisher. 157 getContext().getSystemService(ShortcutManager.class).onApplicationActive( 158 mEntry.notification.getPackageName(), 159 mEntry.notification.getUser().getIdentifier()); 160 161 MetricsLogger.action(mContext, MetricsProto.MetricsEvent.ACTION_REMOTE_INPUT_SEND, 162 mEntry.notification.getPackageName()); 163 try { 164 mPendingIntent.send(mContext, 0, fillInIntent); 165 } catch (PendingIntent.CanceledException e) { 166 Log.i(TAG, "Unable to send remote input result", e); 167 MetricsLogger.action(mContext, MetricsProto.MetricsEvent.ACTION_REMOTE_INPUT_FAIL, 168 mEntry.notification.getPackageName()); 169 } 170 } 171 inflate(Context context, ViewGroup root, NotificationData.Entry entry, RemoteInputController controller)172 public static RemoteInputView inflate(Context context, ViewGroup root, 173 NotificationData.Entry entry, 174 RemoteInputController controller) { 175 RemoteInputView v = (RemoteInputView) 176 LayoutInflater.from(context).inflate(R.layout.remote_input, root, false); 177 v.mController = controller; 178 v.mEntry = entry; 179 v.setTag(VIEW_TAG); 180 181 return v; 182 } 183 184 @Override onClick(View v)185 public void onClick(View v) { 186 if (v == mSendButton) { 187 sendRemoteInput(); 188 } 189 } 190 191 @Override onTouchEvent(MotionEvent event)192 public boolean onTouchEvent(MotionEvent event) { 193 super.onTouchEvent(event); 194 195 // We never want for a touch to escape to an outer view or one we covered. 196 return true; 197 } 198 onDefocus(boolean animate)199 private void onDefocus(boolean animate) { 200 mController.removeRemoteInput(mEntry, mToken); 201 mEntry.remoteInputText = mEditText.getText(); 202 203 // During removal, we get reattached and lose focus. Not hiding in that 204 // case to prevent flicker. 205 if (!mRemoved) { 206 if (animate && mRevealR > 0) { 207 Animator reveal = ViewAnimationUtils.createCircularReveal( 208 this, mRevealCx, mRevealCy, mRevealR, 0); 209 reveal.setInterpolator(Interpolators.FAST_OUT_LINEAR_IN); 210 reveal.setDuration(StackStateAnimator.ANIMATION_DURATION_CLOSE_REMOTE_INPUT); 211 reveal.addListener(new AnimatorListenerAdapter() { 212 @Override 213 public void onAnimationEnd(Animator animation) { 214 setVisibility(INVISIBLE); 215 if (mWrapper != null) { 216 mWrapper.setRemoteInputVisible(false); 217 } 218 } 219 }); 220 reveal.start(); 221 } else { 222 setVisibility(INVISIBLE); 223 if (mWrapper != null) { 224 mWrapper.setRemoteInputVisible(false); 225 } 226 } 227 } 228 MetricsLogger.action(mContext, MetricsProto.MetricsEvent.ACTION_REMOTE_INPUT_CLOSE, 229 mEntry.notification.getPackageName()); 230 } 231 232 @Override onAttachedToWindow()233 protected void onAttachedToWindow() { 234 super.onAttachedToWindow(); 235 if (mEntry.row.isChangingPosition()) { 236 if (getVisibility() == VISIBLE && mEditText.isFocusable()) { 237 mEditText.requestFocus(); 238 } 239 } 240 } 241 242 @Override onDetachedFromWindow()243 protected void onDetachedFromWindow() { 244 super.onDetachedFromWindow(); 245 if (mEntry.row.isChangingPosition() || isTemporarilyDetached()) { 246 return; 247 } 248 mController.removeRemoteInput(mEntry, mToken); 249 mController.removeSpinning(mEntry.key, mToken); 250 } 251 setPendingIntent(PendingIntent pendingIntent)252 public void setPendingIntent(PendingIntent pendingIntent) { 253 mPendingIntent = pendingIntent; 254 } 255 setRemoteInput(RemoteInput[] remoteInputs, RemoteInput remoteInput)256 public void setRemoteInput(RemoteInput[] remoteInputs, RemoteInput remoteInput) { 257 mRemoteInputs = remoteInputs; 258 mRemoteInput = remoteInput; 259 mEditText.setHint(mRemoteInput.getLabel()); 260 } 261 focusAnimated()262 public void focusAnimated() { 263 if (getVisibility() != VISIBLE) { 264 Animator animator = ViewAnimationUtils.createCircularReveal( 265 this, mRevealCx, mRevealCy, 0, mRevealR); 266 animator.setDuration(StackStateAnimator.ANIMATION_DURATION_STANDARD); 267 animator.setInterpolator(Interpolators.LINEAR_OUT_SLOW_IN); 268 animator.start(); 269 } 270 focus(); 271 } 272 focus()273 public void focus() { 274 MetricsLogger.action(mContext, MetricsProto.MetricsEvent.ACTION_REMOTE_INPUT_OPEN, 275 mEntry.notification.getPackageName()); 276 277 setVisibility(VISIBLE); 278 if (mWrapper != null) { 279 mWrapper.setRemoteInputVisible(true); 280 } 281 mController.addRemoteInput(mEntry, mToken); 282 mEditText.setInnerFocusable(true); 283 mEditText.mShowImeOnInputConnection = true; 284 mEditText.setText(mEntry.remoteInputText); 285 mEditText.setSelection(mEditText.getText().length()); 286 mEditText.requestFocus(); 287 updateSendButton(); 288 } 289 onNotificationUpdateOrReset()290 public void onNotificationUpdateOrReset() { 291 boolean sending = mProgressBar.getVisibility() == VISIBLE; 292 293 if (sending) { 294 // Update came in after we sent the reply, time to reset. 295 reset(); 296 } 297 298 if (isActive() && mWrapper != null) { 299 mWrapper.setRemoteInputVisible(true); 300 } 301 } 302 reset()303 private void reset() { 304 mResetting = true; 305 306 mEditText.getText().clear(); 307 mEditText.setEnabled(true); 308 mSendButton.setVisibility(VISIBLE); 309 mProgressBar.setVisibility(INVISIBLE); 310 mController.removeSpinning(mEntry.key, mToken); 311 updateSendButton(); 312 onDefocus(false /* animate */); 313 314 mResetting = false; 315 } 316 317 @Override onRequestSendAccessibilityEvent(View child, AccessibilityEvent event)318 public boolean onRequestSendAccessibilityEvent(View child, AccessibilityEvent event) { 319 if (mResetting && child == mEditText) { 320 // Suppress text events if it happens during resetting. Ideally this would be 321 // suppressed by the text view not being shown, but that doesn't work here because it 322 // needs to stay visible for the animation. 323 return false; 324 } 325 return super.onRequestSendAccessibilityEvent(child, event); 326 } 327 updateSendButton()328 private void updateSendButton() { 329 mSendButton.setEnabled(mEditText.getText().length() != 0); 330 } 331 332 @Override beforeTextChanged(CharSequence s, int start, int count, int after)333 public void beforeTextChanged(CharSequence s, int start, int count, int after) {} 334 335 @Override onTextChanged(CharSequence s, int start, int before, int count)336 public void onTextChanged(CharSequence s, int start, int before, int count) {} 337 338 @Override afterTextChanged(Editable s)339 public void afterTextChanged(Editable s) { 340 updateSendButton(); 341 } 342 close()343 public void close() { 344 mEditText.defocusIfNeeded(false /* animated */); 345 } 346 347 @Override onInterceptTouchEvent(MotionEvent ev)348 public boolean onInterceptTouchEvent(MotionEvent ev) { 349 if (ev.getAction() == MotionEvent.ACTION_DOWN) { 350 findScrollContainer(); 351 if (mScrollContainer != null) { 352 mScrollContainer.requestDisallowLongPress(); 353 mScrollContainer.requestDisallowDismiss(); 354 } 355 } 356 return super.onInterceptTouchEvent(ev); 357 } 358 requestScrollTo()359 public boolean requestScrollTo() { 360 findScrollContainer(); 361 mScrollContainer.lockScrollTo(mScrollContainerChild); 362 return true; 363 } 364 findScrollContainer()365 private void findScrollContainer() { 366 if (mScrollContainer == null) { 367 mScrollContainerChild = null; 368 ViewParent p = this; 369 while (p != null) { 370 if (mScrollContainerChild == null && p instanceof ExpandableView) { 371 mScrollContainerChild = (View) p; 372 } 373 if (p.getParent() instanceof ScrollContainer) { 374 mScrollContainer = (ScrollContainer) p.getParent(); 375 if (mScrollContainerChild == null) { 376 mScrollContainerChild = (View) p; 377 } 378 break; 379 } 380 p = p.getParent(); 381 } 382 } 383 } 384 isActive()385 public boolean isActive() { 386 return mEditText.isFocused() && mEditText.isEnabled(); 387 } 388 stealFocusFrom(RemoteInputView other)389 public void stealFocusFrom(RemoteInputView other) { 390 other.close(); 391 setPendingIntent(other.mPendingIntent); 392 setRemoteInput(other.mRemoteInputs, other.mRemoteInput); 393 setRevealParameters(other.mRevealCx, other.mRevealCy, other.mRevealR); 394 focus(); 395 } 396 397 /** 398 * Tries to find an action in {@param actions} that matches the current pending intent 399 * of this view and updates its state to that of the found action 400 * 401 * @return true if a matching action was found, false otherwise 402 */ updatePendingIntentFromActions(Notification.Action[] actions)403 public boolean updatePendingIntentFromActions(Notification.Action[] actions) { 404 if (mPendingIntent == null || actions == null) { 405 return false; 406 } 407 Intent current = mPendingIntent.getIntent(); 408 if (current == null) { 409 return false; 410 } 411 412 for (Notification.Action a : actions) { 413 RemoteInput[] inputs = a.getRemoteInputs(); 414 if (a.actionIntent == null || inputs == null) { 415 continue; 416 } 417 Intent candidate = a.actionIntent.getIntent(); 418 if (!current.filterEquals(candidate)) { 419 continue; 420 } 421 422 RemoteInput input = null; 423 for (RemoteInput i : inputs) { 424 if (i.getAllowFreeFormInput()) { 425 input = i; 426 } 427 } 428 if (input == null) { 429 continue; 430 } 431 setPendingIntent(a.actionIntent); 432 setRemoteInput(inputs, input); 433 return true; 434 } 435 return false; 436 } 437 getPendingIntent()438 public PendingIntent getPendingIntent() { 439 return mPendingIntent; 440 } 441 setRemoved()442 public void setRemoved() { 443 mRemoved = true; 444 } 445 setRevealParameters(int cx, int cy, int r)446 public void setRevealParameters(int cx, int cy, int r) { 447 mRevealCx = cx; 448 mRevealCy = cy; 449 mRevealR = r; 450 } 451 452 @Override dispatchStartTemporaryDetach()453 public void dispatchStartTemporaryDetach() { 454 super.dispatchStartTemporaryDetach(); 455 // Detach the EditText temporarily such that it doesn't get onDetachedFromWindow and 456 // won't lose IME focus. 457 detachViewFromParent(mEditText); 458 } 459 460 @Override dispatchFinishTemporaryDetach()461 public void dispatchFinishTemporaryDetach() { 462 if (isAttachedToWindow()) { 463 attachViewToParent(mEditText, 0, mEditText.getLayoutParams()); 464 } else { 465 removeDetachedView(mEditText, false /* animate */); 466 } 467 super.dispatchFinishTemporaryDetach(); 468 } 469 setWrapper(NotificationViewWrapper wrapper)470 public void setWrapper(NotificationViewWrapper wrapper) { 471 mWrapper = wrapper; 472 } 473 474 /** 475 * An EditText that changes appearance based on whether it's focusable and becomes 476 * un-focusable whenever the user navigates away from it or it becomes invisible. 477 */ 478 public static class RemoteEditText extends EditText { 479 480 private final Drawable mBackground; 481 private RemoteInputView mRemoteInputView; 482 boolean mShowImeOnInputConnection; 483 RemoteEditText(Context context, AttributeSet attrs)484 public RemoteEditText(Context context, AttributeSet attrs) { 485 super(context, attrs); 486 mBackground = getBackground(); 487 } 488 defocusIfNeeded(boolean animate)489 private void defocusIfNeeded(boolean animate) { 490 if (mRemoteInputView != null && mRemoteInputView.mEntry.row.isChangingPosition() 491 || isTemporarilyDetached()) { 492 if (isTemporarilyDetached()) { 493 // We might get reattached but then the other one of HUN / expanded might steal 494 // our focus, so we'll need to save our text here. 495 if (mRemoteInputView != null) { 496 mRemoteInputView.mEntry.remoteInputText = getText(); 497 } 498 } 499 return; 500 } 501 if (isFocusable() && isEnabled()) { 502 setInnerFocusable(false); 503 if (mRemoteInputView != null) { 504 mRemoteInputView.onDefocus(animate); 505 } 506 mShowImeOnInputConnection = false; 507 } 508 } 509 510 @Override onVisibilityChanged(View changedView, int visibility)511 protected void onVisibilityChanged(View changedView, int visibility) { 512 super.onVisibilityChanged(changedView, visibility); 513 514 if (!isShown()) { 515 defocusIfNeeded(false /* animate */); 516 } 517 } 518 519 @Override onFocusChanged(boolean focused, int direction, Rect previouslyFocusedRect)520 protected void onFocusChanged(boolean focused, int direction, Rect previouslyFocusedRect) { 521 super.onFocusChanged(focused, direction, previouslyFocusedRect); 522 if (!focused) { 523 defocusIfNeeded(true /* animate */); 524 } 525 } 526 527 @Override getFocusedRect(Rect r)528 public void getFocusedRect(Rect r) { 529 super.getFocusedRect(r); 530 r.top = mScrollY; 531 r.bottom = mScrollY + (mBottom - mTop); 532 } 533 534 @Override requestRectangleOnScreen(Rect rectangle)535 public boolean requestRectangleOnScreen(Rect rectangle) { 536 return mRemoteInputView.requestScrollTo(); 537 } 538 539 @Override onKeyDown(int keyCode, KeyEvent event)540 public boolean onKeyDown(int keyCode, KeyEvent event) { 541 if (keyCode == KeyEvent.KEYCODE_BACK) { 542 // Eat the DOWN event here to prevent any default behavior. 543 return true; 544 } 545 return super.onKeyDown(keyCode, event); 546 } 547 548 @Override onKeyUp(int keyCode, KeyEvent event)549 public boolean onKeyUp(int keyCode, KeyEvent event) { 550 if (keyCode == KeyEvent.KEYCODE_BACK) { 551 defocusIfNeeded(true /* animate */); 552 return true; 553 } 554 return super.onKeyUp(keyCode, event); 555 } 556 557 @Override onCheckIsTextEditor()558 public boolean onCheckIsTextEditor() { 559 // Stop being editable while we're being removed. During removal, we get reattached, 560 // and editable views get their spellchecking state re-evaluated which is too costly 561 // during the removal animation. 562 boolean flyingOut = mRemoteInputView != null && mRemoteInputView.mRemoved; 563 return !flyingOut && super.onCheckIsTextEditor(); 564 } 565 566 @Override onCreateInputConnection(EditorInfo outAttrs)567 public InputConnection onCreateInputConnection(EditorInfo outAttrs) { 568 final InputConnection inputConnection = super.onCreateInputConnection(outAttrs); 569 570 if (mShowImeOnInputConnection && inputConnection != null) { 571 final InputMethodManager imm = InputMethodManager.getInstance(); 572 if (imm != null) { 573 // onCreateInputConnection is called by InputMethodManager in the middle of 574 // setting up the connection to the IME; wait with requesting the IME until that 575 // work has completed. 576 post(new Runnable() { 577 @Override 578 public void run() { 579 imm.viewClicked(RemoteEditText.this); 580 imm.showSoftInput(RemoteEditText.this, 0); 581 } 582 }); 583 } 584 } 585 586 return inputConnection; 587 } 588 589 @Override onCommitCompletion(CompletionInfo text)590 public void onCommitCompletion(CompletionInfo text) { 591 clearComposingText(); 592 setText(text.getText()); 593 setSelection(getText().length()); 594 } 595 setInnerFocusable(boolean focusable)596 void setInnerFocusable(boolean focusable) { 597 setFocusableInTouchMode(focusable); 598 setFocusable(focusable); 599 setCursorVisible(focusable); 600 601 if (focusable) { 602 requestFocus(); 603 setBackground(mBackground); 604 } else { 605 setBackground(null); 606 } 607 608 } 609 } 610 } 611