• 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 com.android.server.locales;
18 
19 import static java.util.Objects.requireNonNull;
20 
21 import android.Manifest;
22 import android.annotation.NonNull;
23 import android.annotation.Nullable;
24 import android.annotation.UserIdInt;
25 import android.app.ActivityManager;
26 import android.app.ActivityManagerInternal;
27 import android.app.ILocaleManager;
28 import android.content.Context;
29 import android.content.Intent;
30 import android.content.pm.PackageManager;
31 import android.content.pm.PackageManager.PackageInfoFlags;
32 import android.content.res.Configuration;
33 import android.os.Binder;
34 import android.os.HandlerThread;
35 import android.os.LocaleList;
36 import android.os.Process;
37 import android.os.RemoteException;
38 import android.os.ResultReceiver;
39 import android.os.ShellCallback;
40 import android.os.UserHandle;
41 import android.util.Slog;
42 
43 import com.android.internal.annotations.VisibleForTesting;
44 import com.android.internal.content.PackageMonitor;
45 import com.android.internal.util.FrameworkStatsLog;
46 import com.android.server.LocalServices;
47 import com.android.server.SystemService;
48 import com.android.server.wm.ActivityTaskManagerInternal;
49 
50 import java.io.FileDescriptor;
51 
52 /**
53  * The implementation of ILocaleManager.aidl.
54  *
55  * <p>This service is API entry point for storing app-specific UI locales
56  */
57 public class LocaleManagerService extends SystemService {
58     private static final String TAG = "LocaleManagerService";
59     final Context mContext;
60     private final LocaleManagerService.LocaleManagerBinderService mBinderService;
61     private ActivityTaskManagerInternal mActivityTaskManagerInternal;
62     private ActivityManagerInternal mActivityManagerInternal;
63     private PackageManager mPackageManager;
64 
65     private LocaleManagerBackupHelper mBackupHelper;
66 
67     private final PackageMonitor mPackageMonitor;
68 
69     public static final boolean DEBUG = false;
70 
LocaleManagerService(Context context)71     public LocaleManagerService(Context context) {
72         super(context);
73         mContext = context;
74         mBinderService = new LocaleManagerBinderService();
75         mActivityTaskManagerInternal = LocalServices.getService(ActivityTaskManagerInternal.class);
76         mActivityManagerInternal = LocalServices.getService(ActivityManagerInternal.class);
77         mPackageManager = mContext.getPackageManager();
78 
79         HandlerThread broadcastHandlerThread = new HandlerThread(TAG,
80                 Process.THREAD_PRIORITY_BACKGROUND);
81         broadcastHandlerThread.start();
82 
83         SystemAppUpdateTracker systemAppUpdateTracker =
84                 new SystemAppUpdateTracker(this);
85         broadcastHandlerThread.getThreadHandler().postAtFrontOfQueue(new Runnable() {
86             @Override
87             public void run() {
88                 systemAppUpdateTracker.init();
89             }
90         });
91 
92         mBackupHelper = new LocaleManagerBackupHelper(this,
93                 mPackageManager, broadcastHandlerThread);
94 
95         mPackageMonitor = new LocaleManagerServicePackageMonitor(mBackupHelper,
96                 systemAppUpdateTracker);
97         mPackageMonitor.register(context, broadcastHandlerThread.getLooper(),
98                 UserHandle.ALL,
99                 true);
100     }
101 
102     @VisibleForTesting
LocaleManagerService(Context context, ActivityTaskManagerInternal activityTaskManagerInternal, ActivityManagerInternal activityManagerInternal, PackageManager packageManager, LocaleManagerBackupHelper localeManagerBackupHelper, PackageMonitor packageMonitor)103     LocaleManagerService(Context context, ActivityTaskManagerInternal activityTaskManagerInternal,
104             ActivityManagerInternal activityManagerInternal,
105             PackageManager packageManager,
106             LocaleManagerBackupHelper localeManagerBackupHelper,
107             PackageMonitor packageMonitor) {
108         super(context);
109         mContext = context;
110         mBinderService = new LocaleManagerBinderService();
111         mActivityTaskManagerInternal = activityTaskManagerInternal;
112         mActivityManagerInternal = activityManagerInternal;
113         mPackageManager = packageManager;
114         mBackupHelper = localeManagerBackupHelper;
115         mPackageMonitor = packageMonitor;
116     }
117 
118     @Override
onStart()119     public void onStart() {
120         publishBinderService(Context.LOCALE_SERVICE, mBinderService);
121         LocalServices.addService(LocaleManagerInternal.class, new LocaleManagerInternalImpl());
122     }
123 
124     private final class LocaleManagerInternalImpl extends LocaleManagerInternal {
125 
126         @Override
getBackupPayload(int userId)127         public @Nullable byte[] getBackupPayload(int userId) {
128             checkCallerIsSystem();
129             return mBackupHelper.getBackupPayload(userId);
130         }
131 
132         @Override
stageAndApplyRestoredPayload(byte[] payload, int userId)133         public void stageAndApplyRestoredPayload(byte[] payload, int userId) {
134             mBackupHelper.stageAndApplyRestoredPayload(payload, userId);
135         }
136 
checkCallerIsSystem()137         private void checkCallerIsSystem() {
138             if (Binder.getCallingUid() != Process.SYSTEM_UID) {
139                 throw new SecurityException("Caller is not system.");
140             }
141         }
142     }
143 
144     private final class LocaleManagerBinderService extends ILocaleManager.Stub {
145         @Override
setApplicationLocales(@onNull String appPackageName, @UserIdInt int userId, @NonNull LocaleList locales)146         public void setApplicationLocales(@NonNull String appPackageName, @UserIdInt int userId,
147                 @NonNull LocaleList locales) throws RemoteException {
148             LocaleManagerService.this.setApplicationLocales(appPackageName, userId, locales);
149         }
150 
151         @Override
152         @NonNull
getApplicationLocales(@onNull String appPackageName, @UserIdInt int userId)153         public LocaleList getApplicationLocales(@NonNull String appPackageName,
154                 @UserIdInt int userId) throws RemoteException {
155             return LocaleManagerService.this.getApplicationLocales(appPackageName, userId);
156         }
157 
158         @Override
159         @NonNull
getSystemLocales()160         public LocaleList getSystemLocales() throws RemoteException {
161             return LocaleManagerService.this.getSystemLocales();
162         }
163 
164         @Override
onShellCommand(FileDescriptor in, FileDescriptor out, FileDescriptor err, String[] args, ShellCallback callback, ResultReceiver resultReceiver)165         public void onShellCommand(FileDescriptor in, FileDescriptor out,
166                 FileDescriptor err, String[] args, ShellCallback callback,
167                 ResultReceiver resultReceiver) {
168             (new LocaleManagerShellCommand(mBinderService))
169                     .exec(this, in, out, err, args, callback, resultReceiver);
170         }
171 
172     }
173 
174     /**
175      * Sets the current UI locales for a specified app.
176      */
setApplicationLocales(@onNull String appPackageName, @UserIdInt int userId, @NonNull LocaleList locales)177     public void setApplicationLocales(@NonNull String appPackageName, @UserIdInt int userId,
178             @NonNull LocaleList locales) throws RemoteException, IllegalArgumentException {
179         AppLocaleChangedAtomRecord atomRecordForMetrics = new
180                 AppLocaleChangedAtomRecord(Binder.getCallingUid());
181         try {
182             requireNonNull(appPackageName);
183             requireNonNull(locales);
184             atomRecordForMetrics.setNewLocales(locales.toLanguageTags());
185             //Allow apps with INTERACT_ACROSS_USERS permission to set locales for different user.
186             userId = mActivityManagerInternal.handleIncomingUser(
187                     Binder.getCallingPid(), Binder.getCallingUid(), userId,
188                     false /* allowAll */, ActivityManagerInternal.ALLOW_NON_FULL,
189                     "setApplicationLocales", /* callerPackage= */ null);
190 
191             // This function handles two types of set operations:
192             // 1.) A normal, non-privileged app setting its own locale.
193             // 2.) A privileged system service setting locales of another package.
194             // The least privileged case is a normal app performing a set, so check that first and
195             // set locales if the package name is owned by the app. Next, check if the caller has
196             // the necessary permission and set locales.
197             boolean isCallerOwner = isPackageOwnedByCaller(appPackageName, userId,
198                     atomRecordForMetrics);
199             if (!isCallerOwner) {
200                 enforceChangeConfigurationPermission(atomRecordForMetrics);
201             }
202 
203             final long token = Binder.clearCallingIdentity();
204             try {
205                 setApplicationLocalesUnchecked(appPackageName, userId, locales,
206                         atomRecordForMetrics);
207             } finally {
208                 Binder.restoreCallingIdentity(token);
209             }
210         } finally {
211             logMetric(atomRecordForMetrics);
212         }
213     }
214 
setApplicationLocalesUnchecked(@onNull String appPackageName, @UserIdInt int userId, @NonNull LocaleList locales, @NonNull AppLocaleChangedAtomRecord atomRecordForMetrics)215     private void setApplicationLocalesUnchecked(@NonNull String appPackageName,
216             @UserIdInt int userId, @NonNull LocaleList locales,
217             @NonNull AppLocaleChangedAtomRecord atomRecordForMetrics) {
218         if (DEBUG) {
219             Slog.d(TAG, "setApplicationLocales: setting locales for package " + appPackageName
220                     + " and user " + userId);
221         }
222 
223         atomRecordForMetrics.setPrevLocales(getApplicationLocalesUnchecked(appPackageName, userId)
224                 .toLanguageTags());
225         final ActivityTaskManagerInternal.PackageConfigurationUpdater updater =
226                 mActivityTaskManagerInternal.createPackageConfigurationUpdater(appPackageName,
227                         userId);
228         boolean isConfigChanged = updater.setLocales(locales).commit();
229 
230         //We want to send the broadcasts only if config was actually updated on commit.
231         if (isConfigChanged) {
232             notifyAppWhoseLocaleChanged(appPackageName, userId, locales);
233             notifyInstallerOfAppWhoseLocaleChanged(appPackageName, userId, locales);
234             notifyRegisteredReceivers(appPackageName, userId, locales);
235 
236             mBackupHelper.notifyBackupManager();
237             atomRecordForMetrics.setStatus(
238                     FrameworkStatsLog.APPLICATION_LOCALES_CHANGED__STATUS__CONFIG_COMMITTED);
239         } else {
240             atomRecordForMetrics.setStatus(FrameworkStatsLog
241                     .APPLICATION_LOCALES_CHANGED__STATUS__CONFIG_UNCOMMITTED);
242         }
243     }
244 
245     /**
246      * Sends an implicit broadcast with action
247      * {@link android.content.Intent#ACTION_APPLICATION_LOCALE_CHANGED}
248      * to receivers with {@link android.Manifest.permission#READ_APP_SPECIFIC_LOCALES}.
249      */
notifyRegisteredReceivers(String appPackageName, int userId, LocaleList locales)250     private void notifyRegisteredReceivers(String appPackageName, int userId,
251             LocaleList locales) {
252         Intent intent = createBaseIntent(Intent.ACTION_APPLICATION_LOCALE_CHANGED,
253                 appPackageName, locales);
254         mContext.sendBroadcastAsUser(intent, UserHandle.of(userId),
255                 Manifest.permission.READ_APP_SPECIFIC_LOCALES);
256     }
257 
258     /**
259      * Sends an explicit broadcast with action
260      * {@link android.content.Intent#ACTION_APPLICATION_LOCALE_CHANGED} to
261      * the installer (as per {@link android.content.pm.InstallSourceInfo#getInstallingPackageName})
262      * of app whose locale has changed.
263      *
264      * <p><b>Note:</b> This is can be used by installers to deal with cases such as
265      * language-based APK Splits.
266      */
notifyInstallerOfAppWhoseLocaleChanged(String appPackageName, int userId, LocaleList locales)267     void notifyInstallerOfAppWhoseLocaleChanged(String appPackageName, int userId,
268             LocaleList locales) {
269         String installingPackageName = getInstallingPackageName(appPackageName);
270         if (installingPackageName != null) {
271             Intent intent = createBaseIntent(Intent.ACTION_APPLICATION_LOCALE_CHANGED,
272                     appPackageName, locales);
273             //Set package name to ensure that only installer of the app receives this intent.
274             intent.setPackage(installingPackageName);
275             mContext.sendBroadcastAsUser(intent, UserHandle.of(userId));
276         }
277     }
278 
279     /**
280      * Sends an explicit broadcast with action {@link android.content.Intent#ACTION_LOCALE_CHANGED}
281      * to the app whose locale has changed.
282      */
notifyAppWhoseLocaleChanged(String appPackageName, int userId, LocaleList locales)283     private void notifyAppWhoseLocaleChanged(String appPackageName, int userId,
284             LocaleList locales) {
285         Intent intent = createBaseIntent(Intent.ACTION_LOCALE_CHANGED, appPackageName, locales);
286         //Set package name to ensure that only the app whose locale changed receives this intent.
287         intent.setPackage(appPackageName);
288         intent.addFlags(Intent.FLAG_RECEIVER_VISIBLE_TO_INSTANT_APPS);
289         mContext.sendBroadcastAsUser(intent, UserHandle.of(userId));
290     }
291 
createBaseIntent(String intentAction, String appPackageName, LocaleList locales)292     static Intent createBaseIntent(String intentAction, String appPackageName,
293             LocaleList locales) {
294         return new Intent(intentAction)
295                 .putExtra(Intent.EXTRA_PACKAGE_NAME, appPackageName)
296                 .putExtra(Intent.EXTRA_LOCALE_LIST, locales)
297                 .addFlags(Intent.FLAG_RECEIVER_INCLUDE_BACKGROUND
298                         | Intent.FLAG_RECEIVER_FOREGROUND);
299     }
300 
301     /**
302      * Same as {@link LocaleManagerService#isPackageOwnedByCaller(String, int,
303      * AppLocaleChangedAtomRecord)}, but for methods that do not log locale atom.
304      */
isPackageOwnedByCaller(String appPackageName, int userId)305     private boolean isPackageOwnedByCaller(String appPackageName, int userId) {
306         return isPackageOwnedByCaller(appPackageName, userId, /* atomRecordForMetrics= */null);
307     }
308 
309     /**
310      * Checks if the package is owned by the calling app or not for the given user id.
311      *
312      * @throws IllegalArgumentException if package not found for given userid
313      */
isPackageOwnedByCaller(String appPackageName, int userId, @Nullable AppLocaleChangedAtomRecord atomRecordForMetrics)314     private boolean isPackageOwnedByCaller(String appPackageName, int userId,
315             @Nullable AppLocaleChangedAtomRecord atomRecordForMetrics) {
316         final int uid = getPackageUid(appPackageName, userId);
317         if (uid < 0) {
318             Slog.w(TAG, "Unknown package " + appPackageName + " for user " + userId);
319             if (atomRecordForMetrics != null) {
320                 atomRecordForMetrics.setStatus(FrameworkStatsLog
321                         .APPLICATION_LOCALES_CHANGED__STATUS__FAILURE_INVALID_TARGET_PACKAGE);
322             }
323             throw new IllegalArgumentException("Unknown package: " + appPackageName
324                     + " for user " + userId);
325         }
326         if (atomRecordForMetrics != null) {
327             atomRecordForMetrics.setTargetUid(uid);
328         }
329         //Once valid package found, ignore the userId part for validating package ownership
330         //as apps with INTERACT_ACROSS_USERS permission could be changing locale for different user.
331         return UserHandle.isSameApp(Binder.getCallingUid(), uid);
332     }
333 
enforceChangeConfigurationPermission(@onNull AppLocaleChangedAtomRecord atomRecordForMetrics)334     private void enforceChangeConfigurationPermission(@NonNull AppLocaleChangedAtomRecord
335             atomRecordForMetrics) {
336         try {
337             mContext.enforceCallingOrSelfPermission(
338                     android.Manifest.permission.CHANGE_CONFIGURATION, "setApplicationLocales");
339         } catch (SecurityException e) {
340             atomRecordForMetrics.setStatus(FrameworkStatsLog
341                     .APPLICATION_LOCALES_CHANGED__STATUS__FAILURE_PERMISSION_ABSENT);
342             throw e;
343         }
344     }
345 
346     /**
347      * Returns the current UI locales for the specified app.
348      */
349     @NonNull
getApplicationLocales(@onNull String appPackageName, @UserIdInt int userId)350     public LocaleList getApplicationLocales(@NonNull String appPackageName, @UserIdInt int userId)
351             throws RemoteException, IllegalArgumentException {
352         requireNonNull(appPackageName);
353 
354         //Allow apps with INTERACT_ACROSS_USERS permission to query locales for different user.
355         userId = mActivityManagerInternal.handleIncomingUser(
356                 Binder.getCallingPid(), Binder.getCallingUid(), userId,
357                 false /* allowAll */, ActivityManagerInternal.ALLOW_NON_FULL,
358                 "getApplicationLocales", /* callerPackage= */ null);
359 
360         // This function handles three types of query operations:
361         // 1.) A normal, non-privileged app querying its own locale.
362         // 2.) The installer of the given app querying locales of a package installed
363         // by said installer.
364         // 3.) A privileged system service querying locales of another package.
365         // The least privileged case is a normal app performing a query, so check that first and
366         // get locales if the package name is owned by the app. Next check if the calling app
367         // is the installer of the given app and get locales. If neither conditions matched,
368         // check if the caller has the necessary permission and fetch locales.
369         if (!isPackageOwnedByCaller(appPackageName, userId)
370                 && !isCallerInstaller(appPackageName, userId)) {
371             enforceReadAppSpecificLocalesPermission();
372         }
373         final long token = Binder.clearCallingIdentity();
374         try {
375             return getApplicationLocalesUnchecked(appPackageName, userId);
376         } finally {
377             Binder.restoreCallingIdentity(token);
378         }
379     }
380 
381     @NonNull
getApplicationLocalesUnchecked(@onNull String appPackageName, @UserIdInt int userId)382     private LocaleList getApplicationLocalesUnchecked(@NonNull String appPackageName,
383             @UserIdInt int userId) {
384         if (DEBUG) {
385             Slog.d(TAG, "getApplicationLocales: fetching locales for package " + appPackageName
386                     + " and user " + userId);
387         }
388 
389         final ActivityTaskManagerInternal.PackageConfig appConfig =
390                 mActivityTaskManagerInternal.getApplicationConfig(appPackageName, userId);
391         if (appConfig == null) {
392             if (DEBUG) {
393                 Slog.d(TAG, "getApplicationLocales: application config not found for "
394                         + appPackageName + " and user id " + userId);
395             }
396             return LocaleList.getEmptyLocaleList();
397         }
398         LocaleList locales = appConfig.mLocales;
399         return locales != null ? locales : LocaleList.getEmptyLocaleList();
400     }
401 
402     /**
403      * Checks if the calling app is the installer of the app whose locale changed.
404      */
isCallerInstaller(String appPackageName, int userId)405     private boolean isCallerInstaller(String appPackageName, int userId) {
406         String installingPackageName = getInstallingPackageName(appPackageName);
407         if (installingPackageName != null) {
408             // Get the uid of installer-on-record to compare with the calling uid.
409             int installerUid = getPackageUid(installingPackageName, userId);
410             return installerUid >= 0 && UserHandle.isSameApp(Binder.getCallingUid(), installerUid);
411         }
412         return false;
413     }
414 
enforceReadAppSpecificLocalesPermission()415     private void enforceReadAppSpecificLocalesPermission() {
416         mContext.enforceCallingOrSelfPermission(
417                 android.Manifest.permission.READ_APP_SPECIFIC_LOCALES,
418                 "getApplicationLocales");
419     }
420 
getPackageUid(String appPackageName, int userId)421     private int getPackageUid(String appPackageName, int userId) {
422         try {
423             return mPackageManager
424                     .getPackageUidAsUser(appPackageName, PackageInfoFlags.of(0), userId);
425         } catch (PackageManager.NameNotFoundException e) {
426             return Process.INVALID_UID;
427         }
428     }
429 
430     @Nullable
getInstallingPackageName(String packageName)431     String getInstallingPackageName(String packageName) {
432         try {
433             return mContext.getPackageManager()
434                     .getInstallSourceInfo(packageName).getInstallingPackageName();
435         } catch (PackageManager.NameNotFoundException e) {
436             Slog.w(TAG, "Package not found " + packageName);
437         }
438         return null;
439     }
440 
441     /**
442      * Returns the current system locales.
443      */
444     @NonNull
getSystemLocales()445     public LocaleList getSystemLocales() throws RemoteException {
446         final long token = Binder.clearCallingIdentity();
447         try {
448             return getSystemLocalesUnchecked();
449         } finally {
450             Binder.restoreCallingIdentity(token);
451         }
452     }
453 
454     @NonNull
getSystemLocalesUnchecked()455     private LocaleList getSystemLocalesUnchecked() throws RemoteException {
456         LocaleList systemLocales = null;
457         Configuration conf = ActivityManager.getService().getConfiguration();
458         if (conf != null) {
459             systemLocales = conf.getLocales();
460         }
461         if (systemLocales == null) {
462             systemLocales = LocaleList.getEmptyLocaleList();
463         }
464         return systemLocales;
465     }
466 
logMetric(@onNull AppLocaleChangedAtomRecord atomRecordForMetrics)467     private void logMetric(@NonNull AppLocaleChangedAtomRecord atomRecordForMetrics) {
468         FrameworkStatsLog.write(FrameworkStatsLog.APPLICATION_LOCALES_CHANGED,
469                 atomRecordForMetrics.mCallingUid,
470                 atomRecordForMetrics.mTargetUid,
471                 atomRecordForMetrics.mNewLocales,
472                 atomRecordForMetrics.mPrevLocales,
473                 atomRecordForMetrics.mStatus);
474     }
475 }
476