ClipboardService.java revision d85fc72fb810858f7502e7e7f1bad53e1bf03edd
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.net.Uri;
36import android.os.Binder;
37import android.os.IBinder;
38import android.os.Parcel;
39import android.os.Process;
40import android.os.RemoteCallbackList;
41import android.os.RemoteException;
42import android.os.UserHandle;
43import android.util.Slog;
44import android.util.SparseArray;
45
46import java.util.HashSet;
47
48/**
49 * Implementation of the clipboard for copy and paste.
50 */
51public class ClipboardService extends IClipboard.Stub {
52
53    private static final String TAG = "ClipboardService";
54
55    private final Context mContext;
56    private final IActivityManager mAm;
57    private final PackageManager mPm;
58    private final AppOpsManager mAppOps;
59    private final IBinder mPermissionOwner;
60
61    private class ListenerInfo {
62        final int mUid;
63        final String mPackageName;
64        ListenerInfo(int uid, String packageName) {
65            mUid = uid;
66            mPackageName = packageName;
67        }
68    }
69
70    private class PerUserClipboard {
71        final int userId;
72
73        final RemoteCallbackList<IOnPrimaryClipChangedListener> primaryClipListeners
74                = new RemoteCallbackList<IOnPrimaryClipChangedListener>();
75
76        ClipData primaryClip;
77
78        final HashSet<String> activePermissionOwners
79                = new HashSet<String>();
80
81        PerUserClipboard(int userId) {
82            this.userId = userId;
83        }
84    }
85
86    private SparseArray<PerUserClipboard> mClipboards = new SparseArray<PerUserClipboard>();
87
88    /**
89     * Instantiates the clipboard.
90     */
91    public ClipboardService(Context context) {
92        mContext = context;
93        mAm = ActivityManagerNative.getDefault();
94        mPm = context.getPackageManager();
95        mAppOps = (AppOpsManager)context.getSystemService(Context.APP_OPS_SERVICE);
96        IBinder permOwner = null;
97        try {
98            permOwner = mAm.newUriPermissionOwner("clipboard");
99        } catch (RemoteException e) {
100            Slog.w("clipboard", "AM dead", e);
101        }
102        mPermissionOwner = permOwner;
103
104        // Remove the clipboard if a user is removed
105        IntentFilter userFilter = new IntentFilter();
106        userFilter.addAction(Intent.ACTION_USER_REMOVED);
107        mContext.registerReceiver(new BroadcastReceiver() {
108            @Override
109            public void onReceive(Context context, Intent intent) {
110                String action = intent.getAction();
111                if (Intent.ACTION_USER_REMOVED.equals(action)) {
112                    removeClipboard(intent.getIntExtra(Intent.EXTRA_USER_HANDLE, 0));
113                }
114            }
115        }, userFilter);
116    }
117
118    @Override
119    public boolean onTransact(int code, Parcel data, Parcel reply, int flags)
120            throws RemoteException {
121        try {
122            return super.onTransact(code, data, reply, flags);
123        } catch (RuntimeException e) {
124            if (!(e instanceof SecurityException)) {
125                Slog.wtf("clipboard", "Exception: ", e);
126            }
127            throw e;
128        }
129
130    }
131
132    private PerUserClipboard getClipboard() {
133        return getClipboard(UserHandle.getCallingUserId());
134    }
135
136    private PerUserClipboard getClipboard(int userId) {
137        synchronized (mClipboards) {
138            PerUserClipboard puc = mClipboards.get(userId);
139            if (puc == null) {
140                puc = new PerUserClipboard(userId);
141                mClipboards.put(userId, puc);
142            }
143            return puc;
144        }
145    }
146
147    private void removeClipboard(int userId) {
148        synchronized (mClipboards) {
149            mClipboards.remove(userId);
150        }
151    }
152
153    public void setPrimaryClip(ClipData clip, String callingPackage) {
154        synchronized (this) {
155            if (clip != null && clip.getItemCount() <= 0) {
156                throw new IllegalArgumentException("No items");
157            }
158            final int callingUid = Binder.getCallingUid();
159            if (mAppOps.noteOp(AppOpsManager.OP_WRITE_CLIPBOARD, callingUid,
160                    callingPackage) != AppOpsManager.MODE_ALLOWED) {
161                return;
162            }
163            checkDataOwnerLocked(clip, callingUid);
164            clearActiveOwnersLocked();
165            PerUserClipboard clipboard = getClipboard();
166            clipboard.primaryClip = clip;
167            final long ident = Binder.clearCallingIdentity();
168            final int n = clipboard.primaryClipListeners.beginBroadcast();
169            try {
170                for (int i = 0; i < n; i++) {
171                    try {
172                        ListenerInfo li = (ListenerInfo)
173                                clipboard.primaryClipListeners.getBroadcastCookie(i);
174                        if (mAppOps.checkOpNoThrow(AppOpsManager.OP_READ_CLIPBOARD, li.mUid,
175                                li.mPackageName) == AppOpsManager.MODE_ALLOWED) {
176                            clipboard.primaryClipListeners.getBroadcastItem(i)
177                                    .dispatchPrimaryClipChanged();
178                        }
179                    } catch (RemoteException e) {
180                        // The RemoteCallbackList will take care of removing
181                        // the dead object for us.
182                    }
183                }
184            } finally {
185                clipboard.primaryClipListeners.finishBroadcast();
186                Binder.restoreCallingIdentity(ident);
187            }
188        }
189    }
190
191    public ClipData getPrimaryClip(String pkg) {
192        synchronized (this) {
193            if (mAppOps.noteOp(AppOpsManager.OP_READ_CLIPBOARD, Binder.getCallingUid(),
194                    pkg) != AppOpsManager.MODE_ALLOWED) {
195                return null;
196            }
197            addActiveOwnerLocked(Binder.getCallingUid(), pkg);
198            return getClipboard().primaryClip;
199        }
200    }
201
202    public ClipDescription getPrimaryClipDescription(String callingPackage) {
203        synchronized (this) {
204            if (mAppOps.checkOp(AppOpsManager.OP_READ_CLIPBOARD, Binder.getCallingUid(),
205                    callingPackage) != AppOpsManager.MODE_ALLOWED) {
206                return null;
207            }
208            PerUserClipboard clipboard = getClipboard();
209            return clipboard.primaryClip != null ? clipboard.primaryClip.getDescription() : null;
210        }
211    }
212
213    public boolean hasPrimaryClip(String callingPackage) {
214        synchronized (this) {
215            if (mAppOps.checkOp(AppOpsManager.OP_READ_CLIPBOARD, Binder.getCallingUid(),
216                    callingPackage) != AppOpsManager.MODE_ALLOWED) {
217                return false;
218            }
219            return getClipboard().primaryClip != null;
220        }
221    }
222
223    public void addPrimaryClipChangedListener(IOnPrimaryClipChangedListener listener,
224            String callingPackage) {
225        synchronized (this) {
226            getClipboard().primaryClipListeners.register(listener,
227                    new ListenerInfo(Binder.getCallingUid(), callingPackage));
228        }
229    }
230
231    public void removePrimaryClipChangedListener(IOnPrimaryClipChangedListener listener) {
232        synchronized (this) {
233            getClipboard().primaryClipListeners.unregister(listener);
234        }
235    }
236
237    public boolean hasClipboardText(String callingPackage) {
238        synchronized (this) {
239            if (mAppOps.checkOp(AppOpsManager.OP_READ_CLIPBOARD, Binder.getCallingUid(),
240                    callingPackage) != AppOpsManager.MODE_ALLOWED) {
241                return false;
242            }
243            PerUserClipboard clipboard = getClipboard();
244            if (clipboard.primaryClip != null) {
245                CharSequence text = clipboard.primaryClip.getItemAt(0).getText();
246                return text != null && text.length() > 0;
247            }
248            return false;
249        }
250    }
251
252    private final void checkUriOwnerLocked(Uri uri, int uid) {
253        if (!"content".equals(uri.getScheme())) {
254            return;
255        }
256        long ident = Binder.clearCallingIdentity();
257        try {
258            // This will throw SecurityException for us.
259            mAm.checkGrantUriPermission(uid, null, ContentProvider.getUriWithoutUserId(uri),
260                    Intent.FLAG_GRANT_READ_URI_PERMISSION, resolveUserId(uri, uid));
261        } catch (RemoteException e) {
262        } finally {
263            Binder.restoreCallingIdentity(ident);
264        }
265    }
266
267    private final void checkItemOwnerLocked(ClipData.Item item, int uid) {
268        if (item.getUri() != null) {
269            checkUriOwnerLocked(item.getUri(), uid);
270        }
271        Intent intent = item.getIntent();
272        if (intent != null && intent.getData() != null) {
273            checkUriOwnerLocked(intent.getData(), uid);
274        }
275    }
276
277    private final void checkDataOwnerLocked(ClipData data, int uid) {
278        final int N = data.getItemCount();
279        for (int i=0; i<N; i++) {
280            checkItemOwnerLocked(data.getItemAt(i), uid);
281        }
282    }
283
284    private final void grantUriLocked(Uri uri, String pkg) {
285        long ident = Binder.clearCallingIdentity();
286        try {
287            mAm.grantUriPermissionFromOwner(mPermissionOwner, Process.myUid(), pkg,
288                    ContentProvider.getUriWithoutUserId(uri),
289                    Intent.FLAG_GRANT_READ_URI_PERMISSION,
290                    resolveUserId(uri, Process.myUid()));
291        } catch (RemoteException e) {
292        } finally {
293            Binder.restoreCallingIdentity(ident);
294        }
295    }
296
297    private final void grantItemLocked(ClipData.Item item, String pkg) {
298        if (item.getUri() != null) {
299            grantUriLocked(item.getUri(), pkg);
300        }
301        Intent intent = item.getIntent();
302        if (intent != null && intent.getData() != null) {
303            grantUriLocked(intent.getData(), pkg);
304        }
305    }
306
307    private final void addActiveOwnerLocked(int uid, String pkg) {
308        final IPackageManager pm = AppGlobals.getPackageManager();
309        final int targetUserHandle = UserHandle.getCallingUserId();
310        final long oldIdentity = Binder.clearCallingIdentity();
311        try {
312            PackageInfo pi = pm.getPackageInfo(pkg, 0, targetUserHandle);
313            if (pi == null) {
314                throw new IllegalArgumentException("Unknown package " + pkg);
315            }
316            if (!UserHandle.isSameApp(pi.applicationInfo.uid, uid)) {
317                throw new SecurityException("Calling uid " + uid
318                        + " does not own package " + pkg);
319            }
320        } catch (RemoteException e) {
321            // Can't happen; the package manager is in the same process
322        } finally {
323            Binder.restoreCallingIdentity(oldIdentity);
324        }
325        PerUserClipboard clipboard = getClipboard();
326        if (clipboard.primaryClip != null && !clipboard.activePermissionOwners.contains(pkg)) {
327            final int N = clipboard.primaryClip.getItemCount();
328            for (int i=0; i<N; i++) {
329                grantItemLocked(clipboard.primaryClip.getItemAt(i), pkg);
330            }
331            clipboard.activePermissionOwners.add(pkg);
332        }
333    }
334
335    private final void revokeUriLocked(Uri uri) {
336        long ident = Binder.clearCallingIdentity();
337        try {
338            mAm.revokeUriPermissionFromOwner(mPermissionOwner,
339                    ContentProvider.getUriWithoutUserId(uri),
340                    Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION,
341                    resolveUserId(uri, Process.myUid()));
342        } catch (RemoteException e) {
343        } finally {
344            Binder.restoreCallingIdentity(ident);
345        }
346    }
347
348    private final void revokeItemLocked(ClipData.Item item) {
349        if (item.getUri() != null) {
350            revokeUriLocked(item.getUri());
351        }
352        Intent intent = item.getIntent();
353        if (intent != null && intent.getData() != null) {
354            revokeUriLocked(intent.getData());
355        }
356    }
357
358    private final void clearActiveOwnersLocked() {
359        PerUserClipboard clipboard = getClipboard();
360        clipboard.activePermissionOwners.clear();
361        if (clipboard.primaryClip == null) {
362            return;
363        }
364        final int N = clipboard.primaryClip.getItemCount();
365        for (int i=0; i<N; i++) {
366            revokeItemLocked(clipboard.primaryClip.getItemAt(i));
367        }
368    }
369
370    private final int resolveUserId(Uri uri, int uid) {
371        return ContentProvider.getUserIdFromUri(uri, UserHandle.getUserId(uid));
372    }
373}
374