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