239 lines
9.0 KiB
Java
239 lines
9.0 KiB
Java
![]() |
/*
|
||
|
* 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.annotation.TestApi;
|
||
|
import android.os.Parcel;
|
||
|
import android.os.Parcelable;
|
||
|
import android.util.Log;
|
||
|
import android.util.Pair;
|
||
|
import android.view.autofill.AutofillId;
|
||
|
import android.widget.RemoteViews;
|
||
|
import android.widget.TextView;
|
||
|
|
||
|
import com.android.internal.util.Preconditions;
|
||
|
|
||
|
import java.util.LinkedHashMap;
|
||
|
import java.util.Map.Entry;
|
||
|
import java.util.Objects;
|
||
|
import java.util.regex.Matcher;
|
||
|
import java.util.regex.Pattern;
|
||
|
|
||
|
/**
|
||
|
* Replaces a {@link TextView} child of a {@link CustomDescription} with the contents of one or
|
||
|
* more regular expressions (regexs).
|
||
|
*
|
||
|
* <p>When it contains more than one field, the fields that match their regex are added to the
|
||
|
* overall transformation result.
|
||
|
*
|
||
|
* <p>For example, a transformation to mask a credit card number contained in just one field would
|
||
|
* be:
|
||
|
*
|
||
|
* <pre class="prettyprint">
|
||
|
* new CharSequenceTransformation
|
||
|
* .Builder(ccNumberId, Pattern.compile("^.*(\\d\\d\\d\\d)$"), "...$1")
|
||
|
* .build();
|
||
|
* </pre>
|
||
|
*
|
||
|
* <p>But a transformation that generates a {@code Exp: MM / YYYY} credit expiration date from two
|
||
|
* fields (month and year) would be:
|
||
|
*
|
||
|
* <pre class="prettyprint">
|
||
|
* new CharSequenceTransformation
|
||
|
* .Builder(ccExpMonthId, Pattern.compile("^(\\d\\d)$"), "Exp: $1")
|
||
|
* .addField(ccExpYearId, Pattern.compile("^(\\d\\d\\d\\d)$"), " / $1");
|
||
|
* </pre>
|
||
|
*/
|
||
|
public final class CharSequenceTransformation extends InternalTransformation implements
|
||
|
Transformation, Parcelable {
|
||
|
private static final String TAG = "CharSequenceTransformation";
|
||
|
|
||
|
// Must use LinkedHashMap to preserve insertion order.
|
||
|
@NonNull private final LinkedHashMap<AutofillId, Pair<Pattern, String>> mFields;
|
||
|
|
||
|
private CharSequenceTransformation(Builder builder) {
|
||
|
mFields = builder.mFields;
|
||
|
}
|
||
|
|
||
|
/** @hide */
|
||
|
@Override
|
||
|
@TestApi
|
||
|
public void apply(@NonNull ValueFinder finder, @NonNull RemoteViews parentTemplate,
|
||
|
int childViewId) throws Exception {
|
||
|
final StringBuilder converted = new StringBuilder();
|
||
|
final int size = mFields.size();
|
||
|
if (sDebug) Log.d(TAG, size + " fields on id " + childViewId);
|
||
|
for (Entry<AutofillId, Pair<Pattern, String>> entry : mFields.entrySet()) {
|
||
|
final AutofillId id = entry.getKey();
|
||
|
final Pair<Pattern, String> field = entry.getValue();
|
||
|
final String value = finder.findByAutofillId(id);
|
||
|
if (value == null) {
|
||
|
Log.w(TAG, "No value for id " + id);
|
||
|
return;
|
||
|
}
|
||
|
try {
|
||
|
final Matcher matcher = field.first.matcher(value);
|
||
|
if (!matcher.find()) {
|
||
|
if (sDebug) Log.d(TAG, "Match for " + field.first + " failed on id " + id);
|
||
|
return;
|
||
|
}
|
||
|
// replaceAll throws an exception if the subst is invalid
|
||
|
final String convertedValue = matcher.replaceAll(field.second);
|
||
|
converted.append(convertedValue);
|
||
|
} catch (Exception e) {
|
||
|
// Do not log full exception to avoid PII leaking
|
||
|
Log.w(TAG, "Cannot apply " + field.first.pattern() + "->" + field.second + " to "
|
||
|
+ "field with autofill id" + id + ": " + e.getClass());
|
||
|
throw e;
|
||
|
}
|
||
|
}
|
||
|
// Cannot log converted, it might have PII
|
||
|
Log.d(TAG, "Converting text on child " + childViewId + " to " + converted.length()
|
||
|
+ "_chars");
|
||
|
parentTemplate.setCharSequence(childViewId, "setText", converted);
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Builder for {@link CharSequenceTransformation} objects.
|
||
|
*/
|
||
|
public static class Builder {
|
||
|
|
||
|
// Must use LinkedHashMap to preserve insertion order.
|
||
|
@NonNull private final LinkedHashMap<AutofillId, Pair<Pattern, String>> mFields =
|
||
|
new LinkedHashMap<>();
|
||
|
private boolean mDestroyed;
|
||
|
|
||
|
/**
|
||
|
* Creates a new builder and adds the first transformed contents of a field to the overall
|
||
|
* result of this transformation.
|
||
|
*
|
||
|
* @param id id of the screen field.
|
||
|
* @param regex regular expression with groups (delimited by {@code (} and {@code (}) that
|
||
|
* are used to substitute parts of the value.
|
||
|
* @param subst the string that substitutes the matched regex, using {@code $} for
|
||
|
* group substitution ({@code $1} for 1st group match, {@code $2} for 2nd, etc).
|
||
|
*/
|
||
|
public Builder(@NonNull AutofillId id, @NonNull Pattern regex, @NonNull String subst) {
|
||
|
addField(id, regex, subst);
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Adds the transformed contents of a field to the overall result of this transformation.
|
||
|
*
|
||
|
* @param id id of the screen field.
|
||
|
* @param regex regular expression with groups (delimited by {@code (} and {@code (}) that
|
||
|
* are used to substitute parts of the value.
|
||
|
* @param subst the string that substitutes the matched regex, using {@code $} for
|
||
|
* group substitution ({@code $1} for 1st group match, {@code $2} for 2nd, etc).
|
||
|
*
|
||
|
* @return this builder.
|
||
|
*/
|
||
|
public Builder addField(@NonNull AutofillId id, @NonNull Pattern regex,
|
||
|
@NonNull String subst) {
|
||
|
throwIfDestroyed();
|
||
|
Objects.requireNonNull(id);
|
||
|
Objects.requireNonNull(regex);
|
||
|
Objects.requireNonNull(subst);
|
||
|
|
||
|
mFields.put(id, new Pair<>(regex, subst));
|
||
|
return this;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Creates a new {@link CharSequenceTransformation} instance.
|
||
|
*/
|
||
|
public CharSequenceTransformation build() {
|
||
|
throwIfDestroyed();
|
||
|
mDestroyed = true;
|
||
|
return new CharSequenceTransformation(this);
|
||
|
}
|
||
|
|
||
|
private void throwIfDestroyed() {
|
||
|
Preconditions.checkState(!mDestroyed, "Already called build()");
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/////////////////////////////////////
|
||
|
// Object "contract" methods. //
|
||
|
/////////////////////////////////////
|
||
|
@Override
|
||
|
public String toString() {
|
||
|
if (!sDebug) return super.toString();
|
||
|
|
||
|
return "MultipleViewsCharSequenceTransformation: [fields=" + mFields + "]";
|
||
|
}
|
||
|
|
||
|
/////////////////////////////////////
|
||
|
// Parcelable "contract" methods. //
|
||
|
/////////////////////////////////////
|
||
|
@Override
|
||
|
public int describeContents() {
|
||
|
return 0;
|
||
|
}
|
||
|
|
||
|
@Override
|
||
|
public void writeToParcel(Parcel parcel, int flags) {
|
||
|
final int size = mFields.size();
|
||
|
final AutofillId[] ids = new AutofillId[size];
|
||
|
final Pattern[] regexs = new Pattern[size];
|
||
|
final String[] substs = new String[size];
|
||
|
Pair<Pattern, String> pair;
|
||
|
int i = 0;
|
||
|
for (Entry<AutofillId, Pair<Pattern, String>> entry : mFields.entrySet()) {
|
||
|
ids[i] = entry.getKey();
|
||
|
pair = entry.getValue();
|
||
|
regexs[i] = pair.first;
|
||
|
substs[i] = pair.second;
|
||
|
i++;
|
||
|
}
|
||
|
|
||
|
parcel.writeParcelableArray(ids, flags);
|
||
|
parcel.writeSerializable(regexs);
|
||
|
parcel.writeStringArray(substs);
|
||
|
}
|
||
|
|
||
|
public static final @android.annotation.NonNull Parcelable.Creator<CharSequenceTransformation> CREATOR =
|
||
|
new Parcelable.Creator<CharSequenceTransformation>() {
|
||
|
@Override
|
||
|
public CharSequenceTransformation createFromParcel(Parcel parcel) {
|
||
|
final AutofillId[] ids = parcel.readParcelableArray(null, AutofillId.class);
|
||
|
final Pattern[] regexs = (Pattern[]) parcel.readSerializable();
|
||
|
final String[] substs = parcel.createStringArray();
|
||
|
|
||
|
// 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 CharSequenceTransformation.Builder builder =
|
||
|
new CharSequenceTransformation.Builder(ids[0], regexs[0], substs[0]);
|
||
|
|
||
|
final int size = ids.length;
|
||
|
for (int i = 1; i < size; i++) {
|
||
|
builder.addField(ids[i], regexs[i], substs[i]);
|
||
|
}
|
||
|
return builder.build();
|
||
|
}
|
||
|
|
||
|
@Override
|
||
|
public CharSequenceTransformation[] newArray(int size) {
|
||
|
return new CharSequenceTransformation[size];
|
||
|
}
|
||
|
};
|
||
|
}
|