diff options
author | Sergey Volnov <volnov@google.com> | 2020-01-24 22:05:27 +0000 |
---|---|---|
committer | Android (Google) Code Review <android-gerrit@google.com> | 2020-01-24 22:05:27 +0000 |
commit | ac90cd8bf9f48c81046d15a28b636b844c078913 (patch) | |
tree | 7ed1ea655842de796a0725e67a0b9a5eec0f0823 | |
parent | 78acb10560bc6d92e096f09384d96d3adf0b9fb8 (diff) | |
parent | 6e049014a75aef84c6c76bfa8fcaf761f95a5d2d (diff) |
Merge "Implement data sharing API for Content Capture."
17 files changed, 942 insertions, 0 deletions
diff --git a/api/current.txt b/api/current.txt index 4ab54b3654b8..26c4493d2eac 100644 --- a/api/current.txt +++ b/api/current.txt @@ -56148,6 +56148,7 @@ package android.view.contentcapture { method public boolean isContentCaptureEnabled(); method public void removeData(@NonNull android.view.contentcapture.DataRemovalRequest); method public void setContentCaptureEnabled(boolean); + method public void shareData(@NonNull android.view.contentcapture.DataShareRequest, @NonNull java.util.concurrent.Executor, @NonNull android.view.contentcapture.DataShareWriteAdapter); } public abstract class ContentCaptureSession implements java.lang.AutoCloseable { @@ -56196,6 +56197,24 @@ package android.view.contentcapture { method @NonNull public android.content.LocusId getLocusId(); } + public final class DataShareRequest implements android.os.Parcelable { + ctor public DataShareRequest(@Nullable android.content.LocusId, @NonNull String); + method public int describeContents(); + method @Nullable public android.content.LocusId getLocusId(); + method @NonNull public String getMimeType(); + method @NonNull public String getPackageName(); + method public void writeToParcel(@NonNull android.os.Parcel, int); + field @NonNull public static final android.os.Parcelable.Creator<android.view.contentcapture.DataShareRequest> CREATOR; + } + + public interface DataShareWriteAdapter { + method public default void onError(int); + method public void onRejected(); + method public void onWrite(@NonNull android.os.ParcelFileDescriptor, @NonNull android.os.CancellationSignal); + field public static final int ERROR_CONCURRENT_REQUEST = 1; // 0x1 + field public static final int ERROR_UNKNOWN = 2; // 0x2 + } + } package android.view.inline { diff --git a/api/system-current.txt b/api/system-current.txt index 5691bf3fddbc..168bccb369d6 100755 --- a/api/system-current.txt +++ b/api/system-current.txt @@ -10140,6 +10140,7 @@ package android.service.contentcapture { method public void onContentCaptureEvent(@NonNull android.view.contentcapture.ContentCaptureSessionId, @NonNull android.view.contentcapture.ContentCaptureEvent); method public void onCreateContentCaptureSession(@NonNull android.view.contentcapture.ContentCaptureContext, @NonNull android.view.contentcapture.ContentCaptureSessionId); method public void onDataRemovalRequest(@NonNull android.view.contentcapture.DataRemovalRequest); + method public void onDataShareRequest(@NonNull android.view.contentcapture.DataShareRequest, @NonNull android.service.contentcapture.DataShareCallback); method public void onDestroyContentCaptureSession(@NonNull android.view.contentcapture.ContentCaptureSessionId); method public void onDisconnected(); method public final void setContentCaptureConditions(@NonNull String, @Nullable java.util.Set<android.view.contentcapture.ContentCaptureCondition>); @@ -10148,6 +10149,16 @@ package android.service.contentcapture { field public static final String SERVICE_META_DATA = "android.content_capture"; } + public interface DataShareCallback { + method public void onAccept(@NonNull java.util.concurrent.Executor, @NonNull android.service.contentcapture.DataShareReadAdapter); + method public void onReject(); + } + + public interface DataShareReadAdapter { + method public void onError(int); + method public void onStart(@NonNull android.os.ParcelFileDescriptor, @NonNull android.os.CancellationSignal); + } + public final class SnapshotData implements android.os.Parcelable { method public int describeContents(); method @Nullable public android.app.assist.AssistContent getAssistContent(); diff --git a/core/java/android/service/contentcapture/ContentCaptureService.java b/core/java/android/service/contentcapture/ContentCaptureService.java index 3d82946876d1..36e2d1f6b251 100644 --- a/core/java/android/service/contentcapture/ContentCaptureService.java +++ b/core/java/android/service/contentcapture/ContentCaptureService.java @@ -34,9 +34,12 @@ import android.content.Intent; import android.content.pm.ParceledListSlice; import android.os.Binder; import android.os.Bundle; +import android.os.CancellationSignal; import android.os.Handler; import android.os.IBinder; +import android.os.ICancellationSignal; import android.os.Looper; +import android.os.ParcelFileDescriptor; import android.os.RemoteException; import android.util.Log; import android.util.Slog; @@ -49,15 +52,20 @@ import android.view.contentcapture.ContentCaptureManager; import android.view.contentcapture.ContentCaptureSession; import android.view.contentcapture.ContentCaptureSessionId; import android.view.contentcapture.DataRemovalRequest; +import android.view.contentcapture.DataShareRequest; import android.view.contentcapture.IContentCaptureDirectManager; import android.view.contentcapture.MainContentCaptureSession; import com.android.internal.os.IResultReceiver; +import com.android.internal.util.Preconditions; import java.io.FileDescriptor; import java.io.PrintWriter; +import java.lang.ref.WeakReference; import java.util.List; import java.util.Set; +import java.util.concurrent.Executor; +import java.util.function.Consumer; /** * A service used to capture the content of the screen to provide contextual data in other areas of @@ -166,6 +174,12 @@ public abstract class ContentCaptureService extends Service { } @Override + public void onDataShared(DataShareRequest request, IDataShareCallback callback) { + mHandler.sendMessage(obtainMessage(ContentCaptureService::handleOnDataShared, + ContentCaptureService.this, request, callback)); + } + + @Override public void onActivityEvent(ActivityEvent event) { mHandler.sendMessage(obtainMessage(ContentCaptureService::handleOnActivityEvent, ContentCaptureService.this, event)); @@ -318,6 +332,21 @@ public abstract class ContentCaptureService extends Service { } /** + * Notifies the service that data has been shared via a readable file. + * + * @param request request object containing information about data being shared + * @param callback callback to be fired with response on whether the request is "needed" and can + * be handled by the Content Capture service. + * + * @hide + */ + @SystemApi + public void onDataShareRequest(@NonNull DataShareRequest request, + @NonNull DataShareCallback callback) { + if (sVerbose) Log.v(TAG, "onDataShareRequest()"); + } + + /** * Notifies the service of {@link SnapshotData snapshot data} associated with a session. * * @param sessionId the session's Id @@ -505,6 +534,37 @@ public abstract class ContentCaptureService extends Service { onDataRemovalRequest(request); } + private void handleOnDataShared(@NonNull DataShareRequest request, + IDataShareCallback callback) { + onDataShareRequest(request, new DataShareCallback() { + + @Override + public void onAccept(@NonNull Executor executor, + @NonNull DataShareReadAdapter adapter) { + Preconditions.checkNotNull(adapter); + Preconditions.checkNotNull(executor); + + DataShareReadAdapterDelegate delegate = + new DataShareReadAdapterDelegate(executor, adapter); + + try { + callback.accept(delegate); + } catch (RemoteException e) { + Slog.e(TAG, "Failed to accept data sharing", e); + } + } + + @Override + public void onReject() { + try { + callback.reject(); + } catch (RemoteException e) { + Slog.e(TAG, "Failed to reject data sharing", e); + } + } + }); + } + private void handleOnActivityEvent(@NonNull ActivityEvent event) { onActivityEvent(event); } @@ -589,4 +649,57 @@ public abstract class ContentCaptureService extends Service { Log.e(TAG, "failed to write flush metrics: " + e); } } + + private static class DataShareReadAdapterDelegate extends IDataShareReadAdapter.Stub { + + private final Object mLock = new Object(); + private final WeakReference<DataShareReadAdapter> mAdapterReference; + private final WeakReference<Executor> mExecutorReference; + + DataShareReadAdapterDelegate(Executor executor, DataShareReadAdapter adapter) { + Preconditions.checkNotNull(executor); + Preconditions.checkNotNull(adapter); + + mExecutorReference = new WeakReference<>(executor); + mAdapterReference = new WeakReference<>(adapter); + } + + @Override + public void start(ParcelFileDescriptor fd, ICancellationSignal remoteCancellationSignal) + throws RemoteException { + synchronized (mLock) { + CancellationSignal cancellationSignal = new CancellationSignal(); + cancellationSignal.setRemote(remoteCancellationSignal); + + executeAdapterMethodLocked( + adapter -> adapter.onStart(fd, cancellationSignal), "onStart"); + } + } + + @Override + public void error(int errorCode) throws RemoteException { + synchronized (mLock) { + executeAdapterMethodLocked( + adapter -> adapter.onError(errorCode), "onError"); + } + } + + private void executeAdapterMethodLocked(Consumer<DataShareReadAdapter> adapterFn, + String methodName) { + DataShareReadAdapter adapter = mAdapterReference.get(); + Executor executor = mExecutorReference.get(); + + if (adapter == null || executor == null) { + Slog.w(TAG, "Can't execute " + methodName + "(), references have been GC'ed"); + return; + } + + final long identity = Binder.clearCallingIdentity(); + try { + executor.execute(() -> adapterFn.accept(adapter)); + } finally { + Binder.restoreCallingIdentity(identity); + } + } + } } diff --git a/core/java/android/service/contentcapture/DataShareCallback.java b/core/java/android/service/contentcapture/DataShareCallback.java new file mode 100644 index 000000000000..e3c7bb3cd24f --- /dev/null +++ b/core/java/android/service/contentcapture/DataShareCallback.java @@ -0,0 +1,47 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.service.contentcapture; + +import android.annotation.CallbackExecutor; +import android.annotation.NonNull; +import android.annotation.SystemApi; + +import java.util.concurrent.Executor; + +/** + * Callback for the Content Capture Service to accept or reject the data share request from a client + * app. + * + * If the request is rejected, client app would receive a signal and the data share session wouldn't + * be started. + * + * @hide + **/ +@SystemApi +public interface DataShareCallback { + + /** Accept the data share. + * + * @param executor executor to be used for running the adapter in. + * @param adapter adapter to be used for the share operation + */ + void onAccept(@NonNull @CallbackExecutor Executor executor, + @NonNull DataShareReadAdapter adapter); + + /** Reject the data share. */ + void onReject(); +} diff --git a/core/java/android/service/contentcapture/DataShareReadAdapter.java b/core/java/android/service/contentcapture/DataShareReadAdapter.java new file mode 100644 index 000000000000..d9350ba5d774 --- /dev/null +++ b/core/java/android/service/contentcapture/DataShareReadAdapter.java @@ -0,0 +1,47 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.service.contentcapture; + +import android.annotation.NonNull; +import android.annotation.SystemApi; +import android.os.CancellationSignal; +import android.os.ParcelFileDescriptor; + +/** + * Adapter class to be used for the Content Capture Service app to propagate the status of the + * session + * + * @hide + **/ +@SystemApi +public interface DataShareReadAdapter { + + /** + * Signals the start of the data sharing session. + * + * @param fd file descriptor to use for reading data, that's being shared + * @param cancellationSignal cancellation signal to use if data is no longer needed and the + * session needs to be terminated. + **/ + void onStart(@NonNull ParcelFileDescriptor fd, @NonNull CancellationSignal cancellationSignal); + + /** + * Signals that the session failed to start or terminated unsuccessfully (e.g. due to a + * timeout). + **/ + void onError(int errorCode); +} diff --git a/core/java/android/service/contentcapture/IContentCaptureService.aidl b/core/java/android/service/contentcapture/IContentCaptureService.aidl index a7578af94004..277d82b5d909 100644 --- a/core/java/android/service/contentcapture/IContentCaptureService.aidl +++ b/core/java/android/service/contentcapture/IContentCaptureService.aidl @@ -20,8 +20,10 @@ import android.content.ComponentName; import android.os.IBinder; import android.service.contentcapture.ActivityEvent; import android.service.contentcapture.SnapshotData; +import android.service.contentcapture.IDataShareCallback; import android.view.contentcapture.ContentCaptureContext; import android.view.contentcapture.DataRemovalRequest; +import android.view.contentcapture.DataShareRequest; import com.android.internal.os.IResultReceiver; @@ -40,5 +42,6 @@ oneway interface IContentCaptureService { void onSessionFinished(int sessionId); void onActivitySnapshot(int sessionId, in SnapshotData snapshotData); void onDataRemovalRequest(in DataRemovalRequest request); + void onDataShared(in DataShareRequest request, in IDataShareCallback callback); void onActivityEvent(in ActivityEvent event); } diff --git a/core/java/android/service/contentcapture/IDataShareCallback.aidl b/core/java/android/service/contentcapture/IDataShareCallback.aidl new file mode 100644 index 000000000000..c1aa1bb7dcb5 --- /dev/null +++ b/core/java/android/service/contentcapture/IDataShareCallback.aidl @@ -0,0 +1,25 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.service.contentcapture; + +import android.service.contentcapture.IDataShareReadAdapter; + +/** @hide */ +oneway interface IDataShareCallback { + void accept(in IDataShareReadAdapter adapter); + void reject(); +} diff --git a/core/java/android/service/contentcapture/IDataShareReadAdapter.aidl b/core/java/android/service/contentcapture/IDataShareReadAdapter.aidl new file mode 100644 index 000000000000..73da5d515edc --- /dev/null +++ b/core/java/android/service/contentcapture/IDataShareReadAdapter.aidl @@ -0,0 +1,25 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.service.contentcapture; + +import android.os.ICancellationSignal; + +/** @hide */ +oneway interface IDataShareReadAdapter { + void start(in ParcelFileDescriptor fd, in ICancellationSignal cancellationSignal); + void error(int errorCode); +} diff --git a/core/java/android/view/contentcapture/ContentCaptureManager.java b/core/java/android/view/contentcapture/ContentCaptureManager.java index 6040abd4f5f6..81c83834098c 100644 --- a/core/java/android/view/contentcapture/ContentCaptureManager.java +++ b/core/java/android/view/contentcapture/ContentCaptureManager.java @@ -19,6 +19,7 @@ import static android.view.contentcapture.ContentCaptureHelper.sDebug; import static android.view.contentcapture.ContentCaptureHelper.sVerbose; import static android.view.contentcapture.ContentCaptureHelper.toSet; +import android.annotation.CallbackExecutor; import android.annotation.IntDef; import android.annotation.NonNull; import android.annotation.Nullable; @@ -30,12 +31,16 @@ import android.content.ComponentName; import android.content.ContentCaptureOptions; import android.content.Context; import android.graphics.Canvas; +import android.os.Binder; +import android.os.CancellationSignal; import android.os.Handler; import android.os.IBinder; import android.os.Looper; +import android.os.ParcelFileDescriptor; import android.os.RemoteException; import android.os.ServiceManager; import android.util.Log; +import android.util.Slog; import android.view.View; import android.view.ViewStructure; import android.view.WindowManager; @@ -48,8 +53,11 @@ import com.android.internal.util.SyncResultReceiver; import java.io.PrintWriter; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; +import java.lang.ref.WeakReference; import java.util.ArrayList; import java.util.Set; +import java.util.concurrent.Executor; +import java.util.function.Consumer; /** * <p>The {@link ContentCaptureManager} provides additional ways for for apps to @@ -629,6 +637,33 @@ public final class ContentCaptureManager { } /** + * Called by the app to request data sharing via writing to a file. + * + * <p>The ContentCaptureService app will receive a read-only file descriptor pointing to the + * same file and will be able to read data being shared from it. + * + * <p>Note: using this API doesn't guarantee the app staying alive and is "best-effort". + * Starting a foreground service would minimize the chances of the app getting killed during the + * file sharing session. + * + * @param request object specifying details of the data being shared. + */ + public void shareData(@NonNull DataShareRequest request, + @NonNull @CallbackExecutor Executor executor, + @NonNull DataShareWriteAdapter dataShareWriteAdapter) { + Preconditions.checkNotNull(request); + Preconditions.checkNotNull(dataShareWriteAdapter); + Preconditions.checkNotNull(executor); + + try { + mService.shareData(request, + new DataShareAdapterDelegate(executor, dataShareWriteAdapter)); + } catch (RemoteException e) { + e.rethrowFromSystemServer(); + } + } + + /** * Runs a sync method in the service, properly handling exceptions. * * @throws SecurityException if caller is not allowed to execute the method. @@ -675,4 +710,55 @@ public final class ContentCaptureManager { private interface MyRunnable { void run(@NonNull SyncResultReceiver receiver) throws RemoteException; } + + private static class DataShareAdapterDelegate extends IDataShareWriteAdapter.Stub { + + private final WeakReference<DataShareWriteAdapter> mAdapterReference; + private final WeakReference<Executor> mExecutorReference; + + private DataShareAdapterDelegate(Executor executor, DataShareWriteAdapter adapter) { + Preconditions.checkNotNull(executor); + Preconditions.checkNotNull(adapter); + + mExecutorReference = new WeakReference<>(executor); + mAdapterReference = new WeakReference<>(adapter); + } + + @Override + public void write(ParcelFileDescriptor destination) + throws RemoteException { + // TODO(b/148264965): implement this. + CancellationSignal cancellationSignal = new CancellationSignal(); + executeAdapterMethodLocked(adapter -> adapter.onWrite(destination, cancellationSignal), + "onWrite"); + } + + @Override + public void error(int errorCode) throws RemoteException { + executeAdapterMethodLocked(adapter -> adapter.onError(errorCode), "onError"); + } + + @Override + public void rejected() throws RemoteException { + executeAdapterMethodLocked(DataShareWriteAdapter::onRejected, "onRejected"); + } + + private void executeAdapterMethodLocked(Consumer<DataShareWriteAdapter> adapterFn, + String methodName) { + DataShareWriteAdapter adapter = mAdapterReference.get(); + Executor executor = mExecutorReference.get(); + + if (adapter == null || executor == null) { + Slog.w(TAG, "Can't execute " + methodName + "(), references have been GC'ed"); + return; + } + + final long identity = Binder.clearCallingIdentity(); + try { + executor.execute(() -> adapterFn.accept(adapter)); + } finally { + Binder.restoreCallingIdentity(identity); + } + } + } } diff --git a/core/java/android/view/contentcapture/DataShareRequest.aidl b/core/java/android/view/contentcapture/DataShareRequest.aidl new file mode 100644 index 000000000000..75073e411ec8 --- /dev/null +++ b/core/java/android/view/contentcapture/DataShareRequest.aidl @@ -0,0 +1,18 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package android.view.contentcapture; + +parcelable DataShareRequest; diff --git a/core/java/android/view/contentcapture/DataShareRequest.java b/core/java/android/view/contentcapture/DataShareRequest.java new file mode 100644 index 000000000000..78c0ef9568ba --- /dev/null +++ b/core/java/android/view/contentcapture/DataShareRequest.java @@ -0,0 +1,207 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.view.contentcapture; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.app.ActivityThread; +import android.content.LocusId; +import android.os.Parcel; +import android.os.Parcelable; + +import com.android.internal.util.DataClass; +import com.android.internal.util.Preconditions; + +/** Container class representing a request to share data with Content Capture service. */ +@DataClass( + genConstructor = false, + genEqualsHashCode = true, + genHiddenConstDefs = true, + genParcelable = true, + genToString = true +) +public final class DataShareRequest implements Parcelable { + + /** Name of the package making the request. */ + @NonNull private final String mPackageName; + + /** Locus id helping to identify what data is being shared. */ + @Nullable private final LocusId mLocusId; + + /** MIME type of the data being shared. */ + @NonNull private final String mMimeType; + + /** Constructs a request to share data with the Content Capture Service. */ + public DataShareRequest(@Nullable LocusId locusId, @NonNull String mimeType) { + Preconditions.checkNotNull(mimeType); + + mPackageName = ActivityThread.currentActivityThread().getApplication().getPackageName(); + mLocusId = locusId; + mMimeType = mimeType; + } + + + + // Code below generated by codegen v1.0.14. + // + // DO NOT MODIFY! + // CHECKSTYLE:OFF Generated code + // + // To regenerate run: + // $ codegen $ANDROID_BUILD_TOP/frameworks/base/core/java/android/view/contentcapture/DataShareRequest.java + // + // To exclude the generated code from IntelliJ auto-formatting enable (one-time): + // Settings > Editor > Code Style > Formatter Control + //@formatter:off + + + /** + * Name of the package making the request. + */ + @DataClass.Generated.Member + public @NonNull String getPackageName() { + return mPackageName; + } + + /** + * Locus id helping to identify what data is being shared. + */ + @DataClass.Generated.Member + public @Nullable LocusId getLocusId() { + return mLocusId; + } + + /** + * MIME type of the data being shared. + */ + @DataClass.Generated.Member + public @NonNull String getMimeType() { + return mMimeType; + } + + @Override + @DataClass.Generated.Member + public String toString() { + // You can override field toString logic by defining methods like: + // String fieldNameToString() { ... } + + return "DataShareRequest { " + + "packageName = " + mPackageName + ", " + + "locusId = " + mLocusId + ", " + + "mimeType = " + mMimeType + + " }"; + } + + @Override + @DataClass.Generated.Member + public boolean equals(@Nullable Object o) { + // You can override field equality logic by defining either of the methods like: + // boolean fieldNameEquals(DataShareRequest other) { ... } + // boolean fieldNameEquals(FieldType otherValue) { ... } + + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + @SuppressWarnings("unchecked") + DataShareRequest that = (DataShareRequest) o; + //noinspection PointlessBooleanExpression + return true + && java.util.Objects.equals(mPackageName, that.mPackageName) + && java.util.Objects.equals(mLocusId, that.mLocusId) + && java.util.Objects.equals(mMimeType, that.mMimeType); + } + + @Override + @DataClass.Generated.Member + public int hashCode() { + // You can override field hashCode logic by defining methods like: + // int fieldNameHashCode() { ... } + + int _hash = 1; + _hash = 31 * _hash + java.util.Objects.hashCode(mPackageName); + _hash = 31 * _hash + java.util.Objects.hashCode(mLocusId); + _hash = 31 * _hash + java.util.Objects.hashCode(mMimeType); + return _hash; + } + + @Override + @DataClass.Generated.Member + public void writeToParcel(@NonNull Parcel dest, int flags) { + // You can override field parcelling by defining methods like: + // void parcelFieldName(Parcel dest, int flags) { ... } + + byte flg = 0; + if (mLocusId != null) flg |= 0x2; + dest.writeByte(flg); + dest.writeString(mPackageName); + if (mLocusId != null) dest.writeTypedObject(mLocusId, flags); + dest.writeString(mMimeType); + } + + @Override + @DataClass.Generated.Member + public int describeContents() { return 0; } + + /** @hide */ + @SuppressWarnings({"unchecked", "RedundantCast"}) + @DataClass.Generated.Member + /* package-private */ DataShareRequest(@NonNull Parcel in) { + // You can override field unparcelling by defining methods like: + // static FieldType unparcelFieldName(Parcel in) { ... } + + byte flg = in.readByte(); + String packageName = in.readString(); + LocusId locusId = (flg & 0x2) == 0 ? null : (LocusId) in.readTypedObject(LocusId.CREATOR); + String mimeType = in.readString(); + + this.mPackageName = packageName; + com.android.internal.util.AnnotationValidations.validate( + NonNull.class, null, mPackageName); + this.mLocusId = locusId; + this.mMimeType = mimeType; + com.android.internal.util.AnnotationValidations.validate( + NonNull.class, null, mMimeType); + + // onConstructed(); // You can define this method to get a callback + } + + @DataClass.Generated.Member + public static final @NonNull Parcelable.Creator<DataShareRequest> CREATOR + = new Parcelable.Creator<DataShareRequest>() { + @Override + public DataShareRequest[] newArray(int size) { + return new DataShareRequest[size]; + } + + @Override + public DataShareRequest createFromParcel(@NonNull Parcel in) { + return new DataShareRequest(in); + } + }; + + @DataClass.Generated( + time = 1579870254459L, + codegenVersion = "1.0.14", + sourceFile = "frameworks/base/core/java/android/view/contentcapture/DataShareRequest.java", + inputSignatures = "private final @android.annotation.NonNull java.lang.String mPackageName\nprivate final @android.annotation.Nullable android.content.LocusId mLocusId\nprivate final @android.annotation.NonNull java.lang.String mMimeType\nclass DataShareRequest extends java.lang.Object implements [android.os.Parcelable]\n@com.android.internal.util.DataClass(genConstructor=false, genEqualsHashCode=true, genHiddenConstDefs=true, genParcelable=true, genToString=true)") + @Deprecated + private void __metadata() {} + + + //@formatter:on + // End of generated code + +} diff --git a/core/java/android/view/contentcapture/DataShareWriteAdapter.java b/core/java/android/view/contentcapture/DataShareWriteAdapter.java new file mode 100644 index 000000000000..f791fea7ee8d --- /dev/null +++ b/core/java/android/view/contentcapture/DataShareWriteAdapter.java @@ -0,0 +1,58 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.view.contentcapture; + +import android.annotation.NonNull; +import android.os.CancellationSignal; +import android.os.ParcelFileDescriptor; + +/** Adapter class used by apps to share data with the Content Capture service. */ +public interface DataShareWriteAdapter { + + /** Request has been rejected, because a concurrent data share sessions is in progress. */ + int ERROR_CONCURRENT_REQUEST = 1; + + /** Data share session timed out. */ + int ERROR_UNKNOWN = 2; + + /** + * Method invoked when the data share session has been started and the app needs to start + * writing into the file used for sharing. + * + * <p>App needs to handle explicitly cases when the file descriptor is closed and handle + * gracefully if IOExceptions happen. + * + * @param destination file descriptor used to write data into + * @param cancellationSignal cancellation signal that the app can use to subscribe to cancel + * operations. + */ + void onWrite(@NonNull ParcelFileDescriptor destination, + @NonNull CancellationSignal cancellationSignal); + + /** Data share sessions has been rejected by the Content Capture service. */ + void onRejected(); + + /** + * Method invoked when an error occurred, for example sessions has not been started or + * terminated unsuccessfully. + * + * @param errorCode the error code corresponding to an ERROR_* value. + */ + default void onError(int errorCode) { + /* do nothing - stub */ + } +} diff --git a/core/java/android/view/contentcapture/IContentCaptureManager.aidl b/core/java/android/view/contentcapture/IContentCaptureManager.aidl index 7850b67b8404..e8d85ac69907 100644 --- a/core/java/android/view/contentcapture/IContentCaptureManager.aidl +++ b/core/java/android/view/contentcapture/IContentCaptureManager.aidl @@ -20,6 +20,8 @@ import android.content.ComponentName; import android.view.contentcapture.ContentCaptureContext; import android.view.contentcapture.ContentCaptureEvent; import android.view.contentcapture.DataRemovalRequest; +import android.view.contentcapture.DataShareRequest; +import android.view.contentcapture.IDataShareWriteAdapter; import android.os.IBinder; import com.android.internal.os.IResultReceiver; @@ -64,6 +66,11 @@ oneway interface IContentCaptureManager { void removeData(in DataRemovalRequest request); /** + * Requests sharing of a binary data with the content capture service. + */ + void shareData(in DataShareRequest request, in IDataShareWriteAdapter adapter); + + /** * Returns whether the content capture feature is enabled for the calling user. */ void isContentCaptureFeatureEnabled(in IResultReceiver result); diff --git a/core/java/android/view/contentcapture/IDataShareWriteAdapter.aidl b/core/java/android/view/contentcapture/IDataShareWriteAdapter.aidl new file mode 100644 index 000000000000..80924ef78f85 --- /dev/null +++ b/core/java/android/view/contentcapture/IDataShareWriteAdapter.aidl @@ -0,0 +1,28 @@ +/* + * Copyright (C) 2020 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.view.contentcapture; + +import android.os.ICancellationSignal; + +/** + * @hide + */ +oneway interface IDataShareWriteAdapter { + void write(in ParcelFileDescriptor destination); + void error(int errorCode); + void rejected(); +} diff --git a/services/contentcapture/java/com/android/server/contentcapture/ContentCaptureManagerService.java b/services/contentcapture/java/com/android/server/contentcapture/ContentCaptureManagerService.java index 8eca62a4932b..9245a1da43b2 100644 --- a/services/contentcapture/java/com/android/server/contentcapture/ContentCaptureManagerService.java +++ b/services/contentcapture/java/com/android/server/contentcapture/ContentCaptureManagerService.java @@ -45,7 +45,12 @@ import android.database.ContentObserver; import android.os.Binder; import android.os.Build; import android.os.Bundle; +import android.os.CancellationSignal; +import android.os.Handler; import android.os.IBinder; +import android.os.ICancellationSignal; +import android.os.Looper; +import android.os.ParcelFileDescriptor; import android.os.RemoteException; import android.os.ResultReceiver; import android.os.ShellCallback; @@ -55,8 +60,11 @@ import android.provider.DeviceConfig; import android.provider.DeviceConfig.Properties; import android.provider.Settings; import android.service.contentcapture.ActivityEvent.ActivityEventType; +import android.service.contentcapture.IDataShareCallback; +import android.service.contentcapture.IDataShareReadAdapter; import android.util.ArraySet; import android.util.LocalLog; +import android.util.Pair; import android.util.Slog; import android.util.SparseArray; import android.util.SparseBooleanArray; @@ -64,7 +72,10 @@ import android.view.contentcapture.ContentCaptureCondition; import android.view.contentcapture.ContentCaptureHelper; import android.view.contentcapture.ContentCaptureManager; import android.view.contentcapture.DataRemovalRequest; +import android.view.contentcapture.DataShareRequest; +import android.view.contentcapture.DataShareWriteAdapter; import android.view.contentcapture.IContentCaptureManager; +import android.view.contentcapture.IDataShareWriteAdapter; import com.android.internal.annotations.GuardedBy; import com.android.internal.infra.AbstractRemoteService; @@ -77,9 +88,16 @@ import com.android.server.infra.AbstractMasterSystemService; import com.android.server.infra.FrameworkResourcesServiceNameResolver; import java.io.FileDescriptor; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; import java.io.PrintWriter; import java.util.ArrayList; +import java.util.HashSet; import java.util.List; +import java.util.Set; +import java.util.concurrent.Executor; +import java.util.concurrent.Executors; /** * A service used to observe the contents of the screen. @@ -94,6 +112,9 @@ public final class ContentCaptureManagerService extends static final String RECEIVER_BUNDLE_EXTRA_SESSIONS = "sessions"; private static final int MAX_TEMP_SERVICE_DURATION_MS = 1_000 * 60 * 2; // 2 minutes + private static final int MAX_DATA_SHARE_FILE_DESCRIPTORS_TTL_MS = 1_000 * 60 * 5; // 5 minutes + private static final int MAX_CONCURRENT_FILE_SHARING_REQUESTS = 10; + private static final int DATA_SHARE_BYTE_BUFFER_LENGTH = 1_024; private final LocalService mLocalService = new LocalService(); @@ -126,6 +147,12 @@ public final class ContentCaptureManagerService extends @GuardedBy("mLock") int mDevCfgLogHistorySize; @GuardedBy("mLock") int mDevCfgIdleUnbindTimeoutMs; + private final Executor mDataShareExecutor = Executors.newCachedThreadPool(); + private final Handler mHandler = new Handler(Looper.getMainLooper()); + + @GuardedBy("mLock") + private final Set<String> mPackagesWithShareRequests = new HashSet<>(); + final GlobalContentCaptureOptions mGlobalContentCaptureOptions = new GlobalContentCaptureOptions(); @@ -618,6 +645,33 @@ public final class ContentCaptureManagerService extends } @Override + public void shareData(@NonNull DataShareRequest request, + @NonNull IDataShareWriteAdapter clientAdapter) { + Preconditions.checkNotNull(request); + Preconditions.checkNotNull(clientAdapter); + + assertCalledByPackageOwner(request.getPackageName()); + + final int userId = UserHandle.getCallingUserId(); + synchronized (mLock) { + final ContentCapturePerUserService service = getServiceForUserLocked(userId); + + if (mPackagesWithShareRequests.size() >= MAX_CONCURRENT_FILE_SHARING_REQUESTS + || mPackagesWithShareRequests.contains(request.getPackageName())) { + try { + clientAdapter.error(DataShareWriteAdapter.ERROR_CONCURRENT_REQUEST); + } catch (RemoteException e) { + Slog.e(mTag, "Failed to send error message to client"); + } + return; + } + + service.onDataSharedLocked(request, + new DataShareCallbackDelegate(request, clientAdapter)); + } + } + + @Override public void isContentCaptureFeatureEnabled(@NonNull IResultReceiver result) { boolean enabled; synchronized (mLock) { @@ -860,4 +914,179 @@ public final class ContentCaptureManagerService extends } } } + + // TODO(b/148265162): DataShareCallbackDelegate should be a static class keeping week references + // to the needed info + private class DataShareCallbackDelegate extends IDataShareCallback.Stub { + + @NonNull private final DataShareRequest mDataShareRequest; + @NonNull private final IDataShareWriteAdapter mClientAdapter; + + DataShareCallbackDelegate(@NonNull DataShareRequest dataShareRequest, + @NonNull IDataShareWriteAdapter clientAdapter) { + mDataShareRequest = dataShareRequest; + mClientAdapter = clientAdapter; + } + + @Override + public void accept(IDataShareReadAdapter serviceAdapter) + throws RemoteException { + Slog.i(mTag, "Data share request accepted by Content Capture service"); + + Pair<ParcelFileDescriptor, ParcelFileDescriptor> clientPipe = createPipe(); + if (clientPipe == null) { + mClientAdapter.error(DataShareWriteAdapter.ERROR_UNKNOWN); + serviceAdapter.error(DataShareWriteAdapter.ERROR_UNKNOWN); + return; + } + + ParcelFileDescriptor source_in = clientPipe.second; + ParcelFileDescriptor sink_in = clientPipe.first; + + Pair<ParcelFileDescriptor, ParcelFileDescriptor> servicePipe = createPipe(); + if (servicePipe == null) { + bestEffortCloseFileDescriptors(source_in, sink_in); + + mClientAdapter.error(DataShareWriteAdapter.ERROR_UNKNOWN); + serviceAdapter.error(DataShareWriteAdapter.ERROR_UNKNOWN); + return; + } + + ParcelFileDescriptor source_out = servicePipe.second; + ParcelFileDescriptor sink_out = servicePipe.first; + + ICancellationSignal cancellationSignalTransport = + CancellationSignal.createTransport(); + mPackagesWithShareRequests.add(mDataShareRequest.getPackageName()); + + mClientAdapter.write(source_in); + serviceAdapter.start(sink_out, cancellationSignalTransport); + + // TODO(b/148264965): use cancellation signals for timeouts and cancelling + CancellationSignal cancellationSignal = + CancellationSignal.fromTransport(cancellationSignalTransport); + + cancellationSignal.setOnCancelListener(() -> { + try { + // TODO(b/148264965): this should propagate with the cancellation signal to the + // client + mClientAdapter.error(DataShareWriteAdapter.ERROR_UNKNOWN); + } catch (RemoteException e) { + Slog.e(mTag, "Failed to propagate cancel operation to the caller", e); + } + }); + + // File descriptor received by the client app will be a copy of the current one. Close + // the one that belongs to the system server, so there's only 1 open left for the + // current pipe. + bestEffortCloseFileDescriptor(source_in); + + mDataShareExecutor.execute(() -> { + try (InputStream fis = + new ParcelFileDescriptor.AutoCloseInputStream(sink_in); + OutputStream fos = + new ParcelFileDescriptor.AutoCloseOutputStream(source_out)) { + + byte[] byteBuffer = new byte[DATA_SHARE_BYTE_BUFFER_LENGTH]; + while (true) { + int readBytes = fis.read(byteBuffer); + + if (readBytes == -1) { + break; + } + + fos.write(byteBuffer, 0 /* offset */, readBytes); + } + } catch (IOException e) { + Slog.e(mTag, "Failed to pipe client and service streams", e); + } + }); + + mHandler.postDelayed(() -> { + synchronized (mLock) { + mPackagesWithShareRequests.remove(mDataShareRequest.getPackageName()); + + // Interaction finished successfully <=> all data has been written to Content + // Capture Service. If it hasn't been read successfully, service would be able + // to signal through the cancellation signal. + boolean finishedSuccessfully = !sink_in.getFileDescriptor().valid() + && !source_out.getFileDescriptor().valid(); + + if (finishedSuccessfully) { + Slog.i(mTag, "Content capture data sharing session terminated " + + "successfully for package '" + + mDataShareRequest.getPackageName() + + "'"); + } else { + Slog.i(mTag, "Reached the timeout of Content Capture data sharing session " + + "for package '" + + mDataShareRequest.getPackageName() + + "', terminating the pipe."); + } + + // Ensure all the descriptors are closed after the session. + bestEffortCloseFileDescriptors(source_in, sink_in, source_out, sink_out); + + if (!finishedSuccessfully) { + try { + mClientAdapter.error(DataShareWriteAdapter.ERROR_UNKNOWN); + } catch (RemoteException e) { + Slog.e(mTag, "Failed to call error() to client", e); + } + try { + serviceAdapter.error(DataShareWriteAdapter.ERROR_UNKNOWN); + } catch (RemoteException e) { + Slog.e(mTag, "Failed to call error() to service", e); + } + } + } + }, MAX_DATA_SHARE_FILE_DESCRIPTORS_TTL_MS); + } + + @Override + public void reject() throws RemoteException { + Slog.i(mTag, "Data share request rejected by Content Capture service"); + + mClientAdapter.rejected(); + } + + private Pair<ParcelFileDescriptor, ParcelFileDescriptor> createPipe() { + ParcelFileDescriptor[] fileDescriptors; + try { + fileDescriptors = ParcelFileDescriptor.createPipe(); + } catch (IOException e) { + Slog.e(mTag, "Failed to create a content capture data-sharing pipe", e); + return null; + } + + if (fileDescriptors.length != 2) { + Slog.e(mTag, "Failed to create a content capture data-sharing pipe, " + + "unexpected number of file descriptors"); + return null; + } + + if (!fileDescriptors[0].getFileDescriptor().valid() + || !fileDescriptors[1].getFileDescriptor().valid()) { + Slog.e(mTag, "Failed to create a content capture data-sharing pipe, didn't " + + "receive a pair of valid file descriptors."); + return null; + } + + return Pair.create(fileDescriptors[0], fileDescriptors[1]); + } + + private void bestEffortCloseFileDescriptor(ParcelFileDescriptor fd) { + try { + fd.close(); + } catch (IOException e) { + Slog.e(mTag, "Failed to close a file descriptor", e); + } + } + + private void bestEffortCloseFileDescriptors(ParcelFileDescriptor... fds) { + for (ParcelFileDescriptor fd : fds) { + bestEffortCloseFileDescriptor(fd); + } + } + } } diff --git a/services/contentcapture/java/com/android/server/contentcapture/ContentCapturePerUserService.java b/services/contentcapture/java/com/android/server/contentcapture/ContentCapturePerUserService.java index a186d4e7f467..0f1122e3886a 100644 --- a/services/contentcapture/java/com/android/server/contentcapture/ContentCapturePerUserService.java +++ b/services/contentcapture/java/com/android/server/contentcapture/ContentCapturePerUserService.java @@ -54,6 +54,7 @@ import android.service.contentcapture.ContentCaptureService; import android.service.contentcapture.ContentCaptureServiceInfo; import android.service.contentcapture.FlushMetrics; import android.service.contentcapture.IContentCaptureServiceCallback; +import android.service.contentcapture.IDataShareCallback; import android.service.contentcapture.SnapshotData; import android.util.ArrayMap; import android.util.ArraySet; @@ -63,6 +64,7 @@ import android.util.SparseBooleanArray; import android.util.StatsLog; import android.view.contentcapture.ContentCaptureCondition; import android.view.contentcapture.DataRemovalRequest; +import android.view.contentcapture.DataShareRequest; import com.android.internal.annotations.GuardedBy; import com.android.internal.os.IResultReceiver; @@ -375,6 +377,16 @@ final class ContentCapturePerUserService } @GuardedBy("mLock") + public void onDataSharedLocked(@NonNull DataShareRequest request, + IDataShareCallback.Stub dataShareCallback) { + if (!isEnabledLocked()) { + return; + } + assertCallerLocked(request.getPackageName()); + mRemoteService.onDataShareRequest(request, dataShareCallback); + } + + @GuardedBy("mLock") @Nullable public ComponentName getServiceSettingsActivityLocked() { if (mInfo == null) return null; diff --git a/services/contentcapture/java/com/android/server/contentcapture/RemoteContentCaptureService.java b/services/contentcapture/java/com/android/server/contentcapture/RemoteContentCaptureService.java index 01d33b0e5445..c16df0f19943 100644 --- a/services/contentcapture/java/com/android/server/contentcapture/RemoteContentCaptureService.java +++ b/services/contentcapture/java/com/android/server/contentcapture/RemoteContentCaptureService.java @@ -29,11 +29,13 @@ import android.os.IBinder; import android.service.contentcapture.ActivityEvent; import android.service.contentcapture.IContentCaptureService; import android.service.contentcapture.IContentCaptureServiceCallback; +import android.service.contentcapture.IDataShareCallback; import android.service.contentcapture.SnapshotData; import android.util.Slog; import android.util.StatsLog; import android.view.contentcapture.ContentCaptureContext; import android.view.contentcapture.DataRemovalRequest; +import android.view.contentcapture.DataShareRequest; import com.android.internal.infra.AbstractMultiplePendingRequestsRemoteService; import com.android.internal.os.IResultReceiver; @@ -145,6 +147,11 @@ final class RemoteContentCaptureService mComponentName); } + public void onDataShareRequest(@NonNull DataShareRequest request, + @NonNull IDataShareCallback.Stub dataShareCallback) { + scheduleAsyncRequest((s) -> s.onDataShared(request, dataShareCallback)); + } + /** * Called by {@link ContentCaptureServerSession} to notify a high-level activity event. */ |