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 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 void saveConversationsToDisk() { 115 ConversationInfosProtoDiskReadWriter conversationInfosProtoDiskReadWriter = 116 getConversationInfosProtoDiskReadWriter(); 117 if (conversationInfosProtoDiskReadWriter != null) { 118 List<ConversationInfo> conversations; 119 synchronized (this) { 120 conversations = new ArrayList<>(mConversationInfoMap.values()); 121 } 122 conversationInfosProtoDiskReadWriter.saveConversationsImmediately(conversations); 123 } 124 } 125 126 @MainThread addOrUpdate(@onNull ConversationInfo conversationInfo)127 void addOrUpdate(@NonNull ConversationInfo conversationInfo) { 128 updateConversationsInMemory(conversationInfo); 129 scheduleUpdateConversationsOnDisk(); 130 } 131 132 @MainThread 133 @Nullable deleteConversation(@onNull String shortcutId)134 ConversationInfo deleteConversation(@NonNull String shortcutId) { 135 ConversationInfo conversationInfo; 136 synchronized (this) { 137 conversationInfo = mConversationInfoMap.remove(shortcutId); 138 if (conversationInfo == null) { 139 return null; 140 } 141 142 LocusId locusId = conversationInfo.getLocusId(); 143 if (locusId != null) { 144 mLocusIdToShortcutIdMap.remove(locusId); 145 } 146 147 Uri contactUri = conversationInfo.getContactUri(); 148 if (contactUri != null) { 149 mContactUriToShortcutIdMap.remove(contactUri); 150 } 151 152 String phoneNumber = conversationInfo.getContactPhoneNumber(); 153 if (phoneNumber != null) { 154 mPhoneNumberToShortcutIdMap.remove(phoneNumber); 155 } 156 157 String notifChannelId = conversationInfo.getNotificationChannelId(); 158 if (notifChannelId != null) { 159 mNotifChannelIdToShortcutIdMap.remove(notifChannelId); 160 } 161 } 162 scheduleUpdateConversationsOnDisk(); 163 return conversationInfo; 164 } 165 forAllConversations(@onNull Consumer<ConversationInfo> consumer)166 void forAllConversations(@NonNull Consumer<ConversationInfo> consumer) { 167 List<ConversationInfo> conversations; 168 synchronized (this) { 169 conversations = new ArrayList<>(mConversationInfoMap.values()); 170 } 171 for (ConversationInfo ci : conversations) { 172 consumer.accept(ci); 173 } 174 } 175 176 @Nullable getConversation(@ullable String shortcutId)177 synchronized ConversationInfo getConversation(@Nullable String shortcutId) { 178 return shortcutId != null ? mConversationInfoMap.get(shortcutId) : null; 179 } 180 181 @Nullable getConversationByLocusId(@onNull LocusId locusId)182 synchronized ConversationInfo getConversationByLocusId(@NonNull LocusId locusId) { 183 return getConversation(mLocusIdToShortcutIdMap.get(locusId)); 184 } 185 186 @Nullable getConversationByContactUri(@onNull Uri contactUri)187 synchronized ConversationInfo getConversationByContactUri(@NonNull Uri contactUri) { 188 return getConversation(mContactUriToShortcutIdMap.get(contactUri)); 189 } 190 191 @Nullable getConversationByPhoneNumber(@onNull String phoneNumber)192 synchronized ConversationInfo getConversationByPhoneNumber(@NonNull String phoneNumber) { 193 return getConversation(mPhoneNumberToShortcutIdMap.get(phoneNumber)); 194 } 195 196 @Nullable getConversationByNotificationChannelId( @onNull String notifChannelId)197 synchronized ConversationInfo getConversationByNotificationChannelId( 198 @NonNull String notifChannelId) { 199 return getConversation(mNotifChannelIdToShortcutIdMap.get(notifChannelId)); 200 } 201 onDestroy()202 void onDestroy() { 203 synchronized (this) { 204 mConversationInfoMap.clear(); 205 mContactUriToShortcutIdMap.clear(); 206 mLocusIdToShortcutIdMap.clear(); 207 mNotifChannelIdToShortcutIdMap.clear(); 208 mPhoneNumberToShortcutIdMap.clear(); 209 } 210 ConversationInfosProtoDiskReadWriter writer = getConversationInfosProtoDiskReadWriter(); 211 if (writer != null) { 212 writer.deleteConversationsFile(); 213 } 214 } 215 216 @Nullable getBackupPayload()217 byte[] getBackupPayload() { 218 ByteArrayOutputStream baos = new ByteArrayOutputStream(); 219 DataOutputStream conversationInfosOut = new DataOutputStream(baos); 220 forAllConversations(conversationInfo -> { 221 byte[] backupPayload = conversationInfo.getBackupPayload(); 222 if (backupPayload == null) { 223 return; 224 } 225 try { 226 conversationInfosOut.writeInt(backupPayload.length); 227 conversationInfosOut.write(backupPayload); 228 } catch (IOException e) { 229 Slog.e(TAG, "Failed to write conversation info to backup payload.", e); 230 } 231 }); 232 try { 233 conversationInfosOut.writeInt(CONVERSATION_INFOS_END_TOKEN); 234 } catch (IOException e) { 235 Slog.e(TAG, "Failed to write conversation infos end token to backup payload.", e); 236 return null; 237 } 238 return baos.toByteArray(); 239 } 240 restore(@onNull byte[] payload)241 void restore(@NonNull byte[] payload) { 242 DataInputStream in = new DataInputStream(new ByteArrayInputStream(payload)); 243 try { 244 for (int conversationInfoSize = in.readInt(); 245 conversationInfoSize != CONVERSATION_INFOS_END_TOKEN; 246 conversationInfoSize = in.readInt()) { 247 byte[] conversationInfoPayload = new byte[conversationInfoSize]; 248 in.readFully(conversationInfoPayload, 0, conversationInfoSize); 249 ConversationInfo conversationInfo = ConversationInfo.readFromBackupPayload( 250 conversationInfoPayload); 251 if (conversationInfo != null) { 252 addOrUpdate(conversationInfo); 253 } 254 } 255 } catch (IOException e) { 256 Slog.e(TAG, "Failed to read conversation info from payload.", e); 257 } 258 } 259 updateConversationsInMemory( @onNull ConversationInfo conversationInfo)260 private synchronized void updateConversationsInMemory( 261 @NonNull ConversationInfo conversationInfo) { 262 mConversationInfoMap.put(conversationInfo.getShortcutId(), conversationInfo); 263 264 LocusId locusId = conversationInfo.getLocusId(); 265 if (locusId != null) { 266 mLocusIdToShortcutIdMap.put(locusId, conversationInfo.getShortcutId()); 267 } 268 269 Uri contactUri = conversationInfo.getContactUri(); 270 if (contactUri != null) { 271 mContactUriToShortcutIdMap.put(contactUri, conversationInfo.getShortcutId()); 272 } 273 274 String phoneNumber = conversationInfo.getContactPhoneNumber(); 275 if (phoneNumber != null) { 276 mPhoneNumberToShortcutIdMap.put(phoneNumber, conversationInfo.getShortcutId()); 277 } 278 279 String notifChannelId = conversationInfo.getNotificationChannelId(); 280 if (notifChannelId != null) { 281 mNotifChannelIdToShortcutIdMap.put(notifChannelId, conversationInfo.getShortcutId()); 282 } 283 } 284 285 /** Schedules a dump of all conversations onto disk, overwriting existing values. */ 286 @MainThread scheduleUpdateConversationsOnDisk()287 private void scheduleUpdateConversationsOnDisk() { 288 ConversationInfosProtoDiskReadWriter conversationInfosProtoDiskReadWriter = 289 getConversationInfosProtoDiskReadWriter(); 290 if (conversationInfosProtoDiskReadWriter != null) { 291 List<ConversationInfo> conversations; 292 synchronized (this) { 293 conversations = new ArrayList<>(mConversationInfoMap.values()); 294 } 295 conversationInfosProtoDiskReadWriter.scheduleConversationsSave(conversations); 296 } 297 } 298 299 @Nullable getConversationInfosProtoDiskReadWriter()300 private ConversationInfosProtoDiskReadWriter getConversationInfosProtoDiskReadWriter() { 301 if (!mPackageDir.exists()) { 302 Slog.e(TAG, "Package data directory does not exist: " + mPackageDir.getAbsolutePath()); 303 return null; 304 } 305 if (mConversationInfosProtoDiskReadWriter == null) { 306 mConversationInfosProtoDiskReadWriter = new ConversationInfosProtoDiskReadWriter( 307 mPackageDir, CONVERSATIONS_FILE_NAME, mScheduledExecutorService); 308 } 309 return mConversationInfosProtoDiskReadWriter; 310 } 311 312 /** Reads and writes {@link ConversationInfo}s on disk. */ 313 private static class ConversationInfosProtoDiskReadWriter extends 314 AbstractProtoDiskReadWriter<List<ConversationInfo>> { 315 316 private final String mConversationInfoFileName; 317 ConversationInfosProtoDiskReadWriter(@onNull File rootDir, @NonNull String conversationInfoFileName, @NonNull ScheduledExecutorService scheduledExecutorService)318 ConversationInfosProtoDiskReadWriter(@NonNull File rootDir, 319 @NonNull String conversationInfoFileName, 320 @NonNull ScheduledExecutorService scheduledExecutorService) { 321 super(rootDir, scheduledExecutorService); 322 mConversationInfoFileName = conversationInfoFileName; 323 } 324 325 @Override protoStreamWriter()326 ProtoStreamWriter<List<ConversationInfo>> protoStreamWriter() { 327 return (protoOutputStream, data) -> { 328 for (ConversationInfo conversationInfo : data) { 329 long token = protoOutputStream.start(ConversationInfosProto.CONVERSATION_INFOS); 330 conversationInfo.writeToProto(protoOutputStream); 331 protoOutputStream.end(token); 332 } 333 }; 334 } 335 336 @Override protoStreamReader()337 ProtoStreamReader<List<ConversationInfo>> protoStreamReader() { 338 return protoInputStream -> { 339 List<ConversationInfo> results = Lists.newArrayList(); 340 try { 341 while (protoInputStream.nextField() != ProtoInputStream.NO_MORE_FIELDS) { 342 if (protoInputStream.getFieldNumber() 343 != (int) ConversationInfosProto.CONVERSATION_INFOS) { 344 continue; 345 } 346 long token = protoInputStream.start( 347 ConversationInfosProto.CONVERSATION_INFOS); 348 ConversationInfo conversationInfo = ConversationInfo.readFromProto( 349 protoInputStream); 350 protoInputStream.end(token); 351 results.add(conversationInfo); 352 } 353 } catch (IOException e) { 354 Slog.e(TAG, "Failed to read protobuf input stream.", e); 355 } 356 return results; 357 }; 358 } 359 360 /** 361 * Schedules a flush of the specified conversations to disk. 362 */ 363 @MainThread 364 void scheduleConversationsSave(@NonNull List<ConversationInfo> conversationInfos) { 365 scheduleSave(mConversationInfoFileName, conversationInfos); 366 } 367 368 /** 369 * Saves the specified conversations immediately. This should be used when device is 370 * powering off. 371 */ 372 @MainThread 373 void saveConversationsImmediately(@NonNull List<ConversationInfo> conversationInfos) { 374 saveImmediately(mConversationInfoFileName, conversationInfos); 375 } 376 377 @WorkerThread 378 void deleteConversationsFile() { 379 delete(mConversationInfoFileName); 380 } 381 } 382 } 383