Merge "Give SliceManagerService a concept of pinned slices."

This commit is contained in:
TreeHugger Robot
2017-12-14 04:09:55 +00:00
committed by Android (Google) Code Review
42 changed files with 1268 additions and 72 deletions

View File

@@ -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",

View File

@@ -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 \

View File

@@ -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);
}

View File

@@ -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);
}

View File

@@ -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;

View File

@@ -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));
}
};
}
}

View File

@@ -102,6 +102,14 @@ public abstract class SliceProvider extends ContentProvider {
* @hide
*/
public static final String METHOD_MAP_INTENT = "map_slice";
/**
* @hide
*/
public static final String METHOD_PIN = "pin";
/**
* @hide
*/
public static final String METHOD_UNPIN = "unpin";
/**
* @hide
*/
@@ -142,6 +150,18 @@ public abstract class SliceProvider extends ContentProvider {
return null;
}
/**
* @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
@@ -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);

View File

@@ -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;

View File

@@ -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) {

View File

@@ -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);
}
}
}
}

View File

@@ -16,21 +16,307 @@
package com.android.server.slice;
import android.app.slice.ISliceManager;
import android.content.Context;
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 {
public SliceManagerService(Context context) {
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);
}
}
}

View File

@@ -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 \

View File

@@ -28,6 +28,9 @@
<application>
<uses-library android:name="android.test.runner" />
<provider android:name=".DummyProvider"
android:authorities="com.android.services.uitests" />
</application>
<instrumentation

View File

@@ -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;
}
}

View File

@@ -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");
}
}

View File

@@ -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

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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() {

View File

@@ -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;

View File

@@ -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() {

View File

@@ -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;

View File

@@ -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() {

View File

@@ -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"};

View File

@@ -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;
@@ -110,7 +111,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;

View File

@@ -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;

View File

@@ -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() {

View File

@@ -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;

View File

@@ -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;
}
}

View File

@@ -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);

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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 {

View File

@@ -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;

View File

@@ -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));
}
}

View File

@@ -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();
}
}