/* * Copyright (C) 2020 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.search; import android.annotation.CallbackExecutor; import android.annotation.NonNull; import android.annotation.Nullable; import android.annotation.SystemApi; import android.app.search.ISearchCallback.Stub; import android.content.Context; import android.content.pm.ParceledListSlice; import android.os.Binder; import android.os.Bundle; 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; import java.util.UUID; import java.util.concurrent.Executor; import java.util.concurrent.atomic.AtomicBoolean; import java.util.function.Consumer; /** * Client needs to create {@link SearchSession} object from in order to execute * {@link #query(Query, Executor, Consumer)} method and share client side signals * back to the service using {@link #notifyEvent(Query, SearchTargetEvent)}. * *
* Usage:
{@code * * class MyActivity { * * void onCreate() { * mSearchSession.createSearchSession(searchContext) * } * * void afterTextChanged(...) { * mSearchSession.query(...); * } * * void onTouch(...) OR * void onStateTransitionStarted(...) OR * void onResume(...) OR * void onStop(...) { * mSearchSession.notifyEvent(event); * } * * void onDestroy() { * mSearchSession.close(); * } * * }* * @hide */ @SystemApi public final class SearchSession implements AutoCloseable { private static final String TAG = SearchSession.class.getSimpleName(); private static final boolean DEBUG = false; private final android.app.search.ISearchUiManager mInterface; private final CloseGuard mCloseGuard = CloseGuard.get(); private final AtomicBoolean mIsClosed = new AtomicBoolean(false); private final SearchSessionId mSessionId; private final IBinder mToken = new Binder(); @GuardedBy("itself") private final ArrayMap
* The caller should call {@link SearchSession#destroy()} to dispose the client once it
* no longer used.
*
* @param context the {@link Context} of the user of this {@link SearchSession}.
* @param searchContext the search context.
*/
// b/175668315 Create weak reference child objects to not leak context.
SearchSession(@NonNull Context context, @NonNull SearchContext searchContext) {
IBinder b = ServiceManager.getService(Context.SEARCH_UI_SERVICE);
mInterface = android.app.search.ISearchUiManager.Stub.asInterface(b);
mSessionId = new SearchSessionId(
context.getPackageName() + ":" + UUID.randomUUID().toString(), context.getUserId());
// b/175527717 allowlist possible clients of this API
searchContext.setPackageName(context.getPackageName());
try {
mInterface.createSearchSession(searchContext, mSessionId, mToken);
} catch (RemoteException e) {
Log.e(TAG, "Failed to search session", e);
e.rethrowFromSystemServer();
}
mCloseGuard.open("SearchSession.close");
}
/**
* Notifies the search service of an search target event (e.g., user interaction
* and lifecycle event of the search surface).
*
* {@see SearchTargetEvent}
*
* @param query input object associated with the event.
* @param event The {@link SearchTargetEvent} that represents the search target event.
*/
public void notifyEvent(@NonNull Query query, @NonNull SearchTargetEvent event) {
if (mIsClosed.get()) {
throw new IllegalStateException("This client has already been destroyed.");
}
try {
mInterface.notifyEvent(mSessionId, query, event);
} catch (RemoteException e) {
Log.e(TAG, "Failed to notify event", e);
e.rethrowFromSystemServer();
}
}
/**
* Calls consumer with list of {@link SearchTarget}s based on the input query.
*
* @param input query object to be used for the request.
* @param callbackExecutor The callback executor to use when calling the callback.
* @param callback The callback to return the list of search targets.
*/
@Nullable
public void query(@NonNull Query input,
@NonNull @CallbackExecutor Executor callbackExecutor,
@NonNull Consumer> callback) {
if (mIsClosed.get()) {
throw new IllegalStateException("This client has already been destroyed.");
}
try {
mInterface.query(mSessionId, input, new CallbackWrapper(callbackExecutor, callback));
} catch (RemoteException e) {
Log.e(TAG, "Failed to sort targets", e);
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 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();
}
}
}
/**
* Destroys the client and unregisters the callback. Any method on this class after this call
* will throw {@link IllegalStateException}.
*
* @deprecated
* @removed
*/
@Deprecated
public void destroy() {
if (!mIsClosed.getAndSet(true)) {
mCloseGuard.close();
// Do destroy;
try {
mInterface.destroySearchSession(mSessionId);
} catch (RemoteException e) {
Log.e(TAG, "Failed to notify search target event", e);
e.rethrowFromSystemServer();
}
} else {
throw new IllegalStateException("This client has already been destroyed.");
}
}
@Override
protected void finalize() {
try {
if (mCloseGuard != null) {
mCloseGuard.warnIfOpen();
}
if (!mIsClosed.get()) {
destroy();
}
} finally {
try {
super.finalize();
} catch (Throwable throwable) {
throwable.printStackTrace();
}
}
}
/**
* Destroys the client and unregisters the callback. Any method on this class after this call
* will throw {@link IllegalStateException}.
*
*/
@Override
public void close() {
try {
finalize();
} catch (Throwable throwable) {
throwable.printStackTrace();
}
}
/**
* 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
> mCallback;
private final Executor mExecutor;
CallbackWrapper(@NonNull Executor callbackExecutor,
@NonNull Consumer
> callback) {
mCallback = callback;
mExecutor = callbackExecutor;
}
@Override
public void onResult(ParceledListSlice result) {
final long identity = Binder.clearCallingIdentity();
try {
if (DEBUG) {
Log.d(TAG, "CallbackWrapper.onResult result=" + result.getList());
}
List