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