/*
|
* Copyright (C) 2018 The Android Open Source Project
|
*
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
* you may not use this file except in compliance with the License.
|
* You may obtain a copy of the License at
|
*
|
* http://www.apache.org/licenses/LICENSE-2.0
|
*
|
* Unless required by applicable law or agreed to in writing, software
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
* See the License for the specific language governing permissions and
|
* limitations under the License.
|
*/
|
|
package com.android.server.inputmethod;
|
|
import android.annotation.NonNull;
|
import android.annotation.UserIdInt;
|
import android.os.Environment;
|
import android.os.FileUtils;
|
import android.os.UserHandle;
|
import android.text.TextUtils;
|
import android.util.ArrayMap;
|
import android.util.AtomicFile;
|
import android.util.Slog;
|
import android.util.Xml;
|
import android.view.inputmethod.InputMethodInfo;
|
import android.view.inputmethod.InputMethodSubtype;
|
|
import com.android.internal.util.FastXmlSerializer;
|
|
import org.xmlpull.v1.XmlPullParser;
|
import org.xmlpull.v1.XmlPullParserException;
|
import org.xmlpull.v1.XmlSerializer;
|
|
import java.io.File;
|
import java.io.FileInputStream;
|
import java.io.FileOutputStream;
|
import java.io.IOException;
|
import java.nio.charset.StandardCharsets;
|
import java.util.ArrayList;
|
import java.util.List;
|
|
/**
|
* Utility class to read/write subtype.xml.
|
*/
|
final class AdditionalSubtypeUtils {
|
private static final String TAG = "AdditionalSubtypeUtils";
|
|
private static final String SYSTEM_PATH = "system";
|
private static final String INPUT_METHOD_PATH = "inputmethod";
|
private static final String ADDITIONAL_SUBTYPES_FILE_NAME = "subtypes.xml";
|
private static final String NODE_SUBTYPES = "subtypes";
|
private static final String NODE_SUBTYPE = "subtype";
|
private static final String NODE_IMI = "imi";
|
private static final String ATTR_ID = "id";
|
private static final String ATTR_LABEL = "label";
|
private static final String ATTR_ICON = "icon";
|
private static final String ATTR_IME_SUBTYPE_ID = "subtypeId";
|
private static final String ATTR_IME_SUBTYPE_LOCALE = "imeSubtypeLocale";
|
private static final String ATTR_IME_SUBTYPE_LANGUAGE_TAG = "languageTag";
|
private static final String ATTR_IME_SUBTYPE_MODE = "imeSubtypeMode";
|
private static final String ATTR_IME_SUBTYPE_EXTRA_VALUE = "imeSubtypeExtraValue";
|
private static final String ATTR_IS_AUXILIARY = "isAuxiliary";
|
private static final String ATTR_IS_ASCII_CAPABLE = "isAsciiCapable";
|
|
private AdditionalSubtypeUtils() {
|
}
|
|
/**
|
* Returns a {@link File} that represents the directory at which subtype.xml will be placed.
|
*
|
* @param userId User ID with with subtype.xml path should be determined.
|
* @return {@link File} that represents the directory.
|
*/
|
@NonNull
|
private static File getInputMethodDir(@UserIdInt int userId) {
|
final File systemDir = userId == UserHandle.USER_SYSTEM
|
? new File(Environment.getDataDirectory(), SYSTEM_PATH)
|
: Environment.getUserSystemDirectory(userId);
|
return new File(systemDir, INPUT_METHOD_PATH);
|
}
|
|
/**
|
* Returns an {@link AtomicFile} to read/write additional subtype for the given user id.
|
*
|
* @param inputMethodDir Directory at which subtype.xml will be placed
|
* @return {@link AtomicFile} to be used to read/write additional subtype
|
*/
|
@NonNull
|
private static AtomicFile getAdditionalSubtypeFile(File inputMethodDir) {
|
final File subtypeFile = new File(inputMethodDir, ADDITIONAL_SUBTYPES_FILE_NAME);
|
return new AtomicFile(subtypeFile, "input-subtypes");
|
}
|
|
/**
|
* Write additional subtypes into "subtype.xml".
|
*
|
* <p>This method does not confer any data/file locking semantics. Caller must make sure that
|
* multiple threads are not calling this method at the same time for the same {@code userId}.
|
* </p>
|
*
|
* @param allSubtypes {@link ArrayMap} from IME ID to additional subtype list. Passing an empty
|
* map deletes the file.
|
* @param methodMap {@link ArrayMap} from IME ID to {@link InputMethodInfo}.
|
* @param userId The user ID to be associated with.
|
*/
|
static void save(ArrayMap<String, List<InputMethodSubtype>> allSubtypes,
|
ArrayMap<String, InputMethodInfo> methodMap, @UserIdInt int userId) {
|
final File inputMethodDir = getInputMethodDir(userId);
|
|
if (allSubtypes.isEmpty()) {
|
if (!inputMethodDir.exists()) {
|
// Even the parent directory doesn't exist. There is nothing to clean up.
|
return;
|
}
|
final AtomicFile subtypesFile = getAdditionalSubtypeFile(inputMethodDir);
|
if (subtypesFile.exists()) {
|
subtypesFile.delete();
|
}
|
if (FileUtils.listFilesOrEmpty(inputMethodDir).length == 0) {
|
if (!inputMethodDir.delete()) {
|
Slog.e(TAG, "Failed to delete the empty parent directory " + inputMethodDir);
|
}
|
}
|
return;
|
}
|
|
if (!inputMethodDir.exists() && !inputMethodDir.mkdirs()) {
|
Slog.e(TAG, "Failed to create a parent directory " + inputMethodDir);
|
return;
|
}
|
|
// Safety net for the case that this function is called before methodMap is set.
|
final boolean isSetMethodMap = methodMap != null && methodMap.size() > 0;
|
FileOutputStream fos = null;
|
final AtomicFile subtypesFile = getAdditionalSubtypeFile(inputMethodDir);
|
try {
|
fos = subtypesFile.startWrite();
|
final XmlSerializer out = new FastXmlSerializer();
|
out.setOutput(fos, StandardCharsets.UTF_8.name());
|
out.startDocument(null, true);
|
out.setFeature("http://xmlpull.org/v1/doc/features.html#indent-output", true);
|
out.startTag(null, NODE_SUBTYPES);
|
for (String imiId : allSubtypes.keySet()) {
|
if (isSetMethodMap && !methodMap.containsKey(imiId)) {
|
Slog.w(TAG, "IME uninstalled or not valid.: " + imiId);
|
continue;
|
}
|
out.startTag(null, NODE_IMI);
|
out.attribute(null, ATTR_ID, imiId);
|
final List<InputMethodSubtype> subtypesList = allSubtypes.get(imiId);
|
final int numSubtypes = subtypesList.size();
|
for (int i = 0; i < numSubtypes; ++i) {
|
final InputMethodSubtype subtype = subtypesList.get(i);
|
out.startTag(null, NODE_SUBTYPE);
|
if (subtype.hasSubtypeId()) {
|
out.attribute(null, ATTR_IME_SUBTYPE_ID,
|
String.valueOf(subtype.getSubtypeId()));
|
}
|
out.attribute(null, ATTR_ICON, String.valueOf(subtype.getIconResId()));
|
out.attribute(null, ATTR_LABEL, String.valueOf(subtype.getNameResId()));
|
out.attribute(null, ATTR_IME_SUBTYPE_LOCALE, subtype.getLocale());
|
out.attribute(null, ATTR_IME_SUBTYPE_LANGUAGE_TAG,
|
subtype.getLanguageTag());
|
out.attribute(null, ATTR_IME_SUBTYPE_MODE, subtype.getMode());
|
out.attribute(null, ATTR_IME_SUBTYPE_EXTRA_VALUE, subtype.getExtraValue());
|
out.attribute(null, ATTR_IS_AUXILIARY,
|
String.valueOf(subtype.isAuxiliary() ? 1 : 0));
|
out.attribute(null, ATTR_IS_ASCII_CAPABLE,
|
String.valueOf(subtype.isAsciiCapable() ? 1 : 0));
|
out.endTag(null, NODE_SUBTYPE);
|
}
|
out.endTag(null, NODE_IMI);
|
}
|
out.endTag(null, NODE_SUBTYPES);
|
out.endDocument();
|
subtypesFile.finishWrite(fos);
|
} catch (java.io.IOException e) {
|
Slog.w(TAG, "Error writing subtypes", e);
|
if (fos != null) {
|
subtypesFile.failWrite(fos);
|
}
|
}
|
}
|
|
/**
|
* Read additional subtypes from "subtype.xml".
|
*
|
* <p>This method does not confer any data/file locking semantics. Caller must make sure that
|
* multiple threads are not calling this method at the same time for the same {@code userId}.
|
* </p>
|
*
|
* @param allSubtypes {@link ArrayMap} from IME ID to additional subtype list. This parameter
|
* will be used to return the result.
|
* @param userId The user ID to be associated with.
|
*/
|
static void load(@NonNull ArrayMap<String, List<InputMethodSubtype>> allSubtypes,
|
@UserIdInt int userId) {
|
allSubtypes.clear();
|
|
final AtomicFile subtypesFile = getAdditionalSubtypeFile(getInputMethodDir(userId));
|
if (!subtypesFile.exists()) {
|
// Not having the file means there is no additional subtype.
|
return;
|
}
|
try (FileInputStream fis = subtypesFile.openRead()) {
|
final XmlPullParser parser = Xml.newPullParser();
|
parser.setInput(fis, StandardCharsets.UTF_8.name());
|
int type = parser.getEventType();
|
// Skip parsing until START_TAG
|
while (true) {
|
type = parser.next();
|
if (type == XmlPullParser.START_TAG || type == XmlPullParser.END_DOCUMENT) {
|
break;
|
}
|
}
|
String firstNodeName = parser.getName();
|
if (!NODE_SUBTYPES.equals(firstNodeName)) {
|
throw new XmlPullParserException("Xml doesn't start with subtypes");
|
}
|
final int depth = parser.getDepth();
|
String currentImiId = null;
|
ArrayList<InputMethodSubtype> tempSubtypesArray = null;
|
while (((type = parser.next()) != XmlPullParser.END_TAG
|
|| parser.getDepth() > depth) && type != XmlPullParser.END_DOCUMENT) {
|
if (type != XmlPullParser.START_TAG) {
|
continue;
|
}
|
final String nodeName = parser.getName();
|
if (NODE_IMI.equals(nodeName)) {
|
currentImiId = parser.getAttributeValue(null, ATTR_ID);
|
if (TextUtils.isEmpty(currentImiId)) {
|
Slog.w(TAG, "Invalid imi id found in subtypes.xml");
|
continue;
|
}
|
tempSubtypesArray = new ArrayList<>();
|
allSubtypes.put(currentImiId, tempSubtypesArray);
|
} else if (NODE_SUBTYPE.equals(nodeName)) {
|
if (TextUtils.isEmpty(currentImiId) || tempSubtypesArray == null) {
|
Slog.w(TAG, "IME uninstalled or not valid.: " + currentImiId);
|
continue;
|
}
|
final int icon = Integer.parseInt(
|
parser.getAttributeValue(null, ATTR_ICON));
|
final int label = Integer.parseInt(
|
parser.getAttributeValue(null, ATTR_LABEL));
|
final String imeSubtypeLocale =
|
parser.getAttributeValue(null, ATTR_IME_SUBTYPE_LOCALE);
|
final String languageTag =
|
parser.getAttributeValue(null, ATTR_IME_SUBTYPE_LANGUAGE_TAG);
|
final String imeSubtypeMode =
|
parser.getAttributeValue(null, ATTR_IME_SUBTYPE_MODE);
|
final String imeSubtypeExtraValue =
|
parser.getAttributeValue(null, ATTR_IME_SUBTYPE_EXTRA_VALUE);
|
final boolean isAuxiliary = "1".equals(String.valueOf(
|
parser.getAttributeValue(null, ATTR_IS_AUXILIARY)));
|
final boolean isAsciiCapable = "1".equals(String.valueOf(
|
parser.getAttributeValue(null, ATTR_IS_ASCII_CAPABLE)));
|
final InputMethodSubtype.InputMethodSubtypeBuilder
|
builder = new InputMethodSubtype.InputMethodSubtypeBuilder()
|
.setSubtypeNameResId(label)
|
.setSubtypeIconResId(icon)
|
.setSubtypeLocale(imeSubtypeLocale)
|
.setLanguageTag(languageTag)
|
.setSubtypeMode(imeSubtypeMode)
|
.setSubtypeExtraValue(imeSubtypeExtraValue)
|
.setIsAuxiliary(isAuxiliary)
|
.setIsAsciiCapable(isAsciiCapable);
|
final String subtypeIdString =
|
parser.getAttributeValue(null, ATTR_IME_SUBTYPE_ID);
|
if (subtypeIdString != null) {
|
builder.setSubtypeId(Integer.parseInt(subtypeIdString));
|
}
|
tempSubtypesArray.add(builder.build());
|
}
|
}
|
} catch (XmlPullParserException | IOException | NumberFormatException e) {
|
Slog.w(TAG, "Error reading subtypes", e);
|
}
|
}
|
}
|