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