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 */
16package android.support.v4.media;
17
18import static android.support.test.InstrumentationRegistry.getInstrumentation;
19
20import static junit.framework.Assert.assertEquals;
21import static junit.framework.Assert.assertFalse;
22import static junit.framework.Assert.assertNotNull;
23import static junit.framework.Assert.assertNull;
24import static junit.framework.Assert.assertTrue;
25
26import android.content.ComponentName;
27import android.os.Build;
28import android.os.Bundle;
29import android.support.test.filters.MediumTest;
30import android.support.test.filters.SmallTest;
31import android.support.test.runner.AndroidJUnit4;
32import android.support.testutils.PollingCheck;
33import android.support.v4.media.MediaBrowserCompat.MediaItem;
34
35import org.junit.Before;
36import org.junit.Test;
37import org.junit.runner.RunWith;
38
39import java.util.List;
40
41/**
42 * Test {@link android.support.v4.media.MediaBrowserServiceCompat}.
43 */
44@RunWith(AndroidJUnit4.class)
45public class MediaBrowserServiceCompatTest {
46    // The maximum time to wait for an operation.
47    private static final long TIME_OUT_MS = 3000L;
48    private static final long WAIT_TIME_FOR_NO_RESPONSE_MS = 500L;
49    private static final ComponentName TEST_BROWSER_SERVICE = new ComponentName(
50            "android.support.mediacompat.test",
51            "android.support.v4.media.StubMediaBrowserServiceCompat");
52    private static final ComponentName TEST_BROWSER_SERVICE_DELAYED_MEDIA_SESSION =
53            new ComponentName(
54                    "android.support.mediacompat.test",
55                    "android.support.v4.media"
56                            + ".StubMediaBrowserServiceCompatWithDelayedMediaSession");
57    private static final String TEST_KEY_1 = "key_1";
58    private static final String TEST_VALUE_1 = "value_1";
59    private static final String TEST_KEY_2 = "key_2";
60    private static final String TEST_VALUE_2 = "value_2";
61    private static final String TEST_KEY_3 = "key_3";
62    private static final String TEST_VALUE_3 = "value_3";
63    private static final String TEST_KEY_4 = "key_4";
64    private static final String TEST_VALUE_4 = "value_4";
65    private final Object mWaitLock = new Object();
66
67    private final ConnectionCallback mConnectionCallback = new ConnectionCallback();
68    private final SubscriptionCallback mSubscriptionCallback = new SubscriptionCallback();
69    private final ItemCallback mItemCallback = new ItemCallback();
70    private final SearchCallback mSearchCallback = new SearchCallback();
71
72    private MediaBrowserCompat mMediaBrowser;
73    private MediaBrowserCompat mMediaBrowserForDelayedMediaSession;
74    private StubMediaBrowserServiceCompat mMediaBrowserService;
75    private Bundle mRootHints;
76
77    @Before
78    public void setUp() throws Exception {
79        getInstrumentation().runOnMainSync(new Runnable() {
80            @Override
81            public void run() {
82                mRootHints = new Bundle();
83                mRootHints.putBoolean(MediaBrowserServiceCompat.BrowserRoot.EXTRA_RECENT, true);
84                mRootHints.putBoolean(MediaBrowserServiceCompat.BrowserRoot.EXTRA_OFFLINE, true);
85                mRootHints.putBoolean(MediaBrowserServiceCompat.BrowserRoot.EXTRA_SUGGESTED, true);
86                mMediaBrowser = new MediaBrowserCompat(getInstrumentation().getTargetContext(),
87                        TEST_BROWSER_SERVICE, mConnectionCallback, mRootHints);
88            }
89        });
90        synchronized (mWaitLock) {
91            mMediaBrowser.connect();
92            mWaitLock.wait(TIME_OUT_MS);
93        }
94        assertNotNull(mMediaBrowserService);
95        mMediaBrowserService.mCustomActionExtras = null;
96        mMediaBrowserService.mCustomActionResult = null;
97    }
98
99    @Test
100    @SmallTest
101    public void testGetSessionToken() {
102        assertEquals(StubMediaBrowserServiceCompat.sSession.getSessionToken(),
103                mMediaBrowserService.getSessionToken());
104    }
105
106    @Test
107    @SmallTest
108    public void testNotifyChildrenChanged() throws Exception {
109        synchronized (mWaitLock) {
110            mSubscriptionCallback.reset();
111            mMediaBrowser.subscribe(
112                    StubMediaBrowserServiceCompat.MEDIA_ID_ROOT, mSubscriptionCallback);
113            mWaitLock.wait(TIME_OUT_MS);
114            assertTrue(mSubscriptionCallback.mOnChildrenLoaded);
115
116            mSubscriptionCallback.reset();
117            mMediaBrowserService.notifyChildrenChanged(StubMediaBrowserServiceCompat.MEDIA_ID_ROOT);
118            mWaitLock.wait(TIME_OUT_MS);
119            assertTrue(mSubscriptionCallback.mOnChildrenLoaded);
120        }
121    }
122
123    @Test
124    @SmallTest
125    public void testNotifyChildrenChangedWithPagination() throws Exception {
126        synchronized (mWaitLock) {
127            final int pageSize = 5;
128            final int page = 2;
129            Bundle options = new Bundle();
130            options.putInt(MediaBrowserCompat.EXTRA_PAGE_SIZE, pageSize);
131            options.putInt(MediaBrowserCompat.EXTRA_PAGE, page);
132
133            mSubscriptionCallback.reset();
134            mMediaBrowser.subscribe(StubMediaBrowserServiceCompat.MEDIA_ID_ROOT, options,
135                    mSubscriptionCallback);
136            mWaitLock.wait(TIME_OUT_MS);
137            assertTrue(mSubscriptionCallback.mOnChildrenLoadedWithOptions);
138
139            mSubscriptionCallback.reset();
140            mMediaBrowserService.notifyChildrenChanged(StubMediaBrowserServiceCompat.MEDIA_ID_ROOT);
141            mWaitLock.wait(TIME_OUT_MS);
142            assertTrue(mSubscriptionCallback.mOnChildrenLoadedWithOptions);
143        }
144    }
145
146    @Test
147    @MediumTest
148    public void testDelayedNotifyChildrenChanged() throws Exception {
149        synchronized (mWaitLock) {
150            mSubscriptionCallback.reset();
151            mMediaBrowser.subscribe(StubMediaBrowserServiceCompat.MEDIA_ID_CHILDREN_DELAYED,
152                    mSubscriptionCallback);
153            mWaitLock.wait(WAIT_TIME_FOR_NO_RESPONSE_MS);
154            assertFalse(mSubscriptionCallback.mOnChildrenLoaded);
155
156            mMediaBrowserService.sendDelayedNotifyChildrenChanged();
157            mWaitLock.wait(TIME_OUT_MS);
158            assertTrue(mSubscriptionCallback.mOnChildrenLoaded);
159
160            mSubscriptionCallback.reset();
161            mMediaBrowserService.notifyChildrenChanged(
162                    StubMediaBrowserServiceCompat.MEDIA_ID_CHILDREN_DELAYED);
163            mWaitLock.wait(WAIT_TIME_FOR_NO_RESPONSE_MS);
164            assertFalse(mSubscriptionCallback.mOnChildrenLoaded);
165
166            mMediaBrowserService.sendDelayedNotifyChildrenChanged();
167            mWaitLock.wait(TIME_OUT_MS);
168            assertTrue(mSubscriptionCallback.mOnChildrenLoaded);
169        }
170    }
171
172    @Test
173    @MediumTest
174    public void testDelayedItem() throws Exception {
175        synchronized (mWaitLock) {
176            mItemCallback.reset();
177            mMediaBrowser.getItem(
178                    StubMediaBrowserServiceCompat.MEDIA_ID_CHILDREN_DELAYED, mItemCallback);
179            mWaitLock.wait(WAIT_TIME_FOR_NO_RESPONSE_MS);
180            assertFalse(mItemCallback.mOnItemLoaded);
181
182            mItemCallback.reset();
183            mMediaBrowserService.sendDelayedItemLoaded();
184            mWaitLock.wait(TIME_OUT_MS);
185            assertTrue(mItemCallback.mOnItemLoaded);
186        }
187    }
188
189    @Test
190    @SmallTest
191    public void testSearch() throws Exception {
192        final String key = "test-key";
193        final String val = "test-val";
194
195        synchronized (mWaitLock) {
196            mSearchCallback.reset();
197            mMediaBrowser.search(StubMediaBrowserServiceCompat.SEARCH_QUERY_FOR_NO_RESULT, null,
198                    mSearchCallback);
199            mWaitLock.wait(WAIT_TIME_FOR_NO_RESPONSE_MS);
200            assertTrue(mSearchCallback.mOnSearchResult);
201            assertTrue(mSearchCallback.mSearchResults != null
202                    && mSearchCallback.mSearchResults.size() == 0);
203            assertEquals(null, mSearchCallback.mSearchExtras);
204
205            mSearchCallback.reset();
206            mMediaBrowser.search(StubMediaBrowserServiceCompat.SEARCH_QUERY_FOR_ERROR, null,
207                    mSearchCallback);
208            mWaitLock.wait(WAIT_TIME_FOR_NO_RESPONSE_MS);
209            assertTrue(mSearchCallback.mOnSearchResult);
210            assertNull(mSearchCallback.mSearchResults);
211            assertEquals(null, mSearchCallback.mSearchExtras);
212
213            mSearchCallback.reset();
214            Bundle extras = new Bundle();
215            extras.putString(key, val);
216            mMediaBrowser.search(StubMediaBrowserServiceCompat.SEARCH_QUERY, extras,
217                    mSearchCallback);
218            mWaitLock.wait(WAIT_TIME_FOR_NO_RESPONSE_MS);
219            assertTrue(mSearchCallback.mOnSearchResult);
220            assertNotNull(mSearchCallback.mSearchResults);
221            for (MediaItem item : mSearchCallback.mSearchResults) {
222                assertNotNull(item.getMediaId());
223                assertTrue(item.getMediaId().contains(StubMediaBrowserServiceCompat.SEARCH_QUERY));
224            }
225            assertNotNull(mSearchCallback.mSearchExtras);
226            assertEquals(val, mSearchCallback.mSearchExtras.getString(key));
227        }
228    }
229
230    @Test
231    @SmallTest
232    public void testSendCustomAction() throws Exception {
233        synchronized (mWaitLock) {
234            CustomActionCallback callback = new CustomActionCallback();
235            Bundle extras = new Bundle();
236            extras.putString(TEST_KEY_1, TEST_VALUE_1);
237            mMediaBrowser.sendCustomAction(StubMediaBrowserServiceCompat.CUSTOM_ACTION, extras,
238                    callback);
239            new PollingCheck(TIME_OUT_MS) {
240                @Override
241                protected boolean check() {
242                    return mMediaBrowserService.mCustomActionResult != null;
243                }
244            }.run();
245            assertNotNull(mMediaBrowserService.mCustomActionResult);
246            assertNotNull(mMediaBrowserService.mCustomActionExtras);
247            assertEquals(TEST_VALUE_1,
248                    mMediaBrowserService.mCustomActionExtras.getString(TEST_KEY_1));
249
250            callback.reset();
251            Bundle bundle1 = new Bundle();
252            bundle1.putString(TEST_KEY_2, TEST_VALUE_2);
253            mMediaBrowserService.mCustomActionResult.sendProgressUpdate(bundle1);
254            mWaitLock.wait(WAIT_TIME_FOR_NO_RESPONSE_MS);
255            assertTrue(callback.mOnProgressUpdateCalled);
256            assertNotNull(callback.mExtras);
257            assertEquals(TEST_VALUE_1, callback.mExtras.getString(TEST_KEY_1));
258            assertNotNull(callback.mData);
259            assertEquals(TEST_VALUE_2, callback.mData.getString(TEST_KEY_2));
260
261            callback.reset();
262            Bundle bundle2 = new Bundle();
263            bundle2.putString(TEST_KEY_3, TEST_VALUE_3);
264            mMediaBrowserService.mCustomActionResult.sendProgressUpdate(bundle2);
265            mWaitLock.wait(WAIT_TIME_FOR_NO_RESPONSE_MS);
266            assertTrue(callback.mOnProgressUpdateCalled);
267            assertNotNull(callback.mExtras);
268            assertEquals(TEST_VALUE_1, callback.mExtras.getString(TEST_KEY_1));
269            assertNotNull(callback.mData);
270            assertEquals(TEST_VALUE_3, callback.mData.getString(TEST_KEY_3));
271
272            Bundle bundle3 = new Bundle();
273            bundle3.putString(TEST_KEY_4, TEST_VALUE_4);
274            callback.reset();
275            mMediaBrowserService.mCustomActionResult.sendResult(bundle3);
276            mWaitLock.wait(WAIT_TIME_FOR_NO_RESPONSE_MS);
277            assertTrue(callback.mOnResultCalled);
278            assertNotNull(callback.mExtras);
279            assertEquals(TEST_VALUE_1, callback.mExtras.getString(TEST_KEY_1));
280            assertNotNull(callback.mData);
281            assertEquals(TEST_VALUE_4, callback.mData.getString(TEST_KEY_4));
282        }
283    }
284
285    @Test
286    @SmallTest
287    public void testSendCustomActionWithDetachedError() throws Exception {
288        synchronized (mWaitLock) {
289            CustomActionCallback callback = new CustomActionCallback();
290            Bundle extras = new Bundle();
291            extras.putString(TEST_KEY_1, TEST_VALUE_1);
292            mMediaBrowser.sendCustomAction(StubMediaBrowserServiceCompat.CUSTOM_ACTION, extras,
293                    callback);
294            new PollingCheck(TIME_OUT_MS) {
295                @Override
296                protected boolean check() {
297                    return mMediaBrowserService.mCustomActionResult != null;
298                }
299            }.run();
300            assertNotNull(mMediaBrowserService.mCustomActionResult);
301            assertNotNull(mMediaBrowserService.mCustomActionExtras);
302            assertEquals(TEST_VALUE_1,
303                    mMediaBrowserService.mCustomActionExtras.getString(TEST_KEY_1));
304
305            callback.reset();
306            Bundle bundle1 = new Bundle();
307            bundle1.putString(TEST_KEY_2, TEST_VALUE_2);
308            mMediaBrowserService.mCustomActionResult.sendProgressUpdate(bundle1);
309            mWaitLock.wait(WAIT_TIME_FOR_NO_RESPONSE_MS);
310            assertTrue(callback.mOnProgressUpdateCalled);
311            assertNotNull(callback.mExtras);
312            assertEquals(TEST_VALUE_1, callback.mExtras.getString(TEST_KEY_1));
313            assertNotNull(callback.mData);
314            assertEquals(TEST_VALUE_2, callback.mData.getString(TEST_KEY_2));
315
316            callback.reset();
317            Bundle bundle2 = new Bundle();
318            bundle2.putString(TEST_KEY_3, TEST_VALUE_3);
319            mMediaBrowserService.mCustomActionResult.sendError(bundle2);
320            mWaitLock.wait(WAIT_TIME_FOR_NO_RESPONSE_MS);
321            assertTrue(callback.mOnErrorCalled);
322            assertNotNull(callback.mExtras);
323            assertEquals(TEST_VALUE_1, callback.mExtras.getString(TEST_KEY_1));
324            assertNotNull(callback.mData);
325            assertEquals(TEST_VALUE_3, callback.mData.getString(TEST_KEY_3));
326        }
327    }
328
329    @Test
330    @SmallTest
331    public void testSendCustomActionWithNullCallback() throws Exception {
332        Bundle extras = new Bundle();
333        extras.putString(TEST_KEY_1, TEST_VALUE_1);
334        mMediaBrowser.sendCustomAction(StubMediaBrowserServiceCompat.CUSTOM_ACTION, extras, null);
335        new PollingCheck(TIME_OUT_MS) {
336            @Override
337            protected boolean check() {
338                return mMediaBrowserService.mCustomActionResult != null;
339            }
340        }.run();
341        assertNotNull(mMediaBrowserService.mCustomActionResult);
342        assertNotNull(mMediaBrowserService.mCustomActionExtras);
343        assertEquals(TEST_VALUE_1, mMediaBrowserService.mCustomActionExtras.getString(TEST_KEY_1));
344    }
345
346    @Test
347    @SmallTest
348    public void testSendCustomActionWithError() throws Exception {
349        synchronized (mWaitLock) {
350            CustomActionCallback callback = new CustomActionCallback();
351            mMediaBrowser.sendCustomAction(StubMediaBrowserServiceCompat.CUSTOM_ACTION_FOR_ERROR,
352                    null, callback);
353            new PollingCheck(TIME_OUT_MS) {
354                @Override
355                protected boolean check() {
356                    return mMediaBrowserService.mCustomActionResult != null;
357                }
358            }.run();
359            assertNotNull(mMediaBrowserService.mCustomActionResult);
360            assertNull(mMediaBrowserService.mCustomActionExtras);
361            mWaitLock.wait(WAIT_TIME_FOR_NO_RESPONSE_MS);
362            assertTrue(callback.mOnErrorCalled);
363        }
364    }
365
366    @Test
367    @SmallTest
368    public void testBrowserRoot() {
369        final String id = "test-id";
370        final String key = "test-key";
371        final String val = "test-val";
372        final Bundle extras = new Bundle();
373        extras.putString(key, val);
374
375        MediaBrowserServiceCompat.BrowserRoot browserRoot =
376                new MediaBrowserServiceCompat.BrowserRoot(id, extras);
377        assertEquals(id, browserRoot.getRootId());
378        assertEquals(val, browserRoot.getExtras().getString(key));
379    }
380
381
382    @Test
383    @SmallTest
384    public void testDelayedSetSessionToken() throws Exception {
385        // This test has no meaning in API 21. The framework MediaBrowserService just connects to
386        // the media browser without waiting setMediaSession() to be called.
387        if (Build.VERSION.SDK_INT == 21) {
388            return;
389        }
390        final ConnectionCallbackForDelayedMediaSession callback =
391                new ConnectionCallbackForDelayedMediaSession();
392
393        getInstrumentation().runOnMainSync(new Runnable() {
394            @Override
395            public void run() {
396                mMediaBrowserForDelayedMediaSession =
397                        new MediaBrowserCompat(getInstrumentation().getTargetContext(),
398                                TEST_BROWSER_SERVICE_DELAYED_MEDIA_SESSION, callback, null);
399            }
400        });
401
402        synchronized (mWaitLock) {
403            mMediaBrowserForDelayedMediaSession.connect();
404            mWaitLock.wait(WAIT_TIME_FOR_NO_RESPONSE_MS);
405            assertEquals(0, callback.mConnectedCount);
406
407            StubMediaBrowserServiceCompatWithDelayedMediaSession.sInstance.callSetSessionToken();
408            mWaitLock.wait(TIME_OUT_MS);
409            assertEquals(1, callback.mConnectedCount);
410
411            if (Build.VERSION.SDK_INT >= 21) {
412                assertNotNull(
413                        mMediaBrowserForDelayedMediaSession.getSessionToken().getExtraBinder());
414            }
415        }
416    }
417
418    private void assertRootHints(MediaItem item) {
419        Bundle rootHints = item.getDescription().getExtras();
420        assertNotNull(rootHints);
421        assertEquals(mRootHints.getBoolean(MediaBrowserServiceCompat.BrowserRoot.EXTRA_RECENT),
422                rootHints.getBoolean(MediaBrowserServiceCompat.BrowserRoot.EXTRA_RECENT));
423        assertEquals(mRootHints.getBoolean(MediaBrowserServiceCompat.BrowserRoot.EXTRA_OFFLINE),
424                rootHints.getBoolean(MediaBrowserServiceCompat.BrowserRoot.EXTRA_OFFLINE));
425        assertEquals(mRootHints.getBoolean(MediaBrowserServiceCompat.BrowserRoot.EXTRA_SUGGESTED),
426                rootHints.getBoolean(MediaBrowserServiceCompat.BrowserRoot.EXTRA_SUGGESTED));
427    }
428
429    private class ConnectionCallback extends MediaBrowserCompat.ConnectionCallback {
430        @Override
431        public void onConnected() {
432            synchronized (mWaitLock) {
433                mMediaBrowserService = StubMediaBrowserServiceCompat.sInstance;
434                mWaitLock.notify();
435            }
436        }
437    }
438
439    private class SubscriptionCallback extends MediaBrowserCompat.SubscriptionCallback {
440        boolean mOnChildrenLoaded;
441        boolean mOnChildrenLoadedWithOptions;
442
443        @Override
444        public void onChildrenLoaded(String parentId, List<MediaItem> children) {
445            synchronized (mWaitLock) {
446                mOnChildrenLoaded = true;
447                if (children != null) {
448                    for (MediaItem item : children) {
449                        assertRootHints(item);
450                    }
451                }
452                mWaitLock.notify();
453            }
454        }
455
456        @Override
457        public void onChildrenLoaded(String parentId, List<MediaItem> children, Bundle options) {
458            synchronized (mWaitLock) {
459                mOnChildrenLoadedWithOptions = true;
460                if (children != null) {
461                    for (MediaItem item : children) {
462                        assertRootHints(item);
463                    }
464                }
465                mWaitLock.notify();
466            }
467        }
468
469        public void reset() {
470            mOnChildrenLoaded = false;
471            mOnChildrenLoadedWithOptions = false;
472        }
473    }
474
475    private class ItemCallback extends MediaBrowserCompat.ItemCallback {
476        boolean mOnItemLoaded;
477
478        @Override
479        public void onItemLoaded(MediaItem item) {
480            synchronized (mWaitLock) {
481                mOnItemLoaded = true;
482                assertRootHints(item);
483                mWaitLock.notify();
484            }
485        }
486
487        public void reset() {
488            mOnItemLoaded = false;
489        }
490    }
491
492    private class SearchCallback extends MediaBrowserCompat.SearchCallback {
493        boolean mOnSearchResult;
494        Bundle mSearchExtras;
495        List<MediaItem> mSearchResults;
496
497        @Override
498        public void onSearchResult(String query, Bundle extras, List<MediaItem> items) {
499            synchronized (mWaitLock) {
500                mOnSearchResult = true;
501                mSearchResults = items;
502                mSearchExtras = extras;
503                mWaitLock.notify();
504            }
505        }
506
507        @Override
508        public void onError(String query, Bundle extras) {
509            synchronized (mWaitLock) {
510                mOnSearchResult = true;
511                mSearchResults = null;
512                mSearchExtras = extras;
513                mWaitLock.notify();
514            }
515        }
516
517        public void reset() {
518            mOnSearchResult = false;
519            mSearchExtras = null;
520            mSearchResults = null;
521        }
522    }
523
524    private class CustomActionCallback extends MediaBrowserCompat.CustomActionCallback {
525        String mAction;
526        Bundle mExtras;
527        Bundle mData;
528        boolean mOnProgressUpdateCalled;
529        boolean mOnResultCalled;
530        boolean mOnErrorCalled;
531
532        @Override
533        public void onProgressUpdate(String action, Bundle extras, Bundle data) {
534            synchronized (mWaitLock) {
535                mOnProgressUpdateCalled = true;
536                mAction = action;
537                mExtras = extras;
538                mData = data;
539                mWaitLock.notify();
540            }
541        }
542
543        @Override
544        public void onResult(String action, Bundle extras, Bundle resultData) {
545            synchronized (mWaitLock) {
546                mOnResultCalled = true;
547                mAction = action;
548                mExtras = extras;
549                mData = resultData;
550                mWaitLock.notify();
551            }
552        }
553
554        @Override
555        public void onError(String action, Bundle extras, Bundle data) {
556            synchronized (mWaitLock) {
557                mOnErrorCalled = true;
558                mAction = action;
559                mExtras = extras;
560                mData = data;
561                mWaitLock.notify();
562            }
563        }
564
565        public void reset() {
566            mOnResultCalled = false;
567            mOnProgressUpdateCalled = false;
568            mOnErrorCalled = false;
569            mAction = null;
570            mExtras = null;
571            mData = null;
572        }
573    }
574
575    private class ConnectionCallbackForDelayedMediaSession extends
576            MediaBrowserCompat.ConnectionCallback {
577        private int mConnectedCount = 0;
578
579        @Override
580        public void onConnected() {
581            synchronized (mWaitLock) {
582                mConnectedCount++;
583                mWaitLock.notify();
584            }
585        }
586    };
587
588}
589