diff options
9 files changed, 326 insertions, 45 deletions
diff --git a/core/java/android/provider/Settings.java b/core/java/android/provider/Settings.java index 90ec5aad235d..4f5b67c34845 100644 --- a/core/java/android/provider/Settings.java +++ b/core/java/android/provider/Settings.java @@ -11173,6 +11173,35 @@ public final class Settings { "visual_query_accessibility_detection_enabled"; /** + * Timeout to be used for unbinding to the configured remote + * {@link android.service.ondeviceintelligence.OnDeviceIntelligenceService} if there are no + * requests in the queue. A value of -1 represents to never unbind. + * + * @hide + */ + public static final String ON_DEVICE_INTELLIGENCE_UNBIND_TIMEOUT_MS = + "on_device_intelligence_unbind_timeout_ms"; + + + /** + * Timeout that represents maximum idle time before which a callback should be populated. + * + * @hide + */ + public static final String ON_DEVICE_INTELLIGENCE_IDLE_TIMEOUT_MS = + "on_device_intelligence_idle_timeout_ms"; + + /** + * Timeout to be used for unbinding to the configured remote + * {@link android.service.ondeviceintelligence.OnDeviceSandboxedInferenceService} if there + * are no requests in the queue. A value of -1 represents to never unbind. + * + * @hide + */ + public static final String ON_DEVICE_INFERENCE_UNBIND_TIMEOUT_MS = + "on_device_inference_unbind_timeout_ms"; + + /** * Control whether Night display is currently activated. * @hide */ diff --git a/packages/SettingsProvider/src/android/provider/settings/backup/SecureSettings.java b/packages/SettingsProvider/src/android/provider/settings/backup/SecureSettings.java index be3f4108fdd1..888e39593a2e 100644 --- a/packages/SettingsProvider/src/android/provider/settings/backup/SecureSettings.java +++ b/packages/SettingsProvider/src/android/provider/settings/backup/SecureSettings.java @@ -274,6 +274,9 @@ public class SecureSettings { Settings.Secure.SCREEN_RESOLUTION_MODE, Settings.Secure.ACCESSIBILITY_GESTURE_TARGETS, Settings.Secure.ACCESSIBILITY_DISPLAY_DALTONIZER_SATURATION_LEVEL, - Settings.Secure.CHARGE_OPTIMIZATION_MODE + Settings.Secure.CHARGE_OPTIMIZATION_MODE, + Settings.Secure.ON_DEVICE_INTELLIGENCE_UNBIND_TIMEOUT_MS, + Settings.Secure.ON_DEVICE_INFERENCE_UNBIND_TIMEOUT_MS, + Settings.Secure.ON_DEVICE_INTELLIGENCE_IDLE_TIMEOUT_MS, }; } diff --git a/packages/SettingsProvider/src/android/provider/settings/validators/SecureSettingsValidators.java b/packages/SettingsProvider/src/android/provider/settings/validators/SecureSettingsValidators.java index b1feede57506..b992ddc8a397 100644 --- a/packages/SettingsProvider/src/android/provider/settings/validators/SecureSettingsValidators.java +++ b/packages/SettingsProvider/src/android/provider/settings/validators/SecureSettingsValidators.java @@ -18,6 +18,7 @@ package android.provider.settings.validators; import static android.provider.settings.validators.SettingsValidators.ACCESSIBILITY_SHORTCUT_TARGET_LIST_VALIDATOR; import static android.provider.settings.validators.SettingsValidators.ANY_INTEGER_VALIDATOR; +import static android.provider.settings.validators.SettingsValidators.ANY_LONG_VALIDATOR; import static android.provider.settings.validators.SettingsValidators.ANY_STRING_VALIDATOR; import static android.provider.settings.validators.SettingsValidators.AUTOFILL_SERVICE_VALIDATOR; import static android.provider.settings.validators.SettingsValidators.BOOLEAN_VALIDATOR; @@ -433,5 +434,8 @@ public class SecureSettingsValidators { VALIDATORS.put(Secure.ACCESSIBILITY_DISPLAY_DALTONIZER_SATURATION_LEVEL, new InclusiveIntegerRangeValidator(0, 10)); VALIDATORS.put(Secure.CHARGE_OPTIMIZATION_MODE, new InclusiveIntegerRangeValidator(0, 10)); + VALIDATORS.put(Secure.ON_DEVICE_INFERENCE_UNBIND_TIMEOUT_MS, ANY_LONG_VALIDATOR); + VALIDATORS.put(Secure.ON_DEVICE_INTELLIGENCE_UNBIND_TIMEOUT_MS, ANY_LONG_VALIDATOR); + VALIDATORS.put(Secure.ON_DEVICE_INTELLIGENCE_IDLE_TIMEOUT_MS, NONE_NEGATIVE_LONG_VALIDATOR); } } diff --git a/packages/SettingsProvider/src/android/provider/settings/validators/SettingsValidators.java b/packages/SettingsProvider/src/android/provider/settings/validators/SettingsValidators.java index 677c81ad9271..255b1ad3b3d2 100644 --- a/packages/SettingsProvider/src/android/provider/settings/validators/SettingsValidators.java +++ b/packages/SettingsProvider/src/android/provider/settings/validators/SettingsValidators.java @@ -239,6 +239,18 @@ public class SettingsValidators { } }; + static final Validator ANY_LONG_VALIDATOR = value -> { + if (value == null) { + return true; + } + try { + Long.parseLong(value); + return true; + } catch (NumberFormatException e) { + return false; + } + }; + static final Validator CREDENTIAL_SERVICE_VALIDATOR = new Validator() { @Override public boolean validate(String value) { diff --git a/services/core/java/com/android/server/ondeviceintelligence/BundleUtil.java b/services/core/java/com/android/server/ondeviceintelligence/BundleUtil.java index 681dd0b49f4e..cbede233a110 100644 --- a/services/core/java/com/android/server/ondeviceintelligence/BundleUtil.java +++ b/services/core/java/com/android/server/ondeviceintelligence/BundleUtil.java @@ -41,7 +41,10 @@ import android.system.ErrnoException; import android.system.Os; import android.util.Log; +import com.android.internal.infra.AndroidFuture; + import java.util.concurrent.Executor; +import java.util.concurrent.TimeoutException; /** * Util methods for ensuring the Bundle passed in various methods are read-only and restricted to @@ -183,7 +186,8 @@ public class BundleUtil { public static IStreamingResponseCallback wrapWithValidation( IStreamingResponseCallback streamingResponseCallback, - Executor resourceClosingExecutor) { + Executor resourceClosingExecutor, + AndroidFuture future) { return new IStreamingResponseCallback.Stub() { @Override public void onNewContent(Bundle processedResult) throws RemoteException { @@ -203,6 +207,7 @@ public class BundleUtil { streamingResponseCallback.onSuccess(resultBundle); } finally { resourceClosingExecutor.execute(() -> tryCloseResource(resultBundle)); + future.complete(null); } } @@ -210,6 +215,7 @@ public class BundleUtil { public void onFailure(int errorCode, String errorMessage, PersistableBundle errorParams) throws RemoteException { streamingResponseCallback.onFailure(errorCode, errorMessage, errorParams); + future.completeExceptionally(new TimeoutException()); } @Override @@ -237,7 +243,8 @@ public class BundleUtil { } public static IResponseCallback wrapWithValidation(IResponseCallback responseCallback, - Executor resourceClosingExecutor) { + Executor resourceClosingExecutor, + AndroidFuture future) { return new IResponseCallback.Stub() { @Override public void onSuccess(Bundle resultBundle) @@ -247,6 +254,7 @@ public class BundleUtil { responseCallback.onSuccess(resultBundle); } finally { resourceClosingExecutor.execute(() -> tryCloseResource(resultBundle)); + future.complete(null); } } @@ -254,6 +262,7 @@ public class BundleUtil { public void onFailure(int errorCode, String errorMessage, PersistableBundle errorParams) throws RemoteException { responseCallback.onFailure(errorCode, errorMessage, errorParams); + future.completeExceptionally(new TimeoutException()); } @Override @@ -280,17 +289,20 @@ public class BundleUtil { } - public static ITokenInfoCallback wrapWithValidation(ITokenInfoCallback responseCallback) { + public static ITokenInfoCallback wrapWithValidation(ITokenInfoCallback responseCallback, + AndroidFuture future) { return new ITokenInfoCallback.Stub() { @Override public void onSuccess(TokenInfo tokenInfo) throws RemoteException { responseCallback.onSuccess(tokenInfo); + future.complete(null); } @Override public void onFailure(int errorCode, String errorMessage, PersistableBundle errorParams) throws RemoteException { responseCallback.onFailure(errorCode, errorMessage, errorParams); + future.completeExceptionally(new TimeoutException()); } }; } diff --git a/services/core/java/com/android/server/ondeviceintelligence/OnDeviceIntelligenceManagerService.java b/services/core/java/com/android/server/ondeviceintelligence/OnDeviceIntelligenceManagerService.java index 235e3cd7c9d2..b2e861cf2876 100644 --- a/services/core/java/com/android/server/ondeviceintelligence/OnDeviceIntelligenceManagerService.java +++ b/services/core/java/com/android/server/ondeviceintelligence/OnDeviceIntelligenceManagerService.java @@ -33,6 +33,7 @@ import android.annotation.RequiresPermission; import android.app.AppGlobals; import android.app.ondeviceintelligence.DownloadCallback; import android.app.ondeviceintelligence.Feature; +import android.app.ondeviceintelligence.FeatureDetails; import android.app.ondeviceintelligence.IDownloadCallback; import android.app.ondeviceintelligence.IFeatureCallback; import android.app.ondeviceintelligence.IFeatureDetailsCallback; @@ -64,6 +65,7 @@ import android.os.ResultReceiver; import android.os.ShellCallback; import android.os.UserHandle; import android.provider.DeviceConfig; +import android.provider.Settings; import android.service.ondeviceintelligence.IOnDeviceIntelligenceService; import android.service.ondeviceintelligence.IOnDeviceSandboxedInferenceService; import android.service.ondeviceintelligence.IProcessingUpdateStatusCallback; @@ -82,13 +84,17 @@ import com.android.internal.infra.ServiceConnector; import com.android.internal.os.BackgroundThread; import com.android.server.LocalServices; import com.android.server.SystemService; +import com.android.server.ondeviceintelligence.callbacks.ListenableDownloadCallback; import java.io.FileDescriptor; import java.io.IOException; +import java.util.List; import java.util.Objects; import java.util.Set; import java.util.concurrent.Executor; import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; /** * This is the system service for handling calls on the @@ -135,7 +141,6 @@ public class OnDeviceIntelligenceManagerService extends SystemService { @GuardedBy("mLock") private String[] mTemporaryServiceNames; - @GuardedBy("mLock") private String[] mTemporaryBroadcastKeys; @GuardedBy("mLock") @@ -145,6 +150,8 @@ public class OnDeviceIntelligenceManagerService extends SystemService { * Handler used to reset the temporary service names. */ private Handler mTemporaryHandler; + private final @NonNull Handler mMainHandler = new Handler(Looper.getMainLooper()); + public OnDeviceIntelligenceManagerService(Context context) { super(context); @@ -204,8 +211,16 @@ public class OnDeviceIntelligenceManagerService extends SystemService { return; } ensureRemoteIntelligenceServiceInitialized(); - mRemoteOnDeviceIntelligenceService.run( - service -> service.getVersion(remoteCallback)); + mRemoteOnDeviceIntelligenceService.postAsync( + service -> { + AndroidFuture future = new AndroidFuture(); + service.getVersion(new RemoteCallback( + result -> { + remoteCallback.sendResult(result); + future.complete(null); + })); + return future.orTimeout(getIdleTimeoutMs(), TimeUnit.MILLISECONDS); + }); } @Override @@ -225,8 +240,25 @@ public class OnDeviceIntelligenceManagerService extends SystemService { } ensureRemoteIntelligenceServiceInitialized(); int callerUid = Binder.getCallingUid(); - mRemoteOnDeviceIntelligenceService.run( - service -> service.getFeature(callerUid, id, featureCallback)); + mRemoteOnDeviceIntelligenceService.postAsync( + service -> { + AndroidFuture future = new AndroidFuture(); + service.getFeature(callerUid, id, new IFeatureCallback.Stub() { + @Override + public void onSuccess(Feature result) throws RemoteException { + featureCallback.onSuccess(result); + future.complete(null); + } + + @Override + public void onFailure(int errorCode, String errorMessage, + PersistableBundle errorParams) throws RemoteException { + featureCallback.onFailure(errorCode, errorMessage, errorParams); + future.completeExceptionally(new TimeoutException()); + } + }); + return future.orTimeout(getIdleTimeoutMs(), TimeUnit.MILLISECONDS); + }); } @Override @@ -246,9 +278,29 @@ public class OnDeviceIntelligenceManagerService extends SystemService { } ensureRemoteIntelligenceServiceInitialized(); int callerUid = Binder.getCallingUid(); - mRemoteOnDeviceIntelligenceService.run( - service -> service.listFeatures(callerUid, - listFeaturesCallback)); + mRemoteOnDeviceIntelligenceService.postAsync( + service -> { + AndroidFuture future = new AndroidFuture(); + service.listFeatures(callerUid, + new IListFeaturesCallback.Stub() { + @Override + public void onSuccess(List<Feature> result) + throws RemoteException { + listFeaturesCallback.onSuccess(result); + future.complete(null); + } + + @Override + public void onFailure(int errorCode, String errorMessage, + PersistableBundle errorParams) + throws RemoteException { + listFeaturesCallback.onFailure(errorCode, errorMessage, + errorParams); + future.completeExceptionally(new TimeoutException()); + } + }); + return future.orTimeout(getIdleTimeoutMs(), TimeUnit.MILLISECONDS); + }); } @Override @@ -270,9 +322,29 @@ public class OnDeviceIntelligenceManagerService extends SystemService { } ensureRemoteIntelligenceServiceInitialized(); int callerUid = Binder.getCallingUid(); - mRemoteOnDeviceIntelligenceService.run( - service -> service.getFeatureDetails(callerUid, feature, - featureDetailsCallback)); + mRemoteOnDeviceIntelligenceService.postAsync( + service -> { + AndroidFuture future = new AndroidFuture(); + service.getFeatureDetails(callerUid, feature, + new IFeatureDetailsCallback.Stub() { + @Override + public void onSuccess(FeatureDetails result) + throws RemoteException { + future.complete(null); + featureDetailsCallback.onSuccess(result); + } + + @Override + public void onFailure(int errorCode, String errorMessage, + PersistableBundle errorParams) + throws RemoteException { + future.completeExceptionally(null); + featureDetailsCallback.onFailure(errorCode, + errorMessage, errorParams); + } + }); + return future.orTimeout(getIdleTimeoutMs(), TimeUnit.MILLISECONDS); + }); } @Override @@ -293,10 +365,20 @@ public class OnDeviceIntelligenceManagerService extends SystemService { } ensureRemoteIntelligenceServiceInitialized(); int callerUid = Binder.getCallingUid(); - mRemoteOnDeviceIntelligenceService.run( - service -> service.requestFeatureDownload(callerUid, feature, - wrapCancellationFuture(cancellationSignalFuture), - downloadCallback)); + mRemoteOnDeviceIntelligenceService.postAsync( + service -> { + AndroidFuture future = new AndroidFuture(); + ListenableDownloadCallback listenableDownloadCallback = + new ListenableDownloadCallback( + downloadCallback, + mMainHandler, future, getIdleTimeoutMs()); + service.requestFeatureDownload(callerUid, feature, + wrapCancellationFuture(cancellationSignalFuture), + listenableDownloadCallback); + return future; // this future has no timeout because, actual download + // might take long, but we fail early if there is no progress callbacks. + } + ); } @@ -323,11 +405,15 @@ public class OnDeviceIntelligenceManagerService extends SystemService { } ensureRemoteInferenceServiceInitialized(); int callerUid = Binder.getCallingUid(); - result = mRemoteInferenceService.post( - service -> service.requestTokenInfo(callerUid, feature, - request, - wrapCancellationFuture(cancellationSignalFuture), - wrapWithValidation(tokenInfoCallback))); + result = mRemoteInferenceService.postAsync( + service -> { + AndroidFuture future = new AndroidFuture(); + service.requestTokenInfo(callerUid, feature, + request, + wrapCancellationFuture(cancellationSignalFuture), + wrapWithValidation(tokenInfoCallback, future)); + return future.orTimeout(getIdleTimeoutMs(), TimeUnit.MILLISECONDS); + }); result.whenCompleteAsync((c, e) -> BundleUtil.tryCloseResource(request), resourceClosingExecutor); } finally { @@ -362,13 +448,18 @@ public class OnDeviceIntelligenceManagerService extends SystemService { } ensureRemoteInferenceServiceInitialized(); int callerUid = Binder.getCallingUid(); - result = mRemoteInferenceService.post( - service -> service.processRequest(callerUid, feature, - request, - requestType, - wrapCancellationFuture(cancellationSignalFuture), - wrapProcessingFuture(processingSignalFuture), - wrapWithValidation(responseCallback, resourceClosingExecutor))); + result = mRemoteInferenceService.postAsync( + service -> { + AndroidFuture future = new AndroidFuture(); + service.processRequest(callerUid, feature, + request, + requestType, + wrapCancellationFuture(cancellationSignalFuture), + wrapProcessingFuture(processingSignalFuture), + wrapWithValidation(responseCallback, + resourceClosingExecutor, future)); + return future.orTimeout(getIdleTimeoutMs(), TimeUnit.MILLISECONDS); + }); result.whenCompleteAsync((c, e) -> BundleUtil.tryCloseResource(request), resourceClosingExecutor); } finally { @@ -402,13 +493,18 @@ public class OnDeviceIntelligenceManagerService extends SystemService { } ensureRemoteInferenceServiceInitialized(); int callerUid = Binder.getCallingUid(); - result = mRemoteInferenceService.post( - service -> service.processRequestStreaming(callerUid, - feature, - request, requestType, - wrapCancellationFuture(cancellationSignalFuture), - wrapProcessingFuture(processingSignalFuture), - streamingCallback)); + result = mRemoteInferenceService.postAsync( + service -> { + AndroidFuture future = new AndroidFuture(); + service.processRequestStreaming(callerUid, + feature, + request, requestType, + wrapCancellationFuture(cancellationSignalFuture), + wrapProcessingFuture(processingSignalFuture), + wrapWithValidation(streamingCallback, + resourceClosingExecutor, future)); + return future.orTimeout(getIdleTimeoutMs(), TimeUnit.MILLISECONDS); + }); result.whenCompleteAsync((c, e) -> BundleUtil.tryCloseResource(request), resourceClosingExecutor); } finally { @@ -859,4 +955,10 @@ public class OnDeviceIntelligenceManagerService extends SystemService { return mTemporaryHandler; } + + private long getIdleTimeoutMs() { + return Settings.Secure.getLongForUser(mContext.getContentResolver(), + Settings.Secure.ON_DEVICE_INTELLIGENCE_IDLE_TIMEOUT_MS, TimeUnit.HOURS.toMillis(1), + mContext.getUserId()); + } } diff --git a/services/core/java/com/android/server/ondeviceintelligence/RemoteOnDeviceIntelligenceService.java b/services/core/java/com/android/server/ondeviceintelligence/RemoteOnDeviceIntelligenceService.java index 48258d7bea72..ac9747aa83b3 100644 --- a/services/core/java/com/android/server/ondeviceintelligence/RemoteOnDeviceIntelligenceService.java +++ b/services/core/java/com/android/server/ondeviceintelligence/RemoteOnDeviceIntelligenceService.java @@ -22,17 +22,21 @@ import static android.content.Context.BIND_INCLUDE_CAPABILITIES; import android.content.ComponentName; import android.content.Context; import android.content.Intent; +import android.provider.Settings; import android.service.ondeviceintelligence.IOnDeviceIntelligenceService; import android.service.ondeviceintelligence.OnDeviceIntelligenceService; import com.android.internal.infra.ServiceConnector; +import java.util.concurrent.TimeUnit; + /** * Manages the connection to the remote on-device intelligence service. Also, handles unbinding * logic set by the service implementation via a Secure Settings flag. */ public class RemoteOnDeviceIntelligenceService extends ServiceConnector.Impl<IOnDeviceIntelligenceService> { + private static final long LONG_TIMEOUT = TimeUnit.HOURS.toMillis(4); private static final String TAG = RemoteOnDeviceIntelligenceService.class.getSimpleName(); @@ -48,9 +52,15 @@ public class RemoteOnDeviceIntelligenceService extends } @Override + protected long getRequestTimeoutMs() { + return LONG_TIMEOUT; + } + + @Override protected long getAutoDisconnectTimeoutMs() { - // Disable automatic unbinding. - // TODO: add logic to fetch this flag via SecureSettings. - return -1; + return Settings.Secure.getLongForUser(mContext.getContentResolver(), + Settings.Secure.ON_DEVICE_INTELLIGENCE_UNBIND_TIMEOUT_MS, + TimeUnit.SECONDS.toMillis(30), + mContext.getUserId()); } } diff --git a/services/core/java/com/android/server/ondeviceintelligence/RemoteOnDeviceSandboxedInferenceService.java b/services/core/java/com/android/server/ondeviceintelligence/RemoteOnDeviceSandboxedInferenceService.java index 69ba1d2fb599..18b13838ea7c 100644 --- a/services/core/java/com/android/server/ondeviceintelligence/RemoteOnDeviceSandboxedInferenceService.java +++ b/services/core/java/com/android/server/ondeviceintelligence/RemoteOnDeviceSandboxedInferenceService.java @@ -22,18 +22,24 @@ import static android.content.Context.BIND_INCLUDE_CAPABILITIES; import android.content.ComponentName; import android.content.Context; import android.content.Intent; +import android.provider.Settings; import android.service.ondeviceintelligence.IOnDeviceSandboxedInferenceService; import android.service.ondeviceintelligence.OnDeviceSandboxedInferenceService; import com.android.internal.infra.ServiceConnector; +import java.util.concurrent.TimeUnit; + /** - * Manages the connection to the remote on-device sand boxed inference service. Also, handles unbinding + * Manages the connection to the remote on-device sand boxed inference service. Also, handles + * unbinding * logic set by the service implementation via a SecureSettings flag. */ public class RemoteOnDeviceSandboxedInferenceService extends ServiceConnector.Impl<IOnDeviceSandboxedInferenceService> { + private static final long LONG_TIMEOUT = TimeUnit.HOURS.toMillis(1); + /** * Creates an instance of {@link ServiceConnector} * @@ -54,11 +60,17 @@ public class RemoteOnDeviceSandboxedInferenceService extends connect(); } + @Override + protected long getRequestTimeoutMs() { + return LONG_TIMEOUT; + } + @Override protected long getAutoDisconnectTimeoutMs() { - // Disable automatic unbinding. - // TODO: add logic to fetch this flag via SecureSettings. - return -1; + return Settings.Secure.getLongForUser(mContext.getContentResolver(), + Settings.Secure.ON_DEVICE_INFERENCE_UNBIND_TIMEOUT_MS, + TimeUnit.SECONDS.toMillis(30), + mContext.getUserId()); } } diff --git a/services/core/java/com/android/server/ondeviceintelligence/callbacks/ListenableDownloadCallback.java b/services/core/java/com/android/server/ondeviceintelligence/callbacks/ListenableDownloadCallback.java new file mode 100644 index 000000000000..32f0698a8f9c --- /dev/null +++ b/services/core/java/com/android/server/ondeviceintelligence/callbacks/ListenableDownloadCallback.java @@ -0,0 +1,97 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.ondeviceintelligence.callbacks; + +import android.app.ondeviceintelligence.IDownloadCallback; +import android.os.Handler; +import android.os.PersistableBundle; +import android.os.RemoteException; + +import com.android.internal.infra.AndroidFuture; + +import java.util.concurrent.TimeoutException; + +/** + * This class extends the {@link IDownloadCallback} and adds a timeout Runnable to the callback + * such that, in the case where the callback methods are not invoked, we do not have to wait for + * timeout based on {@link #onDownloadCompleted} which might take minutes or hours to complete in + * some cases. Instead, in such cases we rely on the remote service sending progress updates and if + * there are *no* progress callbacks in the duration of {@link #idleTimeoutMs}, we can assume the + * download will not complete and enabling faster cleanup. + */ +public class ListenableDownloadCallback extends IDownloadCallback.Stub implements Runnable { + private final IDownloadCallback callback; + private final Handler handler; + private final AndroidFuture future; + private final long idleTimeoutMs; + + /** + * Constructor to create a ListenableDownloadCallback. + * + * @param callback callback to send download updates to caller. + * @param handler handler to schedule timeout runnable. + * @param future future to complete to signal the callback has reached a terminal state. + * @param idleTimeoutMs timeout within which download updates should be received. + */ + public ListenableDownloadCallback(IDownloadCallback callback, Handler handler, + AndroidFuture future, + long idleTimeoutMs) { + this.callback = callback; + this.handler = handler; + this.future = future; + this.idleTimeoutMs = idleTimeoutMs; + handler.postDelayed(this, + idleTimeoutMs); // init the timeout runnable in case no callback is ever invoked + } + + @Override + public void onDownloadStarted(long bytesToDownload) throws RemoteException { + callback.onDownloadStarted(bytesToDownload); + handler.removeCallbacks(this); + handler.postDelayed(this, idleTimeoutMs); + } + + @Override + public void onDownloadProgress(long bytesDownloaded) throws RemoteException { + callback.onDownloadProgress(bytesDownloaded); + handler.removeCallbacks(this); // remove previously queued timeout tasks. + handler.postDelayed(this, idleTimeoutMs); // queue fresh timeout task for next update. + } + + @Override + public void onDownloadFailed(int failureStatus, + String errorMessage, PersistableBundle errorParams) throws RemoteException { + callback.onDownloadFailed(failureStatus, errorMessage, errorParams); + handler.removeCallbacks(this); + future.completeExceptionally(new TimeoutException()); + } + + @Override + public void onDownloadCompleted( + android.os.PersistableBundle downloadParams) throws RemoteException { + callback.onDownloadCompleted(downloadParams); + handler.removeCallbacks(this); + future.complete(null); + } + + @Override + public void run() { + future.completeExceptionally( + new TimeoutException()); // complete the future as we haven't received updates + // for download progress. + } +}
\ No newline at end of file |