Merge "Implement backup and restoration of conversation infos." into rvc-dev am: 8839aaee6d am: 350f6f8f9b am: 469b51013d

Change-Id: Ie0f1a8ce93eb3b38f5f53f95cc178408aa93e675
This commit is contained in:
Automerger Merge Worker
2020-02-27 20:54:55 +00:00
9 changed files with 246 additions and 14 deletions

View File

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

View File

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

View File

@@ -24,6 +24,7 @@ import android.content.LocusIdProto;
import android.content.pm.ShortcutInfo; import android.content.pm.ShortcutInfo;
import android.content.pm.ShortcutInfo.ShortcutFlags; import android.content.pm.ShortcutInfo.ShortcutFlags;
import android.net.Uri; import android.net.Uri;
import android.text.TextUtils;
import android.util.Slog; import android.util.Slog;
import android.util.proto.ProtoInputStream; import android.util.proto.ProtoInputStream;
import android.util.proto.ProtoOutputStream; import android.util.proto.ProtoOutputStream;
@@ -31,6 +32,10 @@ import android.util.proto.ProtoOutputStream;
import com.android.internal.util.Preconditions; import com.android.internal.util.Preconditions;
import com.android.server.people.ConversationInfoProto; 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.io.IOException;
import java.lang.annotation.Retention; import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy; 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}. */ /** Reads from {@link ProtoInputStream} and constructs a {@link ConversationInfo}. */
@NonNull @NonNull
static ConversationInfo readFromProto(@NonNull ProtoInputStream protoInputStream) static ConversationInfo readFromProto(@NonNull ProtoInputStream protoInputStream)
@@ -331,6 +355,37 @@ public class ConversationInfo {
return builder.build(); 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. * 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 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.File;
import java.io.IOException; import java.io.IOException;
import java.util.ArrayList; import java.util.ArrayList;
@@ -48,6 +52,8 @@ class ConversationStore {
private static final String CONVERSATIONS_FILE_NAME = "conversations"; private static final String CONVERSATIONS_FILE_NAME = "conversations";
private static final int CONVERSATION_INFOS_END_TOKEN = -1;
// Shortcut ID -> Conversation Info // Shortcut ID -> Conversation Info
@GuardedBy("this") @GuardedBy("this")
private final Map<String, ConversationInfo> mConversationInfoMap = new ArrayMap<>(); private final Map<String, ConversationInfo> mConversationInfoMap = new ArrayMap<>();
@@ -195,6 +201,51 @@ class ConversationStore {
mConversationInfosProtoDiskReadWriter.deleteConversationsFile(); 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 @MainThread
private synchronized void updateConversationsInMemory( private synchronized void updateConversationsInMemory(
@NonNull ConversationInfo conversationInfo) { @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) { private int mimeTypeToShareEventType(String mimeType) {
if (mimeType.startsWith("text/")) { if (mimeType.startsWith("text/")) {
return Event.TYPE_SHARE_TEXT; return Event.TYPE_SHARE_TEXT;

View File

@@ -22,8 +22,14 @@ import android.annotation.UserIdInt;
import android.os.Environment; import android.os.Environment;
import android.text.TextUtils; import android.text.TextUtils;
import android.util.ArrayMap; 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.File;
import java.io.IOException;
import java.util.Map; import java.util.Map;
import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.ScheduledExecutorService;
import java.util.function.Consumer; import java.util.function.Consumer;
@@ -31,6 +37,10 @@ import java.util.function.Consumer;
/** The data associated with a user profile. */ /** The data associated with a user profile. */
class UserData { 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 @UserIdInt int mUserId;
private final File mPerUserPeopleDataDir; private final File mPerUserPeopleDataDir;
@@ -125,6 +135,48 @@ class UserData {
return mDefaultSmsApp != null ? getPackageData(mDefaultSmsApp) : null; 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) { private PackageData createPackageData(String packageName) {
return new PackageData(packageName, mUserId, this::isDefaultDialer, this::isDefaultSmsApp, return new PackageData(packageName, mUserId, this::isDefaultDialer, this::isDefaultSmsApp,
mScheduledExecutorService, mPerUserPeopleDataDir); mScheduledExecutorService, mPerUserPeopleDataDir);

View File

@@ -284,6 +284,30 @@ public final class ConversationStoreTest {
assertEquals(in2, out2); 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() { private void resetConversationStore() {
mFile.mkdir(); mFile.mkdir();
mMockScheduledExecutorService = new MockScheduledExecutorService(); mMockScheduledExecutorService = new MockScheduledExecutorService();

View File

@@ -653,6 +653,33 @@ public final class DataManagerTest {
assertTrue(activeTimeSlots.isEmpty()); 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) { private static <T> void addLocalServiceMock(Class<T> clazz, T mock) {
LocalServices.removeServiceForTest(clazz); LocalServices.removeServiceForTest(clazz);
LocalServices.addService(clazz, mock); LocalServices.addService(clazz, mock);