Merge "Support B&R in gender"

This commit is contained in:
Calvin Pan
2023-01-17 21:27:22 +00:00
committed by Android (Google) Code Review
7 changed files with 530 additions and 2 deletions

View File

@@ -0,0 +1,66 @@
/*
* Copyright (C) 2022 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.backup;
import static android.app.backup.BackupAgent.FLAG_CLIENT_SIDE_ENCRYPTION_ENABLED;
import android.annotation.UserIdInt;
import android.app.backup.BackupDataOutput;
import android.app.backup.BlobBackupHelper;
import android.os.ParcelFileDescriptor;
import com.android.server.LocalServices;
import com.android.server.grammaticalinflection.GrammaticalInflectionManagerInternal;
public class AppGrammaticalGenderBackupHelper extends BlobBackupHelper {
private static final int BLOB_VERSION = 1;
private static final String KEY_APP_GENDER = "app_gender";
private final @UserIdInt int mUserId;
private final GrammaticalInflectionManagerInternal mGrammarInflectionManagerInternal;
public AppGrammaticalGenderBackupHelper(int userId) {
super(BLOB_VERSION, KEY_APP_GENDER);
mUserId = userId;
mGrammarInflectionManagerInternal = LocalServices.getService(
GrammaticalInflectionManagerInternal.class);
}
@Override
public void performBackup(ParcelFileDescriptor oldStateFd, BackupDataOutput data,
ParcelFileDescriptor newStateFd) {
// Only backup the gender data if e2e encryption is present
if ((data.getTransportFlags() & FLAG_CLIENT_SIDE_ENCRYPTION_ENABLED) == 0) {
return;
}
super.performBackup(oldStateFd, data, newStateFd);
}
@Override
protected byte[] getBackupPayload(String key) {
return KEY_APP_GENDER.equals(key) && mGrammarInflectionManagerInternal != null ?
mGrammarInflectionManagerInternal.getBackupPayload(mUserId) : null;
}
@Override
protected void applyRestoredPayload(String key, byte[] payload) {
if (KEY_APP_GENDER.equals(key) && mGrammarInflectionManagerInternal != null) {
mGrammarInflectionManagerInternal.stageAndApplyRestoredPayload(payload, mUserId);
}
}
}

View File

@@ -58,6 +58,7 @@ public class SystemBackupAgent extends BackupAgentHelper {
private static final String SLICES_HELPER = "slices";
private static final String PEOPLE_HELPER = "people";
private static final String APP_LOCALES_HELPER = "app_locales";
private static final String APP_GENDER_HELPER = "app_gender";
// These paths must match what the WallpaperManagerService uses. The leaf *_FILENAME
// are also used in the full-backup file format, so must not change unless steps are
@@ -105,6 +106,7 @@ public class SystemBackupAgent extends BackupAgentHelper {
addHelper(SLICES_HELPER, new SliceBackupHelper(this));
addHelper(PEOPLE_HELPER, new PeopleBackupHelper(mUserId));
addHelper(APP_LOCALES_HELPER, new AppSpecificLocalesBackupHelper(mUserId));
addHelper(APP_GENDER_HELPER, new AppGrammaticalGenderBackupHelper(mUserId));
}
@Override

View File

@@ -0,0 +1,193 @@
/*
* Copyright (C) 2022 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.grammaticalinflection;
import android.app.backup.BackupManager;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.content.res.Configuration;
import android.os.UserHandle;
import android.util.Log;
import android.util.SparseArray;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.time.Clock;
import java.time.Duration;
import java.util.HashMap;
import java.util.Map;
public class GrammaticalInflectionBackupHelper {
private static final String TAG = GrammaticalInflectionBackupHelper.class.getSimpleName();
private static final String SYSTEM_BACKUP_PACKAGE_KEY = "android";
// Stage data would be deleted on reboot since it's stored in memory. So it's retained until
// retention period OR next reboot, whichever happens earlier.
private static final Duration STAGE_DATA_RETENTION_PERIOD = Duration.ofDays(3);
private final SparseArray<StagedData> mCache = new SparseArray<>();
private final Object mCacheLock = new Object();
private final PackageManager mPackageManager;
private final GrammaticalInflectionService mGrammaticalGenderService;
private final Clock mClock;
static class StagedData {
final long mCreationTimeMillis;
final HashMap<String, Integer> mPackageStates;
StagedData(long creationTimeMillis) {
mCreationTimeMillis = creationTimeMillis;
mPackageStates = new HashMap<>();
}
}
public GrammaticalInflectionBackupHelper(GrammaticalInflectionService grammaticalGenderService,
PackageManager packageManager) {
mGrammaticalGenderService = grammaticalGenderService;
mPackageManager = packageManager;
mClock = Clock.systemUTC();
}
public byte[] getBackupPayload(int userId) {
synchronized (mCacheLock) {
cleanStagedDataForOldEntries();
}
HashMap<String, Integer> pkgGenderInfo = new HashMap<>();
for (ApplicationInfo appInfo : mPackageManager.getInstalledApplicationsAsUser(
PackageManager.ApplicationInfoFlags.of(0), userId)) {
int gender = mGrammaticalGenderService.getApplicationGrammaticalGender(
appInfo.packageName, userId);
if (gender != Configuration.GRAMMATICAL_GENDER_NOT_SPECIFIED) {
pkgGenderInfo.put(appInfo.packageName, gender);
}
}
if (!pkgGenderInfo.isEmpty()) {
return convertToByteArray(pkgGenderInfo);
} else {
return null;
}
}
public void stageAndApplyRestoredPayload(byte[] payload, int userId) {
synchronized (mCacheLock) {
cleanStagedDataForOldEntries();
HashMap<String, Integer> pkgInfo = readFromByteArray(payload);
if (pkgInfo.isEmpty()) {
return;
}
StagedData stagedData = new StagedData(mClock.millis());
for (Map.Entry<String, Integer> info : pkgInfo.entrySet()) {
// If app installed, restore immediately, otherwise put it in cache.
if (isPackageInstalledForUser(info.getKey(), userId)) {
if (!hasSetBeforeRestoring(info.getKey(), userId)) {
mGrammaticalGenderService.setRequestedApplicationGrammaticalGender(
info.getKey(), userId, info.getValue());
}
} else {
if (info.getValue() != Configuration.GRAMMATICAL_GENDER_NOT_SPECIFIED) {
stagedData.mPackageStates.put(info.getKey(), info.getValue());
}
}
}
mCache.append(userId, stagedData);
}
}
private boolean hasSetBeforeRestoring(String pkgName, int userId) {
return mGrammaticalGenderService.getApplicationGrammaticalGender(pkgName, userId)
!= Configuration.GRAMMATICAL_GENDER_NOT_SPECIFIED;
}
public void onPackageAdded(String packageName, int uid) {
synchronized (mCacheLock) {
int userId = UserHandle.getUserId(uid);
StagedData cache = mCache.get(userId);
if (cache != null && cache.mPackageStates.containsKey(packageName)) {
int grammaticalGender = cache.mPackageStates.get(packageName);
if (grammaticalGender != Configuration.GRAMMATICAL_GENDER_NOT_SPECIFIED) {
mGrammaticalGenderService.setRequestedApplicationGrammaticalGender(
packageName, userId, grammaticalGender);
}
}
}
}
public void onPackageDataCleared() {
notifyBackupManager();
}
public void onPackageRemoved() {
notifyBackupManager();
}
public static void notifyBackupManager() {
BackupManager.dataChanged(SYSTEM_BACKUP_PACKAGE_KEY);
}
private byte[] convertToByteArray(HashMap<String, Integer> pkgGenderInfo) {
try (final ByteArrayOutputStream out = new ByteArrayOutputStream();
final ObjectOutputStream objStream = new ObjectOutputStream(out)) {
objStream.writeObject(pkgGenderInfo);
return out.toByteArray();
} catch (IOException e) {
Log.e(TAG, "cannot convert payload to byte array.", e);
return null;
}
}
private HashMap<String, Integer> readFromByteArray(byte[] payload) {
HashMap<String, Integer> data = new HashMap<>();
try (ByteArrayInputStream byteIn = new ByteArrayInputStream(payload);
ObjectInputStream in = new ObjectInputStream(byteIn)) {
data = (HashMap<String, Integer>) in.readObject();
} catch (IOException | ClassNotFoundException e) {
Log.e(TAG, "cannot convert payload to HashMap.", e);
e.printStackTrace();
}
return data;
}
private void cleanStagedDataForOldEntries() {
for (int i = 0; i < mCache.size(); i++) {
int userId = mCache.keyAt(i);
StagedData stagedData = mCache.get(userId);
if (stagedData.mCreationTimeMillis
< mClock.millis() - STAGE_DATA_RETENTION_PERIOD.toMillis()) {
mCache.remove(userId);
}
}
}
private boolean isPackageInstalledForUser(String packageName, int userId) {
PackageInfo pkgInfo = null;
try {
pkgInfo = mPackageManager.getPackageInfoAsUser(packageName, /* flags= */ 0, userId);
} catch (PackageManager.NameNotFoundException e) {
// The package is not installed
}
return pkgInfo != null;
}
}

View File

@@ -0,0 +1,41 @@
/*
* Copyright (C) 2022 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.grammaticalinflection;
import android.annotation.Nullable;
/**
* System-server internal interface to the {@link android.app.GrammaticalInflectionManager}.
*
* @hide Only for use within the system server.
*/
public abstract class GrammaticalInflectionManagerInternal {
/**
* Returns the app-gender to be backed up as a data-blob.
*/
public abstract @Nullable byte[] getBackupPayload(int userId);
/**
* Restores the app-gender that were previously backed up.
*
* <p>This method will parse the input data blob and restore the gender for apps which are
* present on the device. It will stage the gender data for the apps which are not installed
* at the time this is called, to be referenced later when the app is installed.
*/
public abstract void stageAndApplyRestoredPayload(byte[] payload, int userId);
}

View File

@@ -0,0 +1,42 @@
/*
* Copyright (C) 2022 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.grammaticalinflection;
import com.android.internal.content.PackageMonitor;
public class GrammaticalInflectionPackageMonitor extends PackageMonitor {
private GrammaticalInflectionBackupHelper mBackupHelper;
GrammaticalInflectionPackageMonitor(GrammaticalInflectionBackupHelper backupHelper) {
mBackupHelper = backupHelper;
}
@Override
public void onPackageAdded(String packageName, int uid) {
mBackupHelper.onPackageAdded(packageName, uid);
}
@Override
public void onPackageDataCleared(String packageName, int uid) {
mBackupHelper.onPackageDataCleared();
}
@Override
public void onPackageRemoved(String packageName, int uid) {
mBackupHelper.onPackageRemoved();
}
}

View File

@@ -18,9 +18,12 @@ package com.android.server.grammaticalinflection;
import static android.content.res.Configuration.GRAMMATICAL_GENDER_NOT_SPECIFIED;
import android.annotation.Nullable;
import android.app.IGrammaticalInflectionManager;
import android.content.Context;
import android.os.Binder;
import android.os.IBinder;
import android.os.Process;
import android.os.SystemProperties;
import com.android.server.LocalServices;
@@ -34,6 +37,7 @@ import com.android.server.wm.ActivityTaskManagerInternal;
*/
public class GrammaticalInflectionService extends SystemService {
private final GrammaticalInflectionBackupHelper mBackupHelper;
private final ActivityTaskManagerInternal mActivityTaskManagerInternal;
private static final String GRAMMATICAL_INFLECTION_ENABLED =
"i18n.grammatical_Inflection.enabled";
@@ -46,17 +50,20 @@ public class GrammaticalInflectionService extends SystemService {
* </p>
*
* @param context The system server context.
*
* @hide
*/
public GrammaticalInflectionService(Context context) {
super(context);
mActivityTaskManagerInternal = LocalServices.getService(ActivityTaskManagerInternal.class);
mBackupHelper = new GrammaticalInflectionBackupHelper(
this, context.getPackageManager());
}
@Override
public void onStart() {
publishBinderService(Context.GRAMMATICAL_INFLECTION_SERVICE, mService);
LocalServices.addService(GrammaticalInflectionManagerInternal.class,
new GrammaticalInflectionManagerInternalImpl());
}
private final IBinder mService = new IGrammaticalInflectionManager.Stub() {
@@ -68,7 +75,40 @@ public class GrammaticalInflectionService extends SystemService {
}
};
private void setRequestedApplicationGrammaticalGender(
private final class GrammaticalInflectionManagerInternalImpl
extends GrammaticalInflectionManagerInternal {
@Override
@Nullable
public byte[] getBackupPayload(int userId) {
checkCallerIsSystem();
return mBackupHelper.getBackupPayload(userId);
}
@Override
public void stageAndApplyRestoredPayload(byte[] payload, int userId) {
mBackupHelper.stageAndApplyRestoredPayload(payload, userId);
}
private void checkCallerIsSystem() {
if (Binder.getCallingUid() != Process.SYSTEM_UID) {
throw new SecurityException("Caller is not system.");
}
}
}
protected int getApplicationGrammaticalGender(String appPackageName, int userId) {
final ActivityTaskManagerInternal.PackageConfig appConfig =
mActivityTaskManagerInternal.getApplicationConfig(appPackageName, userId);
if (appConfig == null || appConfig.mGrammaticalGender == null) {
return GRAMMATICAL_GENDER_NOT_SPECIFIED;
} else {
return appConfig.mGrammaticalGender;
}
}
protected void setRequestedApplicationGrammaticalGender(
String appPackageName, int userId, int gender) {
if (!SystemProperties.getBoolean(GRAMMATICAL_INFLECTION_ENABLED, true)) {
return;

View File

@@ -0,0 +1,144 @@
/*
* Copyright (C) 2022 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.grammaticalinflection;
import static junit.framework.Assert.assertNull;
import static junit.framework.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.verify;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageManager;
import android.content.res.Configuration;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import com.google.common.collect.Maps;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.junit.MockitoJUnit;
import org.mockito.junit.MockitoRule;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.util.HashMap;
import java.util.List;
@RunWith(AndroidJUnit4.class)
public class GrammaticalInflectionBackupTest {
private static final int DEFAULT_USER_ID = 0;
private static final String DEFAULT_PACKAGE_NAME = "com.test.package.name";
@Rule
public final MockitoRule mockito = MockitoJUnit.rule();
@Mock
private PackageManager mMockPackageManager;
@Mock
private GrammaticalInflectionService mGrammaticalInflectionService;
private GrammaticalInflectionBackupHelper mBackupHelper;
@Before
public void setUp() throws Exception {
mBackupHelper = new GrammaticalInflectionBackupHelper(
mGrammaticalInflectionService, mMockPackageManager);
}
@Test
public void testBackupPayload_noAppsInstalled_returnsNull() {
assertNull(mBackupHelper.getBackupPayload(DEFAULT_USER_ID));
}
@Test
public void testBackupPayload_AppsInstalled_returnsGender()
throws IOException, ClassNotFoundException {
mockAppInstalled();
mockGetApplicationGrammaticalGender(Configuration.GRAMMATICAL_GENDER_MASCULINE);
HashMap<String, Integer> payload =
readFromByteArray(mBackupHelper.getBackupPayload(DEFAULT_USER_ID));
// verify the payload
HashMap<String, Integer> expectationMap = new HashMap<>();
expectationMap.put(DEFAULT_PACKAGE_NAME, Configuration.GRAMMATICAL_GENDER_MASCULINE);
assertTrue(Maps.difference(payload, expectationMap).areEqual());
}
@Test
public void testApplyPayload_onPackageAdded_setApplicationGrammaticalGender()
throws IOException {
mockAppInstalled();
HashMap<String, Integer> testData = new HashMap<>();
testData.put(DEFAULT_PACKAGE_NAME, Configuration.GRAMMATICAL_GENDER_NEUTRAL);
mBackupHelper.stageAndApplyRestoredPayload(convertToByteArray(testData), DEFAULT_USER_ID);
mBackupHelper.onPackageAdded(DEFAULT_PACKAGE_NAME, DEFAULT_USER_ID);
verify(mGrammaticalInflectionService).setRequestedApplicationGrammaticalGender(
eq(DEFAULT_PACKAGE_NAME),
eq(DEFAULT_USER_ID),
eq(Configuration.GRAMMATICAL_GENDER_NEUTRAL));
}
private void mockAppInstalled() {
ApplicationInfo dummyApp = new ApplicationInfo();
dummyApp.packageName = DEFAULT_PACKAGE_NAME;
doReturn(List.of(dummyApp)).when(mMockPackageManager)
.getInstalledApplicationsAsUser(any(), anyInt());
}
private void mockGetApplicationGrammaticalGender(int grammaticalGender) {
doReturn(grammaticalGender).when(mGrammaticalInflectionService)
.getApplicationGrammaticalGender(
eq(DEFAULT_PACKAGE_NAME), eq(DEFAULT_USER_ID));
}
private byte[] convertToByteArray(HashMap<String, Integer> pkgGenderInfo) throws IOException{
try (final ByteArrayOutputStream out = new ByteArrayOutputStream();
final ObjectOutputStream objStream = new ObjectOutputStream(out)) {
objStream.writeObject(pkgGenderInfo);
return out.toByteArray();
} catch (IOException e) {
throw e;
}
}
private HashMap<String, Integer> readFromByteArray(byte[] payload)
throws IOException, ClassNotFoundException {
HashMap<String, Integer> data;
try (ByteArrayInputStream byteIn = new ByteArrayInputStream(payload);
ObjectInputStream in = new ObjectInputStream(byteIn)) {
data = (HashMap<String, Integer>) in.readObject();
} catch (IOException | ClassNotFoundException e) {
throw e;
}
return data;
}
}