1/*
2 * Copyright (C) 2017 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package android.service.autofill;
18
19import static android.view.autofill.Helper.sDebug;
20
21import android.annotation.NonNull;
22import android.annotation.Nullable;
23import android.os.Parcel;
24import android.os.Parcelable;
25import android.util.Pair;
26import android.widget.RemoteViews;
27
28import com.android.internal.util.Preconditions;
29
30import java.util.ArrayList;
31
32/**
33 * Defines actions to be applied to a {@link RemoteViews template presentation}.
34 *
35 *
36 * <p>It supports 2 types of actions:
37 *
38 * <ol>
39 *   <li>{@link RemoteViews Actions} to be applied to the template.
40 *   <li>{@link Transformation Transformations} to be applied on child views.
41 * </ol>
42 *
43 * <p>Typically used on {@link CustomDescription custom descriptions} to conditionally display
44 * differents views based on user input - see
45 * {@link CustomDescription.Builder#batchUpdate(Validator, BatchUpdates)} for more information.
46 */
47public final class BatchUpdates implements Parcelable {
48
49    private final ArrayList<Pair<Integer, InternalTransformation>> mTransformations;
50    private final RemoteViews mUpdates;
51
52    private BatchUpdates(Builder builder) {
53        mTransformations = builder.mTransformations;
54        mUpdates = builder.mUpdates;
55    }
56
57    /** @hide */
58    @Nullable
59    public ArrayList<Pair<Integer, InternalTransformation>> getTransformations() {
60        return mTransformations;
61    }
62
63    /** @hide */
64    @Nullable
65    public RemoteViews getUpdates() {
66        return mUpdates;
67    }
68
69    /**
70     * Builder for {@link BatchUpdates} objects.
71     */
72    public static class Builder {
73        private RemoteViews mUpdates;
74
75        private boolean mDestroyed;
76        private ArrayList<Pair<Integer, InternalTransformation>> mTransformations;
77
78        /**
79         * Applies the {@code updates} in the underlying presentation template.
80         *
81         * <p><b>Note:</b> The updates are applied before the
82         * {@link #transformChild(int, Transformation) transformations} are applied to the children
83         * views.
84         *
85         * <p>Theme does not work with RemoteViews layout. Avoid hardcoded text color
86         * or background color: Autofill on different platforms may have different themes.
87         *
88         * @param updates a {@link RemoteViews} with the updated actions to be applied in the
89         * underlying presentation template.
90         *
91         * @return this builder
92         * @throws IllegalArgumentException if {@code condition} is not a class provided
93         * by the Android System.
94         */
95        public Builder updateTemplate(@NonNull RemoteViews updates) {
96            throwIfDestroyed();
97            mUpdates = Preconditions.checkNotNull(updates);
98            return this;
99        }
100
101        /**
102         * Adds a transformation to replace the value of a child view with the fields in the
103         * screen.
104         *
105         * <p>When multiple transformations are added for the same child view, they are applied
106         * in the same order as added.
107         *
108         * <p><b>Note:</b> The transformations are applied after the
109         * {@link #updateTemplate(RemoteViews) updates} are applied to the presentation template.
110         *
111         * @param id view id of the children view.
112         * @param transformation an implementation provided by the Android System.
113         * @return this builder.
114         * @throws IllegalArgumentException if {@code transformation} is not a class provided
115         * by the Android System.
116         */
117        public Builder transformChild(int id, @NonNull Transformation transformation) {
118            throwIfDestroyed();
119            Preconditions.checkArgument((transformation instanceof InternalTransformation),
120                    "not provided by Android System: " + transformation);
121            if (mTransformations == null) {
122                mTransformations = new ArrayList<>();
123            }
124            mTransformations.add(new Pair<>(id, (InternalTransformation) transformation));
125            return this;
126        }
127
128        /**
129         * Creates a new {@link BatchUpdates} instance.
130         *
131         * @throws IllegalStateException if {@link #build()} was already called before or no call
132         * to {@link #updateTemplate(RemoteViews)} or {@link #transformChild(int, Transformation)}
133         * has been made.
134         */
135        public BatchUpdates build() {
136            throwIfDestroyed();
137            Preconditions.checkState(mUpdates != null || mTransformations != null,
138                    "must call either updateTemplate() or transformChild() at least once");
139            mDestroyed = true;
140            return new BatchUpdates(this);
141        }
142
143        private void throwIfDestroyed() {
144            if (mDestroyed) {
145                throw new IllegalStateException("Already called #build()");
146            }
147        }
148    }
149
150    /////////////////////////////////////
151    // Object "contract" methods. //
152    /////////////////////////////////////
153    @Override
154    public String toString() {
155        if (!sDebug) return super.toString();
156
157        return new StringBuilder("BatchUpdates: [")
158                .append(", transformations=")
159                    .append(mTransformations == null ? "N/A" : mTransformations.size())
160                .append(", updates=").append(mUpdates)
161                .append("]").toString();
162    }
163
164    /////////////////////////////////////
165    // Parcelable "contract" methods. //
166    /////////////////////////////////////
167    @Override
168    public int describeContents() {
169        return 0;
170    }
171
172    @Override
173    public void writeToParcel(Parcel dest, int flags) {
174        if (mTransformations == null) {
175            dest.writeIntArray(null);
176        } else {
177            final int size = mTransformations.size();
178            final int[] ids = new int[size];
179            final InternalTransformation[] values = new InternalTransformation[size];
180            for (int i = 0; i < size; i++) {
181                final Pair<Integer, InternalTransformation> pair = mTransformations.get(i);
182                ids[i] = pair.first;
183                values[i] = pair.second;
184            }
185            dest.writeIntArray(ids);
186            dest.writeParcelableArray(values, flags);
187        }
188        dest.writeParcelable(mUpdates, flags);
189    }
190    public static final Parcelable.Creator<BatchUpdates> CREATOR =
191            new Parcelable.Creator<BatchUpdates>() {
192        @Override
193        public BatchUpdates createFromParcel(Parcel parcel) {
194            // Always go through the builder to ensure the data ingested by
195            // the system obeys the contract of the builder to avoid attacks
196            // using specially crafted parcels.
197            final Builder builder = new Builder();
198            final int[] ids = parcel.createIntArray();
199            if (ids != null) {
200                final InternalTransformation[] values =
201                    parcel.readParcelableArray(null, InternalTransformation.class);
202                final int size = ids.length;
203                for (int i = 0; i < size; i++) {
204                    builder.transformChild(ids[i], values[i]);
205                }
206            }
207            final RemoteViews updates = parcel.readParcelable(null);
208            if (updates != null) {
209                builder.updateTemplate(updates);
210            }
211            return builder.build();
212        }
213
214        @Override
215        public BatchUpdates[] newArray(int size) {
216            return new BatchUpdates[size];
217        }
218    };
219}
220