content: /*
   * Copyright (C) 2016 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.internal.app;
  
  import android.annotation.UnsupportedAppUsage;
  import android.content.Context;
  import android.os.LocaleList;
  import android.provider.Settings;
  import android.telephony.TelephonyManager;
  
  import java.io.Serializable;
  import java.util.HashMap;
  import java.util.HashSet;
  import java.util.IllformedLocaleException;
  import java.util.Locale;
  import java.util.Set;
  
  public class LocaleStore {
      private static final HashMap<String, LocaleInfo> sLocaleCache = new HashMap<>();
      private static boolean sFullyInitialized = false;
  
      public static class LocaleInfo implements Serializable {
          private static final int SUGGESTION_TYPE_NONE = 0;
          private static final int SUGGESTION_TYPE_SIM = 1 << 0;
          private static final int SUGGESTION_TYPE_CFG = 1 << 1;
  
          private final Locale mLocale;
          private final Locale mParent;
          private final String mId;
          private boolean mIsTranslated;
          private boolean mIsPseudo;
          private boolean mIsChecked; // Used by the LocaleListEditor to mark entries for deletion
          // Combination of flags for various reasons to show a locale as a suggestion.
          // Can be SIM, location, etc.
          private int mSuggestionFlags;
  
          private String mFullNameNative;
          private String mFullCountryNameNative;
          private String mLangScriptKey;
  
          private LocaleInfo(Locale locale) {
              this.mLocale = locale;
              this.mId = locale.toLanguageTag();
              this.mParent = getParent(locale);
              this.mIsChecked = false;
              this.mSuggestionFlags = SUGGESTION_TYPE_NONE;
              this.mIsTranslated = false;
              this.mIsPseudo = false;
          }
  
          private LocaleInfo(String localeId) {
              this(Locale.forLanguageTag(localeId));
          }
  
          private static Locale getParent(Locale locale) {
              if (locale.getCountry().isEmpty()) {
                  return null;
              }
              return new Locale.Builder()
                      .setLocale(locale)
                      .setRegion("")
                      .setExtension(Locale.UNICODE_LOCALE_EXTENSION, "")
                      .build();
          }
  
          @Override
          public String toString() {
              return mId;
          }
  
          @UnsupportedAppUsage
          public Locale getLocale() {
              return mLocale;
          }
  
          @UnsupportedAppUsage
          public Locale getParent() {
              return mParent;
          }
  
          @UnsupportedAppUsage
          public String getId() {
              return mId;
          }
  
          public boolean isTranslated() {
              return mIsTranslated;
          }
  
          public void setTranslated(boolean isTranslated) {
              mIsTranslated = isTranslated;
          }
  
          /* package */ boolean isSuggested() {
              if (!mIsTranslated) { // Never suggest an untranslated locale
                  return false;
              }
              return mSuggestionFlags != SUGGESTION_TYPE_NONE;
          }
  
          private boolean isSuggestionOfType(int suggestionMask) {
              if (!mIsTranslated) { // Never suggest an untranslated locale
                  return false;
              }
              return (mSuggestionFlags & suggestionMask) == suggestionMask;
          }
  
          @UnsupportedAppUsage
          public String getFullNameNative() {
              if (mFullNameNative == null) {
                  mFullNameNative =
                          LocaleHelper.getDisplayName(mLocale, mLocale, true /* sentence case */);
              }
              return mFullNameNative;
          }
  
          String getFullCountryNameNative() {
              if (mFullCountryNameNative == null) {
                  mFullCountryNameNative = LocaleHelper.getDisplayCountry(mLocale, mLocale);
              }
              return mFullCountryNameNative;
          }
  
          String getFullCountryNameInUiLanguage() {
              // We don't cache the UI name because the default locale keeps changing
              return LocaleHelper.getDisplayCountry(mLocale);
          }
  
          /** Returns the name of the locale in the language of the UI.
           * It is used for search, but never shown.
           * For instance German will show as "Deutsch" in the list, but we will also search for
           * "allemand" if the system UI is in French.
           */
          @UnsupportedAppUsage
          public String getFullNameInUiLanguage() {
              // We don't cache the UI name because the default locale keeps changing
              return LocaleHelper.getDisplayName(mLocale, true /* sentence case */);
          }
  
          private String getLangScriptKey() {
              if (mLangScriptKey == null) {
                  Locale baseLocale = new Locale.Builder()
                      .setLocale(mLocale)
                      .setExtension(Locale.UNICODE_LOCALE_EXTENSION, "")
                      .build();
                  Locale parentWithScript = getParent(LocaleHelper.addLikelySubtags(baseLocale));
                  mLangScriptKey =
                          (parentWithScript == null)
                          ? mLocale.toLanguageTag()
                          : parentWithScript.toLanguageTag();
              }
              return mLangScriptKey;
          }
  
          String getLabel(boolean countryMode) {
              if (countryMode) {
                  return getFullCountryNameNative();
              } else {
                  return getFullNameNative();
              }
          }
  
          String getContentDescription(boolean countryMode) {
              if (countryMode) {
                  return getFullCountryNameInUiLanguage();
              } else {
                  return getFullNameInUiLanguage();
              }
          }
  
          public boolean getChecked() {
              return mIsChecked;
          }
  
          public void setChecked(boolean checked) {
              mIsChecked = checked;
          }
      }
  
      private static Set<String> getSimCountries(Context context) {
          Set<String> result = new HashSet<>();
  
          TelephonyManager tm = TelephonyManager.from(context);
  
          if (tm != null) {
              String iso = tm.getSimCountryIso().toUpperCase(Locale.US);
              if (!iso.isEmpty()) {
                  result.add(iso);
              }
  
              iso = tm.getNetworkCountryIso().toUpperCase(Locale.US);
              if (!iso.isEmpty()) {
                  result.add(iso);
              }
          }
  
          return result;
      }
  
      /*
       * This method is added for SetupWizard, to force an update of the suggested locales
       * when the SIM is initialized.
       *
       * <p>When the device is freshly started, it sometimes gets to the language selection
       * before the SIM is properly initialized.
       * So at the time the cache is filled, the info from the SIM might not be available.
       * The SetupWizard has a SimLocaleMonitor class to detect onSubscriptionsChanged events.
       * SetupWizard will call this function when that happens.</p>
       *
       * <p>TODO: decide if it is worth moving such kind of monitoring in this shared code.
       * The user might change the SIM or might cross border and connect to a network
       * in a different country, without restarting the Settings application or the phone.</p>
       */
      public static void updateSimCountries(Context context) {
          Set<String> simCountries = getSimCountries(context);
  
          for (LocaleInfo li : sLocaleCache.values()) {
              // This method sets the suggestion flags for the (new) SIM locales, but it does not
              // try to clean up the old flags. After all, if the user replaces a German SIM
              // with a French one, it is still possible that they are speaking German.
              // So both French and German are reasonable suggestions.
              if (simCountries.contains(li.getLocale().getCountry())) {
                  li.mSuggestionFlags |= LocaleInfo.SUGGESTION_TYPE_SIM;
              }
          }
      }
  
      /*
       * Show all the languages supported for a country in the suggested list.
       * This is also handy for devices without SIM (tablets).
       */
      private static void addSuggestedLocalesForRegion(Locale locale) {
          if (locale == null) {
              return;
          }
          final String country = locale.getCountry();
          if (country.isEmpty()) {
              return;
          }
  
          for (LocaleInfo li : sLocaleCache.values()) {
              if (country.equals(li.getLocale().getCountry())) {
                  // We don't need to differentiate between manual and SIM suggestions
                  li.mSuggestionFlags |= LocaleInfo.SUGGESTION_TYPE_SIM;
              }
          }
      }
  
      @UnsupportedAppUsage
      public static void fillCache(Context context) {
          if (sFullyInitialized) {
              return;
          }
  
          Set<String> simCountries = getSimCountries(context);
  
          final boolean isInDeveloperMode = Settings.Global.getInt(context.getContentResolver(),
                  Settings.Global.DEVELOPMENT_SETTINGS_ENABLED, 0) != 0;
          for (String localeId : LocalePicker.getSupportedLocales(context)) {
              if (localeId.isEmpty()) {
                  throw new IllformedLocaleException("Bad locale entry in locale_config.xml");
              }
              LocaleInfo li = new LocaleInfo(localeId);
  
              if (LocaleList.isPseudoLocale(li.getLocale())) {
                  if (isInDeveloperMode) {
                      li.setTranslated(true);
                      li.mIsPseudo = true;
                      li.mSuggestionFlags |= LocaleInfo.SUGGESTION_TYPE_SIM;
                  } else {
                      // Do not display pseudolocales unless in development mode.
                      continue;
                  }
              }
  
              if (simCountries.contains(li.getLocale().getCountry())) {
                  li.mSuggestionFlags |= LocaleInfo.SUGGESTION_TYPE_SIM;
              }
              sLocaleCache.put(li.getId(), li);
              final Locale parent = li.getParent();
              if (parent != null) {
                  String parentId = parent.toLanguageTag();
                  if (!sLocaleCache.containsKey(parentId)) {
                      sLocaleCache.put(parentId, new LocaleInfo(parent));
                  }
              }
          }
  
          // TODO: See if we can reuse what LocaleList.matchScore does
          final HashSet<String> localizedLocales = new HashSet<>();
          for (String localeId : LocalePicker.getSystemAssetLocales()) {
              LocaleInfo li = new LocaleInfo(localeId);
              final String country = li.getLocale().getCountry();
              // All this is to figure out if we should suggest a country
              if (!country.isEmpty()) {
                  LocaleInfo cachedLocale = null;
                  if (sLocaleCache.containsKey(li.getId())) { // the simple case, e.g. fr-CH
                      cachedLocale = sLocaleCache.get(li.getId());
                  } else { // e.g. zh-TW localized, zh-Hant-TW in cache
                      final String langScriptCtry = li.getLangScriptKey() + "-" + country;
                      if (sLocaleCache.containsKey(langScriptCtry)) {
                          cachedLocale = sLocaleCache.get(langScriptCtry);
                      }
                  }
                  if (cachedLocale != null) {
                      cachedLocale.mSuggestionFlags |= LocaleInfo.SUGGESTION_TYPE_CFG;
                  }
              }
              localizedLocales.add(li.getLangScriptKey());
          }
  
          for (LocaleInfo li : sLocaleCache.values()) {
              li.setTranslated(localizedLocales.contains(li.getLangScriptKey()));
          }
  
          addSuggestedLocalesForRegion(Locale.getDefault());
  
          sFullyInitialized = true;
      }
  
      private static int getLevel(Set<String> ignorables, LocaleInfo li, boolean translatedOnly) {
          if (ignorables.contains(li.getId())) return 0;
          if (li.mIsPseudo) return 2;
          if (translatedOnly && !li.isTranslated()) return 0;
          if (li.getParent() != null) return 2;
          return 0;
      }
  
      /**
       * Returns a list of locales for language or region selection.
       * If the parent is null, then it is the language list.
       * If it is not null, then the list will contain all the locales that belong to that parent.
       * Example: if the parent is "ar", then the region list will contain all Arabic locales.
       * (this is not language based, but language-script, so that it works for zh-Hant and so on.
       */
      @UnsupportedAppUsage
      public static Set<LocaleInfo> getLevelLocales(Context context, Set<String> ignorables,
              LocaleInfo parent, boolean translatedOnly) {
          fillCache(context);
          String parentId = parent == null ? null : parent.getId();
  
          HashSet<LocaleInfo> result = new HashSet<>();
          for (LocaleStore.LocaleInfo li : sLocaleCache.values()) {
              int level = getLevel(ignorables, li, translatedOnly);
              if (level == 2) {
                  if (parent != null) { // region selection
                      if (parentId.equals(li.getParent().toLanguageTag())) {
                          result.add(li);
                      }
                  } else { // language selection
                      if (li.isSuggestionOfType(LocaleInfo.SUGGESTION_TYPE_SIM)) {
                          result.add(li);
                      } else {
                          result.add(getLocaleInfo(li.getParent()));
                      }
                  }
              }
          }
          return result;
      }
  
      @UnsupportedAppUsage
      public static LocaleInfo getLocaleInfo(Locale locale) {
          String id = locale.toLanguageTag();
          LocaleInfo result;
          if (!sLocaleCache.containsKey(id)) {
              result = new LocaleInfo(locale);
              sLocaleCache.put(id, result);
          } else {
              result = sLocaleCache.get(id);
          }
          return result;
      }
  }
  