diff options
42 files changed, 1265 insertions, 69 deletions
diff --git a/Android.bp b/Android.bp index 948b0a068215..6a59abcbbf23 100644 --- a/Android.bp +++ b/Android.bp @@ -99,6 +99,7 @@ java_library { "core/java/android/app/backup/IRestoreSession.aidl", "core/java/android/app/backup/ISelectBackupTransportCallback.aidl", "core/java/android/app/slice/ISliceManager.aidl", + "core/java/android/app/slice/ISliceListener.aidl", "core/java/android/app/timezone/ICallback.aidl", "core/java/android/app/timezone/IRulesManager.aidl", "core/java/android/app/usage/ICacheQuotaService.aidl", diff --git a/Android.mk b/Android.mk index 9676958605dd..e290f4b19c7a 100644 --- a/Android.mk +++ b/Android.mk @@ -97,6 +97,7 @@ aidl_files := \ frameworks/base/core/java/android/app/admin/SystemUpdatePolicy.aidl \ frameworks/base/core/java/android/app/admin/PasswordMetrics.aidl \ frameworks/base/core/java/android/app/slice/ISliceManager.aidl \ + frameworks/base/core/java/android/app/slice/ISliceListener.aidl \ frameworks/base/core/java/android/print/PrintDocumentInfo.aidl \ frameworks/base/core/java/android/print/PageRange.aidl \ frameworks/base/core/java/android/print/PrintAttributes.aidl \ diff --git a/core/java/android/app/slice/ISliceListener.aidl b/core/java/android/app/slice/ISliceListener.aidl new file mode 100644 index 000000000000..d293fd46d298 --- /dev/null +++ b/core/java/android/app/slice/ISliceListener.aidl @@ -0,0 +1,25 @@ +/** + * Copyright (c) 2017, 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.app.slice; + +import android.app.slice.ISliceManager; +import android.app.slice.Slice; + +/** @hide */ +oneway interface ISliceListener { + void onSliceUpdated(in Slice s); +} diff --git a/core/java/android/app/slice/ISliceManager.aidl b/core/java/android/app/slice/ISliceManager.aidl index 6e52f385bcf7..5f0e542f4b12 100644 --- a/core/java/android/app/slice/ISliceManager.aidl +++ b/core/java/android/app/slice/ISliceManager.aidl @@ -16,6 +16,17 @@ package android.app.slice; +import android.app.slice.ISliceListener; +import android.app.slice.SliceSpec; +import android.net.Uri; + /** @hide */ interface ISliceManager { + void addSliceListener(in Uri uri, String pkg, in ISliceListener listener, + in SliceSpec[] specs); + void removeSliceListener(in Uri uri, String pkg, in ISliceListener listener); + void pinSlice(String pkg, in Uri uri, in SliceSpec[] specs); + void unpinSlice(String pkg, in Uri uri); + boolean hasSliceAccess(String pkg); + SliceSpec[] getPinnedSpecs(in Uri uri, String pkg); } diff --git a/core/java/android/app/slice/Slice.aidl b/core/java/android/app/slice/Slice.aidl new file mode 100644 index 000000000000..e097f9d09a9a --- /dev/null +++ b/core/java/android/app/slice/Slice.aidl @@ -0,0 +1,19 @@ +/** + * Copyright (c) 2017, 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.app.slice; + +parcelable Slice; diff --git a/core/java/android/app/slice/SliceManager.java b/core/java/android/app/slice/SliceManager.java index e99f67632712..f8e19c12b810 100644 --- a/core/java/android/app/slice/SliceManager.java +++ b/core/java/android/app/slice/SliceManager.java @@ -17,8 +17,11 @@ package android.app.slice; import android.annotation.SystemService; +import android.app.slice.ISliceListener.Stub; import android.content.Context; +import android.net.Uri; import android.os.Handler; +import android.os.RemoteException; import android.os.ServiceManager; import android.os.ServiceManager.ServiceNotFoundException; @@ -36,4 +39,93 @@ public class SliceManager { mService = ISliceManager.Stub.asInterface( ServiceManager.getServiceOrThrow(Context.SLICE_SERVICE)); } + + /** + */ + public void addSliceListener(Uri uri, SliceListener listener, SliceSpec[] specs) { + try { + mService.addSliceListener(uri, mContext.getPackageName(), listener.mStub, specs); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + /** + */ + public void removeSliceListener(Uri uri, SliceListener listener) { + try { + mService.removeSliceListener(uri, mContext.getPackageName(), listener.mStub); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + /** + */ + public void pinSlice(Uri uri, SliceSpec[] specs) { + try { + mService.pinSlice(mContext.getPackageName(), uri, specs); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + /** + */ + public void unpinSlice(Uri uri) { + try { + mService.unpinSlice(mContext.getPackageName(), uri); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + /** + */ + public boolean hasSliceAccess() { + try { + return mService.hasSliceAccess(mContext.getPackageName()); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + /** + */ + public SliceSpec[] getPinnedSpecs(Uri uri) { + try { + return mService.getPinnedSpecs(uri, mContext.getPackageName()); + } catch (RemoteException e) { + throw e.rethrowFromSystemServer(); + } + } + + /** + */ + public abstract static class SliceListener { + private final Handler mHandler; + + /** + */ + public SliceListener() { + this(Handler.getMain()); + } + + /** + */ + public SliceListener(Handler h) { + mHandler = h; + } + + /** + */ + public abstract void onSliceUpdated(Slice s); + + private final ISliceListener.Stub mStub = new Stub() { + @Override + public void onSliceUpdated(Slice s) throws RemoteException { + mHandler.post(() -> SliceListener.this.onSliceUpdated(s)); + } + }; + } } diff --git a/core/java/android/app/slice/SliceProvider.java b/core/java/android/app/slice/SliceProvider.java index ac5365c35f49..7dcd2fead73f 100644 --- a/core/java/android/app/slice/SliceProvider.java +++ b/core/java/android/app/slice/SliceProvider.java @@ -105,6 +105,14 @@ public abstract class SliceProvider extends ContentProvider { /** * @hide */ + public static final String METHOD_PIN = "pin"; + /** + * @hide + */ + public static final String METHOD_UNPIN = "unpin"; + /** + * @hide + */ public static final String EXTRA_INTENT = "slice_intent"; /** * @hide @@ -143,6 +151,18 @@ public abstract class SliceProvider extends ContentProvider { } /** + * @hide + */ + public void onSlicePinned(Uri sliceUri) { + } + + /** + * @hide + */ + public void onSliceUnpinned(Uri sliceUri) { + } + + /** * This method must be overridden if an {@link IntentFilter} is specified on the SliceProvider. * In that case, this method can be called and is expected to return a non-null Uri representing * a slice. Otherwise this will throw {@link UnsupportedOperationException}. @@ -221,6 +241,7 @@ public abstract class SliceProvider extends ContentProvider { getContext().enforceCallingPermission(permission.BIND_SLICE, "Slice binding requires the permission BIND_SLICE"); Intent intent = extras.getParcelable(EXTRA_INTENT); + if (intent == null) return null; Uri uri = onMapIntentToUri(intent); List<SliceSpec> supportedSpecs = extras.getParcelableArrayList(EXTRA_SUPPORTED_SPECS); Bundle b = new Bundle(); @@ -231,10 +252,62 @@ public abstract class SliceProvider extends ContentProvider { b.putParcelable(EXTRA_SLICE, null); } return b; + } else if (method.equals(METHOD_PIN)) { + Uri uri = extras.getParcelable(EXTRA_BIND_URI); + if (!UserHandle.isSameApp(Binder.getCallingUid(), Process.myUid())) { + getContext().enforceUriPermission(uri, permission.BIND_SLICE, + permission.BIND_SLICE, Binder.getCallingPid(), Binder.getCallingUid(), + Intent.FLAG_GRANT_WRITE_URI_PERMISSION, + "Slice binding requires the permission BIND_SLICE"); + } + handlePinSlice(uri); + } else if (method.equals(METHOD_UNPIN)) { + Uri uri = extras.getParcelable(EXTRA_BIND_URI); + if (!UserHandle.isSameApp(Binder.getCallingUid(), Process.myUid())) { + getContext().enforceUriPermission(uri, permission.BIND_SLICE, + permission.BIND_SLICE, Binder.getCallingPid(), Binder.getCallingUid(), + Intent.FLAG_GRANT_WRITE_URI_PERMISSION, + "Slice binding requires the permission BIND_SLICE"); + } + handleUnpinSlice(uri); } return super.call(method, arg, extras); } + private void handlePinSlice(Uri sliceUri) { + if (Looper.myLooper() == Looper.getMainLooper()) { + onSlicePinned(sliceUri); + } else { + CountDownLatch latch = new CountDownLatch(1); + Handler.getMain().post(() -> { + onSlicePinned(sliceUri); + latch.countDown(); + }); + try { + latch.await(); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + } + } + + private void handleUnpinSlice(Uri sliceUri) { + if (Looper.myLooper() == Looper.getMainLooper()) { + onSliceUnpinned(sliceUri); + } else { + CountDownLatch latch = new CountDownLatch(1); + Handler.getMain().post(() -> { + onSliceUnpinned(sliceUri); + latch.countDown(); + }); + try { + latch.await(); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + } + } + private Slice handleBindSlice(Uri sliceUri, List<SliceSpec> supportedSpecs) { if (Looper.myLooper() == Looper.getMainLooper()) { return onBindSliceStrict(sliceUri, supportedSpecs); diff --git a/core/java/android/app/slice/SliceSpec.aidl b/core/java/android/app/slice/SliceSpec.aidl new file mode 100644 index 000000000000..92e98b78a7e8 --- /dev/null +++ b/core/java/android/app/slice/SliceSpec.aidl @@ -0,0 +1,19 @@ +/** + * Copyright (c) 2017, 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.app.slice; + +parcelable SliceSpec; diff --git a/core/java/android/app/slice/SliceSpec.java b/core/java/android/app/slice/SliceSpec.java index 433b67e9aacb..8cc0384c1007 100644 --- a/core/java/android/app/slice/SliceSpec.java +++ b/core/java/android/app/slice/SliceSpec.java @@ -103,6 +103,11 @@ public final class SliceSpec implements Parcelable { return mType.equals(other.mType) && mRevision == other.mRevision; } + @Override + public String toString() { + return String.format("SliceSpec{%s,%d}", mType, mRevision); + } + public static final Creator<SliceSpec> CREATOR = new Creator<SliceSpec>() { @Override public SliceSpec createFromParcel(Parcel source) { diff --git a/services/core/java/com/android/server/slice/PinnedSliceState.java b/services/core/java/com/android/server/slice/PinnedSliceState.java new file mode 100644 index 000000000000..cf930f5cc6b7 --- /dev/null +++ b/services/core/java/com/android/server/slice/PinnedSliceState.java @@ -0,0 +1,215 @@ +/* + * Copyright (C) 2017 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 com.android.server.slice; + +import android.app.slice.ISliceListener; +import android.app.slice.Slice; +import android.app.slice.SliceProvider; +import android.app.slice.SliceSpec; +import android.content.ContentProviderClient; +import android.net.Uri; +import android.os.Bundle; +import android.os.RemoteException; +import android.util.ArraySet; +import android.util.Log; + +import com.android.internal.annotations.GuardedBy; +import com.android.internal.annotations.VisibleForTesting; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Objects; + +/** + * Manages the state of a pinned slice. + */ +public class PinnedSliceState { + + private static final long SLICE_TIMEOUT = 5000; + private static final String TAG = "PinnedSliceState"; + + private final Object mLock; + + private final SliceManagerService mService; + private final Uri mUri; + @GuardedBy("mLock") + private final ArraySet<String> mPinnedPkgs = new ArraySet<>(); + @GuardedBy("mLock") + private final ArraySet<ISliceListener> mListeners = new ArraySet<>(); + @GuardedBy("mLock") + private SliceSpec[] mSupportedSpecs = null; + + public PinnedSliceState(SliceManagerService service, Uri uri) { + mService = service; + mUri = uri; + mService.getHandler().post(this::handleSendPinned); + mLock = mService.getLock(); + } + + public SliceSpec[] getSpecs() { + return mSupportedSpecs; + } + + public void mergeSpecs(SliceSpec[] supportedSpecs) { + synchronized (mLock) { + if (mSupportedSpecs == null) { + mSupportedSpecs = supportedSpecs; + } else { + List<SliceSpec> specs = Arrays.asList(mSupportedSpecs); + mSupportedSpecs = specs.stream().map(s -> { + SliceSpec other = findSpec(supportedSpecs, s.getType()); + if (other == null) return null; + if (other.getRevision() < s.getRevision()) { + return other; + } + return s; + }).filter(s -> s != null).toArray(SliceSpec[]::new); + } + } + } + + private SliceSpec findSpec(SliceSpec[] specs, String type) { + for (SliceSpec spec : specs) { + if (Objects.equals(spec.getType(), type)) { + return spec; + } + } + return null; + } + + public Uri getUri() { + return mUri; + } + + public void destroy() { + mService.getHandler().post(this::handleSendUnpinned); + } + + public void onChange() { + mService.getHandler().post(this::handleBind); + } + + public void addSliceListener(ISliceListener listener, SliceSpec[] specs) { + synchronized (mLock) { + if (mListeners.add(listener) && mListeners.size() == 1) { + mService.listen(mUri); + } + mergeSpecs(specs); + } + } + + public boolean removeSliceListener(ISliceListener listener) { + synchronized (mLock) { + if (mListeners.remove(listener) && mListeners.size() == 0) { + mService.unlisten(mUri); + } + } + return !isPinned(); + } + + public void pin(String pkg, SliceSpec[] specs) { + synchronized (mLock) { + mPinnedPkgs.add(pkg); + mergeSpecs(specs); + } + } + + public boolean unpin(String pkg) { + synchronized (mLock) { + mPinnedPkgs.remove(pkg); + } + return !isPinned(); + } + + public boolean isListening() { + synchronized (mLock) { + return !mListeners.isEmpty(); + } + } + + @VisibleForTesting + public boolean isPinned() { + synchronized (mLock) { + return !mPinnedPkgs.isEmpty() || !mListeners.isEmpty(); + } + } + + ContentProviderClient getClient() { + ContentProviderClient client = + mService.getContext().getContentResolver().acquireContentProviderClient(mUri); + client.setDetectNotResponding(SLICE_TIMEOUT); + return client; + } + + private void handleBind() { + Slice s; + try (ContentProviderClient client = getClient()) { + Bundle extras = new Bundle(); + extras.putParcelable(SliceProvider.EXTRA_BIND_URI, mUri); + extras.putParcelableArrayList(SliceProvider.EXTRA_SUPPORTED_SPECS, + new ArrayList<>(Arrays.asList(mSupportedSpecs))); + final Bundle res; + try { + res = client.call(SliceProvider.METHOD_SLICE, null, extras); + } catch (RemoteException e) { + Log.e(TAG, "Unable to bind slice " + mUri, e); + return; + } + if (res == null) return; + Bundle.setDefusable(res, true); + s = res.getParcelable(SliceProvider.EXTRA_SLICE); + } + synchronized (mLock) { + mListeners.removeIf(l -> { + try { + l.onSliceUpdated(s); + return false; + } catch (RemoteException e) { + Log.e(TAG, "Unable to notify slice " + mUri, e); + return true; + } + }); + if (!isPinned()) { + // All the listeners died, remove from pinned state. + mService.removePinnedSlice(mUri); + } + } + } + + private void handleSendPinned() { + try (ContentProviderClient client = getClient()) { + Bundle b = new Bundle(); + b.putParcelable(SliceProvider.EXTRA_BIND_URI, mUri); + try { + client.call(SliceProvider.METHOD_PIN, null, b); + } catch (RemoteException e) { + Log.w(TAG, "Unable to contact " + mUri, e); + } + } + } + + private void handleSendUnpinned() { + try (ContentProviderClient client = getClient()) { + Bundle b = new Bundle(); + b.putParcelable(SliceProvider.EXTRA_BIND_URI, mUri); + try { + client.call(SliceProvider.METHOD_UNPIN, null, b); + } catch (RemoteException e) { + Log.w(TAG, "Unable to contact " + mUri, e); + } + } + } +} diff --git a/services/core/java/com/android/server/slice/SliceManagerService.java b/services/core/java/com/android/server/slice/SliceManagerService.java index 047e270f3fac..2d9e772a6b0c 100644 --- a/services/core/java/com/android/server/slice/SliceManagerService.java +++ b/services/core/java/com/android/server/slice/SliceManagerService.java @@ -16,21 +16,307 @@ package com.android.server.slice; +import static android.content.ContentProvider.getUserIdFromUri; +import static android.content.ContentProvider.maybeAddUserId; + +import android.Manifest.permission; +import android.app.AppOpsManager; +import android.app.slice.ISliceListener; import android.app.slice.ISliceManager; +import android.app.slice.SliceSpec; +import android.content.ComponentName; import android.content.Context; +import android.content.Intent; +import android.content.pm.PackageManagerInternal; +import android.content.pm.ResolveInfo; +import android.database.ContentObserver; +import android.net.Uri; +import android.os.Binder; +import android.os.Handler; +import android.os.Looper; +import android.os.Process; +import android.os.RemoteException; +import android.util.ArrayMap; +import android.util.Log; +import com.android.internal.annotations.GuardedBy; +import com.android.internal.annotations.VisibleForTesting; +import com.android.internal.app.AssistUtils; +import com.android.internal.util.Preconditions; +import com.android.server.LocalServices; +import com.android.server.ServiceThread; import com.android.server.SystemService; +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; + public class SliceManagerService extends ISliceManager.Stub { + private static final String TAG = "SliceManagerService"; + private final Object mLock = new Object(); + + private final Context mContext; + private final PackageManagerInternal mPackageManagerInternal; + private final AppOpsManager mAppOps; + private final AssistUtils mAssistUtils; + + @GuardedBy("mLock") + private final ArrayMap<Uri, PinnedSliceState> mPinnedSlicesByUri = new ArrayMap<>(); + private final Handler mHandler; + private final ContentObserver mObserver; + public SliceManagerService(Context context) { + this(context, createHandler().getLooper()); + } + @VisibleForTesting + SliceManagerService(Context context, Looper looper) { + mContext = context; + mPackageManagerInternal = Preconditions.checkNotNull( + LocalServices.getService(PackageManagerInternal.class)); + mAppOps = context.getSystemService(AppOpsManager.class); + mAssistUtils = new AssistUtils(context); + mHandler = new Handler(looper); + + mObserver = new ContentObserver(mHandler) { + @Override + public void onChange(boolean selfChange, Uri uri) { + try { + getPinnedSlice(uri).onChange(); + } catch (IllegalStateException e) { + Log.e(TAG, "Received change for unpinned slice " + uri, e); + } + } + }; } + /// ----- Lifecycle stuff ----- private void systemReady() { } - private void onUnlockUser(int userHandle) { + private void onUnlockUser(int userId) { + } + + private void onStopUser(int userId) { + synchronized (mLock) { + mPinnedSlicesByUri.values().removeIf(s -> getUserIdFromUri(s.getUri()) == userId); + } + } + + /// ----- ISliceManager stuff ----- + @Override + public void addSliceListener(Uri uri, String pkg, ISliceListener listener, SliceSpec[] specs) + throws RemoteException { + verifyCaller(pkg); + uri = maybeAddUserId(uri, Binder.getCallingUserHandle().getIdentifier()); + enforceAccess(pkg, uri); + getOrCreatePinnedSlice(uri).addSliceListener(listener, specs); + } + + @Override + public void removeSliceListener(Uri uri, String pkg, ISliceListener listener) + throws RemoteException { + verifyCaller(pkg); + uri = maybeAddUserId(uri, Binder.getCallingUserHandle().getIdentifier()); + enforceAccess(pkg, uri); + if (getPinnedSlice(uri).removeSliceListener(listener)) { + removePinnedSlice(uri); + } + } + + @Override + public void pinSlice(String pkg, Uri uri, SliceSpec[] specs) throws RemoteException { + verifyCaller(pkg); + enforceFullAccess(pkg, "pinSlice", uri); + uri = maybeAddUserId(uri, Binder.getCallingUserHandle().getIdentifier()); + getOrCreatePinnedSlice(uri).pin(pkg, specs); + } + + @Override + public void unpinSlice(String pkg, Uri uri) throws RemoteException { + verifyCaller(pkg); + enforceFullAccess(pkg, "unpinSlice", uri); + uri = maybeAddUserId(uri, Binder.getCallingUserHandle().getIdentifier()); + if (getPinnedSlice(uri).unpin(pkg)) { + removePinnedSlice(uri); + } + } + + @Override + public boolean hasSliceAccess(String pkg) throws RemoteException { + verifyCaller(pkg); + return hasFullSliceAccess(pkg, Binder.getCallingUserHandle().getIdentifier()); + } + + @Override + public SliceSpec[] getPinnedSpecs(Uri uri, String pkg) throws RemoteException { + verifyCaller(pkg); + enforceAccess(pkg, uri); + return getPinnedSlice(uri).getSpecs(); + } + + /// ----- internal code ----- + void removePinnedSlice(Uri uri) { + synchronized (mLock) { + mPinnedSlicesByUri.remove(uri).destroy(); + } + } + + private PinnedSliceState getPinnedSlice(Uri uri) { + synchronized (mLock) { + PinnedSliceState manager = mPinnedSlicesByUri.get(uri); + if (manager == null) { + throw new IllegalStateException(String.format("Slice %s not pinned", + uri.toString())); + } + return manager; + } + } + + private PinnedSliceState getOrCreatePinnedSlice(Uri uri) { + synchronized (mLock) { + PinnedSliceState manager = mPinnedSlicesByUri.get(uri); + if (manager == null) { + manager = createPinnedSlice(uri); + mPinnedSlicesByUri.put(uri, manager); + } + return manager; + } + } + + @VisibleForTesting + PinnedSliceState createPinnedSlice(Uri uri) { + return new PinnedSliceState(this, uri); + } + + public Object getLock() { + return mLock; + } + + public Context getContext() { + return mContext; + } + + public Handler getHandler() { + return mHandler; + } + + private void enforceAccess(String pkg, Uri uri) { + getContext().enforceUriPermission(uri, permission.BIND_SLICE, + permission.BIND_SLICE, Binder.getCallingPid(), Binder.getCallingUid(), + Intent.FLAG_GRANT_WRITE_URI_PERMISSION, + "Slice binding requires the permission BIND_SLICE"); + int user = Binder.getCallingUserHandle().getIdentifier(); + if (getUserIdFromUri(uri, user) != user) { + getContext().enforceCallingOrSelfPermission(permission.INTERACT_ACROSS_USERS_FULL, + "Slice interaction across users requires INTERACT_ACROSS_USERS_FULL"); + } + } + + private void enforceFullAccess(String pkg, String name, Uri uri) { + int user = Binder.getCallingUserHandle().getIdentifier(); + if (!hasFullSliceAccess(pkg, user)) { + throw new SecurityException(String.format("Call %s requires full slice access", name)); + } + if (getUserIdFromUri(uri, user) != user) { + getContext().enforceCallingOrSelfPermission(permission.INTERACT_ACROSS_USERS_FULL, + "Slice interaction across users requires INTERACT_ACROSS_USERS_FULL"); + } + } + + private void verifyCaller(String pkg) { + mAppOps.checkPackage(Binder.getCallingUid(), pkg); + } + + private boolean hasFullSliceAccess(String pkg, int userId) { + return isDefaultHomeApp(pkg, userId) || isAssistant(pkg, userId) + || isGrantedFullAccess(pkg, userId); + } + + private boolean isAssistant(String pkg, int userId) { + final ComponentName cn = mAssistUtils.getAssistComponentForUser(userId); + if (cn == null) { + return false; + } + return cn.getPackageName().equals(pkg); + } + + public void listen(Uri uri) { + mContext.getContentResolver().registerContentObserver(uri, true, mObserver); + } + + public void unlisten(Uri uri) { + mContext.getContentResolver().unregisterContentObserver(mObserver); + synchronized (mLock) { + mPinnedSlicesByUri.forEach((u, s) -> { + if (s.isListening()) { + listen(u); + } + }); + } + } + + private boolean isDefaultHomeApp(String pkg, int userId) { + String defaultHome = getDefaultHome(userId); + return Objects.equals(pkg, defaultHome); + } + + // Based on getDefaultHome in ShortcutService. + // TODO: Unify if possible + @VisibleForTesting + String getDefaultHome(int userId) { + final long token = Binder.clearCallingIdentity(); + try { + final List<ResolveInfo> allHomeCandidates = new ArrayList<>(); + + // Default launcher from package manager. + final ComponentName defaultLauncher = mPackageManagerInternal + .getHomeActivitiesAsUser(allHomeCandidates, userId); + + ComponentName detected = null; + if (defaultLauncher != null) { + detected = defaultLauncher; + } + + if (detected == null) { + // If we reach here, that means it's the first check since the user was created, + // and there's already multiple launchers and there's no default set. + // Find the system one with the highest priority. + // (We need to check the priority too because of FallbackHome in Settings.) + // If there's no system launcher yet, then no one can access slices, until + // the user explicitly sets one. + final int size = allHomeCandidates.size(); + + int lastPriority = Integer.MIN_VALUE; + for (int i = 0; i < size; i++) { + final ResolveInfo ri = allHomeCandidates.get(i); + if (!ri.activityInfo.applicationInfo.isSystemApp()) { + continue; + } + if (ri.priority < lastPriority) { + continue; + } + detected = ri.activityInfo.getComponentName(); + lastPriority = ri.priority; + } + } + return detected.getPackageName(); + } finally { + Binder.restoreCallingIdentity(token); + } + } + + private boolean isGrantedFullAccess(String pkg, int userId) { + // TODO: This will be user granted access, if we allow this through a prompt. + return false; + } + + private static ServiceThread createHandler() { + ServiceThread handlerThread = new ServiceThread(TAG, + Process.THREAD_PRIORITY_BACKGROUND, true /*allowIo*/); + handlerThread.start(); + return handlerThread; } public static class Lifecycle extends SystemService { @@ -57,5 +343,10 @@ public class SliceManagerService extends ISliceManager.Stub { public void onUnlockUser(int userHandle) { mService.onUnlockUser(userHandle); } + + @Override + public void onStopUser(int userHandle) { + mService.onStopUser(userHandle); + } } } diff --git a/services/tests/uiservicestests/Android.mk b/services/tests/uiservicestests/Android.mk index 40e78785b9f5..d8e14ecab1cd 100644 --- a/services/tests/uiservicestests/Android.mk +++ b/services/tests/uiservicestests/Android.mk @@ -10,7 +10,8 @@ LOCAL_MODULE_TAGS := tests # Include test java files and source from notifications package. LOCAL_SRC_FILES := $(call all-java-files-under, src) \ - $(call all-java-files-under, ../../core/java/com/android/server/notification) + $(call all-java-files-under, ../../core/java/com/android/server/notification) \ + $(call all-java-files-under, ../../core/java/com/android/server/slice) \ LOCAL_STATIC_JAVA_LIBRARIES := \ frameworks-base-testutils \ diff --git a/services/tests/uiservicestests/AndroidManifest.xml b/services/tests/uiservicestests/AndroidManifest.xml index 621b4572c467..f022dcf376a6 100644 --- a/services/tests/uiservicestests/AndroidManifest.xml +++ b/services/tests/uiservicestests/AndroidManifest.xml @@ -28,6 +28,9 @@ <application> <uses-library android:name="android.test.runner" /> + + <provider android:name=".DummyProvider" + android:authorities="com.android.services.uitests" /> </application> <instrumentation diff --git a/services/tests/uiservicestests/src/com/android/frameworks/tests/uiservices/DummyProvider.java b/services/tests/uiservicestests/src/com/android/frameworks/tests/uiservices/DummyProvider.java new file mode 100644 index 000000000000..574c226699c6 --- /dev/null +++ b/services/tests/uiservicestests/src/com/android/frameworks/tests/uiservices/DummyProvider.java @@ -0,0 +1,53 @@ +/* + * Copyright (C) 2017 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 com.android.frameworks.tests.uiservices; + +import android.content.ContentProvider; +import android.content.ContentValues; +import android.database.Cursor; +import android.net.Uri; + +public class DummyProvider extends ContentProvider { + @Override + public boolean onCreate() { + return true; + } + + @Override + public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, + String sortOrder) { + return null; + } + + @Override + public String getType(Uri uri) { + return null; + } + + @Override + public Uri insert(Uri uri, ContentValues values) { + return null; + } + + @Override + public int delete(Uri uri, String selection, String[] selectionArgs) { + return 0; + } + + @Override + public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) { + return 0; + } +} diff --git a/services/tests/uiservicestests/src/com/android/server/UiServiceTestCase.java b/services/tests/uiservicestests/src/com/android/server/UiServiceTestCase.java new file mode 100644 index 000000000000..f534b5c40267 --- /dev/null +++ b/services/tests/uiservicestests/src/com/android/server/UiServiceTestCase.java @@ -0,0 +1,38 @@ +/* + * Copyright (C) 2017 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 com.android.server; + +import android.content.Context; +import android.support.test.InstrumentationRegistry; +import android.testing.TestableContext; + +import org.junit.Before; +import org.junit.Rule; + + +public class UiServiceTestCase { + @Rule + public final TestableContext mContext = + new TestableContext(InstrumentationRegistry.getContext(), null); + + protected TestableContext getContext() { + return mContext; + } + + @Before + public void setup() { + // Share classloader to allow package access. + System.setProperty("dexmaker.share_classloader", "true"); + } +} diff --git a/services/tests/uiservicestests/src/com/android/server/notification/AlertRateLimiterTest.java b/services/tests/uiservicestests/src/com/android/server/notification/AlertRateLimiterTest.java index faf6a9b76434..d4c41e0285bd 100644 --- a/services/tests/uiservicestests/src/com/android/server/notification/AlertRateLimiterTest.java +++ b/services/tests/uiservicestests/src/com/android/server/notification/AlertRateLimiterTest.java @@ -22,13 +22,16 @@ import static junit.framework.Assert.assertTrue; import android.support.test.runner.AndroidJUnit4; import android.test.suitebuilder.annotation.SmallTest; + +import com.android.server.UiServiceTestCase; + import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; @SmallTest @RunWith(AndroidJUnit4.class) -public class AlertRateLimiterTest extends NotificationTestCase { +public class AlertRateLimiterTest extends UiServiceTestCase { private long mTestStartTime; private diff --git a/services/tests/uiservicestests/src/com/android/server/notification/BadgeExtractorTest.java b/services/tests/uiservicestests/src/com/android/server/notification/BadgeExtractorTest.java index 262516dda7ad..142041a4f705 100644 --- a/services/tests/uiservicestests/src/com/android/server/notification/BadgeExtractorTest.java +++ b/services/tests/uiservicestests/src/com/android/server/notification/BadgeExtractorTest.java @@ -27,13 +27,13 @@ import android.app.ActivityManager; import android.app.Notification; import android.app.Notification.Builder; import android.app.NotificationChannel; -import android.app.NotificationManager; import android.os.UserHandle; -import android.provider.Settings.Secure; import android.service.notification.StatusBarNotification; import android.support.test.runner.AndroidJUnit4; import android.test.suitebuilder.annotation.SmallTest; +import com.android.server.UiServiceTestCase; + import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; @@ -42,7 +42,7 @@ import org.mockito.MockitoAnnotations; @SmallTest @RunWith(AndroidJUnit4.class) -public class BadgeExtractorTest extends NotificationTestCase { +public class BadgeExtractorTest extends UiServiceTestCase { @Mock RankingConfig mConfig; diff --git a/services/tests/uiservicestests/src/com/android/server/notification/BuzzBeepBlinkTest.java b/services/tests/uiservicestests/src/com/android/server/notification/BuzzBeepBlinkTest.java index 0b4d61fb783e..a92f7e7af5d8 100644 --- a/services/tests/uiservicestests/src/com/android/server/notification/BuzzBeepBlinkTest.java +++ b/services/tests/uiservicestests/src/com/android/server/notification/BuzzBeepBlinkTest.java @@ -58,13 +58,13 @@ import android.provider.Settings; import android.service.notification.StatusBarNotification; import android.support.test.runner.AndroidJUnit4; import android.test.suitebuilder.annotation.SmallTest; -import android.util.Slog; import android.view.accessibility.AccessibilityEvent; import android.view.accessibility.AccessibilityManager; import android.view.accessibility.IAccessibilityManager; import android.view.accessibility.IAccessibilityManagerClient; import com.android.internal.util.IntPair; +import com.android.server.UiServiceTestCase; import com.android.server.lights.Light; import org.junit.Before; @@ -74,12 +74,10 @@ import org.mockito.ArgumentMatcher; import org.mockito.Mock; import org.mockito.Mockito; import org.mockito.MockitoAnnotations; -import org.mockito.invocation.InvocationOnMock; -import org.mockito.stubbing.Answer; @SmallTest @RunWith(AndroidJUnit4.class) -public class BuzzBeepBlinkTest extends NotificationTestCase { +public class BuzzBeepBlinkTest extends UiServiceTestCase { @Mock AudioManager mAudioManager; @Mock Vibrator mVibrator; diff --git a/services/tests/uiservicestests/src/com/android/server/notification/GlobalSortKeyComparatorTest.java b/services/tests/uiservicestests/src/com/android/server/notification/GlobalSortKeyComparatorTest.java index f92bd842e815..97f210400141 100644 --- a/services/tests/uiservicestests/src/com/android/server/notification/GlobalSortKeyComparatorTest.java +++ b/services/tests/uiservicestests/src/com/android/server/notification/GlobalSortKeyComparatorTest.java @@ -28,6 +28,8 @@ import android.service.notification.StatusBarNotification; import android.support.test.runner.AndroidJUnit4; import android.test.suitebuilder.annotation.SmallTest; +import com.android.server.UiServiceTestCase; + import org.junit.Test; import org.junit.runner.RunWith; @@ -37,7 +39,7 @@ import java.util.List; @SmallTest @RunWith(AndroidJUnit4.class) -public class GlobalSortKeyComparatorTest extends NotificationTestCase { +public class GlobalSortKeyComparatorTest extends UiServiceTestCase { private final String PKG = "PKG"; private final int UID = 1111111; diff --git a/services/tests/uiservicestests/src/com/android/server/notification/GroupHelperTest.java b/services/tests/uiservicestests/src/com/android/server/notification/GroupHelperTest.java index f75c648f3c3e..8d4c5b1df7d2 100644 --- a/services/tests/uiservicestests/src/com/android/server/notification/GroupHelperTest.java +++ b/services/tests/uiservicestests/src/com/android/server/notification/GroupHelperTest.java @@ -38,6 +38,8 @@ import android.service.notification.StatusBarNotification; import android.support.test.runner.AndroidJUnit4; import android.test.suitebuilder.annotation.SmallTest; +import com.android.server.UiServiceTestCase; + import java.util.ArrayList; import java.util.LinkedHashSet; import java.util.List; @@ -45,7 +47,7 @@ import java.util.Map; @SmallTest @RunWith(AndroidJUnit4.class) -public class GroupHelperTest extends NotificationTestCase { +public class GroupHelperTest extends UiServiceTestCase { private @Mock GroupHelper.Callback mCallback; private GroupHelper mGroupHelper; diff --git a/services/tests/uiservicestests/src/com/android/server/notification/ImportanceExtractorTest.java b/services/tests/uiservicestests/src/com/android/server/notification/ImportanceExtractorTest.java index d325e10b5897..73d5961ee90b 100644 --- a/services/tests/uiservicestests/src/com/android/server/notification/ImportanceExtractorTest.java +++ b/services/tests/uiservicestests/src/com/android/server/notification/ImportanceExtractorTest.java @@ -30,7 +30,6 @@ import android.support.test.runner.AndroidJUnit4; import android.test.suitebuilder.annotation.SmallTest; import org.mockito.Mock; -import org.mockito.Mockito; import org.mockito.MockitoAnnotations; import static org.mockito.Matchers.anyInt; @@ -39,9 +38,11 @@ import static org.mockito.Mockito.when; import static org.junit.Assert.assertEquals; +import com.android.server.UiServiceTestCase; + @SmallTest @RunWith(AndroidJUnit4.class) -public class ImportanceExtractorTest extends NotificationTestCase { +public class ImportanceExtractorTest extends UiServiceTestCase { @Mock RankingConfig mConfig; diff --git a/services/tests/uiservicestests/src/com/android/server/notification/ManagedServicesTest.java b/services/tests/uiservicestests/src/com/android/server/notification/ManagedServicesTest.java index a4b9b256aa07..9ef0ec7ac99b 100644 --- a/services/tests/uiservicestests/src/com/android/server/notification/ManagedServicesTest.java +++ b/services/tests/uiservicestests/src/com/android/server/notification/ManagedServicesTest.java @@ -49,6 +49,7 @@ import android.util.ArrayMap; import android.util.Xml; import com.android.internal.util.FastXmlSerializer; +import com.android.server.UiServiceTestCase; import com.google.android.collect.Lists; @@ -68,7 +69,7 @@ import java.io.ByteArrayOutputStream; import java.util.ArrayList; import java.util.List; -public class ManagedServicesTest extends NotificationTestCase { +public class ManagedServicesTest extends UiServiceTestCase { @Mock private IPackageManager mIpm; diff --git a/services/tests/uiservicestests/src/com/android/server/notification/NotificationAdjustmentExtractorTest.java b/services/tests/uiservicestests/src/com/android/server/notification/NotificationAdjustmentExtractorTest.java index e52764450ee5..fd674f0c3858 100644 --- a/services/tests/uiservicestests/src/com/android/server/notification/NotificationAdjustmentExtractorTest.java +++ b/services/tests/uiservicestests/src/com/android/server/notification/NotificationAdjustmentExtractorTest.java @@ -31,12 +31,14 @@ import android.service.notification.Adjustment; import android.service.notification.SnoozeCriterion; import android.service.notification.StatusBarNotification; +import com.android.server.UiServiceTestCase; + import org.junit.Test; import java.util.ArrayList; import java.util.Objects; -public class NotificationAdjustmentExtractorTest extends NotificationTestCase { +public class NotificationAdjustmentExtractorTest extends UiServiceTestCase { @Test public void testExtractsAdjustment() { diff --git a/services/tests/uiservicestests/src/com/android/server/notification/NotificationChannelExtractorTest.java b/services/tests/uiservicestests/src/com/android/server/notification/NotificationChannelExtractorTest.java index d75213c3e773..eb45960bc82f 100644 --- a/services/tests/uiservicestests/src/com/android/server/notification/NotificationChannelExtractorTest.java +++ b/services/tests/uiservicestests/src/com/android/server/notification/NotificationChannelExtractorTest.java @@ -16,7 +16,6 @@ package com.android.server.notification; -import static android.app.NotificationManager.IMPORTANCE_DEFAULT; import static android.app.NotificationManager.IMPORTANCE_HIGH; import static android.app.NotificationManager.IMPORTANCE_LOW; @@ -31,17 +30,17 @@ import static org.mockito.Mockito.when; import android.app.Notification; import android.app.NotificationChannel; -import android.app.PendingIntent; -import android.content.Intent; import android.os.UserHandle; import android.service.notification.StatusBarNotification; +import com.android.server.UiServiceTestCase; + import org.junit.Before; import org.junit.Test; import org.mockito.Mock; import org.mockito.MockitoAnnotations; -public class NotificationChannelExtractorTest extends NotificationTestCase { +public class NotificationChannelExtractorTest extends UiServiceTestCase { @Mock RankingConfig mConfig; diff --git a/services/tests/uiservicestests/src/com/android/server/notification/NotificationChannelTest.java b/services/tests/uiservicestests/src/com/android/server/notification/NotificationChannelTest.java index f457f6a550c1..2241047dec0e 100644 --- a/services/tests/uiservicestests/src/com/android/server/notification/NotificationChannelTest.java +++ b/services/tests/uiservicestests/src/com/android/server/notification/NotificationChannelTest.java @@ -26,6 +26,7 @@ import android.support.test.runner.AndroidJUnit4; import android.test.suitebuilder.annotation.SmallTest; import com.android.internal.util.FastXmlSerializer; +import com.android.server.UiServiceTestCase; import org.junit.Test; import org.junit.runner.RunWith; @@ -36,7 +37,7 @@ import java.io.ByteArrayOutputStream; @SmallTest @RunWith(AndroidJUnit4.class) -public class NotificationChannelTest extends NotificationTestCase { +public class NotificationChannelTest extends UiServiceTestCase { @Test public void testWriteToParcel() { diff --git a/services/tests/uiservicestests/src/com/android/server/notification/NotificationComparatorTest.java b/services/tests/uiservicestests/src/com/android/server/notification/NotificationComparatorTest.java index 1e5f96f7be3a..3dcd5b9829da 100644 --- a/services/tests/uiservicestests/src/com/android/server/notification/NotificationComparatorTest.java +++ b/services/tests/uiservicestests/src/com/android/server/notification/NotificationComparatorTest.java @@ -38,6 +38,8 @@ import android.telecom.TelecomManager; import android.support.test.runner.AndroidJUnit4; import android.test.suitebuilder.annotation.SmallTest; +import com.android.server.UiServiceTestCase; + import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; @@ -50,7 +52,7 @@ import java.util.List; @SmallTest @RunWith(AndroidJUnit4.class) -public class NotificationComparatorTest extends NotificationTestCase { +public class NotificationComparatorTest extends UiServiceTestCase { @Mock Context mContext; @Mock TelecomManager mTm; @Mock RankingHandler handler; diff --git a/services/tests/uiservicestests/src/com/android/server/notification/NotificationIntrusivenessExtractorTest.java b/services/tests/uiservicestests/src/com/android/server/notification/NotificationIntrusivenessExtractorTest.java index 85852f90c281..00d93ded9a3c 100644 --- a/services/tests/uiservicestests/src/com/android/server/notification/NotificationIntrusivenessExtractorTest.java +++ b/services/tests/uiservicestests/src/com/android/server/notification/NotificationIntrusivenessExtractorTest.java @@ -32,9 +32,11 @@ import android.content.Intent; import android.os.UserHandle; import android.service.notification.StatusBarNotification; +import com.android.server.UiServiceTestCase; + import org.junit.Test; -public class NotificationIntrusivenessExtractorTest extends NotificationTestCase { +public class NotificationIntrusivenessExtractorTest extends UiServiceTestCase { @Test public void testNonIntrusive() { diff --git a/services/tests/uiservicestests/src/com/android/server/notification/NotificationListenerServiceTest.java b/services/tests/uiservicestests/src/com/android/server/notification/NotificationListenerServiceTest.java index d767ba2e8487..f4313b80c520 100644 --- a/services/tests/uiservicestests/src/com/android/server/notification/NotificationListenerServiceTest.java +++ b/services/tests/uiservicestests/src/com/android/server/notification/NotificationListenerServiceTest.java @@ -37,6 +37,8 @@ import android.service.notification.SnoozeCriterion; import android.support.test.runner.AndroidJUnit4; import android.test.suitebuilder.annotation.SmallTest; +import com.android.server.UiServiceTestCase; + import org.junit.Test; import org.junit.runner.RunWith; @@ -45,7 +47,7 @@ import java.util.List; @SmallTest @RunWith(AndroidJUnit4.class) -public class NotificationListenerServiceTest extends NotificationTestCase { +public class NotificationListenerServiceTest extends UiServiceTestCase { private String[] mKeys = new String[] { "key", "key1", "key2", "key3"}; diff --git a/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java b/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java index 55ec133a29da..fcc739911a56 100644 --- a/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java +++ b/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java @@ -83,6 +83,7 @@ import android.util.ArrayMap; import android.util.AtomicFile; import com.android.internal.statusbar.NotificationVisibility; +import com.android.server.UiServiceTestCase; import com.android.server.lights.Light; import com.android.server.lights.LightsManager; import com.android.server.notification.NotificationManagerService.NotificationAssistants; @@ -108,7 +109,7 @@ import java.util.Map; @SmallTest @RunWith(AndroidTestingRunner.class) @RunWithLooper -public class NotificationManagerServiceTest extends NotificationTestCase { +public class NotificationManagerServiceTest extends UiServiceTestCase { private static final String TEST_CHANNEL_ID = "NotificationManagerServiceTestChannelId"; private final int mUid = Binder.getCallingUid(); private NotificationManagerService mService; diff --git a/services/tests/uiservicestests/src/com/android/server/notification/NotificationRecordTest.java b/services/tests/uiservicestests/src/com/android/server/notification/NotificationRecordTest.java index ef26705aa71c..a5fa903c4f00 100644 --- a/services/tests/uiservicestests/src/com/android/server/notification/NotificationRecordTest.java +++ b/services/tests/uiservicestests/src/com/android/server/notification/NotificationRecordTest.java @@ -53,6 +53,7 @@ import android.test.suitebuilder.annotation.SmallTest; import com.android.internal.logging.nano.MetricsProto.MetricsEvent; +import com.android.server.UiServiceTestCase; import org.junit.Before; import org.junit.Test; @@ -65,7 +66,7 @@ import java.util.Objects; @SmallTest @RunWith(AndroidJUnit4.class) -public class NotificationRecordTest extends NotificationTestCase { +public class NotificationRecordTest extends UiServiceTestCase { private final Context mMockContext = Mockito.mock(Context.class); @Mock PackageManager mPm; diff --git a/services/tests/uiservicestests/src/com/android/server/notification/NotificationStatsTest.java b/services/tests/uiservicestests/src/com/android/server/notification/NotificationStatsTest.java index fec28115b418..4f153eed7326 100644 --- a/services/tests/uiservicestests/src/com/android/server/notification/NotificationStatsTest.java +++ b/services/tests/uiservicestests/src/com/android/server/notification/NotificationStatsTest.java @@ -11,12 +11,14 @@ import android.service.notification.NotificationStats; import android.support.test.runner.AndroidJUnit4; import android.test.suitebuilder.annotation.SmallTest; +import com.android.server.UiServiceTestCase; + import org.junit.Test; import org.junit.runner.RunWith; @SmallTest @RunWith(AndroidJUnit4.class) -public class NotificationStatsTest extends NotificationTestCase { +public class NotificationStatsTest extends UiServiceTestCase { @Test public void testConstructor() { diff --git a/services/tests/uiservicestests/src/com/android/server/notification/NotificationTest.java b/services/tests/uiservicestests/src/com/android/server/notification/NotificationTest.java index 4165e9e0aceb..4bfb2362988e 100644 --- a/services/tests/uiservicestests/src/com/android/server/notification/NotificationTest.java +++ b/services/tests/uiservicestests/src/com/android/server/notification/NotificationTest.java @@ -33,6 +33,8 @@ import android.os.Build; import android.support.test.filters.SmallTest; import android.support.test.runner.AndroidJUnit4; +import com.android.server.UiServiceTestCase; + import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; @@ -41,7 +43,7 @@ import org.mockito.MockitoAnnotations; @RunWith(AndroidJUnit4.class) @SmallTest -public class NotificationTest extends NotificationTestCase { +public class NotificationTest extends UiServiceTestCase { @Mock ActivityManager mAm; diff --git a/services/tests/uiservicestests/src/com/android/server/notification/NotificationTestCase.java b/services/tests/uiservicestests/src/com/android/server/notification/NotificationTestCase.java deleted file mode 100644 index 1ee34122f65a..000000000000 --- a/services/tests/uiservicestests/src/com/android/server/notification/NotificationTestCase.java +++ /dev/null @@ -1,33 +0,0 @@ -/* - * Copyright (C) 2017 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 com.android.server.notification; - -import android.content.Context; -import android.support.test.InstrumentationRegistry; -import android.testing.TestableContext; - -import org.junit.Rule; - - -public class NotificationTestCase { - @Rule - public final TestableContext mContext = - new TestableContext(InstrumentationRegistry.getContext(), null); - - protected TestableContext getContext() { - return mContext; - } -} diff --git a/services/tests/uiservicestests/src/com/android/server/notification/RankingHelperTest.java b/services/tests/uiservicestests/src/com/android/server/notification/RankingHelperTest.java index 2d03f111e528..abfc54d19068 100644 --- a/services/tests/uiservicestests/src/com/android/server/notification/RankingHelperTest.java +++ b/services/tests/uiservicestests/src/com/android/server/notification/RankingHelperTest.java @@ -65,6 +65,7 @@ import android.util.ArrayMap; import android.util.Xml; import com.android.internal.util.FastXmlSerializer; +import com.android.server.UiServiceTestCase; import org.json.JSONArray; import org.json.JSONObject; @@ -90,7 +91,7 @@ import java.util.concurrent.ThreadLocalRandom; @SmallTest @RunWith(AndroidJUnit4.class) -public class RankingHelperTest extends NotificationTestCase { +public class RankingHelperTest extends UiServiceTestCase { private static final String PKG = "com.android.server.notification"; private static final int UID = 0; private static final UserHandle USER = UserHandle.of(0); diff --git a/services/tests/uiservicestests/src/com/android/server/notification/RateEstimatorTest.java b/services/tests/uiservicestests/src/com/android/server/notification/RateEstimatorTest.java index e354267dce21..5d8d48f158fa 100644 --- a/services/tests/uiservicestests/src/com/android/server/notification/RateEstimatorTest.java +++ b/services/tests/uiservicestests/src/com/android/server/notification/RateEstimatorTest.java @@ -24,9 +24,11 @@ import org.junit.runner.RunWith; import static org.junit.Assert.assertTrue; import static org.junit.Assert.assertFalse; +import com.android.server.UiServiceTestCase; + @SmallTest @RunWith(AndroidJUnit4.class) -public class RateEstimatorTest extends NotificationTestCase { +public class RateEstimatorTest extends UiServiceTestCase { private long mTestStartTime; private RateEstimator mEstimator; diff --git a/services/tests/uiservicestests/src/com/android/server/notification/ScheduleCalendarTest.java b/services/tests/uiservicestests/src/com/android/server/notification/ScheduleCalendarTest.java index 5ebfd488eb97..9564ab9bdfee 100644 --- a/services/tests/uiservicestests/src/com/android/server/notification/ScheduleCalendarTest.java +++ b/services/tests/uiservicestests/src/com/android/server/notification/ScheduleCalendarTest.java @@ -27,6 +27,8 @@ import android.support.test.filters.FlakyTest; import android.support.test.runner.AndroidJUnit4; import android.test.suitebuilder.annotation.SmallTest; +import com.android.server.UiServiceTestCase; + import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; @@ -36,7 +38,7 @@ import java.util.GregorianCalendar; @SmallTest @RunWith(AndroidJUnit4.class) -public class ScheduleCalendarTest extends NotificationTestCase { +public class ScheduleCalendarTest extends UiServiceTestCase { private ScheduleCalendar mScheduleCalendar; private ZenModeConfig.ScheduleInfo mScheduleInfo; diff --git a/services/tests/uiservicestests/src/com/android/server/notification/ScheduleConditionProviderTest.java b/services/tests/uiservicestests/src/com/android/server/notification/ScheduleConditionProviderTest.java index 610592f6b71c..17fed83c4b02 100644 --- a/services/tests/uiservicestests/src/com/android/server/notification/ScheduleConditionProviderTest.java +++ b/services/tests/uiservicestests/src/com/android/server/notification/ScheduleConditionProviderTest.java @@ -13,6 +13,8 @@ import android.service.notification.ZenModeConfig; import android.support.test.filters.SmallTest; import android.support.test.runner.AndroidJUnit4; +import com.android.server.UiServiceTestCase; + import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; @@ -23,7 +25,7 @@ import java.util.GregorianCalendar; @RunWith(AndroidJUnit4.class) @SmallTest -public class ScheduleConditionProviderTest extends NotificationTestCase { +public class ScheduleConditionProviderTest extends UiServiceTestCase { ScheduleConditionProvider mService; diff --git a/services/tests/uiservicestests/src/com/android/server/notification/SnoozeHelperTest.java b/services/tests/uiservicestests/src/com/android/server/notification/SnoozeHelperTest.java index 07b21fbc8f5f..88c6fcf138cf 100644 --- a/services/tests/uiservicestests/src/com/android/server/notification/SnoozeHelperTest.java +++ b/services/tests/uiservicestests/src/com/android/server/notification/SnoozeHelperTest.java @@ -32,7 +32,6 @@ import android.os.UserHandle; import android.service.notification.StatusBarNotification; import android.support.test.runner.AndroidJUnit4; import android.test.suitebuilder.annotation.SmallTest; -import android.util.Slog; import static junit.framework.Assert.assertEquals; import static junit.framework.Assert.assertFalse; @@ -46,10 +45,12 @@ import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; +import com.android.server.UiServiceTestCase; + @SmallTest @RunWith(AndroidJUnit4.class) -public class SnoozeHelperTest extends NotificationTestCase { +public class SnoozeHelperTest extends UiServiceTestCase { private static final String TEST_CHANNEL_ID = "test_channel_id"; @Mock SnoozeHelper.Callback mCallback; diff --git a/services/tests/uiservicestests/src/com/android/server/notification/ValidateNotificationPeopleTest.java b/services/tests/uiservicestests/src/com/android/server/notification/ValidateNotificationPeopleTest.java index 4ac0c65791fd..58f0ded9b9a1 100644 --- a/services/tests/uiservicestests/src/com/android/server/notification/ValidateNotificationPeopleTest.java +++ b/services/tests/uiservicestests/src/com/android/server/notification/ValidateNotificationPeopleTest.java @@ -30,9 +30,11 @@ import org.junit.runner.RunWith; import static org.junit.Assert.assertNull; import static org.junit.Assert.assertEquals; +import com.android.server.UiServiceTestCase; + @SmallTest @RunWith(AndroidJUnit4.class) -public class ValidateNotificationPeopleTest extends NotificationTestCase { +public class ValidateNotificationPeopleTest extends UiServiceTestCase { @Test public void testNoExtra() throws Exception { diff --git a/services/tests/uiservicestests/src/com/android/server/notification/ZenModeHelperTest.java b/services/tests/uiservicestests/src/com/android/server/notification/ZenModeHelperTest.java index 8ac6481e69d9..0c7397afb879 100644 --- a/services/tests/uiservicestests/src/com/android/server/notification/ZenModeHelperTest.java +++ b/services/tests/uiservicestests/src/com/android/server/notification/ZenModeHelperTest.java @@ -32,6 +32,8 @@ import android.test.suitebuilder.annotation.SmallTest; import android.testing.AndroidTestingRunner; import android.testing.TestableLooper; +import com.android.server.UiServiceTestCase; + import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; @@ -41,7 +43,7 @@ import org.mockito.MockitoAnnotations; @SmallTest @RunWith(AndroidTestingRunner.class) @TestableLooper.RunWithLooper -public class ZenModeHelperTest extends NotificationTestCase { +public class ZenModeHelperTest extends UiServiceTestCase { @Mock ConditionProviders mConditionProviders; private TestableLooper mTestableLooper; diff --git a/services/tests/uiservicestests/src/com/android/server/slice/PinnedSliceStateTest.java b/services/tests/uiservicestests/src/com/android/server/slice/PinnedSliceStateTest.java new file mode 100644 index 000000000000..ce328c29f01c --- /dev/null +++ b/services/tests/uiservicestests/src/com/android/server/slice/PinnedSliceStateTest.java @@ -0,0 +1,214 @@ +package com.android.server.slice; + +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.argThat; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.clearInvocations; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import android.app.slice.ISliceListener; +import android.app.slice.Slice; +import android.app.slice.SliceProvider; +import android.app.slice.SliceSpec; +import android.content.ContentProvider; +import android.content.IContentProvider; +import android.net.Uri; +import android.os.Bundle; +import android.os.Handler; +import android.os.RemoteException; +import android.support.test.filters.SmallTest; +import android.testing.AndroidTestingRunner; +import android.testing.TestableLooper; +import android.testing.TestableLooper.RunWithLooper; + +import com.android.server.UiServiceTestCase; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; + +@SmallTest +@RunWith(AndroidTestingRunner.class) +@RunWithLooper +public class PinnedSliceStateTest extends UiServiceTestCase { + + private static final String AUTH = "my.authority"; + private static final Uri TEST_URI = Uri.parse("content://" + AUTH + "/path"); + + private static final SliceSpec[] FIRST_SPECS = new SliceSpec[]{ + new SliceSpec("spec1", 3), + new SliceSpec("spec2", 3), + new SliceSpec("spec3", 2), + new SliceSpec("spec4", 1), + }; + + private static final SliceSpec[] SECOND_SPECS = new SliceSpec[]{ + new SliceSpec("spec2", 1), + new SliceSpec("spec3", 2), + new SliceSpec("spec4", 3), + new SliceSpec("spec5", 4), + }; + + private SliceManagerService mSliceService; + private PinnedSliceState mPinnedSliceManager; + private IContentProvider mIContentProvider; + private ContentProvider mContentProvider; + + @Before + public void setup() { + mSliceService = mock(SliceManagerService.class); + when(mSliceService.getLock()).thenReturn(new Object()); + when(mSliceService.getContext()).thenReturn(mContext); + when(mSliceService.getHandler()).thenReturn(new Handler(TestableLooper.get(this).getLooper())); + mContentProvider = mock(ContentProvider.class); + mIContentProvider = mock(IContentProvider.class); + when(mContentProvider.getIContentProvider()).thenReturn(mIContentProvider); + mContext.getContentResolver().addProvider(AUTH, mContentProvider); + mPinnedSliceManager = new PinnedSliceState(mSliceService, TEST_URI); + } + + @Test + public void testMergeSpecs() { + // No annotations to start. + assertNull(mPinnedSliceManager.getSpecs()); + + mPinnedSliceManager.mergeSpecs(FIRST_SPECS); + assertArrayEquals(FIRST_SPECS, mPinnedSliceManager.getSpecs()); + + mPinnedSliceManager.mergeSpecs(SECOND_SPECS); + assertArrayEquals(new SliceSpec[]{ + // spec1 is gone because it's not in the second set. + new SliceSpec("spec2", 1), // spec2 is 1 because it's smaller in the second set. + new SliceSpec("spec3", 2), // spec3 is the same in both sets + new SliceSpec("spec4", 1), // spec4 is 1 because it's smaller in the first set. + // spec5 is gone because it's not in the first set. + }, mPinnedSliceManager.getSpecs()); + } + + @Test + public void testSendPinnedOnCreate() throws RemoteException { + // When created, a pinned message should be sent. + TestableLooper.get(this).processAllMessages(); + + verify(mIContentProvider).call(anyString(), eq(SliceProvider.METHOD_PIN), eq(null), + argThat(b -> { + assertEquals(TEST_URI, b.getParcelable(SliceProvider.EXTRA_BIND_URI)); + return true; + })); + } + + @Test + public void testSendUnpinnedOnDestroy() throws RemoteException { + TestableLooper.get(this).processAllMessages(); + clearInvocations(mIContentProvider); + + mPinnedSliceManager.destroy(); + TestableLooper.get(this).processAllMessages(); + + verify(mIContentProvider).call(anyString(), eq(SliceProvider.METHOD_UNPIN), eq(null), + argThat(b -> { + assertEquals(TEST_URI, b.getParcelable(SliceProvider.EXTRA_BIND_URI)); + return true; + })); + } + + @Test + public void testPkgPin() { + assertFalse(mPinnedSliceManager.isPinned()); + + mPinnedSliceManager.pin("pkg", FIRST_SPECS); + assertTrue(mPinnedSliceManager.isPinned()); + + assertTrue(mPinnedSliceManager.unpin("pkg")); + assertFalse(mPinnedSliceManager.isPinned()); + } + + @Test + public void testMultiPkgPin() { + assertFalse(mPinnedSliceManager.isPinned()); + + mPinnedSliceManager.pin("pkg", FIRST_SPECS); + assertTrue(mPinnedSliceManager.isPinned()); + mPinnedSliceManager.pin("pkg2", FIRST_SPECS); + + assertFalse(mPinnedSliceManager.unpin("pkg")); + assertTrue(mPinnedSliceManager.unpin("pkg2")); + assertFalse(mPinnedSliceManager.isPinned()); + } + + @Test + public void testListenerPin() { + ISliceListener listener = mock(ISliceListener.class); + assertFalse(mPinnedSliceManager.isPinned()); + + mPinnedSliceManager.addSliceListener(listener, FIRST_SPECS); + assertTrue(mPinnedSliceManager.isPinned()); + + assertTrue(mPinnedSliceManager.removeSliceListener(listener)); + assertFalse(mPinnedSliceManager.isPinned()); + } + + @Test + public void testMultiListenerPin() { + ISliceListener listener = mock(ISliceListener.class); + ISliceListener listener2 = mock(ISliceListener.class); + assertFalse(mPinnedSliceManager.isPinned()); + + mPinnedSliceManager.addSliceListener(listener, FIRST_SPECS); + assertTrue(mPinnedSliceManager.isPinned()); + mPinnedSliceManager.addSliceListener(listener2, FIRST_SPECS); + + assertFalse(mPinnedSliceManager.removeSliceListener(listener)); + assertTrue(mPinnedSliceManager.removeSliceListener(listener2)); + assertFalse(mPinnedSliceManager.isPinned()); + } + + @Test + public void testPkgListenerPin() { + ISliceListener listener = mock(ISliceListener.class); + assertFalse(mPinnedSliceManager.isPinned()); + + mPinnedSliceManager.addSliceListener(listener, FIRST_SPECS); + assertTrue(mPinnedSliceManager.isPinned()); + mPinnedSliceManager.pin("pkg", FIRST_SPECS); + + assertFalse(mPinnedSliceManager.removeSliceListener(listener)); + assertTrue(mPinnedSliceManager.unpin("pkg")); + assertFalse(mPinnedSliceManager.isPinned()); + } + + @Test + public void testBind() throws RemoteException { + TestableLooper.get(this).processAllMessages(); + clearInvocations(mIContentProvider); + + ISliceListener listener = mock(ISliceListener.class); + Slice s = new Slice.Builder(TEST_URI).build(); + Bundle b = new Bundle(); + b.putParcelable(SliceProvider.EXTRA_SLICE, s); + when(mIContentProvider.call(anyString(), eq(SliceProvider.METHOD_SLICE), eq(null), + any())).thenReturn(b); + + assertFalse(mPinnedSliceManager.isPinned()); + + mPinnedSliceManager.addSliceListener(listener, FIRST_SPECS); + + mPinnedSliceManager.onChange(); + TestableLooper.get(this).processAllMessages(); + + verify(mIContentProvider).call(anyString(), eq(SliceProvider.METHOD_SLICE), eq(null), + argThat(bundle -> { + assertEquals(TEST_URI, bundle.getParcelable(SliceProvider.EXTRA_BIND_URI)); + return true; + })); + verify(listener).onSliceUpdated(eq(s)); + } +}
\ No newline at end of file diff --git a/services/tests/uiservicestests/src/com/android/server/slice/SliceManagerServiceTest.java b/services/tests/uiservicestests/src/com/android/server/slice/SliceManagerServiceTest.java new file mode 100644 index 000000000000..fe9ea7a1bb9b --- /dev/null +++ b/services/tests/uiservicestests/src/com/android/server/slice/SliceManagerServiceTest.java @@ -0,0 +1,135 @@ +/* + * Copyright (C) 2017 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 com.android.server.slice; + +import static android.content.ContentProvider.maybeAddUserId; +import static android.content.pm.PackageManager.PERMISSION_GRANTED; + +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import android.app.AppOpsManager; +import android.app.slice.ISliceListener; +import android.app.slice.SliceSpec; +import android.content.pm.PackageManagerInternal; +import android.net.Uri; +import android.os.RemoteException; +import android.support.test.filters.SmallTest; +import android.testing.AndroidTestingRunner; +import android.testing.TestableLooper; +import android.testing.TestableLooper.RunWithLooper; + +import com.android.server.LocalServices; +import com.android.server.UiServiceTestCase; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; + +@SmallTest +@RunWith(AndroidTestingRunner.class) +@RunWithLooper +public class SliceManagerServiceTest extends UiServiceTestCase { + + private static final String AUTH = "com.android.services.uitests"; + private static final Uri TEST_URI = maybeAddUserId(Uri.parse("content://" + AUTH + "/path"), 0); + + private static final SliceSpec[] EMPTY_SPECS = new SliceSpec[]{ + }; + + private SliceManagerService mService; + private PinnedSliceState mCreatedSliceState; + + @Before + public void setup() { + LocalServices.addService(PackageManagerInternal.class, mock(PackageManagerInternal.class)); + mContext.addMockSystemService(AppOpsManager.class, mock(AppOpsManager.class)); + mContext.getTestablePermissions().setPermission(TEST_URI, PERMISSION_GRANTED); + + mService = spy(new SliceManagerService(mContext, TestableLooper.get(this).getLooper())); + mCreatedSliceState = mock(PinnedSliceState.class); + doReturn(mCreatedSliceState).when(mService).createPinnedSlice(eq(TEST_URI)); + } + + @After + public void teardown() { + LocalServices.removeServiceForTest(PackageManagerInternal.class); + } + + @Test + public void testAddListenerCreatesPinned() throws RemoteException { + mService.addSliceListener(TEST_URI, "pkg", mock(ISliceListener.class), EMPTY_SPECS); + verify(mService, times(1)).createPinnedSlice(eq(TEST_URI)); + } + + @Test + public void testAddListenerCreatesOnePinned() throws RemoteException { + mService.addSliceListener(TEST_URI, "pkg", mock(ISliceListener.class), EMPTY_SPECS); + mService.addSliceListener(TEST_URI, "pkg", mock(ISliceListener.class), EMPTY_SPECS); + verify(mService, times(1)).createPinnedSlice(eq(TEST_URI)); + } + + @Test + public void testRemoveListenerDestroysPinned() throws RemoteException { + ISliceListener listener = mock(ISliceListener.class); + mService.addSliceListener(TEST_URI, "pkg", listener, EMPTY_SPECS); + + when(mCreatedSliceState.removeSliceListener(eq(listener))).thenReturn(false); + mService.removeSliceListener(TEST_URI, "pkg", listener); + verify(mCreatedSliceState, never()).destroy(); + + when(mCreatedSliceState.removeSliceListener(eq(listener))).thenReturn(true); + mService.removeSliceListener(TEST_URI, "pkg", listener); + verify(mCreatedSliceState).destroy(); + } + + @Test(expected = IllegalStateException.class) + public void testUnrecognizedThrows() throws RemoteException { + mService.removeSliceListener(TEST_URI, "pkg", mock(ISliceListener.class)); + } + + @Test + public void testAddPinCreatesPinned() throws RemoteException { + doReturn("pkg").when(mService).getDefaultHome(anyInt()); + + mService.pinSlice("pkg", TEST_URI, EMPTY_SPECS); + mService.pinSlice("pkg", TEST_URI, EMPTY_SPECS); + verify(mService, times(1)).createPinnedSlice(eq(TEST_URI)); + } + + @Test + public void testRemovePinDestroysPinned() throws RemoteException { + doReturn("pkg").when(mService).getDefaultHome(anyInt()); + + mService.pinSlice("pkg", TEST_URI, EMPTY_SPECS); + + when(mCreatedSliceState.unpin(eq("pkg"))).thenReturn(false); + mService.unpinSlice("pkg", TEST_URI); + verify(mCreatedSliceState, never()).destroy(); + + when(mCreatedSliceState.unpin(eq("pkg"))).thenReturn(true); + mService.unpinSlice("pkg", TEST_URI); + verify(mCreatedSliceState).destroy(); + } + +}
\ No newline at end of file |