Felipe Leme | 1dfa9a0 | 2018-10-17 17:24:37 -0700 | [diff] [blame] | 1 | /* |
| 2 | * Copyright (C) 2018 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 | */ |
Felipe Leme | 749b889 | 2018-12-03 16:30:30 -0800 | [diff] [blame] | 16 | package android.view.contentcapture; |
Felipe Leme | 1dfa9a0 | 2018-10-17 17:24:37 -0700 | [diff] [blame] | 17 | |
Felipe Leme | d32d8f6f | 2019-02-15 10:25:33 -0800 | [diff] [blame] | 18 | import static android.view.contentcapture.ContentCaptureHelper.sDebug; |
| 19 | import static android.view.contentcapture.ContentCaptureHelper.sVerbose; |
Felipe Leme | a8d33c2 | 2019-03-25 16:36:09 -0700 | [diff] [blame] | 20 | import static android.view.contentcapture.ContentCaptureHelper.toSet; |
Felipe Leme | be002d8 | 2019-01-23 10:22:32 -0800 | [diff] [blame] | 21 | |
Sergey Volnov | 6e04901 | 2020-01-10 18:10:48 +0000 | [diff] [blame] | 22 | import android.annotation.CallbackExecutor; |
Felipe Leme | d32d8f6f | 2019-02-15 10:25:33 -0800 | [diff] [blame] | 23 | import android.annotation.IntDef; |
Felipe Leme | 1dfa9a0 | 2018-10-17 17:24:37 -0700 | [diff] [blame] | 24 | import android.annotation.NonNull; |
| 25 | import android.annotation.Nullable; |
Felipe Leme | 91ddeca | 2019-01-24 18:01:58 -0800 | [diff] [blame] | 26 | import android.annotation.SystemApi; |
Felipe Leme | e348dc3 | 2018-11-05 12:35:29 -0800 | [diff] [blame] | 27 | import android.annotation.SystemService; |
Felipe Leme | 19652c0 | 2019-02-04 13:01:29 -0800 | [diff] [blame] | 28 | import android.annotation.TestApi; |
Felipe Leme | 87a9dc9 | 2018-12-18 14:28:07 -0800 | [diff] [blame] | 29 | import android.annotation.UiThread; |
Felipe Leme | 1dfa9a0 | 2018-10-17 17:24:37 -0700 | [diff] [blame] | 30 | import android.content.ComponentName; |
Felipe Leme | 326f15a | 2019-02-19 09:42:24 -0800 | [diff] [blame] | 31 | import android.content.ContentCaptureOptions; |
Felipe Leme | 1dfa9a0 | 2018-10-17 17:24:37 -0700 | [diff] [blame] | 32 | import android.content.Context; |
Felipe Leme | f0d44c6 | 2019-03-26 16:36:58 -0700 | [diff] [blame] | 33 | import android.graphics.Canvas; |
Sergey Volnov | 6e04901 | 2020-01-10 18:10:48 +0000 | [diff] [blame] | 34 | import android.os.Binder; |
Felipe Leme | 88eae3b | 2018-11-07 15:11:56 -0800 | [diff] [blame] | 35 | import android.os.Handler; |
Felipe Leme | e348dc3 | 2018-11-05 12:35:29 -0800 | [diff] [blame] | 36 | import android.os.IBinder; |
Felipe Leme | 34ccedf | 2019-01-17 13:42:35 -0800 | [diff] [blame] | 37 | import android.os.Looper; |
Sergey Volnov | 6e04901 | 2020-01-10 18:10:48 +0000 | [diff] [blame] | 38 | import android.os.ParcelFileDescriptor; |
Perumaal S | addabba | 2019-01-04 16:43:35 -0800 | [diff] [blame] | 39 | import android.os.RemoteException; |
Felipe Leme | f8b8778 | 2019-03-20 14:31:18 -0700 | [diff] [blame] | 40 | import android.os.ServiceManager; |
Felipe Leme | e348dc3 | 2018-11-05 12:35:29 -0800 | [diff] [blame] | 41 | import android.util.Log; |
Sergey Volnov | 6e04901 | 2020-01-10 18:10:48 +0000 | [diff] [blame] | 42 | import android.util.Slog; |
Felipe Leme | f0d44c6 | 2019-03-26 16:36:58 -0700 | [diff] [blame] | 43 | import android.view.View; |
| 44 | import android.view.ViewStructure; |
Adam He | 43c0699 | 2019-04-15 15:41:49 -0700 | [diff] [blame] | 45 | import android.view.WindowManager; |
Felipe Leme | 1af85ea | 2019-01-16 13:23:40 -0800 | [diff] [blame] | 46 | import android.view.contentcapture.ContentCaptureSession.FlushReason; |
Felipe Leme | 1dfa9a0 | 2018-10-17 17:24:37 -0700 | [diff] [blame] | 47 | |
Adam He | 6079d15 | 2019-01-10 11:37:17 -0800 | [diff] [blame] | 48 | import com.android.internal.annotations.GuardedBy; |
Felipe Leme | 1dfa9a0 | 2018-10-17 17:24:37 -0700 | [diff] [blame] | 49 | import com.android.internal.util.Preconditions; |
Perumaal S | addabba | 2019-01-04 16:43:35 -0800 | [diff] [blame] | 50 | import com.android.internal.util.SyncResultReceiver; |
Felipe Leme | 1dfa9a0 | 2018-10-17 17:24:37 -0700 | [diff] [blame] | 51 | |
Felipe Leme | e348dc3 | 2018-11-05 12:35:29 -0800 | [diff] [blame] | 52 | import java.io.PrintWriter; |
Felipe Leme | d32d8f6f | 2019-02-15 10:25:33 -0800 | [diff] [blame] | 53 | import java.lang.annotation.Retention; |
| 54 | import java.lang.annotation.RetentionPolicy; |
Sergey Volnov | 6e04901 | 2020-01-10 18:10:48 +0000 | [diff] [blame] | 55 | import java.lang.ref.WeakReference; |
Felipe Leme | a8d33c2 | 2019-03-25 16:36:09 -0700 | [diff] [blame] | 56 | import java.util.ArrayList; |
Felipe Leme | 790be04 | 2019-03-25 09:52:19 -0700 | [diff] [blame] | 57 | import java.util.Set; |
Sergey Volnov | 6e04901 | 2020-01-10 18:10:48 +0000 | [diff] [blame] | 58 | import java.util.concurrent.Executor; |
| 59 | import java.util.function.Consumer; |
Felipe Leme | 1dfa9a0 | 2018-10-17 17:24:37 -0700 | [diff] [blame] | 60 | |
Felipe Leme | ecb08be | 2018-11-27 15:48:47 -0800 | [diff] [blame] | 61 | /** |
Felipe Leme | f0d44c6 | 2019-03-26 16:36:58 -0700 | [diff] [blame] | 62 | * <p>The {@link ContentCaptureManager} provides additional ways for for apps to |
| 63 | * integrate with the content capture subsystem. |
| 64 | * |
| 65 | * <p>Content capture provides real-time, continuous capture of application activity, display and |
| 66 | * events to an intelligence service that is provided by the Android system. The intelligence |
| 67 | * service then uses that info to mediate and speed user journey through different apps. For |
| 68 | * example, when the user receives a restaurant address in a chat app and switchs to a map app |
| 69 | * to search for that restaurant, the intelligence service could offer an autofill dialog to |
| 70 | * let the user automatically select its address. |
| 71 | * |
| 72 | * <p>Content capture was designed with two major concerns in mind: privacy and performance. |
| 73 | * |
| 74 | * <ul> |
| 75 | * <li><b>Privacy:</b> the intelligence service is a trusted component provided that is provided |
| 76 | * by the device manufacturer and that cannot be changed by the user (although the user can |
| 77 | * globaly disable content capture using the Android Settings app). This service can only use the |
| 78 | * data for in-device machine learning, which is enforced both by process isolation and |
| 79 | * <a href="https://source.android.com/compatibility/cdd">CDD requirements</a>. |
| 80 | * <li><b>Performance:</b> content capture is highly optimized to minimize its impact in the app |
| 81 | * jankiness and overall device system health. For example, its only enabled on apps (or even |
| 82 | * specific activities from an app) that were explicitly whitelisted by the intelligence service, |
| 83 | * and it buffers the events so they are sent in a batch to the service (see |
| 84 | * {@link #isContentCaptureEnabled()} for other cases when its disabled). |
| 85 | * </ul> |
| 86 | * |
| 87 | * <p>In fact, before using this manager, the app developer should check if it's available. Example: |
Felipe Leme | 052c942 | 2019-04-19 16:01:29 -0700 | [diff] [blame] | 88 | * <pre><code> |
Felipe Leme | f0d44c6 | 2019-03-26 16:36:58 -0700 | [diff] [blame] | 89 | * ContentCaptureManager mgr = context.getSystemService(ContentCaptureManager.class); |
| 90 | * if (mgr != null && mgr.isContentCaptureEnabled()) { |
| 91 | * // ... |
| 92 | * } |
Felipe Leme | 052c942 | 2019-04-19 16:01:29 -0700 | [diff] [blame] | 93 | * </code></pre> |
Felipe Leme | f0d44c6 | 2019-03-26 16:36:58 -0700 | [diff] [blame] | 94 | * |
| 95 | * <p>App developers usually don't need to explicitly interact with content capture, except when the |
| 96 | * app: |
| 97 | * |
| 98 | * <ul> |
| 99 | * <li>Can define a contextual {@link android.content.LocusId} to identify unique state (such as a |
| 100 | * conversation between 2 chat users). |
| 101 | * <li>Can have multiple view hierarchies with different contextual meaning (for example, a |
| 102 | * browser app with multiple tabs, each representing a different URL). |
| 103 | * <li>Contains custom views (that extend View directly and are not provided by the standard |
| 104 | * Android SDK. |
| 105 | * <li>Contains views that provide their own virtual hierarchy (like a web browser that render the |
| 106 | * HTML elements using a Canvas). |
| 107 | * </ul> |
| 108 | * |
| 109 | * <p>The main integration point with content capture is the {@link ContentCaptureSession}. A "main" |
| 110 | * session is automatically created by the Android System when content capture is enabled for the |
| 111 | * activity and its used by the standard Android views to notify the content capture service of |
| 112 | * events such as views being added, views been removed, and text changed by user input. The session |
| 113 | * could have a {@link ContentCaptureContext} to provide more contextual info about it, such as |
| 114 | * the locus associated with the view hierarchy (see {@link android.content.LocusId} for more info |
| 115 | * about locus). By default, the main session doesn't have a {@code ContentCaptureContext}, but you |
| 116 | * can change it after its created. Example: |
| 117 | * |
| 118 | * <pre><code> |
| 119 | * protected void onCreate(Bundle savedInstanceState) { |
| 120 | * // Initialize view structure |
| 121 | * ContentCaptureSession session = rootView.getContentCaptureSession(); |
| 122 | * if (session != null) { |
| 123 | * session.setContentCaptureContext(ContentCaptureContext.forLocusId("chat_UserA_UserB")); |
| 124 | * } |
| 125 | * } |
| 126 | * </code></pre> |
| 127 | * |
| 128 | * <p>If your activity contains view hierarchies with a different contextual meaning, you should |
| 129 | * created child sessions for each view hierarchy root. For example, if your activity is a browser, |
| 130 | * you could use the main session for the main URL being rendered, then child sessions for each |
| 131 | * {@code IFRAME}: |
| 132 | * |
| 133 | * <pre><code> |
| 134 | * ContentCaptureSession mMainSession; |
| 135 | * |
| 136 | * protected void onCreate(Bundle savedInstanceState) { |
| 137 | * // Initialize view structure... |
| 138 | * mMainSession = rootView.getContentCaptureSession(); |
| 139 | * if (mMainSession != null) { |
| 140 | * mMainSession.setContentCaptureContext( |
| 141 | * ContentCaptureContext.forLocusId("https://example.com")); |
| 142 | * } |
| 143 | * } |
| 144 | * |
| 145 | * private void loadIFrame(View iframeRootView, String url) { |
| 146 | * if (mMainSession != null) { |
| 147 | * ContentCaptureSession iFrameSession = mMainSession.newChild( |
| 148 | * ContentCaptureContext.forLocusId(url)); |
| 149 | * } |
| 150 | * iframeRootView.setContentCaptureSession(iFrameSession); |
| 151 | * } |
| 152 | * // Load iframe... |
| 153 | * } |
| 154 | * </code></pre> |
| 155 | * |
| 156 | * <p>If your activity has custom views (i.e., views that extend {@link View} directly and provide |
| 157 | * just one logical view, not a virtual tree hiearchy) and it provides content that's relevant for |
| 158 | * content capture (as of {@link android.os.Build.VERSION_CODES#Q Android Q}, the only relevant |
| 159 | * content is text), then your view implementation should: |
| 160 | * |
| 161 | * <ul> |
| 162 | * <li>Set it as important for content capture. |
| 163 | * <li>Fill {@link ViewStructure} used for content capture. |
| 164 | * <li>Notify the {@link ContentCaptureSession} when the text is changed by user input. |
| 165 | * </ul> |
| 166 | * |
| 167 | * <p>Here's an example of the relevant methods for an {@code EditText}-like view: |
| 168 | * |
| 169 | * <pre><code> |
| 170 | * public class MyEditText extends View { |
| 171 | * |
| 172 | * public MyEditText(...) { |
| 173 | * if (getImportantForContentCapture() == IMPORTANT_FOR_CONTENT_CAPTURE_AUTO) { |
| 174 | * setImportantForContentCapture(IMPORTANT_FOR_CONTENT_CAPTURE_YES); |
| 175 | * } |
| 176 | * } |
| 177 | * |
| 178 | * public void onProvideContentCaptureStructure(@NonNull ViewStructure structure, int flags) { |
| 179 | * super.onProvideContentCaptureStructure(structure, flags); |
| 180 | * |
| 181 | * structure.setText(getText(), getSelectionStart(), getSelectionEnd()); |
| 182 | * structure.setHint(getHint()); |
| 183 | * structure.setInputType(getInputType()); |
| 184 | * // set other properties like setTextIdEntry(), setTextLines(), setTextStyle(), |
| 185 | * // setMinTextEms(), setMaxTextEms(), setMaxTextLength() |
| 186 | * } |
| 187 | * |
| 188 | * private void onTextChanged() { |
| 189 | * if (isLaidOut() && isImportantForContentCapture() && isTextEditable()) { |
| 190 | * ContentCaptureManager mgr = mContext.getSystemService(ContentCaptureManager.class); |
| 191 | * if (cm != null && cm.isContentCaptureEnabled()) { |
| 192 | * ContentCaptureSession session = getContentCaptureSession(); |
| 193 | * if (session != null) { |
| 194 | * session.notifyViewTextChanged(getAutofillId(), getText()); |
| 195 | * } |
| 196 | * } |
| 197 | * } |
| 198 | * </code></pre> |
| 199 | * |
| 200 | * <p>If your view provides its own virtual hierarchy (for example, if it's a browser that draws |
| 201 | * the HTML using {@link Canvas} or native libraries in a different render process), then the view |
| 202 | * is also responsible to notify the session when the virtual elements appear and disappear - see |
Felipe Leme | d721762 | 2019-05-13 10:11:36 -0700 | [diff] [blame] | 203 | * {@link View#onProvideContentCaptureStructure(ViewStructure, int)} for more info. |
Felipe Leme | ecb08be | 2018-11-27 15:48:47 -0800 | [diff] [blame] | 204 | */ |
| 205 | @SystemService(Context.CONTENT_CAPTURE_MANAGER_SERVICE) |
| 206 | public final class ContentCaptureManager { |
Felipe Leme | 1dfa9a0 | 2018-10-17 17:24:37 -0700 | [diff] [blame] | 207 | |
Felipe Leme | 749b889 | 2018-12-03 16:30:30 -0800 | [diff] [blame] | 208 | private static final String TAG = ContentCaptureManager.class.getSimpleName(); |
Felipe Leme | e348dc3 | 2018-11-05 12:35:29 -0800 | [diff] [blame] | 209 | |
Sergey Volnov | 4392944 | 2020-01-28 15:36:59 +0000 | [diff] [blame] | 210 | /** Error happened during the data sharing session. */ |
| 211 | public static final int DATA_SHARE_ERROR_UNKNOWN = 1; |
| 212 | |
| 213 | /** Request has been rejected, because a concurrent data share sessions is in progress. */ |
| 214 | public static final int DATA_SHARE_ERROR_CONCURRENT_REQUEST = 2; |
| 215 | |
| 216 | /** Request has been interrupted because of data share session timeout. */ |
| 217 | public static final int DATA_SHARE_ERROR_TIMEOUT_INTERRUPTED = 3; |
| 218 | |
Felipe Leme | bb0c2a2 | 2019-01-25 17:29:29 -0800 | [diff] [blame] | 219 | /** @hide */ |
Felipe Leme | f8b8778 | 2019-03-20 14:31:18 -0700 | [diff] [blame] | 220 | public static final int RESULT_CODE_OK = 0; |
| 221 | /** @hide */ |
Felipe Leme | bb0c2a2 | 2019-01-25 17:29:29 -0800 | [diff] [blame] | 222 | public static final int RESULT_CODE_TRUE = 1; |
| 223 | /** @hide */ |
| 224 | public static final int RESULT_CODE_FALSE = 2; |
| 225 | /** @hide */ |
Felipe Leme | f8b8778 | 2019-03-20 14:31:18 -0700 | [diff] [blame] | 226 | public static final int RESULT_CODE_SECURITY_EXCEPTION = -1; |
Felipe Leme | bb0c2a2 | 2019-01-25 17:29:29 -0800 | [diff] [blame] | 227 | |
Perumaal S | addabba | 2019-01-04 16:43:35 -0800 | [diff] [blame] | 228 | /** |
| 229 | * Timeout for calls to system_server. |
| 230 | */ |
| 231 | private static final int SYNC_CALLS_TIMEOUT_MS = 5000; |
| 232 | |
Felipe Leme | 14ef461 | 2019-02-07 12:24:38 -0800 | [diff] [blame] | 233 | /** |
| 234 | * DeviceConfig property used by {@code com.android.server.SystemServer} on start to decide |
Felipe Leme | f0d44c6 | 2019-03-26 16:36:58 -0700 | [diff] [blame] | 235 | * whether the content capture service should be created or not |
Felipe Leme | 14ef461 | 2019-02-07 12:24:38 -0800 | [diff] [blame] | 236 | * |
| 237 | * <p>By default it should *NOT* be set (or set to {@code "default"}, so the decision is based |
| 238 | * on whether the OEM provides an implementation for the service), but it can be overridden to: |
| 239 | * |
| 240 | * <ul> |
| 241 | * <li>Provide a "kill switch" so OEMs can disable it remotely in case of emergency (when |
| 242 | * it's set to {@code "false"}). |
| 243 | * <li>Enable the CTS tests to be run on AOSP builds (when it's set to {@code "true"}). |
| 244 | * </ul> |
| 245 | * |
| 246 | * @hide |
| 247 | */ |
| 248 | @TestApi |
| 249 | public static final String DEVICE_CONFIG_PROPERTY_SERVICE_EXPLICITLY_ENABLED = |
| 250 | "service_explicitly_enabled"; |
| 251 | |
Felipe Leme | d32d8f6f | 2019-02-15 10:25:33 -0800 | [diff] [blame] | 252 | /** |
| 253 | * Maximum number of events that are buffered before sent to the app. |
| 254 | * |
| 255 | * @hide |
| 256 | */ |
| 257 | @TestApi |
| 258 | public static final String DEVICE_CONFIG_PROPERTY_MAX_BUFFER_SIZE = "max_buffer_size"; |
| 259 | |
| 260 | /** |
| 261 | * Frequency (in ms) of buffer flushes when no events are received. |
| 262 | * |
| 263 | * @hide |
| 264 | */ |
| 265 | @TestApi |
| 266 | public static final String DEVICE_CONFIG_PROPERTY_IDLE_FLUSH_FREQUENCY = "idle_flush_frequency"; |
| 267 | |
| 268 | /** |
| 269 | * Frequency (in ms) of buffer flushes when no events are received and the last one was a |
| 270 | * text change event. |
| 271 | * |
| 272 | * @hide |
| 273 | */ |
| 274 | @TestApi |
| 275 | public static final String DEVICE_CONFIG_PROPERTY_TEXT_CHANGE_FLUSH_FREQUENCY = |
| 276 | "text_change_flush_frequency"; |
| 277 | |
| 278 | /** |
| 279 | * Size of events that are logging on {@code dump}. |
| 280 | * |
| 281 | * <p>Set it to {@code 0} or less to disable history. |
| 282 | * |
| 283 | * @hide |
| 284 | */ |
| 285 | @TestApi |
| 286 | public static final String DEVICE_CONFIG_PROPERTY_LOG_HISTORY_SIZE = "log_history_size"; |
| 287 | |
| 288 | /** |
| 289 | * Sets the logging level for {@code logcat} statements. |
| 290 | * |
| 291 | * <p>Valid values are: {@link #LOGGING_LEVEL_OFF}, {@value #LOGGING_LEVEL_DEBUG}, and |
| 292 | * {@link #LOGGING_LEVEL_VERBOSE}. |
| 293 | * |
| 294 | * @hide |
| 295 | */ |
| 296 | @TestApi |
| 297 | public static final String DEVICE_CONFIG_PROPERTY_LOGGING_LEVEL = "logging_level"; |
| 298 | |
Felipe Leme | e764fa2 | 2019-02-21 16:45:35 -0800 | [diff] [blame] | 299 | /** |
| 300 | * Sets how long (in ms) the service is bound while idle. |
| 301 | * |
| 302 | * <p>Use {@code 0} to keep it permanently bound. |
| 303 | * |
| 304 | * @hide |
| 305 | */ |
| 306 | public static final String DEVICE_CONFIG_PROPERTY_IDLE_UNBIND_TIMEOUT = "idle_unbind_timeout"; |
Felipe Leme | d32d8f6f | 2019-02-15 10:25:33 -0800 | [diff] [blame] | 307 | |
| 308 | /** @hide */ |
| 309 | @TestApi |
| 310 | public static final int LOGGING_LEVEL_OFF = 0; |
| 311 | |
| 312 | /** @hide */ |
| 313 | @TestApi |
| 314 | public static final int LOGGING_LEVEL_DEBUG = 1; |
| 315 | |
| 316 | /** @hide */ |
| 317 | @TestApi |
| 318 | public static final int LOGGING_LEVEL_VERBOSE = 2; |
| 319 | |
| 320 | /** @hide */ |
| 321 | @IntDef(flag = false, value = { |
| 322 | LOGGING_LEVEL_OFF, |
| 323 | LOGGING_LEVEL_DEBUG, |
| 324 | LOGGING_LEVEL_VERBOSE |
| 325 | }) |
| 326 | @Retention(RetentionPolicy.SOURCE) |
| 327 | public @interface LoggingLevel {} |
| 328 | |
Felipe Leme | 326f15a | 2019-02-19 09:42:24 -0800 | [diff] [blame] | 329 | |
| 330 | /** @hide */ |
| 331 | public static final int DEFAULT_MAX_BUFFER_SIZE = 100; |
| 332 | /** @hide */ |
| 333 | public static final int DEFAULT_IDLE_FLUSHING_FREQUENCY_MS = 5_000; |
| 334 | /** @hide */ |
| 335 | public static final int DEFAULT_TEXT_CHANGE_FLUSHING_FREQUENCY_MS = 1_000; |
| 336 | /** @hide */ |
| 337 | public static final int DEFAULT_LOG_HISTORY_SIZE = 10; |
| 338 | |
Adam He | 6079d15 | 2019-01-10 11:37:17 -0800 | [diff] [blame] | 339 | private final Object mLock = new Object(); |
| 340 | |
Felipe Leme | b18e317 | 2018-11-27 10:33:41 -0800 | [diff] [blame] | 341 | @NonNull |
Felipe Leme | e348dc3 | 2018-11-05 12:35:29 -0800 | [diff] [blame] | 342 | private final Context mContext; |
| 343 | |
Felipe Leme | d49d52c | 2019-02-15 09:48:20 -0800 | [diff] [blame] | 344 | @NonNull |
Felipe Leme | 749b889 | 2018-12-03 16:30:30 -0800 | [diff] [blame] | 345 | private final IContentCaptureManager mService; |
Felipe Leme | e348dc3 | 2018-11-05 12:35:29 -0800 | [diff] [blame] | 346 | |
Felipe Leme | 326f15a | 2019-02-19 09:42:24 -0800 | [diff] [blame] | 347 | @NonNull |
| 348 | final ContentCaptureOptions mOptions; |
| 349 | |
Adam He | 6079d15 | 2019-01-10 11:37:17 -0800 | [diff] [blame] | 350 | // Flags used for starting session. |
| 351 | @GuardedBy("mLock") |
| 352 | private int mFlags; |
| 353 | |
Felipe Leme | aa5088e | 2018-12-10 14:53:58 -0800 | [diff] [blame] | 354 | // TODO(b/119220549): use UI Thread directly (as calls are one-way) or a shared thread / handler |
Felipe Leme | b18e317 | 2018-11-27 10:33:41 -0800 | [diff] [blame] | 355 | // held at the Application level |
Felipe Leme | aa5088e | 2018-12-10 14:53:58 -0800 | [diff] [blame] | 356 | @NonNull |
Felipe Leme | 88eae3b | 2018-11-07 15:11:56 -0800 | [diff] [blame] | 357 | private final Handler mHandler; |
| 358 | |
Adam He | 6079d15 | 2019-01-10 11:37:17 -0800 | [diff] [blame] | 359 | @GuardedBy("mLock") |
Felipe Leme | 87a9dc9 | 2018-12-18 14:28:07 -0800 | [diff] [blame] | 360 | private MainContentCaptureSession mMainSession; |
Felipe Leme | 4017b20 | 2018-12-10 12:13:31 -0800 | [diff] [blame] | 361 | |
Felipe Leme | e348dc3 | 2018-11-05 12:35:29 -0800 | [diff] [blame] | 362 | /** @hide */ |
Felipe Leme | 41e9eb0 | 2019-04-17 13:57:59 -0700 | [diff] [blame] | 363 | public interface ContentCaptureClient { |
| 364 | /** |
| 365 | * Gets the component name of the client. |
| 366 | */ |
| 367 | @NonNull |
| 368 | ComponentName contentCaptureClientGetComponentName(); |
| 369 | } |
| 370 | |
| 371 | /** @hide */ |
Felipe Leme | 749b889 | 2018-12-03 16:30:30 -0800 | [diff] [blame] | 372 | public ContentCaptureManager(@NonNull Context context, |
Felipe Leme | 326f15a | 2019-02-19 09:42:24 -0800 | [diff] [blame] | 373 | @NonNull IContentCaptureManager service, @NonNull ContentCaptureOptions options) { |
Felipe Leme | d49d52c | 2019-02-15 09:48:20 -0800 | [diff] [blame] | 374 | mContext = Preconditions.checkNotNull(context, "context cannot be null"); |
| 375 | mService = Preconditions.checkNotNull(service, "service cannot be null"); |
Felipe Leme | 326f15a | 2019-02-19 09:42:24 -0800 | [diff] [blame] | 376 | mOptions = Preconditions.checkNotNull(options, "options cannot be null"); |
Felipe Leme | e348dc3 | 2018-11-05 12:35:29 -0800 | [diff] [blame] | 377 | |
Felipe Leme | 326f15a | 2019-02-19 09:42:24 -0800 | [diff] [blame] | 378 | ContentCaptureHelper.setLoggingLevel(mOptions.loggingLevel); |
Felipe Leme | d32d8f6f | 2019-02-15 10:25:33 -0800 | [diff] [blame] | 379 | |
| 380 | if (sVerbose) Log.v(TAG, "Constructor for " + context.getPackageName()); |
| 381 | |
Felipe Leme | 34ccedf | 2019-01-17 13:42:35 -0800 | [diff] [blame] | 382 | // TODO(b/119220549): we might not even need a handler, as the IPCs are oneway. But if we |
| 383 | // do, then we should optimize it to run the tests after the Choreographer finishes the most |
| 384 | // important steps of the frame. |
| 385 | mHandler = Handler.createAsync(Looper.getMainLooper()); |
Felipe Leme | 88eae3b | 2018-11-07 15:11:56 -0800 | [diff] [blame] | 386 | } |
| 387 | |
Felipe Leme | 7a53408 | 2018-11-05 15:03:04 -0800 | [diff] [blame] | 388 | /** |
Felipe Leme | aa5088e | 2018-12-10 14:53:58 -0800 | [diff] [blame] | 389 | * Gets the main session associated with the context. |
Felipe Leme | 88eae3b | 2018-11-07 15:11:56 -0800 | [diff] [blame] | 390 | * |
Felipe Leme | aa5088e | 2018-12-10 14:53:58 -0800 | [diff] [blame] | 391 | * <p>By default there's just one (associated with the activity lifecycle), but apps could |
Felipe Leme | 87a9dc9 | 2018-12-18 14:28:07 -0800 | [diff] [blame] | 392 | * explicitly add more using |
| 393 | * {@link ContentCaptureSession#createContentCaptureSession(ContentCaptureContext)}. |
Felipe Leme | 88eae3b | 2018-11-07 15:11:56 -0800 | [diff] [blame] | 394 | * |
| 395 | * @hide |
| 396 | */ |
| 397 | @NonNull |
Felipe Leme | 87a9dc9 | 2018-12-18 14:28:07 -0800 | [diff] [blame] | 398 | @UiThread |
| 399 | public MainContentCaptureSession getMainContentCaptureSession() { |
Adam He | 6079d15 | 2019-01-10 11:37:17 -0800 | [diff] [blame] | 400 | synchronized (mLock) { |
| 401 | if (mMainSession == null) { |
Felipe Leme | 609991d | 2019-01-30 16:27:24 -0800 | [diff] [blame] | 402 | mMainSession = new MainContentCaptureSession(mContext, this, mHandler, mService); |
Felipe Leme | d32d8f6f | 2019-02-15 10:25:33 -0800 | [diff] [blame] | 403 | if (sVerbose) Log.v(TAG, "getMainContentCaptureSession(): created " + mMainSession); |
Felipe Leme | aa5088e | 2018-12-10 14:53:58 -0800 | [diff] [blame] | 404 | } |
Adam He | 6079d15 | 2019-01-10 11:37:17 -0800 | [diff] [blame] | 405 | return mMainSession; |
Felipe Leme | aa5088e | 2018-12-10 14:53:58 -0800 | [diff] [blame] | 406 | } |
Felipe Leme | aa5088e | 2018-12-10 14:53:58 -0800 | [diff] [blame] | 407 | } |
| 408 | |
| 409 | /** @hide */ |
Felipe Leme | 3fe6e92 | 2019-02-04 17:52:27 -0800 | [diff] [blame] | 410 | @UiThread |
Felipe Leme | b0da18f | 2019-02-22 15:10:02 -0800 | [diff] [blame] | 411 | public void onActivityCreated(@NonNull IBinder applicationToken, |
Adam He | 43c0699 | 2019-04-15 15:41:49 -0700 | [diff] [blame] | 412 | @NonNull ComponentName activityComponent) { |
Felipe Leme | 5001b3b | 2019-03-20 18:25:28 -0700 | [diff] [blame] | 413 | if (mOptions.lite) return; |
Adam He | 6079d15 | 2019-01-10 11:37:17 -0800 | [diff] [blame] | 414 | synchronized (mLock) { |
Adam He | 6079d15 | 2019-01-10 11:37:17 -0800 | [diff] [blame] | 415 | getMainContentCaptureSession().start(applicationToken, activityComponent, mFlags); |
| 416 | } |
Felipe Leme | aa5088e | 2018-12-10 14:53:58 -0800 | [diff] [blame] | 417 | } |
| 418 | |
| 419 | /** @hide */ |
Felipe Leme | 3fe6e92 | 2019-02-04 17:52:27 -0800 | [diff] [blame] | 420 | @UiThread |
Felipe Leme | b0da18f | 2019-02-22 15:10:02 -0800 | [diff] [blame] | 421 | public void onActivityResumed() { |
Felipe Leme | 5001b3b | 2019-03-20 18:25:28 -0700 | [diff] [blame] | 422 | if (mOptions.lite) return; |
Adam He | 42ec1d86 | 2019-09-26 12:24:09 -0700 | [diff] [blame] | 423 | getMainContentCaptureSession().notifySessionResumed(); |
Felipe Leme | b0da18f | 2019-02-22 15:10:02 -0800 | [diff] [blame] | 424 | } |
| 425 | |
| 426 | /** @hide */ |
| 427 | @UiThread |
| 428 | public void onActivityPaused() { |
Felipe Leme | 5001b3b | 2019-03-20 18:25:28 -0700 | [diff] [blame] | 429 | if (mOptions.lite) return; |
Adam He | 42ec1d86 | 2019-09-26 12:24:09 -0700 | [diff] [blame] | 430 | getMainContentCaptureSession().notifySessionPaused(); |
Felipe Leme | b0da18f | 2019-02-22 15:10:02 -0800 | [diff] [blame] | 431 | } |
| 432 | |
| 433 | /** @hide */ |
| 434 | @UiThread |
| 435 | public void onActivityDestroyed() { |
Felipe Leme | 5001b3b | 2019-03-20 18:25:28 -0700 | [diff] [blame] | 436 | if (mOptions.lite) return; |
Felipe Leme | aa5088e | 2018-12-10 14:53:58 -0800 | [diff] [blame] | 437 | getMainContentCaptureSession().destroy(); |
Felipe Leme | 88eae3b | 2018-11-07 15:11:56 -0800 | [diff] [blame] | 438 | } |
| 439 | |
| 440 | /** |
Felipe Leme | aa5088e | 2018-12-10 14:53:58 -0800 | [diff] [blame] | 441 | * Flushes the content of all sessions. |
Felipe Leme | 88eae3b | 2018-11-07 15:11:56 -0800 | [diff] [blame] | 442 | * |
Felipe Leme | aa5088e | 2018-12-10 14:53:58 -0800 | [diff] [blame] | 443 | * <p>Typically called by {@code Activity} when it's paused / resumed. |
Felipe Leme | 88eae3b | 2018-11-07 15:11:56 -0800 | [diff] [blame] | 444 | * |
Felipe Leme | aa5088e | 2018-12-10 14:53:58 -0800 | [diff] [blame] | 445 | * @hide |
Felipe Leme | 88eae3b | 2018-11-07 15:11:56 -0800 | [diff] [blame] | 446 | */ |
Felipe Leme | 3fe6e92 | 2019-02-04 17:52:27 -0800 | [diff] [blame] | 447 | @UiThread |
Felipe Leme | 1af85ea | 2019-01-16 13:23:40 -0800 | [diff] [blame] | 448 | public void flush(@FlushReason int reason) { |
Felipe Leme | 5001b3b | 2019-03-20 18:25:28 -0700 | [diff] [blame] | 449 | if (mOptions.lite) return; |
Felipe Leme | 1af85ea | 2019-01-16 13:23:40 -0800 | [diff] [blame] | 450 | getMainContentCaptureSession().flush(reason); |
Felipe Leme | 88eae3b | 2018-11-07 15:11:56 -0800 | [diff] [blame] | 451 | } |
| 452 | |
Felipe Leme | 1dfa9a0 | 2018-10-17 17:24:37 -0700 | [diff] [blame] | 453 | /** |
Felipe Leme | ecb08be | 2018-11-27 15:48:47 -0800 | [diff] [blame] | 454 | * Returns the component name of the system service that is consuming the captured events for |
| 455 | * the current user. |
Felipe Leme | 1dfa9a0 | 2018-10-17 17:24:37 -0700 | [diff] [blame] | 456 | */ |
| 457 | @Nullable |
Felipe Leme | ecb08be | 2018-11-27 15:48:47 -0800 | [diff] [blame] | 458 | public ComponentName getServiceComponentName() { |
Felipe Leme | 5001b3b | 2019-03-20 18:25:28 -0700 | [diff] [blame] | 459 | if (!isContentCaptureEnabled() && !mOptions.lite) return null; |
Felipe Leme | 91ddeca | 2019-01-24 18:01:58 -0800 | [diff] [blame] | 460 | |
Perumaal S | addabba | 2019-01-04 16:43:35 -0800 | [diff] [blame] | 461 | final SyncResultReceiver resultReceiver = new SyncResultReceiver(SYNC_CALLS_TIMEOUT_MS); |
Perumaal S | addabba | 2019-01-04 16:43:35 -0800 | [diff] [blame] | 462 | try { |
Felipe Leme | effa5f4 | 2019-01-28 14:59:46 -0800 | [diff] [blame] | 463 | mService.getServiceComponentName(resultReceiver); |
Perumaal S | addabba | 2019-01-04 16:43:35 -0800 | [diff] [blame] | 464 | return resultReceiver.getParcelableResult(); |
| 465 | } catch (RemoteException e) { |
Perumaal S | addabba | 2019-01-04 16:43:35 -0800 | [diff] [blame] | 466 | throw e.rethrowFromSystemServer(); |
| 467 | } |
Felipe Leme | 1dfa9a0 | 2018-10-17 17:24:37 -0700 | [diff] [blame] | 468 | } |
| 469 | |
| 470 | /** |
Felipe Leme | f8b8778 | 2019-03-20 14:31:18 -0700 | [diff] [blame] | 471 | * Gets the (optional) intent used to launch the service-specific settings. |
| 472 | * |
| 473 | * <p>This method is static because it's called by Settings, which might not be whitelisted |
| 474 | * for content capture (in which case the ContentCaptureManager on its context would be null). |
| 475 | * |
| 476 | * @hide |
| 477 | */ |
Felipe Leme | 5001b3b | 2019-03-20 18:25:28 -0700 | [diff] [blame] | 478 | // TODO: use "lite" options as it's done by activities from the content capture service |
Felipe Leme | f8b8778 | 2019-03-20 14:31:18 -0700 | [diff] [blame] | 479 | @Nullable |
| 480 | public static ComponentName getServiceSettingsComponentName() { |
| 481 | final IBinder binder = ServiceManager |
| 482 | .checkService(Context.CONTENT_CAPTURE_MANAGER_SERVICE); |
| 483 | if (binder == null) return null; |
| 484 | |
| 485 | final IContentCaptureManager service = IContentCaptureManager.Stub.asInterface(binder); |
| 486 | final SyncResultReceiver resultReceiver = new SyncResultReceiver(SYNC_CALLS_TIMEOUT_MS); |
| 487 | try { |
| 488 | service.getServiceSettingsActivity(resultReceiver); |
| 489 | final int resultCode = resultReceiver.getIntResult(); |
| 490 | if (resultCode == RESULT_CODE_SECURITY_EXCEPTION) { |
| 491 | throw new SecurityException(resultReceiver.getStringResult()); |
| 492 | } |
| 493 | return resultReceiver.getParcelableResult(); |
| 494 | } catch (RemoteException e) { |
| 495 | throw e.rethrowFromSystemServer(); |
| 496 | } |
| 497 | } |
| 498 | |
| 499 | /** |
Felipe Leme | e348dc3 | 2018-11-05 12:35:29 -0800 | [diff] [blame] | 500 | * Checks whether content capture is enabled for this activity. |
Felipe Leme | 91ddeca | 2019-01-24 18:01:58 -0800 | [diff] [blame] | 501 | * |
| 502 | * <p>There are many reasons it could be disabled, such as: |
| 503 | * <ul> |
| 504 | * <li>App itself disabled content capture through {@link #setContentCaptureEnabled(boolean)}. |
Felipe Leme | f0d44c6 | 2019-03-26 16:36:58 -0700 | [diff] [blame] | 505 | * <li>Intelligence service did not whitelist content capture for this activity's package. |
| 506 | * <li>Intelligence service did not whitelist content capture for this specific activity. |
| 507 | * <li>Intelligence service disabled content capture globally. |
| 508 | * <li>User disabled content capture globally through the Android Settings app. |
| 509 | * <li>Device manufacturer (OEM) disabled content capture globally. |
| 510 | * <li>Transient errors, such as intelligence service package being updated. |
Felipe Leme | 91ddeca | 2019-01-24 18:01:58 -0800 | [diff] [blame] | 511 | * </ul> |
Felipe Leme | 1dfa9a0 | 2018-10-17 17:24:37 -0700 | [diff] [blame] | 512 | */ |
| 513 | public boolean isContentCaptureEnabled() { |
Felipe Leme | 5001b3b | 2019-03-20 18:25:28 -0700 | [diff] [blame] | 514 | if (mOptions.lite) return false; |
| 515 | |
Felipe Leme | 609991d | 2019-01-30 16:27:24 -0800 | [diff] [blame] | 516 | final MainContentCaptureSession mainSession; |
Adam He | 6079d15 | 2019-01-10 11:37:17 -0800 | [diff] [blame] | 517 | synchronized (mLock) { |
Felipe Leme | 609991d | 2019-01-30 16:27:24 -0800 | [diff] [blame] | 518 | mainSession = mMainSession; |
Adam He | 6079d15 | 2019-01-10 11:37:17 -0800 | [diff] [blame] | 519 | } |
Felipe Leme | 609991d | 2019-01-30 16:27:24 -0800 | [diff] [blame] | 520 | // The main session is only set when the activity starts, so we need to return true until |
| 521 | // then. |
| 522 | if (mainSession != null && mainSession.isDisabled()) return false; |
| 523 | |
| 524 | return true; |
Felipe Leme | 1dfa9a0 | 2018-10-17 17:24:37 -0700 | [diff] [blame] | 525 | } |
| 526 | |
| 527 | /** |
Felipe Leme | 790be04 | 2019-03-25 09:52:19 -0700 | [diff] [blame] | 528 | * Gets the list of conditions for when content capture should be allowed. |
| 529 | * |
| 530 | * <p>This method is typically used by web browsers so they don't generate unnecessary content |
| 531 | * capture events for websites the content capture service is not interested on. |
| 532 | * |
| 533 | * @return list of conditions, or {@code null} if the service didn't set any restriction |
Felipe Leme | f0d44c6 | 2019-03-26 16:36:58 -0700 | [diff] [blame] | 534 | * (in which case content capture events should always be generated). If the list is empty, |
| 535 | * then it should not generate any event at all. |
Felipe Leme | 790be04 | 2019-03-25 09:52:19 -0700 | [diff] [blame] | 536 | */ |
| 537 | @Nullable |
| 538 | public Set<ContentCaptureCondition> getContentCaptureConditions() { |
Felipe Leme | a8d33c2 | 2019-03-25 16:36:09 -0700 | [diff] [blame] | 539 | // NOTE: we could cache the conditions on ContentCaptureOptions, but then it would be stick |
| 540 | // to the lifetime of the app. OTOH, by dynamically calling the server every time, we allow |
| 541 | // the service to fine tune how long-lived apps (like browsers) are whitelisted. |
| 542 | if (!isContentCaptureEnabled() && !mOptions.lite) return null; |
| 543 | |
Felipe Leme | afbba9f | 2019-03-26 14:02:25 -0700 | [diff] [blame] | 544 | final SyncResultReceiver resultReceiver = syncRun( |
| 545 | (r) -> mService.getContentCaptureConditions(mContext.getPackageName(), r)); |
| 546 | |
| 547 | final ArrayList<ContentCaptureCondition> result = resultReceiver |
| 548 | .getParcelableListResult(); |
| 549 | return toSet(result); |
Felipe Leme | 790be04 | 2019-03-25 09:52:19 -0700 | [diff] [blame] | 550 | } |
| 551 | |
| 552 | /** |
Felipe Leme | 284ad1c | 2018-11-15 18:16:12 -0800 | [diff] [blame] | 553 | * Called by apps to explicitly enable or disable content capture. |
Felipe Leme | 1dfa9a0 | 2018-10-17 17:24:37 -0700 | [diff] [blame] | 554 | * |
| 555 | * <p><b>Note: </b> this call is not persisted accross reboots, so apps should typically call |
| 556 | * it on {@link android.app.Activity#onCreate(android.os.Bundle, android.os.PersistableBundle)}. |
| 557 | */ |
Felipe Leme | 6b3a55c | 2018-11-13 17:14:03 -0800 | [diff] [blame] | 558 | public void setContentCaptureEnabled(boolean enabled) { |
Felipe Leme | d32d8f6f | 2019-02-15 10:25:33 -0800 | [diff] [blame] | 559 | if (sDebug) { |
Felipe Leme | bb0c2a2 | 2019-01-25 17:29:29 -0800 | [diff] [blame] | 560 | Log.d(TAG, "setContentCaptureEnabled(): setting to " + enabled + " for " + mContext); |
| 561 | } |
| 562 | |
Adam He | 6c0afca | 2019-04-15 13:54:11 -0700 | [diff] [blame] | 563 | MainContentCaptureSession mainSession; |
Adam He | 6079d15 | 2019-01-10 11:37:17 -0800 | [diff] [blame] | 564 | synchronized (mLock) { |
Adam He | 6c0afca | 2019-04-15 13:54:11 -0700 | [diff] [blame] | 565 | if (enabled) { |
| 566 | mFlags &= ~ContentCaptureContext.FLAG_DISABLED_BY_APP; |
| 567 | } else { |
| 568 | mFlags |= ContentCaptureContext.FLAG_DISABLED_BY_APP; |
| 569 | } |
| 570 | mainSession = mMainSession; |
| 571 | } |
| 572 | if (mainSession != null) { |
| 573 | mainSession.setDisabled(!enabled); |
Adam He | 6079d15 | 2019-01-10 11:37:17 -0800 | [diff] [blame] | 574 | } |
Felipe Leme | aa5088e | 2018-12-10 14:53:58 -0800 | [diff] [blame] | 575 | } |
| 576 | |
| 577 | /** |
Adam He | 43c0699 | 2019-04-15 15:41:49 -0700 | [diff] [blame] | 578 | * Called by apps to update flag secure when window attributes change. |
| 579 | * |
| 580 | * @hide |
| 581 | */ |
| 582 | public void updateWindowAttributes(@NonNull WindowManager.LayoutParams params) { |
| 583 | if (sDebug) { |
| 584 | Log.d(TAG, "updateWindowAttributes(): window flags=" + params.flags); |
| 585 | } |
| 586 | final boolean flagSecureEnabled = |
| 587 | (params.flags & WindowManager.LayoutParams.FLAG_SECURE) != 0; |
| 588 | |
| 589 | MainContentCaptureSession mainSession; |
| 590 | synchronized (mLock) { |
| 591 | if (flagSecureEnabled) { |
| 592 | mFlags |= ContentCaptureContext.FLAG_DISABLED_BY_FLAG_SECURE; |
| 593 | } else { |
| 594 | mFlags &= ~ContentCaptureContext.FLAG_DISABLED_BY_FLAG_SECURE; |
| 595 | } |
| 596 | mainSession = mMainSession; |
| 597 | } |
| 598 | if (mainSession != null) { |
| 599 | mainSession.setDisabled(flagSecureEnabled); |
| 600 | } |
| 601 | } |
| 602 | |
| 603 | /** |
Felipe Leme | f0d44c6 | 2019-03-26 16:36:58 -0700 | [diff] [blame] | 604 | * Gets whether content capture is enabled for the given user. |
Felipe Leme | 91ddeca | 2019-01-24 18:01:58 -0800 | [diff] [blame] | 605 | * |
Felipe Leme | f0d44c6 | 2019-03-26 16:36:58 -0700 | [diff] [blame] | 606 | * <p>This method is typically used by the content capture service settings page, so it can |
Felipe Leme | 91ddeca | 2019-01-24 18:01:58 -0800 | [diff] [blame] | 607 | * provide a toggle to enable / disable it. |
| 608 | * |
Felipe Leme | f0d44c6 | 2019-03-26 16:36:58 -0700 | [diff] [blame] | 609 | * @throws SecurityException if caller is not the app that owns the content capture service |
Felipe Leme | bb0c2a2 | 2019-01-25 17:29:29 -0800 | [diff] [blame] | 610 | * associated with the user. |
| 611 | * |
Felipe Leme | 91ddeca | 2019-01-24 18:01:58 -0800 | [diff] [blame] | 612 | * @hide |
| 613 | */ |
| 614 | @SystemApi |
Felipe Leme | 19652c0 | 2019-02-04 13:01:29 -0800 | [diff] [blame] | 615 | @TestApi |
Felipe Leme | 91ddeca | 2019-01-24 18:01:58 -0800 | [diff] [blame] | 616 | public boolean isContentCaptureFeatureEnabled() { |
Felipe Leme | afbba9f | 2019-03-26 14:02:25 -0700 | [diff] [blame] | 617 | final SyncResultReceiver resultReceiver = syncRun( |
| 618 | (r) -> mService.isContentCaptureFeatureEnabled(r)); |
| 619 | final int resultCode = resultReceiver.getIntResult(); |
Felipe Leme | bb0c2a2 | 2019-01-25 17:29:29 -0800 | [diff] [blame] | 620 | switch (resultCode) { |
| 621 | case RESULT_CODE_TRUE: |
| 622 | return true; |
| 623 | case RESULT_CODE_FALSE: |
| 624 | return false; |
Felipe Leme | bb0c2a2 | 2019-01-25 17:29:29 -0800 | [diff] [blame] | 625 | default: |
Felipe Leme | 72e83d8 | 2019-02-13 14:51:17 -0800 | [diff] [blame] | 626 | Log.wtf(TAG, "received invalid result: " + resultCode); |
| 627 | return false; |
Felipe Leme | bb0c2a2 | 2019-01-25 17:29:29 -0800 | [diff] [blame] | 628 | } |
Felipe Leme | 91ddeca | 2019-01-24 18:01:58 -0800 | [diff] [blame] | 629 | } |
| 630 | |
| 631 | /** |
Felipe Leme | 4439ee6 | 2019-05-08 10:27:49 -0700 | [diff] [blame] | 632 | * Called by the app to request the content capture service to remove content capture data |
| 633 | * associated with some context. |
Felipe Leme | aa5088e | 2018-12-10 14:53:58 -0800 | [diff] [blame] | 634 | * |
| 635 | * @param request object specifying what user data should be removed. |
| 636 | */ |
Felipe Leme | 4439ee6 | 2019-05-08 10:27:49 -0700 | [diff] [blame] | 637 | public void removeData(@NonNull DataRemovalRequest request) { |
Adam He | 3d0409b | 2019-01-15 14:22:04 -0800 | [diff] [blame] | 638 | Preconditions.checkNotNull(request); |
| 639 | |
| 640 | try { |
Felipe Leme | 4439ee6 | 2019-05-08 10:27:49 -0700 | [diff] [blame] | 641 | mService.removeData(request); |
Adam He | 3d0409b | 2019-01-15 14:22:04 -0800 | [diff] [blame] | 642 | } catch (RemoteException e) { |
| 643 | e.rethrowFromSystemServer(); |
| 644 | } |
Felipe Leme | 1dfa9a0 | 2018-10-17 17:24:37 -0700 | [diff] [blame] | 645 | } |
| 646 | |
Felipe Leme | afbba9f | 2019-03-26 14:02:25 -0700 | [diff] [blame] | 647 | /** |
Sergey Volnov | 6e04901 | 2020-01-10 18:10:48 +0000 | [diff] [blame] | 648 | * Called by the app to request data sharing via writing to a file. |
| 649 | * |
| 650 | * <p>The ContentCaptureService app will receive a read-only file descriptor pointing to the |
| 651 | * same file and will be able to read data being shared from it. |
| 652 | * |
| 653 | * <p>Note: using this API doesn't guarantee the app staying alive and is "best-effort". |
| 654 | * Starting a foreground service would minimize the chances of the app getting killed during the |
| 655 | * file sharing session. |
| 656 | * |
| 657 | * @param request object specifying details of the data being shared. |
| 658 | */ |
| 659 | public void shareData(@NonNull DataShareRequest request, |
| 660 | @NonNull @CallbackExecutor Executor executor, |
| 661 | @NonNull DataShareWriteAdapter dataShareWriteAdapter) { |
| 662 | Preconditions.checkNotNull(request); |
| 663 | Preconditions.checkNotNull(dataShareWriteAdapter); |
| 664 | Preconditions.checkNotNull(executor); |
| 665 | |
| 666 | try { |
Sergey Volnov | 4392944 | 2020-01-28 15:36:59 +0000 | [diff] [blame] | 667 | mService.shareData(request, |
| 668 | new DataShareAdapterDelegate(executor, dataShareWriteAdapter)); |
Sergey Volnov | 6e04901 | 2020-01-10 18:10:48 +0000 | [diff] [blame] | 669 | } catch (RemoteException e) { |
| 670 | e.rethrowFromSystemServer(); |
| 671 | } |
| 672 | } |
| 673 | |
| 674 | /** |
Felipe Leme | afbba9f | 2019-03-26 14:02:25 -0700 | [diff] [blame] | 675 | * Runs a sync method in the service, properly handling exceptions. |
| 676 | * |
| 677 | * @throws SecurityException if caller is not allowed to execute the method. |
| 678 | */ |
| 679 | @NonNull |
| 680 | private SyncResultReceiver syncRun(@NonNull MyRunnable r) { |
| 681 | final SyncResultReceiver resultReceiver = new SyncResultReceiver(SYNC_CALLS_TIMEOUT_MS); |
| 682 | try { |
| 683 | r.run(resultReceiver); |
| 684 | final int resultCode = resultReceiver.getIntResult(); |
| 685 | if (resultCode == RESULT_CODE_SECURITY_EXCEPTION) { |
| 686 | throw new SecurityException(resultReceiver.getStringResult()); |
| 687 | } |
| 688 | return resultReceiver; |
| 689 | } catch (RemoteException e) { |
| 690 | throw e.rethrowFromSystemServer(); |
| 691 | } |
| 692 | } |
| 693 | |
Felipe Leme | e348dc3 | 2018-11-05 12:35:29 -0800 | [diff] [blame] | 694 | /** @hide */ |
| 695 | public void dump(String prefix, PrintWriter pw) { |
Felipe Leme | d49d52c | 2019-02-15 09:48:20 -0800 | [diff] [blame] | 696 | pw.print(prefix); pw.println("ContentCaptureManager"); |
| 697 | final String prefix2 = prefix + " "; |
Adam He | 6079d15 | 2019-01-10 11:37:17 -0800 | [diff] [blame] | 698 | synchronized (mLock) { |
Felipe Leme | d49d52c | 2019-02-15 09:48:20 -0800 | [diff] [blame] | 699 | pw.print(prefix2); pw.print("isContentCaptureEnabled(): "); |
Felipe Leme | 609991d | 2019-01-30 16:27:24 -0800 | [diff] [blame] | 700 | pw.println(isContentCaptureEnabled()); |
Felipe Leme | 326f15a | 2019-02-19 09:42:24 -0800 | [diff] [blame] | 701 | pw.print(prefix2); pw.print("Debug: "); pw.print(sDebug); |
Felipe Leme | d32d8f6f | 2019-02-15 10:25:33 -0800 | [diff] [blame] | 702 | pw.print(" Verbose: "); pw.println(sVerbose); |
Felipe Leme | 326f15a | 2019-02-19 09:42:24 -0800 | [diff] [blame] | 703 | pw.print(prefix2); pw.print("Context: "); pw.println(mContext); |
| 704 | pw.print(prefix2); pw.print("User: "); pw.println(mContext.getUserId()); |
| 705 | pw.print(prefix2); pw.print("Service: "); pw.println(mService); |
| 706 | pw.print(prefix2); pw.print("Flags: "); pw.println(mFlags); |
| 707 | pw.print(prefix2); pw.print("Options: "); mOptions.dumpShort(pw); pw.println(); |
Adam He | 6079d15 | 2019-01-10 11:37:17 -0800 | [diff] [blame] | 708 | if (mMainSession != null) { |
Felipe Leme | d49d52c | 2019-02-15 09:48:20 -0800 | [diff] [blame] | 709 | final String prefix3 = prefix2 + " "; |
| 710 | pw.print(prefix2); pw.println("Main session:"); |
| 711 | mMainSession.dump(prefix3, pw); |
Adam He | 6079d15 | 2019-01-10 11:37:17 -0800 | [diff] [blame] | 712 | } else { |
Felipe Leme | d49d52c | 2019-02-15 09:48:20 -0800 | [diff] [blame] | 713 | pw.print(prefix2); pw.println("No sessions"); |
Adam He | 6079d15 | 2019-01-10 11:37:17 -0800 | [diff] [blame] | 714 | } |
Felipe Leme | e348dc3 | 2018-11-05 12:35:29 -0800 | [diff] [blame] | 715 | } |
| 716 | } |
Felipe Leme | afbba9f | 2019-03-26 14:02:25 -0700 | [diff] [blame] | 717 | |
| 718 | private interface MyRunnable { |
| 719 | void run(@NonNull SyncResultReceiver receiver) throws RemoteException; |
| 720 | } |
Sergey Volnov | 6e04901 | 2020-01-10 18:10:48 +0000 | [diff] [blame] | 721 | |
| 722 | private static class DataShareAdapterDelegate extends IDataShareWriteAdapter.Stub { |
| 723 | |
| 724 | private final WeakReference<DataShareWriteAdapter> mAdapterReference; |
| 725 | private final WeakReference<Executor> mExecutorReference; |
| 726 | |
Sergey Volnov | 4392944 | 2020-01-28 15:36:59 +0000 | [diff] [blame] | 727 | private DataShareAdapterDelegate(Executor executor, DataShareWriteAdapter adapter) { |
Sergey Volnov | 6e04901 | 2020-01-10 18:10:48 +0000 | [diff] [blame] | 728 | Preconditions.checkNotNull(executor); |
| 729 | Preconditions.checkNotNull(adapter); |
| 730 | |
| 731 | mExecutorReference = new WeakReference<>(executor); |
| 732 | mAdapterReference = new WeakReference<>(adapter); |
| 733 | } |
| 734 | |
| 735 | @Override |
| 736 | public void write(ParcelFileDescriptor destination) |
| 737 | throws RemoteException { |
Sergey Volnov | 4392944 | 2020-01-28 15:36:59 +0000 | [diff] [blame] | 738 | executeAdapterMethodLocked(adapter -> adapter.onWrite(destination), "onWrite"); |
Sergey Volnov | 6e04901 | 2020-01-10 18:10:48 +0000 | [diff] [blame] | 739 | } |
| 740 | |
| 741 | @Override |
| 742 | public void error(int errorCode) throws RemoteException { |
| 743 | executeAdapterMethodLocked(adapter -> adapter.onError(errorCode), "onError"); |
| 744 | } |
| 745 | |
| 746 | @Override |
| 747 | public void rejected() throws RemoteException { |
| 748 | executeAdapterMethodLocked(DataShareWriteAdapter::onRejected, "onRejected"); |
| 749 | } |
| 750 | |
| 751 | private void executeAdapterMethodLocked(Consumer<DataShareWriteAdapter> adapterFn, |
| 752 | String methodName) { |
| 753 | DataShareWriteAdapter adapter = mAdapterReference.get(); |
| 754 | Executor executor = mExecutorReference.get(); |
| 755 | |
| 756 | if (adapter == null || executor == null) { |
| 757 | Slog.w(TAG, "Can't execute " + methodName + "(), references have been GC'ed"); |
| 758 | return; |
| 759 | } |
| 760 | |
| 761 | final long identity = Binder.clearCallingIdentity(); |
| 762 | try { |
| 763 | executor.execute(() -> adapterFn.accept(adapter)); |
| 764 | } finally { |
| 765 | Binder.restoreCallingIdentity(identity); |
| 766 | } |
| 767 | } |
| 768 | } |
Felipe Leme | 1dfa9a0 | 2018-10-17 17:24:37 -0700 | [diff] [blame] | 769 | } |