Chalard Jean | 8c141bd | 2018-12-04 20:20:56 +0900 | [diff] [blame] | 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 | |
Chalard Jean | 9521351 | 2019-01-30 21:04:58 +0900 | [diff] [blame] | 17 | package com.android.server.connectivity.ipmemorystore; |
Chalard Jean | 8c141bd | 2018-12-04 20:20:56 +0900 | [diff] [blame] | 18 | |
Chalard Jean | 91549b6 | 2018-12-18 22:05:19 +0900 | [diff] [blame] | 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; |
Chalard Jean | bf73e66 | 2018-12-27 20:59:41 +0900 | [diff] [blame] | 22 | import static android.net.ipmemorystore.Status.SUCCESS; |
Chalard Jean | 91549b6 | 2018-12-18 22:05:19 +0900 | [diff] [blame] | 23 | |
Chalard Jean | 9521351 | 2019-01-30 21:04:58 +0900 | [diff] [blame] | 24 | import static com.android.server.connectivity.ipmemorystore.IpMemoryStoreDatabase.EXPIRY_ERROR; |
paulhu | 028d7a5 | 2019-03-26 01:39:10 +0800 | [diff] [blame^] | 25 | import static com.android.server.connectivity.ipmemorystore.RegularMaintenanceJobService.InterruptMaintenance; |
Chalard Jean | 91549b6 | 2018-12-18 22:05:19 +0900 | [diff] [blame] | 26 | |
Chalard Jean | 8c141bd | 2018-12-04 20:20:56 +0900 | [diff] [blame] | 27 | import android.annotation.NonNull; |
Chalard Jean | f89d7be | 2018-12-07 23:09:02 +0900 | [diff] [blame] | 28 | import android.annotation.Nullable; |
Chalard Jean | 8c141bd | 2018-12-04 20:20:56 +0900 | [diff] [blame] | 29 | import android.content.Context; |
Chalard Jean | 61e27ab | 2018-12-12 17:56:37 +0900 | [diff] [blame] | 30 | import android.database.SQLException; |
| 31 | import android.database.sqlite.SQLiteDatabase; |
Chalard Jean | 8c141bd | 2018-12-04 20:20:56 +0900 | [diff] [blame] | 32 | import android.net.IIpMemoryStore; |
Chalard Jean | f89d7be | 2018-12-07 23:09:02 +0900 | [diff] [blame] | 33 | import android.net.ipmemorystore.Blob; |
| 34 | import android.net.ipmemorystore.IOnBlobRetrievedListener; |
| 35 | import android.net.ipmemorystore.IOnL2KeyResponseListener; |
| 36 | import android.net.ipmemorystore.IOnNetworkAttributesRetrieved; |
| 37 | import android.net.ipmemorystore.IOnSameNetworkResponseListener; |
| 38 | import android.net.ipmemorystore.IOnStatusListener; |
Chalard Jean | 91549b6 | 2018-12-18 22:05:19 +0900 | [diff] [blame] | 39 | import android.net.ipmemorystore.NetworkAttributes; |
Chalard Jean | f89d7be | 2018-12-07 23:09:02 +0900 | [diff] [blame] | 40 | import android.net.ipmemorystore.NetworkAttributesParcelable; |
Chalard Jean | a39756a | 2019-01-16 18:18:44 +0900 | [diff] [blame] | 41 | import android.net.ipmemorystore.SameL3NetworkResponse; |
Chalard Jean | 91549b6 | 2018-12-18 22:05:19 +0900 | [diff] [blame] | 42 | import android.net.ipmemorystore.Status; |
| 43 | import android.net.ipmemorystore.StatusParcelable; |
Chalard Jean | 91549b6 | 2018-12-18 22:05:19 +0900 | [diff] [blame] | 44 | import android.os.RemoteException; |
Chalard Jean | 61e27ab | 2018-12-12 17:56:37 +0900 | [diff] [blame] | 45 | import android.util.Log; |
Chalard Jean | 8c141bd | 2018-12-04 20:20:56 +0900 | [diff] [blame] | 46 | |
paulhu | 028d7a5 | 2019-03-26 01:39:10 +0800 | [diff] [blame^] | 47 | import com.android.internal.annotations.VisibleForTesting; |
| 48 | |
| 49 | import java.io.File; |
Chalard Jean | 0150bd9 | 2018-12-13 23:32:01 +0900 | [diff] [blame] | 50 | import java.util.concurrent.ExecutorService; |
| 51 | import java.util.concurrent.Executors; |
| 52 | |
Chalard Jean | 8c141bd | 2018-12-04 20:20:56 +0900 | [diff] [blame] | 53 | /** |
| 54 | * Implementation for the IP memory store. |
| 55 | * This component offers specialized services for network components to store and retrieve |
| 56 | * knowledge about networks, and provides intelligence that groups level 2 networks together |
| 57 | * into level 3 networks. |
Chalard Jean | f89d7be | 2018-12-07 23:09:02 +0900 | [diff] [blame] | 58 | * |
Chalard Jean | 8c141bd | 2018-12-04 20:20:56 +0900 | [diff] [blame] | 59 | * @hide |
| 60 | */ |
| 61 | public class IpMemoryStoreService extends IIpMemoryStore.Stub { |
Chalard Jean | 61e27ab | 2018-12-12 17:56:37 +0900 | [diff] [blame] | 62 | private static final String TAG = IpMemoryStoreService.class.getSimpleName(); |
Chalard Jean | 0150bd9 | 2018-12-13 23:32:01 +0900 | [diff] [blame] | 63 | private static final int MAX_CONCURRENT_THREADS = 4; |
paulhu | 028d7a5 | 2019-03-26 01:39:10 +0800 | [diff] [blame^] | 64 | private static final int DATABASE_SIZE_THRESHOLD = 10 * 1024 * 1024; //10MB |
| 65 | private static final int MAX_DROP_RECORD_TIMES = 500; |
| 66 | private static final int MIN_DELETE_NUM = 5; |
Chalard Jean | 91549b6 | 2018-12-18 22:05:19 +0900 | [diff] [blame] | 67 | private static final boolean DBG = true; |
Chalard Jean | 8c141bd | 2018-12-04 20:20:56 +0900 | [diff] [blame] | 68 | |
paulhu | 028d7a5 | 2019-03-26 01:39:10 +0800 | [diff] [blame^] | 69 | // Error codes below are internal and used for notifying status beteween IpMemoryStore modules. |
| 70 | static final int ERROR_INTERNAL_BASE = -1_000_000_000; |
| 71 | // This error code is used for maintenance only to notify RegularMaintenanceJobService that |
| 72 | // full maintenance job has been interrupted. |
| 73 | static final int ERROR_INTERNAL_INTERRUPTED = ERROR_INTERNAL_BASE - 1; |
| 74 | |
Chalard Jean | 61e27ab | 2018-12-12 17:56:37 +0900 | [diff] [blame] | 75 | @NonNull |
| 76 | final Context mContext; |
| 77 | @Nullable |
| 78 | final SQLiteDatabase mDb; |
Chalard Jean | 0150bd9 | 2018-12-13 23:32:01 +0900 | [diff] [blame] | 79 | @NonNull |
| 80 | final ExecutorService mExecutor; |
Chalard Jean | 61e27ab | 2018-12-12 17:56:37 +0900 | [diff] [blame] | 81 | |
| 82 | /** |
| 83 | * Construct an IpMemoryStoreService object. |
| 84 | * This constructor will block on disk access to open the database. |
| 85 | * @param context the context to access storage with. |
| 86 | */ |
Chalard Jean | 8c141bd | 2018-12-04 20:20:56 +0900 | [diff] [blame] | 87 | public IpMemoryStoreService(@NonNull final Context context) { |
Chalard Jean | 61e27ab | 2018-12-12 17:56:37 +0900 | [diff] [blame] | 88 | // Note that constructing the service will access the disk and block |
| 89 | // for some time, but it should make no difference to the clients. Because |
| 90 | // the interface is one-way, clients fire and forget requests, and the callback |
| 91 | // will get called eventually in any case, and the framework will wait for the |
| 92 | // service to be created to deliver subsequent requests. |
| 93 | // Avoiding this would mean the mDb member can't be final, which means the service would |
| 94 | // have to test for nullity, care for failure, and allow for a wait at every single access, |
| 95 | // which would make the code a lot more complex and require all methods to possibly block. |
Chalard Jean | 8c141bd | 2018-12-04 20:20:56 +0900 | [diff] [blame] | 96 | mContext = context; |
Chalard Jean | 61e27ab | 2018-12-12 17:56:37 +0900 | [diff] [blame] | 97 | SQLiteDatabase db; |
| 98 | final IpMemoryStoreDatabase.DbHelper helper = new IpMemoryStoreDatabase.DbHelper(context); |
| 99 | try { |
| 100 | db = helper.getWritableDatabase(); |
| 101 | if (null == db) Log.e(TAG, "Unexpected null return of getWriteableDatabase"); |
| 102 | } catch (final SQLException e) { |
| 103 | Log.e(TAG, "Can't open the Ip Memory Store database", e); |
| 104 | db = null; |
| 105 | } catch (final Exception e) { |
| 106 | Log.wtf(TAG, "Impossible exception Ip Memory Store database", e); |
| 107 | db = null; |
| 108 | } |
| 109 | mDb = db; |
Chalard Jean | 0150bd9 | 2018-12-13 23:32:01 +0900 | [diff] [blame] | 110 | // The work-stealing thread pool executor will spawn threads as needed up to |
| 111 | // the max only when there is no free thread available. This generally behaves |
| 112 | // exactly like one would expect it intuitively : |
| 113 | // - When work arrives, it will spawn a new thread iff there are no available threads |
| 114 | // - When there is no work to do it will shutdown threads after a while (the while |
| 115 | // being equal to 2 seconds (not configurable) when max threads are spun up and |
| 116 | // twice as much for every one less thread) |
| 117 | // - When all threads are busy the work is enqueued and waits for any worker |
| 118 | // to become available. |
| 119 | // Because the stealing pool is made for very heavily parallel execution of |
| 120 | // small tasks that spawn others, it creates a queue per thread that in this |
| 121 | // case is overhead. However, the three behaviors above make it a superior |
| 122 | // choice to cached or fixedThreadPoolExecutor, neither of which can actually |
| 123 | // enqueue a task waiting for a thread to be free. This can probably be solved |
| 124 | // with judicious subclassing of ThreadPoolExecutor, but that's a lot of dangerous |
| 125 | // complexity for little benefit in this case. |
| 126 | mExecutor = Executors.newWorkStealingPool(MAX_CONCURRENT_THREADS); |
paulhu | 028d7a5 | 2019-03-26 01:39:10 +0800 | [diff] [blame^] | 127 | RegularMaintenanceJobService.schedule(mContext, this); |
Chalard Jean | 0150bd9 | 2018-12-13 23:32:01 +0900 | [diff] [blame] | 128 | } |
| 129 | |
| 130 | /** |
| 131 | * Shutdown the memory store service, cancelling running tasks and dropping queued tasks. |
| 132 | * |
| 133 | * This is provided to give a way to clean up, and is meant to be available in case of an |
| 134 | * emergency shutdown. |
| 135 | */ |
| 136 | public void shutdown() { |
| 137 | // By contrast with ExecutorService#shutdown, ExecutorService#shutdownNow tries |
| 138 | // to cancel the existing tasks, and does not wait for completion. It does not |
| 139 | // guarantee the threads can be terminated in any given amount of time. |
| 140 | mExecutor.shutdownNow(); |
| 141 | if (mDb != null) mDb.close(); |
paulhu | 028d7a5 | 2019-03-26 01:39:10 +0800 | [diff] [blame^] | 142 | RegularMaintenanceJobService.unschedule(mContext); |
Chalard Jean | 8c141bd | 2018-12-04 20:20:56 +0900 | [diff] [blame] | 143 | } |
| 144 | |
Chalard Jean | 91549b6 | 2018-12-18 22:05:19 +0900 | [diff] [blame] | 145 | /** Helper function to make a status object */ |
| 146 | private StatusParcelable makeStatus(final int code) { |
| 147 | return new Status(code).toParcelable(); |
| 148 | } |
| 149 | |
Chalard Jean | 8c141bd | 2018-12-04 20:20:56 +0900 | [diff] [blame] | 150 | /** |
Chalard Jean | f89d7be | 2018-12-07 23:09:02 +0900 | [diff] [blame] | 151 | * Store network attributes for a given L2 key. |
| 152 | * |
| 153 | * @param l2Key The L2 key for the L2 network. Clients that don't know or care about the L2 |
| 154 | * key and only care about grouping can pass a unique ID here like the ones |
| 155 | * generated by {@code java.util.UUID.randomUUID()}, but keep in mind the low |
| 156 | * relevance of such a network will lead to it being evicted soon if it's not |
| 157 | * refreshed. Use findL2Key to try and find a similar L2Key to these attributes. |
| 158 | * @param attributes The attributes for this network. |
| 159 | * @param listener A listener to inform of the completion of this call, or null if the client |
| 160 | * is not interested in learning about success/failure. |
| 161 | * Through the listener, returns the L2 key. This is useful if the L2 key was not specified. |
| 162 | * If the call failed, the L2 key will be null. |
Chalard Jean | 8c141bd | 2018-12-04 20:20:56 +0900 | [diff] [blame] | 163 | */ |
Chalard Jean | 91549b6 | 2018-12-18 22:05:19 +0900 | [diff] [blame] | 164 | // Note that while l2Key and attributes are non-null in spirit, they are received from |
| 165 | // another process. If the remote process decides to ignore everything and send null, this |
| 166 | // process should still not crash. |
Chalard Jean | f89d7be | 2018-12-07 23:09:02 +0900 | [diff] [blame] | 167 | @Override |
Chalard Jean | 91549b6 | 2018-12-18 22:05:19 +0900 | [diff] [blame] | 168 | public void storeNetworkAttributes(@Nullable final String l2Key, |
| 169 | @Nullable final NetworkAttributesParcelable attributes, |
Chalard Jean | f89d7be | 2018-12-07 23:09:02 +0900 | [diff] [blame] | 170 | @Nullable final IOnStatusListener listener) { |
Chalard Jean | 91549b6 | 2018-12-18 22:05:19 +0900 | [diff] [blame] | 171 | // Because the parcelable is 100% mutable, the thread may not see its members initialized. |
| 172 | // Therefore either an immutable object is created on this same thread before it's passed |
| 173 | // to the executor, or there need to be a write barrier here and a read barrier in the |
| 174 | // remote thread. |
| 175 | final NetworkAttributes na = null == attributes ? null : new NetworkAttributes(attributes); |
| 176 | mExecutor.execute(() -> { |
| 177 | try { |
| 178 | final int code = storeNetworkAttributesAndBlobSync(l2Key, na, |
| 179 | null /* clientId */, null /* name */, null /* data */); |
| 180 | if (null != listener) listener.onComplete(makeStatus(code)); |
| 181 | } catch (final RemoteException e) { |
| 182 | // Client at the other end died |
| 183 | } |
| 184 | }); |
Chalard Jean | f89d7be | 2018-12-07 23:09:02 +0900 | [diff] [blame] | 185 | } |
| 186 | |
| 187 | /** |
| 188 | * Store a binary blob associated with an L2 key and a name. |
| 189 | * |
| 190 | * @param l2Key The L2 key for this network. |
| 191 | * @param clientId The ID of the client. |
| 192 | * @param name The name of this data. |
Chalard Jean | 91549b6 | 2018-12-18 22:05:19 +0900 | [diff] [blame] | 193 | * @param blob The data to store. |
Chalard Jean | f89d7be | 2018-12-07 23:09:02 +0900 | [diff] [blame] | 194 | * @param listener The listener that will be invoked to return the answer, or null if the |
| 195 | * is not interested in learning about success/failure. |
| 196 | * Through the listener, returns a status to indicate success or failure. |
| 197 | */ |
| 198 | @Override |
Chalard Jean | 91549b6 | 2018-12-18 22:05:19 +0900 | [diff] [blame] | 199 | public void storeBlob(@Nullable final String l2Key, @Nullable final String clientId, |
| 200 | @Nullable final String name, @Nullable final Blob blob, |
Chalard Jean | f89d7be | 2018-12-07 23:09:02 +0900 | [diff] [blame] | 201 | @Nullable final IOnStatusListener listener) { |
Chalard Jean | 91549b6 | 2018-12-18 22:05:19 +0900 | [diff] [blame] | 202 | final byte[] data = null == blob ? null : blob.data; |
| 203 | mExecutor.execute(() -> { |
| 204 | try { |
| 205 | final int code = storeNetworkAttributesAndBlobSync(l2Key, |
| 206 | null /* NetworkAttributes */, clientId, name, data); |
| 207 | if (null != listener) listener.onComplete(makeStatus(code)); |
| 208 | } catch (final RemoteException e) { |
| 209 | // Client at the other end died |
| 210 | } |
| 211 | }); |
| 212 | } |
| 213 | |
| 214 | /** |
| 215 | * Helper method for storeNetworkAttributes and storeBlob. |
| 216 | * |
| 217 | * Either attributes or none of clientId, name and data may be null. This will write the |
| 218 | * passed data if non-null, and will write attributes if non-null, but in any case it will |
| 219 | * bump the relevance up. |
| 220 | * Returns a success code from Status. |
| 221 | */ |
| 222 | private int storeNetworkAttributesAndBlobSync(@Nullable final String l2Key, |
| 223 | @Nullable final NetworkAttributes attributes, |
| 224 | @Nullable final String clientId, |
| 225 | @Nullable final String name, @Nullable final byte[] data) { |
| 226 | if (null == l2Key) return ERROR_ILLEGAL_ARGUMENT; |
| 227 | if (null == attributes && null == data) return ERROR_ILLEGAL_ARGUMENT; |
| 228 | if (null != data && (null == clientId || null == name)) return ERROR_ILLEGAL_ARGUMENT; |
| 229 | if (null == mDb) return ERROR_DATABASE_CANNOT_BE_OPENED; |
| 230 | try { |
| 231 | final long oldExpiry = IpMemoryStoreDatabase.getExpiry(mDb, l2Key); |
| 232 | final long newExpiry = RelevanceUtils.bumpExpiryDate( |
| 233 | oldExpiry == EXPIRY_ERROR ? System.currentTimeMillis() : oldExpiry); |
| 234 | final int errorCode = |
| 235 | IpMemoryStoreDatabase.storeNetworkAttributes(mDb, l2Key, newExpiry, attributes); |
| 236 | // If no blob to store, the client is interested in the result of storing the attributes |
| 237 | if (null == data) return errorCode; |
| 238 | // Otherwise it's interested in the result of storing the blob |
| 239 | return IpMemoryStoreDatabase.storeBlob(mDb, l2Key, clientId, name, data); |
| 240 | } catch (Exception e) { |
| 241 | if (DBG) { |
| 242 | Log.e(TAG, "Exception while storing for key {" + l2Key |
| 243 | + "} ; NetworkAttributes {" + (null == attributes ? "null" : attributes) |
| 244 | + "} ; clientId {" + (null == clientId ? "null" : clientId) |
| 245 | + "} ; name {" + (null == name ? "null" : name) |
| 246 | + "} ; data {" + Utils.byteArrayToString(data) + "}", e); |
| 247 | } |
| 248 | } |
| 249 | return ERROR_GENERIC; |
Chalard Jean | f89d7be | 2018-12-07 23:09:02 +0900 | [diff] [blame] | 250 | } |
| 251 | |
| 252 | /** |
| 253 | * Returns the best L2 key associated with the attributes. |
| 254 | * |
| 255 | * This will find a record that would be in the same group as the passed attributes. This is |
| 256 | * useful to choose the key for storing a sample or private data when the L2 key is not known. |
| 257 | * If multiple records are group-close to these attributes, the closest match is returned. |
| 258 | * If multiple records have the same closeness, the one with the smaller (unicode codepoint |
| 259 | * order) L2 key is returned. |
| 260 | * If no record matches these attributes, null is returned. |
| 261 | * |
| 262 | * @param attributes The attributes of the network to find. |
| 263 | * @param listener The listener that will be invoked to return the answer. |
| 264 | * Through the listener, returns the L2 key if one matched, or null. |
| 265 | */ |
| 266 | @Override |
Chalard Jean | 8d1a890 | 2019-01-18 20:21:26 +0900 | [diff] [blame] | 267 | public void findL2Key(@Nullable final NetworkAttributesParcelable attributes, |
| 268 | @Nullable final IOnL2KeyResponseListener listener) { |
| 269 | if (null == listener) return; |
| 270 | mExecutor.execute(() -> { |
| 271 | try { |
| 272 | if (null == attributes) { |
| 273 | listener.onL2KeyResponse(makeStatus(ERROR_ILLEGAL_ARGUMENT), null); |
| 274 | return; |
| 275 | } |
| 276 | if (null == mDb) { |
| 277 | listener.onL2KeyResponse(makeStatus(ERROR_ILLEGAL_ARGUMENT), null); |
| 278 | return; |
| 279 | } |
| 280 | final String key = IpMemoryStoreDatabase.findClosestAttributes(mDb, |
| 281 | new NetworkAttributes(attributes)); |
| 282 | listener.onL2KeyResponse(makeStatus(SUCCESS), key); |
| 283 | } catch (final RemoteException e) { |
| 284 | // Client at the other end died |
| 285 | } |
| 286 | }); |
Chalard Jean | f89d7be | 2018-12-07 23:09:02 +0900 | [diff] [blame] | 287 | } |
| 288 | |
| 289 | /** |
| 290 | * Returns whether, to the best of the store's ability to tell, the two specified L2 keys point |
| 291 | * to the same L3 network. Group-closeness is used to determine this. |
| 292 | * |
| 293 | * @param l2Key1 The key for the first network. |
| 294 | * @param l2Key2 The key for the second network. |
| 295 | * @param listener The listener that will be invoked to return the answer. |
| 296 | * Through the listener, a SameL3NetworkResponse containing the answer and confidence. |
| 297 | */ |
| 298 | @Override |
Chalard Jean | a39756a | 2019-01-16 18:18:44 +0900 | [diff] [blame] | 299 | public void isSameNetwork(@Nullable final String l2Key1, @Nullable final String l2Key2, |
| 300 | @Nullable final IOnSameNetworkResponseListener listener) { |
| 301 | if (null == listener) return; |
| 302 | mExecutor.execute(() -> { |
| 303 | try { |
| 304 | if (null == l2Key1 || null == l2Key2) { |
| 305 | listener.onSameNetworkResponse(makeStatus(ERROR_ILLEGAL_ARGUMENT), null); |
| 306 | return; |
| 307 | } |
| 308 | if (null == mDb) { |
| 309 | listener.onSameNetworkResponse(makeStatus(ERROR_ILLEGAL_ARGUMENT), null); |
| 310 | return; |
| 311 | } |
| 312 | try { |
| 313 | final NetworkAttributes attr1 = |
| 314 | IpMemoryStoreDatabase.retrieveNetworkAttributes(mDb, l2Key1); |
| 315 | final NetworkAttributes attr2 = |
| 316 | IpMemoryStoreDatabase.retrieveNetworkAttributes(mDb, l2Key2); |
| 317 | if (null == attr1 || null == attr2) { |
| 318 | listener.onSameNetworkResponse(makeStatus(SUCCESS), |
| 319 | new SameL3NetworkResponse(l2Key1, l2Key2, |
| 320 | -1f /* never connected */).toParcelable()); |
| 321 | return; |
| 322 | } |
| 323 | final float confidence = attr1.getNetworkGroupSamenessConfidence(attr2); |
| 324 | listener.onSameNetworkResponse(makeStatus(SUCCESS), |
| 325 | new SameL3NetworkResponse(l2Key1, l2Key2, confidence).toParcelable()); |
| 326 | } catch (Exception e) { |
| 327 | listener.onSameNetworkResponse(makeStatus(ERROR_GENERIC), null); |
| 328 | } |
| 329 | } catch (final RemoteException e) { |
| 330 | // Client at the other end died |
| 331 | } |
| 332 | }); |
Chalard Jean | f89d7be | 2018-12-07 23:09:02 +0900 | [diff] [blame] | 333 | } |
| 334 | |
| 335 | /** |
| 336 | * Retrieve the network attributes for a key. |
| 337 | * If no record is present for this key, this will return null attributes. |
| 338 | * |
| 339 | * @param l2Key The key of the network to query. |
| 340 | * @param listener The listener that will be invoked to return the answer. |
| 341 | * Through the listener, returns the network attributes and the L2 key associated with |
| 342 | * the query. |
| 343 | */ |
| 344 | @Override |
Chalard Jean | bf73e66 | 2018-12-27 20:59:41 +0900 | [diff] [blame] | 345 | public void retrieveNetworkAttributes(@Nullable final String l2Key, |
| 346 | @Nullable final IOnNetworkAttributesRetrieved listener) { |
| 347 | if (null == listener) return; |
| 348 | mExecutor.execute(() -> { |
| 349 | try { |
| 350 | if (null == l2Key) { |
Chalard Jean | b67e493 | 2019-01-16 23:05:10 +0900 | [diff] [blame] | 351 | listener.onNetworkAttributesRetrieved( |
| 352 | makeStatus(ERROR_ILLEGAL_ARGUMENT), l2Key, null); |
Chalard Jean | bf73e66 | 2018-12-27 20:59:41 +0900 | [diff] [blame] | 353 | return; |
| 354 | } |
| 355 | if (null == mDb) { |
Chalard Jean | b67e493 | 2019-01-16 23:05:10 +0900 | [diff] [blame] | 356 | listener.onNetworkAttributesRetrieved( |
| 357 | makeStatus(ERROR_DATABASE_CANNOT_BE_OPENED), l2Key, null); |
Chalard Jean | bf73e66 | 2018-12-27 20:59:41 +0900 | [diff] [blame] | 358 | return; |
| 359 | } |
| 360 | try { |
| 361 | final NetworkAttributes attributes = |
| 362 | IpMemoryStoreDatabase.retrieveNetworkAttributes(mDb, l2Key); |
Chalard Jean | b67e493 | 2019-01-16 23:05:10 +0900 | [diff] [blame] | 363 | listener.onNetworkAttributesRetrieved(makeStatus(SUCCESS), l2Key, |
Chalard Jean | bf73e66 | 2018-12-27 20:59:41 +0900 | [diff] [blame] | 364 | null == attributes ? null : attributes.toParcelable()); |
| 365 | } catch (final Exception e) { |
Chalard Jean | b67e493 | 2019-01-16 23:05:10 +0900 | [diff] [blame] | 366 | listener.onNetworkAttributesRetrieved(makeStatus(ERROR_GENERIC), l2Key, null); |
Chalard Jean | bf73e66 | 2018-12-27 20:59:41 +0900 | [diff] [blame] | 367 | } |
| 368 | } catch (final RemoteException e) { |
| 369 | // Client at the other end died |
| 370 | } |
| 371 | }); |
Chalard Jean | f89d7be | 2018-12-07 23:09:02 +0900 | [diff] [blame] | 372 | } |
| 373 | |
| 374 | /** |
| 375 | * Retrieve previously stored private data. |
| 376 | * If no data was stored for this L2 key and name this will return null. |
| 377 | * |
| 378 | * @param l2Key The L2 key. |
| 379 | * @param clientId The id of the client that stored this data. |
| 380 | * @param name The name of the data. |
| 381 | * @param listener The listener that will be invoked to return the answer. |
| 382 | * Through the listener, returns the private data if any or null if none, with the L2 key |
| 383 | * and the name of the data associated with the query. |
| 384 | */ |
| 385 | @Override |
| 386 | public void retrieveBlob(@NonNull final String l2Key, @NonNull final String clientId, |
| 387 | @NonNull final String name, @NonNull final IOnBlobRetrievedListener listener) { |
Chalard Jean | bf73e66 | 2018-12-27 20:59:41 +0900 | [diff] [blame] | 388 | if (null == listener) return; |
| 389 | mExecutor.execute(() -> { |
| 390 | try { |
| 391 | if (null == l2Key) { |
| 392 | listener.onBlobRetrieved(makeStatus(ERROR_ILLEGAL_ARGUMENT), l2Key, name, null); |
| 393 | return; |
| 394 | } |
| 395 | if (null == mDb) { |
| 396 | listener.onBlobRetrieved(makeStatus(ERROR_DATABASE_CANNOT_BE_OPENED), l2Key, |
| 397 | name, null); |
| 398 | return; |
| 399 | } |
| 400 | try { |
| 401 | final Blob b = new Blob(); |
| 402 | b.data = IpMemoryStoreDatabase.retrieveBlob(mDb, l2Key, clientId, name); |
| 403 | listener.onBlobRetrieved(makeStatus(SUCCESS), l2Key, name, b); |
| 404 | } catch (final Exception e) { |
| 405 | listener.onBlobRetrieved(makeStatus(ERROR_GENERIC), l2Key, name, null); |
| 406 | } |
| 407 | } catch (final RemoteException e) { |
| 408 | // Client at the other end died |
| 409 | } |
| 410 | }); |
Chalard Jean | 8c141bd | 2018-12-04 20:20:56 +0900 | [diff] [blame] | 411 | } |
paulhu | 028d7a5 | 2019-03-26 01:39:10 +0800 | [diff] [blame^] | 412 | |
| 413 | /** Get db size threshold. */ |
| 414 | @VisibleForTesting |
| 415 | protected int getDbSizeThreshold() { |
| 416 | return DATABASE_SIZE_THRESHOLD; |
| 417 | } |
| 418 | |
| 419 | private long getDbSize() { |
| 420 | final File dbFile = new File(mDb.getPath()); |
| 421 | try { |
| 422 | return dbFile.length(); |
| 423 | } catch (final SecurityException e) { |
| 424 | if (DBG) Log.e(TAG, "Read db size access deny.", e); |
| 425 | // Return zero value if can't get disk usage exactly. |
| 426 | return 0; |
| 427 | } |
| 428 | } |
| 429 | |
| 430 | /** Check if db size is over the threshold. */ |
| 431 | @VisibleForTesting |
| 432 | boolean isDbSizeOverThreshold() { |
| 433 | return getDbSize() > getDbSizeThreshold(); |
| 434 | } |
| 435 | |
| 436 | /** |
| 437 | * Full maintenance. |
| 438 | * |
| 439 | * @param listener A listener to inform of the completion of this call. |
| 440 | */ |
| 441 | void fullMaintenance(@NonNull final IOnStatusListener listener, |
| 442 | @NonNull final InterruptMaintenance interrupt) { |
| 443 | mExecutor.execute(() -> { |
| 444 | try { |
| 445 | if (null == mDb) { |
| 446 | listener.onComplete(makeStatus(ERROR_DATABASE_CANNOT_BE_OPENED)); |
| 447 | return; |
| 448 | } |
| 449 | |
| 450 | // Interrupt maintenance because the scheduling job has been canceled. |
| 451 | if (checkForInterrupt(listener, interrupt)) return; |
| 452 | |
| 453 | int result = SUCCESS; |
| 454 | // Drop all records whose relevance has decayed to zero. |
| 455 | // This is the first step to decrease memory store size. |
| 456 | result = IpMemoryStoreDatabase.dropAllExpiredRecords(mDb); |
| 457 | |
| 458 | if (checkForInterrupt(listener, interrupt)) return; |
| 459 | |
| 460 | // Aggregate historical data in passes |
| 461 | // TODO : Waiting for historical data implement. |
| 462 | |
| 463 | // Check if db size meets the storage goal(10MB). If not, keep dropping records and |
| 464 | // aggregate historical data until the storage goal is met. Use for loop with 500 |
| 465 | // times restriction to prevent infinite loop (Deleting records always fail and db |
| 466 | // size is still over the threshold) |
| 467 | for (int i = 0; isDbSizeOverThreshold() && i < MAX_DROP_RECORD_TIMES; i++) { |
| 468 | if (checkForInterrupt(listener, interrupt)) return; |
| 469 | |
| 470 | final int totalNumber = IpMemoryStoreDatabase.getTotalRecordNumber(mDb); |
| 471 | final long dbSize = getDbSize(); |
| 472 | final float decreaseRate = (dbSize == 0) |
| 473 | ? 0 : (float) (dbSize - getDbSizeThreshold()) / (float) dbSize; |
| 474 | final int deleteNumber = Math.max( |
| 475 | (int) (totalNumber * decreaseRate), MIN_DELETE_NUM); |
| 476 | |
| 477 | result = IpMemoryStoreDatabase.dropNumberOfRecords(mDb, deleteNumber); |
| 478 | |
| 479 | if (checkForInterrupt(listener, interrupt)) return; |
| 480 | |
| 481 | // Aggregate historical data |
| 482 | // TODO : Waiting for historical data implement. |
| 483 | } |
| 484 | listener.onComplete(makeStatus(result)); |
| 485 | } catch (final RemoteException e) { |
| 486 | // Client at the other end died |
| 487 | } |
| 488 | }); |
| 489 | } |
| 490 | |
| 491 | private boolean checkForInterrupt(@NonNull final IOnStatusListener listener, |
| 492 | @NonNull final InterruptMaintenance interrupt) throws RemoteException { |
| 493 | if (!interrupt.isInterrupted()) return false; |
| 494 | listener.onComplete(makeStatus(ERROR_INTERNAL_INTERRUPTED)); |
| 495 | return true; |
| 496 | } |
Chalard Jean | 8c141bd | 2018-12-04 20:20:56 +0900 | [diff] [blame] | 497 | } |