diff options
12 files changed, 358 insertions, 7 deletions
diff --git a/Android.mk b/Android.mk index 50bed92fd2db..320fe2f2ed17 100644 --- a/Android.mk +++ b/Android.mk @@ -318,6 +318,7 @@ LOCAL_SRC_FILES += \ core/java/com/android/internal/appwidget/IAppWidgetHost.aidl \ core/java/com/android/internal/backup/IBackupTransport.aidl \ core/java/com/android/internal/backup/IObbBackupService.aidl \ + core/java/com/android/internal/inputmethod/IInputContentUriToken.aidl \ core/java/com/android/internal/policy/IKeyguardDrawnCallback.aidl \ core/java/com/android/internal/policy/IKeyguardExitCallback.aidl \ core/java/com/android/internal/policy/IKeyguardService.aidl \ diff --git a/api/current.txt b/api/current.txt index dc04403799ac..49e8f68bb90e 100644 --- a/api/current.txt +++ b/api/current.txt @@ -18857,6 +18857,7 @@ package android.inputmethodservice { public class InputMethodService extends android.inputmethodservice.AbstractInputMethodService { ctor public InputMethodService(); method public deprecated boolean enableHardwareAcceleration(); + method public final boolean exposeContent(android.view.inputmethod.InputContentInfo, android.view.inputmethod.EditorInfo); method public int getBackDisposition(); method public int getCandidatesHiddenVisibility(); method public android.view.inputmethod.InputBinding getCurrentInputBinding(); @@ -44922,13 +44923,15 @@ package android.view.inputmethod { method public void setTarget(android.view.inputmethod.InputConnection); } - public class InputContentInfo implements android.os.Parcelable { + public final class InputContentInfo implements android.os.Parcelable { ctor public InputContentInfo(android.net.Uri, android.content.ClipDescription); ctor public InputContentInfo(android.net.Uri, android.content.ClipDescription, android.net.Uri); method public int describeContents(); method public android.net.Uri getContentUri(); method public android.content.ClipDescription getDescription(); method public android.net.Uri getLinkUri(); + method public void releasePermission(); + method public void requestPermission(); method public void writeToParcel(android.os.Parcel, int); field public static final android.os.Parcelable.Creator<android.view.inputmethod.InputContentInfo> CREATOR; } diff --git a/api/system-current.txt b/api/system-current.txt index e4f8d204e249..7afaf3f9fe6f 100644 --- a/api/system-current.txt +++ b/api/system-current.txt @@ -20057,6 +20057,7 @@ package android.inputmethodservice { public class InputMethodService extends android.inputmethodservice.AbstractInputMethodService { ctor public InputMethodService(); method public deprecated boolean enableHardwareAcceleration(); + method public final boolean exposeContent(android.view.inputmethod.InputContentInfo, android.view.inputmethod.EditorInfo); method public int getBackDisposition(); method public int getCandidatesHiddenVisibility(); method public android.view.inputmethod.InputBinding getCurrentInputBinding(); @@ -48029,13 +48030,15 @@ package android.view.inputmethod { method public void setTarget(android.view.inputmethod.InputConnection); } - public class InputContentInfo implements android.os.Parcelable { + public final class InputContentInfo implements android.os.Parcelable { ctor public InputContentInfo(android.net.Uri, android.content.ClipDescription); ctor public InputContentInfo(android.net.Uri, android.content.ClipDescription, android.net.Uri); method public int describeContents(); method public android.net.Uri getContentUri(); method public android.content.ClipDescription getDescription(); method public android.net.Uri getLinkUri(); + method public void releasePermission(); + method public void requestPermission(); method public void writeToParcel(android.os.Parcel, int); field public static final android.os.Parcelable.Creator<android.view.inputmethod.InputContentInfo> CREATOR; } diff --git a/api/test-current.txt b/api/test-current.txt index 2dfd18d20c51..365b8042ecac 100644 --- a/api/test-current.txt +++ b/api/test-current.txt @@ -18871,6 +18871,7 @@ package android.inputmethodservice { public class InputMethodService extends android.inputmethodservice.AbstractInputMethodService { ctor public InputMethodService(); method public deprecated boolean enableHardwareAcceleration(); + method public final boolean exposeContent(android.view.inputmethod.InputContentInfo, android.view.inputmethod.EditorInfo); method public int getBackDisposition(); method public int getCandidatesHiddenVisibility(); method public android.view.inputmethod.InputBinding getCurrentInputBinding(); @@ -45002,13 +45003,15 @@ package android.view.inputmethod { method public void setTarget(android.view.inputmethod.InputConnection); } - public class InputContentInfo implements android.os.Parcelable { + public final class InputContentInfo implements android.os.Parcelable { ctor public InputContentInfo(android.net.Uri, android.content.ClipDescription); ctor public InputContentInfo(android.net.Uri, android.content.ClipDescription, android.net.Uri); method public int describeContents(); method public android.net.Uri getContentUri(); method public android.content.ClipDescription getDescription(); method public android.net.Uri getLinkUri(); + method public void releasePermission(); + method public void requestPermission(); method public void writeToParcel(android.os.Parcel, int); field public static final android.os.Parcelable.Creator<android.view.inputmethod.InputContentInfo> CREATOR; } diff --git a/core/java/android/inputmethodservice/InputMethodService.java b/core/java/android/inputmethodservice/InputMethodService.java index 4799773d9ed6..8e0e0b0e4b9a 100644 --- a/core/java/android/inputmethodservice/InputMethodService.java +++ b/core/java/android/inputmethodservice/InputMethodService.java @@ -23,6 +23,7 @@ import android.annotation.CallSuper; import android.annotation.DrawableRes; import android.annotation.IntDef; import android.annotation.MainThread; +import android.annotation.NonNull; import android.app.ActivityManager; import android.app.Dialog; import android.content.Context; @@ -65,6 +66,7 @@ import android.view.inputmethod.ExtractedText; import android.view.inputmethod.ExtractedTextRequest; import android.view.inputmethod.InputBinding; import android.view.inputmethod.InputConnection; +import android.view.inputmethod.InputContentInfo; import android.view.inputmethod.InputMethod; import android.view.inputmethod.InputMethodManager; import android.view.inputmethod.InputMethodSubtype; @@ -2598,6 +2600,39 @@ public class InputMethodService extends AbstractInputMethodService { } /** + * Allow the receiver of {@link InputContentInfo} to obtain a temporary read-only access + * permission to the content. + * + * <p>Make sure that the content provider owning the Uri sets the + * {@link android.R.styleable#AndroidManifestProvider_grantUriPermissions + * grantUriPermissions} attribute in its manifest or included the + * {@link android.R.styleable#AndroidManifestGrantUriPermission + * <grant-uri-permissions>} tag. Otherwise {@link InputContentInfo#requestPermission()} + * can fail.</p> + * + * <p>Although calling this API is allowed only for the IME that is currently selected, the + * client is able to request a temporary read-only access even after the current IME is switched + * to any other IME as long as the client keeps {@link InputContentInfo} object.</p> + * + * @param inputContentInfo Content to be temporarily exposed from the input method to the + * application. + * This cannot be {@code null}. + * @param editorInfo The editor that receives {@link InputContentInfo}. + * @return {@code false} if we cannot allow a temporary access permission. + */ + public final boolean exposeContent(@NonNull InputContentInfo inputContentInfo, + @NonNull EditorInfo editorInfo) { + if (inputContentInfo == null) { + throw new NullPointerException("inputContentInfo"); + } + if (editorInfo == null) { + throw new NullPointerException("editorInfo"); + } + + return mImm.exposeContent(mToken, inputContentInfo, editorInfo); + } + + /** * Performs a dump of the InputMethodService's internal state. Override * to add your own information to the dump. */ diff --git a/core/java/android/view/inputmethod/InputContentInfo.java b/core/java/android/view/inputmethod/InputContentInfo.java index e2ecfae7f764..9579bbf32835 100644 --- a/core/java/android/view/inputmethod/InputContentInfo.java +++ b/core/java/android/view/inputmethod/InputContentInfo.java @@ -22,13 +22,16 @@ import android.content.ClipDescription; import android.net.Uri; import android.os.Parcel; import android.os.Parcelable; +import android.os.RemoteException; + +import com.android.internal.inputmethod.IInputContentUriToken; import java.security.InvalidParameterException; /** * A container object with which input methods can send content files to the target application. */ -public class InputContentInfo implements Parcelable { +public final class InputContentInfo implements Parcelable { @NonNull private final Uri mContentUri; @@ -36,6 +39,8 @@ public class InputContentInfo implements Parcelable { private final ClipDescription mDescription; @Nullable private final Uri mLinkUri; + @NonNull + private IInputContentUriToken mUriToken; /** * Constructs {@link InputContentInfo} object only with mandatory data. @@ -110,7 +115,7 @@ public class InputContentInfo implements Parcelable { return false; } final String contentUriScheme = contentUri.getScheme(); - if (contentUriScheme == null || !contentUriScheme.equalsIgnoreCase("content")) { + if (!"content".equals(contentUriScheme)) { if (throwException) { throw new InvalidParameterException("contentUri must have content scheme"); } @@ -137,8 +142,9 @@ public class InputContentInfo implements Parcelable { public Uri getContentUri() { return mContentUri; } /** - * @return {@link ClipDescription} object that contains the metadata of {@code contentUri} such - * as MIME type(s). {@link ClipDescription#getLabel()} can be used for accessibility purpose. + * @return {@link ClipDescription} object that contains the metadata of {@code #getContentUri()} + * such as MIME type(s). {@link ClipDescription#getLabel()} can be used for accessibility + * purpose. */ @NonNull public ClipDescription getDescription() { return mDescription; } @@ -149,6 +155,47 @@ public class InputContentInfo implements Parcelable { @Nullable public Uri getLinkUri() { return mLinkUri; } + void setUriToken(IInputContentUriToken token) { + if (mUriToken != null) { + throw new IllegalStateException("URI token is already set"); + } + mUriToken = token; + } + + /** + * Requests a temporary read-only access permission for content URI associated with this object. + * + * <p>Does nothing if the temporary permission is already granted.</p> + */ + public void requestPermission() { + if (mUriToken == null) { + return; + } + try { + mUriToken.take(); + } catch (RemoteException e) { + e.rethrowFromSystemServer(); + } + } + + /** + * Releases a temporary read-only access permission for content URI associated with this object. + * + * <p>Does nothing if the temporary permission is not granted.</p> + */ + public void releasePermission() { + if (mUriToken == null) { + return; + } + try { + mUriToken.release(); + } catch (RemoteException e) { + e.rethrowFromSystemServer(); + } finally { + mUriToken = null; + } + } + /** * Used to package this object into a {@link Parcel}. * @@ -160,12 +207,23 @@ public class InputContentInfo implements Parcelable { Uri.writeToParcel(dest, mContentUri); mDescription.writeToParcel(dest, flags); Uri.writeToParcel(dest, mLinkUri); + if (mUriToken != null) { + dest.writeInt(1); + dest.writeStrongBinder(mUriToken.asBinder()); + } else { + dest.writeInt(0); + } } private InputContentInfo(@NonNull Parcel source) { mContentUri = Uri.CREATOR.createFromParcel(source); mDescription = ClipDescription.CREATOR.createFromParcel(source); mLinkUri = Uri.CREATOR.createFromParcel(source); + if (source.readInt() == 1) { + mUriToken = IInputContentUriToken.Stub.asInterface(source.readStrongBinder()); + } else { + mUriToken = null; + } } /** diff --git a/core/java/android/view/inputmethod/InputMethodManager.java b/core/java/android/view/inputmethod/InputMethodManager.java index 4013b30fce06..b35f5c3cddb4 100644 --- a/core/java/android/view/inputmethod/InputMethodManager.java +++ b/core/java/android/view/inputmethod/InputMethodManager.java @@ -16,6 +16,7 @@ package android.view.inputmethod; +import com.android.internal.inputmethod.IInputContentUriToken; import com.android.internal.os.SomeArgs; import com.android.internal.view.IInputConnectionWrapper; import com.android.internal.view.IInputContext; @@ -30,6 +31,7 @@ import android.annotation.Nullable; import android.annotation.RequiresPermission; import android.content.Context; import android.graphics.Rect; +import android.net.Uri; import android.os.Bundle; import android.os.Handler; import android.os.IBinder; @@ -56,6 +58,7 @@ import android.view.ViewRootImpl; import java.io.FileDescriptor; import java.io.PrintWriter; +import java.security.InvalidParameterException; import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; @@ -2288,6 +2291,41 @@ public final class InputMethodManager { } } + /** + * Allow the receiver of {@link InputContentInfo} to obtain a temporary read-only access + * permission to the content. + * + * <p>See {@link android.inputmethodservice.InputMethodService#exposeContent(InputContentInfo, EditorInfo)} + * for details.</p> + * + * @param token Supplies the identifying token given to an input method when it was started, + * which allows it to perform this operation on itself. + * @param inputContentInfo Content to be temporarily exposed from the input method to the + * application. + * This cannot be {@code null}. + * @param editorInfo The editor that receives {@link InputContentInfo}. + * @return {@code false} if we cannot allow a temporary access permission. + * @hide + */ + public boolean exposeContent(@NonNull IBinder token, @NonNull InputContentInfo inputContentInfo, + @NonNull EditorInfo editorInfo) { + final IInputContentUriToken uriToken; + final Uri contentUri = inputContentInfo.getContentUri(); + try { + uriToken = mService.createInputContentUriToken(token, contentUri, + editorInfo.packageName); + if (uriToken == null) { + return false; + } + } catch (RemoteException e) { + Log.e(TAG, "createInputContentAccessToken failed. contentUri=" + contentUri.toString() + + " packageName=" + editorInfo.packageName, e); + return false; + } + inputContentInfo.setUriToken(uriToken); + return true; + } + void doDump(FileDescriptor fd, PrintWriter fout, String[] args) { final Printer p = new PrintWriterPrinter(fout); p.println("Input method client state for " + this + ":"); diff --git a/core/java/com/android/internal/inputmethod/IInputContentUriToken.aidl b/core/java/com/android/internal/inputmethod/IInputContentUriToken.aidl new file mode 100644 index 000000000000..8abc8074b5ac --- /dev/null +++ b/core/java/com/android/internal/inputmethod/IInputContentUriToken.aidl @@ -0,0 +1,27 @@ +/* +** Copyright 2016, 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.internal.inputmethod; + +import android.os.IBinder; + +/** + * {@hide} + */ +interface IInputContentUriToken { + void take(); + void release(); +} diff --git a/core/java/com/android/internal/view/IInputMethodManager.aidl b/core/java/com/android/internal/view/IInputMethodManager.aidl index cb7c3bfecc89..9e4b43b6c007 100644 --- a/core/java/com/android/internal/view/IInputMethodManager.aidl +++ b/core/java/com/android/internal/view/IInputMethodManager.aidl @@ -16,11 +16,13 @@ package com.android.internal.view; +import android.net.Uri; import android.os.ResultReceiver; import android.text.style.SuggestionSpan; import android.view.inputmethod.InputMethodInfo; import android.view.inputmethod.InputMethodSubtype; import android.view.inputmethod.EditorInfo; +import com.android.internal.inputmethod.IInputContentUriToken; import com.android.internal.view.InputBindResult; import com.android.internal.view.IInputContext; import com.android.internal.view.IInputMethodClient; @@ -81,5 +83,8 @@ interface IInputMethodManager { int getInputMethodWindowVisibleHeight(); void clearLastInputMethodWindowForTransition(in IBinder token); + IInputContentUriToken createInputContentUriToken(in IBinder token, in Uri contentUri, + in String packageName); + oneway void notifyUserAction(int sequenceNumber); } diff --git a/services/core/java/com/android/server/InputContentUriTokenHandler.java b/services/core/java/com/android/server/InputContentUriTokenHandler.java new file mode 100644 index 000000000000..3f4972babf6e --- /dev/null +++ b/services/core/java/com/android/server/InputContentUriTokenHandler.java @@ -0,0 +1,121 @@ +/* +** Copyright 2016, 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; + +import android.annotation.NonNull; +import android.annotation.UserIdInt; +import android.app.ActivityManagerNative; +import android.content.Intent; +import android.net.Uri; +import android.os.Binder; +import android.os.IBinder; +import android.os.RemoteException; + +import com.android.internal.annotations.GuardedBy; +import com.android.internal.inputmethod.IInputContentUriToken; + +final class InputContentUriTokenHandler extends IInputContentUriToken.Stub { + + @NonNull + private final Uri mUri; + private final int mSourceUid; + @NonNull + private final String mTargetPackage; + @UserIdInt + private final int mSourceUserId; + @UserIdInt + private final int mTargetUserId; + + private final Object mLock = new Object(); + + @GuardedBy("mLock") + private IBinder mPermissionOwnerToken = null; + + InputContentUriTokenHandler(@NonNull Uri contentUri, int sourceUid, + @NonNull String targetPackage, @UserIdInt int sourceUserId, + @UserIdInt int targetUserId) { + mUri = contentUri; + mSourceUid = sourceUid; + mTargetPackage = targetPackage; + mSourceUserId = sourceUserId; + mTargetUserId = targetUserId; + } + + @Override + public void take() { + synchronized (mLock) { + if (mPermissionOwnerToken != null) { + // Permission is already granted. + return; + } + + try { + mPermissionOwnerToken = ActivityManagerNative.getDefault() + .newUriPermissionOwner("InputContentUriTokenHandler"); + } catch (RemoteException e) { + e.rethrowFromSystemServer(); + } + + doTakeLocked(mPermissionOwnerToken); + } + } + + private void doTakeLocked(@NonNull IBinder permissionOwner) { + long origId = Binder.clearCallingIdentity(); + try { + try { + ActivityManagerNative.getDefault().grantUriPermissionFromOwner( + permissionOwner, mSourceUid, mTargetPackage, mUri, + Intent.FLAG_GRANT_READ_URI_PERMISSION, mSourceUserId, mTargetUserId); + } catch (RemoteException e) { + e.rethrowFromSystemServer(); + } + } finally { + Binder.restoreCallingIdentity(origId); + } + } + + @Override + public void release() { + synchronized (mLock) { + if (mPermissionOwnerToken == null) { + return; + } + try { + ActivityManagerNative.getDefault().revokeUriPermissionFromOwner( + mPermissionOwnerToken, mUri, + Intent.FLAG_GRANT_READ_URI_PERMISSION, mSourceUserId); + } catch (RemoteException e) { + e.rethrowFromSystemServer(); + } finally { + mPermissionOwnerToken = null; + } + } + } + + /** + * {@inheritDoc} + */ + @Override + protected void finalize() throws Throwable { + try { + release(); + } finally { + super.finalize(); + } + } +} diff --git a/services/core/java/com/android/server/InputMethodManagerService.java b/services/core/java/com/android/server/InputMethodManagerService.java index 5d8fe7c68fe8..e0d89f2fe6d3 100644 --- a/services/core/java/com/android/server/InputMethodManagerService.java +++ b/services/core/java/com/android/server/InputMethodManagerService.java @@ -18,6 +18,7 @@ package com.android.server; import static java.lang.annotation.RetentionPolicy.SOURCE; import com.android.internal.content.PackageMonitor; +import com.android.internal.inputmethod.IInputContentUriToken; import com.android.internal.inputmethod.InputMethodSubtypeSwitchingController; import com.android.internal.inputmethod.InputMethodSubtypeSwitchingController.ImeSubtypeListItem; import com.android.internal.inputmethod.InputMethodUtils; @@ -137,6 +138,7 @@ import java.io.IOException; import java.io.PrintWriter; import java.lang.annotation.Retention; import java.nio.charset.StandardCharsets; +import java.security.InvalidParameterException; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; @@ -3911,6 +3913,52 @@ public class InputMethodManagerService extends IInputMethodManager.Stub } @Override + public IInputContentUriToken createInputContentUriToken(@Nullable IBinder token, + @Nullable Uri contentUri, @Nullable String packageName) { + if (!calledFromValidUser()) { + return null; + } + + if (token == null) { + throw new NullPointerException("token"); + } + if (packageName == null) { + throw new NullPointerException("packageName"); + } + if (contentUri == null) { + throw new NullPointerException("contentUri"); + } + final String contentUriScheme = contentUri.getScheme(); + if (!"content".equals(contentUriScheme)) { + throw new InvalidParameterException("contentUri must have content scheme"); + } + + synchronized (mMethodMap) { + final int uid = Binder.getCallingUid(); + if (mCurMethodId == null) { + return null; + } + if (mCurToken != token) { + Slog.e(TAG, "Ignoring createInputContentUriToken mCurToken=" + mCurToken + + " token=" + token); + return null; + } + // We cannot simply distinguish a bad IME that reports an arbitrary package name from + // an unfortunate IME whose internal state is already obsolete due to the asynchronous + // nature of our system. Let's compare it with our internal record. + if (!TextUtils.equals(mCurAttribute.packageName, packageName)) { + Slog.e(TAG, "Ignoring createInputContentUriToken mCurAttribute.packageName=" + + mCurAttribute.packageName + " packageName=" + packageName); + return null; + } + final int imeUserId = UserHandle.getUserId(uid); + final int appUserId = UserHandle.getUserId(mCurClient.uid); + return new InputContentUriTokenHandler(contentUri, uid, packageName, imeUserId, + appUserId); + } + } + + @Override protected void dump(FileDescriptor fd, PrintWriter pw, String[] args) { if (mContext.checkCallingOrSelfPermission(android.Manifest.permission.DUMP) != PackageManager.PERMISSION_GRANTED) { diff --git a/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/android/BridgeIInputMethodManager.java b/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/android/BridgeIInputMethodManager.java index 3f276c9375ba..ab73a8b820cd 100644 --- a/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/android/BridgeIInputMethodManager.java +++ b/tools/layoutlib/bridge/src/com/android/layoutlib/bridge/android/BridgeIInputMethodManager.java @@ -16,11 +16,13 @@ package com.android.layoutlib.bridge.android; +import com.android.internal.inputmethod.IInputContentUriToken; import com.android.internal.view.IInputContext; import com.android.internal.view.IInputMethodClient; import com.android.internal.view.IInputMethodManager; import com.android.internal.view.InputBindResult; +import android.net.Uri; import android.os.IBinder; import android.os.RemoteException; import android.os.ResultReceiver; @@ -239,4 +241,11 @@ public class BridgeIInputMethodManager implements IInputMethodManager { // TODO Auto-generated method stub return null; } + + @Override + public IInputContentUriToken createInputContentUriToken(IBinder token, Uri contentUri, + String packageName) { + // TODO Auto-generated method stub + return null; + } } |