1/* 2 * Copyright (C) 2008 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 17package com.android.server.clipboard; 18 19import android.app.ActivityManagerNative; 20import android.app.AppGlobals; 21import android.app.AppOpsManager; 22import android.app.IActivityManager; 23import android.content.BroadcastReceiver; 24import android.content.ClipData; 25import android.content.ClipDescription; 26import android.content.ContentProvider; 27import android.content.IClipboard; 28import android.content.IOnPrimaryClipChangedListener; 29import android.content.Context; 30import android.content.Intent; 31import android.content.IntentFilter; 32import android.content.pm.IPackageManager; 33import android.content.pm.PackageInfo; 34import android.content.pm.PackageManager; 35import android.content.pm.UserInfo; 36import android.net.Uri; 37import android.os.Binder; 38import android.os.IBinder; 39import android.os.IUserManager; 40import android.os.Parcel; 41import android.os.Process; 42import android.os.RemoteCallbackList; 43import android.os.RemoteException; 44import android.os.ServiceManager; 45import android.os.UserHandle; 46import android.os.UserManager; 47import android.util.Slog; 48import android.util.SparseArray; 49 50import java.util.HashSet; 51import java.util.List; 52 53/** 54 * Implementation of the clipboard for copy and paste. 55 */ 56public class ClipboardService extends IClipboard.Stub { 57 58 private static final String TAG = "ClipboardService"; 59 60 private final Context mContext; 61 private final IActivityManager mAm; 62 private final IUserManager mUm; 63 private final PackageManager mPm; 64 private final AppOpsManager mAppOps; 65 private final IBinder mPermissionOwner; 66 67 private class ListenerInfo { 68 final int mUid; 69 final String mPackageName; 70 ListenerInfo(int uid, String packageName) { 71 mUid = uid; 72 mPackageName = packageName; 73 } 74 } 75 76 private class PerUserClipboard { 77 final int userId; 78 79 final RemoteCallbackList<IOnPrimaryClipChangedListener> primaryClipListeners 80 = new RemoteCallbackList<IOnPrimaryClipChangedListener>(); 81 82 ClipData primaryClip; 83 84 final HashSet<String> activePermissionOwners 85 = new HashSet<String>(); 86 87 PerUserClipboard(int userId) { 88 this.userId = userId; 89 } 90 } 91 92 private SparseArray<PerUserClipboard> mClipboards = new SparseArray<PerUserClipboard>(); 93 94 /** 95 * Instantiates the clipboard. 96 */ 97 public ClipboardService(Context context) { 98 mContext = context; 99 mAm = ActivityManagerNative.getDefault(); 100 mPm = context.getPackageManager(); 101 mUm = (IUserManager) ServiceManager.getService(Context.USER_SERVICE); 102 mAppOps = (AppOpsManager)context.getSystemService(Context.APP_OPS_SERVICE); 103 IBinder permOwner = null; 104 try { 105 permOwner = mAm.newUriPermissionOwner("clipboard"); 106 } catch (RemoteException e) { 107 Slog.w("clipboard", "AM dead", e); 108 } 109 mPermissionOwner = permOwner; 110 111 // Remove the clipboard if a user is removed 112 IntentFilter userFilter = new IntentFilter(); 113 userFilter.addAction(Intent.ACTION_USER_REMOVED); 114 mContext.registerReceiver(new BroadcastReceiver() { 115 @Override 116 public void onReceive(Context context, Intent intent) { 117 String action = intent.getAction(); 118 if (Intent.ACTION_USER_REMOVED.equals(action)) { 119 removeClipboard(intent.getIntExtra(Intent.EXTRA_USER_HANDLE, 0)); 120 } 121 } 122 }, userFilter); 123 } 124 125 @Override 126 public boolean onTransact(int code, Parcel data, Parcel reply, int flags) 127 throws RemoteException { 128 try { 129 return super.onTransact(code, data, reply, flags); 130 } catch (RuntimeException e) { 131 if (!(e instanceof SecurityException)) { 132 Slog.wtf("clipboard", "Exception: ", e); 133 } 134 throw e; 135 } 136 137 } 138 139 private PerUserClipboard getClipboard() { 140 return getClipboard(UserHandle.getCallingUserId()); 141 } 142 143 private PerUserClipboard getClipboard(int userId) { 144 synchronized (mClipboards) { 145 PerUserClipboard puc = mClipboards.get(userId); 146 if (puc == null) { 147 puc = new PerUserClipboard(userId); 148 mClipboards.put(userId, puc); 149 } 150 return puc; 151 } 152 } 153 154 private void removeClipboard(int userId) { 155 synchronized (mClipboards) { 156 mClipboards.remove(userId); 157 } 158 } 159 160 public void setPrimaryClip(ClipData clip, String callingPackage) { 161 synchronized (this) { 162 if (clip != null && clip.getItemCount() <= 0) { 163 throw new IllegalArgumentException("No items"); 164 } 165 final int callingUid = Binder.getCallingUid(); 166 if (mAppOps.noteOp(AppOpsManager.OP_WRITE_CLIPBOARD, callingUid, 167 callingPackage) != AppOpsManager.MODE_ALLOWED) { 168 return; 169 } 170 checkDataOwnerLocked(clip, callingUid); 171 final int userId = UserHandle.getUserId(callingUid); 172 PerUserClipboard clipboard = getClipboard(userId); 173 revokeUris(clipboard); 174 setPrimaryClipInternal(clipboard, clip); 175 List<UserInfo> related = getRelatedProfiles(userId); 176 if (related != null) { 177 int size = related.size(); 178 if (size > 1) { // Related profiles list include the current profile. 179 boolean canCopy = false; 180 try { 181 canCopy = !mUm.getUserRestrictions(userId).getBoolean( 182 UserManager.DISALLOW_CROSS_PROFILE_COPY_PASTE); 183 } catch (RemoteException e) { 184 Slog.e(TAG, "Remote Exception calling UserManager: " + e); 185 } 186 // Copy clip data to related users if allowed. If disallowed, then remove 187 // primary clip in related users to prevent pasting stale content. 188 if (!canCopy) { 189 clip = null; 190 } else { 191 clip.fixUrisLight(userId); 192 } 193 for (int i = 0; i < size; i++) { 194 int id = related.get(i).id; 195 if (id != userId) { 196 setPrimaryClipInternal(getClipboard(id), clip); 197 } 198 } 199 } 200 } 201 } 202 } 203 204 List<UserInfo> getRelatedProfiles(int userId) { 205 final List<UserInfo> related; 206 final long origId = Binder.clearCallingIdentity(); 207 try { 208 related = mUm.getProfiles(userId, true); 209 } catch (RemoteException e) { 210 Slog.e(TAG, "Remote Exception calling UserManager: " + e); 211 return null; 212 } finally{ 213 Binder.restoreCallingIdentity(origId); 214 } 215 return related; 216 } 217 218 void setPrimaryClipInternal(PerUserClipboard clipboard, ClipData clip) { 219 clipboard.activePermissionOwners.clear(); 220 if (clip == null && clipboard.primaryClip == null) { 221 return; 222 } 223 clipboard.primaryClip = clip; 224 final long ident = Binder.clearCallingIdentity(); 225 final int n = clipboard.primaryClipListeners.beginBroadcast(); 226 try { 227 for (int i = 0; i < n; i++) { 228 try { 229 ListenerInfo li = (ListenerInfo) 230 clipboard.primaryClipListeners.getBroadcastCookie(i); 231 if (mAppOps.checkOpNoThrow(AppOpsManager.OP_READ_CLIPBOARD, li.mUid, 232 li.mPackageName) == AppOpsManager.MODE_ALLOWED) { 233 clipboard.primaryClipListeners.getBroadcastItem(i) 234 .dispatchPrimaryClipChanged(); 235 } 236 } catch (RemoteException e) { 237 // The RemoteCallbackList will take care of removing 238 // the dead object for us. 239 } 240 } 241 } finally { 242 clipboard.primaryClipListeners.finishBroadcast(); 243 Binder.restoreCallingIdentity(ident); 244 } 245 } 246 247 public ClipData getPrimaryClip(String pkg) { 248 synchronized (this) { 249 if (mAppOps.noteOp(AppOpsManager.OP_READ_CLIPBOARD, Binder.getCallingUid(), 250 pkg) != AppOpsManager.MODE_ALLOWED) { 251 return null; 252 } 253 addActiveOwnerLocked(Binder.getCallingUid(), pkg); 254 return getClipboard().primaryClip; 255 } 256 } 257 258 public ClipDescription getPrimaryClipDescription(String callingPackage) { 259 synchronized (this) { 260 if (mAppOps.checkOp(AppOpsManager.OP_READ_CLIPBOARD, Binder.getCallingUid(), 261 callingPackage) != AppOpsManager.MODE_ALLOWED) { 262 return null; 263 } 264 PerUserClipboard clipboard = getClipboard(); 265 return clipboard.primaryClip != null ? clipboard.primaryClip.getDescription() : null; 266 } 267 } 268 269 public boolean hasPrimaryClip(String callingPackage) { 270 synchronized (this) { 271 if (mAppOps.checkOp(AppOpsManager.OP_READ_CLIPBOARD, Binder.getCallingUid(), 272 callingPackage) != AppOpsManager.MODE_ALLOWED) { 273 return false; 274 } 275 return getClipboard().primaryClip != null; 276 } 277 } 278 279 public void addPrimaryClipChangedListener(IOnPrimaryClipChangedListener listener, 280 String callingPackage) { 281 synchronized (this) { 282 getClipboard().primaryClipListeners.register(listener, 283 new ListenerInfo(Binder.getCallingUid(), callingPackage)); 284 } 285 } 286 287 public void removePrimaryClipChangedListener(IOnPrimaryClipChangedListener listener) { 288 synchronized (this) { 289 getClipboard().primaryClipListeners.unregister(listener); 290 } 291 } 292 293 public boolean hasClipboardText(String callingPackage) { 294 synchronized (this) { 295 if (mAppOps.checkOp(AppOpsManager.OP_READ_CLIPBOARD, Binder.getCallingUid(), 296 callingPackage) != AppOpsManager.MODE_ALLOWED) { 297 return false; 298 } 299 PerUserClipboard clipboard = getClipboard(); 300 if (clipboard.primaryClip != null) { 301 CharSequence text = clipboard.primaryClip.getItemAt(0).getText(); 302 return text != null && text.length() > 0; 303 } 304 return false; 305 } 306 } 307 308 private final void checkUriOwnerLocked(Uri uri, int uid) { 309 if (!"content".equals(uri.getScheme())) { 310 return; 311 } 312 long ident = Binder.clearCallingIdentity(); 313 try { 314 // This will throw SecurityException for us. 315 mAm.checkGrantUriPermission(uid, null, ContentProvider.getUriWithoutUserId(uri), 316 Intent.FLAG_GRANT_READ_URI_PERMISSION, 317 ContentProvider.getUserIdFromUri(uri, UserHandle.getUserId(uid))); 318 } catch (RemoteException e) { 319 } finally { 320 Binder.restoreCallingIdentity(ident); 321 } 322 } 323 324 private final void checkItemOwnerLocked(ClipData.Item item, int uid) { 325 if (item.getUri() != null) { 326 checkUriOwnerLocked(item.getUri(), uid); 327 } 328 Intent intent = item.getIntent(); 329 if (intent != null && intent.getData() != null) { 330 checkUriOwnerLocked(intent.getData(), uid); 331 } 332 } 333 334 private final void checkDataOwnerLocked(ClipData data, int uid) { 335 final int N = data.getItemCount(); 336 for (int i=0; i<N; i++) { 337 checkItemOwnerLocked(data.getItemAt(i), uid); 338 } 339 } 340 341 private final void grantUriLocked(Uri uri, String pkg, int userId) { 342 long ident = Binder.clearCallingIdentity(); 343 try { 344 int sourceUserId = ContentProvider.getUserIdFromUri(uri, userId); 345 uri = ContentProvider.getUriWithoutUserId(uri); 346 mAm.grantUriPermissionFromOwner(mPermissionOwner, Process.myUid(), pkg, 347 uri, Intent.FLAG_GRANT_READ_URI_PERMISSION, sourceUserId, userId); 348 } catch (RemoteException e) { 349 } finally { 350 Binder.restoreCallingIdentity(ident); 351 } 352 } 353 354 private final void grantItemLocked(ClipData.Item item, String pkg, int userId) { 355 if (item.getUri() != null) { 356 grantUriLocked(item.getUri(), pkg, userId); 357 } 358 Intent intent = item.getIntent(); 359 if (intent != null && intent.getData() != null) { 360 grantUriLocked(intent.getData(), pkg, userId); 361 } 362 } 363 364 private final void addActiveOwnerLocked(int uid, String pkg) { 365 final IPackageManager pm = AppGlobals.getPackageManager(); 366 final int targetUserHandle = UserHandle.getCallingUserId(); 367 final long oldIdentity = Binder.clearCallingIdentity(); 368 try { 369 PackageInfo pi = pm.getPackageInfo(pkg, 0, targetUserHandle); 370 if (pi == null) { 371 throw new IllegalArgumentException("Unknown package " + pkg); 372 } 373 if (!UserHandle.isSameApp(pi.applicationInfo.uid, uid)) { 374 throw new SecurityException("Calling uid " + uid 375 + " does not own package " + pkg); 376 } 377 } catch (RemoteException e) { 378 // Can't happen; the package manager is in the same process 379 } finally { 380 Binder.restoreCallingIdentity(oldIdentity); 381 } 382 PerUserClipboard clipboard = getClipboard(); 383 if (clipboard.primaryClip != null && !clipboard.activePermissionOwners.contains(pkg)) { 384 final int N = clipboard.primaryClip.getItemCount(); 385 for (int i=0; i<N; i++) { 386 grantItemLocked(clipboard.primaryClip.getItemAt(i), pkg, UserHandle.getUserId(uid)); 387 } 388 clipboard.activePermissionOwners.add(pkg); 389 } 390 } 391 392 private final void revokeUriLocked(Uri uri) { 393 int userId = ContentProvider.getUserIdFromUri(uri, 394 UserHandle.getUserId(Binder.getCallingUid())); 395 long ident = Binder.clearCallingIdentity(); 396 try { 397 uri = ContentProvider.getUriWithoutUserId(uri); 398 mAm.revokeUriPermissionFromOwner(mPermissionOwner, uri, 399 Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION, 400 userId); 401 } catch (RemoteException e) { 402 } finally { 403 Binder.restoreCallingIdentity(ident); 404 } 405 } 406 407 private final void revokeItemLocked(ClipData.Item item) { 408 if (item.getUri() != null) { 409 revokeUriLocked(item.getUri()); 410 } 411 Intent intent = item.getIntent(); 412 if (intent != null && intent.getData() != null) { 413 revokeUriLocked(intent.getData()); 414 } 415 } 416 417 private final void revokeUris(PerUserClipboard clipboard) { 418 if (clipboard.primaryClip == null) { 419 return; 420 } 421 final int N = clipboard.primaryClip.getItemCount(); 422 for (int i=0; i<N; i++) { 423 revokeItemLocked(clipboard.primaryClip.getItemAt(i)); 424 } 425 } 426} 427