/*
 * Copyright (C) 2016 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.android.bluetooth.avrcpcontroller;

import android.bluetooth.BluetoothAvrcpController;
import android.bluetooth.BluetoothAvrcpPlayerSettings;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothProfile;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.media.AudioManager;
import android.media.MediaDescription;
import android.media.MediaMetadata;
import android.media.browse.MediaBrowser.MediaItem;
import android.media.session.PlaybackState;
import android.os.Bundle;
import android.os.Message;
import android.util.Log;

import com.android.bluetooth.BluetoothMetricsProto;
import com.android.bluetooth.Utils;
import com.android.bluetooth.a2dpsink.A2dpSinkService;
import com.android.bluetooth.btservice.MetricsLogger;
import com.android.bluetooth.btservice.ProfileService;
import com.android.internal.util.State;
import com.android.internal.util.StateMachine;

import java.util.ArrayList;
import java.util.List;

/**
 * Provides Bluetooth AVRCP Controller State Machine responsible for all remote control connections
 * and interactions with a remote controlable device.
 */
class AvrcpControllerStateMachine extends StateMachine {

    // commands from Binder service
    static final int MESSAGE_SEND_PASS_THROUGH_CMD = 1;
    static final int MESSAGE_SEND_GROUP_NAVIGATION_CMD = 3;
    static final int MESSAGE_GET_NOW_PLAYING_LIST = 5;
    static final int MESSAGE_GET_FOLDER_LIST = 6;
    static final int MESSAGE_GET_PLAYER_LIST = 7;
    static final int MESSAGE_CHANGE_FOLDER_PATH = 8;
    static final int MESSAGE_FETCH_ATTR_AND_PLAY_ITEM = 9;
    static final int MESSAGE_SET_BROWSED_PLAYER = 10;

    // commands from native layer
    static final int MESSAGE_PROCESS_SET_ABS_VOL_CMD = 103;
    static final int MESSAGE_PROCESS_REGISTER_ABS_VOL_NOTIFICATION = 104;
    static final int MESSAGE_PROCESS_TRACK_CHANGED = 105;
    static final int MESSAGE_PROCESS_PLAY_POS_CHANGED = 106;
    static final int MESSAGE_PROCESS_PLAY_STATUS_CHANGED = 107;
    static final int MESSAGE_PROCESS_VOLUME_CHANGED_NOTIFICATION = 108;
    static final int MESSAGE_PROCESS_GET_FOLDER_ITEMS = 109;
    static final int MESSAGE_PROCESS_GET_FOLDER_ITEMS_OUT_OF_RANGE = 110;
    static final int MESSAGE_PROCESS_GET_PLAYER_ITEMS = 111;
    static final int MESSAGE_PROCESS_FOLDER_PATH = 112;
    static final int MESSAGE_PROCESS_SET_BROWSED_PLAYER = 113;
    static final int MESSAGE_PROCESS_SET_ADDRESSED_PLAYER = 114;

    // commands from A2DP sink
    static final int MESSAGE_STOP_METADATA_BROADCASTS = 201;
    static final int MESSAGE_START_METADATA_BROADCASTS = 202;

    // commands for connection
    static final int MESSAGE_PROCESS_RC_FEATURES = 301;
    static final int MESSAGE_PROCESS_CONNECTION_CHANGE = 302;
    static final int MESSAGE_PROCESS_BROWSE_CONNECTION_CHANGE = 303;

    // Interal messages
    static final int MESSAGE_INTERNAL_BROWSE_DEPTH_INCREMENT = 401;
    static final int MESSAGE_INTERNAL_MOVE_N_LEVELS_UP = 402;
    static final int MESSAGE_INTERNAL_CMD_TIMEOUT = 403;
    static final int MESSAGE_INTERNAL_ABS_VOL_TIMEOUT = 404;

    static final int ABS_VOL_TIMEOUT_MILLIS = 1000; //1s
    static final int CMD_TIMEOUT_MILLIS = 5000; // 5s
    // Fetch only 20 items at a time.
    static final int GET_FOLDER_ITEMS_PAGINATION_SIZE = 20;
    // Fetch no more than 1000 items per directory.
    static final int MAX_FOLDER_ITEMS = 1000;

    /*
     * Base value for absolute volume from JNI
     */
    private static final int ABS_VOL_BASE = 127;

    /*
     * Notification types for Avrcp protocol JNI.
     */
    private static final byte NOTIFICATION_RSP_TYPE_INTERIM = 0x00;
    private static final byte NOTIFICATION_RSP_TYPE_CHANGED = 0x01;


    private static final String TAG = "AvrcpControllerSM";
    private static final boolean DBG = true;
    private static final boolean VDBG = false;

    private final Context mContext;
    private final AudioManager mAudioManager;

    private final State mDisconnected;
    private final State mConnected;
    private final SetBrowsedPlayer mSetBrowsedPlayer;
    private final SetAddresedPlayerAndPlayItem mSetAddrPlayer;
    private final ChangeFolderPath mChangeFolderPath;
    private final GetFolderList mGetFolderList;
    private final GetPlayerListing mGetPlayerListing;
    private final MoveToRoot mMoveToRoot;

    private final Object mLock = new Object();
    private static final ArrayList<MediaItem> EMPTY_MEDIA_ITEM_LIST = new ArrayList<>();
    private static final MediaMetadata EMPTY_MEDIA_METADATA = new MediaMetadata.Builder().build();

    // APIs exist to access these so they must be thread safe
    private Boolean mIsConnected = false;
    private RemoteDevice mRemoteDevice;
    private AvrcpPlayer mAddressedPlayer;

    // Only accessed from State Machine processMessage
    private int mVolumeChangedNotificationsToIgnore = 0;
    private int mPreviousPercentageVol = -1;

    // Depth from root of current browsing. This can be used to move to root directly.
    private int mBrowseDepth = 0;

    // Browse tree.
    private BrowseTree mBrowseTree = new BrowseTree();

    AvrcpControllerStateMachine(Context context) {
        super(TAG);
        mContext = context;

        mAudioManager = (AudioManager) mContext.getSystemService(Context.AUDIO_SERVICE);
        IntentFilter filter = new IntentFilter(AudioManager.VOLUME_CHANGED_ACTION);
        mContext.registerReceiver(mBroadcastReceiver, filter);

        mDisconnected = new Disconnected();
        mConnected = new Connected();

        // Used to change folder path and fetch the new folder listing.
        mSetBrowsedPlayer = new SetBrowsedPlayer();
        mSetAddrPlayer = new SetAddresedPlayerAndPlayItem();
        mChangeFolderPath = new ChangeFolderPath();
        mGetFolderList = new GetFolderList();
        mGetPlayerListing = new GetPlayerListing();
        mMoveToRoot = new MoveToRoot();

        addState(mDisconnected);
        addState(mConnected);

        // Any action that needs blocking other requests to the state machine will be implemented as
        // a separate substate of the mConnected state. Once transtition to the sub-state we should
        // only handle the messages that are relevant to the sub-action. Everything else should be
        // deferred so that once we transition to the mConnected we can process them hence.
        addState(mSetBrowsedPlayer, mConnected);
        addState(mSetAddrPlayer, mConnected);
        addState(mChangeFolderPath, mConnected);
        addState(mGetFolderList, mConnected);
        addState(mGetPlayerListing, mConnected);
        addState(mMoveToRoot, mConnected);

        setInitialState(mDisconnected);
    }

    class Disconnected extends State {

        @Override
        public boolean processMessage(Message msg) {
            if (DBG) Log.d(TAG, " HandleMessage: " + dumpMessageString(msg.what));
            switch (msg.what) {
                case MESSAGE_PROCESS_CONNECTION_CHANGE:
                    if (msg.arg1 == BluetoothProfile.STATE_CONNECTED) {
                        mBrowseTree.init();
                        transitionTo(mConnected);
                        BluetoothDevice rtDevice = (BluetoothDevice) msg.obj;
                        synchronized (mLock) {
                            mRemoteDevice = new RemoteDevice(rtDevice);
                            mAddressedPlayer = new AvrcpPlayer();
                            mIsConnected = true;
                        }
                        MetricsLogger.logProfileConnectionEvent(
                                BluetoothMetricsProto.ProfileId.AVRCP_CONTROLLER);
                        Intent intent = new Intent(
                                BluetoothAvrcpController.ACTION_CONNECTION_STATE_CHANGED);
                        intent.putExtra(BluetoothProfile.EXTRA_PREVIOUS_STATE,
                                BluetoothProfile.STATE_DISCONNECTED);
                        intent.putExtra(BluetoothProfile.EXTRA_STATE,
                                BluetoothProfile.STATE_CONNECTED);
                        intent.putExtra(BluetoothDevice.EXTRA_DEVICE, rtDevice);
                        mContext.sendBroadcast(intent, ProfileService.BLUETOOTH_PERM);
                    }
                    break;

                default:
                    Log.w(TAG,
                            "Currently Disconnected not handling " + dumpMessageString(msg.what));
                    return false;
            }
            return true;
        }
    }

    class Connected extends State {
        @Override
        public boolean processMessage(Message msg) {
            if (DBG) Log.d(TAG, " HandleMessage: " + dumpMessageString(msg.what));
            A2dpSinkService a2dpSinkService = A2dpSinkService.getA2dpSinkService();
            synchronized (mLock) {
                switch (msg.what) {
                    case MESSAGE_SEND_PASS_THROUGH_CMD:
                        BluetoothDevice device = (BluetoothDevice) msg.obj;
                        AvrcpControllerService.sendPassThroughCommandNative(
                                Utils.getByteAddress(device), msg.arg1, msg.arg2);
                        if (a2dpSinkService != null) {
                            if (DBG) Log.d(TAG, " inform AVRCP Commands to A2DP Sink ");
                            a2dpSinkService.informAvrcpPassThroughCmd(device, msg.arg1, msg.arg2);
                        }
                        break;

                    case MESSAGE_SEND_GROUP_NAVIGATION_CMD:
                        AvrcpControllerService.sendGroupNavigationCommandNative(
                                mRemoteDevice.getBluetoothAddress(), msg.arg1, msg.arg2);
                        break;

                    case MESSAGE_GET_NOW_PLAYING_LIST:
                        mGetFolderList.setFolder((String) msg.obj);
                        mGetFolderList.setBounds((int) msg.arg1, (int) msg.arg2);
                        mGetFolderList.setScope(AvrcpControllerService.BROWSE_SCOPE_NOW_PLAYING);
                        transitionTo(mGetFolderList);
                        break;

                    case MESSAGE_GET_FOLDER_LIST:
                        // Whenever we transition we set the information for folder we need to
                        // return result.
                        mGetFolderList.setBounds(msg.arg1, msg.arg2);
                        mGetFolderList.setFolder((String) msg.obj);
                        mGetFolderList.setScope(AvrcpControllerService.BROWSE_SCOPE_VFS);
                        transitionTo(mGetFolderList);
                        break;

                    case MESSAGE_GET_PLAYER_LIST:
                        AvrcpControllerService.getPlayerListNative(
                                mRemoteDevice.getBluetoothAddress(), (byte) msg.arg1,
                                (byte) msg.arg2);
                        transitionTo(mGetPlayerListing);
                        sendMessageDelayed(MESSAGE_INTERNAL_CMD_TIMEOUT, CMD_TIMEOUT_MILLIS);
                        break;

                    case MESSAGE_CHANGE_FOLDER_PATH: {
                        int direction = msg.arg1;
                        Bundle b = (Bundle) msg.obj;
                        String uid = b.getString(AvrcpControllerService.EXTRA_FOLDER_BT_ID);
                        String fid = b.getString(AvrcpControllerService.EXTRA_FOLDER_ID);

                        // String is encoded as a Hex String (mostly for display purposes)
                        // hence convert this back to real byte string.
                        AvrcpControllerService.changeFolderPathNative(
                                mRemoteDevice.getBluetoothAddress(), (byte) msg.arg1,
                                AvrcpControllerService.hexStringToByteUID(uid));
                        mChangeFolderPath.setFolder(fid);
                        transitionTo(mChangeFolderPath);
                        sendMessage(MESSAGE_INTERNAL_BROWSE_DEPTH_INCREMENT, (byte) msg.arg1);
                        sendMessageDelayed(MESSAGE_INTERNAL_CMD_TIMEOUT, CMD_TIMEOUT_MILLIS);
                        break;
                    }

                    case MESSAGE_FETCH_ATTR_AND_PLAY_ITEM: {
                        int scope = msg.arg1;
                        String playItemUid = (String) msg.obj;
                        BrowseTree.BrowseNode currBrPlayer = mBrowseTree.getCurrentBrowsedPlayer();
                        BrowseTree.BrowseNode currAddrPlayer =
                                mBrowseTree.getCurrentAddressedPlayer();
                        if (DBG) {
                            Log.d(TAG, "currBrPlayer " + currBrPlayer + " currAddrPlayer "
                                    + currAddrPlayer);
                        }

                        if (currBrPlayer == null || currBrPlayer.equals(currAddrPlayer)) {
                            // String is encoded as a Hex String (mostly for display purposes)
                            // hence convert this back to real byte string.
                            // NOTE: It may be possible that sending play while the same item is
                            // playing leads to reset of track.
                            AvrcpControllerService.playItemNative(
                                    mRemoteDevice.getBluetoothAddress(), (byte) scope,
                                    AvrcpControllerService.hexStringToByteUID(playItemUid),
                                    (int) 0);
                        } else {
                            // Send out the request for setting addressed player.
                            AvrcpControllerService.setAddressedPlayerNative(
                                    mRemoteDevice.getBluetoothAddress(),
                                    currBrPlayer.getPlayerID());
                            mSetAddrPlayer.setItemAndScope(currBrPlayer.getID(), playItemUid,
                                    scope);
                            transitionTo(mSetAddrPlayer);
                        }
                        break;
                    }

                    case MESSAGE_SET_BROWSED_PLAYER: {
                        AvrcpControllerService.setBrowsedPlayerNative(
                                mRemoteDevice.getBluetoothAddress(), (int) msg.arg1);
                        mSetBrowsedPlayer.setFolder((String) msg.obj);
                        transitionTo(mSetBrowsedPlayer);
                        break;
                    }

                    case MESSAGE_PROCESS_SET_ADDRESSED_PLAYER:
                        AvrcpControllerService.getPlayerListNative(
                                mRemoteDevice.getBluetoothAddress(), 0, 255);
                        transitionTo(mGetPlayerListing);
                        sendMessageDelayed(MESSAGE_INTERNAL_CMD_TIMEOUT, CMD_TIMEOUT_MILLIS);
                        break;


                    case MESSAGE_PROCESS_CONNECTION_CHANGE:
                        if (msg.arg1 == BluetoothProfile.STATE_DISCONNECTED) {
                            synchronized (mLock) {
                                mIsConnected = false;
                                mRemoteDevice = null;
                            }
                            mBrowseTree.clear();
                            transitionTo(mDisconnected);
                            BluetoothDevice rtDevice = (BluetoothDevice) msg.obj;
                            Intent intent = new Intent(
                                    BluetoothAvrcpController.ACTION_CONNECTION_STATE_CHANGED);
                            intent.putExtra(BluetoothProfile.EXTRA_PREVIOUS_STATE,
                                    BluetoothProfile.STATE_CONNECTED);
                            intent.putExtra(BluetoothProfile.EXTRA_STATE,
                                    BluetoothProfile.STATE_DISCONNECTED);
                            intent.putExtra(BluetoothDevice.EXTRA_DEVICE, rtDevice);
                            mContext.sendBroadcast(intent, ProfileService.BLUETOOTH_PERM);
                        }
                        break;

                    case MESSAGE_PROCESS_BROWSE_CONNECTION_CHANGE:
                        // Service tells us if the browse is connected or disconnected.
                        // This is useful only for deciding whether to send browse commands rest of
                        // the connection state handling should be done via the message
                        // MESSAGE_PROCESS_CONNECTION_CHANGE.
                        Intent intent = new Intent(
                                AvrcpControllerService.ACTION_BROWSE_CONNECTION_STATE_CHANGED);
                        intent.putExtra(BluetoothDevice.EXTRA_DEVICE, (BluetoothDevice) msg.obj);
                        if (DBG) {
                            Log.d(TAG, "Browse connection state " + msg.arg1);
                        }
                        if (msg.arg1 == 1) {
                            intent.putExtra(BluetoothProfile.EXTRA_STATE,
                                    BluetoothProfile.STATE_CONNECTED);
                        } else if (msg.arg1 == 0) {
                            intent.putExtra(BluetoothProfile.EXTRA_STATE,
                                    BluetoothProfile.STATE_DISCONNECTED);
                            // If browse is disconnected, the next time we connect we should
                            // be at the ROOT.
                            mBrowseDepth = 0;
                        } else {
                            Log.w(TAG, "Incorrect browse state " + msg.arg1);
                        }

                        mContext.sendBroadcast(intent, ProfileService.BLUETOOTH_PERM);
                        break;

                    case MESSAGE_PROCESS_RC_FEATURES:
                        mRemoteDevice.setRemoteFeatures(msg.arg1);
                        break;

                    case MESSAGE_PROCESS_SET_ABS_VOL_CMD:
                        mVolumeChangedNotificationsToIgnore++;
                        removeMessages(MESSAGE_INTERNAL_ABS_VOL_TIMEOUT);
                        sendMessageDelayed(MESSAGE_INTERNAL_ABS_VOL_TIMEOUT,
                                ABS_VOL_TIMEOUT_MILLIS);
                        setAbsVolume(msg.arg1, msg.arg2);
                        break;

                    case MESSAGE_PROCESS_REGISTER_ABS_VOL_NOTIFICATION: {
                        mRemoteDevice.setNotificationLabel(msg.arg1);
                        mRemoteDevice.setAbsVolNotificationRequested(true);
                        int percentageVol = getVolumePercentage();
                        if (DBG) {
                            Log.d(TAG, " Sending Interim Response = " + percentageVol + " label "
                                    + msg.arg1);
                        }
                        AvrcpControllerService.sendRegisterAbsVolRspNative(
                                mRemoteDevice.getBluetoothAddress(), NOTIFICATION_RSP_TYPE_INTERIM,
                                percentageVol, mRemoteDevice.getNotificationLabel());
                    }
                    break;

                    case MESSAGE_PROCESS_VOLUME_CHANGED_NOTIFICATION: {
                        if (mVolumeChangedNotificationsToIgnore > 0) {
                            mVolumeChangedNotificationsToIgnore--;
                            if (mVolumeChangedNotificationsToIgnore == 0) {
                                removeMessages(MESSAGE_INTERNAL_ABS_VOL_TIMEOUT);
                            }
                        } else {
                            if (mRemoteDevice.getAbsVolNotificationRequested()) {
                                int percentageVol = getVolumePercentage();
                                if (percentageVol != mPreviousPercentageVol) {
                                    AvrcpControllerService.sendRegisterAbsVolRspNative(
                                            mRemoteDevice.getBluetoothAddress(),
                                            NOTIFICATION_RSP_TYPE_CHANGED, percentageVol,
                                            mRemoteDevice.getNotificationLabel());
                                    mPreviousPercentageVol = percentageVol;
                                    mRemoteDevice.setAbsVolNotificationRequested(false);
                                }
                            }
                        }
                    }
                    break;

                    case MESSAGE_INTERNAL_ABS_VOL_TIMEOUT:
                        // Volume changed notifications should come back promptly from the
                        // AudioManager, if for some reason some notifications were squashed don't
                        // prevent future notifications.
                        if (DBG) Log.d(TAG, "Timed out on volume changed notification");
                        mVolumeChangedNotificationsToIgnore = 0;
                        break;

                    case MESSAGE_PROCESS_TRACK_CHANGED:
                        // Music start playing automatically and update Metadata
                        mAddressedPlayer.updateCurrentTrack((TrackInfo) msg.obj);
                        broadcastMetaDataChanged(
                                mAddressedPlayer.getCurrentTrack().getMediaMetaData());
                        break;

                    case MESSAGE_PROCESS_PLAY_POS_CHANGED:
                        if (msg.arg2 != -1) {
                            mAddressedPlayer.setPlayTime(msg.arg2);
                            broadcastPlayBackStateChanged(getCurrentPlayBackState());
                        }
                        break;

                    case MESSAGE_PROCESS_PLAY_STATUS_CHANGED:
                        int status = msg.arg1;
                        mAddressedPlayer.setPlayStatus(status);
                        broadcastPlayBackStateChanged(getCurrentPlayBackState());
                        if (status == PlaybackState.STATE_PLAYING) {
                            a2dpSinkService.informTGStatePlaying(mRemoteDevice.mBTDevice, true);
                        } else if (status == PlaybackState.STATE_PAUSED
                                || status == PlaybackState.STATE_STOPPED) {
                            a2dpSinkService.informTGStatePlaying(mRemoteDevice.mBTDevice, false);
                        }
                        break;

                    default:
                        return false;
                }
            }
            return true;
        }
    }

    // Handle the change folder path meta-action.
    // a) Send Change folder command
    // b) Once successful transition to folder fetch state.
    class ChangeFolderPath extends CmdState {
        private static final String STATE_TAG = "AVRCPSM.ChangeFolderPath";
        private int mTmpIncrDirection;
        private String mID = "";

        public void setFolder(String id) {
            mID = id;
        }

        @Override
        public void enter() {
            super.enter();
            mTmpIncrDirection = -1;
        }

        @Override
        public boolean processMessage(Message msg) {
            if (DBG) Log.d(STATE_TAG, "processMessage " + msg.what);
            switch (msg.what) {
                case MESSAGE_INTERNAL_BROWSE_DEPTH_INCREMENT:
                    mTmpIncrDirection = msg.arg1;
                    break;

                case MESSAGE_PROCESS_FOLDER_PATH: {
                    // Fetch the listing of objects in this folder.
                    if (DBG) {
                        Log.d(STATE_TAG,
                                "MESSAGE_PROCESS_FOLDER_PATH returned " + msg.arg1 + " elements");
                    }

                    // Update the folder depth.
                    if (mTmpIncrDirection
                            == AvrcpControllerService.FOLDER_NAVIGATION_DIRECTION_UP) {
                        mBrowseDepth -= 1;
                    } else if (mTmpIncrDirection
                            == AvrcpControllerService.FOLDER_NAVIGATION_DIRECTION_DOWN) {
                        mBrowseDepth += 1;
                    } else {
                        throw new IllegalStateException("incorrect nav " + mTmpIncrDirection);
                    }
                    if (DBG) Log.d(STATE_TAG, "New browse depth " + mBrowseDepth);

                    if (msg.arg1 > 0) {
                        sendMessage(MESSAGE_GET_FOLDER_LIST, 0, msg.arg1 - 1, mID);
                    } else {
                        // Return an empty response to the upper layer.
                        broadcastFolderList(mID, EMPTY_MEDIA_ITEM_LIST);
                    }
                    mBrowseTree.setCurrentBrowsedFolder(mID);
                    transitionTo(mConnected);
                    break;
                }

                case MESSAGE_INTERNAL_CMD_TIMEOUT:
                    // We timed out changing folders. It is imperative we tell
                    // the upper layers that we failed by giving them an empty list.
                    Log.e(STATE_TAG, "change folder failed, sending empty list.");
                    broadcastFolderList(mID, EMPTY_MEDIA_ITEM_LIST);
                    transitionTo(mConnected);
                    break;

                case MESSAGE_SEND_PASS_THROUGH_CMD:
                case MESSAGE_SEND_GROUP_NAVIGATION_CMD:
                case MESSAGE_PROCESS_SET_ABS_VOL_CMD:
                case MESSAGE_PROCESS_REGISTER_ABS_VOL_NOTIFICATION:
                case MESSAGE_PROCESS_TRACK_CHANGED:
                case MESSAGE_PROCESS_PLAY_POS_CHANGED:
                case MESSAGE_PROCESS_PLAY_STATUS_CHANGED:
                case MESSAGE_PROCESS_VOLUME_CHANGED_NOTIFICATION:
                case MESSAGE_STOP_METADATA_BROADCASTS:
                case MESSAGE_START_METADATA_BROADCASTS:
                case MESSAGE_PROCESS_CONNECTION_CHANGE:
                case MESSAGE_PROCESS_BROWSE_CONNECTION_CHANGE:
                    // All of these messages should be handled by parent state immediately.
                    return false;

                default:
                    if (DBG) {
                        Log.d(STATE_TAG, "deferring message " + msg.what + " to Connected state.");
                    }
                    deferMessage(msg);
            }
            return true;
        }
    }

    // Handle the get folder listing action
    // a) Fetch the listing of folders
    // b) Once completed return the object listing
    class GetFolderList extends CmdState {
        private static final String STATE_TAG = "AVRCPSM.GetFolderList";

        String mID = "";
        int mStartInd;
        int mEndInd;
        int mCurrInd;
        int mScope;
        private ArrayList<MediaItem> mFolderList = new ArrayList<>();

        @Override
        public void enter() {
            // Setup the timeouts.
            super.enter();
            mCurrInd = 0;
            mFolderList.clear();
            callNativeFunctionForScope(mStartInd,
                    Math.min(mEndInd, mStartInd + GET_FOLDER_ITEMS_PAGINATION_SIZE - 1));
        }

        public void setScope(int scope) {
            mScope = scope;
        }

        public void setFolder(String id) {
            if (DBG) Log.d(STATE_TAG, "Setting folder to " + id);
            mID = id;
        }

        public void setBounds(int startInd, int endInd) {
            if (DBG) {
                Log.d(STATE_TAG, "startInd " + startInd + " endInd " + endInd);
            }
            mStartInd = startInd;
            mEndInd = Math.min(endInd, MAX_FOLDER_ITEMS);
        }

        @Override
        public boolean processMessage(Message msg) {
            Log.d(STATE_TAG, "processMessage " + msg.what);
            switch (msg.what) {
                case MESSAGE_PROCESS_GET_FOLDER_ITEMS:
                    ArrayList<MediaItem> folderList = (ArrayList<MediaItem>) msg.obj;
                    mFolderList.addAll(folderList);
                    if (DBG) {
                        Log.d(STATE_TAG,
                                "Start " + mStartInd + " End " + mEndInd + " Curr " + mCurrInd
                                        + " received " + folderList.size());
                    }
                    mCurrInd += folderList.size();

                    // Always update the node so that the user does not wait forever
                    // for the list to populate.
                    sendFolderBroadcastAndUpdateNode();

                    if (mCurrInd > mEndInd || folderList.size() == 0) {
                        // If we have fetched all the elements or if the remotes sends us 0 elements
                        // (which can lead us into a loop since mCurrInd does not proceed) we simply
                        // abort.
                        transitionTo(mConnected);
                    } else {
                        // Fetch the next set of items.
                        callNativeFunctionForScope(mCurrInd, Math.min(mEndInd,
                                mCurrInd + GET_FOLDER_ITEMS_PAGINATION_SIZE - 1));
                        // Reset the timeout message since we are doing a new fetch now.
                        removeMessages(MESSAGE_INTERNAL_CMD_TIMEOUT);
                        sendMessageDelayed(MESSAGE_INTERNAL_CMD_TIMEOUT, CMD_TIMEOUT_MILLIS);
                    }
                    break;

                case MESSAGE_INTERNAL_CMD_TIMEOUT:
                    // We have timed out to execute the request, we should simply send
                    // whatever listing we have gotten until now.
                    sendFolderBroadcastAndUpdateNode();
                    transitionTo(mConnected);
                    break;

                case MESSAGE_PROCESS_GET_FOLDER_ITEMS_OUT_OF_RANGE:
                    // If we have gotten an error for OUT OF RANGE we have
                    // already sent all the items to the client hence simply
                    // transition to Connected state here.
                    transitionTo(mConnected);
                    break;

                case MESSAGE_CHANGE_FOLDER_PATH:
                case MESSAGE_FETCH_ATTR_AND_PLAY_ITEM:
                case MESSAGE_GET_PLAYER_LIST:
                case MESSAGE_GET_NOW_PLAYING_LIST:
                case MESSAGE_SET_BROWSED_PLAYER:
                    // A new request has come in, no need to fetch more.
                    mEndInd = 0;
                    deferMessage(msg);
                    break;

                case MESSAGE_SEND_PASS_THROUGH_CMD:
                case MESSAGE_SEND_GROUP_NAVIGATION_CMD:
                case MESSAGE_PROCESS_SET_ABS_VOL_CMD:
                case MESSAGE_PROCESS_REGISTER_ABS_VOL_NOTIFICATION:
                case MESSAGE_PROCESS_TRACK_CHANGED:
                case MESSAGE_PROCESS_PLAY_POS_CHANGED:
                case MESSAGE_PROCESS_PLAY_STATUS_CHANGED:
                case MESSAGE_PROCESS_VOLUME_CHANGED_NOTIFICATION:
                case MESSAGE_STOP_METADATA_BROADCASTS:
                case MESSAGE_START_METADATA_BROADCASTS:
                case MESSAGE_PROCESS_CONNECTION_CHANGE:
                case MESSAGE_PROCESS_BROWSE_CONNECTION_CHANGE:
                    // All of these messages should be handled by parent state immediately.
                    return false;

                default:
                    if (DBG) Log.d(STATE_TAG, "deferring message " + msg.what + " to connected!");
                    deferMessage(msg);
            }
            return true;
        }

        private void sendFolderBroadcastAndUpdateNode() {
            BrowseTree.BrowseNode bn = mBrowseTree.findBrowseNodeByID(mID);
            if (bn == null) {
                Log.e(TAG, "Can not find BrowseNode by ID: " + mID);
                return;
            }
            if (bn.isPlayer()) {
                // Add the now playing folder.
                MediaDescription.Builder mdb = new MediaDescription.Builder();
                mdb.setMediaId(BrowseTree.NOW_PLAYING_PREFIX + ":" + bn.getPlayerID());
                mdb.setTitle(BrowseTree.NOW_PLAYING_PREFIX);
                Bundle mdBundle = new Bundle();
                mdBundle.putString(AvrcpControllerService.MEDIA_ITEM_UID_KEY,
                        BrowseTree.NOW_PLAYING_PREFIX + ":" + bn.getID());
                mdb.setExtras(mdBundle);
                mFolderList.add(new MediaItem(mdb.build(), MediaItem.FLAG_BROWSABLE));
            }
            mBrowseTree.refreshChildren(bn, mFolderList);
            broadcastFolderList(mID, mFolderList);

            // For now playing we need to set the current browsed folder here.
            // For normal folders it is set after ChangeFolderPath.
            if (mScope == AvrcpControllerService.BROWSE_SCOPE_NOW_PLAYING) {
                mBrowseTree.setCurrentBrowsedFolder(mID);
            }
        }

        private void callNativeFunctionForScope(int start, int end) {
            switch (mScope) {
                case AvrcpControllerService.BROWSE_SCOPE_NOW_PLAYING:
                    AvrcpControllerService.getNowPlayingListNative(
                            mRemoteDevice.getBluetoothAddress(), start, end);
                    break;
                case AvrcpControllerService.BROWSE_SCOPE_VFS:
                    AvrcpControllerService.getFolderListNative(mRemoteDevice.getBluetoothAddress(),
                            start, end);
                    break;
                default:
                    Log.e(STATE_TAG, "Scope " + mScope + " cannot be handled here.");
            }
        }
    }

    // Handle the get player listing action
    // a) Fetch the listing of players
    // b) Once completed return the object listing
    class GetPlayerListing extends CmdState {
        private static final String STATE_TAG = "AVRCPSM.GetPlayerList";

        @Override
        public boolean processMessage(Message msg) {
            if (DBG) Log.d(STATE_TAG, "processMessage " + msg.what);
            switch (msg.what) {
                case MESSAGE_PROCESS_GET_PLAYER_ITEMS:
                    List<AvrcpPlayer> playerList = (List<AvrcpPlayer>) msg.obj;
                    mBrowseTree.refreshChildren(BrowseTree.ROOT, playerList);
                    ArrayList<MediaItem> mediaItemList = new ArrayList<>();
                    for (BrowseTree.BrowseNode c : mBrowseTree.findBrowseNodeByID(BrowseTree.ROOT)
                            .getChildren()) {
                        mediaItemList.add(c.getMediaItem());
                    }
                    broadcastFolderList(BrowseTree.ROOT, mediaItemList);
                    mBrowseTree.setCurrentBrowsedFolder(BrowseTree.ROOT);
                    transitionTo(mConnected);
                    break;

                case MESSAGE_INTERNAL_CMD_TIMEOUT:
                    // We have timed out to execute the request.
                    // Send an empty list here.
                    broadcastFolderList(BrowseTree.ROOT, EMPTY_MEDIA_ITEM_LIST);
                    transitionTo(mConnected);
                    break;

                case MESSAGE_SEND_PASS_THROUGH_CMD:
                case MESSAGE_SEND_GROUP_NAVIGATION_CMD:
                case MESSAGE_PROCESS_SET_ABS_VOL_CMD:
                case MESSAGE_PROCESS_REGISTER_ABS_VOL_NOTIFICATION:
                case MESSAGE_PROCESS_TRACK_CHANGED:
                case MESSAGE_PROCESS_PLAY_POS_CHANGED:
                case MESSAGE_PROCESS_PLAY_STATUS_CHANGED:
                case MESSAGE_PROCESS_VOLUME_CHANGED_NOTIFICATION:
                case MESSAGE_STOP_METADATA_BROADCASTS:
                case MESSAGE_START_METADATA_BROADCASTS:
                case MESSAGE_PROCESS_CONNECTION_CHANGE:
                case MESSAGE_PROCESS_BROWSE_CONNECTION_CHANGE:
                    // All of these messages should be handled by parent state immediately.
                    return false;

                default:
                    if (DBG) Log.d(STATE_TAG, "deferring message " + msg.what + " to connected!");
                    deferMessage(msg);
            }
            return true;
        }
    }

    class MoveToRoot extends CmdState {
        private static final String STATE_TAG = "AVRCPSM.MoveToRoot";
        private String mID = "";

        public void setFolder(String id) {
            if (DBG) Log.d(STATE_TAG, "setFolder " + id);
            mID = id;
        }

        @Override
        public void enter() {
            // Setup the timeouts.
            super.enter();

            // We need to move mBrowseDepth levels up. The following message is
            // completely internal to this state.
            sendMessage(MESSAGE_INTERNAL_MOVE_N_LEVELS_UP);
        }

        @Override
        public boolean processMessage(Message msg) {
            if (DBG) {
                Log.d(STATE_TAG, "processMessage " + msg.what + " browse depth " + mBrowseDepth);
            }
            switch (msg.what) {
                case MESSAGE_INTERNAL_MOVE_N_LEVELS_UP:
                    if (mBrowseDepth == 0) {
                        Log.w(STATE_TAG, "Already in root!");
                        transitionTo(mConnected);
                        sendMessage(MESSAGE_GET_FOLDER_LIST, 0, 0xff, mID);
                    } else {
                        AvrcpControllerService.changeFolderPathNative(
                                mRemoteDevice.getBluetoothAddress(),
                                (byte) AvrcpControllerService.FOLDER_NAVIGATION_DIRECTION_UP,
                                AvrcpControllerService.hexStringToByteUID(null));
                    }
                    break;

                case MESSAGE_PROCESS_FOLDER_PATH:
                    mBrowseDepth -= 1;
                    if (DBG) Log.d(STATE_TAG, "New browse depth " + mBrowseDepth);
                    if (mBrowseDepth < 0) {
                        throw new IllegalArgumentException("Browse depth negative!");
                    }

                    sendMessage(MESSAGE_INTERNAL_MOVE_N_LEVELS_UP);
                    break;

                case MESSAGE_INTERNAL_CMD_TIMEOUT:
                    broadcastFolderList(BrowseTree.ROOT, EMPTY_MEDIA_ITEM_LIST);
                    transitionTo(mConnected);
                    break;

                case MESSAGE_SEND_PASS_THROUGH_CMD:
                case MESSAGE_SEND_GROUP_NAVIGATION_CMD:
                case MESSAGE_PROCESS_SET_ABS_VOL_CMD:
                case MESSAGE_PROCESS_REGISTER_ABS_VOL_NOTIFICATION:
                case MESSAGE_PROCESS_TRACK_CHANGED:
                case MESSAGE_PROCESS_PLAY_POS_CHANGED:
                case MESSAGE_PROCESS_PLAY_STATUS_CHANGED:
                case MESSAGE_PROCESS_VOLUME_CHANGED_NOTIFICATION:
                case MESSAGE_STOP_METADATA_BROADCASTS:
                case MESSAGE_START_METADATA_BROADCASTS:
                case MESSAGE_PROCESS_CONNECTION_CHANGE:
                case MESSAGE_PROCESS_BROWSE_CONNECTION_CHANGE:
                    // All of these messages should be handled by parent state immediately.
                    return false;

                default:
                    if (DBG) Log.d(STATE_TAG, "deferring message " + msg.what + " to connected!");
                    deferMessage(msg);
            }
            return true;
        }
    }

    class SetBrowsedPlayer extends CmdState {
        private static final String STATE_TAG = "AVRCPSM.SetBrowsedPlayer";
        String mID = "";

        public void setFolder(String id) {
            mID = id;
        }

        @Override
        public boolean processMessage(Message msg) {
            if (DBG) Log.d(STATE_TAG, "processMessage " + msg.what);
            switch (msg.what) {
                case MESSAGE_PROCESS_SET_BROWSED_PLAYER:
                    // Set the new depth.
                    if (DBG) Log.d(STATE_TAG, "player depth " + msg.arg2);
                    mBrowseDepth = msg.arg2;

                    // If we already on top of player and there is no content.
                    // This should very rarely happen.
                    if (mBrowseDepth == 0 && msg.arg1 == 0) {
                        broadcastFolderList(mID, EMPTY_MEDIA_ITEM_LIST);
                        transitionTo(mConnected);
                    } else {
                        // Otherwise move to root and fetch the listing.
                        // the MoveToRoot#enter() function takes care of fetch.
                        mMoveToRoot.setFolder(mID);
                        transitionTo(mMoveToRoot);
                    }
                    mBrowseTree.setCurrentBrowsedFolder(mID);
                    // Also set the browsed player here.
                    mBrowseTree.setCurrentBrowsedPlayer(mID);
                    break;

                case MESSAGE_INTERNAL_CMD_TIMEOUT:
                    broadcastFolderList(mID, EMPTY_MEDIA_ITEM_LIST);
                    transitionTo(mConnected);
                    break;

                case MESSAGE_SEND_PASS_THROUGH_CMD:
                case MESSAGE_SEND_GROUP_NAVIGATION_CMD:
                case MESSAGE_PROCESS_SET_ABS_VOL_CMD:
                case MESSAGE_PROCESS_REGISTER_ABS_VOL_NOTIFICATION:
                case MESSAGE_PROCESS_TRACK_CHANGED:
                case MESSAGE_PROCESS_PLAY_POS_CHANGED:
                case MESSAGE_PROCESS_PLAY_STATUS_CHANGED:
                case MESSAGE_PROCESS_VOLUME_CHANGED_NOTIFICATION:
                case MESSAGE_STOP_METADATA_BROADCASTS:
                case MESSAGE_START_METADATA_BROADCASTS:
                case MESSAGE_PROCESS_CONNECTION_CHANGE:
                case MESSAGE_PROCESS_BROWSE_CONNECTION_CHANGE:
                    // All of these messages should be handled by parent state immediately.
                    return false;

                default:
                    if (DBG) Log.d(STATE_TAG, "deferring message " + msg.what + " to connected!");
                    deferMessage(msg);
            }
            return true;
        }
    }

    class SetAddresedPlayerAndPlayItem extends CmdState {
        private static final String STATE_TAG = "AVRCPSM.SetAddresedPlayerAndPlayItem";
        int mScope;
        String mPlayItemId;
        String mAddrPlayerId;

        public void setItemAndScope(String addrPlayerId, String playItemId, int scope) {
            mAddrPlayerId = addrPlayerId;
            mPlayItemId = playItemId;
            mScope = scope;
        }

        @Override
        public boolean processMessage(Message msg) {
            if (DBG) Log.d(STATE_TAG, "processMessage " + msg.what);
            switch (msg.what) {
                case MESSAGE_PROCESS_SET_ADDRESSED_PLAYER:
                    // Set the new addressed player.
                    mBrowseTree.setCurrentAddressedPlayer(mAddrPlayerId);

                    // And now play the item.
                    AvrcpControllerService.playItemNative(mRemoteDevice.getBluetoothAddress(),
                            (byte) mScope, AvrcpControllerService.hexStringToByteUID(mPlayItemId),
                            (int) 0);

                    // Transition to connected state here.
                    transitionTo(mConnected);
                    break;

                case MESSAGE_INTERNAL_CMD_TIMEOUT:
                    transitionTo(mConnected);
                    break;

                case MESSAGE_SEND_PASS_THROUGH_CMD:
                case MESSAGE_SEND_GROUP_NAVIGATION_CMD:
                case MESSAGE_PROCESS_SET_ABS_VOL_CMD:
                case MESSAGE_PROCESS_REGISTER_ABS_VOL_NOTIFICATION:
                case MESSAGE_PROCESS_TRACK_CHANGED:
                case MESSAGE_PROCESS_PLAY_POS_CHANGED:
                case MESSAGE_PROCESS_PLAY_STATUS_CHANGED:
                case MESSAGE_PROCESS_VOLUME_CHANGED_NOTIFICATION:
                case MESSAGE_STOP_METADATA_BROADCASTS:
                case MESSAGE_START_METADATA_BROADCASTS:
                case MESSAGE_PROCESS_CONNECTION_CHANGE:
                case MESSAGE_PROCESS_BROWSE_CONNECTION_CHANGE:
                    // All of these messages should be handled by parent state immediately.
                    return false;

                default:
                    if (DBG) Log.d(STATE_TAG, "deferring message " + msg.what + " to connected!");
                    deferMessage(msg);
            }
            return true;
        }
    }

    // Class template for commands. Each state should do the following:
    // (a) In enter() send a timeout message which could be tracked in the
    // processMessage() stage.
    // (b) In exit() remove all the timeouts.
    //
    // Essentially the lifecycle of a timeout should be bounded to a CmdState always.
    abstract class CmdState extends State {
        @Override
        public void enter() {
            sendMessageDelayed(MESSAGE_INTERNAL_CMD_TIMEOUT, CMD_TIMEOUT_MILLIS);
        }

        @Override
        public void exit() {
            removeMessages(MESSAGE_INTERNAL_CMD_TIMEOUT);
        }
    }

    // Interface APIs
    boolean isConnected() {
        synchronized (mLock) {
            return mIsConnected;
        }
    }

    void doQuit() {
        try {
            mContext.unregisterReceiver(mBroadcastReceiver);
        } catch (IllegalArgumentException expected) {
            // If the receiver was never registered unregister will throw an
            // IllegalArgumentException.
        }
        quit();
    }

    void dump(StringBuilder sb) {
        ProfileService.println(sb, "StateMachine: " + this.toString());
    }

    MediaMetadata getCurrentMetaData() {
        synchronized (mLock) {
            if (mAddressedPlayer != null && mAddressedPlayer.getCurrentTrack() != null) {
                MediaMetadata mmd = mAddressedPlayer.getCurrentTrack().getMediaMetaData();
                if (DBG) {
                    Log.d(TAG, "getCurrentMetaData mmd " + mmd);
                }
            }
            return EMPTY_MEDIA_METADATA;
        }
    }

    PlaybackState getCurrentPlayBackState() {
        return getCurrentPlayBackState(true);
    }

    PlaybackState getCurrentPlayBackState(boolean cached) {
        if (cached) {
            synchronized (mLock) {
                if (mAddressedPlayer == null) {
                    return new PlaybackState.Builder().setState(PlaybackState.STATE_ERROR,
                            PlaybackState.PLAYBACK_POSITION_UNKNOWN, 0).build();
                }
                return mAddressedPlayer.getPlaybackState();
            }
        } else {
            // Issue a native request, we return NULL since this is only for PTS.
            AvrcpControllerService.getPlaybackStateNative(mRemoteDevice.getBluetoothAddress());
            return null;
        }
    }

    // Entry point to the state machine where the services should call to fetch children
    // for a specific node. It checks if the currently browsed node is the same as the one being
    // asked for, in that case it returns the currently cached children. This saves bandwidth and
    // also if we are already fetching elements for a current folder (since we need to batch
    // fetches) then we should not submit another request but simply return what we have fetched
    // until now.
    //
    // It handles fetches to all VFS, Now Playing and Media Player lists.
    void getChildren(String parentMediaId, int start, int items) {
        BrowseTree.BrowseNode bn = mBrowseTree.findBrowseNodeByID(parentMediaId);
        if (bn == null) {
            Log.e(TAG, "Invalid folder to browse " + mBrowseTree);
            broadcastFolderList(parentMediaId, EMPTY_MEDIA_ITEM_LIST);
            return;
        }

        if (DBG) {
            Log.d(TAG, "To Browse folder " + bn + " is cached " + bn.isCached() + " current folder "
                    + mBrowseTree.getCurrentBrowsedFolder());
        }
        if (bn.equals(mBrowseTree.getCurrentBrowsedFolder()) && bn.isCached()) {
            if (DBG) {
                Log.d(TAG, "Same cached folder -- returning existing children.");
            }
            BrowseTree.BrowseNode n = mBrowseTree.findBrowseNodeByID(parentMediaId);
            ArrayList<MediaItem> childrenList = new ArrayList<MediaItem>();
            for (BrowseTree.BrowseNode cn : n.getChildren()) {
                childrenList.add(cn.getMediaItem());
            }
            broadcastFolderList(parentMediaId, childrenList);
            return;
        }

        Message msg = null;
        int btDirection = mBrowseTree.getDirection(parentMediaId);
        BrowseTree.BrowseNode currFol = mBrowseTree.getCurrentBrowsedFolder();
        if (DBG) {
            Log.d(TAG, "Browse direction parent " + mBrowseTree.getCurrentBrowsedFolder() + " req "
                    + parentMediaId + " direction " + btDirection);
        }
        if (BrowseTree.ROOT.equals(parentMediaId)) {
            // Root contains the list of players.
            msg = obtainMessage(AvrcpControllerStateMachine.MESSAGE_GET_PLAYER_LIST, start, items);
        } else if (bn.isPlayer() && btDirection != BrowseTree.DIRECTION_SAME) {
            // Set browsed (and addressed player) as the new player.
            // This should fetch the list of folders.
            msg = obtainMessage(AvrcpControllerStateMachine.MESSAGE_SET_BROWSED_PLAYER,
                    bn.getPlayerID(), 0, bn.getID());
        } else if (bn.isNowPlaying()) {
            // Issue a request to fetch the items.
            msg = obtainMessage(AvrcpControllerStateMachine.MESSAGE_GET_NOW_PLAYING_LIST, start,
                    items, parentMediaId);
        } else {
            // Only change folder if desired. If an app refreshes a folder
            // (because it resumed etc) and current folder does not change
            // then we can simply fetch list.

            // We exempt two conditions from change folder:
            // a) If the new folder is the same as current folder (refresh of UI)
            // b) If the new folder is ROOT and current folder is NOW_PLAYING (or vice-versa)
            // In this condition we 'fake' child-parent hierarchy but it does not exist in
            // bluetooth world.
            boolean isNowPlayingToRoot =
                    currFol.isNowPlaying() && bn.getID().equals(BrowseTree.ROOT);
            if (!isNowPlayingToRoot) {
                // Find the direction of traversal.
                int direction = -1;
                if (DBG) Log.d(TAG, "Browse direction " + currFol + " " + bn + " = " + btDirection);
                if (btDirection == BrowseTree.DIRECTION_UNKNOWN) {
                    Log.w(TAG, "parent " + bn + " is not a direct "
                            + "successor or predeccessor of current folder " + currFol);
                    broadcastFolderList(parentMediaId, EMPTY_MEDIA_ITEM_LIST);
                    return;
                }

                if (btDirection == BrowseTree.DIRECTION_DOWN) {
                    direction = AvrcpControllerService.FOLDER_NAVIGATION_DIRECTION_DOWN;
                } else if (btDirection == BrowseTree.DIRECTION_UP) {
                    direction = AvrcpControllerService.FOLDER_NAVIGATION_DIRECTION_UP;
                }

                Bundle b = new Bundle();
                b.putString(AvrcpControllerService.EXTRA_FOLDER_ID, bn.getID());
                b.putString(AvrcpControllerService.EXTRA_FOLDER_BT_ID, bn.getFolderUID());
                msg = obtainMessage(AvrcpControllerStateMachine.MESSAGE_CHANGE_FOLDER_PATH,
                        direction, 0, b);
            } else {
                // Fetch the listing without changing paths.
                msg = obtainMessage(AvrcpControllerStateMachine.MESSAGE_GET_FOLDER_LIST, start,
                        items, bn.getFolderUID());
            }
        }

        if (msg != null) {
            sendMessage(msg);
        }
    }

    public void fetchAttrAndPlayItem(String uid) {
        BrowseTree.BrowseNode currItem = mBrowseTree.findFolderByIDLocked(uid);
        BrowseTree.BrowseNode currFolder = mBrowseTree.getCurrentBrowsedFolder();
        if (DBG) Log.d(TAG, "fetchAttrAndPlayItem mediaId=" + uid + " node=" + currItem);
        if (currItem != null) {
            int scope = currFolder.isNowPlaying() ? AvrcpControllerService.BROWSE_SCOPE_NOW_PLAYING
                    : AvrcpControllerService.BROWSE_SCOPE_VFS;
            Message msg =
                    obtainMessage(AvrcpControllerStateMachine.MESSAGE_FETCH_ATTR_AND_PLAY_ITEM,
                            scope, 0, currItem.getFolderUID());
            sendMessage(msg);
        }
    }

    private void broadcastMetaDataChanged(MediaMetadata metadata) {
        Intent intent = new Intent(AvrcpControllerService.ACTION_TRACK_EVENT);
        intent.putExtra(AvrcpControllerService.EXTRA_METADATA, metadata);
        if (VDBG) {
            Log.d(TAG, " broadcastMetaDataChanged = " + metadata.getDescription());
        }
        mContext.sendBroadcast(intent, ProfileService.BLUETOOTH_PERM);
    }

    private void broadcastFolderList(String id, ArrayList<MediaItem> items) {
        Intent intent = new Intent(AvrcpControllerService.ACTION_FOLDER_LIST);
        if (VDBG) Log.d(TAG, "broadcastFolderList id " + id + " items " + items);
        intent.putExtra(AvrcpControllerService.EXTRA_FOLDER_ID, id);
        intent.putParcelableArrayListExtra(AvrcpControllerService.EXTRA_FOLDER_LIST, items);
        mContext.sendBroadcast(intent, ProfileService.BLUETOOTH_PERM);
    }

    private void broadcastPlayBackStateChanged(PlaybackState state) {
        Intent intent = new Intent(AvrcpControllerService.ACTION_TRACK_EVENT);
        intent.putExtra(AvrcpControllerService.EXTRA_PLAYBACK, state);
        if (DBG) {
            Log.d(TAG, " broadcastPlayBackStateChanged = " + state.toString());
        }
        mContext.sendBroadcast(intent, ProfileService.BLUETOOTH_PERM);
    }

    private void setAbsVolume(int absVol, int label) {
        int maxVolume = mAudioManager.getStreamMaxVolume(AudioManager.STREAM_MUSIC);
        int currIndex = mAudioManager.getStreamVolume(AudioManager.STREAM_MUSIC);
        // Ignore first volume command since phone may not know difference between stream volume
        // and amplifier volume.
        if (mRemoteDevice.getFirstAbsVolCmdRecvd()) {
            int newIndex = (maxVolume * absVol) / ABS_VOL_BASE;
            if (DBG) {
                Log.d(TAG, " setAbsVolume =" + absVol + " maxVol = " + maxVolume
                        + " cur = " + currIndex + " new = " + newIndex);
            }
            /*
             * In some cases change in percentage is not sufficient enough to warrant
             * change in index values which are in range of 0-15. For such cases
             * no action is required
             */
            if (newIndex != currIndex) {
                mAudioManager.setStreamVolume(AudioManager.STREAM_MUSIC, newIndex,
                        AudioManager.FLAG_SHOW_UI);
            }
        } else {
            mRemoteDevice.setFirstAbsVolCmdRecvd();
            absVol = (currIndex * ABS_VOL_BASE) / maxVolume;
            if (DBG) Log.d(TAG, " SetAbsVol recvd for first time, respond with " + absVol);
        }
        AvrcpControllerService.sendAbsVolRspNative(mRemoteDevice.getBluetoothAddress(), absVol,
                label);
    }

    private int getVolumePercentage() {
        int maxVolume = mAudioManager.getStreamMaxVolume(AudioManager.STREAM_MUSIC);
        int currIndex = mAudioManager.getStreamVolume(AudioManager.STREAM_MUSIC);
        int percentageVol = ((currIndex * ABS_VOL_BASE) / maxVolume);
        return percentageVol;
    }

    private final BroadcastReceiver mBroadcastReceiver = new BroadcastReceiver() {
        @Override
        public void onReceive(Context context, Intent intent) {
            String action = intent.getAction();
            if (action.equals(AudioManager.VOLUME_CHANGED_ACTION)) {
                int streamType = intent.getIntExtra(AudioManager.EXTRA_VOLUME_STREAM_TYPE, -1);
                if (streamType == AudioManager.STREAM_MUSIC) {
                    sendMessage(MESSAGE_PROCESS_VOLUME_CHANGED_NOTIFICATION);
                }
            }
        }
    };

    public static String dumpMessageString(int message) {
        String str = "UNKNOWN";
        switch (message) {
            case MESSAGE_SEND_PASS_THROUGH_CMD:
                str = "REQ_PASS_THROUGH_CMD";
                break;
            case MESSAGE_SEND_GROUP_NAVIGATION_CMD:
                str = "REQ_GRP_NAV_CMD";
                break;
            case MESSAGE_PROCESS_SET_ABS_VOL_CMD:
                str = "CB_SET_ABS_VOL_CMD";
                break;
            case MESSAGE_PROCESS_REGISTER_ABS_VOL_NOTIFICATION:
                str = "CB_REGISTER_ABS_VOL";
                break;
            case MESSAGE_PROCESS_TRACK_CHANGED:
                str = "CB_TRACK_CHANGED";
                break;
            case MESSAGE_PROCESS_PLAY_POS_CHANGED:
                str = "CB_PLAY_POS_CHANGED";
                break;
            case MESSAGE_PROCESS_PLAY_STATUS_CHANGED:
                str = "CB_PLAY_STATUS_CHANGED";
                break;
            case MESSAGE_PROCESS_RC_FEATURES:
                str = "CB_RC_FEATURES";
                break;
            case MESSAGE_PROCESS_CONNECTION_CHANGE:
                str = "CB_CONN_CHANGED";
                break;
            default:
                str = Integer.toString(message);
                break;
        }
        return str;
    }

    public static String displayBluetoothAvrcpSettings(BluetoothAvrcpPlayerSettings mSett) {
        StringBuffer sb = new StringBuffer();
        int supportedSetting = mSett.getSettings();
        if (VDBG) {
            Log.d(TAG, " setting: " + supportedSetting);
        }
        if ((supportedSetting & BluetoothAvrcpPlayerSettings.SETTING_EQUALIZER) != 0) {
            sb.append(" EQ : ");
            sb.append(Integer.toString(mSett.getSettingValue(
                    BluetoothAvrcpPlayerSettings.SETTING_EQUALIZER)));
        }
        if ((supportedSetting & BluetoothAvrcpPlayerSettings.SETTING_REPEAT) != 0) {
            sb.append(" REPEAT : ");
            sb.append(Integer.toString(mSett.getSettingValue(
                    BluetoothAvrcpPlayerSettings.SETTING_REPEAT)));
        }
        if ((supportedSetting & BluetoothAvrcpPlayerSettings.SETTING_SHUFFLE) != 0) {
            sb.append(" SHUFFLE : ");
            sb.append(Integer.toString(mSett.getSettingValue(
                    BluetoothAvrcpPlayerSettings.SETTING_SHUFFLE)));
        }
        if ((supportedSetting & BluetoothAvrcpPlayerSettings.SETTING_SCAN) != 0) {
            sb.append(" SCAN : ");
            sb.append(Integer.toString(mSett.getSettingValue(
                    BluetoothAvrcpPlayerSettings.SETTING_SCAN)));
        }
        return sb.toString();
    }
}
