Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 3d5edd04 authored by Sharon Su's avatar Sharon Su
Browse files

Implement a push mode path for SearchUiService to allow continous update

for zero state.
Implementation follows the AppPredictionService pattern.

Bug: 267356831
Test: atest CtsSearchUiServiceTestCases

Change-Id: Ice505d4eb88001a413cc93592e21fb875acb9a60
parent d338e257
Loading
Loading
Loading
Loading
+11 −0
Original line number Diff line number Diff line
@@ -2121,6 +2121,13 @@ package android.app.search {
    method protected void finalize();
    method public void notifyEvent(@NonNull android.app.search.Query, @NonNull android.app.search.SearchTargetEvent);
    method @Nullable public void query(@NonNull android.app.search.Query, @NonNull java.util.concurrent.Executor, @NonNull java.util.function.Consumer<java.util.List<android.app.search.SearchTarget>>);
    method public void registerEmptyQueryResultUpdateCallback(@NonNull java.util.concurrent.Executor, @NonNull android.app.search.SearchSession.Callback);
    method public void requestEmptyQueryResultUpdate();
    method public void unregisterEmptyQueryResultUpdateCallback(@NonNull java.util.concurrent.Executor, @NonNull android.app.search.SearchSession.Callback);
  }
  public static interface SearchSession.Callback {
    method public void onTargetsAvailable(@NonNull java.util.List<android.app.search.SearchTarget>);
  }
  public final class SearchSessionId implements android.os.Parcelable {
@@ -12273,7 +12280,11 @@ package android.service.search {
    method @MainThread public abstract void onDestroy(@NonNull android.app.search.SearchSessionId);
    method @MainThread public abstract void onNotifyEvent(@NonNull android.app.search.SearchSessionId, @NonNull android.app.search.Query, @NonNull android.app.search.SearchTargetEvent);
    method @MainThread public abstract void onQuery(@NonNull android.app.search.SearchSessionId, @NonNull android.app.search.Query, @NonNull java.util.function.Consumer<java.util.List<android.app.search.SearchTarget>>);
    method @MainThread public void onRequestEmptyQueryResultUpdate(@NonNull android.app.search.SearchSessionId);
    method public void onSearchSessionCreated(@NonNull android.app.search.SearchContext, @NonNull android.app.search.SearchSessionId);
    method @MainThread public void onStartUpdateEmptyQueryResult();
    method @MainThread public void onStopUpdateEmptyQueryResult();
    method public final void updateEmptyQueryResult(@NonNull android.app.search.SearchSessionId, @NonNull java.util.List<android.app.search.SearchTarget>);
  }
}
+6 −0
Original line number Diff line number Diff line
@@ -36,5 +36,11 @@ interface ISearchUiManager {

    void notifyEvent(in SearchSessionId sessionId, in Query input, in SearchTargetEvent event);

    void registerEmptyQueryResultUpdateCallback(in SearchSessionId sessionId, in ISearchCallback callback);

    void requestEmptyQueryResultUpdate(in SearchSessionId sessionId);

    void unregisterEmptyQueryResultUpdateCallback(in SearchSessionId sessionId, in ISearchCallback callback);

    void destroySearchSession(in SearchSessionId sessionId);
}
+107 −0
Original line number Diff line number Diff line
@@ -28,8 +28,11 @@ import android.os.IBinder;
import android.os.RemoteException;
import android.os.ServiceManager;
import android.os.SystemClock;
import android.util.ArrayMap;
import android.util.Log;

import com.android.internal.annotations.GuardedBy;

import dalvik.system.CloseGuard;

import java.util.List;
@@ -83,6 +86,8 @@ public final class SearchSession implements AutoCloseable {

    private final SearchSessionId mSessionId;
    private final IBinder mToken = new Binder();
    @GuardedBy("itself")
    private final ArrayMap<Callback, CallbackWrapper> mRegisteredCallbacks = new ArrayMap<>();

    /**
     * Creates a new search ui client.
@@ -156,6 +161,95 @@ public final class SearchSession implements AutoCloseable {
            e.rethrowFromSystemServer();
        }
    }
    /**
     * Request the search ui service provide continuous updates of {@link SearchTarget} list
     * via the provided callback to render for zero state, until the given callback is
     * unregistered. Zero state means when user entered search ui but not issued any query yet.
     *
     * @see SearchSession.Callback#onTargetsAvailable(List).
     *
     * @param callbackExecutor The callback executor to use when calling the callback.
     * @param callback The Callback to be called when updates of search targets for zero state
     *                 are available.
     */
    public void registerEmptyQueryResultUpdateCallback(
            @NonNull @CallbackExecutor Executor callbackExecutor,
            @NonNull Callback callback) {
        synchronized (mRegisteredCallbacks) {
            if (mIsClosed.get()) {
                throw new IllegalStateException("This client has already been destroyed.");
            }
            if (mRegisteredCallbacks.containsKey(callback)) {
                // Skip if this callback is already registered
                return;
            }
            try {
                final CallbackWrapper callbackWrapper = new CallbackWrapper(callbackExecutor,
                        callback::onTargetsAvailable);
                mInterface.registerEmptyQueryResultUpdateCallback(mSessionId, callbackWrapper);
                mRegisteredCallbacks.put(callback, callbackWrapper);
            } catch (RemoteException e) {
                Log.e(TAG, "Failed to register for empty query result updates", e);
                e.rethrowAsRuntimeException();
            }
        }
    }

    /**
     * Requests the search ui service to stop providing continuous updates of {@link SearchTarget}
     * to the provided callback for zero state until the callback is re-registered. Zero state
     * means when user entered search ui but not issued any query yet.
     *
     * @see {@link SearchSession#registerEmptyQueryResultUpdateCallback(Executor, Callback)}
     * @param callback The callback to be unregistered.
     */
    public void unregisterEmptyQueryResultUpdateCallback(
            @NonNull @CallbackExecutor Executor callbackExecutor,
            @NonNull Callback callback) {
        synchronized (mRegisteredCallbacks) {
            if (mIsClosed.get()) {
                throw new IllegalStateException("This client has already been destroyed.");
            }

            if (!mRegisteredCallbacks.containsKey(callback)) {
                // Skip if this callback was never registered
                return;
            }
            try {
                final CallbackWrapper callbackWrapper = mRegisteredCallbacks.remove(callback);
                mInterface.unregisterEmptyQueryResultUpdateCallback(mSessionId, callbackWrapper);
            } catch (RemoteException e) {
                Log.e(TAG, "Failed to unregister for empty query result updates", e);
                e.rethrowAsRuntimeException();
            }
        }
    }

    /**
     * Requests the search ui service to dispatch a new set of search targets to the pre-registered
     * callback for zero state. Zero state means when user entered search ui but not issued any
     * query yet. This method can be used for client to sync up with server data if they think data
     * might be out of sync, for example, after restart.
     * Pre-register a callback with
     * {@link SearchSession#registerEmptyQueryResultUpdateCallback(Executor, Callback)}
     * is required before calling this method. Otherwise this is no-op.
     *
     * @see {@link SearchSession#registerEmptyQueryResultUpdateCallback(Executor, Callback)}
     * @see {@link SearchSession.Callback#onTargetsAvailable(List)}.
     */
    public void requestEmptyQueryResultUpdate() {
        if (mIsClosed.get()) {
            throw new IllegalStateException("This client has already been destroyed.");
        }

        try {
            mInterface.requestEmptyQueryResultUpdate(mSessionId);
        } catch (RemoteException e) {
            Log.e(TAG, "Failed to request empty query result update", e);
            e.rethrowAsRuntimeException();
        }
    }


    /**
     * Destroys the client and unregisters the callback. Any method on this class after this call
@@ -213,6 +307,19 @@ public final class SearchSession implements AutoCloseable {
        }
    }

    /**
     *  Callback for receiving {@link SearchTarget} updates for zero state. Zero state
     *  means when user entered search ui but not issued any query yet.
     */
    public interface Callback {

        /**
         * Called when a new set of {@link SearchTarget} are available for zero state.
         * @param targets Sorted list of search targets.
         */
        void onTargetsAvailable(@NonNull List<SearchTarget> targets);
    }

    static class CallbackWrapper extends Stub {

        private final Consumer<List<SearchTarget>> mCallback;
+6 −0
Original line number Diff line number Diff line
@@ -37,5 +37,11 @@ oneway interface ISearchUiService {

    void onNotifyEvent(in SearchSessionId sessionId, in Query input, in SearchTargetEvent event);

    void onRegisterEmptyQueryResultUpdateCallback (in SearchSessionId sessionId, in ISearchCallback callback);

    void onRequestEmptyQueryResultUpdate(in SearchSessionId sessionId);

    void onUnregisterEmptyQueryResultUpdateCallback(in SearchSessionId sessionId, in ISearchCallback callback);

    void onDestroy(in SearchSessionId sessionId);
}
+179 −7
Original line number Diff line number Diff line
@@ -20,6 +20,7 @@ import static com.android.internal.util.function.pooled.PooledLambda.obtainMessa
import android.annotation.CallSuper;
import android.annotation.MainThread;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.SystemApi;
import android.app.Service;
import android.app.search.ISearchCallback;
@@ -35,8 +36,10 @@ import android.os.IBinder;
import android.os.Looper;
import android.os.RemoteException;
import android.service.search.ISearchUiService.Stub;
import android.util.ArrayMap;
import android.util.Slog;

import java.util.ArrayList;
import java.util.List;
import java.util.function.Consumer;

@@ -66,6 +69,9 @@ public abstract class SearchUiService extends Service {
    public static final String SERVICE_INTERFACE =
            "android.service.search.SearchUiService";

    private final ArrayMap<SearchSessionId, ArrayList<CallbackWrapper>>
            mSessionEmptyQueryResultCallbacks = new ArrayMap<>();

    private Handler mHandler;

    private final android.service.search.ISearchUiService mInterface = new Stub() {
@@ -87,7 +93,7 @@ public abstract class SearchUiService extends Service {
            mHandler.sendMessage(
                    obtainMessage(SearchUiService::onQuery,
                            SearchUiService.this, sessionId, input,
                            new CallbackWrapper(callback)));
                            new CallbackWrapper(callback, null)));
        }

        @Override
@@ -97,6 +103,28 @@ public abstract class SearchUiService extends Service {
                            SearchUiService.this, sessionId, query, event));
        }

        @Override
        public void onRegisterEmptyQueryResultUpdateCallback(SearchSessionId sessionId,
                ISearchCallback callback) {
            mHandler.sendMessage(
                    obtainMessage(SearchUiService::doRegisterEmptyQueryResultUpdateCallback,
                            SearchUiService.this, sessionId, callback));
        }

        @Override
        public void onRequestEmptyQueryResultUpdate(SearchSessionId sessionId) {
            mHandler.sendMessage(obtainMessage(SearchUiService::doRequestEmptyQueryResultUpdate,
                    SearchUiService.this, sessionId));
        }

        @Override
        public void onUnregisterEmptyQueryResultUpdateCallback(SearchSessionId sessionId,
                ISearchCallback callback) {
            mHandler.sendMessage(
                    obtainMessage(SearchUiService::doUnregisterEmptyQueryResultUpdateCallback,
                            SearchUiService.this, sessionId, callback));
        }

        @Override
        public void onDestroy(SearchSessionId sessionId) {
            mHandler.sendMessage(
@@ -126,21 +154,23 @@ public abstract class SearchUiService extends Service {
    /**
     * Creates a new search session.
     *
     * @removed
     * @deprecated this is method will be removed as soon as
     * {@link #onSearchSessionCreated(SearchContext, SearchSessionId)}
     * is adopted by the service.
     *
     * @removed
     */
    @Deprecated
    public void onCreateSearchSession(@NonNull SearchContext context,
            @NonNull SearchSessionId sessionId) {}
            @NonNull SearchSessionId sessionId) {
    }

    /**
     * A new search session is created.
     */
    public void onSearchSessionCreated(@NonNull SearchContext context,
            @NonNull SearchSessionId sessionId) {}
            @NonNull SearchSessionId sessionId) {
        mSessionEmptyQueryResultCallbacks.put(sessionId, new ArrayList<>());
    }

    /**
     * Called by the client to request search results using a query string.
@@ -161,6 +191,98 @@ public abstract class SearchUiService extends Service {
            @NonNull Query query,
            @NonNull SearchTargetEvent event);

    private void doRegisterEmptyQueryResultUpdateCallback(@NonNull SearchSessionId sessionId,
            @NonNull ISearchCallback callback) {
        final ArrayList<CallbackWrapper> callbacks = mSessionEmptyQueryResultCallbacks.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) {
                onStartUpdateEmptyQueryResult();
            }
        }
    }

    /**
     * Called when the first empty query result callback is registered. Service provider may make
     * their own decision whether to generate data if no callback is registered to optimize for
     * system health.
     */
    @MainThread
    public void onStartUpdateEmptyQueryResult() {}

    private void doRequestEmptyQueryResultUpdate(@NonNull SearchSessionId sessionId) {
        // Just an optimization, if there are no callbacks, then don't bother notifying the service
        final ArrayList<CallbackWrapper> callbacks = mSessionEmptyQueryResultCallbacks.get(
                sessionId);
        if (callbacks != null && !callbacks.isEmpty()) {
            onRequestEmptyQueryResultUpdate(sessionId);
        }
    }

    /**
     * Called by a client to request empty query search target result for zero state. This method
     * is only called if there are one or more empty query result update callbacks registered.
     *
     * @see #updateEmptyQueryResult(SearchSessionId, List)
     */
    @MainThread
    public void onRequestEmptyQueryResultUpdate(@NonNull SearchSessionId sessionId) {}

    private void doUnregisterEmptyQueryResultUpdateCallback(@NonNull SearchSessionId sessionId,
            @NonNull ISearchCallback callback) {
        final ArrayList<CallbackWrapper> callbacks = mSessionEmptyQueryResultCallbacks.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);
    }

    /**
     * Finds the callback wrapper for the given callback.
     */
    private CallbackWrapper findCallbackWrapper(ArrayList<CallbackWrapper> callbacks,
            ISearchCallback callback) {
        for (int i = callbacks.size() - 1; i >= 0; i--) {
            if (callbacks.get(i).isCallback(callback)) {
                return callbacks.get(i);
            }
        }
        return null;
    }

    private void removeCallbackWrapper(@Nullable ArrayList<CallbackWrapper> callbacks,
            @Nullable CallbackWrapper wrapper) {
        if (callbacks == null || wrapper == null) {
            return;
        }
        callbacks.remove(wrapper);
        wrapper.destroy();
        if (callbacks.isEmpty()) {
            onStopUpdateEmptyQueryResult();
        }
    }

    /**
     * Called when there are no longer any empty query result callbacks registered. Service
     * provider can choose to stop generating data to optimize for system health.
     */
    @MainThread
    public void onStopUpdateEmptyQueryResult() {}

    private void doDestroy(@NonNull SearchSessionId sessionId) {
        super.onDestroy();
        onDestroy(sessionId);
@@ -172,14 +294,49 @@ public abstract class SearchUiService extends Service {
    @MainThread
    public abstract void onDestroy(@NonNull SearchSessionId sessionId);

    private static final class CallbackWrapper implements Consumer<List<SearchTarget>> {
    /**
     * Used by the service provider to send back results the client app. The can be called
     * in response to {@link #onRequestEmptyQueryResultUpdate(SearchSessionId)} or proactively as
     * a result of changes in zero state data.
     */
    public final void updateEmptyQueryResult(@NonNull SearchSessionId sessionId,
            @NonNull List<SearchTarget> targets) {
        List<CallbackWrapper> callbacks = mSessionEmptyQueryResultCallbacks.get(sessionId);
        if (callbacks != null) {
            for (CallbackWrapper callback : callbacks) {
                callback.accept(targets);
            }
        }
    }

    private static final class CallbackWrapper implements Consumer<List<SearchTarget>>,
            IBinder.DeathRecipient {

        private ISearchCallback mCallback;
        private final Consumer<CallbackWrapper> mOnBinderDied;

        CallbackWrapper(ISearchCallback callback) {
        CallbackWrapper(ISearchCallback callback,
                @Nullable Consumer<CallbackWrapper> 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 ISearchCallback callback) {
            if (mCallback == null) {
                Slog.e(TAG, "Callback is null, likely the binder has died.");
                return false;
            }
            return mCallback.asBinder().equals(callback.asBinder());
        }


        @Override
        public void accept(List<SearchTarget> searchTargets) {
            try {
@@ -193,5 +350,20 @@ public abstract class SearchUiService extends Service {
                Slog.e(TAG, "Error sending result:" + e);
            }
        }

        public void destroy() {
            if (mCallback != null && mOnBinderDied != null) {
                mCallback.asBinder().unlinkToDeath(this, 0);
            }
        }

        @Override
        public void binderDied() {
            destroy();
            mCallback = null;
            if (mOnBinderDied != null) {
                mOnBinderDied.accept(this);
            }
        }
    }
}
Loading