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