1 /* 2 * Copyright (C) 2020 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 package android.app.search; 17 18 import android.annotation.CallbackExecutor; 19 import android.annotation.NonNull; 20 import android.annotation.Nullable; 21 import android.annotation.SystemApi; 22 import android.app.search.ISearchCallback.Stub; 23 import android.content.Context; 24 import android.content.pm.ParceledListSlice; 25 import android.os.Binder; 26 import android.os.Bundle; 27 import android.os.IBinder; 28 import android.os.RemoteException; 29 import android.os.ServiceManager; 30 import android.os.SystemClock; 31 import android.util.ArrayMap; 32 import android.util.Log; 33 34 import com.android.internal.annotations.GuardedBy; 35 36 import dalvik.system.CloseGuard; 37 38 import java.util.List; 39 import java.util.UUID; 40 import java.util.concurrent.Executor; 41 import java.util.concurrent.atomic.AtomicBoolean; 42 import java.util.function.Consumer; 43 44 /** 45 * Client needs to create {@link SearchSession} object from in order to execute 46 * {@link #query(Query, Executor, Consumer)} method and share client side signals 47 * back to the service using {@link #notifyEvent(Query, SearchTargetEvent)}. 48 * 49 * <p> 50 * Usage: <pre> {@code 51 * 52 * class MyActivity { 53 * 54 * void onCreate() { 55 * mSearchSession.createSearchSession(searchContext) 56 * } 57 * 58 * void afterTextChanged(...) { 59 * mSearchSession.query(...); 60 * } 61 * 62 * void onTouch(...) OR 63 * void onStateTransitionStarted(...) OR 64 * void onResume(...) OR 65 * void onStop(...) { 66 * mSearchSession.notifyEvent(event); 67 * } 68 * 69 * void onDestroy() { 70 * mSearchSession.close(); 71 * } 72 * 73 * }</pre> 74 * 75 * @hide 76 */ 77 @SystemApi 78 public final class SearchSession implements AutoCloseable { 79 80 private static final String TAG = SearchSession.class.getSimpleName(); 81 private static final boolean DEBUG = false; 82 83 private final android.app.search.ISearchUiManager mInterface; 84 private final CloseGuard mCloseGuard = CloseGuard.get(); 85 private final AtomicBoolean mIsClosed = new AtomicBoolean(false); 86 87 private final SearchSessionId mSessionId; 88 private final IBinder mToken = new Binder(); 89 @GuardedBy("itself") 90 private final ArrayMap<Callback, CallbackWrapper> mRegisteredCallbacks = new ArrayMap<>(); 91 92 /** 93 * Creates a new search ui client. 94 * <p> 95 * The caller should call {@link SearchSession#destroy()} to dispose the client once it 96 * no longer used. 97 * 98 * @param context the {@link Context} of the user of this {@link SearchSession}. 99 * @param searchContext the search context. 100 */ 101 // b/175668315 Create weak reference child objects to not leak context. SearchSession(@onNull Context context, @NonNull SearchContext searchContext)102 SearchSession(@NonNull Context context, @NonNull SearchContext searchContext) { 103 IBinder b = ServiceManager.getService(Context.SEARCH_UI_SERVICE); 104 mInterface = android.app.search.ISearchUiManager.Stub.asInterface(b); 105 mSessionId = new SearchSessionId( 106 context.getPackageName() + ":" + UUID.randomUUID().toString(), context.getUserId()); 107 // b/175527717 whitelist possible clients of this API 108 searchContext.setPackageName(context.getPackageName()); 109 try { 110 mInterface.createSearchSession(searchContext, mSessionId, mToken); 111 } catch (RemoteException e) { 112 Log.e(TAG, "Failed to search session", e); 113 e.rethrowFromSystemServer(); 114 } 115 116 mCloseGuard.open("SearchSession.close"); 117 } 118 119 /** 120 * Notifies the search service of an search target event (e.g., user interaction 121 * and lifecycle event of the search surface). 122 * 123 * {@see SearchTargetEvent} 124 * 125 * @param query input object associated with the event. 126 * @param event The {@link SearchTargetEvent} that represents the search target event. 127 */ notifyEvent(@onNull Query query, @NonNull SearchTargetEvent event)128 public void notifyEvent(@NonNull Query query, @NonNull SearchTargetEvent event) { 129 if (mIsClosed.get()) { 130 throw new IllegalStateException("This client has already been destroyed."); 131 } 132 133 try { 134 mInterface.notifyEvent(mSessionId, query, event); 135 } catch (RemoteException e) { 136 Log.e(TAG, "Failed to notify event", e); 137 e.rethrowFromSystemServer(); 138 } 139 } 140 141 /** 142 * Calls consumer with list of {@link SearchTarget}s based on the input query. 143 * 144 * @param input query object to be used for the request. 145 * @param callbackExecutor The callback executor to use when calling the callback. 146 * @param callback The callback to return the list of search targets. 147 */ 148 @Nullable query(@onNull Query input, @NonNull @CallbackExecutor Executor callbackExecutor, @NonNull Consumer<List<SearchTarget>> callback)149 public void query(@NonNull Query input, 150 @NonNull @CallbackExecutor Executor callbackExecutor, 151 @NonNull Consumer<List<SearchTarget>> callback) { 152 if (mIsClosed.get()) { 153 throw new IllegalStateException("This client has already been destroyed."); 154 } 155 156 try { 157 158 mInterface.query(mSessionId, input, new CallbackWrapper(callbackExecutor, callback)); 159 } catch (RemoteException e) { 160 Log.e(TAG, "Failed to sort targets", e); 161 e.rethrowFromSystemServer(); 162 } 163 } 164 /** 165 * Request the search ui service provide continuous updates of {@link SearchTarget} list 166 * via the provided callback to render for zero state, until the given callback is 167 * unregistered. Zero state means when user entered search ui but not issued any query yet. 168 * 169 * @see SearchSession.Callback#onTargetsAvailable(List). 170 * 171 * @param callbackExecutor The callback executor to use when calling the callback. 172 * @param callback The Callback to be called when updates of search targets for zero state 173 * are available. 174 */ registerEmptyQueryResultUpdateCallback( @onNull @allbackExecutor Executor callbackExecutor, @NonNull Callback callback)175 public void registerEmptyQueryResultUpdateCallback( 176 @NonNull @CallbackExecutor Executor callbackExecutor, 177 @NonNull Callback callback) { 178 synchronized (mRegisteredCallbacks) { 179 if (mIsClosed.get()) { 180 throw new IllegalStateException("This client has already been destroyed."); 181 } 182 if (mRegisteredCallbacks.containsKey(callback)) { 183 // Skip if this callback is already registered 184 return; 185 } 186 try { 187 final CallbackWrapper callbackWrapper = new CallbackWrapper(callbackExecutor, 188 callback::onTargetsAvailable); 189 mInterface.registerEmptyQueryResultUpdateCallback(mSessionId, callbackWrapper); 190 mRegisteredCallbacks.put(callback, callbackWrapper); 191 } catch (RemoteException e) { 192 Log.e(TAG, "Failed to register for empty query result updates", e); 193 e.rethrowAsRuntimeException(); 194 } 195 } 196 } 197 198 /** 199 * Requests the search ui service to stop providing continuous updates of {@link SearchTarget} 200 * to the provided callback for zero state until the callback is re-registered. Zero state 201 * means when user entered search ui but not issued any query yet. 202 * 203 * @see {@link SearchSession#registerEmptyQueryResultUpdateCallback(Executor, Callback)} 204 * @param callback The callback to be unregistered. 205 */ unregisterEmptyQueryResultUpdateCallback( @onNull Callback callback)206 public void unregisterEmptyQueryResultUpdateCallback( 207 @NonNull Callback callback) { 208 synchronized (mRegisteredCallbacks) { 209 if (mIsClosed.get()) { 210 throw new IllegalStateException("This client has already been destroyed."); 211 } 212 213 if (!mRegisteredCallbacks.containsKey(callback)) { 214 // Skip if this callback was never registered 215 return; 216 } 217 try { 218 final CallbackWrapper callbackWrapper = mRegisteredCallbacks.remove(callback); 219 mInterface.unregisterEmptyQueryResultUpdateCallback(mSessionId, callbackWrapper); 220 } catch (RemoteException e) { 221 Log.e(TAG, "Failed to unregister for empty query result updates", e); 222 e.rethrowAsRuntimeException(); 223 } 224 } 225 } 226 227 /** 228 * Destroys the client and unregisters the callback. Any method on this class after this call 229 * will throw {@link IllegalStateException}. 230 * 231 * @deprecated 232 * @removed 233 */ 234 @Deprecated destroy()235 public void destroy() { 236 if (!mIsClosed.getAndSet(true)) { 237 mCloseGuard.close(); 238 239 // Do destroy; 240 try { 241 mInterface.destroySearchSession(mSessionId); 242 } catch (RemoteException e) { 243 Log.e(TAG, "Failed to notify search target event", e); 244 e.rethrowFromSystemServer(); 245 } 246 } else { 247 throw new IllegalStateException("This client has already been destroyed."); 248 } 249 } 250 251 @Override finalize()252 protected void finalize() { 253 try { 254 if (mCloseGuard != null) { 255 mCloseGuard.warnIfOpen(); 256 } 257 if (!mIsClosed.get()) { 258 destroy(); 259 } 260 } finally { 261 try { 262 super.finalize(); 263 } catch (Throwable throwable) { 264 throwable.printStackTrace(); 265 } 266 } 267 } 268 269 /** 270 * Destroys the client and unregisters the callback. Any method on this class after this call 271 * will throw {@link IllegalStateException}. 272 * 273 */ 274 @Override close()275 public void close() { 276 try { 277 finalize(); 278 } catch (Throwable throwable) { 279 throwable.printStackTrace(); 280 } 281 } 282 283 /** 284 * Callback for receiving {@link SearchTarget} updates for zero state. Zero state 285 * means when user entered search ui but not issued any query yet. 286 */ 287 public interface Callback { 288 289 /** 290 * Called when a new set of {@link SearchTarget} are available for zero state. 291 * @param targets Sorted list of search targets. 292 */ onTargetsAvailable(@onNull List<SearchTarget> targets)293 void onTargetsAvailable(@NonNull List<SearchTarget> targets); 294 } 295 296 static class CallbackWrapper extends Stub { 297 298 private final Consumer<List<SearchTarget>> mCallback; 299 private final Executor mExecutor; 300 CallbackWrapper(@onNull Executor callbackExecutor, @NonNull Consumer<List<SearchTarget>> callback)301 CallbackWrapper(@NonNull Executor callbackExecutor, 302 @NonNull Consumer<List<SearchTarget>> callback) { 303 mCallback = callback; 304 mExecutor = callbackExecutor; 305 } 306 307 @Override onResult(ParceledListSlice result)308 public void onResult(ParceledListSlice result) { 309 final long identity = Binder.clearCallingIdentity(); 310 try { 311 if (DEBUG) { 312 Log.d(TAG, "CallbackWrapper.onResult result=" + result.getList()); 313 } 314 List<SearchTarget> list = result.getList(); 315 if (list.size() > 0) { 316 Bundle bundle = list.get(0).getExtras(); 317 if (bundle != null) { 318 bundle.putLong("key_ipc_start", SystemClock.elapsedRealtime()); 319 } 320 } 321 mExecutor.execute(() -> mCallback.accept(list)); 322 } finally { 323 Binder.restoreCallingIdentity(identity); 324 } 325 } 326 } 327 } 328