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