• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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&#39;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