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