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