content: /*
   * 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);
          }
      }
  }
  