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