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.app.LocaleConfig; 29 import android.content.ComponentName; 30 import android.content.Context; 31 import android.content.Intent; 32 import android.content.pm.PackageManager; 33 import android.content.pm.PackageManager.PackageInfoFlags; 34 import android.content.res.Configuration; 35 import android.os.Binder; 36 import android.os.Environment; 37 import android.os.HandlerThread; 38 import android.os.LocaleList; 39 import android.os.Process; 40 import android.os.RemoteException; 41 import android.os.ResultReceiver; 42 import android.os.ShellCallback; 43 import android.os.SystemProperties; 44 import android.os.UserHandle; 45 import android.provider.Settings; 46 import android.text.TextUtils; 47 import android.util.AtomicFile; 48 import android.util.Slog; 49 import android.util.Xml; 50 51 import com.android.internal.annotations.VisibleForTesting; 52 import com.android.internal.content.PackageMonitor; 53 import com.android.internal.util.FrameworkStatsLog; 54 import com.android.internal.util.XmlUtils; 55 import com.android.modules.utils.TypedXmlPullParser; 56 import com.android.modules.utils.TypedXmlSerializer; 57 import com.android.server.LocalServices; 58 import com.android.server.SystemService; 59 import com.android.server.wm.ActivityTaskManagerInternal; 60 61 import org.xmlpull.v1.XmlPullParserException; 62 63 import java.io.ByteArrayOutputStream; 64 import java.io.File; 65 import java.io.FileDescriptor; 66 import java.io.FileInputStream; 67 import java.io.FileOutputStream; 68 import java.io.IOException; 69 import java.io.InputStream; 70 import java.nio.charset.StandardCharsets; 71 import java.util.ArrayList; 72 import java.util.Arrays; 73 import java.util.List; 74 import java.util.Locale; 75 76 /** 77 * The implementation of ILocaleManager.aidl. 78 * 79 * <p>This service is API entry point for storing app-specific UI locales and an override 80 * {@link LocaleConfig} for a specified app. 81 */ 82 public class LocaleManagerService extends SystemService { 83 private static final String TAG = "LocaleManagerService"; 84 // The feature flag control that allows the active IME to query the locales of the foreground 85 // app. 86 private static final String PROP_ALLOW_IME_QUERY_APP_LOCALE = 87 "i18n.feature.allow_ime_query_app_locale"; 88 // The feature flag control that the application can dynamically override the LocaleConfig. 89 private static final String PROP_DYNAMIC_LOCALES_CHANGE = 90 "i18n.feature.dynamic_locales_change"; 91 private static final String LOCALE_CONFIGS = "locale_configs"; 92 private static final String SUFFIX_FILE_NAME = ".xml"; 93 private static final String ATTR_NAME = "name"; 94 95 final Context mContext; 96 private final LocaleManagerService.LocaleManagerBinderService mBinderService; 97 private ActivityTaskManagerInternal mActivityTaskManagerInternal; 98 private ActivityManagerInternal mActivityManagerInternal; 99 private PackageManager mPackageManager; 100 101 private LocaleManagerBackupHelper mBackupHelper; 102 103 private final PackageMonitor mPackageMonitor; 104 105 private final Object mWriteLock = new Object(); 106 107 public static final boolean DEBUG = false; 108 LocaleManagerService(Context context)109 public LocaleManagerService(Context context) { 110 super(context); 111 mContext = context; 112 mBinderService = new LocaleManagerBinderService(); 113 mActivityTaskManagerInternal = LocalServices.getService(ActivityTaskManagerInternal.class); 114 mActivityManagerInternal = LocalServices.getService(ActivityManagerInternal.class); 115 mPackageManager = mContext.getPackageManager(); 116 117 HandlerThread broadcastHandlerThread = new HandlerThread(TAG, 118 Process.THREAD_PRIORITY_BACKGROUND); 119 broadcastHandlerThread.start(); 120 121 SystemAppUpdateTracker systemAppUpdateTracker = 122 new SystemAppUpdateTracker(this); 123 broadcastHandlerThread.getThreadHandler().postAtFrontOfQueue(new Runnable() { 124 @Override 125 public void run() { 126 systemAppUpdateTracker.init(); 127 } 128 }); 129 130 mBackupHelper = new LocaleManagerBackupHelper(this, 131 mPackageManager, broadcastHandlerThread); 132 mPackageMonitor = new LocaleManagerServicePackageMonitor(mBackupHelper, 133 systemAppUpdateTracker, this); 134 mPackageMonitor.register(context, broadcastHandlerThread.getLooper(), 135 UserHandle.ALL, 136 true); 137 } 138 139 @VisibleForTesting LocaleManagerService(Context context, ActivityTaskManagerInternal activityTaskManagerInternal, ActivityManagerInternal activityManagerInternal, PackageManager packageManager, LocaleManagerBackupHelper localeManagerBackupHelper, PackageMonitor packageMonitor)140 LocaleManagerService(Context context, ActivityTaskManagerInternal activityTaskManagerInternal, 141 ActivityManagerInternal activityManagerInternal, 142 PackageManager packageManager, 143 LocaleManagerBackupHelper localeManagerBackupHelper, 144 PackageMonitor packageMonitor) { 145 super(context); 146 mContext = context; 147 mBinderService = new LocaleManagerBinderService(); 148 mActivityTaskManagerInternal = activityTaskManagerInternal; 149 mActivityManagerInternal = activityManagerInternal; 150 mPackageManager = packageManager; 151 mBackupHelper = localeManagerBackupHelper; 152 mPackageMonitor = packageMonitor; 153 } 154 155 @Override onStart()156 public void onStart() { 157 publishBinderService(Context.LOCALE_SERVICE, mBinderService); 158 LocalServices.addService(LocaleManagerInternal.class, new LocaleManagerInternalImpl()); 159 } 160 161 private final class LocaleManagerInternalImpl extends LocaleManagerInternal { 162 163 @Override getBackupPayload(int userId)164 public @Nullable byte[] getBackupPayload(int userId) { 165 checkCallerIsSystem(); 166 return mBackupHelper.getBackupPayload(userId); 167 } 168 169 @Override stageAndApplyRestoredPayload(byte[] payload, int userId)170 public void stageAndApplyRestoredPayload(byte[] payload, int userId) { 171 mBackupHelper.stageAndApplyRestoredPayload(payload, userId); 172 } 173 checkCallerIsSystem()174 private void checkCallerIsSystem() { 175 if (Binder.getCallingUid() != Process.SYSTEM_UID) { 176 throw new SecurityException("Caller is not system."); 177 } 178 } 179 } 180 181 private final class LocaleManagerBinderService extends ILocaleManager.Stub { 182 @Override setApplicationLocales(@onNull String appPackageName, @UserIdInt int userId, @NonNull LocaleList locales, boolean fromDelegate)183 public void setApplicationLocales(@NonNull String appPackageName, @UserIdInt int userId, 184 @NonNull LocaleList locales, boolean fromDelegate) throws RemoteException { 185 int caller = fromDelegate 186 ? FrameworkStatsLog.APPLICATION_LOCALES_CHANGED__CALLER__CALLER_DELEGATE 187 : FrameworkStatsLog.APPLICATION_LOCALES_CHANGED__CALLER__CALLER_APPS; 188 LocaleManagerService.this.setApplicationLocales(appPackageName, userId, locales, 189 fromDelegate, caller); 190 } 191 192 @Override 193 @NonNull getApplicationLocales(@onNull String appPackageName, @UserIdInt int userId)194 public LocaleList getApplicationLocales(@NonNull String appPackageName, 195 @UserIdInt int userId) throws RemoteException { 196 return LocaleManagerService.this.getApplicationLocales(appPackageName, userId); 197 } 198 199 @Override 200 @NonNull getSystemLocales()201 public LocaleList getSystemLocales() throws RemoteException { 202 return LocaleManagerService.this.getSystemLocales(); 203 } 204 205 @Override setOverrideLocaleConfig(@onNull String appPackageName, @UserIdInt int userId, @Nullable LocaleConfig localeConfig)206 public void setOverrideLocaleConfig(@NonNull String appPackageName, @UserIdInt int userId, 207 @Nullable LocaleConfig localeConfig) throws RemoteException { 208 LocaleManagerService.this.setOverrideLocaleConfig(appPackageName, userId, localeConfig); 209 } 210 211 @Override 212 @Nullable getOverrideLocaleConfig(@onNull String appPackageName, @UserIdInt int userId)213 public LocaleConfig getOverrideLocaleConfig(@NonNull String appPackageName, 214 @UserIdInt int userId) { 215 return LocaleManagerService.this.getOverrideLocaleConfig(appPackageName, userId); 216 } 217 218 @Override onShellCommand(FileDescriptor in, FileDescriptor out, FileDescriptor err, String[] args, ShellCallback callback, ResultReceiver resultReceiver)219 public void onShellCommand(FileDescriptor in, FileDescriptor out, 220 FileDescriptor err, String[] args, ShellCallback callback, 221 ResultReceiver resultReceiver) { 222 (new LocaleManagerShellCommand(mBinderService)) 223 .exec(this, in, out, err, args, callback, resultReceiver); 224 } 225 226 } 227 228 /** 229 * Sets the current UI locales for a specified app. 230 */ setApplicationLocales(@onNull String appPackageName, @UserIdInt int userId, @NonNull LocaleList locales, boolean fromDelegate, int caller)231 public void setApplicationLocales(@NonNull String appPackageName, @UserIdInt int userId, 232 @NonNull LocaleList locales, boolean fromDelegate, int caller) 233 throws RemoteException, IllegalArgumentException { 234 AppLocaleChangedAtomRecord atomRecordForMetrics = new 235 AppLocaleChangedAtomRecord(Binder.getCallingUid()); 236 try { 237 requireNonNull(appPackageName); 238 requireNonNull(locales); 239 atomRecordForMetrics.setCaller(caller); 240 atomRecordForMetrics.setNewLocales(locales.toLanguageTags()); 241 //Allow apps with INTERACT_ACROSS_USERS permission to set locales for different user. 242 userId = mActivityManagerInternal.handleIncomingUser( 243 Binder.getCallingPid(), Binder.getCallingUid(), userId, 244 false /* allowAll */, ActivityManagerInternal.ALLOW_NON_FULL, 245 "setApplicationLocales", /* callerPackage= */ null); 246 247 // This function handles two types of set operations: 248 // 1.) A normal, non-privileged app setting its own locale. 249 // 2.) A privileged system service setting locales of another package. 250 // The least privileged case is a normal app performing a set, so check that first and 251 // set locales if the package name is owned by the app. Next, check if the caller has 252 // the necessary permission and set locales. 253 boolean isCallerOwner = isPackageOwnedByCaller(appPackageName, userId, 254 atomRecordForMetrics, null); 255 if (!isCallerOwner) { 256 enforceChangeConfigurationPermission(atomRecordForMetrics); 257 } 258 mBackupHelper.persistLocalesModificationInfo(userId, appPackageName, fromDelegate, 259 locales.isEmpty()); 260 final long token = Binder.clearCallingIdentity(); 261 try { 262 setApplicationLocalesUnchecked(appPackageName, userId, locales, 263 atomRecordForMetrics); 264 } finally { 265 Binder.restoreCallingIdentity(token); 266 } 267 } finally { 268 logAppLocalesMetric(atomRecordForMetrics); 269 } 270 } 271 setApplicationLocalesUnchecked(@onNull String appPackageName, @UserIdInt int userId, @NonNull LocaleList locales, @NonNull AppLocaleChangedAtomRecord atomRecordForMetrics)272 private void setApplicationLocalesUnchecked(@NonNull String appPackageName, 273 @UserIdInt int userId, @NonNull LocaleList locales, 274 @NonNull AppLocaleChangedAtomRecord atomRecordForMetrics) { 275 if (DEBUG) { 276 Slog.d(TAG, "setApplicationLocales: setting locales for package " + appPackageName 277 + " and user " + userId); 278 } 279 280 atomRecordForMetrics.setPrevLocales( 281 getApplicationLocalesUnchecked(appPackageName, userId).toLanguageTags()); 282 final ActivityTaskManagerInternal.PackageConfigurationUpdater updater = 283 mActivityTaskManagerInternal.createPackageConfigurationUpdater(appPackageName, 284 userId); 285 boolean isConfigChanged = updater.setLocales(locales).commit(); 286 287 //We want to send the broadcasts only if config was actually updated on commit. 288 if (isConfigChanged) { 289 notifyAppWhoseLocaleChanged(appPackageName, userId, locales); 290 notifyInstallerOfAppWhoseLocaleChanged(appPackageName, userId, locales); 291 notifyRegisteredReceivers(appPackageName, userId, locales); 292 293 mBackupHelper.notifyBackupManager(); 294 atomRecordForMetrics.setStatus( 295 FrameworkStatsLog.APPLICATION_LOCALES_CHANGED__STATUS__CONFIG_COMMITTED); 296 } else { 297 atomRecordForMetrics.setStatus(FrameworkStatsLog 298 .APPLICATION_LOCALES_CHANGED__STATUS__CONFIG_UNCOMMITTED); 299 } 300 } 301 302 /** 303 * Sends an implicit broadcast with action 304 * {@link android.content.Intent#ACTION_APPLICATION_LOCALE_CHANGED} 305 * to receivers with {@link android.Manifest.permission#READ_APP_SPECIFIC_LOCALES}. 306 */ notifyRegisteredReceivers(String appPackageName, int userId, LocaleList locales)307 private void notifyRegisteredReceivers(String appPackageName, int userId, 308 LocaleList locales) { 309 Intent intent = createBaseIntent(Intent.ACTION_APPLICATION_LOCALE_CHANGED, 310 appPackageName, locales); 311 mContext.sendBroadcastAsUser(intent, UserHandle.of(userId), 312 Manifest.permission.READ_APP_SPECIFIC_LOCALES); 313 } 314 315 /** 316 * Sends an explicit broadcast with action 317 * {@link android.content.Intent#ACTION_APPLICATION_LOCALE_CHANGED} to 318 * the installer (as per {@link android.content.pm.InstallSourceInfo#getInstallingPackageName}) 319 * of app whose locale has changed. 320 * 321 * <p><b>Note:</b> This is can be used by installers to deal with cases such as 322 * language-based APK Splits. 323 */ notifyInstallerOfAppWhoseLocaleChanged(String appPackageName, int userId, LocaleList locales)324 void notifyInstallerOfAppWhoseLocaleChanged(String appPackageName, int userId, 325 LocaleList locales) { 326 String installingPackageName = getInstallingPackageName(appPackageName, userId); 327 if (installingPackageName != null) { 328 Intent intent = createBaseIntent(Intent.ACTION_APPLICATION_LOCALE_CHANGED, 329 appPackageName, locales); 330 //Set package name to ensure that only installer of the app receives this intent. 331 intent.setPackage(installingPackageName); 332 mContext.sendBroadcastAsUser(intent, UserHandle.of(userId)); 333 } 334 } 335 336 /** 337 * Sends an explicit broadcast with action {@link android.content.Intent#ACTION_LOCALE_CHANGED} 338 * to the app whose locale has changed. 339 */ notifyAppWhoseLocaleChanged(String appPackageName, int userId, LocaleList locales)340 private void notifyAppWhoseLocaleChanged(String appPackageName, int userId, 341 LocaleList locales) { 342 Intent intent = createBaseIntent(Intent.ACTION_LOCALE_CHANGED, appPackageName, locales); 343 //Set package name to ensure that only the app whose locale changed receives this intent. 344 intent.setPackage(appPackageName); 345 intent.addFlags(Intent.FLAG_RECEIVER_VISIBLE_TO_INSTANT_APPS); 346 mContext.sendBroadcastAsUser(intent, UserHandle.of(userId)); 347 } 348 createBaseIntent(String intentAction, String appPackageName, LocaleList locales)349 static Intent createBaseIntent(String intentAction, String appPackageName, 350 LocaleList locales) { 351 return new Intent(intentAction) 352 .putExtra(Intent.EXTRA_PACKAGE_NAME, appPackageName) 353 .putExtra(Intent.EXTRA_LOCALE_LIST, locales) 354 .addFlags(Intent.FLAG_RECEIVER_INCLUDE_BACKGROUND 355 | Intent.FLAG_RECEIVER_FOREGROUND); 356 } 357 358 /** 359 * Checks if the package is owned by the calling app or not for the given user id. 360 * 361 * @throws IllegalArgumentException if package not found for given userid 362 */ isPackageOwnedByCaller(String appPackageName, int userId, @Nullable AppLocaleChangedAtomRecord atomRecordForMetrics, @Nullable AppSupportedLocalesChangedAtomRecord appSupportedLocalesChangedAtomRecord)363 private boolean isPackageOwnedByCaller(String appPackageName, int userId, 364 @Nullable AppLocaleChangedAtomRecord atomRecordForMetrics, 365 @Nullable AppSupportedLocalesChangedAtomRecord appSupportedLocalesChangedAtomRecord) { 366 final int uid = getPackageUid(appPackageName, userId); 367 if (uid < 0) { 368 Slog.w(TAG, "Unknown package " + appPackageName + " for user " + userId); 369 if (atomRecordForMetrics != null) { 370 atomRecordForMetrics.setStatus(FrameworkStatsLog 371 .APPLICATION_LOCALES_CHANGED__STATUS__FAILURE_INVALID_TARGET_PACKAGE); 372 } else if (appSupportedLocalesChangedAtomRecord != null) { 373 appSupportedLocalesChangedAtomRecord.setStatus(FrameworkStatsLog 374 .APP_SUPPORTED_LOCALES_CHANGED__STATUS__FAILURE_INVALID_TARGET_PACKAGE); 375 } 376 throw new IllegalArgumentException("Unknown package: " + appPackageName 377 + " for user " + userId); 378 } 379 if (atomRecordForMetrics != null) { 380 atomRecordForMetrics.setTargetUid(uid); 381 } else if (appSupportedLocalesChangedAtomRecord != null) { 382 appSupportedLocalesChangedAtomRecord.setTargetUid(uid); 383 } 384 //Once valid package found, ignore the userId part for validating package ownership 385 //as apps with INTERACT_ACROSS_USERS permission could be changing locale for different user. 386 return UserHandle.isSameApp(Binder.getCallingUid(), uid); 387 } 388 enforceChangeConfigurationPermission(@onNull AppLocaleChangedAtomRecord atomRecordForMetrics)389 private void enforceChangeConfigurationPermission(@NonNull AppLocaleChangedAtomRecord 390 atomRecordForMetrics) { 391 try { 392 mContext.enforceCallingOrSelfPermission( 393 android.Manifest.permission.CHANGE_CONFIGURATION, "setApplicationLocales"); 394 } catch (SecurityException e) { 395 atomRecordForMetrics.setStatus(FrameworkStatsLog 396 .APPLICATION_LOCALES_CHANGED__STATUS__FAILURE_PERMISSION_ABSENT); 397 throw e; 398 } 399 } 400 401 /** 402 * Returns the current UI locales for the specified app. 403 */ 404 @NonNull getApplicationLocales(@onNull String appPackageName, @UserIdInt int userId)405 public LocaleList getApplicationLocales(@NonNull String appPackageName, @UserIdInt int userId) 406 throws RemoteException, IllegalArgumentException { 407 requireNonNull(appPackageName); 408 409 //Allow apps with INTERACT_ACROSS_USERS permission to query locales for different user. 410 userId = mActivityManagerInternal.handleIncomingUser( 411 Binder.getCallingPid(), Binder.getCallingUid(), userId, 412 false /* allowAll */, ActivityManagerInternal.ALLOW_NON_FULL, 413 "getApplicationLocales", /* callerPackage= */ null); 414 415 // This function handles four types of query operations: 416 // 1.) A normal, non-privileged app querying its own locale. 417 // 2.) The installer of the given app querying locales of a package installed by said 418 // installer. 419 // 3.) The current input method querying locales of the current foreground app. 420 // 4.) A privileged system service querying locales of another package. 421 // The least privileged case is a normal app performing a query, so check that first and get 422 // locales if the package name is owned by the app. Next check if the calling app is the 423 // installer of the given app and get locales. Finally check if the calling app is the 424 // current input method, and that app is querying locales of the current foreground app. If 425 // neither conditions matched, check if the caller has the necessary permission and fetch 426 // locales. 427 if (!isPackageOwnedByCaller(appPackageName, userId, null, null) 428 && !isCallerInstaller(appPackageName, userId) 429 && !(isCallerFromCurrentInputMethod(userId) 430 && mActivityManagerInternal.isAppForeground( 431 getPackageUid(appPackageName, userId)))) { 432 enforceReadAppSpecificLocalesPermission(); 433 } 434 final long token = Binder.clearCallingIdentity(); 435 try { 436 return getApplicationLocalesUnchecked(appPackageName, userId); 437 } finally { 438 Binder.restoreCallingIdentity(token); 439 } 440 } 441 442 @NonNull getApplicationLocalesUnchecked(@onNull String appPackageName, @UserIdInt int userId)443 private LocaleList getApplicationLocalesUnchecked(@NonNull String appPackageName, 444 @UserIdInt int userId) { 445 if (DEBUG) { 446 Slog.d(TAG, "getApplicationLocales: fetching locales for package " + appPackageName 447 + " and user " + userId); 448 } 449 450 final ActivityTaskManagerInternal.PackageConfig appConfig = 451 mActivityTaskManagerInternal.getApplicationConfig(appPackageName, userId); 452 if (appConfig == null) { 453 if (DEBUG) { 454 Slog.d(TAG, "getApplicationLocales: application config not found for " 455 + appPackageName + " and user id " + userId); 456 } 457 return LocaleList.getEmptyLocaleList(); 458 } 459 LocaleList locales = appConfig.mLocales; 460 return locales != null ? locales : LocaleList.getEmptyLocaleList(); 461 } 462 463 /** 464 * Checks if the calling app is the installer of the app whose locale changed. 465 */ isCallerInstaller(String appPackageName, int userId)466 private boolean isCallerInstaller(String appPackageName, int userId) { 467 String installingPackageName = getInstallingPackageName(appPackageName, userId); 468 if (installingPackageName != null) { 469 // Get the uid of installer-on-record to compare with the calling uid. 470 int installerUid = getPackageUid(installingPackageName, userId); 471 return installerUid >= 0 && UserHandle.isSameApp(Binder.getCallingUid(), installerUid); 472 } 473 return false; 474 } 475 476 /** 477 * Checks if the calling app is the current input method. 478 */ isCallerFromCurrentInputMethod(int userId)479 private boolean isCallerFromCurrentInputMethod(int userId) { 480 if (!SystemProperties.getBoolean(PROP_ALLOW_IME_QUERY_APP_LOCALE, true)) { 481 return false; 482 } 483 484 String currentInputMethod = Settings.Secure.getStringForUser( 485 mContext.getContentResolver(), 486 Settings.Secure.DEFAULT_INPUT_METHOD, 487 userId); 488 if (!TextUtils.isEmpty(currentInputMethod)) { 489 String inputMethodPkgName = ComponentName 490 .unflattenFromString(currentInputMethod) 491 .getPackageName(); 492 int inputMethodUid = getPackageUid(inputMethodPkgName, userId); 493 return inputMethodUid >= 0 && UserHandle.isSameApp(Binder.getCallingUid(), 494 inputMethodUid); 495 } 496 497 return false; 498 } 499 enforceReadAppSpecificLocalesPermission()500 private void enforceReadAppSpecificLocalesPermission() { 501 mContext.enforceCallingOrSelfPermission( 502 android.Manifest.permission.READ_APP_SPECIFIC_LOCALES, 503 "getApplicationLocales"); 504 } 505 getPackageUid(String appPackageName, int userId)506 private int getPackageUid(String appPackageName, int userId) { 507 try { 508 return mPackageManager 509 .getPackageUidAsUser(appPackageName, PackageInfoFlags.of(0), userId); 510 } catch (PackageManager.NameNotFoundException e) { 511 return Process.INVALID_UID; 512 } 513 } 514 515 @Nullable getInstallingPackageName(String packageName, int userId)516 String getInstallingPackageName(String packageName, int userId) { 517 try { 518 return mContext.createContextAsUser(UserHandle.of(userId), /* flags= */ 519 0).getPackageManager().getInstallSourceInfo( 520 packageName).getInstallingPackageName(); 521 } catch (PackageManager.NameNotFoundException e) { 522 Slog.w(TAG, "Package not found " + packageName); 523 } 524 return null; 525 } 526 527 /** 528 * Returns the current system locales. 529 */ 530 @NonNull getSystemLocales()531 public LocaleList getSystemLocales() throws RemoteException { 532 final long token = Binder.clearCallingIdentity(); 533 try { 534 return getSystemLocalesUnchecked(); 535 } finally { 536 Binder.restoreCallingIdentity(token); 537 } 538 } 539 540 @NonNull getSystemLocalesUnchecked()541 private LocaleList getSystemLocalesUnchecked() throws RemoteException { 542 LocaleList systemLocales = null; 543 Configuration conf = ActivityManager.getService().getConfiguration(); 544 if (conf != null) { 545 systemLocales = conf.getLocales(); 546 } 547 if (systemLocales == null) { 548 systemLocales = LocaleList.getEmptyLocaleList(); 549 } 550 return systemLocales; 551 } 552 logAppLocalesMetric(@onNull AppLocaleChangedAtomRecord atomRecordForMetrics)553 private void logAppLocalesMetric(@NonNull AppLocaleChangedAtomRecord atomRecordForMetrics) { 554 FrameworkStatsLog.write(FrameworkStatsLog.APPLICATION_LOCALES_CHANGED, 555 atomRecordForMetrics.mCallingUid, 556 atomRecordForMetrics.mTargetUid, 557 atomRecordForMetrics.mNewLocales, 558 atomRecordForMetrics.mPrevLocales, 559 atomRecordForMetrics.mStatus, 560 atomRecordForMetrics.mCaller); 561 } 562 563 /** 564 * Storing an override {@link LocaleConfig} for a specified app. 565 */ setOverrideLocaleConfig(@onNull String appPackageName, @UserIdInt int userId, @Nullable LocaleConfig localeConfig)566 public void setOverrideLocaleConfig(@NonNull String appPackageName, @UserIdInt int userId, 567 @Nullable LocaleConfig localeConfig) throws IllegalArgumentException { 568 if (!SystemProperties.getBoolean(PROP_DYNAMIC_LOCALES_CHANGE, true)) { 569 return; 570 } 571 572 AppSupportedLocalesChangedAtomRecord atomRecord = new AppSupportedLocalesChangedAtomRecord( 573 Binder.getCallingUid()); 574 try { 575 requireNonNull(appPackageName); 576 577 //Allow apps with INTERACT_ACROSS_USERS permission to set locales for different user. 578 userId = mActivityManagerInternal.handleIncomingUser( 579 Binder.getCallingPid(), Binder.getCallingUid(), userId, 580 false /* allowAll */, ActivityManagerInternal.ALLOW_NON_FULL, 581 "setOverrideLocaleConfig", /* callerPackage= */ null); 582 583 // This function handles two types of set operations: 584 // 1.) A normal, an app overrides its own LocaleConfig. 585 // 2.) A privileged system application or service is granted the necessary permission to 586 // override a LocaleConfig of another package. 587 if (!isPackageOwnedByCaller(appPackageName, userId, null, atomRecord)) { 588 enforceSetAppSpecificLocaleConfigPermission(atomRecord); 589 } 590 591 final long token = Binder.clearCallingIdentity(); 592 try { 593 setOverrideLocaleConfigUnchecked(appPackageName, userId, localeConfig, atomRecord); 594 } finally { 595 Binder.restoreCallingIdentity(token); 596 } 597 } finally { 598 logAppSupportedLocalesChangedMetric(atomRecord); 599 } 600 } 601 setOverrideLocaleConfigUnchecked(@onNull String appPackageName, @UserIdInt int userId, @Nullable LocaleConfig overrideLocaleConfig, @NonNull AppSupportedLocalesChangedAtomRecord atomRecord)602 private void setOverrideLocaleConfigUnchecked(@NonNull String appPackageName, 603 @UserIdInt int userId, @Nullable LocaleConfig overrideLocaleConfig, 604 @NonNull AppSupportedLocalesChangedAtomRecord atomRecord) { 605 synchronized (mWriteLock) { 606 if (DEBUG) { 607 Slog.d(TAG, 608 "set the override LocaleConfig for package " + appPackageName + " and user " 609 + userId); 610 } 611 LocaleConfig resLocaleConfig = null; 612 try { 613 resLocaleConfig = LocaleConfig.fromContextIgnoringOverride( 614 mContext.createPackageContext(appPackageName, 0)); 615 } catch (PackageManager.NameNotFoundException e) { 616 Slog.e(TAG, "Unknown package name " + appPackageName); 617 return; 618 } 619 final File file = getXmlFileNameForUser(appPackageName, userId); 620 621 if (overrideLocaleConfig == null) { 622 if (file.exists()) { 623 Slog.d(TAG, "remove the override LocaleConfig"); 624 file.delete(); 625 } 626 removeUnsupportedAppLocales(appPackageName, userId, resLocaleConfig, 627 FrameworkStatsLog 628 .APPLICATION_LOCALES_CHANGED__CALLER__CALLER_DYNAMIC_LOCALES_CHANGE 629 ); 630 atomRecord.setOverrideRemoved(true); 631 atomRecord.setStatus(FrameworkStatsLog 632 .APP_SUPPORTED_LOCALES_CHANGED__STATUS__SUCCESS); 633 return; 634 } else { 635 if (overrideLocaleConfig.isSameLocaleConfig( 636 getOverrideLocaleConfig(appPackageName, userId))) { 637 Slog.d(TAG, "the same override, ignore it"); 638 atomRecord.setSameAsPrevConfig(true); 639 return; 640 } 641 642 LocaleList localeList = overrideLocaleConfig.getSupportedLocales(); 643 // Normally the LocaleList object should not be null. However we reassign it as the 644 // empty list in case it happens. 645 if (localeList == null) { 646 localeList = LocaleList.getEmptyLocaleList(); 647 } 648 if (DEBUG) { 649 Slog.d(TAG, 650 "setOverrideLocaleConfig, localeList: " + localeList.toLanguageTags()); 651 } 652 atomRecord.setNumLocales(localeList.size()); 653 654 // Store the override LocaleConfig to the file storage. 655 final AtomicFile atomicFile = new AtomicFile(file); 656 FileOutputStream stream = null; 657 try { 658 stream = atomicFile.startWrite(); 659 stream.write(toXmlByteArray(localeList)); 660 } catch (Exception e) { 661 Slog.e(TAG, "Failed to write file " + atomicFile, e); 662 if (stream != null) { 663 atomicFile.failWrite(stream); 664 } 665 atomRecord.setStatus(FrameworkStatsLog 666 .APP_SUPPORTED_LOCALES_CHANGED__STATUS__FAILURE_WRITE_TO_STORAGE); 667 return; 668 } 669 atomicFile.finishWrite(stream); 670 // Clear per-app locales if they are not in the override LocaleConfig. 671 removeUnsupportedAppLocales(appPackageName, userId, overrideLocaleConfig, 672 FrameworkStatsLog 673 .APPLICATION_LOCALES_CHANGED__CALLER__CALLER_DYNAMIC_LOCALES_CHANGE 674 ); 675 if (overrideLocaleConfig.isSameLocaleConfig(resLocaleConfig)) { 676 Slog.d(TAG, "setOverrideLocaleConfig, same as the app's LocaleConfig"); 677 atomRecord.setSameAsResConfig(true); 678 } 679 atomRecord.setStatus(FrameworkStatsLog 680 .APP_SUPPORTED_LOCALES_CHANGED__STATUS__SUCCESS); 681 if (DEBUG) { 682 Slog.i(TAG, "Successfully written to " + atomicFile); 683 } 684 } 685 } 686 } 687 688 /** 689 * Checks if the per-app locales are in the LocaleConfig. Per-app locales missing from the 690 * LocaleConfig will be removed. 691 * 692 * <p><b>Note:</b> Check whether to remove the per-app locales when the app is upgraded or 693 * the LocaleConfig is overridden. 694 */ removeUnsupportedAppLocales(String appPackageName, int userId, LocaleConfig localeConfig, int caller)695 void removeUnsupportedAppLocales(String appPackageName, int userId, 696 LocaleConfig localeConfig, int caller) { 697 LocaleList appLocales = getApplicationLocalesUnchecked(appPackageName, userId); 698 // Remove the per-app locales from the locale list if they don't exist in the LocaleConfig. 699 boolean resetAppLocales = false; 700 List<Locale> newAppLocales = new ArrayList<Locale>(); 701 702 if (localeConfig == null) { 703 //Reset the app locales to the system default 704 Slog.i(TAG, "There is no LocaleConfig, reset app locales"); 705 resetAppLocales = true; 706 } else { 707 for (int i = 0; i < appLocales.size(); i++) { 708 if (!localeConfig.containsLocale(appLocales.get(i))) { 709 Slog.i(TAG, "Missing from the LocaleConfig, reset app locales"); 710 resetAppLocales = true; 711 continue; 712 } 713 newAppLocales.add(appLocales.get(i)); 714 } 715 } 716 717 if (resetAppLocales) { 718 // Reset the app locales 719 Locale[] locales = new Locale[newAppLocales.size()]; 720 try { 721 setApplicationLocales(appPackageName, userId, 722 new LocaleList(newAppLocales.toArray(locales)), 723 mBackupHelper.areLocalesSetFromDelegate(userId, appPackageName), caller); 724 } catch (RemoteException | IllegalArgumentException e) { 725 Slog.e(TAG, "Could not set locales for " + appPackageName, e); 726 } 727 } 728 } 729 enforceSetAppSpecificLocaleConfigPermission( AppSupportedLocalesChangedAtomRecord atomRecord)730 private void enforceSetAppSpecificLocaleConfigPermission( 731 AppSupportedLocalesChangedAtomRecord atomRecord) { 732 try { 733 mContext.enforceCallingOrSelfPermission( 734 android.Manifest.permission.SET_APP_SPECIFIC_LOCALECONFIG, 735 "setOverrideLocaleConfig"); 736 } catch (SecurityException e) { 737 atomRecord.setStatus(FrameworkStatsLog 738 .APP_SUPPORTED_LOCALES_CHANGED__STATUS__FAILURE_PERMISSION_ABSENT); 739 throw e; 740 } 741 } 742 743 /** 744 * Returns the override LocaleConfig for a specified app. 745 */ 746 @Nullable getOverrideLocaleConfig(@onNull String appPackageName, @UserIdInt int userId)747 public LocaleConfig getOverrideLocaleConfig(@NonNull String appPackageName, 748 @UserIdInt int userId) { 749 if (!SystemProperties.getBoolean(PROP_DYNAMIC_LOCALES_CHANGE, true)) { 750 return null; 751 } 752 753 requireNonNull(appPackageName); 754 755 // Allow apps with INTERACT_ACROSS_USERS permission to query the override LocaleConfig for 756 // different user. 757 userId = mActivityManagerInternal.handleIncomingUser( 758 Binder.getCallingPid(), Binder.getCallingUid(), userId, 759 false /* allowAll */, ActivityManagerInternal.ALLOW_NON_FULL, 760 "getOverrideLocaleConfig", /* callerPackage= */ null); 761 762 final File file = getXmlFileNameForUser(appPackageName, userId); 763 if (!file.exists()) { 764 if (DEBUG) { 765 Slog.i(TAG, "getOverrideLocaleConfig, the file is not existed."); 766 } 767 return null; 768 } 769 770 try (InputStream in = new FileInputStream(file)) { 771 final TypedXmlPullParser parser = Xml.resolvePullParser(in); 772 List<String> overrideLocales = loadFromXml(parser); 773 if (DEBUG) { 774 Slog.i(TAG, "getOverrideLocaleConfig, Loaded locales: " + overrideLocales); 775 } 776 LocaleConfig storedLocaleConfig = new LocaleConfig( 777 LocaleList.forLanguageTags(String.join(",", overrideLocales))); 778 779 return storedLocaleConfig; 780 } catch (IOException | XmlPullParserException e) { 781 Slog.e(TAG, "Failed to parse XML configuration from " + file, e); 782 } 783 784 return null; 785 } 786 787 /** 788 * Delete an override {@link LocaleConfig} for a specified app from the file storage. 789 * 790 * <p>Clear the override LocaleConfig from the storage when the app is uninstalled. 791 */ deleteOverrideLocaleConfig(@onNull String appPackageName, @UserIdInt int userId)792 void deleteOverrideLocaleConfig(@NonNull String appPackageName, @UserIdInt int userId) { 793 final File file = getXmlFileNameForUser(appPackageName, userId); 794 795 if (file.exists()) { 796 Slog.d(TAG, "Delete the override LocaleConfig."); 797 file.delete(); 798 } 799 } 800 toXmlByteArray(LocaleList localeList)801 private byte[] toXmlByteArray(LocaleList localeList) { 802 try (ByteArrayOutputStream os = new ByteArrayOutputStream()) { 803 TypedXmlSerializer out = Xml.newFastSerializer(); 804 out.setOutput(os, StandardCharsets.UTF_8.name()); 805 out.startDocument(/* encoding= */ null, /* standalone= */ true); 806 out.startTag(/* namespace= */ null, LocaleConfig.TAG_LOCALE_CONFIG); 807 808 List<String> locales = new ArrayList<String>( 809 Arrays.asList(localeList.toLanguageTags().split(","))); 810 for (String locale : locales) { 811 out.startTag(null, LocaleConfig.TAG_LOCALE); 812 out.attribute(null, ATTR_NAME, locale); 813 out.endTag(null, LocaleConfig.TAG_LOCALE); 814 } 815 816 out.endTag(/* namespace= */ null, LocaleConfig.TAG_LOCALE_CONFIG); 817 out.endDocument(); 818 819 if (DEBUG) { 820 Slog.d(TAG, "setOverrideLocaleConfig toXmlByteArray, output: " + os.toString()); 821 } 822 return os.toByteArray(); 823 } catch (IOException e) { 824 return null; 825 } 826 } 827 828 @NonNull loadFromXml(TypedXmlPullParser parser)829 private List<String> loadFromXml(TypedXmlPullParser parser) 830 throws IOException, XmlPullParserException { 831 List<String> localeList = new ArrayList<>(); 832 833 XmlUtils.beginDocument(parser, LocaleConfig.TAG_LOCALE_CONFIG); 834 int depth = parser.getDepth(); 835 while (XmlUtils.nextElementWithin(parser, depth)) { 836 final String tagName = parser.getName(); 837 if (LocaleConfig.TAG_LOCALE.equals(tagName)) { 838 String locale = parser.getAttributeValue(/* namespace= */ null, ATTR_NAME); 839 localeList.add(locale); 840 } else { 841 Slog.w(TAG, "Unexpected tag name: " + tagName); 842 XmlUtils.skipCurrentTag(parser); 843 } 844 } 845 846 return localeList; 847 } 848 849 @NonNull getXmlFileNameForUser(@onNull String appPackageName, @UserIdInt int userId)850 private File getXmlFileNameForUser(@NonNull String appPackageName, @UserIdInt int userId) { 851 final File dir = new File(Environment.getDataSystemCeDirectory(userId), LOCALE_CONFIGS); 852 return new File(dir, appPackageName + SUFFIX_FILE_NAME); 853 } 854 logAppSupportedLocalesChangedMetric( @onNull AppSupportedLocalesChangedAtomRecord atomRecord)855 private void logAppSupportedLocalesChangedMetric( 856 @NonNull AppSupportedLocalesChangedAtomRecord atomRecord) { 857 FrameworkStatsLog.write(FrameworkStatsLog.APP_SUPPORTED_LOCALES_CHANGED, 858 atomRecord.mCallingUid, 859 atomRecord.mTargetUid, 860 atomRecord.mNumLocales, 861 atomRecord.mOverrideRemoved, 862 atomRecord.mSameAsResConfig, 863 atomRecord.mSameAsPrevConfig, 864 atomRecord.mStatus); 865 } 866 } 867