blob: 36792bbf6fb6b9989b0168bb26ac1acb2a1266cf [file] [log] [blame]
Adrian Roos7da889d2016-03-16 18:38:58 -07001/*
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
Kirill Grouchnikov8376f9e2016-07-08 11:27:03 -040017package android.widget;
Adrian Roos7da889d2016-03-16 18:38:58 -070018
Sunny Goyal43c97042018-08-23 15:21:26 -070019import static org.junit.Assert.assertArrayEquals;
Sunny Goyal56333a82017-08-29 13:46:29 -070020import static org.junit.Assert.assertEquals;
21import static org.junit.Assert.assertSame;
22import static org.junit.Assert.assertTrue;
23
Sunny Goyal43c97042018-08-23 15:21:26 -070024import android.app.ActivityOptions;
Sunny Goyal5b7689f2017-09-21 11:08:34 -070025import android.app.PendingIntent;
Sunny Goyal43c97042018-08-23 15:21:26 -070026import android.appwidget.AppWidgetHostView;
Adrian Roos7da889d2016-03-16 18:38:58 -070027import android.content.Context;
Sunny Goyal5b7689f2017-09-21 11:08:34 -070028import android.content.Intent;
Adrian Roos7da889d2016-03-16 18:38:58 -070029import android.graphics.Bitmap;
30import android.graphics.drawable.BitmapDrawable;
31import android.graphics.drawable.Drawable;
Sunny Goyal7b0e2c72016-11-03 14:48:05 -070032import android.os.AsyncTask;
Adrian Roosfb921842017-10-26 14:49:56 +020033import android.os.Binder;
Sunny Goyal5d8bcdf2016-10-27 16:11:29 -070034import android.os.Parcel;
Adrian Roos7da889d2016-03-16 18:38:58 -070035import android.support.test.InstrumentationRegistry;
36import android.support.test.filters.SmallTest;
37import android.support.test.runner.AndroidJUnit4;
Kirill Grouchnikov8376f9e2016-07-08 11:27:03 -040038import android.view.View;
Sunny Goyal7b0e2c72016-11-03 14:48:05 -070039import android.view.ViewGroup;
Adrian Roos7da889d2016-03-16 18:38:58 -070040
41import com.android.frameworks.coretests.R;
42
Kirill Grouchnikov8376f9e2016-07-08 11:27:03 -040043import org.junit.Before;
44import org.junit.Rule;
45import org.junit.Test;
46import org.junit.rules.ExpectedException;
47import org.junit.runner.RunWith;
48
Sunny Goyal7b0e2c72016-11-03 14:48:05 -070049import java.util.ArrayList;
50import java.util.Arrays;
51import java.util.concurrent.CountDownLatch;
52
Adrian Roos7da889d2016-03-16 18:38:58 -070053/**
54 * Tests for RemoteViews.
55 */
56@RunWith(AndroidJUnit4.class)
57@SmallTest
58public class RemoteViewsTest {
59
Sunny Goyal5d8bcdf2016-10-27 16:11:29 -070060 // This can point to any other package which exists on the device.
61 private static final String OTHER_PACKAGE = "com.android.systemui";
62
Adrian Roos7da889d2016-03-16 18:38:58 -070063 @Rule
64 public final ExpectedException exception = ExpectedException.none();
65
66 private Context mContext;
67 private String mPackage;
68 private LinearLayout mContainer;
69
70 @Before
71 public void setup() {
72 mContext = InstrumentationRegistry.getContext();
Adrian Roos4d10db72016-05-23 14:32:39 -070073 mPackage = mContext.getPackageName();
Adrian Roos7da889d2016-03-16 18:38:58 -070074 mContainer = new LinearLayout(mContext);
75 }
76
77 @Test
78 public void clone_doesNotCopyBitmap() {
79 RemoteViews original = new RemoteViews(mPackage, R.layout.remote_views_test);
80 Bitmap bitmap = Bitmap.createBitmap(10, 10, Bitmap.Config.ARGB_8888);
81
82 original.setImageViewBitmap(R.id.image, bitmap);
83 RemoteViews clone = original.clone();
84 View inflated = clone.apply(mContext, mContainer);
85
86 Drawable drawable = ((ImageView) inflated.findViewById(R.id.image)).getDrawable();
87 assertSame(bitmap, ((BitmapDrawable)drawable).getBitmap());
88 }
89
90 @Test
91 public void clone_originalCanStillBeApplied() {
92 RemoteViews original = new RemoteViews(mPackage, R.layout.remote_views_test);
93
94 RemoteViews clone = original.clone();
95
96 clone.apply(mContext, mContainer);
97 }
98
99 @Test
100 public void clone_clones() {
101 RemoteViews original = new RemoteViews(mPackage, R.layout.remote_views_test);
102
103 RemoteViews clone = original.clone();
104 original.setTextViewText(R.id.text, "test");
105 View inflated = clone.apply(mContext, mContainer);
106
107 TextView textView = (TextView) inflated.findViewById(R.id.text);
108 assertEquals("", textView.getText());
109 }
110
111 @Test
112 public void clone_child_fails() {
113 RemoteViews original = new RemoteViews(mPackage, R.layout.remote_views_test);
114 RemoteViews child = new RemoteViews(mPackage, R.layout.remote_views_test);
115
116 original.addView(R.id.layout, child);
117
118 exception.expect(IllegalStateException.class);
119 RemoteViews clone = child.clone();
120 }
121
122 @Test
123 public void clone_repeatedly() {
124 RemoteViews original = new RemoteViews(mPackage, R.layout.remote_views_test);
125
126 original.clone();
127 original.clone();
128
129 original.apply(mContext, mContainer);
130 }
131
132 @Test
133 public void clone_chained() {
134 RemoteViews original = new RemoteViews(mPackage, R.layout.remote_views_test);
135
136 RemoteViews clone = original.clone().clone();
137
138 clone.apply(mContext, mContainer);
139 }
140
Sunny Goyal5d8bcdf2016-10-27 16:11:29 -0700141 @Test
142 public void parcelSize_nestedViews() {
143 RemoteViews original = new RemoteViews(mPackage, R.layout.remote_views_test);
144 // We don't care about the actual layout id.
145 RemoteViews child = new RemoteViews(mPackage, 33);
146 int expectedSize = getParcelSize(original) + getParcelSize(child);
147 original.addView(R.id.layout, child);
148
149 // The application info will get written only once.
150 assertTrue(getParcelSize(original) < expectedSize);
151 assertEquals(getParcelSize(original), getParcelSize(original.clone()));
152
153 original = new RemoteViews(mPackage, R.layout.remote_views_test);
154 child = new RemoteViews(OTHER_PACKAGE, 33);
155 expectedSize = getParcelSize(original) + getParcelSize(child);
156 original.addView(R.id.layout, child);
157
158 // Both the views will get written completely along with an additional view operation
159 assertTrue(getParcelSize(original) > expectedSize);
160 assertEquals(getParcelSize(original), getParcelSize(original.clone()));
161 }
162
163 @Test
164 public void parcelSize_differentOrientation() {
165 RemoteViews landscape = new RemoteViews(mPackage, R.layout.remote_views_test);
166 RemoteViews portrait = new RemoteViews(mPackage, 33);
167
168 // The application info will get written only once.
169 RemoteViews views = new RemoteViews(landscape, portrait);
170 assertTrue(getParcelSize(views) < (getParcelSize(landscape) + getParcelSize(portrait)));
171 assertEquals(getParcelSize(views), getParcelSize(views.clone()));
172 }
173
174 private int getParcelSize(RemoteViews view) {
175 Parcel parcel = Parcel.obtain();
176 view.writeToParcel(parcel, 0);
177 int size = parcel.dataSize();
178 parcel.recycle();
179 return size;
180 }
Sunny Goyal7b0e2c72016-11-03 14:48:05 -0700181
182 @Test
183 public void asyncApply_fail() throws Exception {
184 RemoteViews views = new RemoteViews(mPackage, R.layout.remote_view_test_bad_1);
185 ViewAppliedListener listener = new ViewAppliedListener();
186 views.applyAsync(mContext, mContainer, AsyncTask.THREAD_POOL_EXECUTOR, listener);
187
Sunny Goyal692f8c92016-11-11 09:19:14 -0800188 exception.expect(Exception.class);
189 listener.waitAndGetView();
Sunny Goyal7b0e2c72016-11-03 14:48:05 -0700190 }
191
192 @Test
193 public void asyncApply() throws Exception {
194 RemoteViews views = new RemoteViews(mPackage, R.layout.remote_views_test);
195 views.setTextViewText(R.id.text, "Dummy");
196
197 View syncView = views.apply(mContext, mContainer);
198
199 ViewAppliedListener listener = new ViewAppliedListener();
200 views.applyAsync(mContext, mContainer, AsyncTask.THREAD_POOL_EXECUTOR, listener);
201 View asyncView = listener.waitAndGetView();
202
203 verifyViewTree(syncView, asyncView, "Dummy");
204 }
205
206 @Test
207 public void asyncApply_viewStub() throws Exception {
208 RemoteViews views = new RemoteViews(mPackage, R.layout.remote_views_viewstub);
209 views.setInt(R.id.viewStub, "setLayoutResource", R.layout.remote_views_text);
210 // This will cause the view to be inflated
211 views.setViewVisibility(R.id.viewStub, View.INVISIBLE);
212 views.setTextViewText(R.id.stub_inflated, "Dummy");
213
214 View syncView = views.apply(mContext, mContainer);
215
216 ViewAppliedListener listener = new ViewAppliedListener();
217 views.applyAsync(mContext, mContainer, AsyncTask.THREAD_POOL_EXECUTOR, listener);
218 View asyncView = listener.waitAndGetView();
219
220 verifyViewTree(syncView, asyncView, "Dummy");
221 }
222
223 @Test
224 public void asyncApply_nestedViews() throws Exception {
225 RemoteViews views = new RemoteViews(mPackage, R.layout.remote_view_host);
226 views.removeAllViews(R.id.container);
227 views.addView(R.id.container, createViewChained(1, "row1-c1", "row1-c2", "row1-c3"));
228 views.addView(R.id.container, createViewChained(5, "row2-c1", "row2-c2"));
229 views.addView(R.id.container, createViewChained(2, "row3-c1", "row3-c2"));
230
231 View syncView = views.apply(mContext, mContainer);
232
233 ViewAppliedListener listener = new ViewAppliedListener();
234 views.applyAsync(mContext, mContainer, AsyncTask.THREAD_POOL_EXECUTOR, listener);
235 View asyncView = listener.waitAndGetView();
236
237 verifyViewTree(syncView, asyncView,
238 "row1-c1", "row1-c2", "row1-c3", "row2-c1", "row2-c2", "row3-c1", "row3-c2");
239 }
240
241 @Test
242 public void asyncApply_viewstub_nestedViews() throws Exception {
243 RemoteViews viewstub = new RemoteViews(mPackage, R.layout.remote_views_viewstub);
244 viewstub.setInt(R.id.viewStub, "setLayoutResource", R.layout.remote_view_host);
245 // This will cause the view to be inflated
246 viewstub.setViewVisibility(R.id.viewStub, View.INVISIBLE);
247 viewstub.addView(R.id.stub_inflated, createViewChained(1, "row1-c1", "row1-c2", "row1-c3"));
248
249 RemoteViews views = new RemoteViews(mPackage, R.layout.remote_view_host);
250 views.removeAllViews(R.id.container);
251 views.addView(R.id.container, viewstub);
252 views.addView(R.id.container, createViewChained(5, "row2-c1", "row2-c2"));
253
254 View syncView = views.apply(mContext, mContainer);
255
256 ViewAppliedListener listener = new ViewAppliedListener();
257 views.applyAsync(mContext, mContainer, AsyncTask.THREAD_POOL_EXECUTOR, listener);
258 View asyncView = listener.waitAndGetView();
259
260 verifyViewTree(syncView, asyncView, "row1-c1", "row1-c2", "row1-c3", "row2-c1", "row2-c2");
261 }
262
263 private RemoteViews createViewChained(int depth, String... texts) {
264 RemoteViews result = new RemoteViews(mPackage, R.layout.remote_view_host);
265
266 // Create depth
267 RemoteViews parent = result;
268 while(depth > 0) {
269 depth--;
270 RemoteViews child = new RemoteViews(mPackage, R.layout.remote_view_host);
271 parent.addView(R.id.container, child);
272 parent = child;
273 }
274
275 // Add texts
276 for (String text : texts) {
277 RemoteViews child = new RemoteViews(mPackage, R.layout.remote_views_text);
278 child.setTextViewText(R.id.text, text);
279 parent.addView(R.id.container, child);
280 }
281 return result;
282 }
283
284 private void verifyViewTree(View v1, View v2, String... texts) {
285 ArrayList<String> expectedTexts = new ArrayList<>(Arrays.asList(texts));
286 verifyViewTreeRecur(v1, v2, expectedTexts);
287 // Verify that all expected texts were found
288 assertEquals(0, expectedTexts.size());
289 }
290
291 private void verifyViewTreeRecur(View v1, View v2, ArrayList<String> expectedTexts) {
292 assertEquals(v1.getClass(), v2.getClass());
293
294 if (v1 instanceof TextView) {
295 String text = ((TextView) v1).getText().toString();
296 assertEquals(text, ((TextView) v2).getText().toString());
297 // Verify that the text was one of the expected texts and remove it from the list
298 assertTrue(expectedTexts.remove(text));
299 } else if (v1 instanceof ViewGroup) {
300 ViewGroup vg1 = (ViewGroup) v1;
301 ViewGroup vg2 = (ViewGroup) v2;
302 assertEquals(vg1.getChildCount(), vg2.getChildCount());
303 for (int i = vg1.getChildCount() - 1; i >= 0; i--) {
304 verifyViewTreeRecur(vg1.getChildAt(i), vg2.getChildAt(i), expectedTexts);
305 }
306 }
307 }
308
309 private class ViewAppliedListener implements RemoteViews.OnViewAppliedListener {
310
311 private final CountDownLatch mLatch = new CountDownLatch(1);
312 private View mView;
313 private Exception mError;
314
315 @Override
316 public void onViewApplied(View v) {
317 mView = v;
318 mLatch.countDown();
319 }
320
321 @Override
322 public void onError(Exception e) {
323 mError = e;
324 mLatch.countDown();
325 }
326
327 public View waitAndGetView() throws Exception {
328 mLatch.await();
329
330 if (mError != null) {
331 throw new Exception(mError);
332 }
333 return mView;
334 }
335 }
Sunny Goyal692f8c92016-11-11 09:19:14 -0800336
337 @Test
338 public void nestedAddViews() {
339 RemoteViews views = new RemoteViews(mPackage, R.layout.remote_views_test);
340 for (int i = 0; i < 10; i++) {
341 RemoteViews parent = new RemoteViews(mPackage, R.layout.remote_views_test);
342 parent.addView(R.id.layout, views);
343 views = parent;
344 }
Sunny Goyal56333a82017-08-29 13:46:29 -0700345 // Both clone and parcel/unparcel work,
Sunny Goyal692f8c92016-11-11 09:19:14 -0800346 views.clone();
Sunny Goyal56333a82017-08-29 13:46:29 -0700347 parcelAndRecreate(views);
Sunny Goyal692f8c92016-11-11 09:19:14 -0800348
349 views = new RemoteViews(mPackage, R.layout.remote_views_test);
350 for (int i = 0; i < 11; i++) {
351 RemoteViews parent = new RemoteViews(mPackage, R.layout.remote_views_test);
352 parent.addView(R.id.layout, views);
353 views = parent;
354 }
Sunny Goyal56333a82017-08-29 13:46:29 -0700355 // Clone works but parcel/unparcel fails
Sunny Goyal692f8c92016-11-11 09:19:14 -0800356 views.clone();
Sunny Goyal56333a82017-08-29 13:46:29 -0700357 exception.expect(IllegalArgumentException.class);
358 parcelAndRecreate(views);
Sunny Goyal692f8c92016-11-11 09:19:14 -0800359 }
360
361 @Test
362 public void nestedLandscapeViews() {
363 RemoteViews views = new RemoteViews(mPackage, R.layout.remote_views_test);
364 for (int i = 0; i < 10; i++) {
365 views = new RemoteViews(views,
366 new RemoteViews(mPackage, R.layout.remote_views_test));
367 }
Sunny Goyal56333a82017-08-29 13:46:29 -0700368 // Both clone and parcel/unparcel work,
Sunny Goyal692f8c92016-11-11 09:19:14 -0800369 views.clone();
Sunny Goyal56333a82017-08-29 13:46:29 -0700370 parcelAndRecreate(views);
Sunny Goyal692f8c92016-11-11 09:19:14 -0800371
372 views = new RemoteViews(mPackage, R.layout.remote_views_test);
373 for (int i = 0; i < 11; i++) {
Sunny Goyal56333a82017-08-29 13:46:29 -0700374 views = new RemoteViews(views,
375 new RemoteViews(mPackage, R.layout.remote_views_test));
Sunny Goyal692f8c92016-11-11 09:19:14 -0800376 }
Sunny Goyal56333a82017-08-29 13:46:29 -0700377 // Clone works but parcel/unparcel fails
Sunny Goyal692f8c92016-11-11 09:19:14 -0800378 views.clone();
Sunny Goyal56333a82017-08-29 13:46:29 -0700379 exception.expect(IllegalArgumentException.class);
380 parcelAndRecreate(views);
381 }
382
Adrian Roosfb921842017-10-26 14:49:56 +0200383 private RemoteViews parcelAndRecreate(RemoteViews views) {
384 return parcelAndRecreateWithPendingIntentCookie(views, null);
385 }
Sunny Goyal56333a82017-08-29 13:46:29 -0700386
Adrian Roosfb921842017-10-26 14:49:56 +0200387 private RemoteViews parcelAndRecreateWithPendingIntentCookie(RemoteViews views, Object cookie) {
388 Parcel p = Parcel.obtain();
389 try {
390 views.writeToParcel(p, 0);
391 p.setDataPosition(0);
392
393 if (cookie != null) {
394 p.setClassCookie(PendingIntent.class, cookie);
395 }
396
397 return RemoteViews.CREATOR.createFromParcel(p);
398 } finally {
399 p.recycle();
400 }
Sunny Goyal692f8c92016-11-11 09:19:14 -0800401 }
Sunny Goyal5b7689f2017-09-21 11:08:34 -0700402
403 @Test
404 public void copyWithBinders() throws Exception {
405 RemoteViews views = new RemoteViews(mPackage, R.layout.remote_views_test);
406 for (int i = 1; i < 10; i++) {
407 PendingIntent pi = PendingIntent.getBroadcast(mContext, 0,
408 new Intent("android.widget.RemoteViewsTest_" + i), PendingIntent.FLAG_ONE_SHOT);
409 views.setOnClickPendingIntent(i, pi);
410 }
411 try {
412 new RemoteViews(views);
413 } catch (Throwable t) {
414 throw new Exception(t);
415 }
416 }
Adrian Roosfb921842017-10-26 14:49:56 +0200417
418 @Test
419 public void copy_keepsPendingIntentWhitelistToken() throws Exception {
420 Binder whitelistToken = new Binder();
421
422 RemoteViews views = new RemoteViews(mPackage, R.layout.remote_views_test);
423 PendingIntent pi = PendingIntent.getBroadcast(mContext, 0,
424 new Intent("test"), PendingIntent.FLAG_ONE_SHOT);
425 views.setOnClickPendingIntent(1, pi);
426 RemoteViews withCookie = parcelAndRecreateWithPendingIntentCookie(views, whitelistToken);
427
428 RemoteViews cloned = new RemoteViews(withCookie);
429
430 PendingIntent found = extractAnyPendingIntent(cloned);
431 assertEquals(whitelistToken, found.getWhitelistToken());
432 }
433
434 private PendingIntent extractAnyPendingIntent(RemoteViews cloned) {
435 PendingIntent[] found = new PendingIntent[1];
436 Parcel p = Parcel.obtain();
437 try {
438 PendingIntent.setOnMarshaledListener((intent, parcel, flags) -> {
439 if (parcel == p) {
440 found[0] = intent;
441 }
442 });
443 cloned.writeToParcel(p, 0);
444 } finally {
445 p.recycle();
446 PendingIntent.setOnMarshaledListener(null);
447 }
448 return found[0];
449 }
Sunny Goyal43c97042018-08-23 15:21:26 -0700450
451 @Test
452 public void sharedElement_pendingIntent_notifyParent() throws Exception {
453 RemoteViews views = new RemoteViews(mPackage, R.layout.remote_views_test);
454 PendingIntent pi = PendingIntent.getBroadcast(mContext, 0,
455 new Intent("android.widget.RemoteViewsTest_shared_element"),
456 PendingIntent.FLAG_ONE_SHOT);
457 views.setOnClickResponse(R.id.image, RemoteViews.RemoteResponse.fromPendingIntent(pi)
458 .addSharedElement(0, "e0")
459 .addSharedElement(1, "e1")
460 .addSharedElement(2, "e2"));
461
462 WidgetContainer container = new WidgetContainer(mContext);
463 container.addView(new RemoteViews(views).apply(mContext, container));
464 container.findViewById(R.id.image).performClick();
465
466 assertArrayEquals(container.mSharedViewIds, new int[] {0, 1, 2});
467 assertArrayEquals(container.mSharedViewNames, new String[] {"e0", "e1", "e2"});
468 }
469
Tony Mak7d4b3a52018-11-27 17:29:36 +0000470 @Test
471 public void setIntTag() {
472 RemoteViews views = new RemoteViews(mPackage, R.layout.remote_views_test);
473 int index = 10;
474 views.setIntTag(
475 R.id.layout, com.android.internal.R.id.notification_action_index_tag, index);
476
477 RemoteViews recovered = parcelAndRecreate(views);
478 RemoteViews cloned = new RemoteViews(recovered);
479 View inflated = cloned.apply(mContext, mContainer);
480
481 assertEquals(
482 index, inflated.getTag(com.android.internal.R.id.notification_action_index_tag));
483 }
484
Sunny Goyal43c97042018-08-23 15:21:26 -0700485 private class WidgetContainer extends AppWidgetHostView {
486 int[] mSharedViewIds;
487 String[] mSharedViewNames;
488
489 WidgetContainer(Context context) {
490 super(context);
491 }
492
493 @Override
494 public ActivityOptions createSharedElementActivityOptions(
495 int[] sharedViewIds, String[] sharedViewNames, Intent fillInIntent) {
496 mSharedViewIds = sharedViewIds;
497 mSharedViewNames = sharedViewNames;
498 return null;
499 }
500 }
Adrian Roos7da889d2016-03-16 18:38:58 -0700501}