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