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 |