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