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