1 /* 2 * Copyright (C) 2024 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.annotation.AnyThread; 20 import android.annotation.NonNull; 21 import android.annotation.Nullable; 22 import android.annotation.UserIdInt; 23 import android.annotation.WorkerThread; 24 import android.content.Context; 25 import android.content.pm.UserInfo; 26 import android.os.Handler; 27 import android.os.Process; 28 import android.util.IntArray; 29 import android.util.SparseArray; 30 31 import com.android.internal.annotations.GuardedBy; 32 import com.android.internal.inputmethod.DirectBootAwareness; 33 import com.android.server.LocalServices; 34 import com.android.server.pm.UserManagerInternal; 35 36 import java.util.ArrayList; 37 import java.util.concurrent.locks.Condition; 38 import java.util.concurrent.locks.ReentrantLock; 39 40 /** 41 * Provides accesses to per-user additional {@link android.view.inputmethod.InputMethodSubtype} 42 * persistent storages. 43 */ 44 final class AdditionalSubtypeMapRepository { 45 @GuardedBy("ImfLock.class") 46 @NonNull 47 private static final SparseArray<AdditionalSubtypeMap> sPerUserMap = new SparseArray<>(); 48 WriteTask(@serIdInt int userId, @NonNull AdditionalSubtypeMap subtypeMap, @NonNull InputMethodMap inputMethodMap)49 record WriteTask(@UserIdInt int userId, @NonNull AdditionalSubtypeMap subtypeMap, 50 @NonNull InputMethodMap inputMethodMap) { 51 } 52 53 static final class SingleThreadedBackgroundWriter { 54 /** 55 * A {@link ReentrantLock} used to guard {@link #mPendingTasks} and {@link #mRemovedUsers}. 56 */ 57 @NonNull 58 private final ReentrantLock mLock = new ReentrantLock(); 59 /** 60 * A {@link Condition} associated with {@link #mLock} for producer to unblock consumer. 61 */ 62 @NonNull 63 private final Condition mLockNotifier = mLock.newCondition(); 64 65 @GuardedBy("mLock") 66 @NonNull 67 private final SparseArray<WriteTask> mPendingTasks = new SparseArray<>(); 68 69 @GuardedBy("mLock") 70 private final IntArray mRemovedUsers = new IntArray(); 71 72 @NonNull 73 private final Thread mWriterThread = new Thread("android.ime.as") { 74 75 /** 76 * Waits until the next data has come then return the result after filtering out any 77 * already removed users. 78 * 79 * @return A list of {@link WriteTask} to be written into persistent storage 80 */ 81 @WorkerThread 82 private ArrayList<WriteTask> fetchNextTasks() { 83 final SparseArray<WriteTask> tasks; 84 final IntArray removedUsers; 85 mLock.lock(); 86 try { 87 while (true) { 88 if (mPendingTasks.size() != 0) { 89 tasks = mPendingTasks.clone(); 90 mPendingTasks.clear(); 91 if (mRemovedUsers.size() == 0) { 92 removedUsers = null; 93 } else { 94 removedUsers = mRemovedUsers.clone(); 95 } 96 break; 97 } 98 mLockNotifier.awaitUninterruptibly(); 99 } 100 } finally { 101 mLock.unlock(); 102 } 103 final int size = tasks.size(); 104 final ArrayList<WriteTask> result = new ArrayList<>(size); 105 for (int i = 0; i < size; ++i) { 106 final int userId = tasks.keyAt(i); 107 if (removedUsers != null && removedUsers.contains(userId)) { 108 continue; 109 } 110 result.add(tasks.valueAt(i)); 111 } 112 return result; 113 } 114 115 @WorkerThread 116 @Override 117 public void run() { 118 Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND); 119 120 while (true) { 121 final ArrayList<WriteTask> tasks = fetchNextTasks(); 122 tasks.forEach(task -> AdditionalSubtypeUtils.save( 123 task.subtypeMap, task.inputMethodMap, task.userId)); 124 } 125 } 126 }; 127 128 /** 129 * Schedules a write operation 130 * 131 * @param userId the target user ID of this operation 132 * @param subtypeMap {@link AdditionalSubtypeMap} to be saved 133 * @param inputMethodMap {@link InputMethodMap} to be used to filter our {@code subtypeMap} 134 */ 135 @AnyThread scheduleWriteTask(@serIdInt int userId, @NonNull AdditionalSubtypeMap subtypeMap, @NonNull InputMethodMap inputMethodMap)136 void scheduleWriteTask(@UserIdInt int userId, @NonNull AdditionalSubtypeMap subtypeMap, 137 @NonNull InputMethodMap inputMethodMap) { 138 final var task = new WriteTask(userId, subtypeMap, inputMethodMap); 139 mLock.lock(); 140 try { 141 if (mRemovedUsers.contains(userId)) { 142 return; 143 } 144 mPendingTasks.put(userId, task); 145 mLockNotifier.signalAll(); 146 } finally { 147 mLock.unlock(); 148 } 149 } 150 151 /** 152 * Called back when a user is being created. 153 * 154 * @param userId The user ID to be created 155 */ 156 @AnyThread onUserCreated(@serIdInt int userId)157 void onUserCreated(@UserIdInt int userId) { 158 mLock.lock(); 159 try { 160 for (int i = mRemovedUsers.size() - 1; i >= 0; --i) { 161 if (mRemovedUsers.get(i) == userId) { 162 mRemovedUsers.remove(i); 163 } 164 } 165 } finally { 166 mLock.unlock(); 167 } 168 } 169 170 /** 171 * Called back when a user is being removed. Any pending task will be effectively canceled 172 * if the user is removed before the task is fulfilled. 173 * 174 * @param userId The user ID to be removed 175 */ 176 @AnyThread onUserRemoved(@serIdInt int userId)177 void onUserRemoved(@UserIdInt int userId) { 178 mLock.lock(); 179 try { 180 mRemovedUsers.add(userId); 181 mPendingTasks.remove(userId); 182 } finally { 183 mLock.unlock(); 184 } 185 } 186 startThread()187 void startThread() { 188 mWriterThread.start(); 189 } 190 } 191 192 private static final SingleThreadedBackgroundWriter sWriter = 193 new SingleThreadedBackgroundWriter(); 194 195 /** 196 * Not intended to be instantiated. 197 */ AdditionalSubtypeMapRepository()198 private AdditionalSubtypeMapRepository() { 199 } 200 201 @NonNull 202 @GuardedBy("ImfLock.class") get(@serIdInt int userId)203 static AdditionalSubtypeMap get(@UserIdInt int userId) { 204 final AdditionalSubtypeMap map = sPerUserMap.get(userId); 205 if (map != null) { 206 return map; 207 } 208 final AdditionalSubtypeMap newMap = AdditionalSubtypeUtils.load(userId); 209 sPerUserMap.put(userId, newMap); 210 return newMap; 211 } 212 213 @GuardedBy("ImfLock.class") putAndSave(@serIdInt int userId, @NonNull AdditionalSubtypeMap map, @NonNull InputMethodMap inputMethodMap)214 static void putAndSave(@UserIdInt int userId, @NonNull AdditionalSubtypeMap map, 215 @NonNull InputMethodMap inputMethodMap) { 216 final AdditionalSubtypeMap previous = sPerUserMap.get(userId); 217 if (previous == map) { 218 return; 219 } 220 sPerUserMap.put(userId, map); 221 sWriter.scheduleWriteTask(userId, map, inputMethodMap); 222 } 223 startWriterThread()224 static void startWriterThread() { 225 sWriter.startThread(); 226 } 227 initialize(@onNull Handler handler, @NonNull Context context)228 static void initialize(@NonNull Handler handler, @NonNull Context context) { 229 final UserManagerInternal userManagerInternal = 230 LocalServices.getService(UserManagerInternal.class); 231 handler.post(() -> { 232 userManagerInternal.addUserLifecycleListener( 233 new UserManagerInternal.UserLifecycleListener() { 234 @Override 235 public void onUserCreated(UserInfo user, @Nullable Object token) { 236 final int userId = user.id; 237 sWriter.onUserCreated(userId); 238 handler.post(() -> { 239 synchronized (ImfLock.class) { 240 if (!sPerUserMap.contains(userId)) { 241 final AdditionalSubtypeMap additionalSubtypeMap = 242 AdditionalSubtypeUtils.load(userId); 243 sPerUserMap.put(userId, additionalSubtypeMap); 244 final InputMethodSettings settings = 245 InputMethodManagerService 246 .queryInputMethodServicesInternal(context, 247 userId, 248 additionalSubtypeMap, 249 DirectBootAwareness.AUTO); 250 InputMethodSettingsRepository.put(userId, settings); 251 } 252 } 253 }); 254 } 255 256 @Override 257 public void onUserRemoved(UserInfo user) { 258 final int userId = user.id; 259 sWriter.onUserRemoved(userId); 260 handler.post(() -> { 261 synchronized (ImfLock.class) { 262 sPerUserMap.remove(userId); 263 } 264 }); 265 } 266 }); 267 synchronized (ImfLock.class) { 268 for (int userId : userManagerInternal.getUserIds()) { 269 sPerUserMap.put(userId, AdditionalSubtypeUtils.load(userId)); 270 } 271 } 272 }); 273 } 274 } 275