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 17 package com.android.server.clipboard; 18 19 import android.annotation.Nullable; 20 import android.app.ActivityManager; 21 import android.app.AppGlobals; 22 import android.app.AppOpsManager; 23 import android.app.IActivityManager; 24 import android.app.KeyguardManager; 25 import android.content.ClipData; 26 import android.content.ClipDescription; 27 import android.content.ContentProvider; 28 import android.content.ContentResolver; 29 import android.content.Context; 30 import android.content.IClipboard; 31 import android.content.IOnPrimaryClipChangedListener; 32 import android.content.Intent; 33 import android.content.pm.IPackageManager; 34 import android.content.pm.PackageInfo; 35 import android.content.pm.PackageManager; 36 import android.content.pm.UserInfo; 37 import android.net.Uri; 38 import android.os.Binder; 39 import android.os.IBinder; 40 import android.os.IUserManager; 41 import android.os.Parcel; 42 import android.os.RemoteCallbackList; 43 import android.os.RemoteException; 44 import android.os.ServiceManager; 45 import android.os.SystemProperties; 46 import android.os.UserHandle; 47 import android.os.UserManager; 48 import android.util.Slog; 49 import android.util.SparseArray; 50 51 import com.android.server.SystemService; 52 53 import java.io.IOException; 54 import java.io.RandomAccessFile; 55 import java.util.HashSet; 56 import java.util.List; 57 58 // The following class is Android Emulator specific. It is used to read and 59 // write contents of the host system's clipboard. 60 class HostClipboardMonitor implements Runnable { 61 public interface HostClipboardCallback { 62 void onHostClipboardUpdated(String contents); 63 } 64 65 private RandomAccessFile mPipe = null; 66 private HostClipboardCallback mHostClipboardCallback; 67 private static final String PIPE_NAME = "pipe:clipboard"; 68 private static final String PIPE_DEVICE = "/dev/qemu_pipe"; 69 70 private void openPipe() { 71 try { 72 // String.getBytes doesn't include the null terminator, 73 // but the QEMU pipe device requires the pipe service name 74 // to be null-terminated. 75 byte[] b = new byte[PIPE_NAME.length() + 1]; 76 b[PIPE_NAME.length()] = 0; 77 System.arraycopy( 78 PIPE_NAME.getBytes(), 79 0, 80 b, 81 0, 82 PIPE_NAME.length()); 83 mPipe = new RandomAccessFile(PIPE_DEVICE, "rw"); 84 mPipe.write(b); 85 } catch (IOException e) { 86 try { 87 if (mPipe != null) mPipe.close(); 88 } catch (IOException ee) {} 89 mPipe = null; 90 } 91 } 92 93 public HostClipboardMonitor(HostClipboardCallback cb) { 94 mHostClipboardCallback = cb; 95 } 96 97 @Override 98 public void run() { 99 while(!Thread.interrupted()) { 100 try { 101 // There's no guarantee that QEMU pipes will be ready at the moment 102 // this method is invoked. We simply try to get the pipe open and 103 // retry on failure indefinitely. 104 while (mPipe == null) { 105 openPipe(); 106 Thread.sleep(100); 107 } 108 int size = mPipe.readInt(); 109 size = Integer.reverseBytes(size); 110 byte[] receivedData = new byte[size]; 111 mPipe.readFully(receivedData); 112 mHostClipboardCallback.onHostClipboardUpdated( 113 new String(receivedData)); 114 } catch (IOException e) { 115 try { 116 mPipe.close(); 117 } catch (IOException ee) {} 118 mPipe = null; 119 } catch (InterruptedException e) {} 120 } 121 } 122 123 public void setHostClipboard(String content) { 124 try { 125 if (mPipe != null) { 126 mPipe.writeInt(Integer.reverseBytes(content.getBytes().length)); 127 mPipe.write(content.getBytes()); 128 } 129 } catch(IOException e) { 130 Slog.e("HostClipboardMonitor", 131 "Failed to set host clipboard " + e.getMessage()); 132 } 133 } 134 } 135 136 /** 137 * Implementation of the clipboard for copy and paste. 138 */ 139 public class ClipboardService extends SystemService { 140 141 private static final String TAG = "ClipboardService"; 142 private static final boolean IS_EMULATOR = 143 SystemProperties.getBoolean("ro.kernel.qemu", false); 144 145 private final IActivityManager mAm; 146 private final IUserManager mUm; 147 private final PackageManager mPm; 148 private final AppOpsManager mAppOps; 149 private final IBinder mPermissionOwner; 150 private HostClipboardMonitor mHostClipboardMonitor = null; 151 private Thread mHostMonitorThread = null; 152 153 private final SparseArray<PerUserClipboard> mClipboards = new SparseArray<>(); 154 155 /** 156 * Instantiates the clipboard. 157 */ 158 public ClipboardService(Context context) { 159 super(context); 160 161 mAm = ActivityManager.getService(); 162 mPm = getContext().getPackageManager(); 163 mUm = (IUserManager) ServiceManager.getService(Context.USER_SERVICE); 164 mAppOps = (AppOpsManager) getContext().getSystemService(Context.APP_OPS_SERVICE); 165 IBinder permOwner = null; 166 try { 167 permOwner = mAm.newUriPermissionOwner("clipboard"); 168 } catch (RemoteException e) { 169 Slog.w("clipboard", "AM dead", e); 170 } 171 mPermissionOwner = permOwner; 172 if (IS_EMULATOR) { 173 mHostClipboardMonitor = new HostClipboardMonitor( 174 new HostClipboardMonitor.HostClipboardCallback() { 175 @Override 176 public void onHostClipboardUpdated(String contents){ 177 ClipData clip = 178 new ClipData("host clipboard", 179 new String[]{"text/plain"}, 180 new ClipData.Item(contents)); 181 synchronized(mClipboards) { 182 setPrimaryClipInternal(getClipboard(0), clip, 183 android.os.Process.SYSTEM_UID); 184 } 185 } 186 }); 187 mHostMonitorThread = new Thread(mHostClipboardMonitor); 188 mHostMonitorThread.start(); 189 } 190 } 191 192 @Override 193 public void onStart() { 194 publishBinderService(Context.CLIPBOARD_SERVICE, new ClipboardImpl()); 195 } 196 197 @Override 198 public void onCleanupUser(int userId) { 199 synchronized (mClipboards) { 200 mClipboards.remove(userId); 201 } 202 } 203 204 private class ListenerInfo { 205 final int mUid; 206 final String mPackageName; 207 ListenerInfo(int uid, String packageName) { 208 mUid = uid; 209 mPackageName = packageName; 210 } 211 } 212 213 private class PerUserClipboard { 214 final int userId; 215 216 final RemoteCallbackList<IOnPrimaryClipChangedListener> primaryClipListeners 217 = new RemoteCallbackList<IOnPrimaryClipChangedListener>(); 218 219 /** Current primary clip. */ 220 ClipData primaryClip; 221 /** UID that set {@link #primaryClip}. */ 222 int primaryClipUid = android.os.Process.NOBODY_UID; 223 224 final HashSet<String> activePermissionOwners 225 = new HashSet<String>(); 226 227 PerUserClipboard(int userId) { 228 this.userId = userId; 229 } 230 } 231 232 private class ClipboardImpl extends IClipboard.Stub { 233 @Override 234 public boolean onTransact(int code, Parcel data, Parcel reply, int flags) 235 throws RemoteException { 236 try { 237 return super.onTransact(code, data, reply, flags); 238 } catch (RuntimeException e) { 239 if (!(e instanceof SecurityException)) { 240 Slog.wtf("clipboard", "Exception: ", e); 241 } 242 throw e; 243 } 244 245 } 246 247 @Override 248 public void setPrimaryClip(ClipData clip, String callingPackage) { 249 synchronized (this) { 250 if (clip == null || clip.getItemCount() <= 0) { 251 throw new IllegalArgumentException("No items"); 252 } 253 final int callingUid = Binder.getCallingUid(); 254 if (!clipboardAccessAllowed(AppOpsManager.OP_WRITE_CLIPBOARD, callingPackage, 255 callingUid)) { 256 return; 257 } 258 checkDataOwnerLocked(clip, callingUid); 259 setPrimaryClipInternal(clip, callingUid); 260 } 261 } 262 263 @Override 264 public void clearPrimaryClip(String callingPackage) { 265 synchronized (this) { 266 final int callingUid = Binder.getCallingUid(); 267 if (!clipboardAccessAllowed(AppOpsManager.OP_WRITE_CLIPBOARD, callingPackage, 268 callingUid)) { 269 return; 270 } 271 setPrimaryClipInternal(null, callingUid); 272 } 273 } 274 275 @Override 276 public ClipData getPrimaryClip(String pkg) { 277 synchronized (this) { 278 if (!clipboardAccessAllowed(AppOpsManager.OP_READ_CLIPBOARD, pkg, 279 Binder.getCallingUid()) || isDeviceLocked()) { 280 return null; 281 } 282 addActiveOwnerLocked(Binder.getCallingUid(), pkg); 283 return getClipboard().primaryClip; 284 } 285 } 286 287 @Override 288 public ClipDescription getPrimaryClipDescription(String callingPackage) { 289 synchronized (this) { 290 if (!clipboardAccessAllowed(AppOpsManager.OP_READ_CLIPBOARD, callingPackage, 291 Binder.getCallingUid()) || isDeviceLocked()) { 292 return null; 293 } 294 PerUserClipboard clipboard = getClipboard(); 295 return clipboard.primaryClip != null ? clipboard.primaryClip.getDescription() : null; 296 } 297 } 298 299 @Override 300 public boolean hasPrimaryClip(String callingPackage) { 301 synchronized (this) { 302 if (!clipboardAccessAllowed(AppOpsManager.OP_READ_CLIPBOARD, callingPackage, 303 Binder.getCallingUid()) || isDeviceLocked()) { 304 return false; 305 } 306 return getClipboard().primaryClip != null; 307 } 308 } 309 310 @Override 311 public void addPrimaryClipChangedListener(IOnPrimaryClipChangedListener listener, 312 String callingPackage) { 313 synchronized (this) { 314 getClipboard().primaryClipListeners.register(listener, 315 new ListenerInfo(Binder.getCallingUid(), callingPackage)); 316 } 317 } 318 319 @Override 320 public void removePrimaryClipChangedListener(IOnPrimaryClipChangedListener listener) { 321 synchronized (this) { 322 getClipboard().primaryClipListeners.unregister(listener); 323 } 324 } 325 326 @Override 327 public boolean hasClipboardText(String callingPackage) { 328 synchronized (this) { 329 if (!clipboardAccessAllowed(AppOpsManager.OP_READ_CLIPBOARD, callingPackage, 330 Binder.getCallingUid()) || isDeviceLocked()) { 331 return false; 332 } 333 PerUserClipboard clipboard = getClipboard(); 334 if (clipboard.primaryClip != null) { 335 CharSequence text = clipboard.primaryClip.getItemAt(0).getText(); 336 return text != null && text.length() > 0; 337 } 338 return false; 339 } 340 } 341 }; 342 343 private PerUserClipboard getClipboard() { 344 return getClipboard(UserHandle.getCallingUserId()); 345 } 346 347 private PerUserClipboard getClipboard(int userId) { 348 synchronized (mClipboards) { 349 PerUserClipboard puc = mClipboards.get(userId); 350 if (puc == null) { 351 puc = new PerUserClipboard(userId); 352 mClipboards.put(userId, puc); 353 } 354 return puc; 355 } 356 } 357 358 List<UserInfo> getRelatedProfiles(int userId) { 359 final List<UserInfo> related; 360 final long origId = Binder.clearCallingIdentity(); 361 try { 362 related = mUm.getProfiles(userId, true); 363 } catch (RemoteException e) { 364 Slog.e(TAG, "Remote Exception calling UserManager: " + e); 365 return null; 366 } finally{ 367 Binder.restoreCallingIdentity(origId); 368 } 369 return related; 370 } 371 372 /** Check if the user has the given restriction set. Default to true if error occured during 373 * calling UserManager, so it fails safe. 374 */ 375 private boolean hasRestriction(String restriction, int userId) { 376 try { 377 return mUm.hasUserRestriction(restriction, userId); 378 } catch (RemoteException e) { 379 Slog.e(TAG, "Remote Exception calling UserManager.getUserRestrictions: ", e); 380 // Fails safe 381 return true; 382 } 383 } 384 385 void setPrimaryClipInternal(@Nullable ClipData clip, int callingUid) { 386 // Push clipboard to host, if any 387 if (mHostClipboardMonitor != null) { 388 if (clip == null) { 389 // Someone really wants the clipboard cleared, so push empty 390 mHostClipboardMonitor.setHostClipboard(""); 391 } else if (clip.getItemCount() > 0) { 392 final CharSequence text = clip.getItemAt(0).getText(); 393 if (text != null) { 394 mHostClipboardMonitor.setHostClipboard(text.toString()); 395 } 396 } 397 } 398 399 // Update this user 400 final int userId = UserHandle.getUserId(callingUid); 401 setPrimaryClipInternal(getClipboard(userId), clip, callingUid); 402 403 // Update related users 404 List<UserInfo> related = getRelatedProfiles(userId); 405 if (related != null) { 406 int size = related.size(); 407 if (size > 1) { // Related profiles list include the current profile. 408 final boolean canCopy = !hasRestriction( 409 UserManager.DISALLOW_CROSS_PROFILE_COPY_PASTE, userId); 410 // Copy clip data to related users if allowed. If disallowed, then remove 411 // primary clip in related users to prevent pasting stale content. 412 if (!canCopy) { 413 clip = null; 414 } else { 415 // We want to fix the uris of the related user's clip without changing the 416 // uris of the current user's clip. 417 // So, copy the ClipData, and then copy all the items, so that nothing 418 // is shared in memmory. 419 clip = new ClipData(clip); 420 for (int i = clip.getItemCount() - 1; i >= 0; i--) { 421 clip.setItemAt(i, new ClipData.Item(clip.getItemAt(i))); 422 } 423 clip.fixUrisLight(userId); 424 } 425 for (int i = 0; i < size; i++) { 426 int id = related.get(i).id; 427 if (id != userId) { 428 final boolean canCopyIntoProfile = !hasRestriction( 429 UserManager.DISALLOW_SHARE_INTO_MANAGED_PROFILE, id); 430 if (canCopyIntoProfile) { 431 setPrimaryClipInternal(getClipboard(id), clip, callingUid); 432 } 433 } 434 } 435 } 436 } 437 } 438 439 void setPrimaryClipInternal(PerUserClipboard clipboard, @Nullable ClipData clip, 440 int callingUid) { 441 revokeUris(clipboard); 442 clipboard.activePermissionOwners.clear(); 443 if (clip == null && clipboard.primaryClip == null) { 444 return; 445 } 446 clipboard.primaryClip = clip; 447 if (clip != null) { 448 clipboard.primaryClipUid = callingUid; 449 } else { 450 clipboard.primaryClipUid = android.os.Process.NOBODY_UID; 451 } 452 if (clip != null) { 453 final ClipDescription description = clip.getDescription(); 454 if (description != null) { 455 description.setTimestamp(System.currentTimeMillis()); 456 } 457 } 458 final long ident = Binder.clearCallingIdentity(); 459 final int n = clipboard.primaryClipListeners.beginBroadcast(); 460 try { 461 for (int i = 0; i < n; i++) { 462 try { 463 ListenerInfo li = (ListenerInfo) 464 clipboard.primaryClipListeners.getBroadcastCookie(i); 465 466 if (clipboardAccessAllowed(AppOpsManager.OP_READ_CLIPBOARD, li.mPackageName, 467 li.mUid)) { 468 clipboard.primaryClipListeners.getBroadcastItem(i) 469 .dispatchPrimaryClipChanged(); 470 } 471 } catch (RemoteException e) { 472 // The RemoteCallbackList will take care of removing 473 // the dead object for us. 474 } 475 } 476 } finally { 477 clipboard.primaryClipListeners.finishBroadcast(); 478 Binder.restoreCallingIdentity(ident); 479 } 480 } 481 482 private boolean isDeviceLocked() { 483 int callingUserId = UserHandle.getCallingUserId(); 484 final long token = Binder.clearCallingIdentity(); 485 try { 486 final KeyguardManager keyguardManager = getContext().getSystemService( 487 KeyguardManager.class); 488 return keyguardManager != null && keyguardManager.isDeviceLocked(callingUserId); 489 } finally { 490 Binder.restoreCallingIdentity(token); 491 } 492 } 493 494 private final void checkUriOwnerLocked(Uri uri, int sourceUid) { 495 if (uri == null || !ContentResolver.SCHEME_CONTENT.equals(uri.getScheme())) return; 496 497 final long ident = Binder.clearCallingIdentity(); 498 try { 499 // This will throw SecurityException if caller can't grant 500 mAm.checkGrantUriPermission(sourceUid, null, 501 ContentProvider.getUriWithoutUserId(uri), 502 Intent.FLAG_GRANT_READ_URI_PERMISSION, 503 ContentProvider.getUserIdFromUri(uri, UserHandle.getUserId(sourceUid))); 504 } catch (RemoteException ignored) { 505 // Ignored because we're in same process 506 } finally { 507 Binder.restoreCallingIdentity(ident); 508 } 509 } 510 511 private final void checkItemOwnerLocked(ClipData.Item item, int uid) { 512 if (item.getUri() != null) { 513 checkUriOwnerLocked(item.getUri(), uid); 514 } 515 Intent intent = item.getIntent(); 516 if (intent != null && intent.getData() != null) { 517 checkUriOwnerLocked(intent.getData(), uid); 518 } 519 } 520 521 private final void checkDataOwnerLocked(ClipData data, int uid) { 522 final int N = data.getItemCount(); 523 for (int i=0; i<N; i++) { 524 checkItemOwnerLocked(data.getItemAt(i), uid); 525 } 526 } 527 528 private final void grantUriLocked(Uri uri, int sourceUid, String targetPkg, 529 int targetUserId) { 530 if (uri == null || !ContentResolver.SCHEME_CONTENT.equals(uri.getScheme())) return; 531 532 final long ident = Binder.clearCallingIdentity(); 533 try { 534 mAm.grantUriPermissionFromOwner(mPermissionOwner, sourceUid, targetPkg, 535 ContentProvider.getUriWithoutUserId(uri), 536 Intent.FLAG_GRANT_READ_URI_PERMISSION, 537 ContentProvider.getUserIdFromUri(uri, UserHandle.getUserId(sourceUid)), 538 targetUserId); 539 } catch (RemoteException ignored) { 540 // Ignored because we're in same process 541 } finally { 542 Binder.restoreCallingIdentity(ident); 543 } 544 } 545 546 private final void grantItemLocked(ClipData.Item item, int sourceUid, String targetPkg, 547 int targetUserId) { 548 if (item.getUri() != null) { 549 grantUriLocked(item.getUri(), sourceUid, targetPkg, targetUserId); 550 } 551 Intent intent = item.getIntent(); 552 if (intent != null && intent.getData() != null) { 553 grantUriLocked(intent.getData(), sourceUid, targetPkg, targetUserId); 554 } 555 } 556 557 private final void addActiveOwnerLocked(int uid, String pkg) { 558 final IPackageManager pm = AppGlobals.getPackageManager(); 559 final int targetUserHandle = UserHandle.getCallingUserId(); 560 final long oldIdentity = Binder.clearCallingIdentity(); 561 try { 562 PackageInfo pi = pm.getPackageInfo(pkg, 0, targetUserHandle); 563 if (pi == null) { 564 throw new IllegalArgumentException("Unknown package " + pkg); 565 } 566 if (!UserHandle.isSameApp(pi.applicationInfo.uid, uid)) { 567 throw new SecurityException("Calling uid " + uid 568 + " does not own package " + pkg); 569 } 570 } catch (RemoteException e) { 571 // Can't happen; the package manager is in the same process 572 } finally { 573 Binder.restoreCallingIdentity(oldIdentity); 574 } 575 PerUserClipboard clipboard = getClipboard(); 576 if (clipboard.primaryClip != null && !clipboard.activePermissionOwners.contains(pkg)) { 577 final int N = clipboard.primaryClip.getItemCount(); 578 for (int i=0; i<N; i++) { 579 grantItemLocked(clipboard.primaryClip.getItemAt(i), clipboard.primaryClipUid, pkg, 580 UserHandle.getUserId(uid)); 581 } 582 clipboard.activePermissionOwners.add(pkg); 583 } 584 } 585 586 private final void revokeUriLocked(Uri uri, int sourceUid) { 587 if (uri == null || !ContentResolver.SCHEME_CONTENT.equals(uri.getScheme())) return; 588 589 final long ident = Binder.clearCallingIdentity(); 590 try { 591 mAm.revokeUriPermissionFromOwner(mPermissionOwner, 592 ContentProvider.getUriWithoutUserId(uri), 593 Intent.FLAG_GRANT_READ_URI_PERMISSION, 594 ContentProvider.getUserIdFromUri(uri, UserHandle.getUserId(sourceUid))); 595 } catch (RemoteException ignored) { 596 // Ignored because we're in same process 597 } finally { 598 Binder.restoreCallingIdentity(ident); 599 } 600 } 601 602 private final void revokeItemLocked(ClipData.Item item, int sourceUid) { 603 if (item.getUri() != null) { 604 revokeUriLocked(item.getUri(), sourceUid); 605 } 606 Intent intent = item.getIntent(); 607 if (intent != null && intent.getData() != null) { 608 revokeUriLocked(intent.getData(), sourceUid); 609 } 610 } 611 612 private final void revokeUris(PerUserClipboard clipboard) { 613 if (clipboard.primaryClip == null) { 614 return; 615 } 616 final int N = clipboard.primaryClip.getItemCount(); 617 for (int i=0; i<N; i++) { 618 revokeItemLocked(clipboard.primaryClip.getItemAt(i), clipboard.primaryClipUid); 619 } 620 } 621 622 private boolean clipboardAccessAllowed(int op, String callingPackage, int callingUid) { 623 // Check the AppOp. 624 if (mAppOps.noteOp(op, callingUid, callingPackage) != AppOpsManager.MODE_ALLOWED) { 625 return false; 626 } 627 try { 628 // Installed apps can access the clipboard at any time. 629 if (!AppGlobals.getPackageManager().isInstantApp(callingPackage, 630 UserHandle.getUserId(callingUid))) { 631 return true; 632 } 633 // Instant apps can only access the clipboard if they are in the foreground. 634 return mAm.isAppForeground(callingUid); 635 } catch (RemoteException e) { 636 Slog.e("clipboard", "Failed to get Instant App status for package " + callingPackage, 637 e); 638 return false; 639 } 640 } 641 } 642