diff options
Diffstat (limited to 'services/core/java/com/android/server/clipboard/ClipboardService.java')
-rw-r--r-- | services/core/java/com/android/server/clipboard/ClipboardService.java | 366 |
1 files changed, 366 insertions, 0 deletions
diff --git a/services/core/java/com/android/server/clipboard/ClipboardService.java b/services/core/java/com/android/server/clipboard/ClipboardService.java new file mode 100644 index 0000000..6aa596d --- /dev/null +++ b/services/core/java/com/android/server/clipboard/ClipboardService.java @@ -0,0 +1,366 @@ +/* + * Copyright (C) 2008 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.clipboard; + +import android.app.ActivityManagerNative; +import android.app.AppGlobals; +import android.app.AppOpsManager; +import android.app.IActivityManager; +import android.content.BroadcastReceiver; +import android.content.ClipData; +import android.content.ClipDescription; +import android.content.IClipboard; +import android.content.IOnPrimaryClipChangedListener; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.content.pm.IPackageManager; +import android.content.pm.PackageInfo; +import android.content.pm.PackageManager; +import android.content.pm.PackageManager.NameNotFoundException; +import android.net.Uri; +import android.os.Binder; +import android.os.IBinder; +import android.os.Parcel; +import android.os.Process; +import android.os.RemoteCallbackList; +import android.os.RemoteException; +import android.os.UserHandle; +import android.util.Pair; +import android.util.Slog; +import android.util.SparseArray; + +import java.util.HashSet; + +/** + * Implementation of the clipboard for copy and paste. + */ +public class ClipboardService extends IClipboard.Stub { + + private static final String TAG = "ClipboardService"; + + private final Context mContext; + private final IActivityManager mAm; + private final PackageManager mPm; + private final AppOpsManager mAppOps; + private final IBinder mPermissionOwner; + + private class ListenerInfo { + final int mUid; + final String mPackageName; + ListenerInfo(int uid, String packageName) { + mUid = uid; + mPackageName = packageName; + } + } + + private class PerUserClipboard { + final int userId; + + final RemoteCallbackList<IOnPrimaryClipChangedListener> primaryClipListeners + = new RemoteCallbackList<IOnPrimaryClipChangedListener>(); + + ClipData primaryClip; + + final HashSet<String> activePermissionOwners + = new HashSet<String>(); + + PerUserClipboard(int userId) { + this.userId = userId; + } + } + + private SparseArray<PerUserClipboard> mClipboards = new SparseArray<PerUserClipboard>(); + + /** + * Instantiates the clipboard. + */ + public ClipboardService(Context context) { + mContext = context; + mAm = ActivityManagerNative.getDefault(); + mPm = context.getPackageManager(); + mAppOps = (AppOpsManager)context.getSystemService(Context.APP_OPS_SERVICE); + IBinder permOwner = null; + try { + permOwner = mAm.newUriPermissionOwner("clipboard"); + } catch (RemoteException e) { + Slog.w("clipboard", "AM dead", e); + } + mPermissionOwner = permOwner; + + // Remove the clipboard if a user is removed + IntentFilter userFilter = new IntentFilter(); + userFilter.addAction(Intent.ACTION_USER_REMOVED); + mContext.registerReceiver(new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + String action = intent.getAction(); + if (Intent.ACTION_USER_REMOVED.equals(action)) { + removeClipboard(intent.getIntExtra(Intent.EXTRA_USER_HANDLE, 0)); + } + } + }, userFilter); + } + + @Override + public boolean onTransact(int code, Parcel data, Parcel reply, int flags) + throws RemoteException { + try { + return super.onTransact(code, data, reply, flags); + } catch (RuntimeException e) { + if (!(e instanceof SecurityException)) { + Slog.wtf("clipboard", "Exception: ", e); + } + throw e; + } + + } + + private PerUserClipboard getClipboard() { + return getClipboard(UserHandle.getCallingUserId()); + } + + private PerUserClipboard getClipboard(int userId) { + synchronized (mClipboards) { + PerUserClipboard puc = mClipboards.get(userId); + if (puc == null) { + puc = new PerUserClipboard(userId); + mClipboards.put(userId, puc); + } + return puc; + } + } + + private void removeClipboard(int userId) { + synchronized (mClipboards) { + mClipboards.remove(userId); + } + } + + public void setPrimaryClip(ClipData clip, String callingPackage) { + synchronized (this) { + if (clip != null && clip.getItemCount() <= 0) { + throw new IllegalArgumentException("No items"); + } + final int callingUid = Binder.getCallingUid(); + if (mAppOps.noteOp(AppOpsManager.OP_WRITE_CLIPBOARD, callingUid, + callingPackage) != AppOpsManager.MODE_ALLOWED) { + return; + } + checkDataOwnerLocked(clip, callingUid); + clearActiveOwnersLocked(); + PerUserClipboard clipboard = getClipboard(); + clipboard.primaryClip = clip; + final long ident = Binder.clearCallingIdentity(); + final int n = clipboard.primaryClipListeners.beginBroadcast(); + try { + for (int i = 0; i < n; i++) { + try { + ListenerInfo li = (ListenerInfo) + clipboard.primaryClipListeners.getBroadcastCookie(i); + if (mAppOps.checkOpNoThrow(AppOpsManager.OP_READ_CLIPBOARD, li.mUid, + li.mPackageName) == AppOpsManager.MODE_ALLOWED) { + clipboard.primaryClipListeners.getBroadcastItem(i) + .dispatchPrimaryClipChanged(); + } + } catch (RemoteException e) { + // The RemoteCallbackList will take care of removing + // the dead object for us. + } + } + } finally { + clipboard.primaryClipListeners.finishBroadcast(); + Binder.restoreCallingIdentity(ident); + } + } + } + + public ClipData getPrimaryClip(String pkg) { + synchronized (this) { + if (mAppOps.noteOp(AppOpsManager.OP_READ_CLIPBOARD, Binder.getCallingUid(), + pkg) != AppOpsManager.MODE_ALLOWED) { + return null; + } + addActiveOwnerLocked(Binder.getCallingUid(), pkg); + return getClipboard().primaryClip; + } + } + + public ClipDescription getPrimaryClipDescription(String callingPackage) { + synchronized (this) { + if (mAppOps.checkOp(AppOpsManager.OP_READ_CLIPBOARD, Binder.getCallingUid(), + callingPackage) != AppOpsManager.MODE_ALLOWED) { + return null; + } + PerUserClipboard clipboard = getClipboard(); + return clipboard.primaryClip != null ? clipboard.primaryClip.getDescription() : null; + } + } + + public boolean hasPrimaryClip(String callingPackage) { + synchronized (this) { + if (mAppOps.checkOp(AppOpsManager.OP_READ_CLIPBOARD, Binder.getCallingUid(), + callingPackage) != AppOpsManager.MODE_ALLOWED) { + return false; + } + return getClipboard().primaryClip != null; + } + } + + public void addPrimaryClipChangedListener(IOnPrimaryClipChangedListener listener, + String callingPackage) { + synchronized (this) { + getClipboard().primaryClipListeners.register(listener, + new ListenerInfo(Binder.getCallingUid(), callingPackage)); + } + } + + public void removePrimaryClipChangedListener(IOnPrimaryClipChangedListener listener) { + synchronized (this) { + getClipboard().primaryClipListeners.unregister(listener); + } + } + + public boolean hasClipboardText(String callingPackage) { + synchronized (this) { + if (mAppOps.checkOp(AppOpsManager.OP_READ_CLIPBOARD, Binder.getCallingUid(), + callingPackage) != AppOpsManager.MODE_ALLOWED) { + return false; + } + PerUserClipboard clipboard = getClipboard(); + if (clipboard.primaryClip != null) { + CharSequence text = clipboard.primaryClip.getItemAt(0).getText(); + return text != null && text.length() > 0; + } + return false; + } + } + + private final void checkUriOwnerLocked(Uri uri, int uid) { + if (!"content".equals(uri.getScheme())) { + return; + } + long ident = Binder.clearCallingIdentity(); + try { + // This will throw SecurityException for us. + mAm.checkGrantUriPermission(uid, null, uri, Intent.FLAG_GRANT_READ_URI_PERMISSION); + } catch (RemoteException e) { + } finally { + Binder.restoreCallingIdentity(ident); + } + } + + private final void checkItemOwnerLocked(ClipData.Item item, int uid) { + if (item.getUri() != null) { + checkUriOwnerLocked(item.getUri(), uid); + } + Intent intent = item.getIntent(); + if (intent != null && intent.getData() != null) { + checkUriOwnerLocked(intent.getData(), uid); + } + } + + private final void checkDataOwnerLocked(ClipData data, int uid) { + final int N = data.getItemCount(); + for (int i=0; i<N; i++) { + checkItemOwnerLocked(data.getItemAt(i), uid); + } + } + + private final void grantUriLocked(Uri uri, String pkg) { + long ident = Binder.clearCallingIdentity(); + try { + mAm.grantUriPermissionFromOwner(mPermissionOwner, Process.myUid(), pkg, uri, + Intent.FLAG_GRANT_READ_URI_PERMISSION); + } catch (RemoteException e) { + } finally { + Binder.restoreCallingIdentity(ident); + } + } + + private final void grantItemLocked(ClipData.Item item, String pkg) { + if (item.getUri() != null) { + grantUriLocked(item.getUri(), pkg); + } + Intent intent = item.getIntent(); + if (intent != null && intent.getData() != null) { + grantUriLocked(intent.getData(), pkg); + } + } + + private final void addActiveOwnerLocked(int uid, String pkg) { + final IPackageManager pm = AppGlobals.getPackageManager(); + final int targetUserHandle = UserHandle.getCallingUserId(); + final long oldIdentity = Binder.clearCallingIdentity(); + try { + PackageInfo pi = pm.getPackageInfo(pkg, 0, targetUserHandle); + if (pi == null) { + throw new IllegalArgumentException("Unknown package " + pkg); + } + if (!UserHandle.isSameApp(pi.applicationInfo.uid, uid)) { + throw new SecurityException("Calling uid " + uid + + " does not own package " + pkg); + } + } catch (RemoteException e) { + // Can't happen; the package manager is in the same process + } finally { + Binder.restoreCallingIdentity(oldIdentity); + } + PerUserClipboard clipboard = getClipboard(); + if (clipboard.primaryClip != null && !clipboard.activePermissionOwners.contains(pkg)) { + final int N = clipboard.primaryClip.getItemCount(); + for (int i=0; i<N; i++) { + grantItemLocked(clipboard.primaryClip.getItemAt(i), pkg); + } + clipboard.activePermissionOwners.add(pkg); + } + } + + private final void revokeUriLocked(Uri uri) { + long ident = Binder.clearCallingIdentity(); + try { + mAm.revokeUriPermissionFromOwner(mPermissionOwner, uri, + Intent.FLAG_GRANT_READ_URI_PERMISSION + | Intent.FLAG_GRANT_WRITE_URI_PERMISSION); + } catch (RemoteException e) { + } finally { + Binder.restoreCallingIdentity(ident); + } + } + + private final void revokeItemLocked(ClipData.Item item) { + if (item.getUri() != null) { + revokeUriLocked(item.getUri()); + } + Intent intent = item.getIntent(); + if (intent != null && intent.getData() != null) { + revokeUriLocked(intent.getData()); + } + } + + private final void clearActiveOwnersLocked() { + PerUserClipboard clipboard = getClipboard(); + clipboard.activePermissionOwners.clear(); + if (clipboard.primaryClip == null) { + return; + } + final int N = clipboard.primaryClip.getItemCount(); + for (int i=0; i<N; i++) { + revokeItemLocked(clipboard.primaryClip.getItemAt(i)); + } + } +} |