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.IBinder; 27 import android.os.RemoteException; 28 import android.os.ServiceManager; 29 import android.util.Log; 30 31 import dalvik.system.CloseGuard; 32 33 import java.util.List; 34 import java.util.UUID; 35 import java.util.concurrent.Executor; 36 import java.util.concurrent.atomic.AtomicBoolean; 37 import java.util.function.Consumer; 38 39 /** 40 * Client needs to create {@link SearchSession} object from in order to execute 41 * {@link #query(Query, Executor, Consumer)} method and share client side signals 42 * back to the service using {@link #notifyEvent(Query, SearchTargetEvent)}. 43 * 44 * <p> 45 * Usage: <pre> {@code 46 * 47 * class MyActivity { 48 * 49 * void onCreate() { 50 * mSearchSession.createSearchSession(searchContext) 51 * } 52 * 53 * void afterTextChanged(...) { 54 * mSearchSession.query(...); 55 * } 56 * 57 * void onTouch(...) OR 58 * void onStateTransitionStarted(...) OR 59 * void onResume(...) OR 60 * void onStop(...) { 61 * mSearchSession.notifyEvent(event); 62 * } 63 * 64 * void onDestroy() { 65 * mSearchSession.close(); 66 * } 67 * 68 * }</pre> 69 * 70 * @hide 71 */ 72 @SystemApi 73 public final class SearchSession implements AutoCloseable{ 74 75 private static final String TAG = SearchSession.class.getSimpleName(); 76 private static final boolean DEBUG = false; 77 78 private final android.app.search.ISearchUiManager mInterface; 79 private final CloseGuard mCloseGuard = CloseGuard.get(); 80 private final AtomicBoolean mIsClosed = new AtomicBoolean(false); 81 82 private final SearchSessionId mSessionId; 83 private final IBinder mToken = new Binder(); 84 85 /** 86 * Creates a new search ui client. 87 * <p> 88 * The caller should call {@link SearchSession#destroy()} to dispose the client once it 89 * no longer used. 90 * 91 * @param context the {@link Context} of the user of this {@link SearchSession}. 92 * @param searchContext the search context. 93 */ 94 // b/175668315 Create weak reference child objects to not leak context. SearchSession(@onNull Context context, @NonNull SearchContext searchContext)95 SearchSession(@NonNull Context context, @NonNull SearchContext searchContext) { 96 IBinder b = ServiceManager.getService(Context.SEARCH_UI_SERVICE); 97 mInterface = android.app.search.ISearchUiManager.Stub.asInterface(b); 98 mSessionId = new SearchSessionId( 99 context.getPackageName() + ":" + UUID.randomUUID().toString(), context.getUserId()); 100 // b/175527717 whitelist possible clients of this API 101 searchContext.setPackageName(context.getPackageName()); 102 try { 103 mInterface.createSearchSession(searchContext, mSessionId, mToken); 104 } catch (RemoteException e) { 105 Log.e(TAG, "Failed to search session", e); 106 e.rethrowFromSystemServer(); 107 } 108 109 mCloseGuard.open("close"); 110 } 111 112 /** 113 * Notifies the search service of an search target event (e.g., user interaction 114 * and lifecycle event of the search surface). 115 * 116 * {@see SearchTargetEvent} 117 * 118 * @param query input object associated with the event. 119 * @param event The {@link SearchTargetEvent} that represents the search target event. 120 */ notifyEvent(@onNull Query query, @NonNull SearchTargetEvent event)121 public void notifyEvent(@NonNull Query query, @NonNull SearchTargetEvent event) { 122 if (mIsClosed.get()) { 123 throw new IllegalStateException("This client has already been destroyed."); 124 } 125 126 try { 127 mInterface.notifyEvent(mSessionId, query, event); 128 } catch (RemoteException e) { 129 Log.e(TAG, "Failed to notify event", e); 130 e.rethrowFromSystemServer(); 131 } 132 } 133 134 /** 135 * Calls consumer with list of {@link SearchTarget}s based on the input query. 136 * 137 * @param input query object to be used for the request. 138 * @param callbackExecutor The callback executor to use when calling the callback. 139 * @param callback The callback to return the list of search targets. 140 */ 141 @Nullable query(@onNull Query input, @NonNull @CallbackExecutor Executor callbackExecutor, @NonNull Consumer<List<SearchTarget>> callback)142 public void query(@NonNull Query input, 143 @NonNull @CallbackExecutor Executor callbackExecutor, 144 @NonNull Consumer<List<SearchTarget>> callback) { 145 if (mIsClosed.get()) { 146 throw new IllegalStateException("This client has already been destroyed."); 147 } 148 149 try { 150 151 mInterface.query(mSessionId, input, new CallbackWrapper(callbackExecutor, callback)); 152 } catch (RemoteException e) { 153 Log.e(TAG, "Failed to sort targets", e); 154 e.rethrowFromSystemServer(); 155 } 156 } 157 158 /** 159 * Destroys the client and unregisters the callback. Any method on this class after this call 160 * will throw {@link IllegalStateException}. 161 * 162 * @deprecated 163 * @removed 164 */ 165 @Deprecated destroy()166 public void destroy() { 167 if (!mIsClosed.getAndSet(true)) { 168 mCloseGuard.close(); 169 170 // Do destroy; 171 try { 172 mInterface.destroySearchSession(mSessionId); 173 } catch (RemoteException e) { 174 Log.e(TAG, "Failed to notify search target event", e); 175 e.rethrowFromSystemServer(); 176 } 177 } else { 178 throw new IllegalStateException("This client has already been destroyed."); 179 } 180 } 181 182 @Override finalize()183 protected void finalize() { 184 try { 185 if (mCloseGuard != null) { 186 mCloseGuard.warnIfOpen(); 187 } 188 if (!mIsClosed.get()) { 189 destroy(); 190 } 191 } finally { 192 try { 193 super.finalize(); 194 } catch (Throwable throwable) { 195 throwable.printStackTrace(); 196 } 197 } 198 } 199 200 /** 201 * Destroys the client and unregisters the callback. Any method on this class after this call 202 * will throw {@link IllegalStateException}. 203 * 204 */ 205 @Override close()206 public void close() { 207 try { 208 finalize(); 209 } catch (Throwable throwable) { 210 throwable.printStackTrace(); 211 } 212 } 213 214 static class CallbackWrapper extends Stub { 215 216 private final Consumer<List<SearchTarget>> mCallback; 217 private final Executor mExecutor; 218 CallbackWrapper(@onNull Executor callbackExecutor, @NonNull Consumer<List<SearchTarget>> callback)219 CallbackWrapper(@NonNull Executor callbackExecutor, 220 @NonNull Consumer<List<SearchTarget>> callback) { 221 mCallback = callback; 222 mExecutor = callbackExecutor; 223 } 224 225 @Override onResult(ParceledListSlice result)226 public void onResult(ParceledListSlice result) { 227 final long identity = Binder.clearCallingIdentity(); 228 try { 229 if (DEBUG) { 230 Log.d(TAG, "CallbackWrapper.onResult result=" + result.getList()); 231 } 232 mExecutor.execute(() -> mCallback.accept(result.getList())); 233 } finally { 234 Binder.restoreCallingIdentity(identity); 235 } 236 } 237 } 238 } 239