328 lines
12 KiB
Java
328 lines
12 KiB
Java
![]() |
/*
|
||
|
* 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)}.
|
||
|
*
|
||
|
* <p>
|
||
|
* Usage: <pre> {@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();
|
||
|
* }
|
||
|
*
|
||
|
* }</pre>
|
||
|
*
|
||
|
* @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<Callback, CallbackWrapper> mRegisteredCallbacks = new ArrayMap<>();
|
||
|
|
||
|
/**
|
||
|
* Creates a new search ui client.
|
||
|
* <p>
|
||
|
* 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<List<SearchTarget>> 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<SearchTarget> targets);
|
||
|
}
|
||
|
|
||
|
static class CallbackWrapper extends Stub {
|
||
|
|
||
|
private final Consumer<List<SearchTarget>> mCallback;
|
||
|
private final Executor mExecutor;
|
||
|
|
||
|
CallbackWrapper(@NonNull Executor callbackExecutor,
|
||
|
@NonNull Consumer<List<SearchTarget>> 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<SearchTarget> list = result.getList();
|
||
|
if (list.size() > 0) {
|
||
|
Bundle bundle = list.get(0).getExtras();
|
||
|
if (bundle != null) {
|
||
|
bundle.putLong("key_ipc_start", SystemClock.elapsedRealtime());
|
||
|
}
|
||
|
}
|
||
|
mExecutor.execute(() -> mCallback.accept(list));
|
||
|
} finally {
|
||
|
Binder.restoreCallingIdentity(identity);
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}
|