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.ActivityManager;
20import android.app.AppGlobals;
21import android.app.AppOpsManager;
22import android.app.IActivityManager;
23import android.content.ClipData;
24import android.content.ClipDescription;
25import android.content.ContentProvider;
26import android.content.IClipboard;
27import android.content.IOnPrimaryClipChangedListener;
28import android.content.Context;
29import android.content.Intent;
30import android.content.pm.IPackageManager;
31import android.content.pm.PackageInfo;
32import android.content.pm.PackageManager;
33import android.content.pm.UserInfo;
34import android.net.Uri;
35import android.os.Binder;
36import android.os.IBinder;
37import android.os.IUserManager;
38import android.os.Parcel;
39import android.os.Process;
40import android.os.RemoteCallbackList;
41import android.os.RemoteException;
42import android.os.ServiceManager;
43import android.os.SystemProperties;
44import android.os.UserHandle;
45import android.os.UserManager;
46import android.util.Slog;
47import android.util.SparseArray;
48
49import com.android.server.SystemService;
50
51import java.util.HashSet;
52import java.util.List;
53
54import java.lang.Thread;
55import java.lang.Runnable;
56import java.lang.InterruptedException;
57import java.io.IOException;
58import java.io.RandomAccessFile;
59
60// The following class is Android Emulator specific. It is used to read and
61// write contents of the host system's clipboard.
62class HostClipboardMonitor implements Runnable {
63    public interface HostClipboardCallback {
64        void onHostClipboardUpdated(String contents);
65    }
66
67    private RandomAccessFile mPipe = null;
68    private HostClipboardCallback mHostClipboardCallback;
69    private static final String PIPE_NAME = "pipe:clipboard";
70    private static final String PIPE_DEVICE = "/dev/qemu_pipe";
71
72    private void openPipe() {
73        try {
74            // String.getBytes doesn't include the null terminator,
75            // but the QEMU pipe device requires the pipe service name
76            // to be null-terminated.
77            byte[] b = new byte[PIPE_NAME.length() + 1];
78            b[PIPE_NAME.length()] = 0;
79            System.arraycopy(
80                PIPE_NAME.getBytes(),
81                0,
82                b,
83                0,
84                PIPE_NAME.length());
85            mPipe = new RandomAccessFile(PIPE_DEVICE, "rw");
86            mPipe.write(b);
87        } catch (IOException e) {
88            try {
89                if (mPipe != null) mPipe.close();
90            } catch (IOException ee) {}
91            mPipe = null;
92        }
93    }
94
95    public HostClipboardMonitor(HostClipboardCallback cb) {
96        mHostClipboardCallback = cb;
97    }
98
99    @Override
100    public void run() {
101        while(!Thread.interrupted()) {
102            try {
103                // There's no guarantee that QEMU pipes will be ready at the moment
104                // this method is invoked. We simply try to get the pipe open and
105                // retry on failure indefinitely.
106                while (mPipe == null) {
107                    openPipe();
108                    Thread.sleep(100);
109                }
110                int size = mPipe.readInt();
111                size = Integer.reverseBytes(size);
112                byte[] receivedData = new byte[size];
113                mPipe.readFully(receivedData);
114                mHostClipboardCallback.onHostClipboardUpdated(
115                    new String(receivedData));
116            } catch (IOException e) {
117                try {
118                    mPipe.close();
119                } catch (IOException ee) {}
120                mPipe = null;
121            } catch (InterruptedException e) {}
122        }
123    }
124
125    public void setHostClipboard(String content) {
126        try {
127            if (mPipe != null) {
128                mPipe.writeInt(Integer.reverseBytes(content.getBytes().length));
129                mPipe.write(content.getBytes());
130            }
131        } catch(IOException e) {
132            Slog.e("HostClipboardMonitor",
133                   "Failed to set host clipboard " + e.getMessage());
134        }
135    }
136}
137
138/**
139 * Implementation of the clipboard for copy and paste.
140 */
141public class ClipboardService extends SystemService {
142
143    private static final String TAG = "ClipboardService";
144    private static final boolean IS_EMULATOR =
145        SystemProperties.getBoolean("ro.kernel.qemu", false);
146
147    private final IActivityManager mAm;
148    private final IUserManager mUm;
149    private final PackageManager mPm;
150    private final AppOpsManager mAppOps;
151    private final IBinder mPermissionOwner;
152    private HostClipboardMonitor mHostClipboardMonitor = null;
153    private Thread mHostMonitorThread = null;
154
155    private final SparseArray<PerUserClipboard> mClipboards = new SparseArray<>();
156
157    /**
158     * Instantiates the clipboard.
159     */
160    public ClipboardService(Context context) {
161        super(context);
162
163        mAm = ActivityManager.getService();
164        mPm = getContext().getPackageManager();
165        mUm = (IUserManager) ServiceManager.getService(Context.USER_SERVICE);
166        mAppOps = (AppOpsManager) getContext().getSystemService(Context.APP_OPS_SERVICE);
167        IBinder permOwner = null;
168        try {
169            permOwner = mAm.newUriPermissionOwner("clipboard");
170        } catch (RemoteException e) {
171            Slog.w("clipboard", "AM dead", e);
172        }
173        mPermissionOwner = permOwner;
174        if (IS_EMULATOR) {
175            mHostClipboardMonitor = new HostClipboardMonitor(
176                new HostClipboardMonitor.HostClipboardCallback() {
177                    @Override
178                    public void onHostClipboardUpdated(String contents){
179                        ClipData clip =
180                            new ClipData("host clipboard",
181                                         new String[]{"text/plain"},
182                                         new ClipData.Item(contents));
183                        synchronized(mClipboards) {
184                            setPrimaryClipInternal(getClipboard(0), clip);
185                        }
186                    }
187                });
188            mHostMonitorThread = new Thread(mHostClipboardMonitor);
189            mHostMonitorThread.start();
190        }
191    }
192
193    @Override
194    public void onStart() {
195        publishBinderService(Context.CLIPBOARD_SERVICE, new ClipboardImpl());
196    }
197
198    @Override
199    public void onCleanupUser(int userId) {
200        synchronized (mClipboards) {
201            mClipboards.remove(userId);
202        }
203    }
204
205    private class ListenerInfo {
206        final int mUid;
207        final String mPackageName;
208        ListenerInfo(int uid, String packageName) {
209            mUid = uid;
210            mPackageName = packageName;
211        }
212    }
213
214    private class PerUserClipboard {
215        final int userId;
216
217        final RemoteCallbackList<IOnPrimaryClipChangedListener> primaryClipListeners
218                = new RemoteCallbackList<IOnPrimaryClipChangedListener>();
219
220        ClipData primaryClip;
221
222        final HashSet<String> activePermissionOwners
223                = new HashSet<String>();
224
225        PerUserClipboard(int userId) {
226            this.userId = userId;
227        }
228    }
229
230    private class ClipboardImpl extends IClipboard.Stub {
231        @Override
232        public boolean onTransact(int code, Parcel data, Parcel reply, int flags)
233                throws RemoteException {
234            try {
235                return super.onTransact(code, data, reply, flags);
236            } catch (RuntimeException e) {
237                if (!(e instanceof SecurityException)) {
238                    Slog.wtf("clipboard", "Exception: ", e);
239                }
240                throw e;
241            }
242
243        }
244
245        @Override
246        public void setPrimaryClip(ClipData clip, String callingPackage) {
247            synchronized (this) {
248                if (clip != null && clip.getItemCount() <= 0) {
249                    throw new IllegalArgumentException("No items");
250                }
251                if (clip.getItemAt(0).getText() != null &&
252                    mHostClipboardMonitor != null) {
253                    mHostClipboardMonitor.setHostClipboard(
254                        clip.getItemAt(0).getText().toString());
255                }
256                final int callingUid = Binder.getCallingUid();
257                if (!clipboardAccessAllowed(AppOpsManager.OP_WRITE_CLIPBOARD, callingPackage,
258                            callingUid)) {
259                    return;
260                }
261                checkDataOwnerLocked(clip, callingUid);
262                final int userId = UserHandle.getUserId(callingUid);
263                PerUserClipboard clipboard = getClipboard(userId);
264                revokeUris(clipboard);
265                setPrimaryClipInternal(clipboard, clip);
266                List<UserInfo> related = getRelatedProfiles(userId);
267                if (related != null) {
268                    int size = related.size();
269                    if (size > 1) { // Related profiles list include the current profile.
270                        boolean canCopy = false;
271                        try {
272                            canCopy = !mUm.getUserRestrictions(userId).getBoolean(
273                                    UserManager.DISALLOW_CROSS_PROFILE_COPY_PASTE);
274                        } catch (RemoteException e) {
275                            Slog.e(TAG, "Remote Exception calling UserManager: " + e);
276                        }
277                        // Copy clip data to related users if allowed. If disallowed, then remove
278                        // primary clip in related users to prevent pasting stale content.
279                        if (!canCopy) {
280                            clip = null;
281                        } else {
282                            // We want to fix the uris of the related user's clip without changing the
283                            // uris of the current user's clip.
284                            // So, copy the ClipData, and then copy all the items, so that nothing
285                            // is shared in memmory.
286                            clip = new ClipData(clip);
287                            for (int i = clip.getItemCount() - 1; i >= 0; i--) {
288                                clip.setItemAt(i, new ClipData.Item(clip.getItemAt(i)));
289                            }
290                            clip.fixUrisLight(userId);
291                        }
292                        for (int i = 0; i < size; i++) {
293                            int id = related.get(i).id;
294                            if (id != userId) {
295                                setPrimaryClipInternal(getClipboard(id), clip);
296                            }
297                        }
298                    }
299                }
300            }
301        }
302
303        @Override
304        public ClipData getPrimaryClip(String pkg) {
305            synchronized (this) {
306                if (!clipboardAccessAllowed(AppOpsManager.OP_READ_CLIPBOARD, pkg,
307                            Binder.getCallingUid())) {
308                    return null;
309                }
310                addActiveOwnerLocked(Binder.getCallingUid(), pkg);
311                return getClipboard().primaryClip;
312            }
313        }
314
315        @Override
316        public ClipDescription getPrimaryClipDescription(String callingPackage) {
317            synchronized (this) {
318                if (!clipboardAccessAllowed(AppOpsManager.OP_READ_CLIPBOARD, callingPackage,
319                            Binder.getCallingUid())) {
320                    return null;
321                }
322                PerUserClipboard clipboard = getClipboard();
323                return clipboard.primaryClip != null ? clipboard.primaryClip.getDescription() : null;
324            }
325        }
326
327        @Override
328        public boolean hasPrimaryClip(String callingPackage) {
329            synchronized (this) {
330                if (!clipboardAccessAllowed(AppOpsManager.OP_READ_CLIPBOARD, callingPackage,
331                            Binder.getCallingUid())) {
332                    return false;
333                }
334                return getClipboard().primaryClip != null;
335            }
336        }
337
338        @Override
339        public void addPrimaryClipChangedListener(IOnPrimaryClipChangedListener listener,
340                String callingPackage) {
341            synchronized (this) {
342                getClipboard().primaryClipListeners.register(listener,
343                        new ListenerInfo(Binder.getCallingUid(), callingPackage));
344            }
345        }
346
347        @Override
348        public void removePrimaryClipChangedListener(IOnPrimaryClipChangedListener listener) {
349            synchronized (this) {
350                getClipboard().primaryClipListeners.unregister(listener);
351            }
352        }
353
354        @Override
355        public boolean hasClipboardText(String callingPackage) {
356            synchronized (this) {
357                if (!clipboardAccessAllowed(AppOpsManager.OP_READ_CLIPBOARD, callingPackage,
358                            Binder.getCallingUid())) {
359                    return false;
360                }
361                PerUserClipboard clipboard = getClipboard();
362                if (clipboard.primaryClip != null) {
363                    CharSequence text = clipboard.primaryClip.getItemAt(0).getText();
364                    return text != null && text.length() > 0;
365                }
366                return false;
367            }
368        }
369    };
370
371    private PerUserClipboard getClipboard() {
372        return getClipboard(UserHandle.getCallingUserId());
373    }
374
375    private PerUserClipboard getClipboard(int userId) {
376        synchronized (mClipboards) {
377            PerUserClipboard puc = mClipboards.get(userId);
378            if (puc == null) {
379                puc = new PerUserClipboard(userId);
380                mClipboards.put(userId, puc);
381            }
382            return puc;
383        }
384    }
385
386    List<UserInfo> getRelatedProfiles(int userId) {
387        final List<UserInfo> related;
388        final long origId = Binder.clearCallingIdentity();
389        try {
390            related = mUm.getProfiles(userId, true);
391        } catch (RemoteException e) {
392            Slog.e(TAG, "Remote Exception calling UserManager: " + e);
393            return null;
394        } finally{
395            Binder.restoreCallingIdentity(origId);
396        }
397        return related;
398    }
399
400    void setPrimaryClipInternal(PerUserClipboard clipboard, ClipData clip) {
401        clipboard.activePermissionOwners.clear();
402        if (clip == null && clipboard.primaryClip == null) {
403            return;
404        }
405        clipboard.primaryClip = clip;
406        if (clip != null) {
407            final ClipDescription description = clip.getDescription();
408            if (description != null) {
409                description.setTimestamp(System.currentTimeMillis());
410            }
411        }
412        final long ident = Binder.clearCallingIdentity();
413        final int n = clipboard.primaryClipListeners.beginBroadcast();
414        try {
415            for (int i = 0; i < n; i++) {
416                try {
417                    ListenerInfo li = (ListenerInfo)
418                            clipboard.primaryClipListeners.getBroadcastCookie(i);
419
420                    if (clipboardAccessAllowed(AppOpsManager.OP_READ_CLIPBOARD, li.mPackageName,
421                                li.mUid)) {
422                        clipboard.primaryClipListeners.getBroadcastItem(i)
423                                .dispatchPrimaryClipChanged();
424                    }
425                } catch (RemoteException e) {
426                    // The RemoteCallbackList will take care of removing
427                    // the dead object for us.
428                }
429            }
430        } finally {
431            clipboard.primaryClipListeners.finishBroadcast();
432            Binder.restoreCallingIdentity(ident);
433        }
434    }
435
436    private final void checkUriOwnerLocked(Uri uri, int uid) {
437        if (!"content".equals(uri.getScheme())) {
438            return;
439        }
440        long ident = Binder.clearCallingIdentity();
441        try {
442            // This will throw SecurityException for us.
443            mAm.checkGrantUriPermission(uid, null, ContentProvider.getUriWithoutUserId(uri),
444                    Intent.FLAG_GRANT_READ_URI_PERMISSION,
445                    ContentProvider.getUserIdFromUri(uri, UserHandle.getUserId(uid)));
446        } catch (RemoteException e) {
447        } finally {
448            Binder.restoreCallingIdentity(ident);
449        }
450    }
451
452    private final void checkItemOwnerLocked(ClipData.Item item, int uid) {
453        if (item.getUri() != null) {
454            checkUriOwnerLocked(item.getUri(), uid);
455        }
456        Intent intent = item.getIntent();
457        if (intent != null && intent.getData() != null) {
458            checkUriOwnerLocked(intent.getData(), uid);
459        }
460    }
461
462    private final void checkDataOwnerLocked(ClipData data, int uid) {
463        final int N = data.getItemCount();
464        for (int i=0; i<N; i++) {
465            checkItemOwnerLocked(data.getItemAt(i), uid);
466        }
467    }
468
469    private final void grantUriLocked(Uri uri, String pkg, int userId) {
470        long ident = Binder.clearCallingIdentity();
471        try {
472            int sourceUserId = ContentProvider.getUserIdFromUri(uri, userId);
473            uri = ContentProvider.getUriWithoutUserId(uri);
474            mAm.grantUriPermissionFromOwner(mPermissionOwner, Process.myUid(), pkg,
475                    uri, Intent.FLAG_GRANT_READ_URI_PERMISSION, sourceUserId, userId);
476        } catch (RemoteException e) {
477        } finally {
478            Binder.restoreCallingIdentity(ident);
479        }
480    }
481
482    private final void grantItemLocked(ClipData.Item item, String pkg, int userId) {
483        if (item.getUri() != null) {
484            grantUriLocked(item.getUri(), pkg, userId);
485        }
486        Intent intent = item.getIntent();
487        if (intent != null && intent.getData() != null) {
488            grantUriLocked(intent.getData(), pkg, userId);
489        }
490    }
491
492    private final void addActiveOwnerLocked(int uid, String pkg) {
493        final IPackageManager pm = AppGlobals.getPackageManager();
494        final int targetUserHandle = UserHandle.getCallingUserId();
495        final long oldIdentity = Binder.clearCallingIdentity();
496        try {
497            PackageInfo pi = pm.getPackageInfo(pkg, 0, targetUserHandle);
498            if (pi == null) {
499                throw new IllegalArgumentException("Unknown package " + pkg);
500            }
501            if (!UserHandle.isSameApp(pi.applicationInfo.uid, uid)) {
502                throw new SecurityException("Calling uid " + uid
503                        + " does not own package " + pkg);
504            }
505        } catch (RemoteException e) {
506            // Can't happen; the package manager is in the same process
507        } finally {
508            Binder.restoreCallingIdentity(oldIdentity);
509        }
510        PerUserClipboard clipboard = getClipboard();
511        if (clipboard.primaryClip != null && !clipboard.activePermissionOwners.contains(pkg)) {
512            final int N = clipboard.primaryClip.getItemCount();
513            for (int i=0; i<N; i++) {
514                grantItemLocked(clipboard.primaryClip.getItemAt(i), pkg, UserHandle.getUserId(uid));
515            }
516            clipboard.activePermissionOwners.add(pkg);
517        }
518    }
519
520    private final void revokeUriLocked(Uri uri) {
521        int userId = ContentProvider.getUserIdFromUri(uri,
522                UserHandle.getUserId(Binder.getCallingUid()));
523        long ident = Binder.clearCallingIdentity();
524        try {
525            uri = ContentProvider.getUriWithoutUserId(uri);
526            mAm.revokeUriPermissionFromOwner(mPermissionOwner, uri,
527                    Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION,
528                    userId);
529        } catch (RemoteException e) {
530        } finally {
531            Binder.restoreCallingIdentity(ident);
532        }
533    }
534
535    private final void revokeItemLocked(ClipData.Item item) {
536        if (item.getUri() != null) {
537            revokeUriLocked(item.getUri());
538        }
539        Intent intent = item.getIntent();
540        if (intent != null && intent.getData() != null) {
541            revokeUriLocked(intent.getData());
542        }
543    }
544
545    private final void revokeUris(PerUserClipboard clipboard) {
546        if (clipboard.primaryClip == null) {
547            return;
548        }
549        final int N = clipboard.primaryClip.getItemCount();
550        for (int i=0; i<N; i++) {
551            revokeItemLocked(clipboard.primaryClip.getItemAt(i));
552        }
553    }
554
555    private boolean clipboardAccessAllowed(int op, String callingPackage, int callingUid) {
556        // Check the AppOp.
557        if (mAppOps.checkOp(op, callingUid, callingPackage) != AppOpsManager.MODE_ALLOWED) {
558            return false;
559        }
560        try {
561            // Installed apps can access the clipboard at any time.
562            if (!AppGlobals.getPackageManager().isInstantApp(callingPackage,
563                        UserHandle.getUserId(callingUid))) {
564                return true;
565            }
566            // Instant apps can only access the clipboard if they are in the foreground.
567            return mAm.isAppForeground(callingUid);
568        } catch (RemoteException e) {
569            Slog.e("clipboard", "Failed to get Instant App status for package " + callingPackage,
570                    e);
571            return false;
572        }
573    }
574}
575