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.service.search; 17 18 import static com.android.internal.util.function.pooled.PooledLambda.obtainMessage; 19 20 import android.annotation.CallSuper; 21 import android.annotation.MainThread; 22 import android.annotation.NonNull; 23 import android.annotation.Nullable; 24 import android.annotation.SystemApi; 25 import android.app.Service; 26 import android.app.search.ISearchCallback; 27 import android.app.search.Query; 28 import android.app.search.SearchContext; 29 import android.app.search.SearchSessionId; 30 import android.app.search.SearchTarget; 31 import android.app.search.SearchTargetEvent; 32 import android.content.Intent; 33 import android.content.pm.ParceledListSlice; 34 import android.os.Handler; 35 import android.os.IBinder; 36 import android.os.Looper; 37 import android.os.RemoteException; 38 import android.service.search.ISearchUiService.Stub; 39 import android.util.ArrayMap; 40 import android.util.Slog; 41 42 import java.util.ArrayList; 43 import java.util.List; 44 import java.util.function.Consumer; 45 46 /** 47 * A service used to share the lifecycle of search UI (open, close, interaction) 48 * and also to return search result on a query. 49 * 50 * To understand the lifecycle of search session and how a query get issued, 51 * {@see SearchSession} 52 * 53 * @hide 54 */ 55 @SystemApi 56 public abstract class SearchUiService extends Service { 57 58 private static final boolean DEBUG = false; 59 private static final String TAG = "SearchUiService"; 60 61 /** 62 * The {@link Intent} that must be declared as handled by the service. 63 * 64 * <p>The service must also require the {@link android.permission#MANAGE_SEARCH_UI} 65 * permission. 66 * 67 * @hide 68 */ 69 public static final String SERVICE_INTERFACE = 70 "android.service.search.SearchUiService"; 71 72 private final ArrayMap<SearchSessionId, ArrayList<CallbackWrapper>> 73 mSessionEmptyQueryResultCallbacks = new ArrayMap<>(); 74 75 private Handler mHandler; 76 77 private final android.service.search.ISearchUiService mInterface = new Stub() { 78 79 @Override 80 public void onCreateSearchSession(SearchContext context, SearchSessionId sessionId) { 81 mHandler.sendMessage( 82 obtainMessage(SearchUiService::onSearchSessionCreated, 83 SearchUiService.this, context, sessionId)); 84 // to be removed 85 mHandler.sendMessage( 86 obtainMessage(SearchUiService::onCreateSearchSession, 87 SearchUiService.this, context, sessionId)); 88 } 89 90 @Override 91 public void onQuery(SearchSessionId sessionId, Query input, 92 ISearchCallback callback) { 93 mHandler.sendMessage( 94 obtainMessage(SearchUiService::onQuery, 95 SearchUiService.this, sessionId, input, 96 new CallbackWrapper(callback, null))); 97 } 98 99 @Override 100 public void onNotifyEvent(SearchSessionId sessionId, Query query, SearchTargetEvent event) { 101 mHandler.sendMessage( 102 obtainMessage(SearchUiService::onNotifyEvent, 103 SearchUiService.this, sessionId, query, event)); 104 } 105 106 @Override 107 public void onRegisterEmptyQueryResultUpdateCallback(SearchSessionId sessionId, 108 ISearchCallback callback) { 109 mHandler.sendMessage( 110 obtainMessage(SearchUiService::doRegisterEmptyQueryResultUpdateCallback, 111 SearchUiService.this, sessionId, callback)); 112 } 113 114 @Override 115 public void onUnregisterEmptyQueryResultUpdateCallback(SearchSessionId sessionId, 116 ISearchCallback callback) { 117 mHandler.sendMessage( 118 obtainMessage(SearchUiService::doUnregisterEmptyQueryResultUpdateCallback, 119 SearchUiService.this, sessionId, callback)); 120 } 121 122 @Override 123 public void onDestroy(SearchSessionId sessionId) { 124 mHandler.sendMessage( 125 obtainMessage(SearchUiService::doDestroy, 126 SearchUiService.this, sessionId)); 127 } 128 }; 129 130 @CallSuper 131 @Override onCreate()132 public void onCreate() { 133 super.onCreate(); 134 mHandler = new Handler(Looper.getMainLooper(), null, true); 135 } 136 137 @Override 138 @NonNull onBind(@onNull Intent intent)139 public final IBinder onBind(@NonNull Intent intent) { 140 if (SERVICE_INTERFACE.equals(intent.getAction())) { 141 return mInterface.asBinder(); 142 } 143 Slog.w(TAG, "Tried to bind to wrong intent (should be " 144 + SERVICE_INTERFACE + ": " + intent); 145 return null; 146 } 147 148 /** 149 * Creates a new search session. 150 * 151 * @removed 152 * @deprecated this is method will be removed as soon as 153 * {@link #onSearchSessionCreated(SearchContext, SearchSessionId)} 154 * is adopted by the service. 155 */ 156 @Deprecated onCreateSearchSession(@onNull SearchContext context, @NonNull SearchSessionId sessionId)157 public void onCreateSearchSession(@NonNull SearchContext context, 158 @NonNull SearchSessionId sessionId) { 159 } 160 161 /** 162 * A new search session is created. 163 */ onSearchSessionCreated(@onNull SearchContext context, @NonNull SearchSessionId sessionId)164 public void onSearchSessionCreated(@NonNull SearchContext context, 165 @NonNull SearchSessionId sessionId) { 166 mSessionEmptyQueryResultCallbacks.put(sessionId, new ArrayList<>()); 167 } 168 169 /** 170 * Called by the client to request search results using a query string. 171 */ 172 @MainThread onQuery(@onNull SearchSessionId sessionId, @NonNull Query query, @NonNull Consumer<List<SearchTarget>> callback)173 public abstract void onQuery(@NonNull SearchSessionId sessionId, 174 @NonNull Query query, 175 @NonNull Consumer<List<SearchTarget>> callback); 176 177 /** 178 * Called by a client to indicate an interaction (tap, long press, drag, etc) on target(s) 179 * and lifecycle event on the search surface (e.g., visibility change). 180 * 181 * {@see SearchTargetEvent} 182 */ 183 @MainThread onNotifyEvent(@onNull SearchSessionId sessionId, @NonNull Query query, @NonNull SearchTargetEvent event)184 public abstract void onNotifyEvent(@NonNull SearchSessionId sessionId, 185 @NonNull Query query, 186 @NonNull SearchTargetEvent event); 187 doRegisterEmptyQueryResultUpdateCallback(@onNull SearchSessionId sessionId, @NonNull ISearchCallback callback)188 private void doRegisterEmptyQueryResultUpdateCallback(@NonNull SearchSessionId sessionId, 189 @NonNull ISearchCallback callback) { 190 final ArrayList<CallbackWrapper> callbacks = mSessionEmptyQueryResultCallbacks.get( 191 sessionId); 192 if (callbacks == null) { 193 Slog.e(TAG, "Failed to register for updates for unknown session: " + sessionId); 194 return; 195 } 196 197 final CallbackWrapper wrapper = findCallbackWrapper(callbacks, callback); 198 if (wrapper == null) { 199 callbacks.add(new CallbackWrapper(callback, 200 callbackWrapper -> 201 mHandler.post(() -> 202 removeCallbackWrapper(callbacks, callbackWrapper)))); 203 if (callbacks.size() == 1) { 204 onStartUpdateEmptyQueryResult(); 205 } 206 } 207 } 208 209 /** 210 * Called when the first empty query result callback is registered. Service provider may make 211 * their own decision whether to generate data if no callback is registered to optimize for 212 * system health. 213 */ 214 @MainThread onStartUpdateEmptyQueryResult()215 public void onStartUpdateEmptyQueryResult() {} 216 doUnregisterEmptyQueryResultUpdateCallback(@onNull SearchSessionId sessionId, @NonNull ISearchCallback callback)217 private void doUnregisterEmptyQueryResultUpdateCallback(@NonNull SearchSessionId sessionId, 218 @NonNull ISearchCallback callback) { 219 final ArrayList<CallbackWrapper> callbacks = mSessionEmptyQueryResultCallbacks.get( 220 sessionId); 221 if (callbacks == null) { 222 Slog.e(TAG, "Failed to unregister for updates for unknown session: " + sessionId); 223 return; 224 } 225 226 final CallbackWrapper wrapper = findCallbackWrapper(callbacks, callback); 227 removeCallbackWrapper(callbacks, wrapper); 228 } 229 230 /** 231 * Finds the callback wrapper for the given callback. 232 */ findCallbackWrapper(ArrayList<CallbackWrapper> callbacks, ISearchCallback callback)233 private CallbackWrapper findCallbackWrapper(ArrayList<CallbackWrapper> callbacks, 234 ISearchCallback callback) { 235 for (int i = callbacks.size() - 1; i >= 0; i--) { 236 if (callbacks.get(i).isCallback(callback)) { 237 return callbacks.get(i); 238 } 239 } 240 return null; 241 } 242 removeCallbackWrapper(@ullable ArrayList<CallbackWrapper> callbacks, @Nullable CallbackWrapper wrapper)243 private void removeCallbackWrapper(@Nullable ArrayList<CallbackWrapper> callbacks, 244 @Nullable CallbackWrapper wrapper) { 245 if (callbacks == null || wrapper == null) { 246 return; 247 } 248 callbacks.remove(wrapper); 249 wrapper.destroy(); 250 if (callbacks.isEmpty()) { 251 onStopUpdateEmptyQueryResult(); 252 } 253 } 254 255 /** 256 * Called when there are no longer any empty query result callbacks registered. Service 257 * provider can choose to stop generating data to optimize for system health. 258 */ 259 @MainThread onStopUpdateEmptyQueryResult()260 public void onStopUpdateEmptyQueryResult() {} 261 doDestroy(@onNull SearchSessionId sessionId)262 private void doDestroy(@NonNull SearchSessionId sessionId) { 263 super.onDestroy(); 264 onDestroy(sessionId); 265 } 266 267 /** 268 * Destroys a search session. 269 */ 270 @MainThread onDestroy(@onNull SearchSessionId sessionId)271 public abstract void onDestroy(@NonNull SearchSessionId sessionId); 272 273 /** 274 * Used by the service provider to send back results the client app. The can be called 275 * in response to {@link #onRequestEmptyQueryResultUpdate(SearchSessionId)} or proactively as 276 * a result of changes in zero state data. 277 */ updateEmptyQueryResult(@onNull SearchSessionId sessionId, @NonNull List<SearchTarget> targets)278 public final void updateEmptyQueryResult(@NonNull SearchSessionId sessionId, 279 @NonNull List<SearchTarget> targets) { 280 List<CallbackWrapper> callbacks = mSessionEmptyQueryResultCallbacks.get(sessionId); 281 if (callbacks != null) { 282 for (CallbackWrapper callback : callbacks) { 283 callback.accept(targets); 284 } 285 } 286 } 287 288 private static final class CallbackWrapper implements Consumer<List<SearchTarget>>, 289 IBinder.DeathRecipient { 290 291 private ISearchCallback mCallback; 292 private final Consumer<CallbackWrapper> mOnBinderDied; 293 CallbackWrapper(ISearchCallback callback, @Nullable Consumer<CallbackWrapper> onBinderDied)294 CallbackWrapper(ISearchCallback callback, 295 @Nullable Consumer<CallbackWrapper> onBinderDied) { 296 mCallback = callback; 297 mOnBinderDied = onBinderDied; 298 if (mOnBinderDied != null) { 299 try { 300 mCallback.asBinder().linkToDeath(this, 0); 301 } catch (RemoteException e) { 302 Slog.e(TAG, "Failed to link to death:" + e); 303 } 304 } 305 } 306 isCallback(@onNull ISearchCallback callback)307 public boolean isCallback(@NonNull ISearchCallback callback) { 308 if (mCallback == null) { 309 Slog.e(TAG, "Callback is null, likely the binder has died."); 310 return false; 311 } 312 return mCallback.asBinder().equals(callback.asBinder()); 313 } 314 315 316 @Override accept(List<SearchTarget> searchTargets)317 public void accept(List<SearchTarget> searchTargets) { 318 try { 319 if (mCallback != null) { 320 if (DEBUG) { 321 Slog.d(TAG, "CallbackWrapper.accept searchTargets=" + searchTargets); 322 } 323 mCallback.onResult(new ParceledListSlice(searchTargets)); 324 } 325 } catch (RemoteException e) { 326 Slog.e(TAG, "Error sending result:" + e); 327 } 328 } 329 destroy()330 public void destroy() { 331 if (mCallback != null && mOnBinderDied != null) { 332 mCallback.asBinder().unlinkToDeath(this, 0); 333 } 334 } 335 336 @Override binderDied()337 public void binderDied() { 338 destroy(); 339 mCallback = null; 340 if (mOnBinderDied != null) { 341 mOnBinderDied.accept(this); 342 } 343 } 344 } 345 } 346