Jason Monk | 8f5f7ff | 2017-10-17 14:12:42 -0400 | [diff] [blame] | 1 | /* |
| 2 | * Copyright (C) 2017 The Android Open Source Project |
| 3 | * |
| 4 | * Licensed under the Apache License, Version 2.0 (the "License"); |
| 5 | * you may not use this file except in compliance with the License. |
| 6 | * You may obtain a copy of the License at |
| 7 | * |
| 8 | * http://www.apache.org/licenses/LICENSE-2.0 |
| 9 | * |
| 10 | * Unless required by applicable law or agreed to in writing, software |
| 11 | * distributed under the License is distributed on an "AS IS" BASIS, |
| 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| 13 | * See the License for the specific language governing permissions and |
| 14 | * limitations under the License. |
| 15 | */ |
| 16 | |
| 17 | package com.android.server.slice; |
| 18 | |
Jason Monk | 74f5e36 | 2017-12-06 08:56:33 -0500 | [diff] [blame] | 19 | import static android.content.ContentProvider.getUserIdFromUri; |
| 20 | import static android.content.ContentProvider.maybeAddUserId; |
Jason Monk | 8f5f7ff | 2017-10-17 14:12:42 -0400 | [diff] [blame] | 21 | |
Jason Monk | 74f5e36 | 2017-12-06 08:56:33 -0500 | [diff] [blame] | 22 | import android.Manifest.permission; |
| 23 | import android.app.AppOpsManager; |
| 24 | import android.app.slice.ISliceListener; |
| 25 | import android.app.slice.ISliceManager; |
| 26 | import android.app.slice.SliceSpec; |
| 27 | import android.content.ComponentName; |
| 28 | import android.content.Context; |
| 29 | import android.content.Intent; |
| 30 | import android.content.pm.PackageManagerInternal; |
| 31 | import android.content.pm.ResolveInfo; |
| 32 | import android.database.ContentObserver; |
| 33 | import android.net.Uri; |
| 34 | import android.os.Binder; |
| 35 | import android.os.Handler; |
| 36 | import android.os.Looper; |
| 37 | import android.os.Process; |
| 38 | import android.os.RemoteException; |
| 39 | import android.util.ArrayMap; |
| 40 | import android.util.Log; |
| 41 | |
| 42 | import com.android.internal.annotations.GuardedBy; |
| 43 | import com.android.internal.annotations.VisibleForTesting; |
| 44 | import com.android.internal.app.AssistUtils; |
| 45 | import com.android.internal.util.Preconditions; |
| 46 | import com.android.server.LocalServices; |
| 47 | import com.android.server.ServiceThread; |
Jason Monk | 8f5f7ff | 2017-10-17 14:12:42 -0400 | [diff] [blame] | 48 | import com.android.server.SystemService; |
| 49 | |
Jason Monk | 74f5e36 | 2017-12-06 08:56:33 -0500 | [diff] [blame] | 50 | import java.util.ArrayList; |
| 51 | import java.util.List; |
| 52 | import java.util.Objects; |
| 53 | |
Jason Monk | 8f5f7ff | 2017-10-17 14:12:42 -0400 | [diff] [blame] | 54 | public class SliceManagerService extends ISliceManager.Stub { |
| 55 | |
Jason Monk | 74f5e36 | 2017-12-06 08:56:33 -0500 | [diff] [blame] | 56 | private static final String TAG = "SliceManagerService"; |
| 57 | private final Object mLock = new Object(); |
Jason Monk | 8f5f7ff | 2017-10-17 14:12:42 -0400 | [diff] [blame] | 58 | |
Jason Monk | 74f5e36 | 2017-12-06 08:56:33 -0500 | [diff] [blame] | 59 | private final Context mContext; |
| 60 | private final PackageManagerInternal mPackageManagerInternal; |
| 61 | private final AppOpsManager mAppOps; |
| 62 | private final AssistUtils mAssistUtils; |
| 63 | |
| 64 | @GuardedBy("mLock") |
| 65 | private final ArrayMap<Uri, PinnedSliceState> mPinnedSlicesByUri = new ArrayMap<>(); |
| 66 | private final Handler mHandler; |
| 67 | private final ContentObserver mObserver; |
| 68 | |
| 69 | public SliceManagerService(Context context) { |
| 70 | this(context, createHandler().getLooper()); |
Jason Monk | 8f5f7ff | 2017-10-17 14:12:42 -0400 | [diff] [blame] | 71 | } |
| 72 | |
Jason Monk | 74f5e36 | 2017-12-06 08:56:33 -0500 | [diff] [blame] | 73 | @VisibleForTesting |
| 74 | SliceManagerService(Context context, Looper looper) { |
| 75 | mContext = context; |
| 76 | mPackageManagerInternal = Preconditions.checkNotNull( |
| 77 | LocalServices.getService(PackageManagerInternal.class)); |
| 78 | mAppOps = context.getSystemService(AppOpsManager.class); |
| 79 | mAssistUtils = new AssistUtils(context); |
| 80 | mHandler = new Handler(looper); |
| 81 | |
| 82 | mObserver = new ContentObserver(mHandler) { |
| 83 | @Override |
| 84 | public void onChange(boolean selfChange, Uri uri) { |
| 85 | try { |
| 86 | getPinnedSlice(uri).onChange(); |
| 87 | } catch (IllegalStateException e) { |
| 88 | Log.e(TAG, "Received change for unpinned slice " + uri, e); |
| 89 | } |
| 90 | } |
| 91 | }; |
| 92 | } |
| 93 | |
| 94 | /// ----- Lifecycle stuff ----- |
Jason Monk | 8f5f7ff | 2017-10-17 14:12:42 -0400 | [diff] [blame] | 95 | private void systemReady() { |
| 96 | } |
| 97 | |
Jason Monk | 74f5e36 | 2017-12-06 08:56:33 -0500 | [diff] [blame] | 98 | private void onUnlockUser(int userId) { |
| 99 | } |
| 100 | |
| 101 | private void onStopUser(int userId) { |
| 102 | synchronized (mLock) { |
| 103 | mPinnedSlicesByUri.values().removeIf(s -> getUserIdFromUri(s.getUri()) == userId); |
| 104 | } |
| 105 | } |
| 106 | |
| 107 | /// ----- ISliceManager stuff ----- |
| 108 | @Override |
| 109 | public void addSliceListener(Uri uri, String pkg, ISliceListener listener, SliceSpec[] specs) |
| 110 | throws RemoteException { |
| 111 | verifyCaller(pkg); |
| 112 | uri = maybeAddUserId(uri, Binder.getCallingUserHandle().getIdentifier()); |
| 113 | enforceAccess(pkg, uri); |
| 114 | getOrCreatePinnedSlice(uri).addSliceListener(listener, specs); |
| 115 | } |
| 116 | |
| 117 | @Override |
| 118 | public void removeSliceListener(Uri uri, String pkg, ISliceListener listener) |
| 119 | throws RemoteException { |
| 120 | verifyCaller(pkg); |
| 121 | uri = maybeAddUserId(uri, Binder.getCallingUserHandle().getIdentifier()); |
| 122 | enforceAccess(pkg, uri); |
| 123 | if (getPinnedSlice(uri).removeSliceListener(listener)) { |
| 124 | removePinnedSlice(uri); |
| 125 | } |
| 126 | } |
| 127 | |
| 128 | @Override |
| 129 | public void pinSlice(String pkg, Uri uri, SliceSpec[] specs) throws RemoteException { |
| 130 | verifyCaller(pkg); |
| 131 | enforceFullAccess(pkg, "pinSlice", uri); |
| 132 | uri = maybeAddUserId(uri, Binder.getCallingUserHandle().getIdentifier()); |
| 133 | getOrCreatePinnedSlice(uri).pin(pkg, specs); |
| 134 | } |
| 135 | |
| 136 | @Override |
| 137 | public void unpinSlice(String pkg, Uri uri) throws RemoteException { |
| 138 | verifyCaller(pkg); |
| 139 | enforceFullAccess(pkg, "unpinSlice", uri); |
| 140 | uri = maybeAddUserId(uri, Binder.getCallingUserHandle().getIdentifier()); |
| 141 | if (getPinnedSlice(uri).unpin(pkg)) { |
| 142 | removePinnedSlice(uri); |
| 143 | } |
| 144 | } |
| 145 | |
| 146 | @Override |
| 147 | public boolean hasSliceAccess(String pkg) throws RemoteException { |
| 148 | verifyCaller(pkg); |
| 149 | return hasFullSliceAccess(pkg, Binder.getCallingUserHandle().getIdentifier()); |
| 150 | } |
| 151 | |
| 152 | @Override |
| 153 | public SliceSpec[] getPinnedSpecs(Uri uri, String pkg) throws RemoteException { |
| 154 | verifyCaller(pkg); |
| 155 | enforceAccess(pkg, uri); |
| 156 | return getPinnedSlice(uri).getSpecs(); |
| 157 | } |
| 158 | |
| 159 | /// ----- internal code ----- |
| 160 | void removePinnedSlice(Uri uri) { |
| 161 | synchronized (mLock) { |
| 162 | mPinnedSlicesByUri.remove(uri).destroy(); |
| 163 | } |
| 164 | } |
| 165 | |
| 166 | private PinnedSliceState getPinnedSlice(Uri uri) { |
| 167 | synchronized (mLock) { |
| 168 | PinnedSliceState manager = mPinnedSlicesByUri.get(uri); |
| 169 | if (manager == null) { |
| 170 | throw new IllegalStateException(String.format("Slice %s not pinned", |
| 171 | uri.toString())); |
| 172 | } |
| 173 | return manager; |
| 174 | } |
| 175 | } |
| 176 | |
| 177 | private PinnedSliceState getOrCreatePinnedSlice(Uri uri) { |
| 178 | synchronized (mLock) { |
| 179 | PinnedSliceState manager = mPinnedSlicesByUri.get(uri); |
| 180 | if (manager == null) { |
| 181 | manager = createPinnedSlice(uri); |
| 182 | mPinnedSlicesByUri.put(uri, manager); |
| 183 | } |
| 184 | return manager; |
| 185 | } |
| 186 | } |
| 187 | |
| 188 | @VisibleForTesting |
| 189 | PinnedSliceState createPinnedSlice(Uri uri) { |
| 190 | return new PinnedSliceState(this, uri); |
| 191 | } |
| 192 | |
| 193 | public Object getLock() { |
| 194 | return mLock; |
| 195 | } |
| 196 | |
| 197 | public Context getContext() { |
| 198 | return mContext; |
| 199 | } |
| 200 | |
| 201 | public Handler getHandler() { |
| 202 | return mHandler; |
| 203 | } |
| 204 | |
| 205 | private void enforceAccess(String pkg, Uri uri) { |
| 206 | getContext().enforceUriPermission(uri, permission.BIND_SLICE, |
| 207 | permission.BIND_SLICE, Binder.getCallingPid(), Binder.getCallingUid(), |
| 208 | Intent.FLAG_GRANT_WRITE_URI_PERMISSION, |
| 209 | "Slice binding requires the permission BIND_SLICE"); |
| 210 | int user = Binder.getCallingUserHandle().getIdentifier(); |
| 211 | if (getUserIdFromUri(uri, user) != user) { |
| 212 | getContext().enforceCallingOrSelfPermission(permission.INTERACT_ACROSS_USERS_FULL, |
| 213 | "Slice interaction across users requires INTERACT_ACROSS_USERS_FULL"); |
| 214 | } |
| 215 | } |
| 216 | |
| 217 | private void enforceFullAccess(String pkg, String name, Uri uri) { |
| 218 | int user = Binder.getCallingUserHandle().getIdentifier(); |
| 219 | if (!hasFullSliceAccess(pkg, user)) { |
| 220 | throw new SecurityException(String.format("Call %s requires full slice access", name)); |
| 221 | } |
| 222 | if (getUserIdFromUri(uri, user) != user) { |
| 223 | getContext().enforceCallingOrSelfPermission(permission.INTERACT_ACROSS_USERS_FULL, |
| 224 | "Slice interaction across users requires INTERACT_ACROSS_USERS_FULL"); |
| 225 | } |
| 226 | } |
| 227 | |
| 228 | private void verifyCaller(String pkg) { |
| 229 | mAppOps.checkPackage(Binder.getCallingUid(), pkg); |
| 230 | } |
| 231 | |
| 232 | private boolean hasFullSliceAccess(String pkg, int userId) { |
| 233 | return isDefaultHomeApp(pkg, userId) || isAssistant(pkg, userId) |
| 234 | || isGrantedFullAccess(pkg, userId); |
| 235 | } |
| 236 | |
| 237 | private boolean isAssistant(String pkg, int userId) { |
| 238 | final ComponentName cn = mAssistUtils.getAssistComponentForUser(userId); |
| 239 | if (cn == null) { |
| 240 | return false; |
| 241 | } |
| 242 | return cn.getPackageName().equals(pkg); |
| 243 | } |
| 244 | |
| 245 | public void listen(Uri uri) { |
| 246 | mContext.getContentResolver().registerContentObserver(uri, true, mObserver); |
| 247 | } |
| 248 | |
| 249 | public void unlisten(Uri uri) { |
| 250 | mContext.getContentResolver().unregisterContentObserver(mObserver); |
| 251 | synchronized (mLock) { |
| 252 | mPinnedSlicesByUri.forEach((u, s) -> { |
| 253 | if (s.isListening()) { |
| 254 | listen(u); |
| 255 | } |
| 256 | }); |
| 257 | } |
| 258 | } |
| 259 | |
| 260 | private boolean isDefaultHomeApp(String pkg, int userId) { |
| 261 | String defaultHome = getDefaultHome(userId); |
| 262 | return Objects.equals(pkg, defaultHome); |
| 263 | } |
| 264 | |
| 265 | // Based on getDefaultHome in ShortcutService. |
| 266 | // TODO: Unify if possible |
| 267 | @VisibleForTesting |
| 268 | String getDefaultHome(int userId) { |
| 269 | final long token = Binder.clearCallingIdentity(); |
| 270 | try { |
| 271 | final List<ResolveInfo> allHomeCandidates = new ArrayList<>(); |
| 272 | |
| 273 | // Default launcher from package manager. |
| 274 | final ComponentName defaultLauncher = mPackageManagerInternal |
| 275 | .getHomeActivitiesAsUser(allHomeCandidates, userId); |
| 276 | |
| 277 | ComponentName detected = null; |
| 278 | if (defaultLauncher != null) { |
| 279 | detected = defaultLauncher; |
| 280 | } |
| 281 | |
| 282 | if (detected == null) { |
| 283 | // If we reach here, that means it's the first check since the user was created, |
| 284 | // and there's already multiple launchers and there's no default set. |
| 285 | // Find the system one with the highest priority. |
| 286 | // (We need to check the priority too because of FallbackHome in Settings.) |
| 287 | // If there's no system launcher yet, then no one can access slices, until |
| 288 | // the user explicitly sets one. |
| 289 | final int size = allHomeCandidates.size(); |
| 290 | |
| 291 | int lastPriority = Integer.MIN_VALUE; |
| 292 | for (int i = 0; i < size; i++) { |
| 293 | final ResolveInfo ri = allHomeCandidates.get(i); |
| 294 | if (!ri.activityInfo.applicationInfo.isSystemApp()) { |
| 295 | continue; |
| 296 | } |
| 297 | if (ri.priority < lastPriority) { |
| 298 | continue; |
| 299 | } |
| 300 | detected = ri.activityInfo.getComponentName(); |
| 301 | lastPriority = ri.priority; |
| 302 | } |
| 303 | } |
| 304 | return detected.getPackageName(); |
| 305 | } finally { |
| 306 | Binder.restoreCallingIdentity(token); |
| 307 | } |
| 308 | } |
| 309 | |
| 310 | private boolean isGrantedFullAccess(String pkg, int userId) { |
| 311 | // TODO: This will be user granted access, if we allow this through a prompt. |
| 312 | return false; |
| 313 | } |
| 314 | |
| 315 | private static ServiceThread createHandler() { |
| 316 | ServiceThread handlerThread = new ServiceThread(TAG, |
| 317 | Process.THREAD_PRIORITY_BACKGROUND, true /*allowIo*/); |
| 318 | handlerThread.start(); |
| 319 | return handlerThread; |
Jason Monk | 8f5f7ff | 2017-10-17 14:12:42 -0400 | [diff] [blame] | 320 | } |
| 321 | |
| 322 | public static class Lifecycle extends SystemService { |
| 323 | private SliceManagerService mService; |
| 324 | |
| 325 | public Lifecycle(Context context) { |
| 326 | super(context); |
| 327 | } |
| 328 | |
| 329 | @Override |
| 330 | public void onStart() { |
| 331 | mService = new SliceManagerService(getContext()); |
| 332 | publishBinderService(Context.SLICE_SERVICE, mService); |
| 333 | } |
| 334 | |
| 335 | @Override |
| 336 | public void onBootPhase(int phase) { |
| 337 | if (phase == SystemService.PHASE_ACTIVITY_MANAGER_READY) { |
| 338 | mService.systemReady(); |
| 339 | } |
| 340 | } |
| 341 | |
| 342 | @Override |
| 343 | public void onUnlockUser(int userHandle) { |
| 344 | mService.onUnlockUser(userHandle); |
| 345 | } |
Jason Monk | 74f5e36 | 2017-12-06 08:56:33 -0500 | [diff] [blame] | 346 | |
| 347 | @Override |
| 348 | public void onStopUser(int userHandle) { |
| 349 | mService.onStopUser(userHandle); |
| 350 | } |
Jason Monk | 8f5f7ff | 2017-10-17 14:12:42 -0400 | [diff] [blame] | 351 | } |
| 352 | } |