Skip to content
Permalink
Browse files

Add InputMethodService#exposeContent()

This is a follow up CL to my previous CLs [1][2] that introduced
InputConnection#commitContent(InputContentInfo, Bundle) API to enable
IMEs to send a content to the target application.

With this CL, IME developers are able to temporarily expose
InputContentInfo object to the target package without permanently
granting URI permission.  Although calling IMS#exposeContent() 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 InputContentInfo
object.

Here is a sample code snippet about how to use this mechanism.

  [IME]
  InputContentInfo contentInfo = new InputContentInfo(
          contentUri,
          new ClipDescription(description, new String[]{mimeType}),
          linkUrl);
  exposeContent(contentInfo, getCurrentInputEditorInfo());
  getCurrentInputConnection().commitContent(inputContentInfo, null);

  [App]
  try {
      contentInfo.requestPermission();
      // Load inputContentInfo.getContentUri() here.
  } finally {
      contentInfo.releasePermission();
  }

 [1]: Iaadf934a997ffcd6000a516cc3c1873db56e60ad
      152944f
 [2]: Ica1ba3154795c1bf44e140dfe639b299f83cd8af
      adebb52

Bug: 29450031
Change-Id: I2772889ca01f2ecb2cdeed4e04a9319bdf7bc5a6
  • Loading branch information...
yukawa committed Jun 22, 2016
1 parent f082385 commit 25e0813e6eb6315b1016db805fa9b791b4ae5cc2
@@ -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 \
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();
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;
}
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();
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;
}
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();
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;
}
@@ -23,6 +23,7 @@
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.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;
@@ -2597,6 +2599,39 @@ public int getInputMethodWindowRecommendedHeight() {
return mImm.getInputMethodWindowVisibleHeight();
}

/**
* 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
* &lt;grant-uri-permissions&gt;} 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.
@@ -22,20 +22,25 @@
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;
@NonNull
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 @@ private static boolean validateInternal(@NonNull Uri contentUri,
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 @@ private static boolean validateInternal(@NonNull Uri contentUri,
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 @@ private static boolean validateInternal(@NonNull Uri contentUri,
@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 void writeToParcel(Parcel dest, int flags) {
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;
}
}

/**
@@ -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.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 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 InputMethodSubtype getLastInputMethodSubtype() {
}
}

/**
* 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 + ":");
@@ -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();
}
@@ -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);
}

0 comments on commit 25e0813

Please sign in to comment.
You can’t perform that action at this time.