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