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 android.Manifest;
20 import android.annotation.EnforcePermission;
21 import android.annotation.NonNull;
22 import android.annotation.Nullable;
23 import android.os.Binder;
24 import android.os.Handler;
25 import android.os.IBinder;
26 import android.os.Looper;
27 import android.util.Log;
28 import android.view.inputmethod.ImeTracker;
29 
30 import com.android.internal.annotations.GuardedBy;
31 import com.android.internal.inputmethod.IImeTracker;
32 import com.android.internal.inputmethod.InputMethodDebug;
33 import com.android.internal.inputmethod.SoftInputShowHideReason;
34 import com.android.internal.util.FrameworkStatsLog;
35 
36 import java.io.PrintWriter;
37 import java.time.Instant;
38 import java.time.ZoneId;
39 import java.time.format.DateTimeFormatter;
40 import java.util.ArrayDeque;
41 import java.util.Locale;
42 import java.util.WeakHashMap;
43 import java.util.concurrent.atomic.AtomicInteger;
44 
45 /**
46  * Service for managing and logging {@link ImeTracker.Token} instances.
47  *
48  * @implNote Suppresses {@link GuardedBy} warnings, as linter reports that {@link #mHistory}
49  * interactions are guarded by {@code this} instead of {@code ImeTrackerService.this}, which should
50  * be identical.
51  *
52  * @hide
53  */
54 @SuppressWarnings("GuardedBy")
55 public final class ImeTrackerService extends IImeTracker.Stub {
56 
57     private static final String TAG = ImeTracker.TAG;
58 
59     /** The threshold in milliseconds after which a history entry is considered timed out. */
60     private static final long TIMEOUT_MS = 10_000;
61 
62     /** Handler for registering timeouts for live entries. */
63     private final Handler mHandler;
64 
65     /** Singleton instance of the History. */
66     @GuardedBy("ImeTrackerService.this")
67     private final History mHistory = new History();
68 
ImeTrackerService(@onNull Looper looper)69     ImeTrackerService(@NonNull Looper looper) {
70         mHandler = new Handler(looper, null /* callback */, true /* async */);
71     }
72 
73     @NonNull
74     @Override
onRequestShow(@onNull String tag, int uid, @ImeTracker.Origin int origin, @SoftInputShowHideReason int reason)75     public synchronized ImeTracker.Token onRequestShow(@NonNull String tag, int uid,
76             @ImeTracker.Origin int origin, @SoftInputShowHideReason int reason) {
77         final var binder = new Binder();
78         final var token = new ImeTracker.Token(binder, tag);
79         final var entry = new History.Entry(tag, uid, ImeTracker.TYPE_SHOW, ImeTracker.STATUS_RUN,
80                 origin, reason);
81         mHistory.addEntry(binder, entry);
82 
83         // Register a delayed task to handle the case where the new entry times out.
84         mHandler.postDelayed(() -> {
85             synchronized (ImeTrackerService.this) {
86                 mHistory.setFinished(token, ImeTracker.STATUS_TIMEOUT, ImeTracker.PHASE_NOT_SET);
87             }
88         }, TIMEOUT_MS);
89 
90         return token;
91     }
92 
93     @NonNull
94     @Override
onRequestHide(@onNull String tag, int uid, @ImeTracker.Origin int origin, @SoftInputShowHideReason int reason)95     public synchronized ImeTracker.Token onRequestHide(@NonNull String tag, int uid,
96             @ImeTracker.Origin int origin, @SoftInputShowHideReason int reason) {
97         final var binder = new Binder();
98         final var token = new ImeTracker.Token(binder, tag);
99         final var entry = new History.Entry(tag, uid, ImeTracker.TYPE_HIDE, ImeTracker.STATUS_RUN,
100                 origin, reason);
101         mHistory.addEntry(binder, entry);
102 
103         // Register a delayed task to handle the case where the new entry times out.
104         mHandler.postDelayed(() -> {
105             synchronized (ImeTrackerService.this) {
106                 mHistory.setFinished(token, ImeTracker.STATUS_TIMEOUT, ImeTracker.PHASE_NOT_SET);
107             }
108         }, TIMEOUT_MS);
109 
110         return token;
111     }
112 
113     @Override
onProgress(@onNull IBinder binder, @ImeTracker.Phase int phase)114     public synchronized void onProgress(@NonNull IBinder binder, @ImeTracker.Phase int phase) {
115         final var entry = mHistory.getEntry(binder);
116         if (entry == null) return;
117 
118         entry.mPhase = phase;
119     }
120 
121     @Override
onFailed(@onNull ImeTracker.Token statsToken, @ImeTracker.Phase int phase)122     public synchronized void onFailed(@NonNull ImeTracker.Token statsToken,
123             @ImeTracker.Phase int phase) {
124         mHistory.setFinished(statsToken, ImeTracker.STATUS_FAIL, phase);
125     }
126 
127     @Override
onCancelled(@onNull ImeTracker.Token statsToken, @ImeTracker.Phase int phase)128     public synchronized void onCancelled(@NonNull ImeTracker.Token statsToken,
129             @ImeTracker.Phase int phase) {
130         mHistory.setFinished(statsToken, ImeTracker.STATUS_CANCEL, phase);
131     }
132 
133     @Override
onShown(@onNull ImeTracker.Token statsToken)134     public synchronized void onShown(@NonNull ImeTracker.Token statsToken) {
135         mHistory.setFinished(statsToken, ImeTracker.STATUS_SUCCESS, ImeTracker.PHASE_NOT_SET);
136     }
137 
138     @Override
onHidden(@onNull ImeTracker.Token statsToken)139     public synchronized void onHidden(@NonNull ImeTracker.Token statsToken) {
140         mHistory.setFinished(statsToken, ImeTracker.STATUS_SUCCESS, ImeTracker.PHASE_NOT_SET);
141     }
142 
143     /**
144      * Updates the IME request tracking token with new information available in IMMS.
145      *
146      * @param statsToken the token corresponding to the current IME request.
147      * @param requestWindowName the name of the window that created the IME request.
148      */
onImmsUpdate(@onNull ImeTracker.Token statsToken, @NonNull String requestWindowName)149     public synchronized void onImmsUpdate(@NonNull ImeTracker.Token statsToken,
150             @NonNull String requestWindowName) {
151         final var entry = mHistory.getEntry(statsToken.getBinder());
152         if (entry == null) return;
153 
154         entry.mRequestWindowName = requestWindowName;
155     }
156 
157     /** Dumps the contents of the history. */
dump(@onNull PrintWriter pw, @NonNull String prefix)158     public synchronized void dump(@NonNull PrintWriter pw, @NonNull String prefix) {
159         mHistory.dump(pw, prefix);
160     }
161 
162     @EnforcePermission(Manifest.permission.TEST_INPUT_METHOD)
163     @Override
hasPendingImeVisibilityRequests()164     public synchronized boolean hasPendingImeVisibilityRequests() {
165         super.hasPendingImeVisibilityRequests_enforcePermission();
166 
167         return !mHistory.mLiveEntries.isEmpty();
168     }
169 
170     /**
171      * A circular buffer storing the most recent few {@link ImeTracker.Token} entries information.
172      */
173     private static final class History {
174 
175         /** The circular buffer's capacity. */
176         private static final int CAPACITY = 100;
177 
178         /** Backing store for the circular buffer. */
179         @GuardedBy("ImeTrackerService.this")
180         private final ArrayDeque<Entry> mEntries = new ArrayDeque<>(CAPACITY);
181 
182         /** Backing store for the live entries (i.e. entries that are not finished yet). */
183         @GuardedBy("ImeTrackerService.this")
184         private final WeakHashMap<IBinder, Entry> mLiveEntries = new WeakHashMap<>();
185 
186         /** Latest entry sequence number. */
187         private static final AtomicInteger sSequenceNumber = new AtomicInteger(0);
188 
189         /** Adds a live entry corresponding to the given IME tracking token's binder. */
190         @GuardedBy("ImeTrackerService.this")
addEntry(@onNull IBinder binder, @NonNull Entry entry)191         private void addEntry(@NonNull IBinder binder, @NonNull Entry entry) {
192             mLiveEntries.put(binder, entry);
193         }
194 
195         /** Gets the entry corresponding to the given IME tracking token's binder, if it exists. */
196         @Nullable
197         @GuardedBy("ImeTrackerService.this")
getEntry(@onNull IBinder binder)198         private Entry getEntry(@NonNull IBinder binder) {
199             return mLiveEntries.get(binder);
200         }
201 
202         /**
203          * Sets the live entry corresponding to the tracking token, if it exists, as finished,
204          * and uploads the data for metrics.
205          *
206          * @param statsToken the token corresponding to the current IME request.
207          * @param status the finish status of the IME request.
208          * @param phase the phase the IME request finished at, if it exists
209          *              (or {@link ImeTracker#PHASE_NOT_SET} otherwise).
210          */
211         @GuardedBy("ImeTrackerService.this")
setFinished(@onNull ImeTracker.Token statsToken, @ImeTracker.Status int status, @ImeTracker.Phase int phase)212         private void setFinished(@NonNull ImeTracker.Token statsToken,
213                 @ImeTracker.Status int status, @ImeTracker.Phase int phase) {
214             final var entry = mLiveEntries.remove(statsToken.getBinder());
215             if (entry == null) {
216                 // This will be unconditionally called through the postDelayed above to handle
217                 // potential timeouts, and is thus intentionally dropped to avoid having to manually
218                 // save and remove the registered callback. Only timeout calls are expected.
219                 if (status != ImeTracker.STATUS_TIMEOUT) {
220                     Log.i(TAG, statsToken.getTag()
221                             + ": setFinished on previously finished token at "
222                             + ImeTracker.Debug.phaseToString(phase) + " with "
223                             + ImeTracker.Debug.statusToString(status));
224                 }
225                 return;
226             }
227 
228             entry.mDuration = System.currentTimeMillis() - entry.mStartTime;
229             entry.mStatus = status;
230 
231             if (phase != ImeTracker.PHASE_NOT_SET) {
232                 entry.mPhase = phase;
233             }
234 
235             if (status == ImeTracker.STATUS_TIMEOUT) {
236                 // All events other than timeouts are already logged in the client-side ImeTracker.
237                 Log.i(TAG, statsToken.getTag() + ": setFinished at "
238                         + ImeTracker.Debug.phaseToString(entry.mPhase) + " with "
239                         + ImeTracker.Debug.statusToString(status));
240             }
241 
242             // Remove excess entries overflowing capacity (plus one for the new entry).
243             while (mEntries.size() >= CAPACITY) {
244                 mEntries.remove();
245             }
246 
247             mEntries.offer(entry);
248 
249             // Log newly finished entry.
250             FrameworkStatsLog.write(FrameworkStatsLog.IME_REQUEST_FINISHED, entry.mUid,
251                     entry.mDuration, entry.mType, entry.mStatus, entry.mReason,
252                     entry.mOrigin, entry.mPhase);
253         }
254 
255         /** Dumps the contents of the circular buffer. */
256         @GuardedBy("ImeTrackerService.this")
dump(@onNull PrintWriter pw, @NonNull String prefix)257         private void dump(@NonNull PrintWriter pw, @NonNull String prefix) {
258             final var formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss.SSS", Locale.US)
259                     .withZone(ZoneId.systemDefault());
260 
261             pw.print(prefix);
262             pw.println("ImeTrackerService#History.mLiveEntries: "
263                     + mLiveEntries.size() + " elements");
264 
265             for (final var entry: mLiveEntries.values()) {
266                 dumpEntry(entry, pw, prefix, formatter);
267             }
268 
269             pw.print(prefix);
270             pw.println("ImeTrackerService#History.mEntries: "
271                     + mEntries.size() + " elements");
272 
273             for (final var entry: mEntries) {
274                 dumpEntry(entry, pw, prefix, formatter);
275             }
276         }
277 
278         @GuardedBy("ImeTrackerService.this")
dumpEntry(@onNull Entry entry, @NonNull PrintWriter pw, @NonNull String prefix, @NonNull DateTimeFormatter formatter)279         private void dumpEntry(@NonNull Entry entry, @NonNull PrintWriter pw,
280                 @NonNull String prefix, @NonNull DateTimeFormatter formatter) {
281             pw.print(prefix);
282             pw.print(" #" + entry.mSequenceNumber);
283             pw.print(" " + ImeTracker.Debug.typeToString(entry.mType));
284             pw.print(" - " + ImeTracker.Debug.statusToString(entry.mStatus));
285             pw.print(" - " + entry.mTag);
286             pw.println(" (" + entry.mDuration + "ms):");
287 
288             pw.print(prefix);
289             pw.print("   startTime=" + formatter.format(Instant.ofEpochMilli(entry.mStartTime)));
290             pw.println(" " + ImeTracker.Debug.originToString(entry.mOrigin));
291 
292             pw.print(prefix);
293             pw.print("   reason=" + InputMethodDebug.softInputDisplayReasonToString(entry.mReason));
294             pw.println(" " + ImeTracker.Debug.phaseToString(entry.mPhase));
295 
296             pw.print(prefix);
297             pw.println("   requestWindowName=" + entry.mRequestWindowName);
298         }
299 
300         /** A history entry. */
301         private static final class Entry {
302 
303             /** The entry's sequence number in the history. */
304             private final int mSequenceNumber = sSequenceNumber.getAndIncrement();
305 
306             /** Logging tag, of the shape "component:random_hexadecimal". */
307             @NonNull
308             private final String mTag;
309 
310             /** Uid of the client that requested the IME. */
311             private final int mUid;
312 
313             /** Clock time in milliseconds when the IME request was created. */
314             private final long mStartTime = System.currentTimeMillis();
315 
316             /** Duration in milliseconds of the IME request from start to end. */
317             private long mDuration = 0;
318 
319             /** Type of the IME request. */
320             @ImeTracker.Type
321             private final int mType;
322 
323             /** Status of the IME request. */
324             @ImeTracker.Status
325             private int mStatus;
326 
327             /** Origin of the IME request. */
328             @ImeTracker.Origin
329             private final int mOrigin;
330 
331             /** Reason for creating the IME request. */
332             @SoftInputShowHideReason
333             private final int mReason;
334 
335             /** Latest phase of the IME request. */
336             @ImeTracker.Phase
337             private int mPhase = ImeTracker.PHASE_NOT_SET;
338 
339             /**
340              * Name of the window that created the IME request.
341              *
342              * Note: This is later set through {@link #onImmsUpdate}.
343              */
344             @NonNull
345             private String mRequestWindowName = "not set";
346 
Entry(@onNull String tag, int uid, @ImeTracker.Type int type, @ImeTracker.Status int status, @ImeTracker.Origin int origin, @SoftInputShowHideReason int reason)347             private Entry(@NonNull String tag, int uid, @ImeTracker.Type int type,
348                     @ImeTracker.Status int status, @ImeTracker.Origin int origin,
349                     @SoftInputShowHideReason int reason) {
350                 mTag = tag;
351                 mUid = uid;
352                 mType = type;
353                 mStatus = status;
354                 mOrigin = origin;
355                 mReason = reason;
356             }
357         }
358     }
359 }
360