ClipboardService.java revision f1939901d2ed0480069f0b23be64f122fce93995
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