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 ComponentName componentName = ComponentName.unflattenFromString(currentInputMethod); 490 if (componentName == null) { 491 Slog.d(TAG, "inValid input method"); 492 return false; 493 } 494 String inputMethodPkgName = componentName.getPackageName(); 495 int inputMethodUid = getPackageUid(inputMethodPkgName, userId); 496 return inputMethodUid >= 0 && UserHandle.isSameApp(Binder.getCallingUid(), 497 inputMethodUid); 498 } 499 500 return false; 501 } 502 enforceReadAppSpecificLocalesPermission()503 private void enforceReadAppSpecificLocalesPermission() { 504 mContext.enforceCallingOrSelfPermission( 505 android.Manifest.permission.READ_APP_SPECIFIC_LOCALES, 506 "getApplicationLocales"); 507 } 508 getPackageUid(String appPackageName, int userId)509 private int getPackageUid(String appPackageName, int userId) { 510 try { 511 return mPackageManager 512 .getPackageUidAsUser(appPackageName, PackageInfoFlags.of(0), userId); 513 } catch (PackageManager.NameNotFoundException e) { 514 return Process.INVALID_UID; 515 } 516 } 517 518 @Nullable getInstallingPackageName(String packageName, int userId)519 String getInstallingPackageName(String packageName, int userId) { 520 try { 521 return mContext.createContextAsUser(UserHandle.of(userId), /* flags= */ 522 0).getPackageManager().getInstallSourceInfo( 523 packageName).getInstallingPackageName(); 524 } catch (PackageManager.NameNotFoundException e) { 525 Slog.w(TAG, "Package not found " + packageName); 526 } 527 return null; 528 } 529 530 /** 531 * Returns the current system locales. 532 */ 533 @NonNull getSystemLocales()534 public LocaleList getSystemLocales() throws RemoteException { 535 final long token = Binder.clearCallingIdentity(); 536 try { 537 return getSystemLocalesUnchecked(); 538 } finally { 539 Binder.restoreCallingIdentity(token); 540 } 541 } 542 543 @NonNull getSystemLocalesUnchecked()544 private LocaleList getSystemLocalesUnchecked() throws RemoteException { 545 LocaleList systemLocales = null; 546 Configuration conf = ActivityManager.getService().getConfiguration(); 547 if (conf != null) { 548 systemLocales = conf.getLocales(); 549 } 550 if (systemLocales == null) { 551 systemLocales = LocaleList.getEmptyLocaleList(); 552 } 553 return systemLocales; 554 } 555 logAppLocalesMetric(@onNull AppLocaleChangedAtomRecord atomRecordForMetrics)556 private void logAppLocalesMetric(@NonNull AppLocaleChangedAtomRecord atomRecordForMetrics) { 557 FrameworkStatsLog.write(FrameworkStatsLog.APPLICATION_LOCALES_CHANGED, 558 atomRecordForMetrics.mCallingUid, 559 atomRecordForMetrics.mTargetUid, 560 atomRecordForMetrics.mNewLocales, 561 atomRecordForMetrics.mPrevLocales, 562 atomRecordForMetrics.mStatus, 563 atomRecordForMetrics.mCaller); 564 } 565 566 /** 567 * Storing an override {@link LocaleConfig} for a specified app. 568 */ setOverrideLocaleConfig(@onNull String appPackageName, @UserIdInt int userId, @Nullable LocaleConfig localeConfig)569 public void setOverrideLocaleConfig(@NonNull String appPackageName, @UserIdInt int userId, 570 @Nullable LocaleConfig localeConfig) throws IllegalArgumentException { 571 if (!SystemProperties.getBoolean(PROP_DYNAMIC_LOCALES_CHANGE, true)) { 572 return; 573 } 574 575 AppSupportedLocalesChangedAtomRecord atomRecord = new AppSupportedLocalesChangedAtomRecord( 576 Binder.getCallingUid()); 577 try { 578 requireNonNull(appPackageName); 579 580 //Allow apps with INTERACT_ACROSS_USERS permission to set locales for different user. 581 userId = mActivityManagerInternal.handleIncomingUser( 582 Binder.getCallingPid(), Binder.getCallingUid(), userId, 583 false /* allowAll */, ActivityManagerInternal.ALLOW_NON_FULL, 584 "setOverrideLocaleConfig", /* callerPackage= */ null); 585 586 // This function handles two types of set operations: 587 // 1.) A normal, an app overrides its own LocaleConfig. 588 // 2.) A privileged system application or service is granted the necessary permission to 589 // override a LocaleConfig of another package. 590 if (!isPackageOwnedByCaller(appPackageName, userId, null, atomRecord)) { 591 enforceSetAppSpecificLocaleConfigPermission(atomRecord); 592 } 593 594 final long token = Binder.clearCallingIdentity(); 595 try { 596 setOverrideLocaleConfigUnchecked(appPackageName, userId, localeConfig, atomRecord); 597 } finally { 598 Binder.restoreCallingIdentity(token); 599 } 600 } finally { 601 logAppSupportedLocalesChangedMetric(atomRecord); 602 } 603 } 604 setOverrideLocaleConfigUnchecked(@onNull String appPackageName, @UserIdInt int userId, @Nullable LocaleConfig overrideLocaleConfig, @NonNull AppSupportedLocalesChangedAtomRecord atomRecord)605 private void setOverrideLocaleConfigUnchecked(@NonNull String appPackageName, 606 @UserIdInt int userId, @Nullable LocaleConfig overrideLocaleConfig, 607 @NonNull AppSupportedLocalesChangedAtomRecord atomRecord) { 608 synchronized (mWriteLock) { 609 if (DEBUG) { 610 Slog.d(TAG, 611 "set the override LocaleConfig for package " + appPackageName + " and user " 612 + userId); 613 } 614 LocaleConfig resLocaleConfig = null; 615 try { 616 resLocaleConfig = LocaleConfig.fromContextIgnoringOverride( 617 mContext.createPackageContext(appPackageName, 0)); 618 } catch (PackageManager.NameNotFoundException e) { 619 Slog.e(TAG, "Unknown package name " + appPackageName); 620 return; 621 } 622 final File file = getXmlFileNameForUser(appPackageName, userId); 623 624 if (overrideLocaleConfig == null) { 625 if (file.exists()) { 626 Slog.d(TAG, "remove the override LocaleConfig"); 627 file.delete(); 628 } 629 removeUnsupportedAppLocales(appPackageName, userId, resLocaleConfig, 630 FrameworkStatsLog 631 .APPLICATION_LOCALES_CHANGED__CALLER__CALLER_DYNAMIC_LOCALES_CHANGE 632 ); 633 atomRecord.setOverrideRemoved(true); 634 atomRecord.setStatus(FrameworkStatsLog 635 .APP_SUPPORTED_LOCALES_CHANGED__STATUS__SUCCESS); 636 return; 637 } else { 638 if (overrideLocaleConfig.isSameLocaleConfig( 639 getOverrideLocaleConfig(appPackageName, userId))) { 640 Slog.d(TAG, "the same override, ignore it"); 641 atomRecord.setSameAsPrevConfig(true); 642 return; 643 } 644 645 LocaleList localeList = overrideLocaleConfig.getSupportedLocales(); 646 // Normally the LocaleList object should not be null. However we reassign it as the 647 // empty list in case it happens. 648 if (localeList == null) { 649 localeList = LocaleList.getEmptyLocaleList(); 650 } 651 if (DEBUG) { 652 Slog.d(TAG, 653 "setOverrideLocaleConfig, localeList: " + localeList.toLanguageTags()); 654 } 655 atomRecord.setNumLocales(localeList.size()); 656 657 // Store the override LocaleConfig to the file storage. 658 final AtomicFile atomicFile = new AtomicFile(file); 659 FileOutputStream stream = null; 660 try { 661 stream = atomicFile.startWrite(); 662 stream.write(toXmlByteArray(localeList)); 663 } catch (Exception e) { 664 Slog.e(TAG, "Failed to write file " + atomicFile, e); 665 if (stream != null) { 666 atomicFile.failWrite(stream); 667 } 668 atomRecord.setStatus(FrameworkStatsLog 669 .APP_SUPPORTED_LOCALES_CHANGED__STATUS__FAILURE_WRITE_TO_STORAGE); 670 return; 671 } 672 atomicFile.finishWrite(stream); 673 // Clear per-app locales if they are not in the override LocaleConfig. 674 removeUnsupportedAppLocales(appPackageName, userId, overrideLocaleConfig, 675 FrameworkStatsLog 676 .APPLICATION_LOCALES_CHANGED__CALLER__CALLER_DYNAMIC_LOCALES_CHANGE 677 ); 678 if (overrideLocaleConfig.isSameLocaleConfig(resLocaleConfig)) { 679 Slog.d(TAG, "setOverrideLocaleConfig, same as the app's LocaleConfig"); 680 atomRecord.setSameAsResConfig(true); 681 } 682 atomRecord.setStatus(FrameworkStatsLog 683 .APP_SUPPORTED_LOCALES_CHANGED__STATUS__SUCCESS); 684 if (DEBUG) { 685 Slog.i(TAG, "Successfully written to " + atomicFile); 686 } 687 } 688 } 689 } 690 691 /** 692 * Checks if the per-app locales are in the LocaleConfig. Per-app locales missing from the 693 * LocaleConfig will be removed. 694 * 695 * <p><b>Note:</b> Check whether to remove the per-app locales when the app is upgraded or 696 * the LocaleConfig is overridden. 697 */ removeUnsupportedAppLocales(String appPackageName, int userId, LocaleConfig localeConfig, int caller)698 void removeUnsupportedAppLocales(String appPackageName, int userId, 699 LocaleConfig localeConfig, int caller) { 700 LocaleList appLocales = getApplicationLocalesUnchecked(appPackageName, userId); 701 // Remove the per-app locales from the locale list if they don't exist in the LocaleConfig. 702 boolean resetAppLocales = false; 703 List<Locale> newAppLocales = new ArrayList<Locale>(); 704 705 if (localeConfig == null) { 706 //Reset the app locales to the system default 707 Slog.i(TAG, "There is no LocaleConfig, reset app locales"); 708 resetAppLocales = true; 709 } else { 710 for (int i = 0; i < appLocales.size(); i++) { 711 if (!localeConfig.containsLocale(appLocales.get(i))) { 712 Slog.i(TAG, "Missing from the LocaleConfig, reset app locales"); 713 resetAppLocales = true; 714 continue; 715 } 716 newAppLocales.add(appLocales.get(i)); 717 } 718 } 719 720 if (resetAppLocales) { 721 // Reset the app locales 722 Locale[] locales = new Locale[newAppLocales.size()]; 723 try { 724 setApplicationLocales(appPackageName, userId, 725 new LocaleList(newAppLocales.toArray(locales)), 726 mBackupHelper.areLocalesSetFromDelegate(userId, appPackageName), caller); 727 } catch (RemoteException | IllegalArgumentException e) { 728 Slog.e(TAG, "Could not set locales for " + appPackageName, e); 729 } 730 } 731 } 732 enforceSetAppSpecificLocaleConfigPermission( AppSupportedLocalesChangedAtomRecord atomRecord)733 private void enforceSetAppSpecificLocaleConfigPermission( 734 AppSupportedLocalesChangedAtomRecord atomRecord) { 735 try { 736 mContext.enforceCallingOrSelfPermission( 737 android.Manifest.permission.SET_APP_SPECIFIC_LOCALECONFIG, 738 "setOverrideLocaleConfig"); 739 } catch (SecurityException e) { 740 atomRecord.setStatus(FrameworkStatsLog 741 .APP_SUPPORTED_LOCALES_CHANGED__STATUS__FAILURE_PERMISSION_ABSENT); 742 throw e; 743 } 744 } 745 746 /** 747 * Returns the override LocaleConfig for a specified app. 748 */ 749 @Nullable getOverrideLocaleConfig(@onNull String appPackageName, @UserIdInt int userId)750 public LocaleConfig getOverrideLocaleConfig(@NonNull String appPackageName, 751 @UserIdInt int userId) { 752 if (!SystemProperties.getBoolean(PROP_DYNAMIC_LOCALES_CHANGE, true)) { 753 return null; 754 } 755 756 requireNonNull(appPackageName); 757 758 // Allow apps with INTERACT_ACROSS_USERS permission to query the override LocaleConfig for 759 // different user. 760 userId = mActivityManagerInternal.handleIncomingUser( 761 Binder.getCallingPid(), Binder.getCallingUid(), userId, 762 false /* allowAll */, ActivityManagerInternal.ALLOW_NON_FULL, 763 "getOverrideLocaleConfig", /* callerPackage= */ null); 764 765 final File file = getXmlFileNameForUser(appPackageName, userId); 766 if (!file.exists()) { 767 if (DEBUG) { 768 Slog.i(TAG, "getOverrideLocaleConfig, the file is not existed."); 769 } 770 return null; 771 } 772 773 try (InputStream in = new FileInputStream(file)) { 774 final TypedXmlPullParser parser = Xml.resolvePullParser(in); 775 List<String> overrideLocales = loadFromXml(parser); 776 if (DEBUG) { 777 Slog.i(TAG, "getOverrideLocaleConfig, Loaded locales: " + overrideLocales); 778 } 779 LocaleConfig storedLocaleConfig = new LocaleConfig( 780 LocaleList.forLanguageTags(String.join(",", overrideLocales))); 781 782 return storedLocaleConfig; 783 } catch (IOException | XmlPullParserException e) { 784 Slog.e(TAG, "Failed to parse XML configuration from " + file, e); 785 } 786 787 return null; 788 } 789 790 /** 791 * Delete an override {@link LocaleConfig} for a specified app from the file storage. 792 * 793 * <p>Clear the override LocaleConfig from the storage when the app is uninstalled. 794 */ deleteOverrideLocaleConfig(@onNull String appPackageName, @UserIdInt int userId)795 void deleteOverrideLocaleConfig(@NonNull String appPackageName, @UserIdInt int userId) { 796 final File file = getXmlFileNameForUser(appPackageName, userId); 797 798 if (file.exists()) { 799 Slog.d(TAG, "Delete the override LocaleConfig."); 800 file.delete(); 801 } 802 } 803 toXmlByteArray(LocaleList localeList)804 private byte[] toXmlByteArray(LocaleList localeList) { 805 try (ByteArrayOutputStream os = new ByteArrayOutputStream()) { 806 TypedXmlSerializer out = Xml.newFastSerializer(); 807 out.setOutput(os, StandardCharsets.UTF_8.name()); 808 out.startDocument(/* encoding= */ null, /* standalone= */ true); 809 out.startTag(/* namespace= */ null, LocaleConfig.TAG_LOCALE_CONFIG); 810 811 List<String> locales = new ArrayList<String>( 812 Arrays.asList(localeList.toLanguageTags().split(","))); 813 for (String locale : locales) { 814 out.startTag(null, LocaleConfig.TAG_LOCALE); 815 out.attribute(null, ATTR_NAME, locale); 816 out.endTag(null, LocaleConfig.TAG_LOCALE); 817 } 818 819 out.endTag(/* namespace= */ null, LocaleConfig.TAG_LOCALE_CONFIG); 820 out.endDocument(); 821 822 if (DEBUG) { 823 Slog.d(TAG, "setOverrideLocaleConfig toXmlByteArray, output: " + os.toString()); 824 } 825 return os.toByteArray(); 826 } catch (IOException e) { 827 return null; 828 } 829 } 830 831 @NonNull loadFromXml(TypedXmlPullParser parser)832 private List<String> loadFromXml(TypedXmlPullParser parser) 833 throws IOException, XmlPullParserException { 834 List<String> localeList = new ArrayList<>(); 835 836 XmlUtils.beginDocument(parser, LocaleConfig.TAG_LOCALE_CONFIG); 837 int depth = parser.getDepth(); 838 while (XmlUtils.nextElementWithin(parser, depth)) { 839 final String tagName = parser.getName(); 840 if (LocaleConfig.TAG_LOCALE.equals(tagName)) { 841 String locale = parser.getAttributeValue(/* namespace= */ null, ATTR_NAME); 842 localeList.add(locale); 843 } else { 844 Slog.w(TAG, "Unexpected tag name: " + tagName); 845 XmlUtils.skipCurrentTag(parser); 846 } 847 } 848 849 return localeList; 850 } 851 852 @NonNull getXmlFileNameForUser(@onNull String appPackageName, @UserIdInt int userId)853 private File getXmlFileNameForUser(@NonNull String appPackageName, @UserIdInt int userId) { 854 final File dir = new File(Environment.getDataSystemCeDirectory(userId), LOCALE_CONFIGS); 855 return new File(dir, appPackageName + SUFFIX_FILE_NAME); 856 } 857 logAppSupportedLocalesChangedMetric( @onNull AppSupportedLocalesChangedAtomRecord atomRecord)858 private void logAppSupportedLocalesChangedMetric( 859 @NonNull AppSupportedLocalesChangedAtomRecord atomRecord) { 860 FrameworkStatsLog.write(FrameworkStatsLog.APP_SUPPORTED_LOCALES_CHANGED, 861 atomRecord.mCallingUid, 862 atomRecord.mTargetUid, 863 atomRecord.mNumLocales, 864 atomRecord.mOverrideRemoved, 865 atomRecord.mSameAsResConfig, 866 atomRecord.mSameAsPrevConfig, 867 atomRecord.mStatus); 868 } 869 } 870