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