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