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.ActivityManagerNative;
     20 import android.app.AppGlobals;
     21 import android.app.AppOpsManager;
     22 import android.app.IActivityManager;
     23 import android.content.BroadcastReceiver;
     24 import android.content.ClipData;
     25 import android.content.ClipDescription;
     26 import android.content.ContentProvider;
     27 import android.content.IClipboard;
     28 import android.content.IOnPrimaryClipChangedListener;
     29 import android.content.Context;
     30 import android.content.Intent;
     31 import android.content.IntentFilter;
     32 import android.content.pm.IPackageManager;
     33 import android.content.pm.PackageInfo;
     34 import android.content.pm.PackageManager;
     35 import android.content.pm.UserInfo;
     36 import android.net.Uri;
     37 import android.os.Binder;
     38 import android.os.IBinder;
     39 import android.os.IUserManager;
     40 import android.os.Parcel;
     41 import android.os.Process;
     42 import android.os.RemoteCallbackList;
     43 import android.os.RemoteException;
     44 import android.os.ServiceManager;
     45 import android.os.UserHandle;
     46 import android.os.UserManager;
     47 import android.util.Slog;
     48 import android.util.SparseArray;
     49 
     50 import java.util.HashSet;
     51 import java.util.List;
     52 
     53 /**
     54  * Implementation of the clipboard for copy and paste.
     55  */
     56 public 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