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