blob: 5be0735c23b27ce608c7582d4a5bc0f1e97e7db8 [file] [log] [blame]
/*
* 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;
}
}