Home | History | Annotate | Download | only in clipboard
      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.app.ActivityManager;
     20 import android.app.AppGlobals;
     21 import android.app.AppOpsManager;
     22 import android.app.IActivityManager;
     23 import android.content.ClipData;
     24 import android.content.ClipDescription;
     25 import android.content.ContentProvider;
     26 import android.content.IClipboard;
     27 import android.content.IOnPrimaryClipChangedListener;
     28 import android.content.Context;
     29 import android.content.Intent;
     30 import android.content.pm.IPackageManager;
     31 import android.content.pm.PackageInfo;
     32 import android.content.pm.PackageManager;
     33 import android.content.pm.UserInfo;
     34 import android.net.Uri;
     35 import android.os.Binder;
     36 import android.os.IBinder;
     37 import android.os.IUserManager;
     38 import android.os.Parcel;
     39 import android.os.Process;
     40 import android.os.RemoteCallbackList;
     41 import android.os.RemoteException;
     42 import android.os.ServiceManager;
     43 import android.os.SystemProperties;
     44 import android.os.UserHandle;
     45 import android.os.UserManager;
     46 import android.util.Slog;
     47 import android.util.SparseArray;
     48 
     49 import com.android.server.SystemService;
     50 
     51 import java.util.HashSet;
     52 import java.util.List;
     53 
     54 import java.lang.Thread;
     55 import java.lang.Runnable;
     56 import java.lang.InterruptedException;
     57 import java.io.IOException;
     58 import 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.
     62 class 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  */
    141 public 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