blob: 780c637b3c9bb53e7aea44eb379ac1e296e79678 [file] [log] [blame]
/*
* Copyright (C) 2022 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 static java.lang.annotation.RetentionPolicy.SOURCE;
import android.annotation.AnyThread;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.content.ComponentName;
import android.os.Parcel;
import android.os.Parcelable;
import android.text.TextUtils.SimpleStringSplitter;
import android.view.inputmethod.InputMethodInfo;
import android.view.inputmethod.InputMethodSubtype;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;
import java.security.InvalidParameterException;
import java.util.Objects;
/**
* A stable and serializable identifier for the pair of {@link InputMethodInfo#getId()} and
* {@link android.view.inputmethod.InputMethodSubtype}.
*
* <p>To save {@link InputMethodSubtypeHandle} to storage, call {@link #toStringHandle()} to get a
* {@link String} handle and just save it. Once you load a {@link String} handle, you can obtain a
* {@link InputMethodSubtypeHandle} instance from {@link #of(String)}.</p>
*
* <p>For better readability, consider specifying {@link RawHandle} annotation to {@link String}
* object when it is a raw {@link String} handle.</p>
*/
public final class InputMethodSubtypeHandle implements Parcelable {
private static final String SUBTYPE_TAG = "subtype";
private static final char DATA_SEPARATOR = ':';
/**
* Can be used to annotate {@link String} object if it is raw handle format.
*/
@Retention(SOURCE)
@Target({ElementType.METHOD, ElementType.FIELD, ElementType.LOCAL_VARIABLE,
ElementType.PARAMETER})
public @interface RawHandle {
}
/**
* The main content of this {@link InputMethodSubtypeHandle}. Is designed to be safe to be
* saved into storage.
*/
@RawHandle
private final String mHandle;
/**
* Encode {@link InputMethodInfo} and {@link InputMethodSubtype#hashCode()} into
* {@link RawHandle}.
*
* @param imeId {@link InputMethodInfo#getId()} to be used.
* @param subtypeHashCode {@link InputMethodSubtype#hashCode()} to be used.
* @return The encoded {@link RawHandle} string.
*/
@AnyThread
@RawHandle
@NonNull
private static String encodeHandle(@NonNull String imeId, int subtypeHashCode) {
return imeId + DATA_SEPARATOR + SUBTYPE_TAG + DATA_SEPARATOR + subtypeHashCode;
}
private InputMethodSubtypeHandle(@NonNull String handle) {
mHandle = handle;
}
/**
* Creates {@link InputMethodSubtypeHandle} from {@link InputMethodInfo} and
* {@link InputMethodSubtype}.
*
* @param imi {@link InputMethodInfo} to be used.
* @param subtype {@link InputMethodSubtype} to be used.
* @return A {@link InputMethodSubtypeHandle} object.
*/
@AnyThread
@NonNull
public static InputMethodSubtypeHandle of(
@NonNull InputMethodInfo imi, @Nullable InputMethodSubtype subtype) {
final int subtypeHashCode =
subtype != null ? subtype.hashCode() : InputMethodSubtype.SUBTYPE_ID_NONE;
return new InputMethodSubtypeHandle(encodeHandle(imi.getId(), subtypeHashCode));
}
/**
* Creates {@link InputMethodSubtypeHandle} from a {@link RawHandle} {@link String}, which can
* be obtained by {@link #toStringHandle()}.
*
* @param stringHandle {@link RawHandle} {@link String} to be parsed.
* @return A {@link InputMethodSubtypeHandle} object.
* @throws NullPointerException when {@code stringHandle} is {@code null}
* @throws InvalidParameterException when {@code stringHandle} is not a valid {@link RawHandle}.
*/
@AnyThread
@NonNull
public static InputMethodSubtypeHandle of(@RawHandle @NonNull String stringHandle) {
final SimpleStringSplitter splitter = new SimpleStringSplitter(DATA_SEPARATOR);
splitter.setString(Objects.requireNonNull(stringHandle));
if (!splitter.hasNext()) {
throw new InvalidParameterException("Invalid handle=" + stringHandle);
}
final String imeId = splitter.next();
final ComponentName componentName = ComponentName.unflattenFromString(imeId);
if (componentName == null) {
throw new InvalidParameterException("Invalid handle=" + stringHandle);
}
// TODO: Consolidate IME ID validation logic into one place.
if (!Objects.equals(componentName.flattenToShortString(), imeId)) {
throw new InvalidParameterException("Invalid handle=" + stringHandle);
}
if (!splitter.hasNext()) {
throw new InvalidParameterException("Invalid handle=" + stringHandle);
}
final String source = splitter.next();
if (!Objects.equals(source, SUBTYPE_TAG)) {
throw new InvalidParameterException("Invalid handle=" + stringHandle);
}
if (!splitter.hasNext()) {
throw new InvalidParameterException("Invalid handle=" + stringHandle);
}
final String hashCodeStr = splitter.next();
if (splitter.hasNext()) {
throw new InvalidParameterException("Invalid handle=" + stringHandle);
}
final int subtypeHashCode;
try {
subtypeHashCode = Integer.parseInt(hashCodeStr);
} catch (NumberFormatException ignore) {
throw new InvalidParameterException("Invalid handle=" + stringHandle);
}
// Redundant expressions (e.g. "0001" instead of "1") are not allowed.
if (!Objects.equals(encodeHandle(imeId, subtypeHashCode), stringHandle)) {
throw new InvalidParameterException("Invalid handle=" + stringHandle);
}
return new InputMethodSubtypeHandle(stringHandle);
}
/**
* @return {@link ComponentName} of the input method.
* @see InputMethodInfo#getComponent()
*/
@AnyThread
@NonNull
public ComponentName getComponentName() {
return ComponentName.unflattenFromString(getImeId());
}
/**
* @return IME ID.
* @see InputMethodInfo#getId()
*/
@AnyThread
@NonNull
public String getImeId() {
return mHandle.substring(0, mHandle.indexOf(DATA_SEPARATOR));
}
/**
* @return {@link RawHandle} {@link String} data that should be stable and persistable.
* @see #of(String)
*/
@RawHandle
@AnyThread
@NonNull
public String toStringHandle() {
return mHandle;
}
/**
* {@inheritDoc}
*/
@AnyThread
@Override
public boolean equals(Object obj) {
if (!(obj instanceof InputMethodSubtypeHandle)) {
return false;
}
final InputMethodSubtypeHandle that = (InputMethodSubtypeHandle) obj;
return Objects.equals(mHandle, that.mHandle);
}
/**
* {@inheritDoc}
*/
@AnyThread
@Override
public int hashCode() {
return Objects.hashCode(mHandle);
}
/**
* {@inheritDoc}
*/
@AnyThread
@NonNull
@Override
public String toString() {
return "InputMethodSubtypeHandle{mHandle=" + mHandle + "}";
}
/**
* {@link Creator} for parcelable.
*/
public static final Creator<InputMethodSubtypeHandle> CREATOR = new Creator<>() {
@Override
public InputMethodSubtypeHandle createFromParcel(Parcel in) {
return of(in.readString8());
}
@Override
public InputMethodSubtypeHandle[] newArray(int size) {
return new InputMethodSubtypeHandle[size];
}
};
/**
* {@inheritDoc}
*/
@AnyThread
@Override
public int describeContents() {
return 0;
}
/**
* {@inheritDoc}
*/
@AnyThread
@Override
public void writeToParcel(@NonNull Parcel dest, int flags) {
dest.writeString8(toStringHandle());
}
}