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 static com.android.server.people.data.EventStore.CATEGORY_CALL;
20 import static com.android.server.people.data.EventStore.CATEGORY_CLASS_BASED;
21 import static com.android.server.people.data.EventStore.CATEGORY_LOCUS_ID_BASED;
22 import static com.android.server.people.data.EventStore.CATEGORY_SHORTCUT_BASED;
23 import static com.android.server.people.data.EventStore.CATEGORY_SMS;
24 
25 import android.annotation.NonNull;
26 import android.annotation.Nullable;
27 import android.annotation.UserIdInt;
28 import android.annotation.WorkerThread;
29 import android.content.LocusId;
30 import android.os.FileUtils;
31 import android.text.TextUtils;
32 import android.util.ArrayMap;
33 
34 import java.io.File;
35 import java.util.Map;
36 import java.util.concurrent.ScheduledExecutorService;
37 import java.util.function.Consumer;
38 import java.util.function.Predicate;
39 
40 /** The data associated with a package. */
41 public class PackageData {
42 
43     @NonNull
44     private final String mPackageName;
45 
46     private final @UserIdInt int mUserId;
47 
48     @NonNull
49     private final ConversationStore mConversationStore;
50 
51     @NonNull
52     private final EventStore mEventStore;
53 
54     private final Predicate<String> mIsDefaultDialerPredicate;
55 
56     private final Predicate<String> mIsDefaultSmsAppPredicate;
57 
58     private final File mPackageDataDir;
59 
PackageData(@onNull String packageName, @UserIdInt int userId, @NonNull Predicate<String> isDefaultDialerPredicate, @NonNull Predicate<String> isDefaultSmsAppPredicate, @NonNull ScheduledExecutorService scheduledExecutorService, @NonNull File perUserPeopleDataDir)60     PackageData(@NonNull String packageName, @UserIdInt int userId,
61             @NonNull Predicate<String> isDefaultDialerPredicate,
62             @NonNull Predicate<String> isDefaultSmsAppPredicate,
63             @NonNull ScheduledExecutorService scheduledExecutorService,
64             @NonNull File perUserPeopleDataDir) {
65         mPackageName = packageName;
66         mUserId = userId;
67 
68         mPackageDataDir = new File(perUserPeopleDataDir, mPackageName);
69         mPackageDataDir.mkdirs();
70 
71         mConversationStore = new ConversationStore(mPackageDataDir, scheduledExecutorService);
72         mEventStore = new EventStore(mPackageDataDir, scheduledExecutorService);
73         mIsDefaultDialerPredicate = isDefaultDialerPredicate;
74         mIsDefaultSmsAppPredicate = isDefaultSmsAppPredicate;
75     }
76 
77     /**
78      * Returns a map of package directory names as keys and their associated {@link PackageData}.
79      * This should be called when device is powered on and unlocked.
80      */
81     @WorkerThread
82     @NonNull
packagesDataFromDisk(@serIdInt int userId, @NonNull Predicate<String> isDefaultDialerPredicate, @NonNull Predicate<String> isDefaultSmsAppPredicate, @NonNull ScheduledExecutorService scheduledExecutorService, @NonNull File perUserPeopleDataDir)83     static Map<String, PackageData> packagesDataFromDisk(@UserIdInt int userId,
84             @NonNull Predicate<String> isDefaultDialerPredicate,
85             @NonNull Predicate<String> isDefaultSmsAppPredicate,
86             @NonNull ScheduledExecutorService scheduledExecutorService,
87             @NonNull File perUserPeopleDataDir) {
88         Map<String, PackageData> results = new ArrayMap<>();
89         File[] packageDirs = perUserPeopleDataDir.listFiles(File::isDirectory);
90         if (packageDirs == null) {
91             return results;
92         }
93         for (File packageDir : packageDirs) {
94             PackageData packageData = new PackageData(packageDir.getName(), userId,
95                     isDefaultDialerPredicate, isDefaultSmsAppPredicate, scheduledExecutorService,
96                     perUserPeopleDataDir);
97             packageData.loadFromDisk();
98             results.put(packageDir.getName(), packageData);
99         }
100         return results;
101     }
102 
loadFromDisk()103     private void loadFromDisk() {
104         mConversationStore.loadConversationsFromDisk();
105         mEventStore.loadFromDisk();
106     }
107 
108     /** Called when device is shutting down. */
saveToDisk()109     void saveToDisk() {
110         mConversationStore.saveConversationsToDisk();
111         mEventStore.saveToDisk();
112     }
113 
114     @NonNull
getPackageName()115     public String getPackageName() {
116         return mPackageName;
117     }
118 
getUserId()119     public @UserIdInt int getUserId() {
120         return mUserId;
121     }
122 
123     /** Iterates over all the conversations in this package. */
forAllConversations(@onNull Consumer<ConversationInfo> consumer)124     public void forAllConversations(@NonNull Consumer<ConversationInfo> consumer) {
125         mConversationStore.forAllConversations(consumer);
126     }
127 
128     /**
129      * Gets the {@link ConversationInfo} for a given shortcut ID. Returns null if such as {@link
130      * ConversationInfo} does not exist.
131      */
132     @Nullable
getConversationInfo(@onNull String shortcutId)133     public ConversationInfo getConversationInfo(@NonNull String shortcutId) {
134         return getConversationStore().getConversation(shortcutId);
135     }
136 
137     /**
138      * Gets the combined {@link EventHistory} for a given shortcut ID. This returned {@link
139      * EventHistory} has events of all types, no matter whether they're annotated with shortcut ID,
140      * Locus ID, or phone number etc.
141      */
142     @NonNull
getEventHistory(@onNull String shortcutId)143     public EventHistory getEventHistory(@NonNull String shortcutId) {
144         AggregateEventHistoryImpl result = new AggregateEventHistoryImpl();
145 
146         ConversationInfo conversationInfo = mConversationStore.getConversation(shortcutId);
147         if (conversationInfo == null) {
148             return result;
149         }
150 
151         EventHistory shortcutEventHistory = getEventStore().getEventHistory(
152                 CATEGORY_SHORTCUT_BASED, shortcutId);
153         if (shortcutEventHistory != null) {
154             result.addEventHistory(shortcutEventHistory);
155         }
156 
157         LocusId locusId = conversationInfo.getLocusId();
158         if (locusId != null) {
159             EventHistory locusEventHistory = getEventStore().getEventHistory(
160                     CATEGORY_LOCUS_ID_BASED, locusId.getId());
161             if (locusEventHistory != null) {
162                 result.addEventHistory(locusEventHistory);
163             }
164         }
165 
166         String phoneNumber = conversationInfo.getContactPhoneNumber();
167         if (TextUtils.isEmpty(phoneNumber)) {
168             return result;
169         }
170         if (isDefaultDialer()) {
171             EventHistory callEventHistory = getEventStore().getEventHistory(
172                     CATEGORY_CALL, phoneNumber);
173             if (callEventHistory != null) {
174                 result.addEventHistory(callEventHistory);
175             }
176         }
177         if (isDefaultSmsApp()) {
178             EventHistory smsEventHistory = getEventStore().getEventHistory(
179                     CATEGORY_SMS, phoneNumber);
180             if (smsEventHistory != null) {
181                 result.addEventHistory(smsEventHistory);
182             }
183         }
184         return result;
185     }
186 
187     /** Gets the {@link EventHistory} for a given Activity class. */
188     @NonNull
getClassLevelEventHistory(String className)189     public EventHistory getClassLevelEventHistory(String className) {
190         EventHistory eventHistory = getEventStore().getEventHistory(
191                 CATEGORY_CLASS_BASED, className);
192         return eventHistory != null ? eventHistory : new AggregateEventHistoryImpl();
193     }
194 
isDefaultDialer()195     public boolean isDefaultDialer() {
196         return mIsDefaultDialerPredicate.test(mPackageName);
197     }
198 
isDefaultSmsApp()199     public boolean isDefaultSmsApp() {
200         return mIsDefaultSmsAppPredicate.test(mPackageName);
201     }
202 
203     @NonNull
getConversationStore()204     ConversationStore getConversationStore() {
205         return mConversationStore;
206     }
207 
208     @NonNull
getEventStore()209     EventStore getEventStore() {
210         return mEventStore;
211     }
212 
213     /**
214      * Deletes all the data (including conversation, events and index) for the specified
215      * conversation shortcut ID.
216      */
deleteDataForConversation(String shortcutId)217     void deleteDataForConversation(String shortcutId) {
218         ConversationInfo conversationInfo = mConversationStore.deleteConversation(shortcutId);
219         if (conversationInfo == null) {
220             return;
221         }
222         mEventStore.deleteEventHistory(CATEGORY_SHORTCUT_BASED, shortcutId);
223         if (conversationInfo.getLocusId() != null) {
224             mEventStore.deleteEventHistory(
225                     CATEGORY_LOCUS_ID_BASED, conversationInfo.getLocusId().getId());
226         }
227         String phoneNumber = conversationInfo.getContactPhoneNumber();
228         if (!TextUtils.isEmpty(phoneNumber)) {
229             if (isDefaultDialer()) {
230                 mEventStore.deleteEventHistory(CATEGORY_CALL, phoneNumber);
231             }
232             if (isDefaultSmsApp()) {
233                 mEventStore.deleteEventHistory(CATEGORY_SMS, phoneNumber);
234             }
235         }
236     }
237 
238     /** Prunes the events and index data that don't have a associated conversation. */
pruneOrphanEvents()239     void pruneOrphanEvents() {
240         mEventStore.pruneOrphanEventHistories(CATEGORY_SHORTCUT_BASED,
241                 key -> mConversationStore.getConversation(key) != null);
242         mEventStore.pruneOrphanEventHistories(CATEGORY_LOCUS_ID_BASED,
243                 key -> mConversationStore.getConversationByLocusId(new LocusId(key)) != null);
244         if (isDefaultDialer()) {
245             mEventStore.pruneOrphanEventHistories(CATEGORY_CALL,
246                     key -> mConversationStore.getConversationByPhoneNumber(key) != null);
247         }
248         if (isDefaultSmsApp()) {
249             mEventStore.pruneOrphanEventHistories(CATEGORY_SMS,
250                     key -> mConversationStore.getConversationByPhoneNumber(key) != null);
251         }
252     }
253 
onDestroy()254     void onDestroy() {
255         mEventStore.onDestroy();
256         mConversationStore.onDestroy();
257         FileUtils.deleteContentsAndDir(mPackageDataDir);
258     }
259 }
260