• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2018 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.networkstack.ipmemorystore;
18 
19 import static android.net.ipmemorystore.Status.ERROR_DATABASE_CANNOT_BE_OPENED;
20 import static android.net.ipmemorystore.Status.ERROR_GENERIC;
21 import static android.net.ipmemorystore.Status.ERROR_ILLEGAL_ARGUMENT;
22 import static android.net.ipmemorystore.Status.SUCCESS;
23 
24 import static com.android.networkstack.ipmemorystore.IpMemoryStoreDatabase.EXPIRY_ERROR;
25 import static com.android.networkstack.ipmemorystore.RegularMaintenanceJobService.InterruptMaintenance;
26 
27 import android.content.Context;
28 import android.database.SQLException;
29 import android.database.sqlite.SQLiteDatabase;
30 import android.net.IIpMemoryStore;
31 import android.net.ipmemorystore.Blob;
32 import android.net.ipmemorystore.IOnBlobRetrievedListener;
33 import android.net.ipmemorystore.IOnL2KeyResponseListener;
34 import android.net.ipmemorystore.IOnNetworkAttributesRetrievedListener;
35 import android.net.ipmemorystore.IOnNetworkEventCountRetrievedListener;
36 import android.net.ipmemorystore.IOnSameL3NetworkResponseListener;
37 import android.net.ipmemorystore.IOnStatusAndCountListener;
38 import android.net.ipmemorystore.IOnStatusListener;
39 import android.net.ipmemorystore.NetworkAttributes;
40 import android.net.ipmemorystore.NetworkAttributesParcelable;
41 import android.net.ipmemorystore.SameL3NetworkResponse;
42 import android.net.ipmemorystore.Status;
43 import android.net.ipmemorystore.StatusParcelable;
44 import android.os.RemoteException;
45 import android.util.Log;
46 
47 import androidx.annotation.NonNull;
48 import androidx.annotation.Nullable;
49 
50 import com.android.internal.annotations.VisibleForTesting;
51 
52 import java.io.File;
53 import java.util.concurrent.ExecutorService;
54 import java.util.concurrent.Executors;
55 
56 /**
57  * Implementation for the IP memory store.
58  * This component offers specialized services for network components to store and retrieve
59  * knowledge about networks, and provides intelligence that groups level 2 networks together
60  * into level 3 networks.
61  *
62  * @hide
63  */
64 public class IpMemoryStoreService extends IIpMemoryStore.Stub {
65     private static final String TAG = IpMemoryStoreService.class.getSimpleName();
66     private static final int DATABASE_SIZE_THRESHOLD = 10 * 1024 * 1024; //10MB
67     private static final int MAX_DROP_RECORD_TIMES = 500;
68     private static final int MIN_DELETE_NUM = 5;
69     private static final boolean DBG = true;
70 
71     // Error codes below are internal and used for notifying status beteween IpMemoryStore modules.
72     static final int ERROR_INTERNAL_BASE = -1_000_000_000;
73     // This error code is used for maintenance only to notify RegularMaintenanceJobService that
74     // full maintenance job has been interrupted.
75     static final int ERROR_INTERNAL_INTERRUPTED = ERROR_INTERNAL_BASE - 1;
76 
77     @NonNull
78     final Context mContext;
79     @Nullable
80     final SQLiteDatabase mDb;
81     @NonNull
82     final ExecutorService mExecutor;
83 
84     /**
85      * Construct an IpMemoryStoreService object.
86      * This constructor will block on disk access to open the database.
87      * @param context the context to access storage with.
88      */
IpMemoryStoreService(@onNull final Context context)89     public IpMemoryStoreService(@NonNull final Context context) {
90         // Before doing anything at all, rename the legacy database if necessary.
91         IpMemoryStoreDatabase.DbHelper.maybeRenameDatabaseFile(context);
92 
93         // Note that constructing the service will access the disk and block
94         // for some time, but it should make no difference to the clients. Because
95         // the interface is one-way, clients fire and forget requests, and the callback
96         // will get called eventually in any case, and the framework will wait for the
97         // service to be created to deliver subsequent requests.
98         // Avoiding this would mean the mDb member can't be final, which means the service would
99         // have to test for nullity, care for failure, and allow for a wait at every single access,
100         // which would make the code a lot more complex and require all methods to possibly block.
101         mContext = context;
102         SQLiteDatabase db;
103         final IpMemoryStoreDatabase.DbHelper helper = new IpMemoryStoreDatabase.DbHelper(context);
104         try {
105             db = helper.getWritableDatabase();
106             if (null == db) Log.e(TAG, "Unexpected null return of getWriteableDatabase");
107         } catch (final SQLException e) {
108             Log.e(TAG, "Can't open the Ip Memory Store database", e);
109             db = null;
110         } catch (final Exception e) {
111             Log.wtf(TAG, "Impossible exception Ip Memory Store database", e);
112             db = null;
113         }
114         mDb = db;
115         // The single thread executor guarantees that all work is executed sequentially on the
116         // same thread, and no two tasks can be active at the same time. This is required to
117         // ensure operations from multiple clients don't interfere with each other (in particular,
118         // operations involving a transaction must not run concurrently with other operations
119         // as the other operations might be taken as part of the transaction). By default, the
120         // single thread executor runs off an unbounded queue.
121         // TODO : investigate replacing this scheme with a scheme where each thread has its own
122         // instance of the database, as it may be faster. It is likely however that IpMemoryStore
123         // operations are mostly IO-bound anyway, and additional contention is unlikely to bring
124         // benefits. Alternatively, a read-write lock might increase throughput. Also if doing
125         // this work, care must be taken around the privacy-preserving VACUUM operations as
126         // VACUUM will fail if there are other open transactions at the same time, and using
127         // multiple threads will open the possibility of this failure happening, threatening
128         // the privacy guarantees.
129         mExecutor = Executors.newSingleThreadExecutor();
130         RegularMaintenanceJobService.schedule(mContext, this);
131     }
132 
133     /**
134      * Shutdown the memory store service, cancelling running tasks and dropping queued tasks.
135      *
136      * This is provided to give a way to clean up, and is meant to be available in case of an
137      * emergency shutdown.
138      */
shutdown()139     public void shutdown() {
140         // By contrast with ExecutorService#shutdown, ExecutorService#shutdownNow tries
141         // to cancel the existing tasks, and does not wait for completion. It does not
142         // guarantee the threads can be terminated in any given amount of time.
143         mExecutor.shutdownNow();
144         if (mDb != null) mDb.close();
145         RegularMaintenanceJobService.unschedule(mContext);
146     }
147 
148     /** Helper function to make a status object */
makeStatus(final int code)149     private StatusParcelable makeStatus(final int code) {
150         return new Status(code).toParcelable();
151     }
152 
153     /**
154      * Store network attributes for a given L2 key.
155      *
156      * @param l2Key The L2 key for the L2 network. Clients that don't know or care about the L2
157      *              key and only care about grouping can pass a unique ID here like the ones
158      *              generated by {@code java.util.UUID.randomUUID()}, but keep in mind the low
159      *              relevance of such a network will lead to it being evicted soon if it's not
160      *              refreshed. Use findL2Key to try and find a similar L2Key to these attributes.
161      * @param attributes The attributes for this network.
162      * @param listener A listener to inform of the completion of this call, or null if the client
163      *        is not interested in learning about success/failure.
164      * Through the listener, returns the L2 key. This is useful if the L2 key was not specified.
165      * If the call failed, the L2 key will be null.
166      */
167     // Note that while l2Key and attributes are non-null in spirit, they are received from
168     // another process. If the remote process decides to ignore everything and send null, this
169     // process should still not crash.
170     @Override
storeNetworkAttributes(@ullable final String l2Key, @Nullable final NetworkAttributesParcelable attributes, @Nullable final IOnStatusListener listener)171     public void storeNetworkAttributes(@Nullable final String l2Key,
172             @Nullable final NetworkAttributesParcelable attributes,
173             @Nullable final IOnStatusListener listener) {
174         // Because the parcelable is 100% mutable, the thread may not see its members initialized.
175         // Therefore either an immutable object is created on this same thread before it's passed
176         // to the executor, or there need to be a write barrier here and a read barrier in the
177         // remote thread.
178         final NetworkAttributes na = null == attributes ? null : new NetworkAttributes(attributes);
179         mExecutor.execute(() -> {
180             try {
181                 final int code = storeNetworkAttributesAndBlobSync(l2Key, na,
182                         null /* clientId */, null /* name */, null /* data */);
183                 if (null != listener) listener.onComplete(makeStatus(code));
184             } catch (final RemoteException e) {
185                 // Client at the other end died
186             }
187         });
188     }
189 
190     /**
191      * Store a binary blob associated with an L2 key and a name.
192      *
193      * @param l2Key The L2 key for this network.
194      * @param clientId The ID of the client.
195      * @param name The name of this data.
196      * @param blob The data to store.
197      * @param listener The listener that will be invoked to return the answer, or null if the
198      *        is not interested in learning about success/failure.
199      * Through the listener, returns a status to indicate success or failure.
200      */
201     @Override
storeBlob(@ullable final String l2Key, @Nullable final String clientId, @Nullable final String name, @Nullable final Blob blob, @Nullable final IOnStatusListener listener)202     public void storeBlob(@Nullable final String l2Key, @Nullable final String clientId,
203             @Nullable final String name, @Nullable final Blob blob,
204             @Nullable final IOnStatusListener listener) {
205         final byte[] data = null == blob ? null : blob.data;
206         mExecutor.execute(() -> {
207             try {
208                 final int code = storeNetworkAttributesAndBlobSync(l2Key,
209                         null /* NetworkAttributes */, clientId, name, data);
210                 if (null != listener) listener.onComplete(makeStatus(code));
211             } catch (final RemoteException e) {
212                 // Client at the other end died
213             }
214         });
215     }
216 
217     /**
218      * Helper method for storeNetworkAttributes and storeBlob.
219      *
220      * Either attributes or none of clientId, name and data may be null. This will write the
221      * passed data if non-null, and will write attributes if non-null, but in any case it will
222      * bump the relevance up.
223      * Returns a success code from Status.
224      */
storeNetworkAttributesAndBlobSync(@ullable final String l2Key, @Nullable final NetworkAttributes attributes, @Nullable final String clientId, @Nullable final String name, @Nullable final byte[] data)225     private int storeNetworkAttributesAndBlobSync(@Nullable final String l2Key,
226             @Nullable final NetworkAttributes attributes,
227             @Nullable final String clientId,
228             @Nullable final String name, @Nullable final byte[] data) {
229         if (null == l2Key) return ERROR_ILLEGAL_ARGUMENT;
230         if (null == attributes && null == data) return ERROR_ILLEGAL_ARGUMENT;
231         if (null != data && (null == clientId || null == name)) return ERROR_ILLEGAL_ARGUMENT;
232         if (null == mDb) return ERROR_DATABASE_CANNOT_BE_OPENED;
233         try {
234             final long oldExpiry = IpMemoryStoreDatabase.getExpiry(mDb, l2Key);
235             final long newExpiry = RelevanceUtils.bumpExpiryDate(
236                     oldExpiry == EXPIRY_ERROR ? System.currentTimeMillis() : oldExpiry);
237             final int errorCode =
238                     IpMemoryStoreDatabase.storeNetworkAttributes(mDb, l2Key, newExpiry, attributes);
239             // If no blob to store, the client is interested in the result of storing the attributes
240             if (null == data) return errorCode;
241             // Otherwise it's interested in the result of storing the blob
242             return IpMemoryStoreDatabase.storeBlob(mDb, l2Key, clientId, name, data);
243         } catch (Exception e) {
244             if (DBG) {
245                 Log.e(TAG, "Exception while storing for key {" + l2Key
246                         + "} ; NetworkAttributes {" + (null == attributes ? "null" : attributes)
247                         + "} ; clientId {" + (null == clientId ? "null" : clientId)
248                         + "} ; name {" + (null == name ? "null" : name)
249                         + "} ; data {" + Utils.byteArrayToString(data) + "}", e);
250             }
251         }
252         return ERROR_GENERIC;
253     }
254 
255     /**
256      * Returns the best L2 key associated with the attributes.
257      *
258      * This will find a record that would be in the same group as the passed attributes. This is
259      * useful to choose the key for storing a sample or private data when the L2 key is not known.
260      * If multiple records are group-close to these attributes, the closest match is returned.
261      * If multiple records have the same closeness, the one with the smaller (unicode codepoint
262      * order) L2 key is returned.
263      * If no record matches these attributes, null is returned.
264      *
265      * @param attributes The attributes of the network to find.
266      * @param listener The listener that will be invoked to return the answer.
267      * Through the listener, returns the L2 key if one matched, or null.
268      */
269     @Override
findL2Key(@ullable final NetworkAttributesParcelable attributes, @Nullable final IOnL2KeyResponseListener listener)270     public void findL2Key(@Nullable final NetworkAttributesParcelable attributes,
271             @Nullable final IOnL2KeyResponseListener listener) {
272         if (null == listener) return;
273         mExecutor.execute(() -> {
274             try {
275                 if (null == attributes) {
276                     listener.onL2KeyResponse(makeStatus(ERROR_ILLEGAL_ARGUMENT), null);
277                     return;
278                 }
279                 if (null == mDb) {
280                     listener.onL2KeyResponse(makeStatus(ERROR_ILLEGAL_ARGUMENT), null);
281                     return;
282                 }
283                 final String key = IpMemoryStoreDatabase.findClosestAttributes(mDb,
284                         new NetworkAttributes(attributes));
285                 listener.onL2KeyResponse(makeStatus(SUCCESS), key);
286             } catch (final RemoteException e) {
287                 // Client at the other end died
288             }
289         });
290     }
291 
292     /**
293      * Returns whether, to the best of the store's ability to tell, the two specified L2 keys point
294      * to the same L3 network. Group-closeness is used to determine this.
295      *
296      * @param l2Key1 The key for the first network.
297      * @param l2Key2 The key for the second network.
298      * @param listener The listener that will be invoked to return the answer.
299      * Through the listener, a SameL3NetworkResponse containing the answer and confidence.
300      */
301     @Override
isSameNetwork(@ullable final String l2Key1, @Nullable final String l2Key2, @Nullable final IOnSameL3NetworkResponseListener listener)302     public void isSameNetwork(@Nullable final String l2Key1, @Nullable final String l2Key2,
303             @Nullable final IOnSameL3NetworkResponseListener listener) {
304         if (null == listener) return;
305         mExecutor.execute(() -> {
306             try {
307                 if (null == l2Key1 || null == l2Key2) {
308                     listener.onSameL3NetworkResponse(makeStatus(ERROR_ILLEGAL_ARGUMENT), null);
309                     return;
310                 }
311                 if (null == mDb) {
312                     listener.onSameL3NetworkResponse(makeStatus(ERROR_ILLEGAL_ARGUMENT), null);
313                     return;
314                 }
315                 try {
316                     final NetworkAttributes attr1 =
317                             IpMemoryStoreDatabase.retrieveNetworkAttributes(mDb, l2Key1);
318                     final NetworkAttributes attr2 =
319                             IpMemoryStoreDatabase.retrieveNetworkAttributes(mDb, l2Key2);
320                     if (null == attr1 || null == attr2) {
321                         listener.onSameL3NetworkResponse(makeStatus(SUCCESS),
322                                 new SameL3NetworkResponse(l2Key1, l2Key2,
323                                         -1f /* never connected */).toParcelable());
324                         return;
325                     }
326                     final float confidence = attr1.getNetworkGroupSamenessConfidence(attr2);
327                     listener.onSameL3NetworkResponse(makeStatus(SUCCESS),
328                             new SameL3NetworkResponse(l2Key1, l2Key2, confidence).toParcelable());
329                 } catch (Exception e) {
330                     listener.onSameL3NetworkResponse(makeStatus(ERROR_GENERIC), null);
331                 }
332             } catch (final RemoteException e) {
333                 // Client at the other end died
334             }
335         });
336     }
337 
338     /**
339      * Retrieve the network attributes for a key.
340      * If no record is present for this key, this will return null attributes.
341      *
342      * @param l2Key The key of the network to query.
343      * @param listener The listener that will be invoked to return the answer.
344      * Through the listener, returns the network attributes and the L2 key associated with
345      *         the query.
346      */
347     @Override
retrieveNetworkAttributes(@ullable final String l2Key, @Nullable final IOnNetworkAttributesRetrievedListener listener)348     public void retrieveNetworkAttributes(@Nullable final String l2Key,
349             @Nullable final IOnNetworkAttributesRetrievedListener listener) {
350         if (null == listener) return;
351         mExecutor.execute(() -> {
352             try {
353                 if (null == l2Key) {
354                     listener.onNetworkAttributesRetrieved(
355                             makeStatus(ERROR_ILLEGAL_ARGUMENT), l2Key, null);
356                     return;
357                 }
358                 if (null == mDb) {
359                     listener.onNetworkAttributesRetrieved(
360                             makeStatus(ERROR_DATABASE_CANNOT_BE_OPENED), l2Key, null);
361                     return;
362                 }
363                 try {
364                     final NetworkAttributes attributes =
365                             IpMemoryStoreDatabase.retrieveNetworkAttributes(mDb, l2Key);
366                     listener.onNetworkAttributesRetrieved(makeStatus(SUCCESS), l2Key,
367                             null == attributes ? null : attributes.toParcelable());
368                 } catch (final Exception e) {
369                     listener.onNetworkAttributesRetrieved(makeStatus(ERROR_GENERIC), l2Key, null);
370                 }
371             } catch (final RemoteException e) {
372                 // Client at the other end died
373             }
374         });
375     }
376 
377     /**
378      * Retrieve previously stored private data.
379      * If no data was stored for this L2 key and name this will return null.
380      *
381      * @param l2Key The L2 key.
382      * @param clientId The id of the client that stored this data.
383      * @param name The name of the data.
384      * @param listener The listener that will be invoked to return the answer.
385      * Through the listener, returns the private data if any or null if none, with the L2 key
386      *         and the name of the data associated with the query.
387      */
388     @Override
retrieveBlob(@onNull final String l2Key, @NonNull final String clientId, @NonNull final String name, @NonNull final IOnBlobRetrievedListener listener)389     public void retrieveBlob(@NonNull final String l2Key, @NonNull final String clientId,
390             @NonNull final String name, @NonNull final IOnBlobRetrievedListener listener) {
391         if (null == listener) return;
392         mExecutor.execute(() -> {
393             try {
394                 if (null == l2Key) {
395                     listener.onBlobRetrieved(makeStatus(ERROR_ILLEGAL_ARGUMENT), l2Key, name, null);
396                     return;
397                 }
398                 if (null == mDb) {
399                     listener.onBlobRetrieved(makeStatus(ERROR_DATABASE_CANNOT_BE_OPENED), l2Key,
400                             name, null);
401                     return;
402                 }
403                 try {
404                     final Blob b = new Blob();
405                     b.data = IpMemoryStoreDatabase.retrieveBlob(mDb, l2Key, clientId, name);
406                     listener.onBlobRetrieved(makeStatus(SUCCESS), l2Key, name, b);
407                 } catch (final Exception e) {
408                     listener.onBlobRetrieved(makeStatus(ERROR_GENERIC), l2Key, name, null);
409                 }
410             } catch (final RemoteException e) {
411                 // Client at the other end died
412             }
413         });
414     }
415 
416     /**
417      * Delete a single entry.
418      *
419      * @param l2Key The L2 key of the entry to delete.
420      * @param needWipe Whether the data must be wiped from disk immediately for security reasons.
421      *                 This is very expensive and makes no functional difference ; only pass
422      *                 true if security requires this data must be removed from disk immediately.
423      * @param listener A listener that will be invoked to inform of the completion of this call,
424      *                 or null if the client is not interested in learning about success/failure.
425      * returns (through the listener) A status to indicate success and the number of deleted records
426      */
delete(@onNull final String l2Key, final boolean needWipe, @Nullable final IOnStatusAndCountListener listener)427     public void delete(@NonNull final String l2Key, final boolean needWipe,
428             @Nullable final IOnStatusAndCountListener listener) {
429         mExecutor.execute(() -> {
430             try {
431                 if (null == mDb) {
432                     if (null != listener) {
433                         listener.onComplete(makeStatus(ERROR_DATABASE_CANNOT_BE_OPENED), 0);
434                     }
435                     return;
436                 }
437                 final StatusAndCount res = IpMemoryStoreDatabase.delete(mDb, l2Key, needWipe);
438                 if (null != listener) listener.onComplete(makeStatus(res.status), res.count);
439             } catch (final RemoteException e) {
440                 // Client at the other end died
441             }
442         });
443     }
444 
445     /**
446      * Delete all entries in a cluster.
447      *
448      * This method will delete all entries in the memory store that have the cluster attribute
449      * passed as an argument.
450      *
451      * @param cluster The cluster to delete.
452      * @param needWipe Whether the data must be wiped from disk immediately for security reasons.
453      *                 This is very expensive and makes no functional difference ; only pass
454      *                 true if security requires this data must be removed from disk immediately.
455      * @param listener A listener that will be invoked to inform of the completion of this call,
456      *                 or null if the client is not interested in learning about success/failure.
457      * returns (through the listener) A status to indicate success and the number of deleted records
458      */
deleteCluster(@onNull final String cluster, final boolean needWipe, @Nullable final IOnStatusAndCountListener listener)459     public void deleteCluster(@NonNull final String cluster, final boolean needWipe,
460             @Nullable final IOnStatusAndCountListener listener) {
461         mExecutor.execute(() -> {
462             try {
463                 if (null == mDb) {
464                     if (null != listener) {
465                         listener.onComplete(makeStatus(ERROR_DATABASE_CANNOT_BE_OPENED), 0);
466                     }
467                     return;
468                 }
469                 final StatusAndCount res =
470                         IpMemoryStoreDatabase.deleteCluster(mDb, cluster, needWipe);
471                 if (null != listener) listener.onComplete(makeStatus(res.status), res.count);
472             } catch (final RemoteException e) {
473                 // Client at the other end died
474             }
475         });
476     }
477 
478     /**
479      * Wipe the data in IpMemoryStore database upon network factory reset.
480      */
481     @Override
factoryReset()482     public void factoryReset() {
483         mExecutor.execute(() -> {
484             if (null == mDb) {
485                 return;
486             }
487             IpMemoryStoreDatabase.wipeDataUponNetworkReset(mDb);
488         });
489     }
490 
491     /**
492      * Retrieve the specific network event counts for a given cluster and event type since one or
493      * more timestamps in the past.
494      *
495      * @param cluster The cluster to query.
496      * @param sinceTimes An array of timestamps in the past. The query will return an array of
497      *                   equal size. Each element in the array will contain the number of network
498      *                   events between the corresponding timestamp and the current time, e.g. query
499      *                   since the last week and/or the last day.
500      * @param eventTypes An array of network event types to query, which can be one or more of the
501      *                   above NETWORK_EVENT constants.
502      * @param listener The listener that will be invoked to return the answer.
503      * returns (through the listener) The event counts associated with the query, or an empty array
504      *                                if the query failed.
505      */
506     @Override
retrieveNetworkEventCount(@onNull final String cluster, @NonNull final long[] sinceTimes, @NonNull final int[] eventTypes, @Nullable final IOnNetworkEventCountRetrievedListener listener)507     public void retrieveNetworkEventCount(@NonNull final String cluster,
508             @NonNull final long[] sinceTimes,
509             @NonNull final int[] eventTypes,
510             @Nullable final IOnNetworkEventCountRetrievedListener listener) {
511         if (null == listener) return;
512         mExecutor.execute(() -> {
513             try {
514                 if (null == cluster) {
515                     listener.onNetworkEventCountRetrieved(
516                             makeStatus(ERROR_ILLEGAL_ARGUMENT), new int[0] /* counts */);
517                     return;
518                 }
519                 if (0 == sinceTimes.length) {
520                     listener.onNetworkEventCountRetrieved(
521                             makeStatus(ERROR_ILLEGAL_ARGUMENT), new int[0] /* counts */);
522                     return;
523                 }
524                 if (null == mDb) {
525                     listener.onNetworkEventCountRetrieved(
526                             makeStatus(ERROR_DATABASE_CANNOT_BE_OPENED), new int[0] /* counts */);
527                     return;
528                 }
529                 try {
530                     final int[] counts = IpMemoryStoreDatabase.retrieveNetworkEventCount(mDb,
531                             cluster, sinceTimes, eventTypes);
532                     listener.onNetworkEventCountRetrieved(makeStatus(SUCCESS), counts);
533                 } catch (final Exception e) {
534                     listener.onNetworkEventCountRetrieved(makeStatus(ERROR_GENERIC),
535                             new int[0] /* counts */);
536                 }
537             } catch (final RemoteException e) {
538                 // Client at the other end died
539             }
540         });
541     }
542 
543     /**
544      * Store a specific network event to database for a given cluster.
545      *
546      * @param cluster The cluster representing a notion of network group (e.g., BSSIDs with the
547      *                same SSID).
548      * @param timestamp The timestamp {@link System.currentTimeMillis} when a specific network
549      *                  event occurred.
550      * @param expiry The timestamp {@link System.currentTimeMillis} when a specific network
551      *               event stored in the database expires, e.g. it might be one week from now.
552      * @param eventType One of the NETWORK_EVENT constants above.
553      * @param listener A listener that will be invoked to inform of the completion of this call.
554      * returns (through the listener) A status to indicate success or failure.
555      */
556     @Override
storeNetworkEvent(@onNull final String cluster, final long timestamp, final long expiry, final int eventType, @Nullable final IOnStatusListener listener)557     public void storeNetworkEvent(@NonNull final String cluster,
558             final long timestamp,
559             final long expiry,
560             final int eventType,
561             @Nullable final IOnStatusListener listener) {
562         mExecutor.execute(() -> {
563             try {
564                 if (null == cluster) {
565                     listener.onComplete(makeStatus(ERROR_ILLEGAL_ARGUMENT));
566                     return;
567                 }
568                 if (null == mDb) {
569                     listener.onComplete(makeStatus(ERROR_DATABASE_CANNOT_BE_OPENED));
570                     return;
571                 }
572                 try {
573                     final int code = IpMemoryStoreDatabase.storeNetworkEvent(mDb, cluster,
574                             timestamp, expiry, eventType);
575                     if (null != listener) listener.onComplete(makeStatus(code));
576                 } catch (final Exception e) {
577                     if (null != listener) listener.onComplete(makeStatus(ERROR_GENERIC));
578                 }
579             } catch (final RemoteException e) {
580                 // Client at the other end died
581             }
582         });
583     }
584 
585     /** Get db size threshold. */
586     @VisibleForTesting
getDbSizeThreshold()587     protected int getDbSizeThreshold() {
588         return DATABASE_SIZE_THRESHOLD;
589     }
590 
getDbSize()591     private long getDbSize() {
592         if (null == mDb) {
593             return 0;
594         }
595         final File dbFile = new File(mDb.getPath());
596         try {
597             return dbFile.length();
598         } catch (final SecurityException e) {
599             if (DBG) Log.e(TAG, "Read db size access deny.", e);
600             // Return zero value if can't get disk usage exactly.
601             return 0;
602         }
603     }
604 
605     /** Check if db size is over the threshold. */
606     @VisibleForTesting
isDbSizeOverThreshold()607     boolean isDbSizeOverThreshold() {
608         return getDbSize() > getDbSizeThreshold();
609     }
610 
611     /**
612      * Full maintenance.
613      *
614      * @param listener A listener to inform of the completion of this call.
615      */
fullMaintenance(@onNull final IOnStatusListener listener, @NonNull final InterruptMaintenance interrupt)616     void fullMaintenance(@NonNull final IOnStatusListener listener,
617             @NonNull final InterruptMaintenance interrupt) {
618         mExecutor.execute(() -> {
619             try {
620                 if (null == mDb) {
621                     listener.onComplete(makeStatus(ERROR_DATABASE_CANNOT_BE_OPENED));
622                     return;
623                 }
624 
625                 // Interrupt maintenance because the scheduling job has been canceled.
626                 if (checkForInterrupt(listener, interrupt)) return;
627 
628                 int result = SUCCESS;
629                 // Drop all records whose relevance has decayed to zero.
630                 // This is the first step to decrease memory store size.
631                 result = IpMemoryStoreDatabase.dropAllExpiredRecords(mDb);
632 
633                 if (checkForInterrupt(listener, interrupt)) return;
634 
635                 // Aggregate historical data in passes
636                 // TODO : Waiting for historical data implement.
637 
638                 // Check if db size meets the storage goal(10MB). If not, keep dropping records and
639                 // aggregate historical data until the storage goal is met. Use for loop with 500
640                 // times restriction to prevent infinite loop (Deleting records always fail and db
641                 // size is still over the threshold)
642                 for (int i = 0; isDbSizeOverThreshold() && i < MAX_DROP_RECORD_TIMES; i++) {
643                     if (checkForInterrupt(listener, interrupt)) return;
644 
645                     final int totalNumber = IpMemoryStoreDatabase.getTotalRecordNumber(mDb);
646                     final long dbSize = getDbSize();
647                     final float decreaseRate = (dbSize == 0)
648                             ? 0 : (float) (dbSize - getDbSizeThreshold()) / (float) dbSize;
649                     final int deleteNumber = Math.max(
650                             (int) (totalNumber * decreaseRate), MIN_DELETE_NUM);
651 
652                     result = IpMemoryStoreDatabase.dropNumberOfRecords(mDb, deleteNumber);
653 
654                     if (checkForInterrupt(listener, interrupt)) return;
655 
656                     // Aggregate historical data
657                     // TODO : Waiting for historical data implement.
658                 }
659                 listener.onComplete(makeStatus(result));
660             } catch (final RemoteException e) {
661                 // Client at the other end died
662             }
663         });
664     }
665 
checkForInterrupt(@onNull final IOnStatusListener listener, @NonNull final InterruptMaintenance interrupt)666     private boolean checkForInterrupt(@NonNull final IOnStatusListener listener,
667             @NonNull final InterruptMaintenance interrupt) throws RemoteException {
668         if (!interrupt.isInterrupted()) return false;
669         listener.onComplete(makeStatus(ERROR_INTERNAL_INTERRUPTED));
670         return true;
671     }
672 
673     @Override
getInterfaceVersion()674     public int getInterfaceVersion() {
675         return this.VERSION;
676     }
677 
678     @Override
getInterfaceHash()679     public String getInterfaceHash() {
680         return this.HASH;
681     }
682 }
683