Merge "Implement backup and restoration of conversation infos." into rvc-dev

am: 8839aaee6d

Change-Id: I9987221be8dc60bc1c31f0a88eb7e0140da38d1b
This commit is contained in:
Trung Lam
2020-02-27 12:39:40 -08:00
committed by android-build-merger
9 changed files with 246 additions and 14 deletions

View File

@@ -50,7 +50,7 @@ class PeopleBackupHelper extends BlobBackupHelper {
if (DEBUG) {
Slog.d(TAG, "Handling backup of " + key);
}
return ps.backupConversationInfos(mUserId);
return ps.getBackupPayload(mUserId);
}
@Override
@@ -63,6 +63,6 @@ class PeopleBackupHelper extends BlobBackupHelper {
if (DEBUG) {
Slog.d(TAG, "Handling restore of " + key);
}
ps.restoreConversationInfos(mUserId, key, payload);
ps.restore(mUserId, payload);
}
}

View File

@@ -17,6 +17,7 @@
package com.android.server.people;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.UserIdInt;
import android.os.CancellationSignal;
import android.service.appprediction.IPredictionService;
@@ -34,16 +35,17 @@ public abstract class PeopleServiceInternal extends IPredictionService.Stub {
@NonNull CancellationSignal signal);
/**
* The number conversation infos will be dynamic, based on the currently installed apps on the
* device. All of which should be combined into a single blob to be backed up.
* Returns a backup payload that contains conversation infos. The number conversation infos will
* be dynamic, based on the currently installed apps on the device. All of which should be
* combined into a single blob to be backed up.
*/
public abstract byte[] backupConversationInfos(@UserIdInt int userId);
@Nullable
public abstract byte[] getBackupPayload(@UserIdInt int userId);
/**
* Multiple conversation infos may exist in the restore payload, child classes are required to
* manage the restoration based on how individual conversation infos were originally combined
* during backup.
* Restores conversation infos stored in payload blob. Multiple conversation infos may exist in
* the restore payload, child classes are required to manage the restoration based on how
* individual conversation infos were originally combined during backup.
*/
public abstract void restoreConversationInfos(@UserIdInt int userId, @NonNull String key,
@NonNull byte[] payload);
public abstract void restore(@UserIdInt int userId, @NonNull byte[] payload);
}

View File

@@ -17,6 +17,7 @@
package com.android.server.people;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.UserIdInt;
import android.app.prediction.AppPredictionContext;
import android.app.prediction.AppPredictionSessionId;
@@ -145,14 +146,15 @@ public class PeopleService extends SystemService {
mDataManager.pruneDataForUser(userId, signal);
}
@Nullable
@Override
public byte[] backupConversationInfos(@UserIdInt int userId) {
return new byte[0];
public byte[] getBackupPayload(@UserIdInt int userId) {
return mDataManager.getBackupPayload(userId);
}
@Override
public void restoreConversationInfos(@UserIdInt int userId, @NonNull String key,
@NonNull byte[] payload) {
public void restore(@UserIdInt int userId, @NonNull byte[] payload) {
mDataManager.restore(userId, payload);
}
@VisibleForTesting

View File

@@ -24,6 +24,7 @@ import android.content.LocusIdProto;
import android.content.pm.ShortcutInfo;
import android.content.pm.ShortcutInfo.ShortcutFlags;
import android.net.Uri;
import android.text.TextUtils;
import android.util.Slog;
import android.util.proto.ProtoInputStream;
import android.util.proto.ProtoOutputStream;
@@ -31,6 +32,10 @@ import android.util.proto.ProtoOutputStream;
import com.android.internal.util.Preconditions;
import com.android.server.people.ConversationInfoProto;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
@@ -280,6 +285,25 @@ public class ConversationInfo {
}
}
@Nullable
byte[] getBackupPayload() {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
DataOutputStream out = new DataOutputStream(baos);
try {
out.writeUTF(mShortcutId);
out.writeUTF(mLocusId != null ? mLocusId.getId() : "");
out.writeUTF(mContactUri != null ? mContactUri.toString() : "");
out.writeUTF(mNotificationChannelId != null ? mNotificationChannelId : "");
out.writeInt(mShortcutFlags);
out.writeInt(mConversationFlags);
out.writeUTF(mContactPhoneNumber != null ? mContactPhoneNumber : "");
} catch (IOException e) {
Slog.e(TAG, "Failed to write fields to backup payload.", e);
return null;
}
return baos.toByteArray();
}
/** Reads from {@link ProtoInputStream} and constructs a {@link ConversationInfo}. */
@NonNull
static ConversationInfo readFromProto(@NonNull ProtoInputStream protoInputStream)
@@ -331,6 +355,37 @@ public class ConversationInfo {
return builder.build();
}
@Nullable
static ConversationInfo readFromBackupPayload(@NonNull byte[] payload) {
ConversationInfo.Builder builder = new ConversationInfo.Builder();
DataInputStream in = new DataInputStream(new ByteArrayInputStream(payload));
try {
builder.setShortcutId(in.readUTF());
String locusId = in.readUTF();
if (!TextUtils.isEmpty(locusId)) {
builder.setLocusId(new LocusId(locusId));
}
String contactUri = in.readUTF();
if (!TextUtils.isEmpty(contactUri)) {
builder.setContactUri(Uri.parse(contactUri));
}
String notificationChannelId = in.readUTF();
if (!TextUtils.isEmpty(notificationChannelId)) {
builder.setNotificationChannelId(notificationChannelId);
}
builder.setShortcutFlags(in.readInt());
builder.setConversationFlags(in.readInt());
String contactPhoneNumber = in.readUTF();
if (!TextUtils.isEmpty(contactPhoneNumber)) {
builder.setContactPhoneNumber(contactPhoneNumber);
}
} catch (IOException e) {
Slog.e(TAG, "Failed to read conversation info fields from backup payload.", e);
return null;
}
return builder.build();
}
/**
* Builder class for {@link ConversationInfo} objects.
*/

View File

@@ -31,6 +31,10 @@ import com.android.server.people.ConversationInfosProto;
import com.google.android.collect.Lists;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
@@ -48,6 +52,8 @@ class ConversationStore {
private static final String CONVERSATIONS_FILE_NAME = "conversations";
private static final int CONVERSATION_INFOS_END_TOKEN = -1;
// Shortcut ID -> Conversation Info
@GuardedBy("this")
private final Map<String, ConversationInfo> mConversationInfoMap = new ArrayMap<>();
@@ -195,6 +201,51 @@ class ConversationStore {
mConversationInfosProtoDiskReadWriter.deleteConversationsFile();
}
@Nullable
synchronized byte[] getBackupPayload() {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
DataOutputStream conversationInfosOut = new DataOutputStream(baos);
for (ConversationInfo conversationInfo : mConversationInfoMap.values()) {
byte[] backupPayload = conversationInfo.getBackupPayload();
if (backupPayload == null) {
continue;
}
try {
conversationInfosOut.writeInt(backupPayload.length);
conversationInfosOut.write(backupPayload);
} catch (IOException e) {
Slog.e(TAG, "Failed to write conversation info to backup payload.", e);
return null;
}
}
try {
conversationInfosOut.writeInt(CONVERSATION_INFOS_END_TOKEN);
} catch (IOException e) {
Slog.e(TAG, "Failed to write conversation infos end token to backup payload.", e);
return null;
}
return baos.toByteArray();
}
synchronized void restore(@NonNull byte[] payload) {
DataInputStream in = new DataInputStream(new ByteArrayInputStream(payload));
try {
for (int conversationInfoSize = in.readInt();
conversationInfoSize != CONVERSATION_INFOS_END_TOKEN;
conversationInfoSize = in.readInt()) {
byte[] conversationInfoPayload = new byte[conversationInfoSize];
in.readFully(conversationInfoPayload, 0, conversationInfoSize);
ConversationInfo conversationInfo = ConversationInfo.readFromBackupPayload(
conversationInfoPayload);
if (conversationInfo != null) {
addOrUpdate(conversationInfo);
}
}
} catch (IOException e) {
Slog.e(TAG, "Failed to read conversation info from payload.", e);
}
}
@MainThread
private synchronized void updateConversationsInMemory(
@NonNull ConversationInfo conversationInfo) {

View File

@@ -334,6 +334,25 @@ public class DataManager {
});
}
/** Retrieves a backup payload blob for specified user id. */
@Nullable
public byte[] getBackupPayload(@UserIdInt int userId) {
UserData userData = getUnlockedUserData(userId);
if (userData == null) {
return null;
}
return userData.getBackupPayload();
}
/** Attempts to restore data for the specified user id. */
public void restore(@UserIdInt int userId, @NonNull byte[] payload) {
UserData userData = getUnlockedUserData(userId);
if (userData == null) {
return;
}
userData.restore(payload);
}
private int mimeTypeToShareEventType(String mimeType) {
if (mimeType.startsWith("text/")) {
return Event.TYPE_SHARE_TEXT;

View File

@@ -22,8 +22,14 @@ import android.annotation.UserIdInt;
import android.os.Environment;
import android.text.TextUtils;
import android.util.ArrayMap;
import android.util.Slog;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.File;
import java.io.IOException;
import java.util.Map;
import java.util.concurrent.ScheduledExecutorService;
import java.util.function.Consumer;
@@ -31,6 +37,10 @@ import java.util.function.Consumer;
/** The data associated with a user profile. */
class UserData {
private static final String TAG = UserData.class.getSimpleName();
private static final int CONVERSATIONS_END_TOKEN = -1;
private final @UserIdInt int mUserId;
private final File mPerUserPeopleDataDir;
@@ -125,6 +135,48 @@ class UserData {
return mDefaultSmsApp != null ? getPackageData(mDefaultSmsApp) : null;
}
@Nullable
byte[] getBackupPayload() {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
DataOutputStream out = new DataOutputStream(baos);
for (PackageData packageData : mPackageDataMap.values()) {
try {
byte[] conversationsBackupPayload =
packageData.getConversationStore().getBackupPayload();
out.writeInt(conversationsBackupPayload.length);
out.write(conversationsBackupPayload);
out.writeUTF(packageData.getPackageName());
} catch (IOException e) {
Slog.e(TAG, "Failed to write conversations to backup payload.", e);
return null;
}
}
try {
out.writeInt(CONVERSATIONS_END_TOKEN);
} catch (IOException e) {
Slog.e(TAG, "Failed to write conversations end token to backup payload.", e);
return null;
}
return baos.toByteArray();
}
void restore(@NonNull byte[] payload) {
DataInputStream in = new DataInputStream(new ByteArrayInputStream(payload));
try {
for (int conversationsPayloadSize = in.readInt();
conversationsPayloadSize != CONVERSATIONS_END_TOKEN;
conversationsPayloadSize = in.readInt()) {
byte[] conversationsPayload = new byte[conversationsPayloadSize];
in.readFully(conversationsPayload, 0, conversationsPayloadSize);
String packageName = in.readUTF();
getOrCreatePackageData(packageName).getConversationStore().restore(
conversationsPayload);
}
} catch (IOException e) {
Slog.e(TAG, "Failed to restore conversations from backup payload.", e);
}
}
private PackageData createPackageData(String packageName) {
return new PackageData(packageName, mUserId, this::isDefaultDialer, this::isDefaultSmsApp,
mScheduledExecutorService, mPerUserPeopleDataDir);

View File

@@ -284,6 +284,30 @@ public final class ConversationStoreTest {
assertEquals(in2, out2);
}
@Test
public void testBackupAndRestore() {
ConversationInfo in1 = buildConversationInfo(SHORTCUT_ID, LOCUS_ID, CONTACT_URI,
PHONE_NUMBER, NOTIFICATION_CHANNEL_ID);
ConversationInfo in2 = buildConversationInfo(SHORTCUT_ID_2, LOCUS_ID_2, CONTACT_URI_2,
PHONE_NUMBER_2, NOTIFICATION_CHANNEL_ID_2);
mConversationStore.addOrUpdate(in1);
mConversationStore.addOrUpdate(in2);
byte[] backupPayload = mConversationStore.getBackupPayload();
assertNotNull(backupPayload);
ConversationStore conversationStore = new ConversationStore(mFile,
mMockScheduledExecutorService);
ConversationInfo out1 = conversationStore.getConversation(SHORTCUT_ID);
assertNull(out1);
conversationStore.restore(backupPayload);
out1 = conversationStore.getConversation(SHORTCUT_ID);
ConversationInfo out2 = conversationStore.getConversation(SHORTCUT_ID_2);
assertEquals(in1, out1);
assertEquals(in2, out2);
}
private void resetConversationStore() {
mFile.mkdir();
mMockScheduledExecutorService = new MockScheduledExecutorService();

View File

@@ -653,6 +653,33 @@ public final class DataManagerTest {
assertTrue(activeTimeSlots.isEmpty());
}
@Test
public void testBackupAndRestoration()
throws IntentFilter.MalformedMimeTypeException {
mDataManager.onUserUnlocked(USER_ID_PRIMARY);
ShortcutInfo shortcut = buildShortcutInfo(TEST_PKG_NAME, USER_ID_PRIMARY, TEST_SHORTCUT_ID,
null);
AppTarget appTarget = new AppTarget.Builder(new AppTargetId(TEST_SHORTCUT_ID), shortcut)
.build();
AppTargetEvent appTargetEvent =
new AppTargetEvent.Builder(appTarget, AppTargetEvent.ACTION_LAUNCH)
.setLaunchLocation(ChooserActivity.LAUNCH_LOCATON_DIRECT_SHARE)
.build();
IntentFilter intentFilter = new IntentFilter(Intent.ACTION_SEND, "image/jpg");
mDataManager.reportShareTargetEvent(appTargetEvent, intentFilter);
byte[] payload = mDataManager.getBackupPayload(USER_ID_PRIMARY);
DataManager dataManager = new DataManager(mContext, mInjector);
dataManager.onUserUnlocked(USER_ID_PRIMARY);
dataManager.restore(USER_ID_PRIMARY, payload);
ConversationInfo conversationInfo = dataManager.getPackage(TEST_PKG_NAME, USER_ID_PRIMARY)
.getConversationStore()
.getConversation(TEST_SHORTCUT_ID);
assertNotNull(conversationInfo);
assertEquals(conversationInfo.getShortcutId(), TEST_SHORTCUT_ID);
}
private static <T> void addLocalServiceMock(Class<T> clazz, T mock) {
LocalServices.removeServiceForTest(clazz);
LocalServices.addService(clazz, mock);