1 /* 2 * Copyright (C) 2022 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.os; 18 19 import static android.os.BundleMerger.STRATEGY_ARRAY_APPEND; 20 import static android.os.BundleMerger.STRATEGY_ARRAY_LIST_APPEND; 21 import static android.os.BundleMerger.STRATEGY_ARRAY_UNION; 22 import static android.os.BundleMerger.STRATEGY_BOOLEAN_AND; 23 import static android.os.BundleMerger.STRATEGY_BOOLEAN_OR; 24 import static android.os.BundleMerger.STRATEGY_COMPARABLE_MAX; 25 import static android.os.BundleMerger.STRATEGY_COMPARABLE_MIN; 26 import static android.os.BundleMerger.STRATEGY_FIRST; 27 import static android.os.BundleMerger.STRATEGY_LAST; 28 import static android.os.BundleMerger.STRATEGY_NUMBER_ADD; 29 import static android.os.BundleMerger.STRATEGY_NUMBER_INCREMENT_FIRST; 30 import static android.os.BundleMerger.STRATEGY_NUMBER_INCREMENT_FIRST_AND_ADD; 31 import static android.os.BundleMerger.STRATEGY_REJECT; 32 import static android.os.BundleMerger.STRATEGY_STRING_APPEND; 33 import static android.os.BundleMerger.merge; 34 35 import static org.junit.Assert.assertArrayEquals; 36 import static org.junit.Assert.assertEquals; 37 import static org.junit.Assert.assertThrows; 38 39 import android.content.Intent; 40 import android.net.Uri; 41 42 import androidx.test.filters.SmallTest; 43 44 import org.junit.Test; 45 import org.junit.runner.RunWith; 46 import org.junit.runners.JUnit4; 47 48 import java.util.ArrayList; 49 import java.util.List; 50 import java.util.Set; 51 52 @SmallTest 53 @RunWith(JUnit4.class) 54 public class BundleMergerTest { 55 /** 56 * Strategies are only applied when there is an actual conflict; in the 57 * absence of conflict we pick whichever value is defined. 58 */ 59 @Test testNoConflict()60 public void testNoConflict() throws Exception { 61 for (int strategy = Byte.MIN_VALUE; strategy < Byte.MAX_VALUE; strategy++) { 62 assertEquals(null, merge(strategy, null, null)); 63 assertEquals(10, merge(strategy, 10, null)); 64 assertEquals(20, merge(strategy, null, 20)); 65 } 66 } 67 68 /** 69 * Strategies are only applied to identical data types; if there are mixed 70 * types we always reject the two conflicting values. 71 */ 72 @Test testMixedTypes()73 public void testMixedTypes() throws Exception { 74 for (int strategy = Byte.MIN_VALUE; strategy < Byte.MAX_VALUE; strategy++) { 75 final int finalStrategy = strategy; 76 assertThrows(Exception.class, () -> { 77 merge(finalStrategy, 10, "foo"); 78 }); 79 assertThrows(Exception.class, () -> { 80 merge(finalStrategy, List.of("foo"), "bar"); 81 }); 82 assertThrows(Exception.class, () -> { 83 merge(finalStrategy, new String[] { "foo" }, "bar"); 84 }); 85 assertThrows(Exception.class, () -> { 86 merge(finalStrategy, Integer.valueOf(10), Long.valueOf(10)); 87 }); 88 } 89 } 90 91 @Test testStrategyReject()92 public void testStrategyReject() throws Exception { 93 assertEquals(null, merge(STRATEGY_REJECT, 10, 20)); 94 95 // Identical values aren't technically a conflict, so they're passed 96 // through without being rejected 97 assertEquals(10, merge(STRATEGY_REJECT, 10, 10)); 98 assertArrayEquals(new int[] {10}, 99 (int[]) merge(STRATEGY_REJECT, new int[] {10}, new int[] {10})); 100 } 101 102 @Test testStrategyFirst()103 public void testStrategyFirst() throws Exception { 104 assertEquals(10, merge(STRATEGY_FIRST, 10, 20)); 105 } 106 107 @Test testStrategyLast()108 public void testStrategyLast() throws Exception { 109 assertEquals(20, merge(STRATEGY_LAST, 10, 20)); 110 } 111 112 @Test testStrategyComparableMin()113 public void testStrategyComparableMin() throws Exception { 114 assertEquals(10, merge(STRATEGY_COMPARABLE_MIN, 10, 20)); 115 assertEquals(10, merge(STRATEGY_COMPARABLE_MIN, 20, 10)); 116 assertEquals("a", merge(STRATEGY_COMPARABLE_MIN, "a", "z")); 117 assertEquals("a", merge(STRATEGY_COMPARABLE_MIN, "z", "a")); 118 119 assertThrows(Exception.class, () -> { 120 merge(STRATEGY_COMPARABLE_MIN, new Binder(), new Binder()); 121 }); 122 } 123 124 @Test testStrategyComparableMax()125 public void testStrategyComparableMax() throws Exception { 126 assertEquals(20, merge(STRATEGY_COMPARABLE_MAX, 10, 20)); 127 assertEquals(20, merge(STRATEGY_COMPARABLE_MAX, 20, 10)); 128 assertEquals("z", merge(STRATEGY_COMPARABLE_MAX, "a", "z")); 129 assertEquals("z", merge(STRATEGY_COMPARABLE_MAX, "z", "a")); 130 131 assertThrows(Exception.class, () -> { 132 merge(STRATEGY_COMPARABLE_MAX, new Binder(), new Binder()); 133 }); 134 } 135 136 @Test testStrategyNumberAdd()137 public void testStrategyNumberAdd() throws Exception { 138 assertEquals(30, merge(STRATEGY_NUMBER_ADD, 10, 20)); 139 assertEquals(30, merge(STRATEGY_NUMBER_ADD, 20, 10)); 140 assertEquals(30L, merge(STRATEGY_NUMBER_ADD, 10L, 20L)); 141 assertEquals(30L, merge(STRATEGY_NUMBER_ADD, 20L, 10L)); 142 143 assertThrows(Exception.class, () -> { 144 merge(STRATEGY_NUMBER_ADD, new Binder(), new Binder()); 145 }); 146 } 147 148 @Test testStrategyNumberIncrementFirst()149 public void testStrategyNumberIncrementFirst() throws Exception { 150 assertEquals(11, merge(STRATEGY_NUMBER_INCREMENT_FIRST, 10, 20)); 151 assertEquals(21, merge(STRATEGY_NUMBER_INCREMENT_FIRST, 20, 10)); 152 assertEquals(11L, merge(STRATEGY_NUMBER_INCREMENT_FIRST, 10L, 20L)); 153 assertEquals(21L, merge(STRATEGY_NUMBER_INCREMENT_FIRST, 20L, 10L)); 154 } 155 156 @Test testStrategyNumberIncrementFirstAndAdd()157 public void testStrategyNumberIncrementFirstAndAdd() throws Exception { 158 assertEquals(31, merge(STRATEGY_NUMBER_INCREMENT_FIRST_AND_ADD, 10, 20)); 159 assertEquals(31, merge(STRATEGY_NUMBER_INCREMENT_FIRST_AND_ADD, 20, 10)); 160 assertEquals(31L, merge(STRATEGY_NUMBER_INCREMENT_FIRST_AND_ADD, 10L, 20L)); 161 assertEquals(31L, merge(STRATEGY_NUMBER_INCREMENT_FIRST_AND_ADD, 20L, 10L)); 162 } 163 164 @Test testStrategyBooleanAnd()165 public void testStrategyBooleanAnd() throws Exception { 166 assertEquals(false, merge(STRATEGY_BOOLEAN_AND, false, false)); 167 assertEquals(false, merge(STRATEGY_BOOLEAN_AND, true, false)); 168 assertEquals(false, merge(STRATEGY_BOOLEAN_AND, false, true)); 169 assertEquals(true, merge(STRATEGY_BOOLEAN_AND, true, true)); 170 171 assertThrows(Exception.class, () -> { 172 merge(STRATEGY_BOOLEAN_AND, "True!", "False?"); 173 }); 174 } 175 176 @Test testStrategyBooleanOr()177 public void testStrategyBooleanOr() throws Exception { 178 assertEquals(false, merge(STRATEGY_BOOLEAN_OR, false, false)); 179 assertEquals(true, merge(STRATEGY_BOOLEAN_OR, true, false)); 180 assertEquals(true, merge(STRATEGY_BOOLEAN_OR, false, true)); 181 assertEquals(true, merge(STRATEGY_BOOLEAN_OR, true, true)); 182 183 assertThrows(Exception.class, () -> { 184 merge(STRATEGY_BOOLEAN_OR, "True!", "False?"); 185 }); 186 } 187 188 @Test testStrategyArrayAppend()189 public void testStrategyArrayAppend() throws Exception { 190 assertArrayEquals(new int[] {}, 191 (int[]) merge(STRATEGY_ARRAY_APPEND, new int[] {}, new int[] {})); 192 assertArrayEquals(new int[] {10}, 193 (int[]) merge(STRATEGY_ARRAY_APPEND, new int[] {10}, new int[] {})); 194 assertArrayEquals(new int[] {20}, 195 (int[]) merge(STRATEGY_ARRAY_APPEND, new int[] {}, new int[] {20})); 196 assertArrayEquals(new int[] {10, 20}, 197 (int[]) merge(STRATEGY_ARRAY_APPEND, new int[] {10}, new int[] {20})); 198 assertArrayEquals(new int[] {10, 30, 20, 40}, 199 (int[]) merge(STRATEGY_ARRAY_APPEND, new int[] {10, 30}, new int[] {20, 40})); 200 assertArrayEquals(new String[] {"a", "b"}, 201 (String[]) merge(STRATEGY_ARRAY_APPEND, new String[] {"a"}, new String[] {"b"})); 202 203 assertThrows(Exception.class, () -> { 204 merge(STRATEGY_ARRAY_APPEND, 10, 20); 205 }); 206 } 207 208 @Test testStrategyArrayUnion()209 public void testStrategyArrayUnion() throws Exception { 210 assertArrayEquals(new int[] {}, 211 (int[]) merge(STRATEGY_ARRAY_UNION, new int[] {}, new int[] {})); 212 assertArrayEquals(new int[] {10}, 213 (int[]) merge(STRATEGY_ARRAY_UNION, new int[] {10}, new int[] {})); 214 assertArrayEquals(new int[] {20}, 215 (int[]) merge(STRATEGY_ARRAY_UNION, new int[] {}, new int[] {20})); 216 assertArrayEquals(new int[] {10, 20}, 217 (int[]) merge(STRATEGY_ARRAY_UNION, new int[] {10}, new int[] {20})); 218 assertArrayEquals(new int[] {10, 20, 30, 40}, 219 (int[]) merge(STRATEGY_ARRAY_UNION, new int[] {10, 30}, new int[] {20, 40})); 220 assertArrayEquals(new int[] {10, 20, 30}, 221 (int[]) merge(STRATEGY_ARRAY_UNION, new int[] {10, 30}, new int[] {10, 20})); 222 assertArrayEquals(new int[] {10, 20}, 223 (int[]) merge(STRATEGY_ARRAY_UNION, new int[] {10, 20}, new int[] {20})); 224 assertArrayEquals(new String[] {"a", "b"}, 225 (String[]) merge(STRATEGY_ARRAY_UNION, new String[] {"a"}, new String[] {"b"})); 226 assertArrayEquals(new String[] {"a", "b", "c"}, 227 (String[]) merge(STRATEGY_ARRAY_UNION, new String[] {"a", "b"}, 228 new String[] {"b", "c"})); 229 230 assertThrows(Exception.class, () -> { 231 merge(STRATEGY_ARRAY_UNION, 10, 20); 232 }); 233 } 234 235 @Test testStrategyArrayListAppend()236 public void testStrategyArrayListAppend() throws Exception { 237 assertEquals(arrayListOf(), 238 merge(STRATEGY_ARRAY_LIST_APPEND, arrayListOf(), arrayListOf())); 239 assertEquals(arrayListOf(10), 240 merge(STRATEGY_ARRAY_LIST_APPEND, arrayListOf(10), arrayListOf())); 241 assertEquals(arrayListOf(20), 242 merge(STRATEGY_ARRAY_LIST_APPEND, arrayListOf(), arrayListOf(20))); 243 assertEquals(arrayListOf(10, 20), 244 merge(STRATEGY_ARRAY_LIST_APPEND, arrayListOf(10), arrayListOf(20))); 245 assertEquals(arrayListOf(10, 30, 20, 40), 246 merge(STRATEGY_ARRAY_LIST_APPEND, arrayListOf(10, 30), arrayListOf(20, 40))); 247 assertEquals(arrayListOf("a", "b"), 248 merge(STRATEGY_ARRAY_LIST_APPEND, arrayListOf("a"), arrayListOf("b"))); 249 250 assertThrows(Exception.class, () -> { 251 merge(STRATEGY_ARRAY_LIST_APPEND, 10, 20); 252 }); 253 } 254 255 @Test testStrategyStringAppend()256 public void testStrategyStringAppend() throws Exception { 257 assertEquals("ab", merge(STRATEGY_STRING_APPEND, "a", "b")); 258 assertEquals("abc", merge(STRATEGY_STRING_APPEND, "a", "bc")); 259 assertEquals("abc", merge(STRATEGY_STRING_APPEND, "ab", "c")); 260 assertEquals("a,b,c,", merge(STRATEGY_STRING_APPEND, "a,", "b,c,")); 261 262 assertThrows(Exception.class, () -> { 263 merge(STRATEGY_STRING_APPEND, 10, 20); 264 }); 265 } 266 267 @Test testSetDefaultMergeStrategy()268 public void testSetDefaultMergeStrategy() throws Exception { 269 final BundleMerger merger = new BundleMerger(); 270 merger.setDefaultMergeStrategy(STRATEGY_FIRST); 271 merger.setMergeStrategy(Intent.EXTRA_INDEX, STRATEGY_COMPARABLE_MAX); 272 273 Bundle a = new Bundle(); 274 a.putString(Intent.EXTRA_SUBJECT, "SubjectA"); 275 a.putInt(Intent.EXTRA_INDEX, 10); 276 277 Bundle b = new Bundle(); 278 b.putString(Intent.EXTRA_SUBJECT, "SubjectB"); 279 b.putInt(Intent.EXTRA_INDEX, 20); 280 281 Bundle ab = merger.merge(a, b); 282 assertEquals("SubjectA", ab.getString(Intent.EXTRA_SUBJECT)); 283 assertEquals(20, ab.getInt(Intent.EXTRA_INDEX)); 284 285 Bundle ba = merger.merge(b, a); 286 assertEquals("SubjectB", ba.getString(Intent.EXTRA_SUBJECT)); 287 assertEquals(20, ba.getInt(Intent.EXTRA_INDEX)); 288 } 289 290 @Test testMerge_Simple()291 public void testMerge_Simple() throws Exception { 292 final BundleMerger merger = new BundleMerger(); 293 final Bundle probe = new Bundle(); 294 probe.putInt(Intent.EXTRA_INDEX, 42); 295 296 assertEquals(null, merger.merge(null, null)); 297 assertEquals(probe.keySet(), merger.merge(probe, null).keySet()); 298 assertEquals(probe.keySet(), merger.merge(null, probe).keySet()); 299 assertEquals(probe.keySet(), merger.merge(probe, probe).keySet()); 300 } 301 302 /** 303 * Verify that we can merge parcelables present in the base classpath, since 304 * everyone on the device will be able to unpack them. 305 */ 306 @Test testMerge_Parcelable_BCP()307 public void testMerge_Parcelable_BCP() throws Exception { 308 final BundleMerger merger = new BundleMerger(); 309 merger.setMergeStrategy(Intent.EXTRA_STREAM, STRATEGY_COMPARABLE_MIN); 310 311 Bundle a = new Bundle(); 312 a.putParcelable(Intent.EXTRA_STREAM, Uri.parse("http://example.com")); 313 a = parcelAndUnparcel(a); 314 315 Bundle b = new Bundle(); 316 b.putParcelable(Intent.EXTRA_STREAM, Uri.parse("http://example.net")); 317 b = parcelAndUnparcel(b); 318 319 assertEquals(Uri.parse("http://example.com"), 320 merger.merge(a, b).getParcelable(Intent.EXTRA_STREAM, Uri.class)); 321 assertEquals(Uri.parse("http://example.com"), 322 merger.merge(b, a).getParcelable(Intent.EXTRA_STREAM, Uri.class)); 323 } 324 325 /** 326 * Verify that we tiptoe around custom parcelables while still merging other 327 * known data types. Custom parcelables aren't in the base classpath, so not 328 * everyone on the device will be able to unpack them. 329 */ 330 @Test testMerge_Parcelable_Custom()331 public void testMerge_Parcelable_Custom() throws Exception { 332 final BundleMerger merger = new BundleMerger(); 333 merger.setMergeStrategy(Intent.EXTRA_INDEX, STRATEGY_NUMBER_ADD); 334 335 Bundle a = new Bundle(); 336 a.putInt(Intent.EXTRA_INDEX, 10); 337 a.putString(Intent.EXTRA_CC, "foo@bar.com"); 338 a.putParcelable(Intent.EXTRA_SUBJECT, new ExplodingParcelable()); 339 a = parcelAndUnparcel(a); 340 341 Bundle b = new Bundle(); 342 b.putInt(Intent.EXTRA_INDEX, 20); 343 a.putString(Intent.EXTRA_BCC, "foo@baz.com"); 344 b.putParcelable(Intent.EXTRA_STREAM, new ExplodingParcelable()); 345 b = parcelAndUnparcel(b); 346 347 Bundle ab = merger.merge(a, b); 348 assertEquals(Set.of(Intent.EXTRA_INDEX, Intent.EXTRA_CC, Intent.EXTRA_BCC, 349 Intent.EXTRA_SUBJECT, Intent.EXTRA_STREAM), ab.keySet()); 350 assertEquals(30, ab.getInt(Intent.EXTRA_INDEX)); 351 assertEquals("foo@bar.com", ab.getString(Intent.EXTRA_CC)); 352 assertEquals("foo@baz.com", ab.getString(Intent.EXTRA_BCC)); 353 354 // And finally, make sure that if we try unpacking one of our custom 355 // values that we actually explode 356 assertThrows(BadParcelableException.class, () -> { 357 ab.getParcelable(Intent.EXTRA_SUBJECT, ExplodingParcelable.class); 358 }); 359 assertThrows(BadParcelableException.class, () -> { 360 ab.getParcelable(Intent.EXTRA_STREAM, ExplodingParcelable.class); 361 }); 362 } 363 364 @Test testMerge_PackageChanged()365 public void testMerge_PackageChanged() throws Exception { 366 final BundleMerger merger = new BundleMerger(); 367 merger.setMergeStrategy(Intent.EXTRA_CHANGED_COMPONENT_NAME_LIST, STRATEGY_ARRAY_APPEND); 368 369 final Bundle first = new Bundle(); 370 first.putInt(Intent.EXTRA_UID, 10001); 371 first.putStringArray(Intent.EXTRA_CHANGED_COMPONENT_NAME_LIST, new String[] { 372 "com.example.Foo", 373 }); 374 375 final Bundle second = new Bundle(); 376 second.putInt(Intent.EXTRA_UID, 10001); 377 second.putStringArray(Intent.EXTRA_CHANGED_COMPONENT_NAME_LIST, new String[] { 378 "com.example.Bar", 379 "com.example.Baz", 380 }); 381 382 final Bundle res = merger.merge(first, second); 383 assertEquals(10001, res.getInt(Intent.EXTRA_UID)); 384 assertArrayEquals(new String[] { 385 "com.example.Foo", "com.example.Bar", "com.example.Baz", 386 }, res.getStringArray(Intent.EXTRA_CHANGED_COMPONENT_NAME_LIST)); 387 } 388 389 /** 390 * Each event in isolation reports "zero events dropped", but if we need to 391 * merge them together, then we start incrementing. 392 */ 393 @Test testMerge_DropBox()394 public void testMerge_DropBox() throws Exception { 395 final BundleMerger merger = new BundleMerger(); 396 merger.setMergeStrategy(DropBoxManager.EXTRA_TIME, 397 STRATEGY_COMPARABLE_MAX); 398 merger.setMergeStrategy(DropBoxManager.EXTRA_DROPPED_COUNT, 399 STRATEGY_NUMBER_INCREMENT_FIRST_AND_ADD); 400 401 final long now = System.currentTimeMillis(); 402 final Bundle a = new Bundle(); 403 a.putString(DropBoxManager.EXTRA_TAG, "system_server_strictmode"); 404 a.putLong(DropBoxManager.EXTRA_TIME, now); 405 a.putInt(DropBoxManager.EXTRA_DROPPED_COUNT, 0); 406 407 final Bundle b = new Bundle(); 408 b.putString(DropBoxManager.EXTRA_TAG, "system_server_strictmode"); 409 b.putLong(DropBoxManager.EXTRA_TIME, now + 1000); 410 b.putInt(DropBoxManager.EXTRA_DROPPED_COUNT, 0); 411 412 final Bundle c = new Bundle(); 413 c.putString(DropBoxManager.EXTRA_TAG, "system_server_strictmode"); 414 c.putLong(DropBoxManager.EXTRA_TIME, now + 2000); 415 c.putInt(DropBoxManager.EXTRA_DROPPED_COUNT, 0); 416 417 final Bundle d = new Bundle(); 418 d.putString(DropBoxManager.EXTRA_TAG, "system_server_strictmode"); 419 d.putLong(DropBoxManager.EXTRA_TIME, now + 3000); 420 d.putInt(DropBoxManager.EXTRA_DROPPED_COUNT, 5); 421 422 final Bundle ab = merger.merge(a, b); 423 assertEquals("system_server_strictmode", ab.getString(DropBoxManager.EXTRA_TAG)); 424 assertEquals(now + 1000, ab.getLong(DropBoxManager.EXTRA_TIME)); 425 assertEquals(1, ab.getInt(DropBoxManager.EXTRA_DROPPED_COUNT)); 426 427 final Bundle abc = merger.merge(ab, c); 428 assertEquals("system_server_strictmode", abc.getString(DropBoxManager.EXTRA_TAG)); 429 assertEquals(now + 2000, abc.getLong(DropBoxManager.EXTRA_TIME)); 430 assertEquals(2, abc.getInt(DropBoxManager.EXTRA_DROPPED_COUNT)); 431 432 final Bundle abcd = merger.merge(abc, d); 433 assertEquals("system_server_strictmode", abcd.getString(DropBoxManager.EXTRA_TAG)); 434 assertEquals(now + 3000, abcd.getLong(DropBoxManager.EXTRA_TIME)); 435 assertEquals(8, abcd.getInt(DropBoxManager.EXTRA_DROPPED_COUNT)); 436 } 437 arrayListOf(Object... values)438 private static ArrayList<Object> arrayListOf(Object... values) { 439 final ArrayList<Object> res = new ArrayList<>(values.length); 440 for (Object value : values) { 441 res.add(value); 442 } 443 return res; 444 } 445 parcelAndUnparcel(Bundle input)446 private static Bundle parcelAndUnparcel(Bundle input) { 447 final Parcel parcel = Parcel.obtain(); 448 try { 449 input.writeToParcel(parcel, 0); 450 parcel.setDataPosition(0); 451 return Bundle.CREATOR.createFromParcel(parcel); 452 } finally { 453 parcel.recycle(); 454 } 455 } 456 457 /** 458 * Object that only offers to parcel itself; if something tries unparceling 459 * it, it will "explode" by throwing an exception. 460 * <p> 461 * Useful for verifying interactions that must leave unknown data in a 462 * parceled state. 463 */ 464 public static class ExplodingParcelable implements Parcelable { ExplodingParcelable()465 public ExplodingParcelable() { 466 } 467 468 @Override describeContents()469 public int describeContents() { 470 return 0; 471 } 472 473 @Override writeToParcel(Parcel out, int flags)474 public void writeToParcel(Parcel out, int flags) { 475 out.writeInt(42); 476 } 477 478 public static final Creator<ExplodingParcelable> CREATOR = 479 new Creator<ExplodingParcelable>() { 480 @Override 481 public ExplodingParcelable createFromParcel(Parcel in) { 482 throw new BadParcelableException("exploding!"); 483 } 484 485 @Override 486 public ExplodingParcelable[] newArray(int size) { 487 throw new BadParcelableException("exploding!"); 488 } 489 }; 490 } 491 } 492