diff --git a/core/java/android/app/LocaleConfig.java b/core/java/android/app/LocaleConfig.java
index 69693fc3977b2..f4cd60d088045 100644
--- a/core/java/android/app/LocaleConfig.java
+++ b/core/java/android/app/LocaleConfig.java
@@ -38,7 +38,10 @@ import org.xmlpull.v1.XmlPullParserException;
import java.io.IOException;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
+import java.util.Arrays;
+import java.util.Collections;
import java.util.HashSet;
+import java.util.List;
import java.util.Locale;
import java.util.Set;
@@ -262,6 +265,43 @@ public class LocaleConfig implements Parcelable {
}
};
+ /**
+ * Compare whether the LocaleConfig is the same.
+ *
+ *
If the elements of {@code mLocales} in LocaleConfig are the same but arranged in different
+ * positions, they are also considered to be the same LocaleConfig.
+ *
+ * @param other The {@link LocaleConfig} to compare for.
+ *
+ * @return true if the LocaleConfig is the same, false otherwise.
+ *
+ * @hide
+ */
+ public boolean isSameLocaleConfig(@Nullable LocaleConfig other) {
+ if (other == this) {
+ return true;
+ }
+
+ if (other != null) {
+ if (mStatus != other.mStatus) {
+ return false;
+ }
+ LocaleList otherLocales = other.mLocales;
+ if (mLocales == null && otherLocales == null) {
+ return true;
+ } else if (mLocales != null && otherLocales != null) {
+ List hostStrList = Arrays.asList(mLocales.toLanguageTags().split(","));
+ List targetStrList = Arrays.asList(
+ otherLocales.toLanguageTags().split(","));
+ Collections.sort(hostStrList);
+ Collections.sort(targetStrList);
+ return hostStrList.equals(targetStrList);
+ }
+ }
+
+ return false;
+ }
+
/**
* Compare whether the locale is existed in the {@code mLocales} of the LocaleConfig.
*
diff --git a/services/core/java/com/android/server/locales/AppSupportedLocalesChangedAtomRecord.java b/services/core/java/com/android/server/locales/AppSupportedLocalesChangedAtomRecord.java
new file mode 100644
index 0000000000000..de429f008fcf3
--- /dev/null
+++ b/services/core/java/com/android/server/locales/AppSupportedLocalesChangedAtomRecord.java
@@ -0,0 +1,72 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.locales;
+
+import static android.os.Process.INVALID_UID;
+
+import com.android.internal.util.FrameworkStatsLog;
+
+/**
+ * Holds data used to report the AppSupportedLocalesChanged atom.
+ */
+public final class AppSupportedLocalesChangedAtomRecord {
+ // The uid which invoked this update.
+ final int mCallingUid;
+ // The uid for which the override of app’s supported locales change is being done.
+ int mTargetUid = INVALID_UID;
+ // The total number of locales in the override LocaleConfig.
+ int mNumLocales = -1;
+ // Whether the override is removed LocaleConfig from the storage.
+ boolean mOverrideRemoved = false;
+ // Whether the new override LocaleConfig is the same as the app’s LocaleConfig.
+ boolean mSameAsResConfig = false;
+ // Whether the new override LocaleConfig is the same as the previously effective one. This means
+ // a comparison with the previous override LocaleConfig if there was one, and a comparison with
+ // the resource LocaleConfig if no override was present.
+ boolean mSameAsPrevConfig = false;
+ // Application supported locales changed status.
+ int mStatus = FrameworkStatsLog
+ .APP_SUPPORTED_LOCALES_CHANGED__STATUS__STATUS_UNSPECIFIED;
+
+ AppSupportedLocalesChangedAtomRecord(int callingUid) {
+ this.mCallingUid = callingUid;
+ }
+
+ void setTargetUid(int targetUid) {
+ this.mTargetUid = targetUid;
+ }
+
+ void setNumLocales(int numLocales) {
+ this.mNumLocales = numLocales;
+ }
+
+ void setOverrideRemoved(boolean overrideRemoved) {
+ this.mOverrideRemoved = overrideRemoved;
+ }
+
+ void setSameAsResConfig(boolean sameAsResConfig) {
+ this.mSameAsResConfig = sameAsResConfig;
+ }
+
+ void setSameAsPrevConfig(boolean sameAsPrevConfig) {
+ this.mSameAsPrevConfig = sameAsPrevConfig;
+ }
+
+ void setStatus(int status) {
+ this.mStatus = status;
+ }
+}
diff --git a/services/core/java/com/android/server/locales/LocaleManagerService.java b/services/core/java/com/android/server/locales/LocaleManagerService.java
index 99d30f54ce217..e5f589718deb9 100644
--- a/services/core/java/com/android/server/locales/LocaleManagerService.java
+++ b/services/core/java/com/android/server/locales/LocaleManagerService.java
@@ -250,7 +250,7 @@ public class LocaleManagerService extends SystemService {
// set locales if the package name is owned by the app. Next, check if the caller has
// the necessary permission and set locales.
boolean isCallerOwner = isPackageOwnedByCaller(appPackageName, userId,
- atomRecordForMetrics);
+ atomRecordForMetrics, null);
if (!isCallerOwner) {
enforceChangeConfigurationPermission(atomRecordForMetrics);
}
@@ -264,7 +264,7 @@ public class LocaleManagerService extends SystemService {
Binder.restoreCallingIdentity(token);
}
} finally {
- logMetric(atomRecordForMetrics);
+ logAppLocalesMetric(atomRecordForMetrics);
}
}
@@ -354,33 +354,31 @@ public class LocaleManagerService extends SystemService {
| Intent.FLAG_RECEIVER_FOREGROUND);
}
- /**
- * Same as {@link LocaleManagerService#isPackageOwnedByCaller(String, int,
- * AppLocaleChangedAtomRecord)}, but for methods that do not log locale atom.
- */
- private boolean isPackageOwnedByCaller(String appPackageName, int userId) {
- return isPackageOwnedByCaller(appPackageName, userId, /* atomRecordForMetrics= */null);
- }
-
/**
* Checks if the package is owned by the calling app or not for the given user id.
*
* @throws IllegalArgumentException if package not found for given userid
*/
private boolean isPackageOwnedByCaller(String appPackageName, int userId,
- @Nullable AppLocaleChangedAtomRecord atomRecordForMetrics) {
+ @Nullable AppLocaleChangedAtomRecord atomRecordForMetrics,
+ @Nullable AppSupportedLocalesChangedAtomRecord appSupportedLocalesChangedAtomRecord) {
final int uid = getPackageUid(appPackageName, userId);
if (uid < 0) {
Slog.w(TAG, "Unknown package " + appPackageName + " for user " + userId);
if (atomRecordForMetrics != null) {
atomRecordForMetrics.setStatus(FrameworkStatsLog
.APPLICATION_LOCALES_CHANGED__STATUS__FAILURE_INVALID_TARGET_PACKAGE);
+ } else if (appSupportedLocalesChangedAtomRecord != null) {
+ appSupportedLocalesChangedAtomRecord.setStatus(FrameworkStatsLog
+ .APP_SUPPORTED_LOCALES_CHANGED__STATUS__FAILURE_INVALID_TARGET_PACKAGE);
}
throw new IllegalArgumentException("Unknown package: " + appPackageName
+ " for user " + userId);
}
if (atomRecordForMetrics != null) {
atomRecordForMetrics.setTargetUid(uid);
+ } else if (appSupportedLocalesChangedAtomRecord != null) {
+ appSupportedLocalesChangedAtomRecord.setTargetUid(uid);
}
//Once valid package found, ignore the userId part for validating package ownership
//as apps with INTERACT_ACROSS_USERS permission could be changing locale for different user.
@@ -425,7 +423,7 @@ public class LocaleManagerService extends SystemService {
// current input method, and that app is querying locales of the current foreground app. If
// neither conditions matched, check if the caller has the necessary permission and fetch
// locales.
- if (!isPackageOwnedByCaller(appPackageName, userId)
+ if (!isPackageOwnedByCaller(appPackageName, userId, null, null)
&& !isCallerInstaller(appPackageName, userId)
&& !(isCallerFromCurrentInputMethod(userId)
&& mActivityManagerInternal.isAppForeground(
@@ -550,7 +548,7 @@ public class LocaleManagerService extends SystemService {
return systemLocales;
}
- private void logMetric(@NonNull AppLocaleChangedAtomRecord atomRecordForMetrics) {
+ private void logAppLocalesMetric(@NonNull AppLocaleChangedAtomRecord atomRecordForMetrics) {
FrameworkStatsLog.write(FrameworkStatsLog.APPLICATION_LOCALES_CHANGED,
atomRecordForMetrics.mCallingUid,
atomRecordForMetrics.mTargetUid,
@@ -569,33 +567,39 @@ public class LocaleManagerService extends SystemService {
return;
}
- requireNonNull(appPackageName);
-
- //Allow apps with INTERACT_ACROSS_USERS permission to set locales for different user.
- userId = mActivityManagerInternal.handleIncomingUser(
- Binder.getCallingPid(), Binder.getCallingUid(), userId,
- false /* allowAll */, ActivityManagerInternal.ALLOW_NON_FULL,
- "setOverrideLocaleConfig", /* callerPackage= */ null);
-
- // This function handles two types of set operations:
- // 1.) A normal, an app overrides its own LocaleConfig.
- // 2.) A privileged system application or service is granted the necessary permission to
- // override a LocaleConfig of another package.
- if (!isPackageOwnedByCaller(appPackageName, userId)) {
- enforceSetAppSpecificLocaleConfigPermission();
- }
-
- final long token = Binder.clearCallingIdentity();
+ AppSupportedLocalesChangedAtomRecord atomRecord = new AppSupportedLocalesChangedAtomRecord(
+ Binder.getCallingUid());
try {
- setOverrideLocaleConfigUnchecked(appPackageName, userId, localeConfig);
- } finally {
- Binder.restoreCallingIdentity(token);
- }
+ requireNonNull(appPackageName);
- //TODO: Add metrics to monitor the usage by applications
+ //Allow apps with INTERACT_ACROSS_USERS permission to set locales for different user.
+ userId = mActivityManagerInternal.handleIncomingUser(
+ Binder.getCallingPid(), Binder.getCallingUid(), userId,
+ false /* allowAll */, ActivityManagerInternal.ALLOW_NON_FULL,
+ "setOverrideLocaleConfig", /* callerPackage= */ null);
+
+ // This function handles two types of set operations:
+ // 1.) A normal, an app overrides its own LocaleConfig.
+ // 2.) A privileged system application or service is granted the necessary permission to
+ // override a LocaleConfig of another package.
+ if (!isPackageOwnedByCaller(appPackageName, userId, null, atomRecord)) {
+ enforceSetAppSpecificLocaleConfigPermission(atomRecord);
+ }
+
+ final long token = Binder.clearCallingIdentity();
+ try {
+ setOverrideLocaleConfigUnchecked(appPackageName, userId, localeConfig, atomRecord);
+ } finally {
+ Binder.restoreCallingIdentity(token);
+ }
+ } finally {
+ logAppSupportedLocalesChangedMetric(atomRecord);
+ }
}
+
private void setOverrideLocaleConfigUnchecked(@NonNull String appPackageName,
- @UserIdInt int userId, @Nullable LocaleConfig overridelocaleConfig) {
+ @UserIdInt int userId, @Nullable LocaleConfig overridelocaleConfig,
+ @NonNull AppSupportedLocalesChangedAtomRecord atomRecord) {
synchronized (mWriteLock) {
if (DEBUG) {
Slog.d(TAG,
@@ -609,8 +613,18 @@ public class LocaleManagerService extends SystemService {
Slog.d(TAG, "remove the override LocaleConfig");
file.delete();
}
+ atomRecord.setOverrideRemoved(true);
+ atomRecord.setStatus(FrameworkStatsLog
+ .APP_SUPPORTED_LOCALES_CHANGED__STATUS__SUCCESS);
return;
} else {
+ if (overridelocaleConfig.isSameLocaleConfig(
+ getOverrideLocaleConfig(appPackageName, userId))) {
+ Slog.d(TAG, "the same override, ignore it");
+ atomRecord.setSameAsPrevConfig(true);
+ return;
+ }
+
LocaleList localeList = overridelocaleConfig.getSupportedLocales();
// Normally the LocaleList object should not be null. However we reassign it as the
// empty list in case it happens.
@@ -621,6 +635,7 @@ public class LocaleManagerService extends SystemService {
Slog.d(TAG,
"setOverrideLocaleConfig, localeList: " + localeList.toLanguageTags());
}
+ atomRecord.setNumLocales(localeList.size());
// Store the override LocaleConfig to the file storage.
final AtomicFile atomicFile = new AtomicFile(file);
@@ -633,11 +648,25 @@ public class LocaleManagerService extends SystemService {
if (stream != null) {
atomicFile.failWrite(stream);
}
+ atomRecord.setStatus(FrameworkStatsLog
+ .APP_SUPPORTED_LOCALES_CHANGED__STATUS__FAILURE_WRITE_TO_STORAGE);
return;
}
atomicFile.finishWrite(stream);
// Clear per-app locales if they are not in the override LocaleConfig.
removeUnsupportedAppLocales(appPackageName, userId, overridelocaleConfig);
+ try {
+ Context appContext = mContext.createPackageContext(appPackageName, 0);
+ if (overridelocaleConfig.isSameLocaleConfig(
+ LocaleConfig.fromContextIgnoringOverride(appContext))) {
+ Slog.d(TAG, "setOverrideLocaleConfig, same as the app's LocaleConfig");
+ atomRecord.setSameAsResConfig(true);
+ }
+ } catch (PackageManager.NameNotFoundException e) {
+ Slog.e(TAG, "Unknown package name " + appPackageName);
+ }
+ atomRecord.setStatus(FrameworkStatsLog
+ .APP_SUPPORTED_LOCALES_CHANGED__STATUS__SUCCESS);
if (DEBUG) {
Slog.i(TAG, "Successfully written to " + atomicFile);
}
@@ -677,10 +706,17 @@ public class LocaleManagerService extends SystemService {
}
}
- private void enforceSetAppSpecificLocaleConfigPermission() {
- mContext.enforceCallingOrSelfPermission(
- android.Manifest.permission.SET_APP_SPECIFIC_LOCALECONFIG,
- "setOverrideLocaleConfig");
+ private void enforceSetAppSpecificLocaleConfigPermission(
+ AppSupportedLocalesChangedAtomRecord atomRecord) {
+ try {
+ mContext.enforceCallingOrSelfPermission(
+ android.Manifest.permission.SET_APP_SPECIFIC_LOCALECONFIG,
+ "setOverrideLocaleConfig");
+ } catch (SecurityException e) {
+ atomRecord.setStatus(FrameworkStatsLog
+ .APP_SUPPORTED_LOCALES_CHANGED__STATUS__FAILURE_PERMISSION_ABSENT);
+ throw e;
+ }
}
/**
@@ -796,4 +832,16 @@ public class LocaleManagerService extends SystemService {
final File dir = new File(Environment.getDataSystemDeDirectory(userId), LOCALE_CONFIGS);
return new File(dir, appPackageName + SUFFIX_FILE_NAME);
}
+
+ private void logAppSupportedLocalesChangedMetric(
+ @NonNull AppSupportedLocalesChangedAtomRecord atomRecord) {
+ FrameworkStatsLog.write(FrameworkStatsLog.APP_SUPPORTED_LOCALES_CHANGED,
+ atomRecord.mCallingUid,
+ atomRecord.mTargetUid,
+ atomRecord.mNumLocales,
+ atomRecord.mOverrideRemoved,
+ atomRecord.mSameAsResConfig,
+ atomRecord.mSameAsPrevConfig,
+ atomRecord.mStatus);
+ }
}