RemoteViewsTest.java revision fb92184ebc7da516b8cb59932e1be6421879fa0a
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