17e37ae05a9c7c9ecf8569a5bfe752a54f660a6baRakesh Iyer/*
27e37ae05a9c7c9ecf8569a5bfe752a54f660a6baRakesh Iyer * Copyright (c) 2016, The Android Open Source Project
37e37ae05a9c7c9ecf8569a5bfe752a54f660a6baRakesh Iyer *
47e37ae05a9c7c9ecf8569a5bfe752a54f660a6baRakesh Iyer * Licensed under the Apache License, Version 2.0 (the "License");
57e37ae05a9c7c9ecf8569a5bfe752a54f660a6baRakesh Iyer * you may not use this file except in compliance with the License.
67e37ae05a9c7c9ecf8569a5bfe752a54f660a6baRakesh Iyer * You may obtain a copy of the License at
77e37ae05a9c7c9ecf8569a5bfe752a54f660a6baRakesh Iyer *
87e37ae05a9c7c9ecf8569a5bfe752a54f660a6baRakesh Iyer *     http://www.apache.org/licenses/LICENSE-2.0
97e37ae05a9c7c9ecf8569a5bfe752a54f660a6baRakesh Iyer *
107e37ae05a9c7c9ecf8569a5bfe752a54f660a6baRakesh Iyer * Unless required by applicable law or agreed to in writing, software
117e37ae05a9c7c9ecf8569a5bfe752a54f660a6baRakesh Iyer * distributed under the License is distributed on an "AS IS" BASIS,
127e37ae05a9c7c9ecf8569a5bfe752a54f660a6baRakesh Iyer * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
137e37ae05a9c7c9ecf8569a5bfe752a54f660a6baRakesh Iyer * See the License for the specific language governing permissions and
147e37ae05a9c7c9ecf8569a5bfe752a54f660a6baRakesh Iyer * limitations under the License.
157e37ae05a9c7c9ecf8569a5bfe752a54f660a6baRakesh Iyer */
167e37ae05a9c7c9ecf8569a5bfe752a54f660a6baRakesh Iyerpackage com.android.car.media.localmediaplayer;
177e37ae05a9c7c9ecf8569a5bfe752a54f660a6baRakesh Iyer
18f4a24e8cfd616016b1d64c0a31f57d4268431ad4Rakesh Iyerimport android.app.Notification;
19f4a24e8cfd616016b1d64c0a31f57d4268431ad4Rakesh Iyerimport android.app.NotificationManager;
20f4a24e8cfd616016b1d64c0a31f57d4268431ad4Rakesh Iyerimport android.app.PendingIntent;
217e37ae05a9c7c9ecf8569a5bfe752a54f660a6baRakesh Iyerimport android.content.Context;
22f4a24e8cfd616016b1d64c0a31f57d4268431ad4Rakesh Iyerimport android.content.Intent;
23a48989c04282c7041b7f17b58cb2d0753075722eRakesh Iyerimport android.content.SharedPreferences;
247e37ae05a9c7c9ecf8569a5bfe752a54f660a6baRakesh Iyerimport android.media.AudioManager;
257e37ae05a9c7c9ecf8569a5bfe752a54f660a6baRakesh Iyerimport android.media.AudioManager.OnAudioFocusChangeListener;
267e37ae05a9c7c9ecf8569a5bfe752a54f660a6baRakesh Iyerimport android.media.MediaDescription;
277e37ae05a9c7c9ecf8569a5bfe752a54f660a6baRakesh Iyerimport android.media.MediaMetadata;
287e37ae05a9c7c9ecf8569a5bfe752a54f660a6baRakesh Iyerimport android.media.MediaPlayer;
297e37ae05a9c7c9ecf8569a5bfe752a54f660a6baRakesh Iyerimport android.media.MediaPlayer.OnCompletionListener;
307e37ae05a9c7c9ecf8569a5bfe752a54f660a6baRakesh Iyerimport android.media.session.MediaSession;
317e37ae05a9c7c9ecf8569a5bfe752a54f660a6baRakesh Iyerimport android.media.session.MediaSession.QueueItem;
327e37ae05a9c7c9ecf8569a5bfe752a54f660a6baRakesh Iyerimport android.media.session.PlaybackState;
33d47e834a2872587d83e727d8001815476713e297Rakesh Iyerimport android.media.session.PlaybackState.CustomAction;
347e37ae05a9c7c9ecf8569a5bfe752a54f660a6baRakesh Iyerimport android.os.Bundle;
357e37ae05a9c7c9ecf8569a5bfe752a54f660a6baRakesh Iyerimport android.util.Log;
367e37ae05a9c7c9ecf8569a5bfe752a54f660a6baRakesh Iyer
37f304f0a68ff116f402523d2f35f4382797e33d68Tamas Berghammerimport com.android.car.media.localmediaplayer.nano.Proto.Playlist;
38f304f0a68ff116f402523d2f35f4382797e33d68Tamas Berghammerimport com.android.car.media.localmediaplayer.nano.Proto.Song;
39a48989c04282c7041b7f17b58cb2d0753075722eRakesh Iyer
40a48989c04282c7041b7f17b58cb2d0753075722eRakesh Iyer// Proto should be available in AOSP.
41a48989c04282c7041b7f17b58cb2d0753075722eRakesh Iyerimport com.google.protobuf.nano.MessageNano;
42a48989c04282c7041b7f17b58cb2d0753075722eRakesh Iyerimport com.google.protobuf.nano.InvalidProtocolBufferNanoException;
43a48989c04282c7041b7f17b58cb2d0753075722eRakesh Iyer
447e37ae05a9c7c9ecf8569a5bfe752a54f660a6baRakesh Iyerimport java.io.IOException;
45a48989c04282c7041b7f17b58cb2d0753075722eRakesh Iyerimport java.io.File;
46a48989c04282c7041b7f17b58cb2d0753075722eRakesh Iyerimport java.util.ArrayList;
47a48989c04282c7041b7f17b58cb2d0753075722eRakesh Iyerimport java.util.Base64;
48d47e834a2872587d83e727d8001815476713e297Rakesh Iyerimport java.util.Collections;
497e37ae05a9c7c9ecf8569a5bfe752a54f660a6baRakesh Iyerimport java.util.List;
507e37ae05a9c7c9ecf8569a5bfe752a54f660a6baRakesh Iyer
517e37ae05a9c7c9ecf8569a5bfe752a54f660a6baRakesh Iyer/**
527e37ae05a9c7c9ecf8569a5bfe752a54f660a6baRakesh Iyer * TODO: Consider doing all content provider accesses and player operations asynchronously.
537e37ae05a9c7c9ecf8569a5bfe752a54f660a6baRakesh Iyer */
547e37ae05a9c7c9ecf8569a5bfe752a54f660a6baRakesh Iyerpublic class Player extends MediaSession.Callback {
557e37ae05a9c7c9ecf8569a5bfe752a54f660a6baRakesh Iyer    private static final String TAG = "LMPlayer";
56a48989c04282c7041b7f17b58cb2d0753075722eRakesh Iyer    private static final String SHARED_PREFS_NAME = "com.android.car.media.localmediaplayer.prefs";
57a48989c04282c7041b7f17b58cb2d0753075722eRakesh Iyer    private static final String CURRENT_PLAYLIST_KEY = "__CURRENT_PLAYLIST_KEY__";
58f4a24e8cfd616016b1d64c0a31f57d4268431ad4Rakesh Iyer    private static final int NOTIFICATION_ID = 42;
59f4a24e8cfd616016b1d64c0a31f57d4268431ad4Rakesh Iyer    private static final int REQUEST_CODE = 94043;
607e37ae05a9c7c9ecf8569a5bfe752a54f660a6baRakesh Iyer
617e37ae05a9c7c9ecf8569a5bfe752a54f660a6baRakesh Iyer    private static final float PLAYBACK_SPEED = 1.0f;
627e37ae05a9c7c9ecf8569a5bfe752a54f660a6baRakesh Iyer    private static final float PLAYBACK_SPEED_STOPPED = 1.0f;
637e37ae05a9c7c9ecf8569a5bfe752a54f660a6baRakesh Iyer    private static final long PLAYBACK_POSITION_STOPPED = 0;
647e37ae05a9c7c9ecf8569a5bfe752a54f660a6baRakesh Iyer
657e37ae05a9c7c9ecf8569a5bfe752a54f660a6baRakesh Iyer    // Note: Queues loop around so next/previous are always available.
667e37ae05a9c7c9ecf8569a5bfe752a54f660a6baRakesh Iyer    private static final long PLAYING_ACTIONS = PlaybackState.ACTION_PAUSE
677e37ae05a9c7c9ecf8569a5bfe752a54f660a6baRakesh Iyer            | PlaybackState.ACTION_PLAY_FROM_MEDIA_ID | PlaybackState.ACTION_SKIP_TO_NEXT
687e37ae05a9c7c9ecf8569a5bfe752a54f660a6baRakesh Iyer            | PlaybackState.ACTION_SKIP_TO_PREVIOUS | PlaybackState.ACTION_SKIP_TO_QUEUE_ITEM;
697e37ae05a9c7c9ecf8569a5bfe752a54f660a6baRakesh Iyer
707e37ae05a9c7c9ecf8569a5bfe752a54f660a6baRakesh Iyer    private static final long PAUSED_ACTIONS = PlaybackState.ACTION_PLAY
717e37ae05a9c7c9ecf8569a5bfe752a54f660a6baRakesh Iyer            | PlaybackState.ACTION_PLAY_FROM_MEDIA_ID | PlaybackState.ACTION_SKIP_TO_NEXT
727e37ae05a9c7c9ecf8569a5bfe752a54f660a6baRakesh Iyer            | PlaybackState.ACTION_SKIP_TO_PREVIOUS;
737e37ae05a9c7c9ecf8569a5bfe752a54f660a6baRakesh Iyer
747e37ae05a9c7c9ecf8569a5bfe752a54f660a6baRakesh Iyer    private static final long STOPPED_ACTIONS = PlaybackState.ACTION_PLAY
757e37ae05a9c7c9ecf8569a5bfe752a54f660a6baRakesh Iyer            | PlaybackState.ACTION_PLAY_FROM_MEDIA_ID | PlaybackState.ACTION_SKIP_TO_NEXT
767e37ae05a9c7c9ecf8569a5bfe752a54f660a6baRakesh Iyer            | PlaybackState.ACTION_SKIP_TO_PREVIOUS;
777e37ae05a9c7c9ecf8569a5bfe752a54f660a6baRakesh Iyer
78d47e834a2872587d83e727d8001815476713e297Rakesh Iyer    private static final String SHUFFLE = "android.car.media.localmediaplayer.shuffle";
79d47e834a2872587d83e727d8001815476713e297Rakesh Iyer
807e37ae05a9c7c9ecf8569a5bfe752a54f660a6baRakesh Iyer    private final Context mContext;
817e37ae05a9c7c9ecf8569a5bfe752a54f660a6baRakesh Iyer    private final MediaSession mSession;
827e37ae05a9c7c9ecf8569a5bfe752a54f660a6baRakesh Iyer    private final AudioManager mAudioManager;
837e37ae05a9c7c9ecf8569a5bfe752a54f660a6baRakesh Iyer    private final PlaybackState mErrorState;
847e37ae05a9c7c9ecf8569a5bfe752a54f660a6baRakesh Iyer    private final DataModel mDataModel;
85d47e834a2872587d83e727d8001815476713e297Rakesh Iyer    private final CustomAction mShuffle;
867e37ae05a9c7c9ecf8569a5bfe752a54f660a6baRakesh Iyer
877e37ae05a9c7c9ecf8569a5bfe752a54f660a6baRakesh Iyer    private List<QueueItem> mQueue;
887e37ae05a9c7c9ecf8569a5bfe752a54f660a6baRakesh Iyer    private int mCurrentQueueIdx = 0;
89a48989c04282c7041b7f17b58cb2d0753075722eRakesh Iyer    private final SharedPreferences mSharedPrefs;
907e37ae05a9c7c9ecf8569a5bfe752a54f660a6baRakesh Iyer
91f4a24e8cfd616016b1d64c0a31f57d4268431ad4Rakesh Iyer    private NotificationManager mNotificationManager;
92f4a24e8cfd616016b1d64c0a31f57d4268431ad4Rakesh Iyer    private Notification.Builder mPlayingNotificationBuilder;
93f4a24e8cfd616016b1d64c0a31f57d4268431ad4Rakesh Iyer    private Notification.Builder mPausedNotificationBuilder;
94f4a24e8cfd616016b1d64c0a31f57d4268431ad4Rakesh Iyer
957e37ae05a9c7c9ecf8569a5bfe752a54f660a6baRakesh Iyer    // TODO: Use multiple media players for gapless playback.
967e37ae05a9c7c9ecf8569a5bfe752a54f660a6baRakesh Iyer    private final MediaPlayer mMediaPlayer;
977e37ae05a9c7c9ecf8569a5bfe752a54f660a6baRakesh Iyer
987e37ae05a9c7c9ecf8569a5bfe752a54f660a6baRakesh Iyer    public Player(Context context, MediaSession session, DataModel dataModel) {
997e37ae05a9c7c9ecf8569a5bfe752a54f660a6baRakesh Iyer        mContext = context;
1007e37ae05a9c7c9ecf8569a5bfe752a54f660a6baRakesh Iyer        mDataModel = dataModel;
1017e37ae05a9c7c9ecf8569a5bfe752a54f660a6baRakesh Iyer        mAudioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
1027e37ae05a9c7c9ecf8569a5bfe752a54f660a6baRakesh Iyer        mSession = session;
103a48989c04282c7041b7f17b58cb2d0753075722eRakesh Iyer        mSharedPrefs = context.getSharedPreferences(SHARED_PREFS_NAME, Context.MODE_PRIVATE);
1047e37ae05a9c7c9ecf8569a5bfe752a54f660a6baRakesh Iyer
105d47e834a2872587d83e727d8001815476713e297Rakesh Iyer        mShuffle = new CustomAction.Builder(SHUFFLE, context.getString(R.string.shuffle),
106d47e834a2872587d83e727d8001815476713e297Rakesh Iyer                R.drawable.shuffle).build();
107d47e834a2872587d83e727d8001815476713e297Rakesh Iyer
1087e37ae05a9c7c9ecf8569a5bfe752a54f660a6baRakesh Iyer        mMediaPlayer = new MediaPlayer();
1097e37ae05a9c7c9ecf8569a5bfe752a54f660a6baRakesh Iyer        mMediaPlayer.reset();
1107e37ae05a9c7c9ecf8569a5bfe752a54f660a6baRakesh Iyer        mMediaPlayer.setOnCompletionListener(mOnCompletionListener);
1117e37ae05a9c7c9ecf8569a5bfe752a54f660a6baRakesh Iyer        mErrorState = new PlaybackState.Builder()
1127e37ae05a9c7c9ecf8569a5bfe752a54f660a6baRakesh Iyer                .setState(PlaybackState.STATE_ERROR, 0, 0)
1137e37ae05a9c7c9ecf8569a5bfe752a54f660a6baRakesh Iyer                .setErrorMessage(context.getString(R.string.playback_error))
1147e37ae05a9c7c9ecf8569a5bfe752a54f660a6baRakesh Iyer                .build();
115f4a24e8cfd616016b1d64c0a31f57d4268431ad4Rakesh Iyer
116f4a24e8cfd616016b1d64c0a31f57d4268431ad4Rakesh Iyer        mNotificationManager =
117f4a24e8cfd616016b1d64c0a31f57d4268431ad4Rakesh Iyer                (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
118f4a24e8cfd616016b1d64c0a31f57d4268431ad4Rakesh Iyer
119f4a24e8cfd616016b1d64c0a31f57d4268431ad4Rakesh Iyer        // There are 2 forms of the media notification, when playing it needs to show the controls
120f4a24e8cfd616016b1d64c0a31f57d4268431ad4Rakesh Iyer        // to pause & skip whereas when paused it needs to show controls to play & skip. Setup
121f4a24e8cfd616016b1d64c0a31f57d4268431ad4Rakesh Iyer        // pre-populated builders for both of these up front.
122f4a24e8cfd616016b1d64c0a31f57d4268431ad4Rakesh Iyer        Notification.Action prevAction = makeNotificationAction(
123f4a24e8cfd616016b1d64c0a31f57d4268431ad4Rakesh Iyer                LocalMediaBrowserService.ACTION_PREV, R.drawable.ic_prev, R.string.prev);
124f4a24e8cfd616016b1d64c0a31f57d4268431ad4Rakesh Iyer        Notification.Action nextAction = makeNotificationAction(
125f4a24e8cfd616016b1d64c0a31f57d4268431ad4Rakesh Iyer                LocalMediaBrowserService.ACTION_NEXT, R.drawable.ic_next, R.string.next);
126f4a24e8cfd616016b1d64c0a31f57d4268431ad4Rakesh Iyer        Notification.Action playAction = makeNotificationAction(
127f4a24e8cfd616016b1d64c0a31f57d4268431ad4Rakesh Iyer                LocalMediaBrowserService.ACTION_PLAY, R.drawable.ic_play, R.string.play);
128f4a24e8cfd616016b1d64c0a31f57d4268431ad4Rakesh Iyer        Notification.Action pauseAction = makeNotificationAction(
129f4a24e8cfd616016b1d64c0a31f57d4268431ad4Rakesh Iyer                LocalMediaBrowserService.ACTION_PAUSE, R.drawable.ic_pause, R.string.pause);
130f4a24e8cfd616016b1d64c0a31f57d4268431ad4Rakesh Iyer
131f4a24e8cfd616016b1d64c0a31f57d4268431ad4Rakesh Iyer        // While playing, you need prev, pause, next.
132f4a24e8cfd616016b1d64c0a31f57d4268431ad4Rakesh Iyer        mPlayingNotificationBuilder = new Notification.Builder(context)
133f4a24e8cfd616016b1d64c0a31f57d4268431ad4Rakesh Iyer                .setVisibility(Notification.VISIBILITY_PUBLIC)
134f4a24e8cfd616016b1d64c0a31f57d4268431ad4Rakesh Iyer                .setSmallIcon(R.drawable.ic_sd_storage_black)
135f4a24e8cfd616016b1d64c0a31f57d4268431ad4Rakesh Iyer                .addAction(prevAction)
136f4a24e8cfd616016b1d64c0a31f57d4268431ad4Rakesh Iyer                .addAction(pauseAction)
137f4a24e8cfd616016b1d64c0a31f57d4268431ad4Rakesh Iyer                .addAction(nextAction);
138f4a24e8cfd616016b1d64c0a31f57d4268431ad4Rakesh Iyer
139f4a24e8cfd616016b1d64c0a31f57d4268431ad4Rakesh Iyer        // While paused, you need prev, play, next.
140f4a24e8cfd616016b1d64c0a31f57d4268431ad4Rakesh Iyer        mPausedNotificationBuilder = new Notification.Builder(context)
141f4a24e8cfd616016b1d64c0a31f57d4268431ad4Rakesh Iyer                .setVisibility(Notification.VISIBILITY_PUBLIC)
142f4a24e8cfd616016b1d64c0a31f57d4268431ad4Rakesh Iyer                .setSmallIcon(R.drawable.ic_sd_storage_black)
143f4a24e8cfd616016b1d64c0a31f57d4268431ad4Rakesh Iyer                .addAction(prevAction)
144f4a24e8cfd616016b1d64c0a31f57d4268431ad4Rakesh Iyer                .addAction(playAction)
145f4a24e8cfd616016b1d64c0a31f57d4268431ad4Rakesh Iyer                .addAction(nextAction);
146f4a24e8cfd616016b1d64c0a31f57d4268431ad4Rakesh Iyer    }
147f4a24e8cfd616016b1d64c0a31f57d4268431ad4Rakesh Iyer
148f4a24e8cfd616016b1d64c0a31f57d4268431ad4Rakesh Iyer    private Notification.Action makeNotificationAction(String action, int iconId, int stringId) {
149f4a24e8cfd616016b1d64c0a31f57d4268431ad4Rakesh Iyer        PendingIntent intent = PendingIntent.getBroadcast(mContext, REQUEST_CODE,
150f4a24e8cfd616016b1d64c0a31f57d4268431ad4Rakesh Iyer                new Intent(action), PendingIntent.FLAG_UPDATE_CURRENT);
151f4a24e8cfd616016b1d64c0a31f57d4268431ad4Rakesh Iyer        Notification.Action notificationAction = new Notification.Action.Builder(iconId,
152f4a24e8cfd616016b1d64c0a31f57d4268431ad4Rakesh Iyer                mContext.getString(stringId), intent)
153f4a24e8cfd616016b1d64c0a31f57d4268431ad4Rakesh Iyer                .build();
154f4a24e8cfd616016b1d64c0a31f57d4268431ad4Rakesh Iyer        return notificationAction;
1557e37ae05a9c7c9ecf8569a5bfe752a54f660a6baRakesh Iyer    }
1567e37ae05a9c7c9ecf8569a5bfe752a54f660a6baRakesh Iyer
1574f47437963a9271cda42fea8d57d29f30823b5a7Rakesh Iyer    private boolean requestAudioFocus(Runnable onSuccess) {
1584f47437963a9271cda42fea8d57d29f30823b5a7Rakesh Iyer        int result = mAudioManager.requestAudioFocus(mAudioFocusListener, AudioManager.STREAM_MUSIC,
1594f47437963a9271cda42fea8d57d29f30823b5a7Rakesh Iyer                AudioManager.AUDIOFOCUS_GAIN);
1604f47437963a9271cda42fea8d57d29f30823b5a7Rakesh Iyer        if (result == AudioManager.AUDIOFOCUS_REQUEST_GRANTED) {
1614f47437963a9271cda42fea8d57d29f30823b5a7Rakesh Iyer            onSuccess.run();
1624f47437963a9271cda42fea8d57d29f30823b5a7Rakesh Iyer            return true;
1634f47437963a9271cda42fea8d57d29f30823b5a7Rakesh Iyer        }
1644f47437963a9271cda42fea8d57d29f30823b5a7Rakesh Iyer        Log.e(TAG, "Failed to acquire audio focus");
1654f47437963a9271cda42fea8d57d29f30823b5a7Rakesh Iyer        return false;
1664f47437963a9271cda42fea8d57d29f30823b5a7Rakesh Iyer    }
1674f47437963a9271cda42fea8d57d29f30823b5a7Rakesh Iyer
1687e37ae05a9c7c9ecf8569a5bfe752a54f660a6baRakesh Iyer    @Override
1697e37ae05a9c7c9ecf8569a5bfe752a54f660a6baRakesh Iyer    public void onPlay() {
1707e37ae05a9c7c9ecf8569a5bfe752a54f660a6baRakesh Iyer        super.onPlay();
1717e37ae05a9c7c9ecf8569a5bfe752a54f660a6baRakesh Iyer        if (Log.isLoggable(TAG, Log.DEBUG)) {
1727e37ae05a9c7c9ecf8569a5bfe752a54f660a6baRakesh Iyer            Log.d(TAG, "onPlay");
1737e37ae05a9c7c9ecf8569a5bfe752a54f660a6baRakesh Iyer        }
1744f47437963a9271cda42fea8d57d29f30823b5a7Rakesh Iyer        requestAudioFocus(() -> resumePlayback());
1757e37ae05a9c7c9ecf8569a5bfe752a54f660a6baRakesh Iyer    }
1767e37ae05a9c7c9ecf8569a5bfe752a54f660a6baRakesh Iyer
1777e37ae05a9c7c9ecf8569a5bfe752a54f660a6baRakesh Iyer    @Override
1787e37ae05a9c7c9ecf8569a5bfe752a54f660a6baRakesh Iyer    public void onPause() {
1797e37ae05a9c7c9ecf8569a5bfe752a54f660a6baRakesh Iyer        super.onPause();
1807e37ae05a9c7c9ecf8569a5bfe752a54f660a6baRakesh Iyer        if (Log.isLoggable(TAG, Log.DEBUG)) {
1817e37ae05a9c7c9ecf8569a5bfe752a54f660a6baRakesh Iyer            Log.d(TAG, "onPause");
1827e37ae05a9c7c9ecf8569a5bfe752a54f660a6baRakesh Iyer        }
1837e37ae05a9c7c9ecf8569a5bfe752a54f660a6baRakesh Iyer        pausePlayback();
1847e37ae05a9c7c9ecf8569a5bfe752a54f660a6baRakesh Iyer        mAudioManager.abandonAudioFocus(mAudioFocusListener);
1857e37ae05a9c7c9ecf8569a5bfe752a54f660a6baRakesh Iyer    }
1867e37ae05a9c7c9ecf8569a5bfe752a54f660a6baRakesh Iyer
1877e37ae05a9c7c9ecf8569a5bfe752a54f660a6baRakesh Iyer    public void destroy() {
1887e37ae05a9c7c9ecf8569a5bfe752a54f660a6baRakesh Iyer        stopPlayback();
189f4a24e8cfd616016b1d64c0a31f57d4268431ad4Rakesh Iyer        mNotificationManager.cancelAll();
1907e37ae05a9c7c9ecf8569a5bfe752a54f660a6baRakesh Iyer        mAudioManager.abandonAudioFocus(mAudioFocusListener);
1917e37ae05a9c7c9ecf8569a5bfe752a54f660a6baRakesh Iyer        mMediaPlayer.release();
1927e37ae05a9c7c9ecf8569a5bfe752a54f660a6baRakesh Iyer    }
1937e37ae05a9c7c9ecf8569a5bfe752a54f660a6baRakesh Iyer
194a48989c04282c7041b7f17b58cb2d0753075722eRakesh Iyer    public void saveState() {
195a48989c04282c7041b7f17b58cb2d0753075722eRakesh Iyer        if (mQueue == null || mQueue.isEmpty()) {
196a48989c04282c7041b7f17b58cb2d0753075722eRakesh Iyer            return;
197a48989c04282c7041b7f17b58cb2d0753075722eRakesh Iyer        }
198a48989c04282c7041b7f17b58cb2d0753075722eRakesh Iyer
199a48989c04282c7041b7f17b58cb2d0753075722eRakesh Iyer        Playlist playlist = new Playlist();
200a48989c04282c7041b7f17b58cb2d0753075722eRakesh Iyer        playlist.songs = new Song[mQueue.size()];
201a48989c04282c7041b7f17b58cb2d0753075722eRakesh Iyer
202a48989c04282c7041b7f17b58cb2d0753075722eRakesh Iyer        int idx = 0;
203a48989c04282c7041b7f17b58cb2d0753075722eRakesh Iyer        for (QueueItem item : mQueue) {
204a48989c04282c7041b7f17b58cb2d0753075722eRakesh Iyer            Song song = new Song();
205a48989c04282c7041b7f17b58cb2d0753075722eRakesh Iyer            song.queueId = item.getQueueId();
206a48989c04282c7041b7f17b58cb2d0753075722eRakesh Iyer            MediaDescription description = item.getDescription();
207a48989c04282c7041b7f17b58cb2d0753075722eRakesh Iyer            song.mediaId = description.getMediaId();
208a48989c04282c7041b7f17b58cb2d0753075722eRakesh Iyer            song.title = description.getTitle().toString();
209a48989c04282c7041b7f17b58cb2d0753075722eRakesh Iyer            song.subtitle = description.getSubtitle().toString();
210a48989c04282c7041b7f17b58cb2d0753075722eRakesh Iyer            song.path = description.getExtras().getString(DataModel.PATH_KEY);
211a48989c04282c7041b7f17b58cb2d0753075722eRakesh Iyer
212a48989c04282c7041b7f17b58cb2d0753075722eRakesh Iyer            playlist.songs[idx] = song;
213a48989c04282c7041b7f17b58cb2d0753075722eRakesh Iyer            idx++;
214a48989c04282c7041b7f17b58cb2d0753075722eRakesh Iyer        }
215a48989c04282c7041b7f17b58cb2d0753075722eRakesh Iyer        playlist.currentQueueId = mQueue.get(mCurrentQueueIdx).getQueueId();
216a48989c04282c7041b7f17b58cb2d0753075722eRakesh Iyer        playlist.name = CURRENT_PLAYLIST_KEY;
217a48989c04282c7041b7f17b58cb2d0753075722eRakesh Iyer
218a48989c04282c7041b7f17b58cb2d0753075722eRakesh Iyer        // Go to Base64 to ensure that we can actually store the string in a sharedpref. This is
219a48989c04282c7041b7f17b58cb2d0753075722eRakesh Iyer        // slightly wasteful because of the fact that base64 expands the size a bit but it's a
220a48989c04282c7041b7f17b58cb2d0753075722eRakesh Iyer        // lot less riskier than abusing the java string to directly store bytes coming out of
221a48989c04282c7041b7f17b58cb2d0753075722eRakesh Iyer        // proto encoding.
222a48989c04282c7041b7f17b58cb2d0753075722eRakesh Iyer        String serialized = Base64.getEncoder().encodeToString(MessageNano.toByteArray(playlist));
223a48989c04282c7041b7f17b58cb2d0753075722eRakesh Iyer        SharedPreferences.Editor editor = mSharedPrefs.edit();
224a48989c04282c7041b7f17b58cb2d0753075722eRakesh Iyer        editor.putString(CURRENT_PLAYLIST_KEY, serialized);
225a48989c04282c7041b7f17b58cb2d0753075722eRakesh Iyer        editor.commit();
226a48989c04282c7041b7f17b58cb2d0753075722eRakesh Iyer    }
227a48989c04282c7041b7f17b58cb2d0753075722eRakesh Iyer
228a48989c04282c7041b7f17b58cb2d0753075722eRakesh Iyer    private boolean maybeRebuildQueue(Playlist playlist) {
229a48989c04282c7041b7f17b58cb2d0753075722eRakesh Iyer        List<QueueItem> queue = new ArrayList<>();
230a48989c04282c7041b7f17b58cb2d0753075722eRakesh Iyer        int foundIdx = 0;
231a48989c04282c7041b7f17b58cb2d0753075722eRakesh Iyer        // You need to check if the playlist actually is still valid because the user could have
232a48989c04282c7041b7f17b58cb2d0753075722eRakesh Iyer        // deleted files or taken out the sd card between runs so we might as well check this ahead
233a48989c04282c7041b7f17b58cb2d0753075722eRakesh Iyer        // of time before we load up the playlist.
234a48989c04282c7041b7f17b58cb2d0753075722eRakesh Iyer        for (Song song : playlist.songs) {
235a48989c04282c7041b7f17b58cb2d0753075722eRakesh Iyer            File tmp = new File(song.path);
236a48989c04282c7041b7f17b58cb2d0753075722eRakesh Iyer            if (!tmp.exists()) {
237a48989c04282c7041b7f17b58cb2d0753075722eRakesh Iyer                continue;
238a48989c04282c7041b7f17b58cb2d0753075722eRakesh Iyer            }
239a48989c04282c7041b7f17b58cb2d0753075722eRakesh Iyer
240a48989c04282c7041b7f17b58cb2d0753075722eRakesh Iyer            if (playlist.currentQueueId == song.queueId) {
241a48989c04282c7041b7f17b58cb2d0753075722eRakesh Iyer                foundIdx = queue.size();
242a48989c04282c7041b7f17b58cb2d0753075722eRakesh Iyer            }
243a48989c04282c7041b7f17b58cb2d0753075722eRakesh Iyer
244a48989c04282c7041b7f17b58cb2d0753075722eRakesh Iyer            Bundle bundle = new Bundle();
245a48989c04282c7041b7f17b58cb2d0753075722eRakesh Iyer            bundle.putString(DataModel.PATH_KEY, song.path);
246a48989c04282c7041b7f17b58cb2d0753075722eRakesh Iyer            MediaDescription description = new MediaDescription.Builder()
247a48989c04282c7041b7f17b58cb2d0753075722eRakesh Iyer                    .setMediaId(song.mediaId)
248a48989c04282c7041b7f17b58cb2d0753075722eRakesh Iyer                    .setTitle(song.title)
249a48989c04282c7041b7f17b58cb2d0753075722eRakesh Iyer                    .setSubtitle(song.subtitle)
250a48989c04282c7041b7f17b58cb2d0753075722eRakesh Iyer                    .setExtras(bundle)
251a48989c04282c7041b7f17b58cb2d0753075722eRakesh Iyer                    .build();
252a48989c04282c7041b7f17b58cb2d0753075722eRakesh Iyer            queue.add(new QueueItem(description, song.queueId));
253a48989c04282c7041b7f17b58cb2d0753075722eRakesh Iyer        }
254a48989c04282c7041b7f17b58cb2d0753075722eRakesh Iyer
255a48989c04282c7041b7f17b58cb2d0753075722eRakesh Iyer        if (queue.isEmpty()) {
256a48989c04282c7041b7f17b58cb2d0753075722eRakesh Iyer            return false;
257a48989c04282c7041b7f17b58cb2d0753075722eRakesh Iyer        }
258a48989c04282c7041b7f17b58cb2d0753075722eRakesh Iyer
259a48989c04282c7041b7f17b58cb2d0753075722eRakesh Iyer        mQueue = queue;
260a48989c04282c7041b7f17b58cb2d0753075722eRakesh Iyer        mCurrentQueueIdx = foundIdx;  // Resumes from beginning if last playing song was not found.
261a48989c04282c7041b7f17b58cb2d0753075722eRakesh Iyer
262a48989c04282c7041b7f17b58cb2d0753075722eRakesh Iyer        return true;
263a48989c04282c7041b7f17b58cb2d0753075722eRakesh Iyer    }
264a48989c04282c7041b7f17b58cb2d0753075722eRakesh Iyer
265a48989c04282c7041b7f17b58cb2d0753075722eRakesh Iyer    public boolean maybeRestoreState() {
266a48989c04282c7041b7f17b58cb2d0753075722eRakesh Iyer        String serialized = mSharedPrefs.getString(CURRENT_PLAYLIST_KEY, null);
267a48989c04282c7041b7f17b58cb2d0753075722eRakesh Iyer        if (serialized == null) {
268a48989c04282c7041b7f17b58cb2d0753075722eRakesh Iyer            return false;
269a48989c04282c7041b7f17b58cb2d0753075722eRakesh Iyer        }
270a48989c04282c7041b7f17b58cb2d0753075722eRakesh Iyer
271a48989c04282c7041b7f17b58cb2d0753075722eRakesh Iyer        try {
272a48989c04282c7041b7f17b58cb2d0753075722eRakesh Iyer            Playlist playlist = Playlist.parseFrom(Base64.getDecoder().decode(serialized));
273a48989c04282c7041b7f17b58cb2d0753075722eRakesh Iyer            if (!maybeRebuildQueue(playlist)) {
274a48989c04282c7041b7f17b58cb2d0753075722eRakesh Iyer                return false;
275a48989c04282c7041b7f17b58cb2d0753075722eRakesh Iyer            }
276a48989c04282c7041b7f17b58cb2d0753075722eRakesh Iyer            updateSessionQueueState();
2774f47437963a9271cda42fea8d57d29f30823b5a7Rakesh Iyer
2784f47437963a9271cda42fea8d57d29f30823b5a7Rakesh Iyer            requestAudioFocus(() -> {
2794f47437963a9271cda42fea8d57d29f30823b5a7Rakesh Iyer                try {
2804f47437963a9271cda42fea8d57d29f30823b5a7Rakesh Iyer                    updatePlaybackStatePlaying();
2814f47437963a9271cda42fea8d57d29f30823b5a7Rakesh Iyer                    playCurrentQueueIndex();
2824f47437963a9271cda42fea8d57d29f30823b5a7Rakesh Iyer                } catch (IOException e) {
2834f47437963a9271cda42fea8d57d29f30823b5a7Rakesh Iyer                    Log.e(TAG, "Restored queue, but couldn't resume playback.");
2844f47437963a9271cda42fea8d57d29f30823b5a7Rakesh Iyer                }
2854f47437963a9271cda42fea8d57d29f30823b5a7Rakesh Iyer            });
286a48989c04282c7041b7f17b58cb2d0753075722eRakesh Iyer        } catch (IllegalArgumentException | InvalidProtocolBufferNanoException e) {
287a48989c04282c7041b7f17b58cb2d0753075722eRakesh Iyer            // Couldn't restore the playlist. Not the end of the world.
288a48989c04282c7041b7f17b58cb2d0753075722eRakesh Iyer            return false;
289a48989c04282c7041b7f17b58cb2d0753075722eRakesh Iyer        }
290a48989c04282c7041b7f17b58cb2d0753075722eRakesh Iyer
291a48989c04282c7041b7f17b58cb2d0753075722eRakesh Iyer        return true;
292a48989c04282c7041b7f17b58cb2d0753075722eRakesh Iyer    }
293a48989c04282c7041b7f17b58cb2d0753075722eRakesh Iyer
294a48989c04282c7041b7f17b58cb2d0753075722eRakesh Iyer    private void updateSessionQueueState() {
295a48989c04282c7041b7f17b58cb2d0753075722eRakesh Iyer        mSession.setQueueTitle(mContext.getString(R.string.playlist));
296a48989c04282c7041b7f17b58cb2d0753075722eRakesh Iyer        mSession.setQueue(mQueue);
297a48989c04282c7041b7f17b58cb2d0753075722eRakesh Iyer    }
298a48989c04282c7041b7f17b58cb2d0753075722eRakesh Iyer
2997e37ae05a9c7c9ecf8569a5bfe752a54f660a6baRakesh Iyer    private void startPlayback(String key) {
3007e37ae05a9c7c9ecf8569a5bfe752a54f660a6baRakesh Iyer        if (Log.isLoggable(TAG, Log.DEBUG)) {
3017e37ae05a9c7c9ecf8569a5bfe752a54f660a6baRakesh Iyer            Log.d(TAG, "startPlayback()");
3027e37ae05a9c7c9ecf8569a5bfe752a54f660a6baRakesh Iyer        }
3037e37ae05a9c7c9ecf8569a5bfe752a54f660a6baRakesh Iyer
3047e37ae05a9c7c9ecf8569a5bfe752a54f660a6baRakesh Iyer        List<QueueItem> queue = mDataModel.getQueue();
3057e37ae05a9c7c9ecf8569a5bfe752a54f660a6baRakesh Iyer        int idx = 0;
3067e37ae05a9c7c9ecf8569a5bfe752a54f660a6baRakesh Iyer        int foundIdx = -1;
3077e37ae05a9c7c9ecf8569a5bfe752a54f660a6baRakesh Iyer        for (QueueItem item : queue) {
3087e37ae05a9c7c9ecf8569a5bfe752a54f660a6baRakesh Iyer            if (item.getDescription().getMediaId().equals(key)) {
3097e37ae05a9c7c9ecf8569a5bfe752a54f660a6baRakesh Iyer                foundIdx = idx;
3107e37ae05a9c7c9ecf8569a5bfe752a54f660a6baRakesh Iyer                break;
3117e37ae05a9c7c9ecf8569a5bfe752a54f660a6baRakesh Iyer            }
3127e37ae05a9c7c9ecf8569a5bfe752a54f660a6baRakesh Iyer            idx++;
3137e37ae05a9c7c9ecf8569a5bfe752a54f660a6baRakesh Iyer        }
3147e37ae05a9c7c9ecf8569a5bfe752a54f660a6baRakesh Iyer
3157e37ae05a9c7c9ecf8569a5bfe752a54f660a6baRakesh Iyer        if (foundIdx == -1) {
3167e37ae05a9c7c9ecf8569a5bfe752a54f660a6baRakesh Iyer            mSession.setPlaybackState(mErrorState);
3177e37ae05a9c7c9ecf8569a5bfe752a54f660a6baRakesh Iyer            return;
3187e37ae05a9c7c9ecf8569a5bfe752a54f660a6baRakesh Iyer        }
3197e37ae05a9c7c9ecf8569a5bfe752a54f660a6baRakesh Iyer
3207e37ae05a9c7c9ecf8569a5bfe752a54f660a6baRakesh Iyer        mQueue = queue;
3217e37ae05a9c7c9ecf8569a5bfe752a54f660a6baRakesh Iyer        mCurrentQueueIdx = foundIdx;
3227e37ae05a9c7c9ecf8569a5bfe752a54f660a6baRakesh Iyer        QueueItem current = mQueue.get(mCurrentQueueIdx);
3237e37ae05a9c7c9ecf8569a5bfe752a54f660a6baRakesh Iyer        String path = current.getDescription().getExtras().getString(DataModel.PATH_KEY);
3247e37ae05a9c7c9ecf8569a5bfe752a54f660a6baRakesh Iyer        MediaMetadata metadata = mDataModel.getMetadata(current.getDescription().getMediaId());
325a48989c04282c7041b7f17b58cb2d0753075722eRakesh Iyer        updateSessionQueueState();
3267e37ae05a9c7c9ecf8569a5bfe752a54f660a6baRakesh Iyer
3277e37ae05a9c7c9ecf8569a5bfe752a54f660a6baRakesh Iyer        try {
3287e37ae05a9c7c9ecf8569a5bfe752a54f660a6baRakesh Iyer            play(path, metadata);
3297e37ae05a9c7c9ecf8569a5bfe752a54f660a6baRakesh Iyer        } catch (IOException e) {
3307e37ae05a9c7c9ecf8569a5bfe752a54f660a6baRakesh Iyer            Log.e(TAG, "Playback failed.", e);
3317e37ae05a9c7c9ecf8569a5bfe752a54f660a6baRakesh Iyer            mSession.setPlaybackState(mErrorState);
3327e37ae05a9c7c9ecf8569a5bfe752a54f660a6baRakesh Iyer        }
3337e37ae05a9c7c9ecf8569a5bfe752a54f660a6baRakesh Iyer    }
3347e37ae05a9c7c9ecf8569a5bfe752a54f660a6baRakesh Iyer
3357e37ae05a9c7c9ecf8569a5bfe752a54f660a6baRakesh Iyer    private void resumePlayback() {
3367e37ae05a9c7c9ecf8569a5bfe752a54f660a6baRakesh Iyer        if (Log.isLoggable(TAG, Log.DEBUG)) {
3377e37ae05a9c7c9ecf8569a5bfe752a54f660a6baRakesh Iyer            Log.d(TAG, "resumePlayback()");
3387e37ae05a9c7c9ecf8569a5bfe752a54f660a6baRakesh Iyer        }
3397e37ae05a9c7c9ecf8569a5bfe752a54f660a6baRakesh Iyer
3407e37ae05a9c7c9ecf8569a5bfe752a54f660a6baRakesh Iyer        updatePlaybackStatePlaying();
3417e37ae05a9c7c9ecf8569a5bfe752a54f660a6baRakesh Iyer
3427e37ae05a9c7c9ecf8569a5bfe752a54f660a6baRakesh Iyer        if (!mMediaPlayer.isPlaying()) {
3437e37ae05a9c7c9ecf8569a5bfe752a54f660a6baRakesh Iyer            mMediaPlayer.start();
3447e37ae05a9c7c9ecf8569a5bfe752a54f660a6baRakesh Iyer        }
3457e37ae05a9c7c9ecf8569a5bfe752a54f660a6baRakesh Iyer    }
3467e37ae05a9c7c9ecf8569a5bfe752a54f660a6baRakesh Iyer
347f4a24e8cfd616016b1d64c0a31f57d4268431ad4Rakesh Iyer    private void postMediaNotification(Notification.Builder builder) {
348f4a24e8cfd616016b1d64c0a31f57d4268431ad4Rakesh Iyer        if (mQueue == null) {
349f4a24e8cfd616016b1d64c0a31f57d4268431ad4Rakesh Iyer            return;
350f4a24e8cfd616016b1d64c0a31f57d4268431ad4Rakesh Iyer        }
351f4a24e8cfd616016b1d64c0a31f57d4268431ad4Rakesh Iyer
352f4a24e8cfd616016b1d64c0a31f57d4268431ad4Rakesh Iyer        MediaDescription current = mQueue.get(mCurrentQueueIdx).getDescription();
353f4a24e8cfd616016b1d64c0a31f57d4268431ad4Rakesh Iyer        Notification notification = builder
354f4a24e8cfd616016b1d64c0a31f57d4268431ad4Rakesh Iyer                .setStyle(new Notification.MediaStyle().setMediaSession(mSession.getSessionToken()))
355f4a24e8cfd616016b1d64c0a31f57d4268431ad4Rakesh Iyer                .setContentTitle(current.getTitle())
356f4a24e8cfd616016b1d64c0a31f57d4268431ad4Rakesh Iyer                .setContentText(current.getSubtitle())
357f4a24e8cfd616016b1d64c0a31f57d4268431ad4Rakesh Iyer                .build();
358f4a24e8cfd616016b1d64c0a31f57d4268431ad4Rakesh Iyer        notification.flags |= Notification.FLAG_NO_CLEAR;
359f4a24e8cfd616016b1d64c0a31f57d4268431ad4Rakesh Iyer        mNotificationManager.notify(NOTIFICATION_ID, notification);
360f4a24e8cfd616016b1d64c0a31f57d4268431ad4Rakesh Iyer    }
361f4a24e8cfd616016b1d64c0a31f57d4268431ad4Rakesh Iyer
3627e37ae05a9c7c9ecf8569a5bfe752a54f660a6baRakesh Iyer    private void updatePlaybackStatePlaying() {
3637e37ae05a9c7c9ecf8569a5bfe752a54f660a6baRakesh Iyer        if (!mSession.isActive()) {
3647e37ae05a9c7c9ecf8569a5bfe752a54f660a6baRakesh Iyer            mSession.setActive(true);
3657e37ae05a9c7c9ecf8569a5bfe752a54f660a6baRakesh Iyer        }
3667e37ae05a9c7c9ecf8569a5bfe752a54f660a6baRakesh Iyer
367f4a24e8cfd616016b1d64c0a31f57d4268431ad4Rakesh Iyer        // Update the state in the media session.
3687e37ae05a9c7c9ecf8569a5bfe752a54f660a6baRakesh Iyer        PlaybackState state = new PlaybackState.Builder()
3697e37ae05a9c7c9ecf8569a5bfe752a54f660a6baRakesh Iyer                .setState(PlaybackState.STATE_PLAYING,
3707e37ae05a9c7c9ecf8569a5bfe752a54f660a6baRakesh Iyer                        mMediaPlayer.getCurrentPosition(), PLAYBACK_SPEED)
3717e37ae05a9c7c9ecf8569a5bfe752a54f660a6baRakesh Iyer                .setActions(PLAYING_ACTIONS)
372d47e834a2872587d83e727d8001815476713e297Rakesh Iyer                .addCustomAction(mShuffle)
373d47e834a2872587d83e727d8001815476713e297Rakesh Iyer                .setActiveQueueItemId(mQueue.get(mCurrentQueueIdx).getQueueId())
3747e37ae05a9c7c9ecf8569a5bfe752a54f660a6baRakesh Iyer                .build();
3757e37ae05a9c7c9ecf8569a5bfe752a54f660a6baRakesh Iyer        mSession.setPlaybackState(state);
376f4a24e8cfd616016b1d64c0a31f57d4268431ad4Rakesh Iyer
377f4a24e8cfd616016b1d64c0a31f57d4268431ad4Rakesh Iyer        // Update the media styled notification.
378f4a24e8cfd616016b1d64c0a31f57d4268431ad4Rakesh Iyer        postMediaNotification(mPlayingNotificationBuilder);
3797e37ae05a9c7c9ecf8569a5bfe752a54f660a6baRakesh Iyer    }
3807e37ae05a9c7c9ecf8569a5bfe752a54f660a6baRakesh Iyer
3817e37ae05a9c7c9ecf8569a5bfe752a54f660a6baRakesh Iyer    private void pausePlayback() {
3827e37ae05a9c7c9ecf8569a5bfe752a54f660a6baRakesh Iyer        if (Log.isLoggable(TAG, Log.DEBUG)) {
3837e37ae05a9c7c9ecf8569a5bfe752a54f660a6baRakesh Iyer            Log.d(TAG, "pausePlayback()");
3847e37ae05a9c7c9ecf8569a5bfe752a54f660a6baRakesh Iyer        }
3857e37ae05a9c7c9ecf8569a5bfe752a54f660a6baRakesh Iyer
3867e37ae05a9c7c9ecf8569a5bfe752a54f660a6baRakesh Iyer        long currentPosition = 0;
3877e37ae05a9c7c9ecf8569a5bfe752a54f660a6baRakesh Iyer        if (mMediaPlayer.isPlaying()) {
3887e37ae05a9c7c9ecf8569a5bfe752a54f660a6baRakesh Iyer            currentPosition = mMediaPlayer.getCurrentPosition();
3897e37ae05a9c7c9ecf8569a5bfe752a54f660a6baRakesh Iyer            mMediaPlayer.pause();
3907e37ae05a9c7c9ecf8569a5bfe752a54f660a6baRakesh Iyer        }
3917e37ae05a9c7c9ecf8569a5bfe752a54f660a6baRakesh Iyer
3927e37ae05a9c7c9ecf8569a5bfe752a54f660a6baRakesh Iyer        PlaybackState state = new PlaybackState.Builder()
3937e37ae05a9c7c9ecf8569a5bfe752a54f660a6baRakesh Iyer                .setState(PlaybackState.STATE_PAUSED, currentPosition, PLAYBACK_SPEED_STOPPED)
3947e37ae05a9c7c9ecf8569a5bfe752a54f660a6baRakesh Iyer                .setActions(PAUSED_ACTIONS)
395d47e834a2872587d83e727d8001815476713e297Rakesh Iyer                .addCustomAction(mShuffle)
3967e37ae05a9c7c9ecf8569a5bfe752a54f660a6baRakesh Iyer                .build();
3977e37ae05a9c7c9ecf8569a5bfe752a54f660a6baRakesh Iyer        mSession.setPlaybackState(state);
398f4a24e8cfd616016b1d64c0a31f57d4268431ad4Rakesh Iyer
399f4a24e8cfd616016b1d64c0a31f57d4268431ad4Rakesh Iyer        // Update the media styled notification.
400f4a24e8cfd616016b1d64c0a31f57d4268431ad4Rakesh Iyer        postMediaNotification(mPausedNotificationBuilder);
4017e37ae05a9c7c9ecf8569a5bfe752a54f660a6baRakesh Iyer    }
4027e37ae05a9c7c9ecf8569a5bfe752a54f660a6baRakesh Iyer
4037e37ae05a9c7c9ecf8569a5bfe752a54f660a6baRakesh Iyer    private void stopPlayback() {
4047e37ae05a9c7c9ecf8569a5bfe752a54f660a6baRakesh Iyer        if (Log.isLoggable(TAG, Log.DEBUG)) {
4057e37ae05a9c7c9ecf8569a5bfe752a54f660a6baRakesh Iyer            Log.d(TAG, "stopPlayback()");
4067e37ae05a9c7c9ecf8569a5bfe752a54f660a6baRakesh Iyer        }
4077e37ae05a9c7c9ecf8569a5bfe752a54f660a6baRakesh Iyer
4087e37ae05a9c7c9ecf8569a5bfe752a54f660a6baRakesh Iyer        if (mMediaPlayer.isPlaying()) {
4097e37ae05a9c7c9ecf8569a5bfe752a54f660a6baRakesh Iyer            mMediaPlayer.stop();
4107e37ae05a9c7c9ecf8569a5bfe752a54f660a6baRakesh Iyer        }
4117e37ae05a9c7c9ecf8569a5bfe752a54f660a6baRakesh Iyer
4127e37ae05a9c7c9ecf8569a5bfe752a54f660a6baRakesh Iyer        PlaybackState state = new PlaybackState.Builder()
4137e37ae05a9c7c9ecf8569a5bfe752a54f660a6baRakesh Iyer                .setState(PlaybackState.STATE_STOPPED, PLAYBACK_POSITION_STOPPED,
4147e37ae05a9c7c9ecf8569a5bfe752a54f660a6baRakesh Iyer                        PLAYBACK_SPEED_STOPPED)
4157e37ae05a9c7c9ecf8569a5bfe752a54f660a6baRakesh Iyer                .setActions(STOPPED_ACTIONS)
4167e37ae05a9c7c9ecf8569a5bfe752a54f660a6baRakesh Iyer                .build();
4177e37ae05a9c7c9ecf8569a5bfe752a54f660a6baRakesh Iyer        mSession.setPlaybackState(state);
4187e37ae05a9c7c9ecf8569a5bfe752a54f660a6baRakesh Iyer    }
4197e37ae05a9c7c9ecf8569a5bfe752a54f660a6baRakesh Iyer
4207e37ae05a9c7c9ecf8569a5bfe752a54f660a6baRakesh Iyer    private void advance() throws IOException {
4217e37ae05a9c7c9ecf8569a5bfe752a54f660a6baRakesh Iyer        if (Log.isLoggable(TAG, Log.DEBUG)) {
4227e37ae05a9c7c9ecf8569a5bfe752a54f660a6baRakesh Iyer            Log.d(TAG, "advance()");
4237e37ae05a9c7c9ecf8569a5bfe752a54f660a6baRakesh Iyer        }
4247e37ae05a9c7c9ecf8569a5bfe752a54f660a6baRakesh Iyer        // Go to the next song if one exists. Note that if you were to support gapless
4257e37ae05a9c7c9ecf8569a5bfe752a54f660a6baRakesh Iyer        // playback, you would have to change this code such that you had a currently
4267e37ae05a9c7c9ecf8569a5bfe752a54f660a6baRakesh Iyer        // playing and a loading MediaPlayer and juggled between them while also calling
4277e37ae05a9c7c9ecf8569a5bfe752a54f660a6baRakesh Iyer        // setNextMediaPlayer.
4287e37ae05a9c7c9ecf8569a5bfe752a54f660a6baRakesh Iyer
4297e37ae05a9c7c9ecf8569a5bfe752a54f660a6baRakesh Iyer        if (mQueue != null) {
4307e37ae05a9c7c9ecf8569a5bfe752a54f660a6baRakesh Iyer            // Keep looping around when we run off the end of our current queue.
4317e37ae05a9c7c9ecf8569a5bfe752a54f660a6baRakesh Iyer            mCurrentQueueIdx = (mCurrentQueueIdx + 1) % mQueue.size();
4327e37ae05a9c7c9ecf8569a5bfe752a54f660a6baRakesh Iyer            playCurrentQueueIndex();
4337e37ae05a9c7c9ecf8569a5bfe752a54f660a6baRakesh Iyer        } else {
4347e37ae05a9c7c9ecf8569a5bfe752a54f660a6baRakesh Iyer            stopPlayback();
4357e37ae05a9c7c9ecf8569a5bfe752a54f660a6baRakesh Iyer        }
4367e37ae05a9c7c9ecf8569a5bfe752a54f660a6baRakesh Iyer    }
4377e37ae05a9c7c9ecf8569a5bfe752a54f660a6baRakesh Iyer
4387e37ae05a9c7c9ecf8569a5bfe752a54f660a6baRakesh Iyer    private void retreat() throws IOException {
4397e37ae05a9c7c9ecf8569a5bfe752a54f660a6baRakesh Iyer        if (Log.isLoggable(TAG, Log.DEBUG)) {
4407e37ae05a9c7c9ecf8569a5bfe752a54f660a6baRakesh Iyer            Log.d(TAG, "retreat()");
4417e37ae05a9c7c9ecf8569a5bfe752a54f660a6baRakesh Iyer        }
4427e37ae05a9c7c9ecf8569a5bfe752a54f660a6baRakesh Iyer        // Go to the next song if one exists. Note that if you were to support gapless
4437e37ae05a9c7c9ecf8569a5bfe752a54f660a6baRakesh Iyer        // playback, you would have to change this code such that you had a currently
4447e37ae05a9c7c9ecf8569a5bfe752a54f660a6baRakesh Iyer        // playing and a loading MediaPlayer and juggled between them while also calling
4457e37ae05a9c7c9ecf8569a5bfe752a54f660a6baRakesh Iyer        // setNextMediaPlayer.
4467e37ae05a9c7c9ecf8569a5bfe752a54f660a6baRakesh Iyer        if (mQueue != null) {
4477e37ae05a9c7c9ecf8569a5bfe752a54f660a6baRakesh Iyer            // Keep looping around when we run off the end of our current queue.
4487e37ae05a9c7c9ecf8569a5bfe752a54f660a6baRakesh Iyer            mCurrentQueueIdx--;
4497e37ae05a9c7c9ecf8569a5bfe752a54f660a6baRakesh Iyer            if (mCurrentQueueIdx < 0) {
4507e37ae05a9c7c9ecf8569a5bfe752a54f660a6baRakesh Iyer                mCurrentQueueIdx = mQueue.size() - 1;
4517e37ae05a9c7c9ecf8569a5bfe752a54f660a6baRakesh Iyer            }
4527e37ae05a9c7c9ecf8569a5bfe752a54f660a6baRakesh Iyer            playCurrentQueueIndex();
4537e37ae05a9c7c9ecf8569a5bfe752a54f660a6baRakesh Iyer        } else {
4547e37ae05a9c7c9ecf8569a5bfe752a54f660a6baRakesh Iyer            stopPlayback();
4557e37ae05a9c7c9ecf8569a5bfe752a54f660a6baRakesh Iyer        }
4567e37ae05a9c7c9ecf8569a5bfe752a54f660a6baRakesh Iyer    }
4577e37ae05a9c7c9ecf8569a5bfe752a54f660a6baRakesh Iyer
4587e37ae05a9c7c9ecf8569a5bfe752a54f660a6baRakesh Iyer    private void playCurrentQueueIndex() throws IOException {
4597e37ae05a9c7c9ecf8569a5bfe752a54f660a6baRakesh Iyer        MediaDescription next = mQueue.get(mCurrentQueueIdx).getDescription();
4607e37ae05a9c7c9ecf8569a5bfe752a54f660a6baRakesh Iyer        String path = next.getExtras().getString(DataModel.PATH_KEY);
4617e37ae05a9c7c9ecf8569a5bfe752a54f660a6baRakesh Iyer        MediaMetadata metadata = mDataModel.getMetadata(next.getMediaId());
4627e37ae05a9c7c9ecf8569a5bfe752a54f660a6baRakesh Iyer
4637e37ae05a9c7c9ecf8569a5bfe752a54f660a6baRakesh Iyer        play(path, metadata);
4647e37ae05a9c7c9ecf8569a5bfe752a54f660a6baRakesh Iyer    }
4657e37ae05a9c7c9ecf8569a5bfe752a54f660a6baRakesh Iyer
4667e37ae05a9c7c9ecf8569a5bfe752a54f660a6baRakesh Iyer    private void play(String path, MediaMetadata metadata) throws IOException {
4677e37ae05a9c7c9ecf8569a5bfe752a54f660a6baRakesh Iyer        if (Log.isLoggable(TAG, Log.DEBUG)) {
4687e37ae05a9c7c9ecf8569a5bfe752a54f660a6baRakesh Iyer            Log.d(TAG, "play path=" + path + " metadata=" + metadata);
4697e37ae05a9c7c9ecf8569a5bfe752a54f660a6baRakesh Iyer        }
4707e37ae05a9c7c9ecf8569a5bfe752a54f660a6baRakesh Iyer
4717e37ae05a9c7c9ecf8569a5bfe752a54f660a6baRakesh Iyer        mMediaPlayer.reset();
4727e37ae05a9c7c9ecf8569a5bfe752a54f660a6baRakesh Iyer        mMediaPlayer.setDataSource(path);
4737e37ae05a9c7c9ecf8569a5bfe752a54f660a6baRakesh Iyer        mMediaPlayer.prepare();
4747e37ae05a9c7c9ecf8569a5bfe752a54f660a6baRakesh Iyer        mMediaPlayer.start();
4757e37ae05a9c7c9ecf8569a5bfe752a54f660a6baRakesh Iyer
4767e37ae05a9c7c9ecf8569a5bfe752a54f660a6baRakesh Iyer        if (metadata != null) {
4777e37ae05a9c7c9ecf8569a5bfe752a54f660a6baRakesh Iyer            mSession.setMetadata(metadata);
4787e37ae05a9c7c9ecf8569a5bfe752a54f660a6baRakesh Iyer        }
4797e37ae05a9c7c9ecf8569a5bfe752a54f660a6baRakesh Iyer        updatePlaybackStatePlaying();
4807e37ae05a9c7c9ecf8569a5bfe752a54f660a6baRakesh Iyer    }
4817e37ae05a9c7c9ecf8569a5bfe752a54f660a6baRakesh Iyer
4827e37ae05a9c7c9ecf8569a5bfe752a54f660a6baRakesh Iyer    private void safeAdvance() {
4837e37ae05a9c7c9ecf8569a5bfe752a54f660a6baRakesh Iyer        try {
4847e37ae05a9c7c9ecf8569a5bfe752a54f660a6baRakesh Iyer            advance();
4857e37ae05a9c7c9ecf8569a5bfe752a54f660a6baRakesh Iyer        } catch (IOException e) {
4867e37ae05a9c7c9ecf8569a5bfe752a54f660a6baRakesh Iyer            Log.e(TAG, "Failed to advance.", e);
4877e37ae05a9c7c9ecf8569a5bfe752a54f660a6baRakesh Iyer            mSession.setPlaybackState(mErrorState);
4887e37ae05a9c7c9ecf8569a5bfe752a54f660a6baRakesh Iyer        }
4897e37ae05a9c7c9ecf8569a5bfe752a54f660a6baRakesh Iyer    }
4907e37ae05a9c7c9ecf8569a5bfe752a54f660a6baRakesh Iyer
4917e37ae05a9c7c9ecf8569a5bfe752a54f660a6baRakesh Iyer    private void safeRetreat() {
4927e37ae05a9c7c9ecf8569a5bfe752a54f660a6baRakesh Iyer        try {
4937e37ae05a9c7c9ecf8569a5bfe752a54f660a6baRakesh Iyer            retreat();
4947e37ae05a9c7c9ecf8569a5bfe752a54f660a6baRakesh Iyer        } catch (IOException e) {
4957e37ae05a9c7c9ecf8569a5bfe752a54f660a6baRakesh Iyer            Log.e(TAG, "Failed to advance.", e);
4967e37ae05a9c7c9ecf8569a5bfe752a54f660a6baRakesh Iyer            mSession.setPlaybackState(mErrorState);
4977e37ae05a9c7c9ecf8569a5bfe752a54f660a6baRakesh Iyer        }
4987e37ae05a9c7c9ecf8569a5bfe752a54f660a6baRakesh Iyer    }
4997e37ae05a9c7c9ecf8569a5bfe752a54f660a6baRakesh Iyer
500d47e834a2872587d83e727d8001815476713e297Rakesh Iyer    /**
501d47e834a2872587d83e727d8001815476713e297Rakesh Iyer     * This is a naive implementation of shuffle, previously played songs may repeat after the
502d47e834a2872587d83e727d8001815476713e297Rakesh Iyer     * shuffle operation. Only call this from the main thread.
503d47e834a2872587d83e727d8001815476713e297Rakesh Iyer     */
504d47e834a2872587d83e727d8001815476713e297Rakesh Iyer    private void shuffle() {
505d47e834a2872587d83e727d8001815476713e297Rakesh Iyer        if (Log.isLoggable(TAG, Log.DEBUG)) {
506d47e834a2872587d83e727d8001815476713e297Rakesh Iyer            Log.d(TAG, "Shuffling");
507d47e834a2872587d83e727d8001815476713e297Rakesh Iyer        }
508d47e834a2872587d83e727d8001815476713e297Rakesh Iyer
509d47e834a2872587d83e727d8001815476713e297Rakesh Iyer        if (mQueue != null) {
510d47e834a2872587d83e727d8001815476713e297Rakesh Iyer            QueueItem current = mQueue.remove(mCurrentQueueIdx);
511d47e834a2872587d83e727d8001815476713e297Rakesh Iyer            Collections.shuffle(mQueue);
512d47e834a2872587d83e727d8001815476713e297Rakesh Iyer            mQueue.add(0, current);
513d47e834a2872587d83e727d8001815476713e297Rakesh Iyer            mCurrentQueueIdx = 0;
514a48989c04282c7041b7f17b58cb2d0753075722eRakesh Iyer            updateSessionQueueState();
515d47e834a2872587d83e727d8001815476713e297Rakesh Iyer            updatePlaybackStatePlaying();
516d47e834a2872587d83e727d8001815476713e297Rakesh Iyer        }
517d47e834a2872587d83e727d8001815476713e297Rakesh Iyer    }
518d47e834a2872587d83e727d8001815476713e297Rakesh Iyer
5197e37ae05a9c7c9ecf8569a5bfe752a54f660a6baRakesh Iyer    @Override
5207e37ae05a9c7c9ecf8569a5bfe752a54f660a6baRakesh Iyer    public void onPlayFromMediaId(String mediaId, Bundle extras) {
5217e37ae05a9c7c9ecf8569a5bfe752a54f660a6baRakesh Iyer        super.onPlayFromMediaId(mediaId, extras);
5227e37ae05a9c7c9ecf8569a5bfe752a54f660a6baRakesh Iyer        if (Log.isLoggable(TAG, Log.DEBUG)) {
5237e37ae05a9c7c9ecf8569a5bfe752a54f660a6baRakesh Iyer            Log.d(TAG, "onPlayFromMediaId mediaId" + mediaId + " extras=" + extras);
5247e37ae05a9c7c9ecf8569a5bfe752a54f660a6baRakesh Iyer        }
5257e37ae05a9c7c9ecf8569a5bfe752a54f660a6baRakesh Iyer
5264f47437963a9271cda42fea8d57d29f30823b5a7Rakesh Iyer        requestAudioFocus(() -> startPlayback(mediaId));
5277e37ae05a9c7c9ecf8569a5bfe752a54f660a6baRakesh Iyer    }
5287e37ae05a9c7c9ecf8569a5bfe752a54f660a6baRakesh Iyer
5297e37ae05a9c7c9ecf8569a5bfe752a54f660a6baRakesh Iyer    @Override
5307e37ae05a9c7c9ecf8569a5bfe752a54f660a6baRakesh Iyer    public void onSkipToNext() {
5317e37ae05a9c7c9ecf8569a5bfe752a54f660a6baRakesh Iyer        if (Log.isLoggable(TAG, Log.DEBUG)) {
5327e37ae05a9c7c9ecf8569a5bfe752a54f660a6baRakesh Iyer            Log.d(TAG, "onSkipToNext()");
5337e37ae05a9c7c9ecf8569a5bfe752a54f660a6baRakesh Iyer        }
5347e37ae05a9c7c9ecf8569a5bfe752a54f660a6baRakesh Iyer        safeAdvance();
5357e37ae05a9c7c9ecf8569a5bfe752a54f660a6baRakesh Iyer    }
5367e37ae05a9c7c9ecf8569a5bfe752a54f660a6baRakesh Iyer
5377e37ae05a9c7c9ecf8569a5bfe752a54f660a6baRakesh Iyer    @Override
5387e37ae05a9c7c9ecf8569a5bfe752a54f660a6baRakesh Iyer    public void onSkipToPrevious() {
5397e37ae05a9c7c9ecf8569a5bfe752a54f660a6baRakesh Iyer        if (Log.isLoggable(TAG, Log.DEBUG)) {
5407e37ae05a9c7c9ecf8569a5bfe752a54f660a6baRakesh Iyer            Log.d(TAG, "onSkipToPrevious()");
5417e37ae05a9c7c9ecf8569a5bfe752a54f660a6baRakesh Iyer        }
5427e37ae05a9c7c9ecf8569a5bfe752a54f660a6baRakesh Iyer        safeRetreat();
5437e37ae05a9c7c9ecf8569a5bfe752a54f660a6baRakesh Iyer    }
5447e37ae05a9c7c9ecf8569a5bfe752a54f660a6baRakesh Iyer
5457e37ae05a9c7c9ecf8569a5bfe752a54f660a6baRakesh Iyer    @Override
5467e37ae05a9c7c9ecf8569a5bfe752a54f660a6baRakesh Iyer    public void onSkipToQueueItem(long id) {
5477e37ae05a9c7c9ecf8569a5bfe752a54f660a6baRakesh Iyer        int idx = (int) id;
5487e37ae05a9c7c9ecf8569a5bfe752a54f660a6baRakesh Iyer        MediaSession.QueueItem item = mQueue.get(idx);
5497e37ae05a9c7c9ecf8569a5bfe752a54f660a6baRakesh Iyer        MediaDescription description = item.getDescription();
5507e37ae05a9c7c9ecf8569a5bfe752a54f660a6baRakesh Iyer
5517e37ae05a9c7c9ecf8569a5bfe752a54f660a6baRakesh Iyer        String path = description.getExtras().getString(DataModel.PATH_KEY);
5527e37ae05a9c7c9ecf8569a5bfe752a54f660a6baRakesh Iyer        MediaMetadata metadata = mDataModel.getMetadata(description.getMediaId());
5537e37ae05a9c7c9ecf8569a5bfe752a54f660a6baRakesh Iyer
5547e37ae05a9c7c9ecf8569a5bfe752a54f660a6baRakesh Iyer        try {
5557e37ae05a9c7c9ecf8569a5bfe752a54f660a6baRakesh Iyer            play(path, metadata);
5567e37ae05a9c7c9ecf8569a5bfe752a54f660a6baRakesh Iyer            mCurrentQueueIdx = idx;
5577e37ae05a9c7c9ecf8569a5bfe752a54f660a6baRakesh Iyer        } catch (IOException e) {
5587e37ae05a9c7c9ecf8569a5bfe752a54f660a6baRakesh Iyer            Log.e(TAG, "Failed to play.", e);
5597e37ae05a9c7c9ecf8569a5bfe752a54f660a6baRakesh Iyer            mSession.setPlaybackState(mErrorState);
5607e37ae05a9c7c9ecf8569a5bfe752a54f660a6baRakesh Iyer        }
5617e37ae05a9c7c9ecf8569a5bfe752a54f660a6baRakesh Iyer    }
5627e37ae05a9c7c9ecf8569a5bfe752a54f660a6baRakesh Iyer
563d47e834a2872587d83e727d8001815476713e297Rakesh Iyer    @Override
564d47e834a2872587d83e727d8001815476713e297Rakesh Iyer    public void onCustomAction(String action, Bundle extras) {
565d47e834a2872587d83e727d8001815476713e297Rakesh Iyer        switch (action) {
566d47e834a2872587d83e727d8001815476713e297Rakesh Iyer            case SHUFFLE:
567d47e834a2872587d83e727d8001815476713e297Rakesh Iyer                shuffle();
568d47e834a2872587d83e727d8001815476713e297Rakesh Iyer                break;
569d47e834a2872587d83e727d8001815476713e297Rakesh Iyer            default:
570d47e834a2872587d83e727d8001815476713e297Rakesh Iyer                Log.e(TAG, "Unhandled custom action: " + action);
571d47e834a2872587d83e727d8001815476713e297Rakesh Iyer        }
572d47e834a2872587d83e727d8001815476713e297Rakesh Iyer    }
573d47e834a2872587d83e727d8001815476713e297Rakesh Iyer
5747e37ae05a9c7c9ecf8569a5bfe752a54f660a6baRakesh Iyer    private OnAudioFocusChangeListener mAudioFocusListener = new OnAudioFocusChangeListener() {
5757e37ae05a9c7c9ecf8569a5bfe752a54f660a6baRakesh Iyer        @Override
5767e37ae05a9c7c9ecf8569a5bfe752a54f660a6baRakesh Iyer        public void onAudioFocusChange(int focus) {
5777e37ae05a9c7c9ecf8569a5bfe752a54f660a6baRakesh Iyer            switch (focus) {
5787e37ae05a9c7c9ecf8569a5bfe752a54f660a6baRakesh Iyer                case AudioManager.AUDIOFOCUS_GAIN:
5797e37ae05a9c7c9ecf8569a5bfe752a54f660a6baRakesh Iyer                    resumePlayback();
5807e37ae05a9c7c9ecf8569a5bfe752a54f660a6baRakesh Iyer                    break;
5817e37ae05a9c7c9ecf8569a5bfe752a54f660a6baRakesh Iyer                case AudioManager.AUDIOFOCUS_LOSS:
5827e37ae05a9c7c9ecf8569a5bfe752a54f660a6baRakesh Iyer                case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT:
5837e37ae05a9c7c9ecf8569a5bfe752a54f660a6baRakesh Iyer                case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK:
5847e37ae05a9c7c9ecf8569a5bfe752a54f660a6baRakesh Iyer                    pausePlayback();
5857e37ae05a9c7c9ecf8569a5bfe752a54f660a6baRakesh Iyer                    break;
586d47e834a2872587d83e727d8001815476713e297Rakesh Iyer                default:
587d47e834a2872587d83e727d8001815476713e297Rakesh Iyer                    Log.e(TAG, "Unhandled audio focus type: " + focus);
5887e37ae05a9c7c9ecf8569a5bfe752a54f660a6baRakesh Iyer            }
5897e37ae05a9c7c9ecf8569a5bfe752a54f660a6baRakesh Iyer        }
5907e37ae05a9c7c9ecf8569a5bfe752a54f660a6baRakesh Iyer    };
5917e37ae05a9c7c9ecf8569a5bfe752a54f660a6baRakesh Iyer
5927e37ae05a9c7c9ecf8569a5bfe752a54f660a6baRakesh Iyer    private OnCompletionListener mOnCompletionListener = new OnCompletionListener() {
5937e37ae05a9c7c9ecf8569a5bfe752a54f660a6baRakesh Iyer        @Override
5947e37ae05a9c7c9ecf8569a5bfe752a54f660a6baRakesh Iyer        public void onCompletion(MediaPlayer mediaPlayer) {
5957e37ae05a9c7c9ecf8569a5bfe752a54f660a6baRakesh Iyer            if (Log.isLoggable(TAG, Log.DEBUG)) {
5967e37ae05a9c7c9ecf8569a5bfe752a54f660a6baRakesh Iyer                Log.d(TAG, "onCompletion()");
5977e37ae05a9c7c9ecf8569a5bfe752a54f660a6baRakesh Iyer            }
5987e37ae05a9c7c9ecf8569a5bfe752a54f660a6baRakesh Iyer            safeAdvance();
5997e37ae05a9c7c9ecf8569a5bfe752a54f660a6baRakesh Iyer        }
6007e37ae05a9c7c9ecf8569a5bfe752a54f660a6baRakesh Iyer    };
6017e37ae05a9c7c9ecf8569a5bfe752a54f660a6baRakesh Iyer}
602