blob: 9f6eedc8b3a2b1ee01ebbc25b7c77cfe028d67fc [file] [log] [blame]
/*
* Copyright (C) 2017 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 android.service.autofill;
import static android.view.autofill.Helper.sDebug;
import android.annotation.NonNull;
import android.os.Parcel;
import android.os.Parcelable;
import android.util.ArrayMap;
import android.util.Log;
import android.view.autofill.AutofillId;
import android.widget.ImageView;
import android.widget.RemoteViews;
import com.android.internal.util.Preconditions;
/**
* Replaces the content of a child {@link ImageView} of a
* {@link RemoteViews presentation template} with the first image that matches a regular expression
* (regex).
*
* <p>Typically used to display credit card logos. Example:
*
* <pre class="prettyprint">
* new ImageTransformation.Builder(ccNumberId)
* .addOption("^4815.*$", R.drawable.ic_credit_card_logo1)
* .addOption("^1623.*$", R.drawable.ic_credit_card_logo2)
* .addOption("^42.*$", R.drawable.ic_credit_card_logo3)
* .build();
* </pre>
*
* <p>There is no imposed limit in the number of options, but keep in mind that regexs are
* expensive to evaluate, so try to:
* <ul>
* <li>Use the minimum number of regex per image.
* <li>Add the most common images first.
* </ul>
*/
//TODO(b/62534917): add unit tests
public final class ImageTransformation extends InternalTransformation implements Parcelable {
private static final String TAG = "ImageTransformation";
private final AutofillId mId;
private final ArrayMap<String, Integer> mOptions;
private ImageTransformation(Builder builder) {
mId = builder.mId;
mOptions = builder.mOptions;
}
/** @hide */
@Override
public void apply(@NonNull ValueFinder finder, @NonNull RemoteViews parentTemplate,
int childViewId) {
final String value = finder.findByAutofillId(mId);
if (value == null) {
Log.w(TAG, "No view for id " + mId);
return;
}
final int size = mOptions.size();
if (sDebug) {
Log.d(TAG, size + " multiple options on id " + childViewId + " to compare against "
+ value);
}
for (int i = 0; i < size; i++) {
final String regex = mOptions.keyAt(i);
if (value.matches(regex)) {
Log.d(TAG, "Found match at " + i + ": " + regex);
parentTemplate.setImageViewResource(childViewId, mOptions.valueAt(i));
return;
}
}
Log.w(TAG, "No match for " + value);
}
/**
* Builder for {@link ImageTransformation} objects.
*/
public static class Builder {
private final AutofillId mId;
private ArrayMap<String, Integer> mOptions;
private boolean mDestroyed;
/**
* Default constructor.
*
* @param id id of the screen field that will be used to evaluate whether the image should
* be used.
*/
//TODO(b/62534917): add a regex/resid so we force it to have at least one
// (and then remove the check for empty from build())
public Builder(@NonNull AutofillId id) {
mId = Preconditions.checkNotNull(id);
}
/**
* Adds an option to replace the child view with a different image when the regex matches.
*
* @param regex regular expression defining what should be matched to use this image.
* @param resId resource id of the image (in the autofill service's package). The
* {@link RemoteViews presentation} must contain a {@link ImageView} child with that id.
*
* @return this build
*/
public Builder addOption(String regex, int resId) {
//TODO(b/62534917): throw exception if regex / resId are invalid
throwIfDestroyed();
if (mOptions == null) {
mOptions = new ArrayMap<>();
}
mOptions.put(regex, resId);
return this;
}
/**
* Creates a new {@link ImageTransformation} instance.
*
* @throws IllegalStateException if no call to {@link #addOption(String, int)} was made.
*/
public ImageTransformation build() {
throwIfDestroyed();
Preconditions.checkState(mOptions != null && !mOptions.isEmpty(),
"Must add at least one option");
mDestroyed = true;
return new ImageTransformation(this);
}
private void throwIfDestroyed() {
Preconditions.checkState(!mDestroyed, "Already called build()");
}
}
/////////////////////////////////////
// Object "contract" methods. //
/////////////////////////////////////
@Override
public String toString() {
if (!sDebug) return super.toString();
return "ImageTransformation: [id=" + mId + ", options=" + mOptions + "]";
}
/////////////////////////////////////
// Parcelable "contract" methods. //
/////////////////////////////////////
@Override
public int describeContents() {
return 0;
}
@Override
public void writeToParcel(Parcel parcel, int flags) {
parcel.writeParcelable(mId, flags);
if (mOptions == null) {
parcel.writeStringArray(null);
return;
}
final int size = mOptions.size();
final String[] regexs = new String[size];
final int[] resIds = new int[size];
for (int i = 0; i < size; i++) {
regexs[i] = mOptions.keyAt(i);
resIds[i] = mOptions.valueAt(i);
}
parcel.writeStringArray(regexs);
parcel.writeIntArray(resIds);
}
public static final Parcelable.Creator<ImageTransformation> CREATOR =
new Parcelable.Creator<ImageTransformation>() {
@Override
public ImageTransformation createFromParcel(Parcel parcel) {
// Always go through the builder to ensure the data ingested by
// the system obeys the contract of the builder to avoid attacks
// using specially crafted parcels.
final ImageTransformation.Builder builder =
new ImageTransformation.Builder(parcel.readParcelable(null));
final String[] regexs = parcel.createStringArray();
if (regexs != null) {
final int[] resIds = parcel.createIntArray();
final int size = regexs.length;
for (int i = 0; i < size; i++) {
builder.addOption(regexs[i], resIds[i]);
}
}
return builder.build();
}
@Override
public ImageTransformation[] newArray(int size) {
return new ImageTransformation[size];
}
};
}