From e5fffb8ecff347f6f7c40363d032b2345a38528f Mon Sep 17 00:00:00 2001 From: Nishant Kumar Singh Date: Mon, 25 Oct 2021 12:40:30 +0000 Subject: [PATCH] Add the backup logic for app-locales. Bug: 203266156 Test: atest FrameworksServicesTests:LocaleManagerBackupRestoreTest Change-Id: I38dae4f797b184d255479fca235de5af9a3bf3d1 --- .../locales/LocaleManagerBackupHelper.java | 151 ++++++++++++++++++ .../server/locales/LocaleManagerInternal.java | 31 ++++ .../server/locales/LocaleManagerService.java | 33 +++- 3 files changed, 214 insertions(+), 1 deletion(-) create mode 100644 services/core/java/com/android/server/locales/LocaleManagerBackupHelper.java create mode 100644 services/core/java/com/android/server/locales/LocaleManagerInternal.java diff --git a/services/core/java/com/android/server/locales/LocaleManagerBackupHelper.java b/services/core/java/com/android/server/locales/LocaleManagerBackupHelper.java new file mode 100644 index 0000000000000..6b5e7d73b9849 --- /dev/null +++ b/services/core/java/com/android/server/locales/LocaleManagerBackupHelper.java @@ -0,0 +1,151 @@ +/* + * Copyright (C) 2021 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 com.android.server.locales.LocaleManagerService.DEBUG; + +import android.annotation.NonNull; +import android.app.backup.BackupManager; +import android.content.pm.ApplicationInfo; +import android.content.pm.PackageManagerInternal; +import android.os.Binder; +import android.os.LocaleList; +import android.os.RemoteException; +import android.util.Slog; +import android.util.TypedXmlSerializer; +import android.util.Xml; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.OutputStream; +import java.io.UnsupportedEncodingException; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; + +/** + * Helper class for managing backup and restore of app-specific locales. + */ +final class LocaleManagerBackupHelper { + private static final String TAG = "LocaleManagerBkpHelper"; // must be < 23 chars + private final LocaleManagerService mLocaleManagerService; + private final PackageManagerInternal mPackageManagerInternal; + private static final String SYSTEM_BACKUP_PACKAGE_KEY = "android"; + + LocaleManagerBackupHelper(LocaleManagerService localeManagerService, + PackageManagerInternal pmInternal) { + mLocaleManagerService = localeManagerService; + mPackageManagerInternal = pmInternal; + } + + /** + * @see LocaleManagerInternal#getBackupPayload(int userId) + */ + public byte[] getBackupPayload(int userId) { + if (DEBUG) { + Slog.d(TAG, "getBackupPayload invoked for user id " + userId); + } + + ArrayList pkgStates = new ArrayList<>(); + for (ApplicationInfo appInfo : mPackageManagerInternal.getInstalledApplications(/*flags*/0, + userId, Binder.getCallingUid())) { + try { + LocaleList appLocales = mLocaleManagerService.getApplicationLocales( + appInfo.packageName, + userId); + // Backup locales only for apps which do have app-specific overrides. + if (!appLocales.isEmpty()) { + if (DEBUG) { + Slog.d(TAG, "Add package=" + appInfo.packageName + " locales=" + + appLocales.toLanguageTags() + " to backup payload"); + } + pkgStates.add(new BackupPackageState(appInfo.packageName, + appLocales.toLanguageTags())); + } + } catch (RemoteException | IllegalArgumentException e) { + Slog.e(TAG, "Exception when getting locales for package: " + appInfo.packageName, + e); + } + } + + if (pkgStates.isEmpty()) { + if (DEBUG) { + Slog.d(TAG, "Final payload=null"); + } + // Returning null here will ensure deletion of the entry for LMS from the backup data. + return null; + } + + final ByteArrayOutputStream out = new ByteArrayOutputStream(); + try { + writeToXml(out, pkgStates); + } catch (IOException e) { + Slog.e(TAG, "Could not write to xml for backup ", e); + return null; + } + + if (DEBUG) { + try { + Slog.d(TAG, "Final payload=" + out.toString("UTF-8")); + } catch (UnsupportedEncodingException e) { + Slog.w(TAG, "Could not encode payload to UTF-8", e); + } + } + return out.toByteArray(); + } + + /** + * Notifies the backup manager to include the "android" package in the next backup pass. + */ + public void notifyBackupManager() { + BackupManager.dataChanged(SYSTEM_BACKUP_PACKAGE_KEY); + } + + + /** + * Converts the list of app backup data into a serialized xml stream. + */ + private static void writeToXml(OutputStream stream, + @NonNull ArrayList pkgStates) throws IOException { + TypedXmlSerializer out = Xml.newFastSerializer(); + out.setOutput(stream, StandardCharsets.UTF_8.name()); + out.startDocument(/* encoding= */ null, /* standalone= */ true); + out.startTag(/* namespace= */ null, TAG); + + for (BackupPackageState pkgState : pkgStates) { + out.startTag(/* namespace= */ null, "package"); + out.attribute(/* namespace= */ null, "name", pkgState.mPackageName); + out.attribute(/* namespace= */ null, "locales", pkgState.mLanguageTags); + out.endTag(/*namespace= */ null, "package"); + } + + out.endTag(/* namespace= */ null, TAG); + out.endDocument(); + } + + /** + * Data structure containing the backup data details for an app. + */ + private static class BackupPackageState { + final @NonNull String mPackageName; + final @NonNull String mLanguageTags; + + BackupPackageState(@NonNull String packageNameArg, @NonNull String languageTagsArg) { + mPackageName = packageNameArg; + mLanguageTags = languageTagsArg; + } + } +} diff --git a/services/core/java/com/android/server/locales/LocaleManagerInternal.java b/services/core/java/com/android/server/locales/LocaleManagerInternal.java new file mode 100644 index 0000000000000..4b1fbe7f954c5 --- /dev/null +++ b/services/core/java/com/android/server/locales/LocaleManagerInternal.java @@ -0,0 +1,31 @@ +/* + * Copyright (C) 2021 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 android.annotation.Nullable; + +/** + * System-server internal interface to the {@link LocaleManagerService}. + * + * @hide Only for use within the system server. + */ +public abstract class LocaleManagerInternal { + /** + * Returns the app-specific locales to be backed up as a data-blob. + */ + public abstract @Nullable byte[] getBackupPayload(int userId); +} diff --git a/services/core/java/com/android/server/locales/LocaleManagerService.java b/services/core/java/com/android/server/locales/LocaleManagerService.java index 457c2fd6fc419..6d029f1a9fd4e 100644 --- a/services/core/java/com/android/server/locales/LocaleManagerService.java +++ b/services/core/java/com/android/server/locales/LocaleManagerService.java @@ -20,6 +20,7 @@ import static java.util.Objects.requireNonNull; import android.Manifest; import android.annotation.NonNull; +import android.annotation.Nullable; import android.annotation.UserIdInt; import android.app.ActivityManagerInternal; import android.app.ILocaleManager; @@ -29,6 +30,7 @@ import android.content.pm.PackageManager; import android.content.pm.PackageManagerInternal; import android.os.Binder; import android.os.LocaleList; +import android.os.Process; import android.os.RemoteException; import android.os.ResultReceiver; import android.os.ShellCallback; @@ -51,11 +53,14 @@ import java.io.PrintWriter; */ public class LocaleManagerService extends SystemService { private static final String TAG = "LocaleManagerService"; - private final Context mContext; + final Context mContext; private final LocaleManagerService.LocaleManagerBinderService mBinderService; private ActivityTaskManagerInternal mActivityTaskManagerInternal; private ActivityManagerInternal mActivityManagerInternal; private PackageManagerInternal mPackageManagerInternal; + + private LocaleManagerBackupHelper mBackupHelper; + public static final boolean DEBUG = false; public LocaleManagerService(Context context) { @@ -82,6 +87,24 @@ public class LocaleManagerService extends SystemService { @Override public void onStart() { publishBinderService(Context.LOCALE_SERVICE, mBinderService); + mBackupHelper = new LocaleManagerBackupHelper(LocaleManagerService.this, + mPackageManagerInternal); + LocalServices.addService(LocaleManagerInternal.class, new LocaleManagerInternalImpl()); + } + + private final class LocaleManagerInternalImpl extends LocaleManagerInternal { + + @Override + public @Nullable byte[] getBackupPayload(int userId) { + checkCallerIsSystem(); + return mBackupHelper.getBackupPayload(userId); + } + + private void checkCallerIsSystem() { + if (Binder.getCallingUid() != Process.SYSTEM_UID) { + throw new SecurityException("Caller is not system."); + } + } } private final class LocaleManagerBinderService extends ILocaleManager.Stub { @@ -110,6 +133,12 @@ public class LocaleManagerService extends SystemService { (new LocaleManagerShellCommand(mBinderService)) .exec(this, in, out, err, args, callback, resultReceiver); } + + } + + @VisibleForTesting + LocaleManagerBackupHelper getBackupHelper() { + return mBackupHelper; } /** @@ -161,6 +190,8 @@ public class LocaleManagerService extends SystemService { notifyAppWhoseLocaleChanged(appPackageName, userId, locales); notifyInstallerOfAppWhoseLocaleChanged(appPackageName, userId, locales); notifyRegisteredReceivers(appPackageName, userId, locales); + + mBackupHelper.notifyBackupManager(); } }