diff options
| author | 2024-08-15 17:57:03 +0000 | |
|---|---|---|
| committer | 2024-08-15 17:59:39 +0000 | |
| commit | 270b169c8a1c4b3417ea44b0e022e0044e24c22b (patch) | |
| tree | d2d52753fe960ad4c52f36c8d44b54ca7ca51e9c | |
| parent | 70282a7d6e17488de8c3db955fc18f1653be7cb4 (diff) | |
Add ServiceCallHelper
Bug: 360141291
Flag: android.app.appfunctions.flags.enable_app_function_manager
Test: manual
Change-Id: Ied2b7b4462d70c10bbf06116183360a04c04b881
| -rw-r--r-- | core/java/android/app/appfunctions/ServiceCallHelper.java | 86 | ||||
| -rw-r--r-- | core/java/android/app/appfunctions/ServiceCallHelperImpl.java | 157 |
2 files changed, 243 insertions, 0 deletions
diff --git a/core/java/android/app/appfunctions/ServiceCallHelper.java b/core/java/android/app/appfunctions/ServiceCallHelper.java new file mode 100644 index 000000000000..cc882bd4ba4a --- /dev/null +++ b/core/java/android/app/appfunctions/ServiceCallHelper.java @@ -0,0 +1,86 @@ +/* + * 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 android.app.appfunctions; + +import android.annotation.NonNull; +import android.content.Intent; +import android.os.UserHandle; + +/** + * Defines a contract for establishing temporary connections to services and executing operations + * within a specified timeout. Implementations of this interface provide mechanisms to ensure that + * services are properly unbound after the operation completes or a timeout occurs. + * + * @param <T> Class of wrapped service. + * @hide + */ +public interface ServiceCallHelper<T> { + + /** + * Initiates service binding and executes a provided method when the service connects. Unbinds + * the service after execution or upon timeout. Returns the result of the bindService API. + * + * <p>When the service connection was made successfully, it's the caller responsibility to + * report the usage is completed and can be unbound by calling {@link + * ServiceUsageCompleteListener#onCompleted()}. + * + * <p>This method includes a timeout mechanism to prevent the system from being stuck in a state + * where a service is bound indefinitely (for example, if the binder method never returns). This + * helps ensure that the calling app does not remain alive unnecessarily. + * + * @param intent An Intent object that describes the service that should be bound. + * @param bindFlags Flags used to control the binding process See {@link + * android.content.Context#bindService}. + * @param timeoutInMillis The maximum time in milliseconds to wait for the service connection. + * @param userHandle The UserHandle of the user for which the service should be bound. + * @param callback A callback to be invoked for various events. See {@link + * RunServiceCallCallback}. + */ + boolean runServiceCall( + @NonNull Intent intent, + int bindFlags, + long timeoutInMillis, + @NonNull UserHandle userHandle, + @NonNull RunServiceCallCallback<T> callback); + + /** An interface for clients to signal that they have finished using a bound service. */ + interface ServiceUsageCompleteListener { + /** + * Called when a client has finished using a bound service. This indicates that the service + * can be safely unbound. + */ + void onCompleted(); + } + + interface RunServiceCallCallback<T> { + /** + * Called when the service connection has been established. Uses {@code + * serviceUsageCompleteListener} to report finish using the connected service. + */ + void onServiceConnected( + @NonNull T service, + @NonNull ServiceUsageCompleteListener serviceUsageCompleteListener); + + /** Called when the service connection was failed to establish. */ + void onFailedToConnect(); + + /** + * Called when the whole operation(i.e. binding and the service call) takes longer than + * allowed. + */ + void onTimedOut(); + } +} diff --git a/core/java/android/app/appfunctions/ServiceCallHelperImpl.java b/core/java/android/app/appfunctions/ServiceCallHelperImpl.java new file mode 100644 index 000000000000..2e585467f437 --- /dev/null +++ b/core/java/android/app/appfunctions/ServiceCallHelperImpl.java @@ -0,0 +1,157 @@ +/* + * 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 android.app.appfunctions; + +import android.annotation.NonNull; +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.content.ServiceConnection; +import android.os.Handler; +import android.os.IBinder; +import android.os.Looper; +import android.os.UserHandle; +import android.util.Log; + +import java.util.concurrent.Executor; +import java.util.function.Function; + +/** + * An implementation of {@link android.app.appfunctions.ServiceCallHelper} that that is based on + * {@link Context#bindService}. + * + * @param <T> Class of wrapped service. + * @hide + */ +public class ServiceCallHelperImpl<T> implements ServiceCallHelper<T> { + private static final String TAG = "AppFunctionsServiceCall"; + + @NonNull private final Context mContext; + @NonNull private final Function<IBinder, T> mInterfaceConverter; + private final Handler mHandler = new Handler(Looper.getMainLooper()); + private final Executor mExecutor; + + /** + * @param interfaceConverter A function responsible for converting an IBinder object into the + * desired service interface. + * @param executor An Executor instance to dispatch callback. + * @param context The system context. + */ + public ServiceCallHelperImpl( + @NonNull Context context, + @NonNull Function<IBinder, T> interfaceConverter, + @NonNull Executor executor) { + mContext = context; + mInterfaceConverter = interfaceConverter; + mExecutor = executor; + } + + @Override + public boolean runServiceCall( + @NonNull Intent intent, + int bindFlags, + long timeoutInMillis, + @NonNull UserHandle userHandle, + @NonNull RunServiceCallCallback<T> callback) { + OneOffServiceConnection serviceConnection = + new OneOffServiceConnection( + intent, bindFlags, timeoutInMillis, userHandle, callback); + + return serviceConnection.bindAndRun(); + } + + private class OneOffServiceConnection + implements ServiceConnection, ServiceUsageCompleteListener { + private final Intent mIntent; + private final int mFlags; + private final long mTimeoutMillis; + private final UserHandle mUserHandle; + private final RunServiceCallCallback<T> mCallback; + private final Runnable mTimeoutCallback; + + OneOffServiceConnection( + @NonNull Intent intent, + int flags, + long timeoutMillis, + @NonNull UserHandle userHandle, + @NonNull RunServiceCallCallback<T> callback) { + mIntent = intent; + mFlags = flags; + mTimeoutMillis = timeoutMillis; + mCallback = callback; + mTimeoutCallback = + () -> + mExecutor.execute( + () -> { + safeUnbind(); + mCallback.onTimedOut(); + }); + mUserHandle = userHandle; + } + + public boolean bindAndRun() { + boolean bindServiceResult = + mContext.bindServiceAsUser(mIntent, this, mFlags, mUserHandle); + + if (bindServiceResult) { + mHandler.postDelayed(mTimeoutCallback, mTimeoutMillis); + } else { + safeUnbind(); + } + + return bindServiceResult; + } + + @Override + public void onServiceConnected(ComponentName name, IBinder service) { + T serviceInterface = mInterfaceConverter.apply(service); + + mExecutor.execute(() -> mCallback.onServiceConnected(serviceInterface, this)); + } + + @Override + public void onServiceDisconnected(ComponentName name) { + safeUnbind(); + mExecutor.execute(mCallback::onFailedToConnect); + } + + @Override + public void onBindingDied(ComponentName name) { + safeUnbind(); + mExecutor.execute(mCallback::onFailedToConnect); + } + + @Override + public void onNullBinding(ComponentName name) { + safeUnbind(); + mExecutor.execute(mCallback::onFailedToConnect); + } + + private void safeUnbind() { + try { + mHandler.removeCallbacks(mTimeoutCallback); + mContext.unbindService(this); + } catch (Exception ex) { + Log.w(TAG, "Failed to unbind", ex); + } + } + + @Override + public void onCompleted() { + safeUnbind(); + } + } +} |