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