summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
author Aldi Fahrezi <aldifahrezi@google.com> 2024-08-15 17:57:03 +0000
committer Aldi Fahrezi <aldifahrezi@google.com> 2024-08-15 17:59:39 +0000
commit270b169c8a1c4b3417ea44b0e022e0044e24c22b (patch)
treed2d52753fe960ad4c52f36c8d44b54ca7ca51e9c
parent70282a7d6e17488de8c3db955fc18f1653be7cb4 (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.java86
-rw-r--r--core/java/android/app/appfunctions/ServiceCallHelperImpl.java157
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();
+ }
+ }
+}