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&#39;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