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:
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
*/
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user