Home | History | Annotate | Download | only in widget
      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.assertEquals;
     20 import static org.junit.Assert.assertSame;
     21 import static org.junit.Assert.assertTrue;
     22 
     23 import android.app.PendingIntent;
     24 import android.content.Context;
     25 import android.content.Intent;
     26 import android.graphics.Bitmap;
     27 import android.graphics.drawable.BitmapDrawable;
     28 import android.graphics.drawable.Drawable;
     29 import android.os.AsyncTask;
     30 import android.os.Binder;
     31 import android.os.Parcel;
     32 import android.support.test.InstrumentationRegistry;
     33 import android.support.test.filters.SmallTest;
     34 import android.support.test.runner.AndroidJUnit4;
     35 import android.view.View;
     36 import android.view.ViewGroup;
     37 
     38 import com.android.frameworks.coretests.R;
     39 
     40 import org.junit.Before;
     41 import org.junit.Rule;
     42 import org.junit.Test;
     43 import org.junit.rules.ExpectedException;
     44 import org.junit.runner.RunWith;
     45 
     46 import java.util.ArrayList;
     47 import java.util.Arrays;
     48 import java.util.concurrent.CountDownLatch;
     49 
     50 /**
     51  * Tests for RemoteViews.
     52  */
     53 @RunWith(AndroidJUnit4.class)
     54 @SmallTest
     55 public 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