diff options
11 files changed, 325 insertions, 18 deletions
diff --git a/core/java/android/service/contentcapture/ContentCaptureService.java b/core/java/android/service/contentcapture/ContentCaptureService.java index 302e1a656833..020de7f24048 100644 --- a/core/java/android/service/contentcapture/ContentCaptureService.java +++ b/core/java/android/service/contentcapture/ContentCaptureService.java @@ -116,6 +116,13 @@ public abstract class ContentCaptureService extends Service { mHandler.sendMessage(obtainMessage(ContentCaptureService::handleFinishSession, ContentCaptureService.this, sessionId)); } + + @Override + public void onUserDataRemovalRequest(UserDataRemovalRequest request) { + mHandler.sendMessage( + obtainMessage(ContentCaptureService::handleOnUserDataRemovalRequest, + ContentCaptureService.this, request)); + } }; /** @@ -431,6 +438,10 @@ public abstract class ContentCaptureService extends Service { onDestroyContentCaptureSession(new ContentCaptureSessionId(sessionId)); } + private void handleOnUserDataRemovalRequest(@NonNull UserDataRemovalRequest request) { + onUserDataRemovalRequest(request); + } + /** * Checks if the given {@code uid} owns the session associated with the event. */ diff --git a/core/java/android/service/contentcapture/IContentCaptureService.aidl b/core/java/android/service/contentcapture/IContentCaptureService.aidl index a8dd21392337..d92fb3bed679 100644 --- a/core/java/android/service/contentcapture/IContentCaptureService.aidl +++ b/core/java/android/service/contentcapture/IContentCaptureService.aidl @@ -19,6 +19,7 @@ package android.service.contentcapture; import android.os.IBinder; import android.service.contentcapture.SnapshotData; import android.view.contentcapture.ContentCaptureContext; +import android.view.contentcapture.UserDataRemovalRequest; import com.android.internal.os.IResultReceiver; @@ -36,4 +37,5 @@ oneway interface IContentCaptureService { in IResultReceiver clientReceiver); void onSessionFinished(String sessionId); void onActivitySnapshot(String sessionId, in SnapshotData snapshotData); + void onUserDataRemovalRequest(in UserDataRemovalRequest request); } diff --git a/core/java/android/view/contentcapture/ContentCaptureManager.java b/core/java/android/view/contentcapture/ContentCaptureManager.java index 3474e232987e..b9017b365f90 100644 --- a/core/java/android/view/contentcapture/ContentCaptureManager.java +++ b/core/java/android/view/contentcapture/ContentCaptureManager.java @@ -189,13 +189,19 @@ public final class ContentCaptureManager { } /** - * Called by the ap to request the Content Capture service to remove user-data associated with + * Called by the app to request the Content Capture service to remove user-data associated with * some context. * * @param request object specifying what user data should be removed. */ public void removeUserData(@NonNull UserDataRemovalRequest request) { - //TODO(b/111276913): implement + Preconditions.checkNotNull(request); + + try { + mService.removeUserData(mContext.getUserId(), request); + } catch (RemoteException e) { + e.rethrowFromSystemServer(); + } } /** @hide */ diff --git a/core/java/android/view/contentcapture/IContentCaptureManager.aidl b/core/java/android/view/contentcapture/IContentCaptureManager.aidl index be9c00f04d99..51aea162f1db 100644 --- a/core/java/android/view/contentcapture/IContentCaptureManager.aidl +++ b/core/java/android/view/contentcapture/IContentCaptureManager.aidl @@ -19,6 +19,7 @@ package android.view.contentcapture; import android.content.ComponentName; import android.view.contentcapture.ContentCaptureContext; import android.view.contentcapture.ContentCaptureEvent; +import android.view.contentcapture.UserDataRemovalRequest; import android.os.IBinder; import com.android.internal.os.IResultReceiver; @@ -56,4 +57,9 @@ oneway interface IContentCaptureManager { * provided {@code Bundle} with key "{@code EXTRA}". */ void getReceiverServiceComponentName(int userId, in IResultReceiver result); + + /** + * Requests the removal of user data for the provided {@code userId}. + */ + void removeUserData(int userId, in UserDataRemovalRequest request); } diff --git a/core/java/android/view/contentcapture/UserDataRemovalRequest.aidl b/core/java/android/view/contentcapture/UserDataRemovalRequest.aidl new file mode 100644 index 000000000000..fbe47e08ea7c --- /dev/null +++ b/core/java/android/view/contentcapture/UserDataRemovalRequest.aidl @@ -0,0 +1,19 @@ +/* + * Copyright (C) 2019 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 UserDataRemovalRequest; diff --git a/core/java/android/view/contentcapture/UserDataRemovalRequest.java b/core/java/android/view/contentcapture/UserDataRemovalRequest.java index 0261b70825a5..8ee63ef74685 100644 --- a/core/java/android/view/contentcapture/UserDataRemovalRequest.java +++ b/core/java/android/view/contentcapture/UserDataRemovalRequest.java @@ -17,10 +17,15 @@ package android.view.contentcapture; import android.annotation.NonNull; import android.annotation.SystemApi; +import android.app.ActivityThread; import android.net.Uri; import android.os.Parcel; import android.os.Parcelable; +import android.util.IntArray; +import com.android.internal.util.Preconditions; + +import java.util.ArrayList; import java.util.List; /** @@ -29,8 +34,35 @@ import java.util.List; */ public final class UserDataRemovalRequest implements Parcelable { - private UserDataRemovalRequest(Builder builder) { - // TODO(b/111276913): implement + private final String mPackageName; + + private final boolean mForEverything; + private ArrayList<UriRequest> mUriRequests; + + private UserDataRemovalRequest(@NonNull Builder builder) { + mPackageName = ActivityThread.currentActivityThread().getApplication().getPackageName(); + mForEverything = builder.mForEverything; + if (builder.mUris != null) { + final int size = builder.mUris.size(); + mUriRequests = new ArrayList<>(size); + for (int i = 0; i < size; i++) { + mUriRequests.add(new UriRequest(builder.mUris.get(i), + builder.mRecursive.get(i) == 1)); + } + } + } + + private UserDataRemovalRequest(@NonNull Parcel parcel) { + mPackageName = parcel.readString(); + mForEverything = parcel.readBoolean(); + if (!mForEverything) { + final int size = parcel.readInt(); + mUriRequests = new ArrayList<>(size); + for (int i = 0; i < size; i++) { + mUriRequests.add(new UriRequest((Uri) parcel.readValue(null), + parcel.readBoolean())); + } + } } /** @@ -40,9 +72,7 @@ public final class UserDataRemovalRequest implements Parcelable { @SystemApi @NonNull public String getPackageName() { - // TODO(b/111276913): implement - // TODO(b/111276913): make sure it's set on system_service so it cannot be faked by app - return null; + return mPackageName; } /** @@ -52,8 +82,7 @@ public final class UserDataRemovalRequest implements Parcelable { */ @SystemApi public boolean isForEverything() { - // TODO(b/111276913): implement - return false; + return mForEverything; } /** @@ -64,8 +93,7 @@ public final class UserDataRemovalRequest implements Parcelable { @SystemApi @NonNull public List<UriRequest> getUriRequests() { - // TODO(b/111276913): implement - return null; + return mUriRequests; } /** @@ -73,6 +101,12 @@ public final class UserDataRemovalRequest implements Parcelable { */ public static final class Builder { + private boolean mForEverything; + private ArrayList<Uri> mUris; + private IntArray mRecursive; + + private boolean mDestroyed; + /** * Requests servive to remove all user data associated with the app's package. * @@ -80,7 +114,12 @@ public final class UserDataRemovalRequest implements Parcelable { */ @NonNull public Builder forEverything() { - // TODO(b/111276913): implement + throwIfDestroyed(); + if (mUris != null) { + throw new IllegalStateException("Already added Uris"); + } + + mForEverything = true; return this; } @@ -94,7 +133,19 @@ public final class UserDataRemovalRequest implements Parcelable { * @return this builder */ public Builder addUri(@NonNull Uri uri, boolean recursive) { - // TODO(b/111276913): implement + throwIfDestroyed(); + if (mForEverything) { + throw new IllegalStateException("Already is for everything"); + } + Preconditions.checkNotNull(uri); + + if (mUris == null) { + mUris = new ArrayList<>(); + mRecursive = new IntArray(); + } + + mUris.add(uri); + mRecursive.add(recursive ? 1 : 0); return this; } @@ -103,8 +154,16 @@ public final class UserDataRemovalRequest implements Parcelable { */ @NonNull public UserDataRemovalRequest build() { - // TODO(b/111276913): implement / unit test / check built / document exceptions - return null; + throwIfDestroyed(); + + Preconditions.checkState(mForEverything || mUris != null); + + mDestroyed = true; + return new UserDataRemovalRequest(this); + } + + private void throwIfDestroyed() { + Preconditions.checkState(!mDestroyed, "Already destroyed!"); } } @@ -115,7 +174,17 @@ public final class UserDataRemovalRequest implements Parcelable { @Override public void writeToParcel(Parcel parcel, int flags) { - // TODO(b/111276913): implement + parcel.writeString(mPackageName); + parcel.writeBoolean(mForEverything); + if (!mForEverything) { + final int size = mUriRequests.size(); + parcel.writeInt(size); + for (int i = 0; i < size; i++) { + final UriRequest request = mUriRequests.get(i); + parcel.writeValue(request.getUri()); + parcel.writeBoolean(request.isRecursive()); + } + } } public static final Parcelable.Creator<UserDataRemovalRequest> CREATOR = @@ -123,8 +192,7 @@ public final class UserDataRemovalRequest implements Parcelable { @Override public UserDataRemovalRequest createFromParcel(Parcel parcel) { - // TODO(b/111276913): implement - return null; + return new UserDataRemovalRequest(parcel); } @Override diff --git a/core/tests/coretests/src/android/view/contentcapture/ContentCaptureManagerTest.java b/core/tests/coretests/src/android/view/contentcapture/ContentCaptureManagerTest.java new file mode 100644 index 000000000000..312e0e0d8268 --- /dev/null +++ b/core/tests/coretests/src/android/view/contentcapture/ContentCaptureManagerTest.java @@ -0,0 +1,51 @@ +/* + * Copyright (C) 2019 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 static org.testng.Assert.assertThrows; + +import android.content.Context; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; + +/** + * Unit test for {@link ContentCaptureManager}. + * + * <p>To run it: + * {@code atest FrameworksCoreTests:android.view.contentcapture.ContentCaptureManagerTest} + */ +@RunWith(MockitoJUnitRunner.class) +public class ContentCaptureManagerTest { + + @Mock + private Context mMockContext; + + private ContentCaptureManager mManager; + + @Before + public void before() { + mManager = new ContentCaptureManager(mMockContext, null); + } + + @Test + public void testRemoveUserData_invalid() { + assertThrows(NullPointerException.class, () -> mManager.removeUserData(null)); + } +} diff --git a/core/tests/coretests/src/android/view/contentcapture/UserDataRemovalRequestTest.java b/core/tests/coretests/src/android/view/contentcapture/UserDataRemovalRequestTest.java new file mode 100644 index 000000000000..bebb2a89f480 --- /dev/null +++ b/core/tests/coretests/src/android/view/contentcapture/UserDataRemovalRequestTest.java @@ -0,0 +1,88 @@ +/* + * Copyright (C) 2019 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 static com.google.common.truth.Truth.assertThat; + +import static org.testng.Assert.assertThrows; + +import android.net.Uri; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; + +/** + * Unit test for {@link UserDataRemovalRequest}. + * + * <p>To run it: + * {@code atest FrameworksCoreTests:android.view.contentcapture.UserDataRemovalRequestTest} + */ +@RunWith(MockitoJUnitRunner.class) +public class UserDataRemovalRequestTest { + + @Mock + private final Uri mUri = Uri.parse("content://com.example/"); + + private UserDataRemovalRequest.Builder mBuilder = new UserDataRemovalRequest.Builder(); + + @Test + public void testBuilder_addUri_invalid() { + assertThrows(NullPointerException.class, () -> mBuilder.addUri(null, false)); + } + + @Test + public void testBuilder_addUri_valid() { + assertThat(mBuilder.addUri(mUri, false)).isNotNull(); + assertThat(mBuilder.addUri(Uri.parse("content://com.example2"), true)).isNotNull(); + } + + @Test + public void testBuilder_addUriAfterForEverything() { + assertThat(mBuilder.forEverything()).isNotNull(); + assertThrows(IllegalStateException.class, () -> mBuilder.addUri(mUri, false)); + } + + @Test + public void testBuilder_forEverythingAfterAddingUri() { + assertThat(mBuilder.addUri(mUri, false)).isNotNull(); + assertThrows(IllegalStateException.class, () -> mBuilder.forEverything()); + } + + @Test + public void testBuild_invalid() { + assertThrows(IllegalStateException.class, () -> mBuilder.build()); + } + + @Test + public void testBuild_valid() { + assertThat(new UserDataRemovalRequest.Builder().forEverything().build()) + .isNotNull(); + assertThat(new UserDataRemovalRequest.Builder().addUri(mUri, false).build()) + .isNotNull(); + } + + @Test + public void testNoMoreInteractionsAfterBuild() { + assertThat(mBuilder.forEverything().build()).isNotNull(); + + assertThrows(IllegalStateException.class, () -> mBuilder.addUri(mUri, false)); + assertThrows(IllegalStateException.class, () -> mBuilder.forEverything()); + assertThrows(IllegalStateException.class, () -> mBuilder.build()); + + } +} diff --git a/services/contentcapture/java/com/android/server/contentcapture/ContentCaptureManagerService.java b/services/contentcapture/java/com/android/server/contentcapture/ContentCaptureManagerService.java index e4bbcd67d4df..844096d9d717 100644 --- a/services/contentcapture/java/com/android/server/contentcapture/ContentCaptureManagerService.java +++ b/services/contentcapture/java/com/android/server/contentcapture/ContentCaptureManagerService.java @@ -35,6 +35,7 @@ import android.os.UserManager; import android.util.LocalLog; import android.util.Slog; import android.view.contentcapture.IContentCaptureManager; +import android.view.contentcapture.UserDataRemovalRequest; import com.android.internal.annotations.GuardedBy; import com.android.internal.os.IResultReceiver; @@ -224,6 +225,15 @@ public final class ContentCaptureManagerService extends } @Override + public void removeUserData(@UserIdInt int userId, @NonNull UserDataRemovalRequest request) { + Preconditions.checkNotNull(request); + synchronized (mLock) { + final ContentCapturePerUserService service = getServiceForUserLocked(userId); + service.removeUserDataLocked(request); + } + } + + @Override public void dump(FileDescriptor fd, PrintWriter pw, String[] args) { if (!DumpUtils.checkDumpPermission(getContext(), TAG, pw)) return; diff --git a/services/contentcapture/java/com/android/server/contentcapture/ContentCapturePerUserService.java b/services/contentcapture/java/com/android/server/contentcapture/ContentCapturePerUserService.java index 8d2c79bd9923..bc0e19a69040 100644 --- a/services/contentcapture/java/com/android/server/contentcapture/ContentCapturePerUserService.java +++ b/services/contentcapture/java/com/android/server/contentcapture/ContentCapturePerUserService.java @@ -28,6 +28,7 @@ import static com.android.server.wm.ActivityTaskManagerInternal.ASSIST_KEY_STRUC import android.Manifest; import android.annotation.NonNull; import android.annotation.UserIdInt; +import android.app.ActivityManagerInternal; import android.app.AppGlobals; import android.app.assist.AssistContent; import android.app.assist.AssistStructure; @@ -35,18 +36,22 @@ import android.content.ComponentName; import android.content.pm.PackageManager; import android.content.pm.PackageManager.NameNotFoundException; import android.content.pm.ServiceInfo; +import android.os.Binder; import android.os.Bundle; import android.os.IBinder; import android.os.RemoteException; +import android.os.UserHandle; import android.service.contentcapture.ContentCaptureService; import android.service.contentcapture.IContentCaptureServiceCallback; import android.service.contentcapture.SnapshotData; import android.util.ArrayMap; import android.util.Log; import android.util.Slog; +import android.view.contentcapture.UserDataRemovalRequest; import com.android.internal.annotations.GuardedBy; import com.android.internal.os.IResultReceiver; +import com.android.server.LocalServices; import com.android.server.contentcapture.RemoteContentCaptureService.ContentCaptureServiceCallbacks; import com.android.server.infra.AbstractPerUserSystemService; @@ -249,6 +254,39 @@ final class ContentCapturePerUserService } @GuardedBy("mLock") + public void removeUserDataLocked(@NonNull UserDataRemovalRequest request) { + if (!isEnabledLocked()) { + return; + } + assertCallerLocked(request.getPackageName()); + mRemoteService.onUserDataRemovalRequest(request); + } + + /** + * Asserts the component is owned by the caller. + */ + @GuardedBy("mLock") + private void assertCallerLocked(@NonNull String packageName) { + final PackageManager pm = getContext().getPackageManager(); + final int callingUid = Binder.getCallingUid(); + final int packageUid; + try { + packageUid = pm.getPackageUidAsUser(packageName, UserHandle.getCallingUserId()); + } catch (NameNotFoundException e) { + throw new SecurityException("Could not verify UID for " + packageName); + } + if (callingUid != packageUid && !LocalServices.getService(ActivityManagerInternal.class) + .hasRunningActivity(callingUid, packageName)) { + final String[] packages = pm.getPackagesForUid(callingUid); + final String callingPackage = packages != null ? packages[0] : "uid-" + callingUid; + Slog.w(TAG, "App (package=" + callingPackage + ", UID=" + callingUid + + ") passed package (" + packageName + ") owned by UID " + packageUid); + + throw new SecurityException("Invalid package: " + packageName); + } + } + + @GuardedBy("mLock") public boolean sendActivityAssistDataLocked(@NonNull IBinder activityToken, @NonNull Bundle data) { final String id = getSessionId(activityToken); diff --git a/services/contentcapture/java/com/android/server/contentcapture/RemoteContentCaptureService.java b/services/contentcapture/java/com/android/server/contentcapture/RemoteContentCaptureService.java index 12742ca0a46f..54eea5d8591c 100644 --- a/services/contentcapture/java/com/android/server/contentcapture/RemoteContentCaptureService.java +++ b/services/contentcapture/java/com/android/server/contentcapture/RemoteContentCaptureService.java @@ -26,6 +26,7 @@ import android.service.contentcapture.SnapshotData; import android.text.format.DateUtils; import android.util.Slog; import android.view.contentcapture.ContentCaptureContext; +import android.view.contentcapture.UserDataRemovalRequest; import com.android.internal.infra.AbstractMultiplePendingRequestsRemoteService; import com.android.internal.os.IResultReceiver; @@ -108,6 +109,13 @@ final class RemoteContentCaptureService scheduleAsyncRequest((s) -> s.onActivitySnapshot(sessionId, snapshotData)); } + /** + * Called by {@link ContentCaptureServerSession} to request removal of user data. + */ + public void onUserDataRemovalRequest(@NonNull UserDataRemovalRequest request) { + scheduleAsyncRequest((s) -> s.onUserDataRemovalRequest(request)); + } + public interface ContentCaptureServiceCallbacks extends VultureCallback<RemoteContentCaptureService> { // NOTE: so far we don't need to notify the callback implementation |