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