/*
|
* 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.app.usage.UsageEvents.Event.SLICE_PINNED;
|
import static android.app.usage.UsageEvents.Event.SLICE_PINNED_PRIV;
|
import static android.content.ContentProvider.getUriWithoutUserId;
|
import static android.content.ContentProvider.getUserIdFromUri;
|
import static android.content.ContentProvider.maybeAddUserId;
|
import static android.content.pm.PackageManager.PERMISSION_DENIED;
|
import static android.content.pm.PackageManager.PERMISSION_GRANTED;
|
import static android.os.Process.SYSTEM_UID;
|
|
import android.Manifest.permission;
|
import android.app.AppOpsManager;
|
import android.app.slice.ISliceManager;
|
import android.app.slice.SliceSpec;
|
import android.app.usage.UsageStatsManagerInternal;
|
import android.content.BroadcastReceiver;
|
import android.content.ComponentName;
|
import android.content.ContentProvider;
|
import android.content.ContentResolver;
|
import android.content.Context;
|
import android.content.Intent;
|
import android.content.IntentFilter;
|
import android.content.pm.PackageManager;
|
import android.content.pm.PackageManagerInternal;
|
import android.content.pm.ProviderInfo;
|
import android.content.pm.ResolveInfo;
|
import android.net.Uri;
|
import android.os.Binder;
|
import android.os.Handler;
|
import android.os.IBinder;
|
import android.os.Looper;
|
import android.os.Process;
|
import android.os.RemoteException;
|
import android.os.ResultReceiver;
|
import android.os.ShellCallback;
|
import android.os.UserHandle;
|
import android.util.ArrayMap;
|
import android.util.Slog;
|
import android.util.SparseArray;
|
import android.util.Xml.Encoding;
|
|
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 org.xmlpull.v1.XmlPullParser;
|
import org.xmlpull.v1.XmlPullParserException;
|
import org.xmlpull.v1.XmlPullParserFactory;
|
import org.xmlpull.v1.XmlSerializer;
|
|
import java.io.ByteArrayInputStream;
|
import java.io.ByteArrayOutputStream;
|
import java.io.FileDescriptor;
|
import java.io.IOException;
|
import java.util.ArrayList;
|
import java.util.List;
|
import java.util.Objects;
|
import java.util.function.Supplier;
|
|
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<>();
|
@GuardedBy("mLock")
|
private final SparseArray<PackageMatchingCache> mAssistantLookup = new SparseArray<>();
|
@GuardedBy("mLock")
|
private final SparseArray<PackageMatchingCache> mHomeLookup = new SparseArray<>();
|
private final Handler mHandler;
|
|
private final SlicePermissionManager mPermissions;
|
private final UsageStatsManagerInternal mAppUsageStats;
|
|
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);
|
|
mAppUsageStats = LocalServices.getService(UsageStatsManagerInternal.class);
|
|
mPermissions = new SlicePermissionManager(mContext, looper);
|
|
IntentFilter filter = new IntentFilter();
|
filter.addAction(Intent.ACTION_PACKAGE_DATA_CLEARED);
|
filter.addAction(Intent.ACTION_PACKAGE_REMOVED);
|
filter.addDataScheme("package");
|
mContext.registerReceiverAsUser(mReceiver, UserHandle.ALL, filter, null, mHandler);
|
}
|
|
/// ----- Lifecycle stuff -----
|
private void systemReady() {
|
}
|
|
private void onUnlockUser(int userId) {
|
}
|
|
private void onStopUser(int userId) {
|
synchronized (mLock) {
|
mPinnedSlicesByUri.values().removeIf(s -> getUserIdFromUri(s.getUri()) == userId);
|
}
|
}
|
|
/// ----- ISliceManager stuff -----
|
@Override
|
public Uri[] getPinnedSlices(String pkg) {
|
verifyCaller(pkg);
|
int callingUser = Binder.getCallingUserHandle().getIdentifier();
|
ArrayList<Uri> ret = new ArrayList<>();
|
synchronized (mLock) {
|
for (PinnedSliceState state : mPinnedSlicesByUri.values()) {
|
if (Objects.equals(pkg, state.getPkg())) {
|
Uri uri = state.getUri();
|
int userId = ContentProvider.getUserIdFromUri(uri, callingUser);
|
if (userId == callingUser) {
|
ret.add(ContentProvider.getUriWithoutUserId(uri));
|
}
|
}
|
}
|
}
|
return ret.toArray(new Uri[ret.size()]);
|
}
|
|
@Override
|
public void pinSlice(String pkg, Uri uri, SliceSpec[] specs, IBinder token)
|
throws RemoteException {
|
verifyCaller(pkg);
|
enforceAccess(pkg, uri);
|
int user = Binder.getCallingUserHandle().getIdentifier();
|
uri = maybeAddUserId(uri, user);
|
String slicePkg = getProviderPkg(uri, user);
|
getOrCreatePinnedSlice(uri, slicePkg).pin(pkg, specs, token);
|
|
mHandler.post(() -> {
|
if (slicePkg != null && !Objects.equals(pkg, slicePkg)) {
|
mAppUsageStats.reportEvent(slicePkg, user,
|
isAssistant(pkg, user) || isDefaultHomeApp(pkg, user)
|
? SLICE_PINNED_PRIV : SLICE_PINNED);
|
}
|
});
|
}
|
|
@Override
|
public void unpinSlice(String pkg, Uri uri, IBinder token) throws RemoteException {
|
verifyCaller(pkg);
|
enforceAccess(pkg, uri);
|
uri = maybeAddUserId(uri, Binder.getCallingUserHandle().getIdentifier());
|
if (getPinnedSlice(uri).unpin(pkg, token)) {
|
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);
|
uri = maybeAddUserId(uri, Binder.getCallingUserHandle().getIdentifier());
|
return getPinnedSlice(uri).getSpecs();
|
}
|
|
@Override
|
public void grantSlicePermission(String pkg, String toPkg, Uri uri) throws RemoteException {
|
verifyCaller(pkg);
|
int user = Binder.getCallingUserHandle().getIdentifier();
|
enforceOwner(pkg, uri, user);
|
mPermissions.grantSliceAccess(toPkg, user, pkg, user, uri);
|
}
|
|
@Override
|
public void revokeSlicePermission(String pkg, String toPkg, Uri uri) throws RemoteException {
|
verifyCaller(pkg);
|
int user = Binder.getCallingUserHandle().getIdentifier();
|
enforceOwner(pkg, uri, user);
|
mPermissions.revokeSliceAccess(toPkg, user, pkg, user, uri);
|
}
|
|
@Override
|
public int checkSlicePermission(Uri uri, String callingPkg, String pkg, int pid, int uid,
|
String[] autoGrantPermissions) {
|
int userId = UserHandle.getUserId(uid);
|
if (pkg == null) {
|
for (String p : mContext.getPackageManager().getPackagesForUid(uid)) {
|
if (checkSlicePermission(uri, callingPkg, p, pid, uid, autoGrantPermissions)
|
== PERMISSION_GRANTED) {
|
return PERMISSION_GRANTED;
|
}
|
}
|
return PERMISSION_DENIED;
|
}
|
if (hasFullSliceAccess(pkg, userId)) {
|
return PackageManager.PERMISSION_GRANTED;
|
}
|
if (mPermissions.hasPermission(pkg, userId, uri)) {
|
return PackageManager.PERMISSION_GRANTED;
|
}
|
if (autoGrantPermissions != null && callingPkg != null) {
|
// Need to own the Uri to call in with permissions to grant.
|
enforceOwner(callingPkg, uri, userId);
|
for (String perm : autoGrantPermissions) {
|
if (mContext.checkPermission(perm, pid, uid) == PERMISSION_GRANTED) {
|
int providerUser = ContentProvider.getUserIdFromUri(uri, userId);
|
String providerPkg = getProviderPkg(uri, providerUser);
|
mPermissions.grantSliceAccess(pkg, userId, providerPkg, providerUser, uri);
|
return PackageManager.PERMISSION_GRANTED;
|
}
|
}
|
}
|
// Fallback to allowing uri permissions through.
|
if (mContext.checkUriPermission(uri, pid, uid, Intent.FLAG_GRANT_WRITE_URI_PERMISSION)
|
== PERMISSION_GRANTED) {
|
return PackageManager.PERMISSION_GRANTED;
|
}
|
return PackageManager.PERMISSION_DENIED;
|
}
|
|
@Override
|
public void grantPermissionFromUser(Uri uri, String pkg, String callingPkg, boolean allSlices) {
|
verifyCaller(callingPkg);
|
getContext().enforceCallingOrSelfPermission(permission.MANAGE_SLICE_PERMISSIONS,
|
"Slice granting requires MANAGE_SLICE_PERMISSIONS");
|
int userId = Binder.getCallingUserHandle().getIdentifier();
|
if (allSlices) {
|
mPermissions.grantFullAccess(pkg, userId);
|
} else {
|
// When granting, grant to all slices in the provider.
|
Uri grantUri = uri.buildUpon()
|
.path("")
|
.build();
|
int providerUser = ContentProvider.getUserIdFromUri(grantUri, userId);
|
String providerPkg = getProviderPkg(grantUri, providerUser);
|
mPermissions.grantSliceAccess(pkg, userId, providerPkg, providerUser, grantUri);
|
}
|
long ident = Binder.clearCallingIdentity();
|
try {
|
mContext.getContentResolver().notifyChange(uri, null);
|
} finally {
|
Binder.restoreCallingIdentity(ident);
|
}
|
}
|
|
// Backup/restore interface
|
@Override
|
public byte[] getBackupPayload(int user) {
|
if (Binder.getCallingUid() != SYSTEM_UID) {
|
throw new SecurityException("Caller must be system");
|
}
|
//TODO: http://b/22388012
|
if (user != UserHandle.USER_SYSTEM) {
|
Slog.w(TAG, "getBackupPayload: cannot backup policy for user " + user);
|
return null;
|
}
|
final ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
try {
|
XmlSerializer out = XmlPullParserFactory.newInstance().newSerializer();
|
out.setOutput(baos, Encoding.UTF_8.name());
|
|
mPermissions.writeBackup(out);
|
|
out.flush();
|
return baos.toByteArray();
|
} catch (IOException | XmlPullParserException e) {
|
Slog.w(TAG, "getBackupPayload: error writing payload for user " + user, e);
|
}
|
return null;
|
}
|
|
@Override
|
public void applyRestore(byte[] payload, int user) {
|
if (Binder.getCallingUid() != SYSTEM_UID) {
|
throw new SecurityException("Caller must be system");
|
}
|
if (payload == null) {
|
Slog.w(TAG, "applyRestore: no payload to restore for user " + user);
|
return;
|
}
|
//TODO: http://b/22388012
|
if (user != UserHandle.USER_SYSTEM) {
|
Slog.w(TAG, "applyRestore: cannot restore policy for user " + user);
|
return;
|
}
|
final ByteArrayInputStream bais = new ByteArrayInputStream(payload);
|
try {
|
XmlPullParser parser = XmlPullParserFactory.newInstance().newPullParser();
|
parser.setInput(bais, Encoding.UTF_8.name());
|
mPermissions.readRestore(parser);
|
} catch (NumberFormatException | XmlPullParserException | IOException e) {
|
Slog.w(TAG, "applyRestore: error reading payload", e);
|
}
|
}
|
|
@Override
|
public void onShellCommand(FileDescriptor in, FileDescriptor out, FileDescriptor err,
|
String[] args, ShellCallback callback, ResultReceiver resultReceiver) {
|
new SliceShellCommand(this).exec(this, in, out, err, args, callback, resultReceiver);
|
}
|
|
/// ----- internal code -----
|
private void enforceOwner(String pkg, Uri uri, int user) {
|
if (!Objects.equals(getProviderPkg(uri, user), pkg) || pkg == null) {
|
throw new SecurityException("Caller must own " + uri);
|
}
|
}
|
|
protected 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, String pkg) {
|
synchronized (mLock) {
|
PinnedSliceState manager = mPinnedSlicesByUri.get(uri);
|
if (manager == null) {
|
manager = createPinnedSlice(uri, pkg);
|
mPinnedSlicesByUri.put(uri, manager);
|
}
|
return manager;
|
}
|
}
|
|
@VisibleForTesting
|
protected PinnedSliceState createPinnedSlice(Uri uri, String pkg) {
|
return new PinnedSliceState(this, uri, pkg);
|
}
|
|
public Object getLock() {
|
return mLock;
|
}
|
|
public Context getContext() {
|
return mContext;
|
}
|
|
public Handler getHandler() {
|
return mHandler;
|
}
|
|
protected int checkAccess(String pkg, Uri uri, int uid, int pid) {
|
return checkSlicePermission(uri, null, pkg, pid, uid, null);
|
}
|
|
private String getProviderPkg(Uri uri, int user) {
|
long ident = Binder.clearCallingIdentity();
|
try {
|
String providerName = getUriWithoutUserId(uri).getAuthority();
|
ProviderInfo provider = mContext.getPackageManager().resolveContentProviderAsUser(
|
providerName, 0, getUserIdFromUri(uri, user));
|
return provider.packageName;
|
} finally {
|
Binder.restoreCallingIdentity(ident);
|
}
|
}
|
|
private void enforceCrossUser(String pkg, Uri uri) {
|
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 enforceAccess(String pkg, Uri uri) throws RemoteException {
|
if (checkAccess(pkg, uri, Binder.getCallingUid(), Binder.getCallingPid())
|
!= PERMISSION_GRANTED) {
|
int userId = ContentProvider.getUserIdFromUri(uri,
|
Binder.getCallingUserHandle().getIdentifier());
|
if (!Objects.equals(pkg, getProviderPkg(uri, userId))) {
|
throw new SecurityException("Access to slice " + uri + " is required");
|
}
|
}
|
enforceCrossUser(pkg, uri);
|
}
|
|
private void verifyCaller(String pkg) {
|
mAppOps.checkPackage(Binder.getCallingUid(), pkg);
|
}
|
|
private boolean hasFullSliceAccess(String pkg, int userId) {
|
long ident = Binder.clearCallingIdentity();
|
try {
|
boolean ret = isDefaultHomeApp(pkg, userId) || isAssistant(pkg, userId)
|
|| isGrantedFullAccess(pkg, userId);
|
return ret;
|
} finally {
|
Binder.restoreCallingIdentity(ident);
|
}
|
}
|
|
private boolean isAssistant(String pkg, int userId) {
|
return getAssistantMatcher(userId).matches(pkg);
|
}
|
|
private boolean isDefaultHomeApp(String pkg, int userId) {
|
return getHomeMatcher(userId).matches(pkg);
|
}
|
|
private PackageMatchingCache getAssistantMatcher(int userId) {
|
PackageMatchingCache matcher = mAssistantLookup.get(userId);
|
if (matcher == null) {
|
matcher = new PackageMatchingCache(() -> getAssistant(userId));
|
mAssistantLookup.put(userId, matcher);
|
}
|
return matcher;
|
}
|
|
private PackageMatchingCache getHomeMatcher(int userId) {
|
PackageMatchingCache matcher = mHomeLookup.get(userId);
|
if (matcher == null) {
|
matcher = new PackageMatchingCache(() -> getDefaultHome(userId));
|
mHomeLookup.put(userId, matcher);
|
}
|
return matcher;
|
}
|
|
private String getAssistant(int userId) {
|
final ComponentName cn = mAssistUtils.getAssistComponentForUser(userId);
|
if (cn == null) {
|
return null;
|
}
|
return cn.getPackageName();
|
}
|
|
// Based on getDefaultHome in ShortcutService.
|
// TODO: Unify if possible
|
@VisibleForTesting
|
protected 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 != null ? detected.getPackageName() : null;
|
} finally {
|
Binder.restoreCallingIdentity(token);
|
}
|
}
|
|
private boolean isGrantedFullAccess(String pkg, int userId) {
|
return mPermissions.hasFullAccess(pkg, userId);
|
}
|
|
private static ServiceThread createHandler() {
|
ServiceThread handlerThread = new ServiceThread(TAG,
|
Process.THREAD_PRIORITY_BACKGROUND, true /*allowIo*/);
|
handlerThread.start();
|
return handlerThread;
|
}
|
|
private final BroadcastReceiver mReceiver = new BroadcastReceiver() {
|
@Override
|
public void onReceive(Context context, Intent intent) {
|
final int userId = intent.getIntExtra(Intent.EXTRA_USER_HANDLE, UserHandle.USER_NULL);
|
if (userId == UserHandle.USER_NULL) {
|
Slog.w(TAG, "Intent broadcast does not contain user handle: " + intent);
|
return;
|
}
|
Uri data = intent.getData();
|
String pkg = data != null ? data.getSchemeSpecificPart() : null;
|
if (pkg == null) {
|
Slog.w(TAG, "Intent broadcast does not contain package name: " + intent);
|
return;
|
}
|
switch (intent.getAction()) {
|
case Intent.ACTION_PACKAGE_REMOVED:
|
final boolean replacing =
|
intent.getBooleanExtra(Intent.EXTRA_REPLACING, false);
|
if (!replacing) {
|
mPermissions.removePkg(pkg, userId);
|
}
|
break;
|
case Intent.ACTION_PACKAGE_DATA_CLEARED:
|
mPermissions.removePkg(pkg, userId);
|
break;
|
}
|
}
|
};
|
|
public String[] getAllPackagesGranted(String authority) {
|
String pkg = getProviderPkg(new Uri.Builder()
|
.scheme(ContentResolver.SCHEME_CONTENT)
|
.authority(authority)
|
.build(), 0);
|
return mPermissions.getAllPackagesGranted(pkg);
|
}
|
|
/**
|
* Holder that caches a package that has access to a slice.
|
*/
|
static class PackageMatchingCache {
|
|
private final Supplier<String> mPkgSource;
|
private String mCurrentPkg;
|
|
public PackageMatchingCache(Supplier<String> pkgSource) {
|
mPkgSource = pkgSource;
|
}
|
|
public boolean matches(String pkgCandidate) {
|
if (pkgCandidate == null) return false;
|
|
if (Objects.equals(pkgCandidate, mCurrentPkg)) {
|
return true;
|
}
|
// Failed on cached value, try updating.
|
mCurrentPkg = mPkgSource.get();
|
return Objects.equals(pkgCandidate, mCurrentPkg);
|
}
|
}
|
|
public static class Lifecycle extends SystemService {
|
private SliceManagerService mService;
|
|
public Lifecycle(Context context) {
|
super(context);
|
}
|
|
@Override
|
public void onStart() {
|
mService = new SliceManagerService(getContext());
|
publishBinderService(Context.SLICE_SERVICE, mService);
|
}
|
|
@Override
|
public void onBootPhase(int phase) {
|
if (phase == SystemService.PHASE_ACTIVITY_MANAGER_READY) {
|
mService.systemReady();
|
}
|
}
|
|
@Override
|
public void onUnlockUser(int userHandle) {
|
mService.onUnlockUser(userHandle);
|
}
|
|
@Override
|
public void onStopUser(int userHandle) {
|
mService.onStopUser(userHandle);
|
}
|
}
|
|
private class SliceGrant {
|
private final Uri mUri;
|
private final String mPkg;
|
private final int mUserId;
|
|
public SliceGrant(Uri uri, String pkg, int userId) {
|
mUri = uri;
|
mPkg = pkg;
|
mUserId = userId;
|
}
|
|
@Override
|
public int hashCode() {
|
return mUri.hashCode() + mPkg.hashCode();
|
}
|
|
@Override
|
public boolean equals(Object obj) {
|
if (!(obj instanceof SliceGrant)) return false;
|
SliceGrant other = (SliceGrant) obj;
|
return Objects.equals(other.mUri, mUri) && Objects.equals(other.mPkg, mPkg)
|
&& (other.mUserId == mUserId);
|
}
|
}
|
}
|