1 // Copyright 2019 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
4 
5 package org.chromium.weblayer.test;
6 
7 import android.content.Intent;
8 import android.net.Uri;
9 import android.os.Bundle;
10 
11 import androidx.annotation.NonNull;
12 import androidx.fragment.app.FragmentManager;
13 import androidx.test.filters.SmallTest;
14 
15 import org.junit.Assert;
16 import org.junit.Rule;
17 import org.junit.Test;
18 import org.junit.runner.RunWith;
19 
20 import org.chromium.base.test.util.CallbackHelper;
21 import org.chromium.content_public.browser.test.util.TestThreadUtils;
22 import org.chromium.weblayer.Browser;
23 import org.chromium.weblayer.BrowserRestoreCallback;
24 import org.chromium.weblayer.Navigation;
25 import org.chromium.weblayer.NavigationCallback;
26 import org.chromium.weblayer.NavigationController;
27 import org.chromium.weblayer.Profile;
28 import org.chromium.weblayer.Tab;
29 import org.chromium.weblayer.WebLayer;
30 import org.chromium.weblayer.shell.InstrumentationActivity;
31 
32 import java.util.ArrayList;
33 import java.util.HashMap;
34 import java.util.HashSet;
35 import java.util.List;
36 import java.util.Map;
37 import java.util.Set;
38 
39 /**
40  * Tests that fragment lifecycle works as expected.
41  */
42 @RunWith(WebLayerJUnit4ClassRunner.class)
43 public class BrowserFragmentLifecycleTest {
44     @Rule
45     public InstrumentationActivityTestRule mActivityTestRule =
46             new InstrumentationActivityTestRule();
47 
getTab()48     private Tab getTab() {
49         return TestThreadUtils.runOnUiThreadBlockingNoException(
50                 () -> mActivityTestRule.getActivity().getTab());
51     }
52 
isRestoringPreviousState()53     private boolean isRestoringPreviousState() {
54         return TestThreadUtils.runOnUiThreadBlockingNoException(
55                 () -> mActivityTestRule.getActivity().getBrowser().isRestoringPreviousState());
56     }
57 
getSupportedMajorVersion()58     private int getSupportedMajorVersion() {
59         return TestThreadUtils.runOnUiThreadBlockingNoException(
60                 () -> WebLayer.getSupportedMajorVersion(mActivityTestRule.getActivity()));
61     }
62 
63     @Test
64     @SmallTest
successfullyLoadsUrlAfterRecreation()65     public void successfullyLoadsUrlAfterRecreation() {
66         mActivityTestRule.launchShellWithUrl("about:blank");
67         String url = "data:text,foo";
68         mActivityTestRule.navigateAndWait(getTab(), url, false);
69 
70         mActivityTestRule.recreateActivity();
71 
72         url = "data:text,bar";
73         mActivityTestRule.navigateAndWait(getTab(), url, false);
74     }
75 
76     @Test
77     @SmallTest
restoreAfterRecreate()78     public void restoreAfterRecreate() throws Throwable {
79         mActivityTestRule.launchShellWithUrl("about:blank");
80         String url = "data:text,foo";
81         mActivityTestRule.navigateAndWait(getTab(), url, false);
82 
83         mActivityTestRule.recreateActivity();
84 
85         waitForTabToFinishRestore(getTab(), url);
86     }
87 
destroyFragment(CallbackHelper helper)88     private void destroyFragment(CallbackHelper helper) {
89         FragmentManager fm = mActivityTestRule.getActivity().getSupportFragmentManager();
90         fm.beginTransaction()
91                 .remove(fm.getFragments().get(0))
92                 .runOnCommit(helper::notifyCalled)
93                 .commit();
94     }
95 
96     // https://crbug.com/1021041
97     @Test
98     @SmallTest
handlesFragmentDestroyWhileNavigating()99     public void handlesFragmentDestroyWhileNavigating() throws Throwable {
100         InstrumentationActivity activity = mActivityTestRule.launchShellWithUrl("about:blank");
101         CallbackHelper helper = new CallbackHelper();
102         TestThreadUtils.runOnUiThreadBlocking(() -> {
103             NavigationController navigationController = activity.getTab().getNavigationController();
104             navigationController.registerNavigationCallback(new NavigationCallback() {
105                 @Override
106                 public void onReadyToCommitNavigation(@NonNull Navigation navigation) {
107                     destroyFragment(helper);
108                 }
109             });
110             navigationController.navigate(Uri.parse("data:text,foo"));
111         });
112         helper.waitForFirst();
113     }
114 
115     // Waits for |tab| to finish loadding |url. This is intended to be called after restore.
waitForTabToFinishRestore(Tab tab, String url)116     private void waitForTabToFinishRestore(Tab tab, String url) {
117         BoundedCountDownLatch latch = new BoundedCountDownLatch(1);
118         TestThreadUtils.runOnUiThreadBlocking(() -> {
119             // It's possible the NavigationController hasn't loaded yet, handle either scenario.
120             NavigationController navigationController = tab.getNavigationController();
121             for (int i = 0; i < navigationController.getNavigationListSize(); ++i) {
122                 if (navigationController.getNavigationEntryDisplayUri(i).equals(Uri.parse(url))) {
123                     latch.countDown();
124                     return;
125                 }
126             }
127             navigationController.registerNavigationCallback(new NavigationCallback() {
128                 @Override
129                 public void onNavigationCompleted(@NonNull Navigation navigation) {
130                     if (navigation.getUri().equals(Uri.parse(url))) {
131                         latch.countDown();
132                     }
133                 }
134             });
135         });
136         latch.timedAwait();
137     }
138 
139     // Recreates the activity and waits for the first tab to be restored. |extras| is the Bundle
140     // used to launch the shell.
restoresPreviousSession(Bundle extras)141     private void restoresPreviousSession(Bundle extras) {
142         extras.putString(InstrumentationActivity.EXTRA_PERSISTENCE_ID, "x");
143         final String url = mActivityTestRule.getTestDataURL("simple_page.html");
144         mActivityTestRule.launchShellWithUrl(url, extras);
145         if (getSupportedMajorVersion() >= 88) {
146             Assert.assertFalse(isRestoringPreviousState());
147         }
148 
149         mActivityTestRule.recreateActivity();
150 
151         Tab tab = getTab();
152         Assert.assertNotNull(tab);
153         waitForTabToFinishRestore(tab, url);
154         if (getSupportedMajorVersion() >= 88) {
155             Assert.assertFalse(isRestoringPreviousState());
156         }
157     }
158 
159     @Test
160     @SmallTest
restoresPreviousSession()161     public void restoresPreviousSession() throws Throwable {
162         restoresPreviousSession(new Bundle());
163     }
164 
165     @Test
166     @SmallTest
restoresPreviousSessionIncognito()167     public void restoresPreviousSessionIncognito() throws Throwable {
168         Bundle extras = new Bundle();
169         // This forces incognito.
170         extras.putString(InstrumentationActivity.EXTRA_PROFILE_NAME, null);
171         restoresPreviousSession(extras);
172     }
173 
174     @Test
175     @SmallTest
restoresTabGuid()176     public void restoresTabGuid() throws Throwable {
177         Bundle extras = new Bundle();
178         extras.putString(InstrumentationActivity.EXTRA_PERSISTENCE_ID, "x");
179         final String url = mActivityTestRule.getTestDataURL("simple_page.html");
180         mActivityTestRule.launchShellWithUrl(url, extras);
181         final String initialTabId = TestThreadUtils.runOnUiThreadBlocking(
182                 () -> { return mActivityTestRule.getActivity().getTab().getGuid(); });
183         Assert.assertNotNull(initialTabId);
184         Assert.assertFalse(initialTabId.isEmpty());
185 
186         mActivityTestRule.recreateActivity();
187 
188         Tab tab = getTab();
189         Assert.assertNotNull(tab);
190         waitForTabToFinishRestore(tab, url);
191         final String restoredTabId =
192                 TestThreadUtils.runOnUiThreadBlockingNoException(() -> { return tab.getGuid(); });
193         Assert.assertEquals(initialTabId, restoredTabId);
194     }
195 
196     @Test
197     @SmallTest
restoreTabGuidAfterRecreate()198     public void restoreTabGuidAfterRecreate() throws Throwable {
199         InstrumentationActivity activity = mActivityTestRule.launchShellWithUrl("about:blank");
200         final Tab tab = getTab();
201         final String initialTabId =
202                 TestThreadUtils.runOnUiThreadBlocking(() -> { return tab.getGuid(); });
203         String url = "data:text,foo";
204         mActivityTestRule.navigateAndWait(tab, url, false);
205 
206         mActivityTestRule.recreateActivity();
207 
208         final Tab restoredTab = getTab();
209         Assert.assertNotEquals(tab, restoredTab);
210         waitForTabToFinishRestore(restoredTab, url);
211         final String restoredTabId = TestThreadUtils.runOnUiThreadBlockingNoException(
212                 () -> { return restoredTab.getGuid(); });
213         Assert.assertEquals(initialTabId, restoredTabId);
214     }
215 
216     @Test
217     @SmallTest
218     @MinWebLayerVersion(85)
restoresTabData()219     public void restoresTabData() throws Throwable {
220         Bundle extras = new Bundle();
221         extras.putString(InstrumentationActivity.EXTRA_PERSISTENCE_ID, "x");
222 
223         Map<String, String> initialData = new HashMap<>();
224         initialData.put("foo", "bar");
225         restoreTabData(extras, initialData);
226     }
227 
228     @Test
229     @SmallTest
230     @MinWebLayerVersion(85)
restoreTabDataAfterRecreate()231     public void restoreTabDataAfterRecreate() throws Throwable {
232         Map<String, String> initialData = new HashMap<>();
233         initialData.put("foo", "bar");
234         restoreTabData(new Bundle(), initialData);
235     }
236 
restoreTabData(Bundle extras, Map<String, String> initialData)237     private void restoreTabData(Bundle extras, Map<String, String> initialData) {
238         String url = mActivityTestRule.getTestDataURL("simple_page.html");
239         mActivityTestRule.launchShellWithUrl(url, extras);
240 
241         TestThreadUtils.runOnUiThreadBlocking(() -> {
242             Tab tab = mActivityTestRule.getActivity().getTab();
243             Assert.assertTrue(tab.getData().isEmpty());
244             tab.setData(initialData);
245         });
246 
247         mActivityTestRule.recreateActivity();
248 
249         Tab tab = getTab();
250         Assert.assertNotNull(tab);
251         waitForTabToFinishRestore(tab, url);
252         TestThreadUtils.runOnUiThreadBlocking(
253                 () -> Assert.assertEquals(initialData, tab.getData()));
254     }
255 
256     @Test
257     @SmallTest
258     @MinWebLayerVersion(85)
getAndRemoveBrowserPersistenceIds()259     public void getAndRemoveBrowserPersistenceIds() throws Throwable {
260         // Creates a browser with the persistence id 'x'.
261         final String persistenceId = "x";
262         Bundle extras = new Bundle();
263         extras.putString(InstrumentationActivity.EXTRA_PERSISTENCE_ID, persistenceId);
264         final String url = mActivityTestRule.getTestDataURL("simple_page.html");
265         mActivityTestRule.launchShellWithUrl(url, extras);
266 
267         // Destroy the frament, which ensures the persistence file was written to.
268         CallbackHelper helper = new CallbackHelper();
269         Profile profile = TestThreadUtils.runOnUiThreadBlocking(
270                 () -> { return mActivityTestRule.getActivity().getBrowser().getProfile(); });
271         TestThreadUtils.runOnUiThreadBlocking(() -> destroyFragment(helper));
272         helper.waitForCallback(0, 1);
273         int callCount = helper.getCallCount();
274 
275         // Verify the id can be fetched.
276         TestThreadUtils.runOnUiThreadBlocking(() -> {
277             profile.getBrowserPersistenceIds((Set<String> ids) -> {
278                 Assert.assertEquals(1, ids.size());
279                 Assert.assertTrue(ids.contains(persistenceId));
280                 helper.notifyCalled();
281             });
282         });
283         helper.waitForCallback(callCount, 1);
284         callCount = helper.getCallCount();
285 
286         // Remove the storage.
287         HashSet<String> ids = new HashSet<String>();
288         ids.add("x");
289         TestThreadUtils.runOnUiThreadBlocking(() -> {
290             profile.removeBrowserPersistenceStorage(ids, (Boolean result) -> {
291                 Assert.assertTrue(result);
292                 helper.notifyCalled();
293             });
294         });
295         helper.waitForCallback(callCount, 1);
296         callCount = helper.getCallCount();
297 
298         // Verify it was actually removed.
299         TestThreadUtils.runOnUiThreadBlocking(() -> {
300             profile.getBrowserPersistenceIds((Set<String> actualIds) -> {
301                 Assert.assertTrue(actualIds.isEmpty());
302                 helper.notifyCalled();
303             });
304         });
305         helper.waitForCallback(callCount, 1);
306     }
307 
308     @Test
309     @SmallTest
browserAndTabIsDestroyedWhenFragmentDestroyed()310     public void browserAndTabIsDestroyedWhenFragmentDestroyed() throws Throwable {
311         mActivityTestRule.launchShellWithUrl(mActivityTestRule.getTestDataURL("simple_page.html"));
312 
313         CallbackHelper helper = new CallbackHelper();
314         Browser browser = TestThreadUtils.runOnUiThreadBlocking(
315                 () -> { return mActivityTestRule.getActivity().getBrowser(); });
316         Tab tab = TestThreadUtils.runOnUiThreadBlocking(() -> { return browser.getActiveTab(); });
317         TestThreadUtils.runOnUiThreadBlocking(() -> {
318             Assert.assertFalse(browser.isDestroyed());
319             Assert.assertFalse(tab.isDestroyed());
320             destroyFragment(helper);
321         });
322         helper.waitForFirst();
323         TestThreadUtils.runOnUiThreadBlocking(() -> {
324             Assert.assertTrue(browser.isDestroyed());
325             Assert.assertTrue(tab.isDestroyed());
326         });
327     }
328 
329     // Used to track new Browsers finish restoring.
330     private static final class BrowserRestoreHelper
331             implements InstrumentationActivity.OnCreatedCallback {
332         private final CallbackHelper mCallbackHelper;
333         public List<Browser> mBrowsers;
334 
335         // |helper| is notified once restore is complete (or if a browser is created already
336         // restored).
BrowserRestoreHelper(CallbackHelper helper)337         BrowserRestoreHelper(CallbackHelper helper) {
338             mCallbackHelper = helper;
339             mBrowsers = new ArrayList<Browser>();
340             InstrumentationActivity.registerOnCreatedCallback(this);
341         }
342 
343         @Override
onCreated(Browser browser)344         public void onCreated(Browser browser) {
345             mBrowsers.add(browser);
346             if (!browser.isRestoringPreviousState()) {
347                 mCallbackHelper.notifyCalled();
348                 return;
349             }
350             browser.registerBrowserRestoreCallback(new BrowserRestoreCallback() {
351                 @Override
352                 public void onRestoreCompleted() {
353                     Assert.assertFalse(browser.isRestoringPreviousState());
354                     browser.unregisterBrowserRestoreCallback(this);
355                     mCallbackHelper.notifyCalled();
356                 }
357             });
358         }
359     }
360     @Test
361     @SmallTest
362     @MinWebLayerVersion(88)
restoreUsingOnRestoreCompleted()363     public void restoreUsingOnRestoreCompleted() throws Throwable {
364         final String persistenceId = "x";
365         Bundle extras = new Bundle();
366         extras.putString(InstrumentationActivity.EXTRA_PERSISTENCE_ID, persistenceId);
367         CallbackHelper callbackHelper = new CallbackHelper();
368         BrowserRestoreHelper restoreHelper = new BrowserRestoreHelper(callbackHelper);
369 
370         final String url = mActivityTestRule.getTestDataURL("simple_page.html");
371         mActivityTestRule.launchShellWithUrl(url, extras);
372         // Wait for the restore to complete.
373         callbackHelper.waitForCallback(0, 1);
374 
375         // Recreate and wait for restore.
376         mActivityTestRule.recreateActivity();
377         callbackHelper.waitForCallback(1, 1);
378     }
379 
getCurrentDisplayUri(Browser browser)380     private String getCurrentDisplayUri(Browser browser) {
381         NavigationController navigationController =
382                 browser.getActiveTab().getNavigationController();
383         return navigationController
384                 .getNavigationEntryDisplayUri(navigationController.getNavigationListCurrentIndex())
385                 .toString();
386     }
387 
388     @Test
389     @SmallTest
390     @MinWebLayerVersion(87)
twoFragmentsDifferentIncognitoProfiles()391     public void twoFragmentsDifferentIncognitoProfiles() throws Throwable {
392         // This test creates two browsers with different profile names and persistence ids.
393         final String persistenceId1 = "x";
394         final String persistenceId2 = "y";
395         Bundle extras = new Bundle();
396         extras.putString(InstrumentationActivity.EXTRA_PERSISTENCE_ID, persistenceId1);
397         extras.putString(InstrumentationActivity.EXTRA_PROFILE_NAME, persistenceId1);
398         extras.putBoolean(InstrumentationActivity.EXTRA_IS_INCOGNITO, true);
399         CallbackHelper callbackHelper = new CallbackHelper();
400         BrowserRestoreHelper restoreHelper = new BrowserRestoreHelper(callbackHelper);
401         final String url1 = mActivityTestRule.getTestDataURL("simple_page.html");
402         mActivityTestRule.launchShellWithUrl(url1, extras);
403 
404         // Wait for the restore to complete.
405         int currentCallCount = 0;
406         callbackHelper.waitForCallback(currentCallCount++, 1);
407 
408         // Create another fragment
409         Browser newBrowser = TestThreadUtils.runOnUiThreadBlocking(() -> {
410             InstrumentationActivity activity = mActivityTestRule.getActivity();
411             Intent intent = new Intent(activity.getIntent());
412             intent.putExtra(InstrumentationActivity.EXTRA_PERSISTENCE_ID, persistenceId2);
413             intent.putExtra(InstrumentationActivity.EXTRA_PROFILE_NAME, persistenceId2);
414 
415             // The newly created browser should have a different Profile, but be incognito.
416             Browser browser = Browser.fromFragment(
417                     activity.createBrowserFragment(android.R.id.content, intent));
418             Assert.assertNotEquals(browser, activity.getBrowser());
419             Assert.assertNotEquals(browser.getProfile(), activity.getBrowser().getProfile());
420             Assert.assertTrue(activity.getBrowser().getProfile().isIncognito());
421             Assert.assertTrue(browser.getProfile().isIncognito());
422             return browser;
423         });
424 
425         // Wait for restore.
426         callbackHelper.waitForCallback(currentCallCount++, 1);
427 
428         // Navigate to url2.
429         final String url2 = mActivityTestRule.getTestDataURL("simple_page2.html");
430         Tab newTab = TestThreadUtils.runOnUiThreadBlocking(() -> newBrowser.getActiveTab());
431         mActivityTestRule.navigateAndWait(newTab, url2, true);
432 
433         Profile[] profiles = new Profile[2];
434         TestThreadUtils.runOnUiThreadBlocking(() -> {
435             profiles[0] = mActivityTestRule.getActivity().getBrowser().getProfile();
436             profiles[1] = newBrowser.getProfile();
437         });
438 
439         // Recreate the activity and wait for two restores (for the two fragments).
440         InstrumentationActivity.sAllowMultipleFragments = true;
441         restoreHelper.mBrowsers.clear();
442         mActivityTestRule.recreateActivity();
443         callbackHelper.waitForCallback(currentCallCount++, 1);
444         callbackHelper.waitForCallback(currentCallCount++, 1);
445 
446         TestThreadUtils.runOnUiThreadBlocking(() -> {
447             // Two new Browsers should be created, but have the profiles created earlier.
448             Assert.assertEquals(2, restoreHelper.mBrowsers.size());
449             Browser restoredBrowser1 = restoreHelper.mBrowsers.get(0);
450             Browser restoredBrowser2 = restoreHelper.mBrowsers.get(1);
451             if (restoredBrowser2.getProfile().getName().equals(persistenceId1)) {
452                 restoredBrowser1 = restoreHelper.mBrowsers.get(1);
453                 restoredBrowser2 = restoreHelper.mBrowsers.get(0);
454             }
455             Assert.assertEquals(restoredBrowser1.getProfile().getName(), persistenceId1);
456             Assert.assertTrue(restoredBrowser1.getProfile().isIncognito());
457             Assert.assertEquals(profiles[0], restoredBrowser1.getProfile());
458             Assert.assertEquals(url1, getCurrentDisplayUri(restoredBrowser1));
459 
460             Assert.assertEquals(restoredBrowser2.getProfile().getName(), persistenceId2);
461             Assert.assertTrue(restoredBrowser2.getProfile().isIncognito());
462             Assert.assertEquals(profiles[1], restoredBrowser2.getProfile());
463             Assert.assertEquals(url2, getCurrentDisplayUri(restoredBrowser2));
464             Assert.assertNotEquals(restoredBrowser2, newBrowser);
465         });
466     }
467 
468     @Test
469     @SmallTest
470     @MinWebLayerVersion(87)
twoFragmentsSameIncognitoProfile()471     public void twoFragmentsSameIncognitoProfile() throws Throwable {
472         // This test creates two browsers with the same profile, but different persistence ids.
473         final String persistenceId1 = "x";
474         final String persistenceId2 = "y";
475         Bundle extras = new Bundle();
476         extras.putString(InstrumentationActivity.EXTRA_PERSISTENCE_ID, persistenceId1);
477         extras.putString(InstrumentationActivity.EXTRA_PROFILE_NAME, persistenceId1);
478         extras.putBoolean(InstrumentationActivity.EXTRA_IS_INCOGNITO, true);
479         CallbackHelper callbackHelper = new CallbackHelper();
480         BrowserRestoreHelper restoreHelper = new BrowserRestoreHelper(callbackHelper);
481 
482         final String url1 = mActivityTestRule.getTestDataURL("simple_page.html");
483         mActivityTestRule.launchShellWithUrl(url1, extras);
484         // Wait for the restore to complete.
485         int currentCallCount = 0;
486         callbackHelper.waitForCallback(currentCallCount++, 1);
487 
488         // Create another fragment
489         Browser newBrowser = TestThreadUtils.runOnUiThreadBlocking(() -> {
490             InstrumentationActivity activity = mActivityTestRule.getActivity();
491             Intent intent = new Intent(activity.getIntent());
492             intent.putExtra(InstrumentationActivity.EXTRA_PERSISTENCE_ID, persistenceId2);
493 
494             // The newly created browser should have a different Profile, but be incognito.
495             Browser browser = Browser.fromFragment(
496                     activity.createBrowserFragment(android.R.id.content, intent));
497             Assert.assertNotEquals(browser, activity.getBrowser());
498             Assert.assertEquals(browser.getProfile(), activity.getBrowser().getProfile());
499             Assert.assertTrue(activity.getBrowser().getProfile().isIncognito());
500             return browser;
501         });
502         Profile profile = TestThreadUtils.runOnUiThreadBlocking(() -> newBrowser.getProfile());
503         // Wait for restore.
504         callbackHelper.waitForCallback(currentCallCount++, 1);
505 
506         // Navigate to url2.
507         final String url2 = mActivityTestRule.getTestDataURL("simple_page2.html");
508         Tab newTab = TestThreadUtils.runOnUiThreadBlocking(() -> newBrowser.getActiveTab());
509         mActivityTestRule.navigateAndWait(newTab, url2, true);
510 
511         // Recreate the activity and wait for two restores (for the two fragments).
512         InstrumentationActivity.sAllowMultipleFragments = true;
513         restoreHelper.mBrowsers.clear();
514         mActivityTestRule.recreateActivity();
515         callbackHelper.waitForCallback(currentCallCount++, 1);
516         callbackHelper.waitForCallback(currentCallCount++, 1);
517 
518         TestThreadUtils.runOnUiThreadBlocking(() -> {
519             // Two new Browsers should be created.
520             Assert.assertEquals(2, restoreHelper.mBrowsers.size());
521             Browser restoredBrowser1 = restoreHelper.mBrowsers.get(0);
522             Browser restoredBrowser2 = restoreHelper.mBrowsers.get(1);
523             Assert.assertEquals(profile, restoredBrowser1.getProfile());
524 
525             Assert.assertEquals(profile, restoredBrowser2.getProfile());
526             Assert.assertNotEquals(restoredBrowser2, newBrowser);
527 
528             if (getCurrentDisplayUri(restoredBrowser1).equals(url1)) {
529                 Assert.assertEquals(url2, getCurrentDisplayUri(restoredBrowser2));
530             } else {
531                 Assert.assertEquals(url1, getCurrentDisplayUri(restoredBrowser2));
532                 Assert.assertEquals(url2, getCurrentDisplayUri(restoredBrowser1));
533             }
534         });
535     }
536 }
537