• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2022 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.server.inputmethod;
18 
19 import static com.android.text.flags.Flags.handwritingEndOfLineTap;
20 
21 import android.Manifest;
22 import android.annotation.AnyThread;
23 import android.annotation.NonNull;
24 import android.annotation.Nullable;
25 import android.annotation.RequiresPermission;
26 import android.annotation.UiThread;
27 import android.content.ComponentName;
28 import android.content.Context;
29 import android.content.pm.PackageManagerInternal;
30 import android.graphics.Region;
31 import android.hardware.input.InputManager;
32 import android.hardware.input.InputManagerGlobal;
33 import android.os.Handler;
34 import android.os.IBinder;
35 import android.os.Looper;
36 import android.os.SystemClock;
37 import android.text.TextUtils;
38 import android.util.Slog;
39 import android.view.BatchedInputEventReceiver;
40 import android.view.Choreographer;
41 import android.view.Display;
42 import android.view.InputChannel;
43 import android.view.InputEvent;
44 import android.view.InputEventReceiver;
45 import android.view.MotionEvent;
46 import android.view.PointerIcon;
47 import android.view.SurfaceControl;
48 import android.view.View;
49 import android.view.inputmethod.InputMethodManager;
50 
51 import com.android.server.LocalServices;
52 import com.android.server.input.InputManagerInternal;
53 import com.android.server.wm.WindowManagerInternal;
54 
55 import java.util.ArrayList;
56 import java.util.List;
57 import java.util.Objects;
58 import java.util.OptionalInt;
59 
60 // TODO(b/210039666): See if we can make this class thread-safe.
61 final class HandwritingModeController {
62 
63     public static final String TAG = HandwritingModeController.class.getSimpleName();
64     static final boolean DEBUG = false;
65     // Use getHandwritingBufferSize() and not this value directly.
66     private static final int EVENT_BUFFER_SIZE = 100;
67     // A longer event buffer used for handwriting delegation
68     // TODO(b/210039666): make this device touch sampling rate dependent.
69     // Use getHandwritingBufferSize() and not this value directly.
70     private static final int LONG_EVENT_BUFFER_SIZE = EVENT_BUFFER_SIZE * 20;
71     private static final long HANDWRITING_DELEGATION_IDLE_TIMEOUT_MS = 3000;
72     private static final long AFTER_STYLUS_UP_ALLOW_PERIOD_MS = 200L;
73 
74     private final Context mContext;
75     // This must be the looper for the UiThread.
76     private final Looper mLooper;
77     private final InputManagerInternal mInputManagerInternal;
78     private final WindowManagerInternal mWindowManagerInternal;
79     private final PackageManagerInternal mPackageManagerInternal;
80 
81     private ArrayList<MotionEvent> mHandwritingBuffer;
82     private InputEventReceiver mHandwritingEventReceiver;
83     private Runnable mInkWindowInitRunnable;
84     private boolean mRecordingGesture;
85     private boolean mRecordingGestureAfterStylusUp;
86     private int mCurrentDisplayId;
87     // when set, package names are used for handwriting delegation.
88     private @Nullable String mDelegatePackageName;
89     private @Nullable String mDelegatorPackageName;
90     private boolean mDelegatorFromDefaultHomePackage;
91     private boolean mDelegationConnectionlessFlow;
92     private Runnable mDelegationIdleTimeoutRunnable;
93     private Handler mDelegationIdleTimeoutHandler;
94     private final Runnable mDiscardDelegationTextRunnable;
95     private HandwritingEventReceiverSurface mHandwritingSurface;
96 
97     private int mCurrentRequestId;
98 
99     @AnyThread
HandwritingModeController(Context context, Looper uiThreadLooper, Runnable inkWindowInitRunnable, Runnable discardDelegationTextRunnable)100     HandwritingModeController(Context context, Looper uiThreadLooper,
101             Runnable inkWindowInitRunnable,
102             Runnable discardDelegationTextRunnable) {
103         mContext = context;
104         mLooper = uiThreadLooper;
105         mCurrentDisplayId = Display.INVALID_DISPLAY;
106         mInputManagerInternal = LocalServices.getService(InputManagerInternal.class);
107         mWindowManagerInternal = LocalServices.getService(WindowManagerInternal.class);
108         mPackageManagerInternal = LocalServices.getService(PackageManagerInternal.class);
109         mCurrentRequestId = 0;
110         mInkWindowInitRunnable = inkWindowInitRunnable;
111         mDiscardDelegationTextRunnable = discardDelegationTextRunnable;
112     }
113 
114     /**
115      * Initializes the handwriting spy on the given displayId.
116      *
117      * This must be called from the UI Thread because it will start processing events using an
118      * InputEventReceiver that batches events according to the current thread's Choreographer.
119      */
120     @UiThread
initializeHandwritingSpy(int displayId)121     void initializeHandwritingSpy(int displayId) {
122         // When resetting, reuse resources if we are reinitializing on the same display.
123         reset(displayId == mCurrentDisplayId);
124         mCurrentDisplayId = displayId;
125 
126         if (mHandwritingBuffer == null) {
127             mHandwritingBuffer = new ArrayList<>(getHandwritingBufferSize());
128         }
129 
130         if (DEBUG) Slog.d(TAG, "Initializing handwriting spy monitor for display: " + displayId);
131         final String name = "stylus-handwriting-event-receiver-" + displayId;
132         final InputChannel channel = mInputManagerInternal.createInputChannel(name);
133         Objects.requireNonNull(channel, "Failed to create input channel");
134         final SurfaceControl surface =
135                 mHandwritingSurface != null ? mHandwritingSurface.getSurface()
136                         : mWindowManagerInternal.getHandwritingSurfaceForDisplay(displayId);
137         if (surface == null) {
138             Slog.e(TAG, "Failed to create input surface");
139             return;
140         }
141 
142         mHandwritingSurface = new HandwritingEventReceiverSurface(
143                 mContext, name, displayId, surface, channel);
144 
145         // Use a dup of the input channel so that event processing can be paused by disposing the
146         // event receiver without causing a fd hangup.
147         mHandwritingEventReceiver = new BatchedInputEventReceiver.SimpleBatchedInputEventReceiver(
148                 channel.dup(), mLooper, Choreographer.getInstance(), this::onInputEvent);
149         mCurrentRequestId++;
150     }
151 
getCurrentRequestId()152     OptionalInt getCurrentRequestId() {
153         if (mHandwritingSurface == null) {
154             Slog.e(TAG, "Cannot get requestId: Handwriting was not initialized.");
155             return OptionalInt.empty();
156         }
157         return OptionalInt.of(mCurrentRequestId);
158     }
159 
setNotTouchable(boolean notTouchable)160     void setNotTouchable(boolean notTouchable) {
161         if (!getCurrentRequestId().isPresent()) {
162             return;
163         }
164         mHandwritingSurface.setNotTouchable(notTouchable);
165     }
166 
setHandwritingTouchableRegion(Region region)167     void setHandwritingTouchableRegion(Region region) {
168         if (!getCurrentRequestId().isPresent()) {
169             return;
170         }
171         mHandwritingSurface.setTouchableRegion(region);
172     }
173 
isStylusGestureOngoing()174     boolean isStylusGestureOngoing() {
175         if (mRecordingGestureAfterStylusUp && !mHandwritingBuffer.isEmpty()) {
176             // If it is less than AFTER_STYLUS_UP_ALLOW_PERIOD_MS after the stylus up event, return
177             // true so that handwriting can start.
178             MotionEvent lastEvent = mHandwritingBuffer.get(mHandwritingBuffer.size() - 1);
179             if (lastEvent.getActionMasked() == MotionEvent.ACTION_UP) {
180                 return SystemClock.uptimeMillis() - lastEvent.getEventTime()
181                         < AFTER_STYLUS_UP_ALLOW_PERIOD_MS;
182             }
183         }
184         return mRecordingGesture;
185     }
186 
hasOngoingStylusHandwritingSession()187     boolean hasOngoingStylusHandwritingSession() {
188         return mHandwritingSurface != null && mHandwritingSurface.isIntercepting();
189     }
190 
191     /**
192      * Prepare delegation of stylus handwriting to a different editor
193      * @see InputMethodManager#prepareStylusHandwritingDelegation(View, String)
194      */
prepareStylusHandwritingDelegation( int userId, @NonNull String delegatePackageName, @NonNull String delegatorPackageName, boolean connectionless)195     void prepareStylusHandwritingDelegation(
196             int userId, @NonNull String delegatePackageName, @NonNull String delegatorPackageName,
197             boolean connectionless) {
198         mDelegatePackageName = delegatePackageName;
199         mDelegatorPackageName = delegatorPackageName;
200         mDelegatorFromDefaultHomePackage = false;
201         // mDelegatorFromDefaultHomeActivity is only used in the cross-package delegation case.
202         // For same-package delegation, it doesn't need to be checked.
203         if (!delegatorPackageName.equals(delegatePackageName)) {
204             ComponentName defaultHomeActivity =
205                     mPackageManagerInternal.getDefaultHomeActivity(userId);
206             if (defaultHomeActivity != null) {
207                 mDelegatorFromDefaultHomePackage =
208                         delegatorPackageName.equals(defaultHomeActivity.getPackageName());
209             }
210         }
211         mDelegationConnectionlessFlow = connectionless;
212         if (!connectionless) {
213             if (mHandwritingBuffer == null) {
214                 mHandwritingBuffer = new ArrayList<>(getHandwritingBufferSize());
215             } else {
216                 mHandwritingBuffer.ensureCapacity(getHandwritingBufferSize());
217             }
218         }
219         scheduleHandwritingDelegationTimeout();
220     }
221 
getDelegatePackageName()222     @Nullable String getDelegatePackageName() {
223         return mDelegatePackageName;
224     }
225 
getDelegatorPackageName()226     @Nullable String getDelegatorPackageName() {
227         return mDelegatorPackageName;
228     }
229 
isDelegatorFromDefaultHomePackage()230     boolean isDelegatorFromDefaultHomePackage() {
231         return mDelegatorFromDefaultHomePackage;
232     }
233 
isDelegationUsingConnectionlessFlow()234     boolean isDelegationUsingConnectionlessFlow() {
235         return mDelegationConnectionlessFlow;
236     }
237 
scheduleHandwritingDelegationTimeout()238     private void scheduleHandwritingDelegationTimeout() {
239         if (mDelegationIdleTimeoutHandler == null) {
240             mDelegationIdleTimeoutHandler = new Handler(mLooper);
241         } else {
242             mDelegationIdleTimeoutHandler.removeCallbacks(mDelegationIdleTimeoutRunnable);
243         }
244         mDelegationIdleTimeoutRunnable =  () -> {
245             Slog.d(TAG, "Stylus handwriting delegation idle timed-out.");
246             clearPendingHandwritingDelegation();
247             if (mHandwritingBuffer != null) {
248                 mHandwritingBuffer.forEach(MotionEvent::recycle);
249                 mHandwritingBuffer.clear();
250                 mHandwritingBuffer.trimToSize();
251                 mHandwritingBuffer.ensureCapacity(getHandwritingBufferSize());
252             }
253         };
254         mDelegationIdleTimeoutHandler.postDelayed(
255                 mDelegationIdleTimeoutRunnable, HANDWRITING_DELEGATION_IDLE_TIMEOUT_MS);
256     }
257 
getHandwritingBufferSize()258     private int getHandwritingBufferSize() {
259         if (mDelegatePackageName != null && mDelegatorPackageName != null) {
260             return LONG_EVENT_BUFFER_SIZE;
261         }
262         return EVENT_BUFFER_SIZE;
263     }
264     /**
265      * Clear any pending handwriting delegation info.
266      */
clearPendingHandwritingDelegation()267     void clearPendingHandwritingDelegation() {
268         if (DEBUG) {
269             Slog.d(TAG, "clearPendingHandwritingDelegation");
270         }
271         if (mDelegationIdleTimeoutHandler != null) {
272             mDelegationIdleTimeoutHandler.removeCallbacks(mDelegationIdleTimeoutRunnable);
273             mDelegationIdleTimeoutHandler = null;
274         }
275         mDelegationIdleTimeoutRunnable = null;
276         mDelegatorPackageName = null;
277         mDelegatePackageName = null;
278         mDelegatorFromDefaultHomePackage = false;
279         if (mDelegationConnectionlessFlow) {
280             mDelegationConnectionlessFlow = false;
281             mDiscardDelegationTextRunnable.run();
282         }
283     }
284 
285     /**
286      * Starts a {@link HandwritingSession} to transfer to the IME.
287      *
288      * This must be called from the UI Thread to avoid race conditions between processing more
289      * input events and disposing the input event receiver.
290      * @return the handwriting session to send to the IME, or null if the request was invalid.
291      */
292     @RequiresPermission(Manifest.permission.MONITOR_INPUT)
293     @UiThread
294     @Nullable
startHandwritingSession( int requestId, int imePid, int imeUid, IBinder focusedWindowToken)295     HandwritingSession startHandwritingSession(
296             int requestId, int imePid, int imeUid, IBinder focusedWindowToken) {
297         clearPendingHandwritingDelegation();
298         if (mHandwritingSurface == null) {
299             Slog.e(TAG, "Cannot start handwriting session: Handwriting was not initialized.");
300             return null;
301         }
302         if (requestId != mCurrentRequestId) {
303             Slog.e(TAG, "Cannot start handwriting session: Invalid request id: " + requestId);
304             return null;
305         }
306         if (!isStylusGestureOngoing()) {
307             Slog.e(TAG, "Cannot start handwriting session: No stylus gesture is being recorded.");
308             return null;
309         }
310         Objects.requireNonNull(mHandwritingEventReceiver,
311                 "Handwriting session was already transferred to IME.");
312         final MotionEvent downEvent = mHandwritingBuffer.get(0);
313         assert (downEvent.getActionMasked() == MotionEvent.ACTION_DOWN);
314         if (!mWindowManagerInternal.isPointInsideWindow(
315                 focusedWindowToken, mCurrentDisplayId, downEvent.getRawX(), downEvent.getRawY())) {
316             Slog.e(TAG, "Cannot start handwriting session: "
317                     + "Stylus gesture did not start inside the focused window.");
318             return null;
319         }
320         if (DEBUG) Slog.d(TAG, "Starting handwriting session in display: " + mCurrentDisplayId);
321 
322         InputManagerGlobal.getInstance()
323                 .pilferPointers(mHandwritingSurface.getInputChannel().getToken());
324 
325         // Stop processing more events.
326         mHandwritingEventReceiver.dispose();
327         mHandwritingEventReceiver = null;
328         mRecordingGesture = false;
329         mRecordingGestureAfterStylusUp = false;
330 
331         if (mHandwritingSurface.isIntercepting()) {
332             throw new IllegalStateException(
333                     "Handwriting surface should not be already intercepting.");
334         }
335         mHandwritingSurface.startIntercepting(imePid, imeUid);
336 
337         // Unset the pointer icon for the stylus in case the app had set it.
338         Objects.requireNonNull(mContext.getSystemService(InputManager.class)).setPointerIcon(
339                 PointerIcon.getSystemIcon(mContext, PointerIcon.TYPE_NOT_SPECIFIED),
340                 downEvent.getDisplayId(), downEvent.getDeviceId(), downEvent.getPointerId(0),
341                 mHandwritingSurface.getInputChannel().getToken());
342 
343         return new HandwritingSession(mCurrentRequestId, mHandwritingSurface.getInputChannel(),
344                 mHandwritingBuffer);
345     }
346 
347     /**
348      * Reset the current handwriting session without initializing another session.
349      *
350      * This must be called from UI Thread to avoid race conditions between processing more input
351      * events and disposing the input event receiver.
352      */
353     @UiThread
reset()354     void reset() {
355         reset(false /* reinitializing */);
356     }
357 
setInkWindowInitializer(Runnable inkWindowInitializer)358     void setInkWindowInitializer(Runnable inkWindowInitializer) {
359         mInkWindowInitRunnable = inkWindowInitializer;
360     }
361 
reset(boolean reinitializing)362     private void reset(boolean reinitializing) {
363         if (mHandwritingEventReceiver != null) {
364             mHandwritingEventReceiver.dispose();
365             mHandwritingEventReceiver = null;
366         }
367 
368         if (mHandwritingBuffer != null) {
369             mHandwritingBuffer.forEach(MotionEvent::recycle);
370             mHandwritingBuffer.clear();
371             if (!reinitializing) {
372                 mHandwritingBuffer = null;
373             }
374         }
375 
376         if (mHandwritingSurface != null) {
377             mHandwritingSurface.getInputChannel().dispose();
378             if (!reinitializing) {
379                 mHandwritingSurface.remove();
380                 mHandwritingSurface = null;
381             }
382         }
383 
384         if (!mDelegationConnectionlessFlow) {
385             clearPendingHandwritingDelegation();
386         }
387         mRecordingGesture = false;
388         mRecordingGestureAfterStylusUp = false;
389     }
390 
onInputEvent(InputEvent ev)391     private boolean onInputEvent(InputEvent ev) {
392         if (mHandwritingEventReceiver == null) {
393             throw new IllegalStateException(
394                     "Input Event should not be processed when IME has the spy channel.");
395         }
396 
397         if (!(ev instanceof MotionEvent event)) {
398             Slog.wtf(TAG, "Received non-motion event in stylus monitor.");
399             return false;
400         }
401         if (!event.isStylusPointer()) {
402             return false;
403         }
404         if (event.getDisplayId() != mCurrentDisplayId) {
405             Slog.wtf(TAG, "Received stylus event associated with the incorrect display.");
406             return false;
407         }
408 
409         onStylusEvent(event);
410         return true;
411     }
412 
onStylusEvent(MotionEvent event)413     private void onStylusEvent(MotionEvent event) {
414         final int action = event.getActionMasked();
415 
416         if (mInkWindowInitRunnable != null && (action == MotionEvent.ACTION_HOVER_ENTER
417                 || event.getAction() == MotionEvent.ACTION_HOVER_ENTER)) {
418             // Ask IMMS to make ink window ready.
419             mInkWindowInitRunnable.run();
420             mInkWindowInitRunnable = null;
421             return;
422         } else if (event.isHoverEvent()) {
423             // Hover events need not be recorded to buffer.
424             return;
425         }
426 
427         // If handwriting delegation is ongoing, don't clear the buffer so that multiple strokes
428         // can be buffered across windows.
429         // (This isn't needed for the connectionless delegation flow.)
430         if ((TextUtils.isEmpty(mDelegatePackageName) || mDelegationConnectionlessFlow)
431                 && (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_CANCEL)) {
432             mRecordingGesture = false;
433             if (handwritingEndOfLineTap() && action == MotionEvent.ACTION_UP) {
434                 mRecordingGestureAfterStylusUp = true;
435             } else {
436                 mHandwritingBuffer.clear();
437                 return;
438             }
439         }
440 
441         if (action == MotionEvent.ACTION_DOWN) {
442             clearBufferIfRecordingAfterStylusUp();
443             mRecordingGesture = true;
444         }
445 
446         if (!mRecordingGesture && !mRecordingGestureAfterStylusUp) {
447             return;
448         }
449 
450         if (mHandwritingBuffer.size() >= getHandwritingBufferSize()) {
451             if (DEBUG) {
452                 Slog.w(TAG, "Current gesture exceeds the buffer capacity."
453                         + " The rest of the gesture will not be recorded.");
454             }
455             mRecordingGesture = false;
456             clearBufferIfRecordingAfterStylusUp();
457             return;
458         }
459 
460         mHandwritingBuffer.add(MotionEvent.obtain(event));
461     }
462 
clearBufferIfRecordingAfterStylusUp()463     private void clearBufferIfRecordingAfterStylusUp() {
464         if (mRecordingGestureAfterStylusUp) {
465             mHandwritingBuffer.clear();
466             mRecordingGestureAfterStylusUp = false;
467         }
468     }
469 
470     static final class HandwritingSession {
471         private final int mRequestId;
472         private final InputChannel mHandwritingChannel;
473         private final List<MotionEvent> mRecordedEvents;
474 
HandwritingSession(int requestId, InputChannel handwritingChannel, List<MotionEvent> recordedEvents)475         private HandwritingSession(int requestId, InputChannel handwritingChannel,
476                 List<MotionEvent> recordedEvents) {
477             mRequestId = requestId;
478             mHandwritingChannel = handwritingChannel;
479             mRecordedEvents = recordedEvents;
480         }
481 
getRequestId()482         int getRequestId() {
483             return mRequestId;
484         }
485 
getHandwritingChannel()486         InputChannel getHandwritingChannel() {
487             return mHandwritingChannel;
488         }
489 
getRecordedEvents()490         List<MotionEvent> getRecordedEvents() {
491             return mRecordedEvents;
492         }
493     }
494 }
495