1 /*
2  * Copyright (C) 2020 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.server.people.data;
18 
19 import android.annotation.MainThread;
20 import android.annotation.NonNull;
21 import android.annotation.Nullable;
22 import android.annotation.WorkerThread;
23 import android.content.LocusId;
24 import android.net.Uri;
25 import android.util.ArrayMap;
26 import android.util.Slog;
27 import android.util.proto.ProtoInputStream;
28 
29 import com.android.internal.annotations.GuardedBy;
30 import com.android.server.people.ConversationInfosProto;
31 
32 import com.google.android.collect.Lists;
33 
34 import java.io.ByteArrayInputStream;
35 import java.io.ByteArrayOutputStream;
36 import java.io.DataInputStream;
37 import java.io.DataOutputStream;
38 import java.io.File;
39 import java.io.IOException;
40 import java.util.ArrayList;
41 import java.util.List;
42 import java.util.Map;
43 import java.util.concurrent.ScheduledExecutorService;
44 import java.util.function.Consumer;
45 
46 /**
47  * The store that stores and accesses the conversations data for a package.
48  */
49 class ConversationStore {
50 
51     private static final String TAG = ConversationStore.class.getSimpleName();
52 
53     private static final String CONVERSATIONS_FILE_NAME = "conversations";
54 
55     private static final int CONVERSATION_INFOS_END_TOKEN = -1;
56 
57     // Shortcut ID -> Conversation Info
58     @GuardedBy("this")
59     private final Map<String, ConversationInfo> mConversationInfoMap = new ArrayMap<>();
60 
61     // Locus ID -> Shortcut ID
62     @GuardedBy("this")
63     private final Map<LocusId, String> mLocusIdToShortcutIdMap = new ArrayMap<>();
64 
65     // Contact URI -> Shortcut ID
66     @GuardedBy("this")
67     private final Map<Uri, String> mContactUriToShortcutIdMap = new ArrayMap<>();
68 
69     // Phone Number -> Shortcut ID
70     @GuardedBy("this")
71     private final Map<String, String> mPhoneNumberToShortcutIdMap = new ArrayMap<>();
72 
73     // Notification Channel ID -> Shortcut ID
74     @GuardedBy("this")
75     private final Map<String, String> mNotifChannelIdToShortcutIdMap = new ArrayMap<>();
76 
77     private final ScheduledExecutorService mScheduledExecutorService;
78     private final File mPackageDir;
79 
80     private ConversationInfosProtoDiskReadWriter mConversationInfosProtoDiskReadWriter;
81 
ConversationStore(@onNull File packageDir, @NonNull ScheduledExecutorService scheduledExecutorService)82     ConversationStore(@NonNull File packageDir,
83             @NonNull ScheduledExecutorService scheduledExecutorService) {
84         mScheduledExecutorService = scheduledExecutorService;
85         mPackageDir = packageDir;
86     }
87 
88     /**
89      * Loads conversations from disk to memory in a background thread. This should be called
90      * after the device powers on and the user has been unlocked.
91      */
92     @WorkerThread
loadConversationsFromDisk()93     synchronized void loadConversationsFromDisk() {
94         ConversationInfosProtoDiskReadWriter conversationInfosProtoDiskReadWriter =
95                 getConversationInfosProtoDiskReadWriter();
96         if (conversationInfosProtoDiskReadWriter == null) {
97             return;
98         }
99         List<ConversationInfo> conversationsOnDisk =
100                 conversationInfosProtoDiskReadWriter.read(CONVERSATIONS_FILE_NAME);
101         if (conversationsOnDisk == null) {
102             return;
103         }
104         for (ConversationInfo conversationInfo : conversationsOnDisk) {
105             updateConversationsInMemory(conversationInfo);
106         }
107     }
108 
109     /**
110      * Immediately flushes current conversations to disk. This should be called when device is
111      * powering off.
112      */
113     @MainThread
saveConversationsToDisk()114     synchronized void saveConversationsToDisk() {
115         ConversationInfosProtoDiskReadWriter conversationInfosProtoDiskReadWriter =
116                 getConversationInfosProtoDiskReadWriter();
117         if (conversationInfosProtoDiskReadWriter != null) {
118             conversationInfosProtoDiskReadWriter.saveConversationsImmediately(
119                     new ArrayList<>(mConversationInfoMap.values()));
120         }
121     }
122 
123     @MainThread
addOrUpdate(@onNull ConversationInfo conversationInfo)124     synchronized void addOrUpdate(@NonNull ConversationInfo conversationInfo) {
125         updateConversationsInMemory(conversationInfo);
126         scheduleUpdateConversationsOnDisk();
127     }
128 
129     @MainThread
130     @Nullable
deleteConversation(@onNull String shortcutId)131     synchronized ConversationInfo deleteConversation(@NonNull String shortcutId) {
132         ConversationInfo conversationInfo = mConversationInfoMap.remove(shortcutId);
133         if (conversationInfo == null) {
134             return null;
135         }
136 
137         LocusId locusId = conversationInfo.getLocusId();
138         if (locusId != null) {
139             mLocusIdToShortcutIdMap.remove(locusId);
140         }
141 
142         Uri contactUri = conversationInfo.getContactUri();
143         if (contactUri != null) {
144             mContactUriToShortcutIdMap.remove(contactUri);
145         }
146 
147         String phoneNumber = conversationInfo.getContactPhoneNumber();
148         if (phoneNumber != null) {
149             mPhoneNumberToShortcutIdMap.remove(phoneNumber);
150         }
151 
152         String notifChannelId = conversationInfo.getNotificationChannelId();
153         if (notifChannelId != null) {
154             mNotifChannelIdToShortcutIdMap.remove(notifChannelId);
155         }
156         scheduleUpdateConversationsOnDisk();
157         return conversationInfo;
158     }
159 
forAllConversations(@onNull Consumer<ConversationInfo> consumer)160     synchronized void forAllConversations(@NonNull Consumer<ConversationInfo> consumer) {
161         for (ConversationInfo ci : mConversationInfoMap.values()) {
162             consumer.accept(ci);
163         }
164     }
165 
166     @Nullable
getConversation(@ullable String shortcutId)167     synchronized ConversationInfo getConversation(@Nullable String shortcutId) {
168         return shortcutId != null ? mConversationInfoMap.get(shortcutId) : null;
169     }
170 
171     @Nullable
getConversationByLocusId(@onNull LocusId locusId)172     synchronized ConversationInfo getConversationByLocusId(@NonNull LocusId locusId) {
173         return getConversation(mLocusIdToShortcutIdMap.get(locusId));
174     }
175 
176     @Nullable
getConversationByContactUri(@onNull Uri contactUri)177     synchronized ConversationInfo getConversationByContactUri(@NonNull Uri contactUri) {
178         return getConversation(mContactUriToShortcutIdMap.get(contactUri));
179     }
180 
181     @Nullable
getConversationByPhoneNumber(@onNull String phoneNumber)182     synchronized ConversationInfo getConversationByPhoneNumber(@NonNull String phoneNumber) {
183         return getConversation(mPhoneNumberToShortcutIdMap.get(phoneNumber));
184     }
185 
186     @Nullable
getConversationByNotificationChannelId(@onNull String notifChannelId)187     ConversationInfo getConversationByNotificationChannelId(@NonNull String notifChannelId) {
188         return getConversation(mNotifChannelIdToShortcutIdMap.get(notifChannelId));
189     }
190 
onDestroy()191     synchronized void onDestroy() {
192         mConversationInfoMap.clear();
193         mContactUriToShortcutIdMap.clear();
194         mLocusIdToShortcutIdMap.clear();
195         mNotifChannelIdToShortcutIdMap.clear();
196         mPhoneNumberToShortcutIdMap.clear();
197         ConversationInfosProtoDiskReadWriter writer = getConversationInfosProtoDiskReadWriter();
198         if (writer != null) {
199             writer.deleteConversationsFile();
200         }
201     }
202 
203     @Nullable
getBackupPayload()204     synchronized byte[] getBackupPayload() {
205         ByteArrayOutputStream baos = new ByteArrayOutputStream();
206         DataOutputStream conversationInfosOut = new DataOutputStream(baos);
207         for (ConversationInfo conversationInfo : mConversationInfoMap.values()) {
208             byte[] backupPayload = conversationInfo.getBackupPayload();
209             if (backupPayload == null) {
210                 continue;
211             }
212             try {
213                 conversationInfosOut.writeInt(backupPayload.length);
214                 conversationInfosOut.write(backupPayload);
215             } catch (IOException e) {
216                 Slog.e(TAG, "Failed to write conversation info to backup payload.", e);
217                 return null;
218             }
219         }
220         try {
221             conversationInfosOut.writeInt(CONVERSATION_INFOS_END_TOKEN);
222         } catch (IOException e) {
223             Slog.e(TAG, "Failed to write conversation infos end token to backup payload.", e);
224             return null;
225         }
226         return baos.toByteArray();
227     }
228 
restore(@onNull byte[] payload)229     synchronized void restore(@NonNull byte[] payload) {
230         DataInputStream in = new DataInputStream(new ByteArrayInputStream(payload));
231         try {
232             for (int conversationInfoSize = in.readInt();
233                     conversationInfoSize != CONVERSATION_INFOS_END_TOKEN;
234                     conversationInfoSize = in.readInt()) {
235                 byte[] conversationInfoPayload = new byte[conversationInfoSize];
236                 in.readFully(conversationInfoPayload, 0, conversationInfoSize);
237                 ConversationInfo conversationInfo = ConversationInfo.readFromBackupPayload(
238                         conversationInfoPayload);
239                 if (conversationInfo != null) {
240                     addOrUpdate(conversationInfo);
241                 }
242             }
243         } catch (IOException e) {
244             Slog.e(TAG, "Failed to read conversation info from payload.", e);
245         }
246     }
247 
248     @MainThread
updateConversationsInMemory( @onNull ConversationInfo conversationInfo)249     private synchronized void updateConversationsInMemory(
250             @NonNull ConversationInfo conversationInfo) {
251         mConversationInfoMap.put(conversationInfo.getShortcutId(), conversationInfo);
252 
253         LocusId locusId = conversationInfo.getLocusId();
254         if (locusId != null) {
255             mLocusIdToShortcutIdMap.put(locusId, conversationInfo.getShortcutId());
256         }
257 
258         Uri contactUri = conversationInfo.getContactUri();
259         if (contactUri != null) {
260             mContactUriToShortcutIdMap.put(contactUri, conversationInfo.getShortcutId());
261         }
262 
263         String phoneNumber = conversationInfo.getContactPhoneNumber();
264         if (phoneNumber != null) {
265             mPhoneNumberToShortcutIdMap.put(phoneNumber, conversationInfo.getShortcutId());
266         }
267 
268         String notifChannelId = conversationInfo.getNotificationChannelId();
269         if (notifChannelId != null) {
270             mNotifChannelIdToShortcutIdMap.put(notifChannelId, conversationInfo.getShortcutId());
271         }
272     }
273 
274     /** Schedules a dump of all conversations onto disk, overwriting existing values. */
275     @MainThread
scheduleUpdateConversationsOnDisk()276     private synchronized void scheduleUpdateConversationsOnDisk() {
277         ConversationInfosProtoDiskReadWriter conversationInfosProtoDiskReadWriter =
278                 getConversationInfosProtoDiskReadWriter();
279         if (conversationInfosProtoDiskReadWriter != null) {
280             conversationInfosProtoDiskReadWriter.scheduleConversationsSave(
281                     new ArrayList<>(mConversationInfoMap.values()));
282         }
283     }
284 
285     @Nullable
getConversationInfosProtoDiskReadWriter()286     private ConversationInfosProtoDiskReadWriter getConversationInfosProtoDiskReadWriter() {
287         if (!mPackageDir.exists()) {
288             Slog.e(TAG, "Package data directory does not exist: " + mPackageDir.getAbsolutePath());
289             return null;
290         }
291         if (mConversationInfosProtoDiskReadWriter == null) {
292             mConversationInfosProtoDiskReadWriter = new ConversationInfosProtoDiskReadWriter(
293                     mPackageDir, CONVERSATIONS_FILE_NAME, mScheduledExecutorService);
294         }
295         return mConversationInfosProtoDiskReadWriter;
296     }
297 
298     /** Reads and writes {@link ConversationInfo}s on disk. */
299     private static class ConversationInfosProtoDiskReadWriter extends
300             AbstractProtoDiskReadWriter<List<ConversationInfo>> {
301 
302         private final String mConversationInfoFileName;
303 
ConversationInfosProtoDiskReadWriter(@onNull File rootDir, @NonNull String conversationInfoFileName, @NonNull ScheduledExecutorService scheduledExecutorService)304         ConversationInfosProtoDiskReadWriter(@NonNull File rootDir,
305                 @NonNull String conversationInfoFileName,
306                 @NonNull ScheduledExecutorService scheduledExecutorService) {
307             super(rootDir, scheduledExecutorService);
308             mConversationInfoFileName = conversationInfoFileName;
309         }
310 
311         @Override
protoStreamWriter()312         ProtoStreamWriter<List<ConversationInfo>> protoStreamWriter() {
313             return (protoOutputStream, data) -> {
314                 for (ConversationInfo conversationInfo : data) {
315                     long token = protoOutputStream.start(ConversationInfosProto.CONVERSATION_INFOS);
316                     conversationInfo.writeToProto(protoOutputStream);
317                     protoOutputStream.end(token);
318                 }
319             };
320         }
321 
322         @Override
protoStreamReader()323         ProtoStreamReader<List<ConversationInfo>> protoStreamReader() {
324             return protoInputStream -> {
325                 List<ConversationInfo> results = Lists.newArrayList();
326                 try {
327                     while (protoInputStream.nextField() != ProtoInputStream.NO_MORE_FIELDS) {
328                         if (protoInputStream.getFieldNumber()
329                                 != (int) ConversationInfosProto.CONVERSATION_INFOS) {
330                             continue;
331                         }
332                         long token = protoInputStream.start(
333                                 ConversationInfosProto.CONVERSATION_INFOS);
334                         ConversationInfo conversationInfo = ConversationInfo.readFromProto(
335                                 protoInputStream);
336                         protoInputStream.end(token);
337                         results.add(conversationInfo);
338                     }
339                 } catch (IOException e) {
340                     Slog.e(TAG, "Failed to read protobuf input stream.", e);
341                 }
342                 return results;
343             };
344         }
345 
346         /**
347          * Schedules a flush of the specified conversations to disk.
348          */
349         @MainThread
350         void scheduleConversationsSave(@NonNull List<ConversationInfo> conversationInfos) {
351             scheduleSave(mConversationInfoFileName, conversationInfos);
352         }
353 
354         /**
355          * Saves the specified conversations immediately. This should be used when device is
356          * powering off.
357          */
358         @MainThread
359         void saveConversationsImmediately(@NonNull List<ConversationInfo> conversationInfos) {
360             saveImmediately(mConversationInfoFileName, conversationInfos);
361         }
362 
363         @WorkerThread
364         void deleteConversationsFile() {
365             delete(mConversationInfoFileName);
366         }
367     }
368 }
369