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