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