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