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