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