AddressedMediaPlayer.java revision 4ffc87667492e40e448efa2ef5b11ae3de2f449b
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 com.android.bluetooth.avrcp;
18
19import android.annotation.NonNull;
20import android.annotation.Nullable;
21import android.bluetooth.BluetoothAvrcp;
22import android.media.session.MediaSession;
23import android.media.session.PlaybackState;
24import android.media.session.MediaSession.QueueItem;
25import android.media.MediaDescription;
26import android.media.MediaMetadata;
27import android.os.Bundle;
28import android.util.Log;
29
30import com.android.bluetooth.btservice.ProfileService;
31import com.android.bluetooth.Utils;
32
33import java.nio.ByteBuffer;
34import java.util.List;
35import java.util.Arrays;
36import java.util.ArrayList;
37
38/*************************************************************************************************
39 * Provides functionality required for Addressed Media Player, like Now Playing List related
40 * browsing commands, control commands to the current addressed player(playItem, play, pause, etc)
41 * Acts as an Interface to communicate with media controller APIs for NowPlayingItems.
42 ************************************************************************************************/
43
44public class AddressedMediaPlayer {
45    static private final String TAG = "AddressedMediaPlayer";
46    static private final Boolean DEBUG = false;
47
48    static private final long SINGLE_QID = 1;
49    static private final String UNKNOWN_TITLE = "(unknown)";
50
51    private AvrcpMediaRspInterface mMediaInterface;
52    private @NonNull List<MediaSession.QueueItem> mNowPlayingList;
53
54    private final List<MediaSession.QueueItem> mEmptyNowPlayingList;
55
56    private long mLastTrackIdSent;
57
58    public AddressedMediaPlayer(AvrcpMediaRspInterface mediaInterface) {
59        mEmptyNowPlayingList = new ArrayList<MediaSession.QueueItem>();
60        mNowPlayingList = mEmptyNowPlayingList;
61        mMediaInterface = mediaInterface;
62        mLastTrackIdSent = MediaSession.QueueItem.UNKNOWN_ID;
63    }
64
65    void cleanup() {
66        if (DEBUG) Log.v(TAG, "cleanup");
67        mNowPlayingList = mEmptyNowPlayingList;
68        mMediaInterface = null;
69        mLastTrackIdSent = MediaSession.QueueItem.UNKNOWN_ID;
70    }
71
72    /* get now playing list from addressed player */
73    void getFolderItemsNowPlaying(byte[] bdaddr, AvrcpCmd.FolderItemsCmd reqObj,
74            @Nullable MediaController mediaController) {
75        if (DEBUG) Log.v(TAG, "getFolderItemsNowPlaying");
76        if (mediaController == null) {
77            // No players (if a player exists, we would have selected it)
78            Log.e(TAG, "mediaController = null, sending no available players response");
79            mMediaInterface.folderItemsRsp(bdaddr, AvrcpConstants.RSP_NO_AVBL_PLAY, null);
80            return;
81        }
82        List<MediaSession.QueueItem> items = getNowPlayingList(mediaController);
83        getFolderItemsFilterAttr(bdaddr, reqObj, items, AvrcpConstants.BTRC_SCOPE_NOW_PLAYING,
84                reqObj.mStartItem, reqObj.mEndItem, mediaController);
85    }
86
87    /* get item attributes for item in now playing list */
88    void getItemAttr(byte[] bdaddr, AvrcpCmd.ItemAttrCmd itemAttr,
89            @Nullable MediaController mediaController) {
90        int status = AvrcpConstants.RSP_NO_ERROR;
91        long mediaId = ByteBuffer.wrap(itemAttr.mUid).getLong();
92        List<MediaSession.QueueItem> items = getNowPlayingList(mediaController);
93
94        // NOTE: this is out-of-spec (AVRCP 1.6.1 sec 6.10.4.3, p90) but we answer it anyway
95        // because some CTs ask for it.
96        if (Arrays.equals(itemAttr.mUid, AvrcpConstants.TRACK_IS_SELECTED)) {
97            if (DEBUG) Log.d(TAG, "getItemAttr: Remote requests for now playing contents:");
98
99            // get the current playing metadata and send.
100            getItemAttrFilterAttr(bdaddr, itemAttr, getCurrentQueueItem(mediaController, mediaId),
101                    mediaController);
102            return;
103        }
104
105        if (DEBUG) Log.d(TAG, "getItemAttr-UID: 0x" + Utils.byteArrayToString(itemAttr.mUid));
106        for (MediaSession.QueueItem item : items) {
107            if (item.getQueueId() == mediaId) {
108                getItemAttrFilterAttr(bdaddr, itemAttr, item, mediaController);
109                return;
110            }
111        }
112
113        // Couldn't find it, so the id is invalid
114        mMediaInterface.getItemAttrRsp(bdaddr, AvrcpConstants.RSP_INV_ITEM, null);
115    }
116
117    /* Refresh and get the queue of now playing.
118     */
119    private @NonNull List<MediaSession.QueueItem> getNowPlayingList(
120            @Nullable MediaController mediaController) {
121        if (mediaController == null) return mEmptyNowPlayingList;
122        List<MediaSession.QueueItem> items = mediaController.getQueue();
123        if (items == mNowPlayingList) return mNowPlayingList;
124        if (items == null) {
125            Log.i(TAG, "null queue from " + mediaController.getPackageName()
126                            + ", constructing single-item list");
127            MediaMetadata metadata = mediaController.getMetadata();
128            // Because we are database-unaware, we can just number the item here whatever we want
129            // because they have to re-poll it every time.
130            MediaSession.QueueItem current = getCurrentQueueItem(mediaController, SINGLE_QID);
131            items = new ArrayList<MediaSession.QueueItem>();
132            items.add(current);
133        }
134        mNowPlayingList = items;
135        // TODO (jamuraa): test to see if the single-item queue is the same and don't send
136        mMediaInterface.nowPlayingChangedRsp(AvrcpConstants.NOTIFICATION_TYPE_CHANGED);
137        return items;
138    }
139
140    /* Constructs a queue item representing the current playing metadata from an
141     * active controller with queue id |qid|.
142     */
143    private MediaSession.QueueItem getCurrentQueueItem(
144            @Nullable MediaController controller, long qid) {
145        if (controller == null) {
146            MediaDescription.Builder bob = new MediaDescription.Builder();
147            bob.setTitle(UNKNOWN_TITLE);
148            return new QueueItem(bob.build(), qid);
149        }
150
151        MediaMetadata metadata = controller.getMetadata();
152        if (metadata == null) {
153            Log.w(TAG, "Controller has no metadata!? Making an empty one");
154            metadata = (new MediaMetadata.Builder()).build();
155        }
156
157        MediaDescription.Builder bob = new MediaDescription.Builder();
158        MediaDescription desc = metadata.getDescription();
159
160        // set the simple ones that MediaMetadata builds for us
161        bob.setMediaId(desc.getMediaId());
162        bob.setTitle(desc.getTitle());
163        bob.setSubtitle(desc.getSubtitle());
164        bob.setDescription(desc.getDescription());
165        // fill the ones that we use later
166        bob.setExtras(fillBundle(metadata, desc.getExtras()));
167
168        // build queue item with the new metadata
169        desc = bob.build();
170        return new QueueItem(desc, qid);
171    }
172
173    private Bundle fillBundle(MediaMetadata metadata, Bundle currentExtras) {
174        if (metadata == null) {
175            return currentExtras;
176        }
177
178        Bundle bundle = currentExtras;
179        if (bundle == null) bundle = new Bundle();
180
181        String[] stringKeys = {MediaMetadata.METADATA_KEY_TITLE, MediaMetadata.METADATA_KEY_ARTIST,
182                MediaMetadata.METADATA_KEY_ALBUM, MediaMetadata.METADATA_KEY_GENRE};
183        for (String key : stringKeys) {
184            String current = bundle.getString(key);
185            if (current == null) bundle.putString(key, metadata.getString(key));
186        }
187
188        String[] longKeys = {MediaMetadata.METADATA_KEY_TRACK_NUMBER,
189                MediaMetadata.METADATA_KEY_NUM_TRACKS, MediaMetadata.METADATA_KEY_DURATION};
190        for (String key : longKeys) {
191            if (!bundle.containsKey(key)) bundle.putLong(key, metadata.getLong(key));
192        }
193        return bundle;
194    }
195
196    void updateNowPlayingList(@Nullable MediaController mediaController) {
197        getNowPlayingList(mediaController);
198    }
199
200    /* Instructs media player to play particular media item */
201    void playItem(byte[] bdaddr, byte[] uid, @Nullable MediaController mediaController) {
202        long qid = ByteBuffer.wrap(uid).getLong();
203        List<MediaSession.QueueItem> items = getNowPlayingList(mediaController);
204
205        if (mediaController == null) {
206            Log.e(TAG, "No mediaController when PlayItem " + qid + " requested");
207            mMediaInterface.playItemRsp(bdaddr, AvrcpConstants.RSP_INTERNAL_ERR);
208            return;
209        }
210
211        MediaController.TransportControls mediaControllerCntrl =
212                mediaController.getTransportControls();
213
214        if (items == null) {
215            Log.w(TAG, "nowPlayingItems is null");
216            mMediaInterface.playItemRsp(bdaddr, AvrcpConstants.RSP_INTERNAL_ERR);
217            return;
218        }
219
220        for (MediaSession.QueueItem item : items) {
221            if (qid == item.getQueueId()) {
222                if (DEBUG) Log.d(TAG, "Skipping to ID " + qid);
223                mediaControllerCntrl.skipToQueueItem(qid);
224                mMediaInterface.playItemRsp(bdaddr, AvrcpConstants.RSP_NO_ERROR);
225                return;
226            }
227        }
228
229        Log.w(TAG, "Invalid now playing Queue ID " + qid);
230        mMediaInterface.playItemRsp(bdaddr, AvrcpConstants.RSP_INV_ITEM);
231    }
232
233    void getTotalNumOfItems(byte[] bdaddr, @Nullable MediaController mediaController) {
234        List<MediaSession.QueueItem> items = getNowPlayingList(mediaController);
235        if (DEBUG) Log.d(TAG, "getTotalNumOfItems: " + items.size() + " items.");
236        mMediaInterface.getTotalNumOfItemsRsp(bdaddr, AvrcpConstants.RSP_NO_ERROR, 0, items.size());
237    }
238
239    boolean sendTrackChangeWithId(boolean requesting, @Nullable MediaController mediaController) {
240        if (DEBUG)
241            Log.d(TAG, "sendTrackChangeWithId (" + requesting + "): controller " + mediaController);
242        long qid = getActiveQueueItemId(mediaController);
243        byte[] track = ByteBuffer.allocate(AvrcpConstants.UID_SIZE).putLong(qid).array();
244        if (DEBUG) Log.d(TAG, "trackChangedRsp: 0x" + Utils.byteArrayToString(track));
245        int trackChangedNT = AvrcpConstants.NOTIFICATION_TYPE_CHANGED;
246        if (requesting) trackChangedNT = AvrcpConstants.NOTIFICATION_TYPE_INTERIM;
247        mMediaInterface.trackChangedRsp(trackChangedNT, track);
248        mLastTrackIdSent = qid;
249        // The nowPlaying might have changed.
250        updateNowPlayingList(mediaController);
251        return (trackChangedNT == AvrcpConstants.NOTIFICATION_TYPE_CHANGED);
252    }
253
254    /*
255     * helper method to check if startItem and endItem index is with range of
256     * MediaItem list. (Resultset containing all items in current path)
257     */
258    private @Nullable List<MediaSession.QueueItem> getQueueSubset(
259            @NonNull List<MediaSession.QueueItem> items, long startItem, long endItem) {
260        if (endItem > items.size()) endItem = items.size() - 1;
261        if (startItem > Integer.MAX_VALUE) startItem = Integer.MAX_VALUE;
262        try {
263            List<MediaSession.QueueItem> selected =
264                    items.subList((int) startItem, (int) Math.min(items.size(), endItem + 1));
265            if (selected.isEmpty()) {
266                Log.i(TAG, "itemsSubList is empty.");
267                return null;
268            }
269            return selected;
270        } catch (IndexOutOfBoundsException ex) {
271            Log.i(TAG, "Range (" + startItem + ", " + endItem + ") invalid");
272        } catch (IllegalArgumentException ex) {
273            Log.i(TAG, "Range start " + startItem + " > size (" + items.size() + ")");
274        }
275        return null;
276    }
277
278    /*
279     * helper method to filter required attibutes before sending GetFolderItems
280     * response
281     */
282    private void getFolderItemsFilterAttr(byte[] bdaddr, AvrcpCmd.FolderItemsCmd folderItemsReqObj,
283            @NonNull List<MediaSession.QueueItem> items, byte scope, long startItem, long endItem,
284            @NonNull MediaController mediaController) {
285        if (DEBUG) Log.d(TAG, "getFolderItemsFilterAttr: startItem =" + startItem + ", endItem = "
286                + endItem);
287
288        List<MediaSession.QueueItem> result_items = getQueueSubset(items, startItem, endItem);
289        /* check for index out of bound errors */
290        if (result_items == null) {
291            Log.w(TAG, "getFolderItemsFilterAttr: result_items is empty");
292            mMediaInterface.folderItemsRsp(bdaddr, AvrcpConstants.RSP_INV_RANGE, null);
293            return;
294        }
295
296        FolderItemsData folderDataNative = new FolderItemsData(result_items.size());
297
298        /* variables to accumulate attrs */
299        ArrayList<String> attrArray = new ArrayList<String>();
300        ArrayList<Integer> attrId = new ArrayList<Integer>();
301
302        for (int itemIndex = 0; itemIndex < result_items.size(); itemIndex++) {
303            MediaSession.QueueItem item = result_items.get(itemIndex);
304            // get the queue id
305            long qid = item.getQueueId();
306            byte[] uid = ByteBuffer.allocate(AvrcpConstants.UID_SIZE).putLong(qid).array();
307
308            // get the array of uid from 2d to array 1D array
309            for (int idx = 0; idx < AvrcpConstants.UID_SIZE; idx++) {
310                folderDataNative.mItemUid[itemIndex * AvrcpConstants.UID_SIZE + idx] = uid[idx];
311            }
312
313            /* Set display name for current item */
314            folderDataNative.mDisplayNames[itemIndex] =
315                    getAttrValue(AvrcpConstants.ATTRID_TITLE, item, mediaController);
316
317            int maxAttributesRequested = 0;
318            boolean isAllAttribRequested = false;
319            /* check if remote requested for attributes */
320            if (folderItemsReqObj.mNumAttr != AvrcpConstants.NUM_ATTR_NONE) {
321                int attrCnt = 0;
322
323                /* add requested attr ids to a temp array */
324                if (folderItemsReqObj.mNumAttr == AvrcpConstants.NUM_ATTR_ALL) {
325                    isAllAttribRequested = true;
326                    maxAttributesRequested = AvrcpConstants.MAX_NUM_ATTR;
327                } else {
328                    /* get only the requested attribute ids from the request */
329                    maxAttributesRequested = folderItemsReqObj.mNumAttr;
330                }
331
332                /* lookup and copy values of attributes for ids requested above */
333                for (int idx = 0; idx < maxAttributesRequested; idx++) {
334                    /* check if media player provided requested attributes */
335                    String value = null;
336
337                    int attribId =
338                            isAllAttribRequested ? (idx + 1) : folderItemsReqObj.mAttrIDs[idx];
339                    value = getAttrValue(attribId, item, mediaController);
340                    if (value != null) {
341                        attrArray.add(value);
342                        attrId.add(attribId);
343                        attrCnt++;
344                    }
345                }
346                /* add num attr actually received from media player for a particular item */
347                folderDataNative.mAttributesNum[itemIndex] = attrCnt;
348            }
349        }
350
351        /* copy filtered attr ids and attr values to response parameters */
352        if (folderItemsReqObj.mNumAttr != AvrcpConstants.NUM_ATTR_NONE) {
353            folderDataNative.mAttrIds = new int[attrId.size()];
354            for (int attrIndex = 0; attrIndex < attrId.size(); attrIndex++)
355                folderDataNative.mAttrIds[attrIndex] = attrId.get(attrIndex);
356            folderDataNative.mAttrValues = attrArray.toArray(new String[attrArray.size()]);
357        }
358        for (int attrIndex = 0; attrIndex < folderDataNative.mAttributesNum.length; attrIndex++)
359            if (DEBUG)
360                Log.d(TAG, "folderDataNative.mAttributesNum"
361                                + folderDataNative.mAttributesNum[attrIndex] + " attrIndex "
362                                + attrIndex);
363
364        /* create rsp object and send response to remote device */
365        FolderItemsRsp rspObj = new FolderItemsRsp(AvrcpConstants.RSP_NO_ERROR, Avrcp.sUIDCounter,
366                scope, folderDataNative.mNumItems, folderDataNative.mFolderTypes,
367                folderDataNative.mPlayable, folderDataNative.mItemTypes, folderDataNative.mItemUid,
368                folderDataNative.mDisplayNames, folderDataNative.mAttributesNum,
369                folderDataNative.mAttrIds, folderDataNative.mAttrValues);
370        mMediaInterface.folderItemsRsp(bdaddr, AvrcpConstants.RSP_NO_ERROR, rspObj);
371    }
372
373    private String getAttrValue(
374            int attr, MediaSession.QueueItem item, @Nullable MediaController mediaController) {
375        String attrValue = null;
376        if (item == null) {
377            if (DEBUG) Log.d(TAG, "getAttrValue received null item");
378            return null;
379        }
380        try {
381            MediaDescription desc = item.getDescription();
382            Bundle extras = desc.getExtras();
383            boolean isCurrentTrack = item.getQueueId() == getActiveQueueItemId(mediaController);
384            if (isCurrentTrack) {
385                if (DEBUG) Log.d(TAG, "getAttrValue: item is active, using current data");
386                extras = fillBundle(mediaController.getMetadata(), extras);
387            }
388            if (DEBUG) Log.d(TAG, "getAttrValue: item " + item + " : " + desc);
389            switch (attr) {
390                case AvrcpConstants.ATTRID_TITLE:
391                    /* Title is mandatory attribute */
392                    if (isCurrentTrack) {
393                        attrValue = extras.getString(MediaMetadata.METADATA_KEY_TITLE);
394                    } else {
395                        attrValue = desc.getTitle().toString();
396                    }
397                    break;
398
399                case AvrcpConstants.ATTRID_ARTIST:
400                    attrValue = extras.getString(MediaMetadata.METADATA_KEY_ARTIST);
401                    break;
402
403                case AvrcpConstants.ATTRID_ALBUM:
404                    attrValue = extras.getString(MediaMetadata.METADATA_KEY_ALBUM);
405                    break;
406
407                case AvrcpConstants.ATTRID_TRACK_NUM:
408                    attrValue =
409                            Long.toString(extras.getLong(MediaMetadata.METADATA_KEY_TRACK_NUMBER));
410                    break;
411
412                case AvrcpConstants.ATTRID_NUM_TRACKS:
413                    attrValue =
414                            Long.toString(extras.getLong(MediaMetadata.METADATA_KEY_NUM_TRACKS));
415                    break;
416
417                case AvrcpConstants.ATTRID_GENRE:
418                    attrValue = extras.getString(MediaMetadata.METADATA_KEY_GENRE);
419                    break;
420
421                case AvrcpConstants.ATTRID_PLAY_TIME:
422                    attrValue = Long.toString(extras.getLong(MediaMetadata.METADATA_KEY_DURATION));
423                    break;
424
425                case AvrcpConstants.ATTRID_COVER_ART:
426                    Log.e(TAG, "getAttrValue: Cover art attribute not supported");
427                    return null;
428
429                default:
430                    Log.e(TAG, "getAttrValue: Unknown attribute ID requested: " + attr);
431                    return null;
432            }
433        } catch (NullPointerException ex) {
434            Log.w(TAG, "getAttrValue: attr id not found in result");
435            /* checking if attribute is title, then it is mandatory and cannot send null */
436            if (attr == AvrcpConstants.ATTRID_TITLE) {
437                attrValue = "<Unknown Title>";
438            } else {
439                return null;
440            }
441        }
442        if (DEBUG) Log.d(TAG, "getAttrValue: attrvalue = " + attrValue + ", attr id:" + attr);
443        return attrValue;
444    }
445
446    private void getItemAttrFilterAttr(byte[] bdaddr, AvrcpCmd.ItemAttrCmd mItemAttrReqObj,
447            MediaSession.QueueItem mediaItem, @Nullable MediaController mediaController) {
448        /* Response parameters */
449        int[] attrIds = null; /* array of attr ids */
450        String[] attrValues = null; /* array of attr values */
451
452        /* variables to temperorily add attrs */
453        ArrayList<String> attrArray = new ArrayList<String>();
454        ArrayList<Integer> attrId = new ArrayList<Integer>();
455        ArrayList<Integer> attrTempId = new ArrayList<Integer>();
456
457        /* check if remote device has requested for attributes */
458        if (mItemAttrReqObj.mNumAttr != AvrcpConstants.NUM_ATTR_NONE) {
459            if (mItemAttrReqObj.mNumAttr == AvrcpConstants.NUM_ATTR_ALL) {
460                for (int idx = 1; idx < AvrcpConstants.MAX_NUM_ATTR; idx++) {
461                    attrTempId.add(idx); /* attr id 0x00 is unused */
462                }
463            } else {
464                /* get only the requested attribute ids from the request */
465                for (int idx = 0; idx < mItemAttrReqObj.mNumAttr; idx++) {
466                    if (DEBUG)
467                        Log.d(TAG, "getItemAttrFilterAttr: attr id[" + idx + "] :"
468                                        + mItemAttrReqObj.mAttrIDs[idx]);
469                    attrTempId.add(mItemAttrReqObj.mAttrIDs[idx]);
470                }
471            }
472        }
473
474        if (DEBUG) Log.d(TAG, "getItemAttrFilterAttr: attr id list size:" + attrTempId.size());
475        /* lookup and copy values of attributes for ids requested above */
476        for (int idx = 0; idx < attrTempId.size(); idx++) {
477            /* check if media player provided requested attributes */
478            String value = getAttrValue(attrTempId.get(idx), mediaItem, mediaController);
479            if (value != null) {
480                attrArray.add(value);
481                attrId.add(attrTempId.get(idx));
482            }
483        }
484
485        /* copy filtered attr ids and attr values to response parameters */
486        if (mItemAttrReqObj.mNumAttr != AvrcpConstants.NUM_ATTR_NONE) {
487            attrIds = new int[attrId.size()];
488
489            for (int attrIndex = 0; attrIndex < attrId.size(); attrIndex++)
490                attrIds[attrIndex] = attrId.get(attrIndex);
491
492            attrValues = attrArray.toArray(new String[attrId.size()]);
493
494            /* create rsp object and send response */
495            ItemAttrRsp rspObj = new ItemAttrRsp(AvrcpConstants.RSP_NO_ERROR, attrIds, attrValues);
496            mMediaInterface.getItemAttrRsp(bdaddr, AvrcpConstants.RSP_NO_ERROR, rspObj);
497            return;
498        }
499    }
500
501    private long getActiveQueueItemId(@Nullable MediaController controller) {
502        if (controller == null) return MediaSession.QueueItem.UNKNOWN_ID;
503        PlaybackState state = controller.getPlaybackState();
504        if (state == null) return MediaSession.QueueItem.UNKNOWN_ID;
505        long qid = state.getActiveQueueItemId();
506        if (qid != MediaSession.QueueItem.UNKNOWN_ID) return qid;
507        // Check if we're presenting a "one item queue"
508        if (controller.getMetadata() != null) return SINGLE_QID;
509        return MediaSession.QueueItem.UNKNOWN_ID;
510    }
511
512    public void dump(StringBuilder sb, @Nullable MediaController mediaController) {
513        ProfileService.println(sb, "AddressedPlayer info:");
514        ProfileService.println(sb, "mLastTrackIdSent: " + mLastTrackIdSent);
515        ProfileService.println(sb, "mNowPlayingList: " + mNowPlayingList.size() + " elements");
516        long currentQueueId = getActiveQueueItemId(mediaController);
517        for (MediaSession.QueueItem item : mNowPlayingList) {
518            long itemId = item.getQueueId();
519            ProfileService.println(sb, (itemId == currentQueueId ? "*" : " ") + item.toString());
520        }
521    }
522}
523