1 /* 2 * Copyright (C) 2021 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 android.app; 18 19 import android.annotation.FlaggedApi; 20 import android.annotation.IntDef; 21 import android.annotation.NonNull; 22 import android.annotation.Nullable; 23 import android.annotation.SuppressLint; 24 import android.content.Context; 25 import android.content.res.Resources; 26 import android.content.res.TypedArray; 27 import android.content.res.XmlResourceParser; 28 import android.os.LocaleList; 29 import android.os.Parcel; 30 import android.os.Parcelable; 31 import android.util.AttributeSet; 32 import android.util.Slog; 33 import android.util.Xml; 34 35 import com.android.internal.R; 36 import com.android.internal.util.XmlUtils; 37 38 import org.xmlpull.v1.XmlPullParserException; 39 40 import java.io.IOException; 41 import java.lang.annotation.Retention; 42 import java.lang.annotation.RetentionPolicy; 43 import java.util.Arrays; 44 import java.util.Collections; 45 import java.util.HashSet; 46 import java.util.List; 47 import java.util.Locale; 48 import java.util.Set; 49 50 /** 51 * The LocaleConfig of an application. 52 * There are two sources. One is from an XML resource file with an {@code <locale-config>} element 53 * and referenced in the manifest via {@code android:localeConfig} on {@code <application>}. The 54 * other is that the application dynamically provides an override version which is persisted in 55 * {@link LocaleManager#setOverrideLocaleConfig(LocaleConfig)}. 56 * 57 * <p>For more information about the LocaleConfig from an XML resource file, see 58 * <a href="https://developer.android.com/about/versions/13/features/app-languages#use-localeconfig"> 59 * the section on per-app language preferences</a>. 60 * 61 * @attr ref android.R.styleable#LocaleConfig_Locale_name 62 * @attr ref android.R.styleable#AndroidManifestApplication_localeConfig 63 */ 64 // Add following to last Note: when guide is written: 65 // For more information about the LocaleConfig overridden by the application, see TODO(b/261528306): 66 // add link to guide 67 public class LocaleConfig implements Parcelable { 68 private static final String TAG = "LocaleConfig"; 69 public static final String TAG_LOCALE_CONFIG = "locale-config"; 70 public static final String TAG_LOCALE = "locale"; 71 private LocaleList mLocales; 72 73 private Locale mDefaultLocale; 74 private int mStatus = STATUS_NOT_SPECIFIED; 75 76 /** 77 * succeeded reading the LocaleConfig structure stored in an XML file. 78 */ 79 public static final int STATUS_SUCCESS = 0; 80 /** 81 * No android:localeConfig tag on <application>. 82 */ 83 public static final int STATUS_NOT_SPECIFIED = 1; 84 /** 85 * Malformed input in the XML file where the LocaleConfig was stored. 86 */ 87 public static final int STATUS_PARSING_FAILED = 2; 88 89 /** @hide */ 90 @IntDef(prefix = { "STATUS_" }, value = { 91 STATUS_SUCCESS, 92 STATUS_NOT_SPECIFIED, 93 STATUS_PARSING_FAILED 94 }) 95 @Retention(RetentionPolicy.SOURCE) 96 public @interface Status{} 97 98 /** 99 * Returns an override LocaleConfig if it has been set via 100 * {@link LocaleManager#setOverrideLocaleConfig(LocaleConfig)}. Otherwise, returns the 101 * LocaleConfig from the application resources. 102 * 103 * @param context the context of the application. 104 * 105 * @see Context#createPackageContext(String, int). 106 */ LocaleConfig(@onNull Context context)107 public LocaleConfig(@NonNull Context context) { 108 this(context, true); 109 } 110 111 /** 112 * Returns a LocaleConfig from the application resources regardless of whether any LocaleConfig 113 * is overridden via {@link LocaleManager#setOverrideLocaleConfig(LocaleConfig)}. 114 * 115 * @param context the context of the application. 116 * 117 * @see Context#createPackageContext(String, int). 118 */ 119 @NonNull fromContextIgnoringOverride(@onNull Context context)120 public static LocaleConfig fromContextIgnoringOverride(@NonNull Context context) { 121 return new LocaleConfig(context, false); 122 } 123 LocaleConfig(@onNull Context context, boolean allowOverride)124 private LocaleConfig(@NonNull Context context, boolean allowOverride) { 125 if (allowOverride) { 126 LocaleManager localeManager = context.getSystemService(LocaleManager.class); 127 if (localeManager == null) { 128 Slog.w(TAG, "LocaleManager is null, cannot get the override LocaleConfig"); 129 mStatus = STATUS_NOT_SPECIFIED; 130 return; 131 } 132 LocaleConfig localeConfig = localeManager.getOverrideLocaleConfig(); 133 if (localeConfig != null) { 134 Slog.d(TAG, "Has the override LocaleConfig"); 135 mStatus = localeConfig.getStatus(); 136 mLocales = localeConfig.getSupportedLocales(); 137 return; 138 } 139 } 140 Resources res = context.getResources(); 141 //Get the resource id 142 int resId = context.getApplicationInfo().getLocaleConfigRes(); 143 if (resId == 0) { 144 mStatus = STATUS_NOT_SPECIFIED; 145 return; 146 } 147 try { 148 //Get the parser to read XML data 149 XmlResourceParser parser = res.getXml(resId); 150 parseLocaleConfig(parser, res); 151 } catch (Resources.NotFoundException e) { 152 Slog.w(TAG, "The resource file pointed to by the given resource ID isn't found."); 153 mStatus = STATUS_NOT_SPECIFIED; 154 } catch (XmlPullParserException | IOException e) { 155 Slog.w(TAG, "Failed to parse XML configuration from " 156 + res.getResourceEntryName(resId), e); 157 mStatus = STATUS_PARSING_FAILED; 158 } 159 } 160 161 /** 162 * Return the LocaleConfig with any sequence of locales combined into a {@link LocaleList}. 163 * 164 * <p><b>Note:</b> Applications seeking to create an override LocaleConfig via 165 * {@link LocaleManager#setOverrideLocaleConfig(LocaleConfig)} should use this constructor to 166 * first create the LocaleConfig they intend the system to see as the override. 167 * 168 * <p><b>Note:</b> The creation of this LocaleConfig does not automatically mean it will 169 * become the override config for an application. Any LocaleConfig desired to be the override 170 * must be passed into the {@link LocaleManager#setOverrideLocaleConfig(LocaleConfig)}, 171 * otherwise it will not persist or affect the system's understanding of app-supported 172 * resources. 173 * 174 * @param locales the desired locales for a specified application 175 */ LocaleConfig(@onNull LocaleList locales)176 public LocaleConfig(@NonNull LocaleList locales) { 177 mStatus = STATUS_SUCCESS; 178 mLocales = locales; 179 } 180 181 /** 182 * Instantiate a new LocaleConfig from the data in a Parcel that was 183 * previously written with {@link #writeToParcel(Parcel, int)}. 184 * 185 * @param in The Parcel containing the previously written LocaleConfig, 186 * positioned at the location in the buffer where it was written. 187 */ LocaleConfig(@onNull Parcel in)188 private LocaleConfig(@NonNull Parcel in) { 189 mStatus = in.readInt(); 190 mLocales = in.readTypedObject(LocaleList.CREATOR); 191 } 192 193 /** 194 * Parse the XML content and get the locales supported by the application 195 */ parseLocaleConfig(XmlResourceParser parser, Resources res)196 private void parseLocaleConfig(XmlResourceParser parser, Resources res) 197 throws IOException, XmlPullParserException { 198 XmlUtils.beginDocument(parser, TAG_LOCALE_CONFIG); 199 int outerDepth = parser.getDepth(); 200 AttributeSet attrs = Xml.asAttributeSet(parser); 201 202 String defaultLocale = null; 203 if (android.content.res.Flags.defaultLocale()) { 204 // Read the defaultLocale attribute of the LocaleConfig element 205 TypedArray att = res.obtainAttributes( 206 attrs, com.android.internal.R.styleable.LocaleConfig); 207 defaultLocale = att.getString( 208 R.styleable.LocaleConfig_defaultLocale); 209 att.recycle(); 210 } 211 212 Set<String> localeNames = new HashSet<>(); 213 while (XmlUtils.nextElementWithin(parser, outerDepth)) { 214 if (TAG_LOCALE.equals(parser.getName())) { 215 final TypedArray attributes = res.obtainAttributes( 216 attrs, com.android.internal.R.styleable.LocaleConfig_Locale); 217 String nameAttr = attributes.getString( 218 com.android.internal.R.styleable.LocaleConfig_Locale_name); 219 localeNames.add(nameAttr); 220 attributes.recycle(); 221 } else { 222 XmlUtils.skipCurrentTag(parser); 223 } 224 } 225 mStatus = STATUS_SUCCESS; 226 mLocales = LocaleList.forLanguageTags(String.join(",", localeNames)); 227 if (defaultLocale != null) { 228 if (localeNames.contains(defaultLocale)) { 229 mDefaultLocale = Locale.forLanguageTag(defaultLocale); 230 } else { 231 Slog.w(TAG, "Default locale specified that is not contained in the list: " 232 + defaultLocale); 233 mStatus = STATUS_PARSING_FAILED; 234 } 235 } 236 } 237 238 /** 239 * Returns the locales supported by the specified application. 240 * 241 * <p><b>Note:</b> The locale format should follow the 242 * <a href="https://www.rfc-editor.org/rfc/bcp/bcp47.txt">IETF BCP47 regular expression</a> 243 * 244 * @return the {@link LocaleList} 245 */ getSupportedLocales()246 public @Nullable LocaleList getSupportedLocales() { 247 return mLocales; 248 } 249 250 /** 251 * Returns the locale the strings in values/strings.xml (the default strings in the directory 252 * with no locale qualifier) are in if specified, otherwise null 253 * 254 * @return The default Locale or null 255 */ 256 @SuppressLint("UseIcu") 257 @FlaggedApi(android.content.res.Flags.FLAG_DEFAULT_LOCALE) getDefaultLocale()258 public @Nullable Locale getDefaultLocale() { 259 return mDefaultLocale; 260 } 261 262 /** 263 * Get the status of reading the resource file where the LocaleConfig was stored. 264 * 265 * <p>Distinguish "the application didn't provide the resource file" from "the application 266 * provided malformed input" if {@link #getSupportedLocales()} returns {@code null}. 267 * 268 * @return {@code STATUS_SUCCESS} if the LocaleConfig structure existed in an XML file was 269 * successfully read, or {@code STATUS_NOT_SPECIFIED} if no android:localeConfig tag on 270 * <application> pointing to an XML file that stores the LocaleConfig, or 271 * {@code STATUS_PARSING_FAILED} if the application provided malformed input for the 272 * LocaleConfig structure. 273 * 274 * @see #STATUS_SUCCESS 275 * @see #STATUS_NOT_SPECIFIED 276 * @see #STATUS_PARSING_FAILED 277 * 278 */ getStatus()279 public @Status int getStatus() { 280 return mStatus; 281 } 282 283 @Override describeContents()284 public int describeContents() { 285 return 0; 286 } 287 288 @Override writeToParcel(@onNull Parcel dest, int flags)289 public void writeToParcel(@NonNull Parcel dest, int flags) { 290 dest.writeInt(mStatus); 291 dest.writeTypedObject(mLocales, flags); 292 } 293 294 public static final @NonNull Parcelable.Creator<LocaleConfig> CREATOR = 295 new Parcelable.Creator<LocaleConfig>() { 296 @Override 297 public LocaleConfig createFromParcel(Parcel source) { 298 return new LocaleConfig(source); 299 } 300 301 @Override 302 public LocaleConfig[] newArray(int size) { 303 return new LocaleConfig[size]; 304 } 305 }; 306 307 /** 308 * Compare whether the LocaleConfig is the same. 309 * 310 * <p>If the elements of {@code mLocales} in LocaleConfig are the same but arranged in different 311 * positions, they are also considered to be the same LocaleConfig. 312 * 313 * @param other The {@link LocaleConfig} to compare for. 314 * 315 * @return true if the LocaleConfig is the same, false otherwise. 316 * 317 * @hide 318 */ isSameLocaleConfig(@ullable LocaleConfig other)319 public boolean isSameLocaleConfig(@Nullable LocaleConfig other) { 320 if (other == this) { 321 return true; 322 } 323 324 if (other != null) { 325 if (mStatus != other.mStatus) { 326 return false; 327 } 328 LocaleList otherLocales = other.mLocales; 329 if (mLocales == null && otherLocales == null) { 330 return true; 331 } else if (mLocales != null && otherLocales != null) { 332 List<String> hostStrList = Arrays.asList(mLocales.toLanguageTags().split(",")); 333 List<String> targetStrList = Arrays.asList( 334 otherLocales.toLanguageTags().split(",")); 335 Collections.sort(hostStrList); 336 Collections.sort(targetStrList); 337 return hostStrList.equals(targetStrList); 338 } 339 } 340 341 return false; 342 } 343 344 /** 345 * Compare whether the locale is existed in the {@code mLocales} of the LocaleConfig. 346 * 347 * @param locale The {@link Locale} to compare for. 348 * 349 * @return true if the locale is existed in the {@code mLocales} of the LocaleConfig, false 350 * otherwise. 351 * 352 * @hide 353 */ containsLocale(Locale locale)354 public boolean containsLocale(Locale locale) { 355 if (mLocales == null) { 356 return false; 357 } 358 359 for (int i = 0; i < mLocales.size(); i++) { 360 if (LocaleList.matchesLanguageAndScript(mLocales.get(i), locale)) { 361 return true; 362 } 363 } 364 365 return false; 366 } 367 } 368