1/*
2 * Copyright (C) 2016 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.widget;
18
19import static org.junit.Assert.assertEquals;
20import static org.junit.Assert.assertSame;
21import static org.junit.Assert.assertTrue;
22
23import android.app.PendingIntent;
24import android.content.Context;
25import android.content.Intent;
26import android.graphics.Bitmap;
27import android.graphics.drawable.BitmapDrawable;
28import android.graphics.drawable.Drawable;
29import android.os.AsyncTask;
30import android.os.Binder;
31import android.os.Parcel;
32import android.support.test.InstrumentationRegistry;
33import android.support.test.filters.SmallTest;
34import android.support.test.runner.AndroidJUnit4;
35import android.view.View;
36import android.view.ViewGroup;
37
38import com.android.frameworks.coretests.R;
39
40import org.junit.Before;
41import org.junit.Rule;
42import org.junit.Test;
43import org.junit.rules.ExpectedException;
44import org.junit.runner.RunWith;
45
46import java.util.ArrayList;
47import java.util.Arrays;
48import java.util.concurrent.CountDownLatch;
49
50/**
51 * Tests for RemoteViews.
52 */
53@RunWith(AndroidJUnit4.class)
54@SmallTest
55public class RemoteViewsTest {
56
57    // This can point to any other package which exists on the device.
58    private static final String OTHER_PACKAGE = "com.android.systemui";
59
60    @Rule
61    public final ExpectedException exception = ExpectedException.none();
62
63    private Context mContext;
64    private String mPackage;
65    private LinearLayout mContainer;
66
67    @Before
68    public void setup() {
69        mContext = InstrumentationRegistry.getContext();
70        mPackage = mContext.getPackageName();
71        mContainer = new LinearLayout(mContext);
72    }
73
74    @Test
75    public void clone_doesNotCopyBitmap() {
76        RemoteViews original = new RemoteViews(mPackage, R.layout.remote_views_test);
77        Bitmap bitmap = Bitmap.createBitmap(10, 10, Bitmap.Config.ARGB_8888);
78
79        original.setImageViewBitmap(R.id.image, bitmap);
80        RemoteViews clone = original.clone();
81        View inflated = clone.apply(mContext, mContainer);
82
83        Drawable drawable = ((ImageView) inflated.findViewById(R.id.image)).getDrawable();
84        assertSame(bitmap, ((BitmapDrawable)drawable).getBitmap());
85    }
86
87    @Test
88    public void clone_originalCanStillBeApplied() {
89        RemoteViews original = new RemoteViews(mPackage, R.layout.remote_views_test);
90
91        RemoteViews clone = original.clone();
92
93        clone.apply(mContext, mContainer);
94    }
95
96    @Test
97    public void clone_clones() {
98        RemoteViews original = new RemoteViews(mPackage, R.layout.remote_views_test);
99
100        RemoteViews clone = original.clone();
101        original.setTextViewText(R.id.text, "test");
102        View inflated = clone.apply(mContext, mContainer);
103
104        TextView textView = (TextView) inflated.findViewById(R.id.text);
105        assertEquals("", textView.getText());
106    }
107
108    @Test
109    public void clone_child_fails() {
110        RemoteViews original = new RemoteViews(mPackage, R.layout.remote_views_test);
111        RemoteViews child = new RemoteViews(mPackage, R.layout.remote_views_test);
112
113        original.addView(R.id.layout, child);
114
115        exception.expect(IllegalStateException.class);
116        RemoteViews clone = child.clone();
117    }
118
119    @Test
120    public void clone_repeatedly() {
121        RemoteViews original = new RemoteViews(mPackage, R.layout.remote_views_test);
122
123        original.clone();
124        original.clone();
125
126        original.apply(mContext, mContainer);
127    }
128
129    @Test
130    public void clone_chained() {
131        RemoteViews original = new RemoteViews(mPackage, R.layout.remote_views_test);
132
133        RemoteViews clone = original.clone().clone();
134
135        clone.apply(mContext, mContainer);
136    }
137
138    @Test
139    public void parcelSize_nestedViews() {
140        RemoteViews original = new RemoteViews(mPackage, R.layout.remote_views_test);
141        // We don't care about the actual layout id.
142        RemoteViews child = new RemoteViews(mPackage, 33);
143        int expectedSize = getParcelSize(original) + getParcelSize(child);
144        original.addView(R.id.layout, child);
145
146        // The application info will get written only once.
147        assertTrue(getParcelSize(original) < expectedSize);
148        assertEquals(getParcelSize(original), getParcelSize(original.clone()));
149
150        original = new RemoteViews(mPackage, R.layout.remote_views_test);
151        child = new RemoteViews(OTHER_PACKAGE, 33);
152        expectedSize = getParcelSize(original) + getParcelSize(child);
153        original.addView(R.id.layout, child);
154
155        // Both the views will get written completely along with an additional view operation
156        assertTrue(getParcelSize(original) > expectedSize);
157        assertEquals(getParcelSize(original), getParcelSize(original.clone()));
158    }
159
160    @Test
161    public void parcelSize_differentOrientation() {
162        RemoteViews landscape = new RemoteViews(mPackage, R.layout.remote_views_test);
163        RemoteViews portrait = new RemoteViews(mPackage, 33);
164
165        // The application info will get written only once.
166        RemoteViews views = new RemoteViews(landscape, portrait);
167        assertTrue(getParcelSize(views) < (getParcelSize(landscape) + getParcelSize(portrait)));
168        assertEquals(getParcelSize(views), getParcelSize(views.clone()));
169    }
170
171    private int getParcelSize(RemoteViews view) {
172        Parcel parcel = Parcel.obtain();
173        view.writeToParcel(parcel, 0);
174        int size = parcel.dataSize();
175        parcel.recycle();
176        return size;
177    }
178
179    @Test
180    public void asyncApply_fail() throws Exception {
181        RemoteViews views = new RemoteViews(mPackage, R.layout.remote_view_test_bad_1);
182        ViewAppliedListener listener = new ViewAppliedListener();
183        views.applyAsync(mContext, mContainer, AsyncTask.THREAD_POOL_EXECUTOR, listener);
184
185        exception.expect(Exception.class);
186        listener.waitAndGetView();
187    }
188
189    @Test
190    public void asyncApply() throws Exception {
191        RemoteViews views = new RemoteViews(mPackage, R.layout.remote_views_test);
192        views.setTextViewText(R.id.text, "Dummy");
193
194        View syncView = views.apply(mContext, mContainer);
195
196        ViewAppliedListener listener = new ViewAppliedListener();
197        views.applyAsync(mContext, mContainer, AsyncTask.THREAD_POOL_EXECUTOR, listener);
198        View asyncView = listener.waitAndGetView();
199
200        verifyViewTree(syncView, asyncView, "Dummy");
201    }
202
203    @Test
204    public void asyncApply_viewStub() throws Exception {
205        RemoteViews views = new RemoteViews(mPackage, R.layout.remote_views_viewstub);
206        views.setInt(R.id.viewStub, "setLayoutResource", R.layout.remote_views_text);
207        // This will cause the view to be inflated
208        views.setViewVisibility(R.id.viewStub, View.INVISIBLE);
209        views.setTextViewText(R.id.stub_inflated, "Dummy");
210
211        View syncView = views.apply(mContext, mContainer);
212
213        ViewAppliedListener listener = new ViewAppliedListener();
214        views.applyAsync(mContext, mContainer, AsyncTask.THREAD_POOL_EXECUTOR, listener);
215        View asyncView = listener.waitAndGetView();
216
217        verifyViewTree(syncView, asyncView, "Dummy");
218    }
219
220    @Test
221    public void asyncApply_nestedViews() throws Exception {
222        RemoteViews views = new RemoteViews(mPackage, R.layout.remote_view_host);
223        views.removeAllViews(R.id.container);
224        views.addView(R.id.container, createViewChained(1, "row1-c1", "row1-c2", "row1-c3"));
225        views.addView(R.id.container, createViewChained(5, "row2-c1", "row2-c2"));
226        views.addView(R.id.container, createViewChained(2, "row3-c1", "row3-c2"));
227
228        View syncView = views.apply(mContext, mContainer);
229
230        ViewAppliedListener listener = new ViewAppliedListener();
231        views.applyAsync(mContext, mContainer, AsyncTask.THREAD_POOL_EXECUTOR, listener);
232        View asyncView = listener.waitAndGetView();
233
234        verifyViewTree(syncView, asyncView,
235                "row1-c1", "row1-c2", "row1-c3", "row2-c1", "row2-c2", "row3-c1", "row3-c2");
236    }
237
238    @Test
239    public void asyncApply_viewstub_nestedViews() throws Exception {
240        RemoteViews viewstub = new RemoteViews(mPackage, R.layout.remote_views_viewstub);
241        viewstub.setInt(R.id.viewStub, "setLayoutResource", R.layout.remote_view_host);
242        // This will cause the view to be inflated
243        viewstub.setViewVisibility(R.id.viewStub, View.INVISIBLE);
244        viewstub.addView(R.id.stub_inflated, createViewChained(1, "row1-c1", "row1-c2", "row1-c3"));
245
246        RemoteViews views = new RemoteViews(mPackage, R.layout.remote_view_host);
247        views.removeAllViews(R.id.container);
248        views.addView(R.id.container, viewstub);
249        views.addView(R.id.container, createViewChained(5, "row2-c1", "row2-c2"));
250
251        View syncView = views.apply(mContext, mContainer);
252
253        ViewAppliedListener listener = new ViewAppliedListener();
254        views.applyAsync(mContext, mContainer, AsyncTask.THREAD_POOL_EXECUTOR, listener);
255        View asyncView = listener.waitAndGetView();
256
257        verifyViewTree(syncView, asyncView, "row1-c1", "row1-c2", "row1-c3", "row2-c1", "row2-c2");
258    }
259
260    private RemoteViews createViewChained(int depth, String... texts) {
261        RemoteViews result = new RemoteViews(mPackage, R.layout.remote_view_host);
262
263        // Create depth
264        RemoteViews parent = result;
265        while(depth > 0) {
266            depth--;
267            RemoteViews child = new RemoteViews(mPackage, R.layout.remote_view_host);
268            parent.addView(R.id.container, child);
269            parent = child;
270        }
271
272        // Add texts
273        for (String text : texts) {
274            RemoteViews child = new RemoteViews(mPackage, R.layout.remote_views_text);
275            child.setTextViewText(R.id.text, text);
276            parent.addView(R.id.container, child);
277        }
278        return result;
279    }
280
281    private void verifyViewTree(View v1, View v2, String... texts) {
282        ArrayList<String> expectedTexts = new ArrayList<>(Arrays.asList(texts));
283        verifyViewTreeRecur(v1, v2, expectedTexts);
284        // Verify that all expected texts were found
285        assertEquals(0, expectedTexts.size());
286    }
287
288    private void verifyViewTreeRecur(View v1, View v2, ArrayList<String> expectedTexts) {
289        assertEquals(v1.getClass(), v2.getClass());
290
291        if (v1 instanceof TextView) {
292            String text = ((TextView) v1).getText().toString();
293            assertEquals(text, ((TextView) v2).getText().toString());
294            // Verify that the text was one of the expected texts and remove it from the list
295            assertTrue(expectedTexts.remove(text));
296        } else if (v1 instanceof ViewGroup) {
297            ViewGroup vg1 = (ViewGroup) v1;
298            ViewGroup vg2 = (ViewGroup) v2;
299            assertEquals(vg1.getChildCount(), vg2.getChildCount());
300            for (int i = vg1.getChildCount() - 1; i >= 0; i--) {
301                verifyViewTreeRecur(vg1.getChildAt(i), vg2.getChildAt(i), expectedTexts);
302            }
303        }
304    }
305
306    private class ViewAppliedListener implements RemoteViews.OnViewAppliedListener {
307
308        private final CountDownLatch mLatch = new CountDownLatch(1);
309        private View mView;
310        private Exception mError;
311
312        @Override
313        public void onViewApplied(View v) {
314            mView = v;
315            mLatch.countDown();
316        }
317
318        @Override
319        public void onError(Exception e) {
320            mError = e;
321            mLatch.countDown();
322        }
323
324        public View waitAndGetView() throws Exception {
325            mLatch.await();
326
327            if (mError != null) {
328                throw new Exception(mError);
329            }
330            return mView;
331        }
332    }
333
334    @Test
335    public void nestedAddViews() {
336        RemoteViews views = new RemoteViews(mPackage, R.layout.remote_views_test);
337        for (int i = 0; i < 10; i++) {
338            RemoteViews parent = new RemoteViews(mPackage, R.layout.remote_views_test);
339            parent.addView(R.id.layout, views);
340            views = parent;
341        }
342        // Both clone and parcel/unparcel work,
343        views.clone();
344        parcelAndRecreate(views);
345
346        views = new RemoteViews(mPackage, R.layout.remote_views_test);
347        for (int i = 0; i < 11; i++) {
348            RemoteViews parent = new RemoteViews(mPackage, R.layout.remote_views_test);
349            parent.addView(R.id.layout, views);
350            views = parent;
351        }
352        // Clone works but parcel/unparcel fails
353        views.clone();
354        exception.expect(IllegalArgumentException.class);
355        parcelAndRecreate(views);
356    }
357
358    @Test
359    public void nestedLandscapeViews() {
360        RemoteViews views = new RemoteViews(mPackage, R.layout.remote_views_test);
361        for (int i = 0; i < 10; i++) {
362            views = new RemoteViews(views,
363                    new RemoteViews(mPackage, R.layout.remote_views_test));
364        }
365        // Both clone and parcel/unparcel work,
366        views.clone();
367        parcelAndRecreate(views);
368
369        views = new RemoteViews(mPackage, R.layout.remote_views_test);
370        for (int i = 0; i < 11; i++) {
371            views = new RemoteViews(views,
372                    new RemoteViews(mPackage, R.layout.remote_views_test));
373        }
374        // Clone works but parcel/unparcel fails
375        views.clone();
376        exception.expect(IllegalArgumentException.class);
377        parcelAndRecreate(views);
378    }
379
380    private RemoteViews parcelAndRecreate(RemoteViews views) {
381        return parcelAndRecreateWithPendingIntentCookie(views, null);
382    }
383
384    private RemoteViews parcelAndRecreateWithPendingIntentCookie(RemoteViews views, Object cookie) {
385        Parcel p = Parcel.obtain();
386        try {
387            views.writeToParcel(p, 0);
388            p.setDataPosition(0);
389
390            if (cookie != null) {
391                p.setClassCookie(PendingIntent.class, cookie);
392            }
393
394            return RemoteViews.CREATOR.createFromParcel(p);
395        } finally {
396            p.recycle();
397        }
398    }
399
400    @Test
401    public void copyWithBinders() throws Exception {
402        RemoteViews views = new RemoteViews(mPackage, R.layout.remote_views_test);
403        for (int i = 1; i < 10; i++) {
404            PendingIntent pi = PendingIntent.getBroadcast(mContext, 0,
405                    new Intent("android.widget.RemoteViewsTest_" + i), PendingIntent.FLAG_ONE_SHOT);
406            views.setOnClickPendingIntent(i, pi);
407        }
408        try {
409            new RemoteViews(views);
410        } catch (Throwable t) {
411            throw new Exception(t);
412        }
413    }
414
415    @Test
416    public void copy_keepsPendingIntentWhitelistToken() throws Exception {
417        Binder whitelistToken = new Binder();
418
419        RemoteViews views = new RemoteViews(mPackage, R.layout.remote_views_test);
420        PendingIntent pi = PendingIntent.getBroadcast(mContext, 0,
421                new Intent("test"), PendingIntent.FLAG_ONE_SHOT);
422        views.setOnClickPendingIntent(1, pi);
423        RemoteViews withCookie = parcelAndRecreateWithPendingIntentCookie(views, whitelistToken);
424
425        RemoteViews cloned = new RemoteViews(withCookie);
426
427        PendingIntent found = extractAnyPendingIntent(cloned);
428        assertEquals(whitelistToken, found.getWhitelistToken());
429    }
430
431    private PendingIntent extractAnyPendingIntent(RemoteViews cloned) {
432        PendingIntent[] found = new PendingIntent[1];
433        Parcel p = Parcel.obtain();
434        try {
435            PendingIntent.setOnMarshaledListener((intent, parcel, flags) -> {
436                if (parcel == p) {
437                    found[0] = intent;
438                }
439            });
440            cloned.writeToParcel(p, 0);
441        } finally {
442            p.recycle();
443            PendingIntent.setOnMarshaledListener(null);
444        }
445        return found[0];
446    }
447}
448