/* * Copyright (C) 2018 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.service.appprediction; import static com.android.internal.util.function.pooled.PooledLambda.obtainMessage; import android.annotation.CallSuper; import android.annotation.FlaggedApi; import android.annotation.MainThread; import android.annotation.NonNull; import android.annotation.Nullable; import android.annotation.SystemApi; import android.app.Service; import android.app.prediction.AppPredictionContext; import android.app.prediction.AppPredictionSessionId; import android.app.prediction.AppTarget; import android.app.prediction.AppTargetEvent; import android.app.prediction.AppTargetId; import android.app.prediction.IPredictionCallback; import android.content.Intent; import android.content.pm.ParceledListSlice; import android.os.Bundle; import android.os.CancellationSignal; import android.os.Handler; import android.os.IBinder; import android.os.IRemoteCallback; import android.os.Looper; import android.os.RemoteException; import android.service.appprediction.IPredictionService.Stub; import android.service.appprediction.flags.Flags; import android.util.ArrayMap; import android.util.Log; import android.util.Slog; import java.util.ArrayList; import java.util.List; import java.util.function.Consumer; /** * A service used to predict app and shortcut usage. * * @hide */ @SystemApi public abstract class AppPredictionService extends Service { private static final String TAG = "AppPredictionService"; /** * The {@link Intent} that must be declared as handled by the service. * *

The service must also require the {@link android.permission#MANAGE_APP_PREDICTIONS} * permission. * * @hide */ public static final String SERVICE_INTERFACE = "android.service.appprediction.AppPredictionService"; private final ArrayMap> mSessionCallbacks = new ArrayMap<>(); private Handler mHandler; private final IPredictionService mInterface = new Stub() { @Override public void onCreatePredictionSession(AppPredictionContext context, AppPredictionSessionId sessionId) { mHandler.sendMessage( obtainMessage(AppPredictionService::doCreatePredictionSession, AppPredictionService.this, context, sessionId)); } @Override public void notifyAppTargetEvent(AppPredictionSessionId sessionId, AppTargetEvent event) { mHandler.sendMessage( obtainMessage(AppPredictionService::onAppTargetEvent, AppPredictionService.this, sessionId, event)); } @Override public void notifyLaunchLocationShown(AppPredictionSessionId sessionId, String launchLocation, ParceledListSlice targetIds) { mHandler.sendMessage( obtainMessage(AppPredictionService::onLaunchLocationShown, AppPredictionService.this, sessionId, launchLocation, targetIds.getList())); } @Override public void sortAppTargets(AppPredictionSessionId sessionId, ParceledListSlice targets, IPredictionCallback callback) { mHandler.sendMessage( obtainMessage(AppPredictionService::onSortAppTargets, AppPredictionService.this, sessionId, targets.getList(), null, new CallbackWrapper(callback, null))); } @Override public void registerPredictionUpdates(AppPredictionSessionId sessionId, IPredictionCallback callback) { mHandler.sendMessage( obtainMessage(AppPredictionService::doRegisterPredictionUpdates, AppPredictionService.this, sessionId, callback)); } @Override public void unregisterPredictionUpdates(AppPredictionSessionId sessionId, IPredictionCallback callback) { mHandler.sendMessage( obtainMessage(AppPredictionService::doUnregisterPredictionUpdates, AppPredictionService.this, sessionId, callback)); } @Override public void requestPredictionUpdate(AppPredictionSessionId sessionId) { mHandler.sendMessage( obtainMessage(AppPredictionService::doRequestPredictionUpdate, AppPredictionService.this, sessionId)); } @Override public void onDestroyPredictionSession(AppPredictionSessionId sessionId) { mHandler.sendMessage( obtainMessage(AppPredictionService::doDestroyPredictionSession, AppPredictionService.this, sessionId)); } @FlaggedApi(Flags.FLAG_SERVICE_FEATURES_API) @Override public void requestServiceFeatures(AppPredictionSessionId sessionId, IRemoteCallback callback) { mHandler.sendMessage( obtainMessage(AppPredictionService::onRequestServiceFeatures, AppPredictionService.this, sessionId, new RemoteCallbackWrapper(callback, null))); } }; @CallSuper @Override public void onCreate() { super.onCreate(); mHandler = new Handler(Looper.getMainLooper(), null, true); } @Override @NonNull public final IBinder onBind(@NonNull Intent intent) { if (SERVICE_INTERFACE.equals(intent.getAction())) { return mInterface.asBinder(); } Log.w(TAG, "Tried to bind to wrong intent (should be " + SERVICE_INTERFACE + ": " + intent); return null; } /** * Called by a client app to indicate a target launch */ @MainThread public abstract void onAppTargetEvent(@NonNull AppPredictionSessionId sessionId, @NonNull AppTargetEvent event); /** * Called by a client app to indication a particular location has been shown to the user. */ @MainThread public abstract void onLaunchLocationShown(@NonNull AppPredictionSessionId sessionId, @NonNull String launchLocation, @NonNull List targetIds); private void doCreatePredictionSession(@NonNull AppPredictionContext context, @NonNull AppPredictionSessionId sessionId) { mSessionCallbacks.put(sessionId, new ArrayList<>()); onCreatePredictionSession(context, sessionId); } /** * Creates a new interaction session. * * @param context interaction context * @param sessionId the session's Id */ public void onCreatePredictionSession(@NonNull AppPredictionContext context, @NonNull AppPredictionSessionId sessionId) {} /** * Called by the client app to request sorting of targets based on prediction rank. */ @MainThread public abstract void onSortAppTargets(@NonNull AppPredictionSessionId sessionId, @NonNull List targets, @NonNull CancellationSignal cancellationSignal, @NonNull Consumer> callback); private void doRegisterPredictionUpdates(@NonNull AppPredictionSessionId sessionId, @NonNull IPredictionCallback callback) { final ArrayList callbacks = mSessionCallbacks.get(sessionId); if (callbacks == null) { Slog.e(TAG, "Failed to register for updates for unknown session: " + sessionId); return; } final CallbackWrapper wrapper = findCallbackWrapper(callbacks, callback); if (wrapper == null) { callbacks.add(new CallbackWrapper(callback, callbackWrapper -> mHandler.post(() -> removeCallbackWrapper(callbacks, callbackWrapper)))); if (callbacks.size() == 1) { onStartPredictionUpdates(); } } } /** * Called when any continuous prediction callback is registered. */ @MainThread public void onStartPredictionUpdates() {} private void doUnregisterPredictionUpdates(@NonNull AppPredictionSessionId sessionId, @NonNull IPredictionCallback callback) { final ArrayList callbacks = mSessionCallbacks.get(sessionId); if (callbacks == null) { Slog.e(TAG, "Failed to unregister for updates for unknown session: " + sessionId); return; } final CallbackWrapper wrapper = findCallbackWrapper(callbacks, callback); removeCallbackWrapper(callbacks, wrapper); } private void removeCallbackWrapper(@Nullable ArrayList callbacks, @Nullable CallbackWrapper wrapper) { if (callbacks == null || wrapper == null) { return; } callbacks.remove(wrapper); wrapper.destroy(); if (callbacks.isEmpty()) { onStopPredictionUpdates(); } } /** * Called when there are no longer any continuous prediction callbacks registered. */ @MainThread public void onStopPredictionUpdates() {} private void doRequestPredictionUpdate(@NonNull AppPredictionSessionId sessionId) { // Just an optimization, if there are no callbacks, then don't bother notifying the service final ArrayList callbacks = mSessionCallbacks.get(sessionId); if (callbacks != null && !callbacks.isEmpty()) { onRequestPredictionUpdate(sessionId); } } /** * Called by the client app to request target predictions. This method is only called if there * are one or more prediction callbacks registered. * * @see #updatePredictions(AppPredictionSessionId, List) */ @MainThread public abstract void onRequestPredictionUpdate(@NonNull AppPredictionSessionId sessionId); private void doDestroyPredictionSession(@NonNull AppPredictionSessionId sessionId) { final ArrayList callbacks = mSessionCallbacks.remove(sessionId); if (callbacks != null) callbacks.forEach(CallbackWrapper::destroy); onDestroyPredictionSession(sessionId); } /** * Destroys the interaction session. * * @param sessionId the id of the session to destroy */ @MainThread public void onDestroyPredictionSession(@NonNull AppPredictionSessionId sessionId) {} /** * Called by the client app to request {@link AppPredictionService} features info. * * @param sessionId the session's Id. It is @NonNull. * @param callback the callback to return the Bundle which includes service features info. It * is @NonNull. */ @FlaggedApi(Flags.FLAG_SERVICE_FEATURES_API) @MainThread public void onRequestServiceFeatures(@NonNull AppPredictionSessionId sessionId, @NonNull Consumer callback) {} /** * Used by the prediction factory to send back results the client app. The can be called * in response to {@link #onRequestPredictionUpdate(AppPredictionSessionId)} or proactively as * a result of changes in predictions. */ public final void updatePredictions(@NonNull AppPredictionSessionId sessionId, @NonNull List targets) { List callbacks = mSessionCallbacks.get(sessionId); if (callbacks != null) { for (CallbackWrapper callback : callbacks) { callback.accept(targets); } } } /** * Finds the callback wrapper for the given callback. */ private CallbackWrapper findCallbackWrapper(ArrayList callbacks, IPredictionCallback callback) { for (int i = callbacks.size() - 1; i >= 0; i--) { if (callbacks.get(i).isCallback(callback)) { return callbacks.get(i); } } return null; } private static final class CallbackWrapper implements Consumer>, IBinder.DeathRecipient { private IPredictionCallback mCallback; private final Consumer mOnBinderDied; CallbackWrapper(IPredictionCallback callback, @Nullable Consumer onBinderDied) { mCallback = callback; mOnBinderDied = onBinderDied; if (mOnBinderDied != null) { try { mCallback.asBinder().linkToDeath(this, 0); } catch (RemoteException e) { Slog.e(TAG, "Failed to link to death: " + e); } } } public boolean isCallback(@NonNull IPredictionCallback callback) { if (mCallback == null) { Slog.e(TAG, "Callback is null, likely the binder has died."); return false; } return mCallback.asBinder().equals(callback.asBinder()); } public void destroy() { if (mCallback != null && mOnBinderDied != null) { mCallback.asBinder().unlinkToDeath(this, 0); } } @Override public void accept(List ts) { try { if (mCallback != null) { mCallback.onResult(new ParceledListSlice(ts)); } } catch (RemoteException e) { Slog.e(TAG, "Error sending result:" + e); } } @Override public void binderDied() { destroy(); mCallback = null; if (mOnBinderDied != null) { mOnBinderDied.accept(this); } } } private static final class RemoteCallbackWrapper implements Consumer, IBinder.DeathRecipient { private IRemoteCallback mCallback; private final Consumer mOnBinderDied; RemoteCallbackWrapper(IRemoteCallback callback, @Nullable Consumer onBinderDied) { mCallback = callback; mOnBinderDied = onBinderDied; if (mOnBinderDied != null) { try { mCallback.asBinder().linkToDeath(this, 0); } catch (RemoteException e) { Slog.e(TAG, "Failed to link to death: " + e); } } } public void destroy() { if (mCallback != null && mOnBinderDied != null) { mCallback.asBinder().unlinkToDeath(this, 0); } } @Override public void accept(Bundle bundle) { try { if (mCallback != null) { mCallback.sendResult(bundle); } } catch (RemoteException e) { Slog.e(TAG, "Error sending result:" + e); } } @Override public void binderDied() { destroy(); mCallback = null; if (mOnBinderDied != null) { mOnBinderDied.accept(this); } } } }