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.IntDef;
20 import android.annotation.NonNull;
21 import android.annotation.Nullable;
22 import android.annotation.WorkerThread;
23 import android.net.Uri;
24 import android.util.ArrayMap;
25 
26 import com.android.internal.annotations.GuardedBy;
27 
28 import java.io.File;
29 import java.lang.annotation.Retention;
30 import java.lang.annotation.RetentionPolicy;
31 import java.util.ArrayList;
32 import java.util.List;
33 import java.util.Map;
34 import java.util.Set;
35 import java.util.concurrent.ScheduledExecutorService;
36 import java.util.function.Predicate;
37 
38 /** The store that stores and accesses the events data for a package. */
39 class EventStore {
40 
41     /** The events that are queryable with a shortcut ID. */
42     static final int CATEGORY_SHORTCUT_BASED = 0;
43 
44     /** The events that are queryable with a {@link android.content.LocusId}. */
45     static final int CATEGORY_LOCUS_ID_BASED = 1;
46 
47     /** The phone call events that are queryable with a phone number. */
48     static final int CATEGORY_CALL = 2;
49 
50     /** The SMS or MMS events that are queryable with a phone number. */
51     static final int CATEGORY_SMS = 3;
52 
53     /** The events that are queryable with an {@link android.app.Activity} class name. */
54     static final int CATEGORY_CLASS_BASED = 4;
55 
56     @IntDef(prefix = { "CATEGORY_" }, value = {
57             CATEGORY_SHORTCUT_BASED,
58             CATEGORY_LOCUS_ID_BASED,
59             CATEGORY_CALL,
60             CATEGORY_SMS,
61             CATEGORY_CLASS_BASED,
62     })
63     @Retention(RetentionPolicy.SOURCE)
64     @interface EventCategory {}
65 
66     @GuardedBy("this")
67     private final List<Map<String, EventHistoryImpl>> mEventHistoryMaps = new ArrayList<>();
68     private final List<File> mEventsCategoryDirs = new ArrayList<>();
69     private final ScheduledExecutorService mScheduledExecutorService;
70 
EventStore(@onNull File packageDir, @NonNull ScheduledExecutorService scheduledExecutorService)71     EventStore(@NonNull File packageDir,
72             @NonNull ScheduledExecutorService scheduledExecutorService) {
73         mEventHistoryMaps.add(CATEGORY_SHORTCUT_BASED, new ArrayMap<>());
74         mEventHistoryMaps.add(CATEGORY_LOCUS_ID_BASED, new ArrayMap<>());
75         mEventHistoryMaps.add(CATEGORY_CALL, new ArrayMap<>());
76         mEventHistoryMaps.add(CATEGORY_SMS, new ArrayMap<>());
77         mEventHistoryMaps.add(CATEGORY_CLASS_BASED, new ArrayMap<>());
78 
79         File eventDir = new File(packageDir, "event");
80         mEventsCategoryDirs.add(CATEGORY_SHORTCUT_BASED, new File(eventDir, "shortcut"));
81         mEventsCategoryDirs.add(CATEGORY_LOCUS_ID_BASED, new File(eventDir, "locus"));
82         mEventsCategoryDirs.add(CATEGORY_CALL, new File(eventDir, "call"));
83         mEventsCategoryDirs.add(CATEGORY_SMS, new File(eventDir, "sms"));
84         mEventsCategoryDirs.add(CATEGORY_CLASS_BASED, new File(eventDir, "class"));
85 
86         mScheduledExecutorService = scheduledExecutorService;
87     }
88 
89     /**
90      * Loads existing {@link EventHistoryImpl}s from disk. This should be called when device powers
91      * on and user is unlocked.
92      */
93     @WorkerThread
loadFromDisk()94     synchronized void loadFromDisk() {
95         for (@EventCategory int category = 0; category < mEventsCategoryDirs.size();
96                 category++) {
97             File categoryDir = mEventsCategoryDirs.get(category);
98             Map<String, EventHistoryImpl> existingEventHistoriesImpl =
99                     EventHistoryImpl.eventHistoriesImplFromDisk(categoryDir,
100                             mScheduledExecutorService);
101             mEventHistoryMaps.get(category).putAll(existingEventHistoriesImpl);
102         }
103     }
104 
105     /**
106      * Flushes all {@link EventHistoryImpl}s to disk. Should be called when device is shutting down.
107      */
saveToDisk()108     synchronized void saveToDisk() {
109         for (Map<String, EventHistoryImpl> map : mEventHistoryMaps) {
110             for (EventHistoryImpl eventHistory : map.values()) {
111                 eventHistory.saveToDisk();
112             }
113         }
114     }
115 
116     /**
117      * Gets the {@link EventHistory} for the specified key if exists.
118      *
119      * @param key Category-specific key, it can be shortcut ID, locus ID, phone number, or class
120      *            name.
121      */
122     @Nullable
getEventHistory(@ventCategory int category, String key)123     synchronized EventHistory getEventHistory(@EventCategory int category, String key) {
124         return mEventHistoryMaps.get(category).get(key);
125     }
126 
127     /**
128      * Gets the {@link EventHistoryImpl} for the specified ID or creates a new instance and put it
129      * into the store if not exists. The caller needs to verify if the associated conversation
130      * exists before calling this method.
131      *
132      * @param key Category-specific key, it can be shortcut ID, locus ID, phone number, or class
133      *            name.
134      */
135     @NonNull
getOrCreateEventHistory(@ventCategory int category, String key)136     synchronized EventHistoryImpl getOrCreateEventHistory(@EventCategory int category, String key) {
137         return mEventHistoryMaps.get(category).computeIfAbsent(key,
138                 k -> new EventHistoryImpl(
139                         new File(mEventsCategoryDirs.get(category), Uri.encode(key)),
140                         mScheduledExecutorService));
141     }
142 
143     /**
144      * Deletes the events and index data for the specified key.
145      *
146      * @param key Category-specific key, it can be shortcut ID, locus ID, phone number, or class
147      *            name.
148      */
deleteEventHistory(@ventCategory int category, String key)149     synchronized void deleteEventHistory(@EventCategory int category, String key) {
150         EventHistoryImpl eventHistory = mEventHistoryMaps.get(category).remove(key);
151         if (eventHistory != null) {
152             eventHistory.onDestroy();
153         }
154     }
155 
156     /** Deletes all the events and index data for the specified category from disk. */
deleteEventHistories(@ventCategory int category)157     synchronized void deleteEventHistories(@EventCategory int category) {
158         for (EventHistoryImpl eventHistory : mEventHistoryMaps.get(category).values()) {
159             eventHistory.onDestroy();
160         }
161         mEventHistoryMaps.get(category).clear();
162     }
163 
164     /** Deletes the events data that exceeds the retention period. */
pruneOldEvents()165     synchronized void pruneOldEvents() {
166         for (Map<String, EventHistoryImpl> map : mEventHistoryMaps) {
167             for (EventHistoryImpl eventHistory : map.values()) {
168                 eventHistory.pruneOldEvents();
169             }
170         }
171     }
172 
173     /**
174      * Prunes the event histories whose key (shortcut ID, locus ID or phone number) does not match
175      * any conversations.
176      *
177      * @param keyChecker Check whether there exists a conversation contains this key.
178      */
pruneOrphanEventHistories(@ventCategory int category, Predicate<String> keyChecker)179     synchronized void pruneOrphanEventHistories(@EventCategory int category,
180             Predicate<String> keyChecker) {
181         Set<String> keys = mEventHistoryMaps.get(category).keySet();
182         List<String> keysToDelete = new ArrayList<>();
183         for (String key : keys) {
184             if (!keyChecker.test(key)) {
185                 keysToDelete.add(key);
186             }
187         }
188         Map<String, EventHistoryImpl> eventHistoryMap = mEventHistoryMaps.get(category);
189         for (String key : keysToDelete) {
190             EventHistoryImpl eventHistory = eventHistoryMap.remove(key);
191             if (eventHistory != null) {
192                 eventHistory.onDestroy();
193             }
194         }
195     }
196 
onDestroy()197     synchronized void onDestroy() {
198         for (Map<String, EventHistoryImpl> map : mEventHistoryMaps) {
199             for (EventHistoryImpl eventHistory : map.values()) {
200                 eventHistory.onDestroy();
201             }
202         }
203     }
204 }
205