blob: f79ac164ef555112c3a329f9ed6b111595010092 [file] [log] [blame]
Yohei Yukawab557d572018-12-29 21:26:26 -08001/*
2 * Copyright (C) 2018 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
17package com.android.server.inputmethod;
18
19import android.annotation.NonNull;
20import android.annotation.UserIdInt;
21import android.os.Environment;
22import android.os.FileUtils;
23import android.os.UserHandle;
24import android.text.TextUtils;
25import android.util.ArrayMap;
26import android.util.AtomicFile;
27import android.util.Slog;
28import android.util.Xml;
29import android.view.inputmethod.InputMethodInfo;
30import android.view.inputmethod.InputMethodSubtype;
31
32import com.android.internal.util.FastXmlSerializer;
33
34import org.xmlpull.v1.XmlPullParser;
35import org.xmlpull.v1.XmlPullParserException;
36import org.xmlpull.v1.XmlSerializer;
37
38import java.io.File;
39import java.io.FileInputStream;
40import java.io.FileOutputStream;
41import java.io.IOException;
42import java.nio.charset.StandardCharsets;
43import java.util.ArrayList;
44import java.util.List;
45
46/**
47 * Utility class to read/write subtype.xml.
48 */
49final class AdditionalSubtypeUtils {
50 private static final String TAG = "AdditionalSubtypeUtils";
51
52 private static final String SYSTEM_PATH = "system";
53 private static final String INPUT_METHOD_PATH = "inputmethod";
54 private static final String ADDITIONAL_SUBTYPES_FILE_NAME = "subtypes.xml";
55 private static final String NODE_SUBTYPES = "subtypes";
56 private static final String NODE_SUBTYPE = "subtype";
57 private static final String NODE_IMI = "imi";
58 private static final String ATTR_ID = "id";
59 private static final String ATTR_LABEL = "label";
60 private static final String ATTR_ICON = "icon";
61 private static final String ATTR_IME_SUBTYPE_ID = "subtypeId";
62 private static final String ATTR_IME_SUBTYPE_LOCALE = "imeSubtypeLocale";
63 private static final String ATTR_IME_SUBTYPE_LANGUAGE_TAG = "languageTag";
64 private static final String ATTR_IME_SUBTYPE_MODE = "imeSubtypeMode";
65 private static final String ATTR_IME_SUBTYPE_EXTRA_VALUE = "imeSubtypeExtraValue";
66 private static final String ATTR_IS_AUXILIARY = "isAuxiliary";
67 private static final String ATTR_IS_ASCII_CAPABLE = "isAsciiCapable";
68
69 private AdditionalSubtypeUtils() {
70 }
71
72 /**
73 * Returns a {@link File} that represents the directory at which subtype.xml will be placed.
74 *
75 * @param userId User ID with with subtype.xml path should be determined.
76 * @return {@link File} that represents the directory.
77 */
78 @NonNull
79 private static File getInputMethodDir(@UserIdInt int userId) {
80 final File systemDir = userId == UserHandle.USER_SYSTEM
81 ? new File(Environment.getDataDirectory(), SYSTEM_PATH)
82 : Environment.getUserSystemDirectory(userId);
83 return new File(systemDir, INPUT_METHOD_PATH);
84 }
85
86 /**
87 * Returns an {@link AtomicFile} to read/write additional subtype for the given user id.
88 *
89 * @param inputMethodDir Directory at which subtype.xml will be placed
90 * @return {@link AtomicFile} to be used to read/write additional subtype
91 */
92 @NonNull
93 private static AtomicFile getAdditionalSubtypeFile(File inputMethodDir) {
94 final File subtypeFile = new File(inputMethodDir, ADDITIONAL_SUBTYPES_FILE_NAME);
95 return new AtomicFile(subtypeFile, "input-subtypes");
96 }
97
98 /**
99 * Write additional subtypes into "subtype.xml".
100 *
101 * <p>This method does not confer any data/file locking semantics. Caller must make sure that
102 * multiple threads are not calling this method at the same time for the same {@code userId}.
103 * </p>
104 *
105 * @param allSubtypes {@link ArrayMap} from IME ID to additional subtype list. Passing an empty
106 * map deletes the file.
107 * @param methodMap {@link ArrayMap} from IME ID to {@link InputMethodInfo}.
108 * @param userId The user ID to be associated with.
109 */
110 static void save(ArrayMap<String, List<InputMethodSubtype>> allSubtypes,
111 ArrayMap<String, InputMethodInfo> methodMap, @UserIdInt int userId) {
112 final File inputMethodDir = getInputMethodDir(userId);
113
114 if (allSubtypes.isEmpty()) {
115 if (!inputMethodDir.exists()) {
116 // Even the parent directory doesn't exist. There is nothing to clean up.
117 return;
118 }
119 final AtomicFile subtypesFile = getAdditionalSubtypeFile(inputMethodDir);
120 if (subtypesFile.exists()) {
121 subtypesFile.delete();
122 }
123 if (FileUtils.listFilesOrEmpty(inputMethodDir).length == 0) {
124 if (!inputMethodDir.delete()) {
125 Slog.e(TAG, "Failed to delete the empty parent directory " + inputMethodDir);
126 }
127 }
128 return;
129 }
130
131 if (!inputMethodDir.exists() && !inputMethodDir.mkdirs()) {
132 Slog.e(TAG, "Failed to create a parent directory " + inputMethodDir);
133 return;
134 }
135
136 // Safety net for the case that this function is called before methodMap is set.
137 final boolean isSetMethodMap = methodMap != null && methodMap.size() > 0;
138 FileOutputStream fos = null;
139 final AtomicFile subtypesFile = getAdditionalSubtypeFile(inputMethodDir);
140 try {
141 fos = subtypesFile.startWrite();
142 final XmlSerializer out = new FastXmlSerializer();
143 out.setOutput(fos, StandardCharsets.UTF_8.name());
144 out.startDocument(null, true);
145 out.setFeature("http://xmlpull.org/v1/doc/features.html#indent-output", true);
146 out.startTag(null, NODE_SUBTYPES);
147 for (String imiId : allSubtypes.keySet()) {
148 if (isSetMethodMap && !methodMap.containsKey(imiId)) {
149 Slog.w(TAG, "IME uninstalled or not valid.: " + imiId);
150 continue;
151 }
152 out.startTag(null, NODE_IMI);
153 out.attribute(null, ATTR_ID, imiId);
154 final List<InputMethodSubtype> subtypesList = allSubtypes.get(imiId);
155 final int numSubtypes = subtypesList.size();
156 for (int i = 0; i < numSubtypes; ++i) {
157 final InputMethodSubtype subtype = subtypesList.get(i);
158 out.startTag(null, NODE_SUBTYPE);
159 if (subtype.hasSubtypeId()) {
160 out.attribute(null, ATTR_IME_SUBTYPE_ID,
161 String.valueOf(subtype.getSubtypeId()));
162 }
163 out.attribute(null, ATTR_ICON, String.valueOf(subtype.getIconResId()));
164 out.attribute(null, ATTR_LABEL, String.valueOf(subtype.getNameResId()));
165 out.attribute(null, ATTR_IME_SUBTYPE_LOCALE, subtype.getLocale());
166 out.attribute(null, ATTR_IME_SUBTYPE_LANGUAGE_TAG,
167 subtype.getLanguageTag());
168 out.attribute(null, ATTR_IME_SUBTYPE_MODE, subtype.getMode());
169 out.attribute(null, ATTR_IME_SUBTYPE_EXTRA_VALUE, subtype.getExtraValue());
170 out.attribute(null, ATTR_IS_AUXILIARY,
171 String.valueOf(subtype.isAuxiliary() ? 1 : 0));
172 out.attribute(null, ATTR_IS_ASCII_CAPABLE,
173 String.valueOf(subtype.isAsciiCapable() ? 1 : 0));
174 out.endTag(null, NODE_SUBTYPE);
175 }
176 out.endTag(null, NODE_IMI);
177 }
178 out.endTag(null, NODE_SUBTYPES);
179 out.endDocument();
180 subtypesFile.finishWrite(fos);
181 } catch (java.io.IOException e) {
182 Slog.w(TAG, "Error writing subtypes", e);
183 if (fos != null) {
184 subtypesFile.failWrite(fos);
185 }
186 }
187 }
188
189 /**
190 * Read additional subtypes from "subtype.xml".
191 *
192 * <p>This method does not confer any data/file locking semantics. Caller must make sure that
193 * multiple threads are not calling this method at the same time for the same {@code userId}.
194 * </p>
195 *
196 * @param allSubtypes {@link ArrayMap} from IME ID to additional subtype list. This parameter
197 * will be used to return the result.
198 * @param userId The user ID to be associated with.
199 */
200 static void load(@NonNull ArrayMap<String, List<InputMethodSubtype>> allSubtypes,
201 @UserIdInt int userId) {
202 allSubtypes.clear();
203
204 final AtomicFile subtypesFile = getAdditionalSubtypeFile(getInputMethodDir(userId));
205 if (!subtypesFile.exists()) {
206 // Not having the file means there is no additional subtype.
207 return;
208 }
209 try (FileInputStream fis = subtypesFile.openRead()) {
210 final XmlPullParser parser = Xml.newPullParser();
211 parser.setInput(fis, StandardCharsets.UTF_8.name());
212 int type = parser.getEventType();
213 // Skip parsing until START_TAG
214 while (true) {
215 type = parser.next();
216 if (type == XmlPullParser.START_TAG || type == XmlPullParser.END_DOCUMENT) {
217 break;
218 }
219 }
220 String firstNodeName = parser.getName();
221 if (!NODE_SUBTYPES.equals(firstNodeName)) {
222 throw new XmlPullParserException("Xml doesn't start with subtypes");
223 }
224 final int depth = parser.getDepth();
225 String currentImiId = null;
226 ArrayList<InputMethodSubtype> tempSubtypesArray = null;
227 while (((type = parser.next()) != XmlPullParser.END_TAG
228 || parser.getDepth() > depth) && type != XmlPullParser.END_DOCUMENT) {
229 if (type != XmlPullParser.START_TAG) {
230 continue;
231 }
232 final String nodeName = parser.getName();
233 if (NODE_IMI.equals(nodeName)) {
234 currentImiId = parser.getAttributeValue(null, ATTR_ID);
235 if (TextUtils.isEmpty(currentImiId)) {
236 Slog.w(TAG, "Invalid imi id found in subtypes.xml");
237 continue;
238 }
239 tempSubtypesArray = new ArrayList<>();
240 allSubtypes.put(currentImiId, tempSubtypesArray);
241 } else if (NODE_SUBTYPE.equals(nodeName)) {
242 if (TextUtils.isEmpty(currentImiId) || tempSubtypesArray == null) {
243 Slog.w(TAG, "IME uninstalled or not valid.: " + currentImiId);
244 continue;
245 }
246 final int icon = Integer.parseInt(
247 parser.getAttributeValue(null, ATTR_ICON));
248 final int label = Integer.parseInt(
249 parser.getAttributeValue(null, ATTR_LABEL));
250 final String imeSubtypeLocale =
251 parser.getAttributeValue(null, ATTR_IME_SUBTYPE_LOCALE);
252 final String languageTag =
253 parser.getAttributeValue(null, ATTR_IME_SUBTYPE_LANGUAGE_TAG);
254 final String imeSubtypeMode =
255 parser.getAttributeValue(null, ATTR_IME_SUBTYPE_MODE);
256 final String imeSubtypeExtraValue =
257 parser.getAttributeValue(null, ATTR_IME_SUBTYPE_EXTRA_VALUE);
258 final boolean isAuxiliary = "1".equals(String.valueOf(
259 parser.getAttributeValue(null, ATTR_IS_AUXILIARY)));
260 final boolean isAsciiCapable = "1".equals(String.valueOf(
261 parser.getAttributeValue(null, ATTR_IS_ASCII_CAPABLE)));
262 final InputMethodSubtype.InputMethodSubtypeBuilder
263 builder = new InputMethodSubtype.InputMethodSubtypeBuilder()
264 .setSubtypeNameResId(label)
265 .setSubtypeIconResId(icon)
266 .setSubtypeLocale(imeSubtypeLocale)
267 .setLanguageTag(languageTag)
268 .setSubtypeMode(imeSubtypeMode)
269 .setSubtypeExtraValue(imeSubtypeExtraValue)
270 .setIsAuxiliary(isAuxiliary)
271 .setIsAsciiCapable(isAsciiCapable);
272 final String subtypeIdString =
273 parser.getAttributeValue(null, ATTR_IME_SUBTYPE_ID);
274 if (subtypeIdString != null) {
275 builder.setSubtypeId(Integer.parseInt(subtypeIdString));
276 }
277 tempSubtypesArray.add(builder.build());
278 }
279 }
280 } catch (XmlPullParserException | IOException | NumberFormatException e) {
281 Slog.w(TAG, "Error reading subtypes", e);
282 }
283 }
284}