<commit_before>/*
 * Copyright (c) 2009 Google Inc.  All Rights Reserved.
 *
 * 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 uk.org.ngo.squeezer.service;

import android.annotation.TargetApi;
import android.app.Notification;
import android.app.PendingIntent;
import android.app.Service;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.media.AudioManager;
import android.media.MediaMetadata;
import android.net.Uri;
import android.net.wifi.WifiManager;
import android.os.Binder;
import android.os.Build;
import android.os.IBinder;
import android.os.PowerManager;

import androidx.annotation.DrawableRes;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import androidx.core.app.NotificationCompat;
import androidx.core.app.NotificationManagerCompat;
import androidx.media.VolumeProviderCompat;

import android.support.v4.media.MediaMetadataCompat;
import android.support.v4.media.session.MediaSessionCompat;
import android.support.v4.media.session.PlaybackStateCompat;
import android.telephony.PhoneStateListener;
import android.telephony.TelephonyManager;
import android.util.Log;
import android.widget.RemoteViews;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

import uk.org.ngo.squeezer.NowPlayingActivity;
import uk.org.ngo.squeezer.Preferences;
import uk.org.ngo.squeezer.R;
import uk.org.ngo.squeezer.Util;
import uk.org.ngo.squeezer.download.DownloadDatabase;
import uk.org.ngo.squeezer.model.Action;
import uk.org.ngo.squeezer.model.JiveItem;
import uk.org.ngo.squeezer.model.MusicFolderItem;
import uk.org.ngo.squeezer.model.SlimCommand;
import uk.org.ngo.squeezer.itemlist.IServiceItemListCallback;
import uk.org.ngo.squeezer.model.Alarm;
import uk.org.ngo.squeezer.model.AlarmPlaylist;
import uk.org.ngo.squeezer.model.CurrentPlaylistItem;
import uk.org.ngo.squeezer.model.Player;
import uk.org.ngo.squeezer.model.PlayerState;
import uk.org.ngo.squeezer.model.Song;
import uk.org.ngo.squeezer.service.event.ConnectionChanged;
import uk.org.ngo.squeezer.service.event.HandshakeComplete;
import uk.org.ngo.squeezer.service.event.MusicChanged;
import uk.org.ngo.squeezer.service.event.PlayStatusChanged;
import uk.org.ngo.squeezer.service.event.PlayerStateChanged;
import uk.org.ngo.squeezer.service.event.PlayerVolume;
import uk.org.ngo.squeezer.service.event.PlayersChanged;
import uk.org.ngo.squeezer.util.ImageFetcher;
import uk.org.ngo.squeezer.util.NotificationUtil;
import uk.org.ngo.squeezer.util.Scrobble;

/**
 * Persistent service which acts as an interface to for activities to communicate with LMS.
 * <p>
 * The interface is documented here {@link ISqueezeService}
 * <p>
 * The service lifecycle is managed as both a bound and a started servic. as follows.
 * <ul>
 *     <li>On connect to LMS call Context.start[Foreground]Service and Service.startForeground</li>
 *     <li>On disconnect from LMS call Service.stopForeground and Service.stopSelf</li>
 *     <li>bind to the SqueezeService in activities in onCreate</li>
 *     <li>unbind the SqueezeService in activities  onDestroy</li>
 * </ul>
 * This means the service will as long as there is a Squeezer or we are connected to LMS activity.
 * When we are connected to LMS it runs as a foreground service and a notification is displayed.
 */
public class SqueezeService extends Service {

    private static final String TAG = "SqueezeService";

    public static final String NOTIFICATION_CHANNEL_ID = "channel_squeezer_1";
    private static final int PLAYBACKSERVICE_STATUS = 1;
    public static final int DOWNLOAD_ERROR = 2;

    /** Service-specific eventbus. All events generated by the service will be sent here. */
    private final EventBus mEventBus = new EventBus();

    /** True if the handshake with the server has completed, otherwise false. */
    private volatile boolean mHandshakeComplete = false;

    /** Media session to associate with ongoing notifications. */
    private MediaSessionCompat mMediaSession;

    /** Are the service currently in the foregrund */
    private volatile boolean foreGround;

    /** The most recent notifcation. */
    private NotificationState ongoingNotification;

    private final SlimDelegate mDelegate = new SlimDelegate(mEventBus);

    /**
     * Is scrobbling enabled?
     */
    private boolean scrobblingEnabled;

    /**
     * Was scrobbling enabled?
     */
    private boolean scrobblingPreviouslyEnabled;

    int mFadeInSecs;

    private static final String ACTION_NEXT_TRACK = "uk.org.ngo.squeezer.service.ACTION_NEXT_TRACK";
    private static final String ACTION_PREV_TRACK = "uk.org.ngo.squeezer.service.ACTION_PREV_TRACK";
    private static final String ACTION_PLAY = "uk.org.ngo.squeezer.service.ACTION_PLAY";
    private static final String ACTION_PAUSE = "uk.org.ngo.squeezer.service.ACTION_PAUSE";
    private static final String ACTION_CLOSE = "uk.org.ngo.squeezer.service.ACTION_CLOSE";

    private final BroadcastReceiver deviceIdleModeReceiver = new BroadcastReceiver() {
        @Override
        @RequiresApi(api = Build.VERSION_CODES.M)
        public void onReceive(Context context, Intent intent) {
            // On M and above going in to Doze mode suspends the network but does not shut down
            // existing network connections or cause them to generate exceptions. Explicitly
            // disconnect here, so that resuming from Doze mode forces a reconnect. See
            // https://github.com/nikclayton/android-squeezer/issues/177.
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
                PowerManager pm = (PowerManager) context.getSystemService(Context.POWER_SERVICE);

                if (pm.isDeviceIdleMode()) {
                    Log.d(TAG, "Entering doze mode, disconnecting");
                    disconnect(false);
                }
            }
        }
    };

    private MyVolumeProvider mVolumeProvider;

    /**
     * Thrown when the service is asked to send a command to the server before the server
     * handshake completes.
     */
    public static class HandshakeNotCompleteException extends IllegalStateException {
        public HandshakeNotCompleteException() {
            super();
        }

        public HandshakeNotCompleteException(String message) {
            super(message);
        }

        public HandshakeNotCompleteException(String message, Throwable cause) {
            super(message, cause);
        }

        public HandshakeNotCompleteException(Throwable cause) {
            super(cause);
        }
    }

    @Override
    public void onCreate() {
        super.onCreate();

        // Clear leftover notification in case this service previously got killed while playing
        NotificationManagerCompat nm = NotificationManagerCompat.from(this);
        nm.cancel(PLAYBACKSERVICE_STATUS);

        cachePreferences();

        WifiManager wifiManager = (WifiManager) getApplicationContext().getSystemService(Context.WIFI_SERVICE);
        this.wifiLock = wifiManager.createWifiLock(WifiManager.WIFI_MODE_FULL, "Squeezer_WifiLock");

        mEventBus.register(this, 1);  // Get events before other subscribers

        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
            registerReceiver(deviceIdleModeReceiver, new IntentFilter(
                    PowerManager.ACTION_DEVICE_IDLE_MODE_CHANGED));
        }
    }

    @Override
    public int onStartCommand(Intent intent, int flags, int startId) {
        try{
            if(intent != null && intent.getAction()!= null ) {
                switch (intent.getAction()) {
                    case ACTION_NEXT_TRACK:
                        squeezeService.nextTrack();
                        break;
                    case ACTION_PREV_TRACK:
                        squeezeService.previousTrack();
                        break;
                    case ACTION_PLAY:
                        squeezeService.play();
                        break;
                    case ACTION_PAUSE:
                        squeezeService.pause();
                        break;
                    case ACTION_CLOSE:
                        squeezeService.disconnect();
                        break;
                }
            }
        } catch(Exception e) {
            Log.w(TAG, "Error executing intent: ", e);
        }
        return START_STICKY;
    }

    /**
     * Cache the value of various preferences.
     */
    private void cachePreferences() {
        final Preferences preferences = new Preferences(this);
        scrobblingEnabled = preferences.isScrobbleEnabled();
        mFadeInSecs = preferences.getFadeInSecs();
        mVolumeProvider = new MyVolumeProvider(preferences.getVolumeIncrements());
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
            if (squeezeService.isConnected()) {
                if (preferences.isBackgroundVolume()) {
                    mMediaSession.setPlaybackToRemote(mVolumeProvider);
                } else {
                    mMediaSession.setPlaybackToLocal(AudioManager.STREAM_MUSIC);
                }
            }
        }
    }

    @Override
    public IBinder onBind(Intent intent) {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
            mMediaSession = new MediaSessionCompat(getApplicationContext(), "squeezer");
        }
        return (IBinder) squeezeService;
    }

    @Override
    public boolean onUnbind(Intent intent) {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
            if (mMediaSession != null) {
                mMediaSession.release();
            }
        }
        return super.onUnbind(intent);
    }

    @Override
    public void onDestroy() {
        super.onDestroy();
        disconnect(false);
        mEventBus.unregister(this);

        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
            try {
                unregisterReceiver(deviceIdleModeReceiver);
            } catch (IllegalArgumentException e) {
                // Do nothing. This can occur in testing when we destroy the service before the
                // receiver is registered.
            }
        }
    }

    @Override
    public void onTaskRemoved(Intent rootIntent) {
        disconnect(false);
        super.onTaskRemoved(rootIntent);
    }

    void disconnect(boolean fromUser) {
        mDelegate.disconnect(fromUser);
    }

    @Nullable public PlayerState getActivePlayerState() {
        Player activePlayer = mDelegate.getActivePlayer();
        return activePlayer == null ? null : activePlayer.getPlayerState();

    }

    /**
     * Updates the playing status of the current player.
     * <p>
     * Updates the Wi-Fi lock and ongoing status notification as necessary.
     */
    public void onEvent(PlayStatusChanged event) {
        if (event.player.equals(mDelegate.getActivePlayer())) {
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
                int state = PlayerState.PLAY_STATE_PLAY.equals(event.playStatus) ? PlaybackStateCompat.STATE_PLAYING : PlaybackStateCompat.STATE_STOPPED;
                mMediaSession.setPlaybackState(new PlaybackStateCompat.Builder().setState(state, 0, 0).build());
            }
            updateOngoingNotification();
        }
    }

    /**
     * Change the player that is controlled by Squeezer (the "active" player).
     *
     * @param newActivePlayer The new active player. May be null, in which case no players
     *     are controlled.
     */
    private void changeActivePlayer(@Nullable final Player newActivePlayer) {
        Player prevActivePlayer = mDelegate.getActivePlayer();

        // Do nothing if the player hasn't actually changed.
        if (prevActivePlayer == newActivePlayer) {
            return;
        }

        Log.i(TAG, "Active player now: " + newActivePlayer);
        mDelegate.setActivePlayer(newActivePlayer);

        if (prevActivePlayer != null) {
            mDelegate.subscribeDisplayStatus(prevActivePlayer, false);
            mDelegate.subscribeMenuStatus(prevActivePlayer, false);
        }

        updateAllPlayerSubscriptionStates();
        requestPlayerData();
        new Preferences(this).setLastPlayer(newActivePlayer);
    }

    private void requestPlayerData() {
        Player activePlayer = mDelegate.getActivePlayer();

        if (activePlayer != null) {
            mDelegate.subscribeDisplayStatus(activePlayer, true);
            mDelegate.subscribeMenuStatus(activePlayer, true);
            mDelegate.requestPlayerStatus(activePlayer);

            // Start an asynchronous fetch of the squeezeservers "home menu" items
            // See http://wiki.slimdevices.com/index.php/SqueezePlayAndSqueezeCenterPlugins
            mDelegate.requestItems(activePlayer, 0, new IServiceItemListCallback<JiveItem>() {
                private final List<JiveItem> homeMenu = new ArrayList<>();

                @Override
                public void onItemsReceived(int count, int start, Map<String, Object> parameters, List<JiveItem> items, Class<JiveItem> dataType) {
                    homeMenu.addAll(items);
                    if (homeMenu.size() == count) {
                        Preferences preferences = new Preferences(SqueezeService.this);
                        boolean useArchive = preferences.getCustomizeHomeMenuMode() != Preferences.CustomizeHomeMenuMode.DISABLED;
                        List<String> archivedMenuItems = useArchive ? preferences.getArchivedMenuItems(activePlayer) : Collections.emptyList();
                        mDelegate.setHomeMenu(homeMenu, archivedMenuItems);
                    }
                }
                @Override
                public Object getClient() {
                    return SqueezeService.this;
                }
            }).cmd("menu").param("direct", "1").exec();
        }
    }

    /**
     * Adjusts the subscription to players' status updates.
     */
    private void updateAllPlayerSubscriptionStates() {
        for (Player player : mDelegate.getPlayers().values()) {
            updatePlayerSubscription(player, calculateSubscriptionTypeFor(player));
        }
    }

    /**
     * Determine the correct status subscription type for the given player, based on
     * how frequently we need to know its status.
     */
    private PlayerState.PlayerSubscriptionType calculateSubscriptionTypeFor(Player player) {
        Player activePlayer = mDelegate.getActivePlayer();

        if (mEventBus.hasSubscriberForEvent(PlayerStateChanged.class) ||
                (mEventBus.hasSubscriberForEvent(PlayStatusChanged.class) && player.equals(activePlayer))) {
            return PlayerState.PlayerSubscriptionType.NOTIFY_ON_CHANGE;
        } else {
            return PlayerState.PlayerSubscriptionType.NOTIFY_NONE;
        }
    }

    /**
     * Manage subscription to a player's status updates.
     *
     * @param player player to manage.
     * @param playerSubscriptionType the new subscription type
     */
    private void updatePlayerSubscription(
            Player player,
            @NonNull PlayerState.PlayerSubscriptionType playerSubscriptionType) {
        PlayerState playerState = player.getPlayerState();

        // Do nothing if the player subscription type hasn't changed.
        if (playerState.getSubscriptionType().equals(playerSubscriptionType)) {
            return;
        }

        mDelegate.subscribePlayerStatus(player, playerSubscriptionType);
    }

    /**
     * Manages the state of any ongoing notification based on the player and connection state.
     */
    private void updateOngoingNotification() {
        PlayerState activePlayerState = getActivePlayerState();

        // Update scrobble state, if either we're currently scrobbling, or we
        // were (to catch the case where we started scrobbling a song, and the
        // user went in to settings to disable scrobbling).
        if (scrobblingEnabled || scrobblingPreviouslyEnabled) {
            scrobblingPreviouslyEnabled = scrobblingEnabled;
            Scrobble.scrobbleFromPlayerState(this, activePlayerState);
        }

        NotificationState notificationState = notificationState();

        // Compare the current state with the state when the notification was last updated.
        // If there are no changes (same song, same playing state) then there's nothing to do.
        if (notificationState.equals(ongoingNotification)) {
            return;
        }
        ongoingNotification = notificationState;

        final NotificationManagerCompat nm = NotificationManagerCompat.from(this);
        final NotificationData notificationData = new NotificationData(notificationState);
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
            final MediaMetadataCompat.Builder metaBuilder = new MediaMetadataCompat.Builder();
            metaBuilder.putString(MediaMetadata.METADATA_KEY_ARTIST, notificationState.artistName);
            metaBuilder.putString(MediaMetadata.METADATA_KEY_ALBUM, notificationState.albumName);
            metaBuilder.putString(MediaMetadata.METADATA_KEY_TITLE, notificationState.songName);
            mMediaSession.setMetadata(metaBuilder.build());

            ImageFetcher.getInstance(this).loadImage(notificationState.artworkUrl,
                    getResources().getDimensionPixelSize(android.R.dimen.notification_large_icon_width),
                    getResources().getDimensionPixelSize(android.R.dimen.notification_large_icon_height),
                    (data, bitmap) -> {
                        if (bitmap == null) {
                            bitmap = BitmapFactory.decodeResource(getResources(), R.drawable.icon_no_artwork);
                        }

                        metaBuilder.putBitmap(MediaMetadata.METADATA_KEY_ALBUM_ART, bitmap);
                        metaBuilder.putBitmap(MediaMetadata.METADATA_KEY_ART, bitmap);
                        mMediaSession.setMetadata(metaBuilder.build());
                        notificationData.builder.setLargeIcon(bitmap);
                        nm.notify(PLAYBACKSERVICE_STATUS, notificationData.builder.build());
                    });
        } else {
            Notification notification = notificationData.builder.build();
            notification.bigContentView = notificationData.expandedView;

            nm.notify(PLAYBACKSERVICE_STATUS, notification);

            ImageFetcher.getInstance(this).loadImage(this, notificationState.artworkUrl, notificationData.normalView, R.id.album,
                    getResources().getDimensionPixelSize(R.dimen.album_art_icon_normal_notification_width),
                    getResources().getDimensionPixelSize(R.dimen.album_art_icon_normal_notification_height),
                    nm, PLAYBACKSERVICE_STATUS, notification);
            ImageFetcher.getInstance(this).loadImage(this, notificationState.artworkUrl, notificationData.expandedView, R.id.album,
                    getResources().getDimensionPixelSize(R.dimen.album_art_icon_expanded_notification_width),
                    getResources().getDimensionPixelSize(R.dimen.album_art_icon_expanded_notification_height),
                    nm, PLAYBACKSERVICE_STATUS, notification);
        }
    }

    private class NotificationData {
        private final NotificationCompat.Builder builder;
        private RemoteViews normalView;
        private RemoteViews expandedView;

        /**
         * Prepare a notification builder from the supplied notification state.
         */
        @TargetApi(21)
        private NotificationData(NotificationState notificationState) {
            PendingIntent nextPendingIntent = getPendingIntent(ACTION_NEXT_TRACK);
            PendingIntent prevPendingIntent = getPendingIntent(ACTION_PREV_TRACK);
            PendingIntent playPendingIntent = getPendingIntent(ACTION_PLAY);
            PendingIntent pausePendingIntent = getPendingIntent(ACTION_PAUSE);
            PendingIntent closePendingIntent = getPendingIntent(ACTION_CLOSE);

            Intent showNowPlaying = new Intent(SqueezeService.this, NowPlayingActivity.class)
                    .setFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP | Intent.FLAG_ACTIVITY_REORDER_TO_FRONT);
            PendingIntent pIntent = PendingIntent.getActivity(SqueezeService.this, 0, showNowPlaying, 0);


            NotificationUtil.createNotificationChannel(SqueezeService.this, NOTIFICATION_CHANNEL_ID,
                    "Squeezer ongoing notification",
                    "Notifications of player and connection state",
                    NotificationManagerCompat.IMPORTANCE_LOW, false, NotificationCompat.VISIBILITY_PUBLIC);
            builder = new NotificationCompat.Builder(SqueezeService.this, NOTIFICATION_CHANNEL_ID);

            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
                builder.setContentIntent(pIntent);
                builder.setSmallIcon(R.drawable.squeezer_notification);
                builder.setVisibility(NotificationCompat.VISIBILITY_PUBLIC);
                builder.setShowWhen(false);
                builder.setContentTitle(notificationState.songName);
                builder.setContentText(notificationState.artistAlbum());
                builder.setSubText(notificationState.playerName);
                builder.setStyle(new androidx.media.app.NotificationCompat.MediaStyle()
                        .setShowActionsInCompactView(2, 3)
                        .setMediaSession(mMediaSession.getSessionToken()));

                // Don't set an ongoing notification, otherwise wearable's won't show it.
                builder.setOngoing(false);

                builder.setDeleteIntent(closePendingIntent);
                builder.addAction(new NotificationCompat.Action(R.drawable.ic_action_disconnect, "Disconnect", closePendingIntent));
                builder.addAction(new NotificationCompat.Action(R.drawable.ic_action_previous, "Previous", prevPendingIntent));
                if (notificationState.playing) {
                    builder.addAction(new NotificationCompat.Action(R.drawable.ic_action_pause, "Pause", pausePendingIntent));
                } else {
                    builder.addAction(new NotificationCompat.Action(R.drawable.ic_action_play, "Play", playPendingIntent));
                }
                builder.addAction(new NotificationCompat.Action(R.drawable.ic_action_next, "Next", nextPendingIntent));
            } else {
                normalView = new RemoteViews(SqueezeService.this.getPackageName(), R.layout.notification_player_normal);
                expandedView = new RemoteViews(SqueezeService.this.getPackageName(), R.layout.notification_player_expanded);

                builder.setOngoing(true);
                builder.setCategory(NotificationCompat.CATEGORY_SERVICE);
                builder.setSmallIcon(R.drawable.squeezer_notification);

                normalView.setImageViewBitmap(R.id.next, vectorToBitmap(R.drawable.ic_action_next));
                normalView.setOnClickPendingIntent(R.id.next, nextPendingIntent);

                expandedView.setImageViewBitmap(R.id.disconnect, vectorToBitmap(R.drawable.ic_action_disconnect));
                expandedView.setOnClickPendingIntent(R.id.disconnect, closePendingIntent);
                expandedView.setImageViewBitmap(R.id.previous, vectorToBitmap(R.drawable.ic_action_previous));
                expandedView.setOnClickPendingIntent(R.id.previous, prevPendingIntent);
                expandedView.setImageViewBitmap(R.id.next, vectorToBitmap(R.drawable.ic_action_next));
                expandedView.setOnClickPendingIntent(R.id.next, nextPendingIntent);

                builder.setContent(normalView);
                builder.setCustomBigContentView(expandedView);

                normalView.setTextViewText(R.id.trackname, notificationState.songName);
                normalView.setTextViewText(R.id.artist_album, notificationState.artistAlbum());

                expandedView.setTextViewText(R.id.trackname, notificationState.songName);
                expandedView.setTextViewText(R.id.artist_album, notificationState.artistAlbum());
                expandedView.setTextViewText(R.id.player_name, notificationState.playerName);

                if (notificationState.playing) {
                    normalView.setImageViewBitmap(R.id.pause, vectorToBitmap(R.drawable.ic_action_pause));
                    normalView.setOnClickPendingIntent(R.id.pause, pausePendingIntent);

                    expandedView.setImageViewBitmap(R.id.pause, vectorToBitmap(R.drawable.ic_action_pause));
                    expandedView.setOnClickPendingIntent(R.id.pause, pausePendingIntent);
                } else {
                    normalView.setImageViewBitmap(R.id.pause, vectorToBitmap(R.drawable.ic_action_play));
                    normalView.setOnClickPendingIntent(R.id.pause, playPendingIntent);

                    expandedView.setImageViewBitmap(R.id.pause, vectorToBitmap(R.drawable.ic_action_play));
                    expandedView.setOnClickPendingIntent(R.id.pause, playPendingIntent);
                }

                builder.setContentTitle(notificationState.songName);
                builder.setContentText(getString(R.string.notification_playing_text, notificationState.playerName));
                builder.setContentIntent(pIntent);
            }
        }
    }

    private Bitmap vectorToBitmap(@DrawableRes int vectorResource) {
        return Util.vectorToBitmap(this, vectorResource, 0xAA);
    }

    /**
     * Build current notification state based on the player and connection state.
     */
    private NotificationState notificationState() {
        NotificationState notificationState = new NotificationState();

        final Player activePlayer = mDelegate.getActivePlayer();
        notificationState.hasPlayer = (activePlayer != null);
        if (notificationState.hasPlayer) {
            final PlayerState activePlayerState = activePlayer.getPlayerState();

            notificationState.playing = activePlayerState.isPlaying();

            final CurrentPlaylistItem currentSong = activePlayerState.getCurrentSong();
            notificationState.hasSong = (currentSong != null);
            if (currentSong != null) {
                notificationState.songName = currentSong.getName();
                notificationState.albumName = currentSong.getAlbum();
                notificationState.artistName = currentSong.getArtist();
                notificationState.artworkUrl = currentSong.getIcon();
                notificationState.playerName = activePlayer.getName();
            }
        }

        return notificationState;
    }

    /**
     * @param action The action to be performed.
     * @return A new {@link PendingIntent} for {@literal action} that will update any existing
     *     intents that use the same action.
     */
    @NonNull
    private PendingIntent getPendingIntent(@NonNull String action){
        Intent intent = new Intent(this, SqueezeService.class);
        intent.setAction(action);
        return PendingIntent.getService(this, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT);
    }

    public void onEvent(ConnectionChanged event) {
        if (ConnectionState.isConnected(event.connectionState) ||
                ConnectionState.isConnectInProgress(event.connectionState)) {
            startForeground();

            TelephonyManager telephonyManager = (TelephonyManager) getSystemService(TELEPHONY_SERVICE);
            telephonyManager.listen(phoneStateListener, PhoneStateListener.LISTEN_CALL_STATE);
        } else {
            TelephonyManager telephonyManager = (TelephonyManager) getSystemService(TELEPHONY_SERVICE);
            telephonyManager.listen(phoneStateListener, PhoneStateListener.LISTEN_NONE);

            mHandshakeComplete = false;
            stopForeground();<commit_msg>Actions on call state change only for API 21+<commit_after>/*
 * Copyright (c) 2009 Google Inc.  All Rights Reserved.
 *
 * 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 uk.org.ngo.squeezer.service;

import android.annotation.TargetApi;
import android.app.Notification;
import android.app.PendingIntent;
import android.app.Service;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.media.AudioManager;
import android.media.MediaMetadata;
import android.net.Uri;
import android.net.wifi.WifiManager;
import android.os.Binder;
import android.os.Build;
import android.os.IBinder;
import android.os.PowerManager;

import androidx.annotation.DrawableRes;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import androidx.core.app.NotificationCompat;
import androidx.core.app.NotificationManagerCompat;
import androidx.media.VolumeProviderCompat;

import android.support.v4.media.MediaMetadataCompat;
import android.support.v4.media.session.MediaSessionCompat;
import android.support.v4.media.session.PlaybackStateCompat;
import android.telephony.PhoneStateListener;
import android.telephony.TelephonyManager;
import android.util.Log;
import android.widget.RemoteViews;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

import uk.org.ngo.squeezer.NowPlayingActivity;
import uk.org.ngo.squeezer.Preferences;
import uk.org.ngo.squeezer.R;
import uk.org.ngo.squeezer.Util;
import uk.org.ngo.squeezer.download.DownloadDatabase;
import uk.org.ngo.squeezer.model.Action;
import uk.org.ngo.squeezer.model.JiveItem;
import uk.org.ngo.squeezer.model.MusicFolderItem;
import uk.org.ngo.squeezer.model.SlimCommand;
import uk.org.ngo.squeezer.itemlist.IServiceItemListCallback;
import uk.org.ngo.squeezer.model.Alarm;
import uk.org.ngo.squeezer.model.AlarmPlaylist;
import uk.org.ngo.squeezer.model.CurrentPlaylistItem;
import uk.org.ngo.squeezer.model.Player;
import uk.org.ngo.squeezer.model.PlayerState;
import uk.org.ngo.squeezer.model.Song;
import uk.org.ngo.squeezer.service.event.ConnectionChanged;
import uk.org.ngo.squeezer.service.event.HandshakeComplete;
import uk.org.ngo.squeezer.service.event.MusicChanged;
import uk.org.ngo.squeezer.service.event.PlayStatusChanged;
import uk.org.ngo.squeezer.service.event.PlayerStateChanged;
import uk.org.ngo.squeezer.service.event.PlayerVolume;
import uk.org.ngo.squeezer.service.event.PlayersChanged;
import uk.org.ngo.squeezer.util.ImageFetcher;
import uk.org.ngo.squeezer.util.NotificationUtil;
import uk.org.ngo.squeezer.util.Scrobble;

/**
 * Persistent service which acts as an interface to for activities to communicate with LMS.
 * <p>
 * The interface is documented here {@link ISqueezeService}
 * <p>
 * The service lifecycle is managed as both a bound and a started servic. as follows.
 * <ul>
 *     <li>On connect to LMS call Context.start[Foreground]Service and Service.startForeground</li>
 *     <li>On disconnect from LMS call Service.stopForeground and Service.stopSelf</li>
 *     <li>bind to the SqueezeService in activities in onCreate</li>
 *     <li>unbind the SqueezeService in activities  onDestroy</li>
 * </ul>
 * This means the service will as long as there is a Squeezer or we are connected to LMS activity.
 * When we are connected to LMS it runs as a foreground service and a notification is displayed.
 */
public class SqueezeService extends Service {

    private static final String TAG = "SqueezeService";

    public static final String NOTIFICATION_CHANNEL_ID = "channel_squeezer_1";
    private static final int PLAYBACKSERVICE_STATUS = 1;
    public static final int DOWNLOAD_ERROR = 2;

    /** Service-specific eventbus. All events generated by the service will be sent here. */
    private final EventBus mEventBus = new EventBus();

    /** True if the handshake with the server has completed, otherwise false. */
    private volatile boolean mHandshakeComplete = false;

    /** Media session to associate with ongoing notifications. */
    private MediaSessionCompat mMediaSession;

    /** Are the service currently in the foregrund */
    private volatile boolean foreGround;

    /** The most recent notifcation. */
    private NotificationState ongoingNotification;

    private final SlimDelegate mDelegate = new SlimDelegate(mEventBus);

    /**
     * Is scrobbling enabled?
     */
    private boolean scrobblingEnabled;

    /**
     * Was scrobbling enabled?
     */
    private boolean scrobblingPreviouslyEnabled;

    int mFadeInSecs;

    private static final String ACTION_NEXT_TRACK = "uk.org.ngo.squeezer.service.ACTION_NEXT_TRACK";
    private static final String ACTION_PREV_TRACK = "uk.org.ngo.squeezer.service.ACTION_PREV_TRACK";
    private static final String ACTION_PLAY = "uk.org.ngo.squeezer.service.ACTION_PLAY";
    private static final String ACTION_PAUSE = "uk.org.ngo.squeezer.service.ACTION_PAUSE";
    private static final String ACTION_CLOSE = "uk.org.ngo.squeezer.service.ACTION_CLOSE";

    private final BroadcastReceiver deviceIdleModeReceiver = new BroadcastReceiver() {
        @Override
        @RequiresApi(api = Build.VERSION_CODES.M)
        public void onReceive(Context context, Intent intent) {
            // On M and above going in to Doze mode suspends the network but does not shut down
            // existing network connections or cause them to generate exceptions. Explicitly
            // disconnect here, so that resuming from Doze mode forces a reconnect. See
            // https://github.com/nikclayton/android-squeezer/issues/177.
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
                PowerManager pm = (PowerManager) context.getSystemService(Context.POWER_SERVICE);

                if (pm.isDeviceIdleMode()) {
                    Log.d(TAG, "Entering doze mode, disconnecting");
                    disconnect(false);
                }
            }
        }
    };

    private MyVolumeProvider mVolumeProvider;

    /**
     * Thrown when the service is asked to send a command to the server before the server
     * handshake completes.
     */
    public static class HandshakeNotCompleteException extends IllegalStateException {
        public HandshakeNotCompleteException() {
            super();
        }

        public HandshakeNotCompleteException(String message) {
            super(message);
        }

        public HandshakeNotCompleteException(String message, Throwable cause) {
            super(message, cause);
        }

        public HandshakeNotCompleteException(Throwable cause) {
            super(cause);
        }
    }

    @Override
    public void onCreate() {
        super.onCreate();

        // Clear leftover notification in case this service previously got killed while playing
        NotificationManagerCompat nm = NotificationManagerCompat.from(this);
        nm.cancel(PLAYBACKSERVICE_STATUS);

        cachePreferences();

        WifiManager wifiManager = (WifiManager) getApplicationContext().getSystemService(Context.WIFI_SERVICE);
        this.wifiLock = wifiManager.createWifiLock(WifiManager.WIFI_MODE_FULL, "Squeezer_WifiLock");

        mEventBus.register(this, 1);  // Get events before other subscribers

        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
            registerReceiver(deviceIdleModeReceiver, new IntentFilter(
                    PowerManager.ACTION_DEVICE_IDLE_MODE_CHANGED));
        }
    }

    @Override
    public int onStartCommand(Intent intent, int flags, int startId) {
        try{
            if(intent != null && intent.getAction()!= null ) {
                switch (intent.getAction()) {
                    case ACTION_NEXT_TRACK:
                        squeezeService.nextTrack();
                        break;
                    case ACTION_PREV_TRACK:
                        squeezeService.previousTrack();
                        break;
                    case ACTION_PLAY:
                        squeezeService.play();
                        break;
                    case ACTION_PAUSE:
                        squeezeService.pause();
                        break;
                    case ACTION_CLOSE:
                        squeezeService.disconnect();
                        break;
                }
            }
        } catch(Exception e) {
            Log.w(TAG, "Error executing intent: ", e);
        }
        return START_STICKY;
    }

    /**
     * Cache the value of various preferences.
     */
    private void cachePreferences() {
        final Preferences preferences = new Preferences(this);
        scrobblingEnabled = preferences.isScrobbleEnabled();
        mFadeInSecs = preferences.getFadeInSecs();
        mVolumeProvider = new MyVolumeProvider(preferences.getVolumeIncrements());
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
            if (squeezeService.isConnected()) {
                if (preferences.isBackgroundVolume()) {
                    mMediaSession.setPlaybackToRemote(mVolumeProvider);
                } else {
                    mMediaSession.setPlaybackToLocal(AudioManager.STREAM_MUSIC);
                }
            }
        }
    }

    @Override
    public IBinder onBind(Intent intent) {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
            mMediaSession = new MediaSessionCompat(getApplicationContext(), "squeezer");
        }
        return (IBinder) squeezeService;
    }

    @Override
    public boolean onUnbind(Intent intent) {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
            if (mMediaSession != null) {
                mMediaSession.release();
            }
        }
        return super.onUnbind(intent);
    }

    @Override
    public void onDestroy() {
        super.onDestroy();
        disconnect(false);
        mEventBus.unregister(this);

        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
            try {
                unregisterReceiver(deviceIdleModeReceiver);
            } catch (IllegalArgumentException e) {
                // Do nothing. This can occur in testing when we destroy the service before the
                // receiver is registered.
            }
        }
    }

    @Override
    public void onTaskRemoved(Intent rootIntent) {
        disconnect(false);
        super.onTaskRemoved(rootIntent);
    }

    void disconnect(boolean fromUser) {
        mDelegate.disconnect(fromUser);
    }

    @Nullable public PlayerState getActivePlayerState() {
        Player activePlayer = mDelegate.getActivePlayer();
        return activePlayer == null ? null : activePlayer.getPlayerState();

    }

    /**
     * Updates the playing status of the current player.
     * <p>
     * Updates the Wi-Fi lock and ongoing status notification as necessary.
     */
    public void onEvent(PlayStatusChanged event) {
        if (event.player.equals(mDelegate.getActivePlayer())) {
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
                int state = PlayerState.PLAY_STATE_PLAY.equals(event.playStatus) ? PlaybackStateCompat.STATE_PLAYING : PlaybackStateCompat.STATE_STOPPED;
                mMediaSession.setPlaybackState(new PlaybackStateCompat.Builder().setState(state, 0, 0).build());
            }
            updateOngoingNotification();
        }
    }

    /**
     * Change the player that is controlled by Squeezer (the "active" player).
     *
     * @param newActivePlayer The new active player. May be null, in which case no players
     *     are controlled.
     */
    private void changeActivePlayer(@Nullable final Player newActivePlayer) {
        Player prevActivePlayer = mDelegate.getActivePlayer();

        // Do nothing if the player hasn't actually changed.
        if (prevActivePlayer == newActivePlayer) {
            return;
        }

        Log.i(TAG, "Active player now: " + newActivePlayer);
        mDelegate.setActivePlayer(newActivePlayer);

        if (prevActivePlayer != null) {
            mDelegate.subscribeDisplayStatus(prevActivePlayer, false);
            mDelegate.subscribeMenuStatus(prevActivePlayer, false);
        }

        updateAllPlayerSubscriptionStates();
        requestPlayerData();
        new Preferences(this).setLastPlayer(newActivePlayer);
    }

    private void requestPlayerData() {
        Player activePlayer = mDelegate.getActivePlayer();

        if (activePlayer != null) {
            mDelegate.subscribeDisplayStatus(activePlayer, true);
            mDelegate.subscribeMenuStatus(activePlayer, true);
            mDelegate.requestPlayerStatus(activePlayer);

            // Start an asynchronous fetch of the squeezeservers "home menu" items
            // See http://wiki.slimdevices.com/index.php/SqueezePlayAndSqueezeCenterPlugins
            mDelegate.requestItems(activePlayer, 0, new IServiceItemListCallback<JiveItem>() {
                private final List<JiveItem> homeMenu = new ArrayList<>();

                @Override
                public void onItemsReceived(int count, int start, Map<String, Object> parameters, List<JiveItem> items, Class<JiveItem> dataType) {
                    homeMenu.addAll(items);
                    if (homeMenu.size() == count) {
                        Preferences preferences = new Preferences(SqueezeService.this);
                        boolean useArchive = preferences.getCustomizeHomeMenuMode() != Preferences.CustomizeHomeMenuMode.DISABLED;
                        List<String> archivedMenuItems = useArchive ? preferences.getArchivedMenuItems(activePlayer) : Collections.emptyList();
                        mDelegate.setHomeMenu(homeMenu, archivedMenuItems);
                    }
                }
                @Override
                public Object getClient() {
                    return SqueezeService.this;
                }
            }).cmd("menu").param("direct", "1").exec();
        }
    }

    /**
     * Adjusts the subscription to players' status updates.
     */
    private void updateAllPlayerSubscriptionStates() {
        for (Player player : mDelegate.getPlayers().values()) {
            updatePlayerSubscription(player, calculateSubscriptionTypeFor(player));
        }
    }

    /**
     * Determine the correct status subscription type for the given player, based on
     * how frequently we need to know its status.
     */
    private PlayerState.PlayerSubscriptionType calculateSubscriptionTypeFor(Player player) {
        Player activePlayer = mDelegate.getActivePlayer();

        if (mEventBus.hasSubscriberForEvent(PlayerStateChanged.class) ||
                (mEventBus.hasSubscriberForEvent(PlayStatusChanged.class) && player.equals(activePlayer))) {
            return PlayerState.PlayerSubscriptionType.NOTIFY_ON_CHANGE;
        } else {
            return PlayerState.PlayerSubscriptionType.NOTIFY_NONE;
        }
    }

    /**
     * Manage subscription to a player's status updates.
     *
     * @param player player to manage.
     * @param playerSubscriptionType the new subscription type
     */
    private void updatePlayerSubscription(
            Player player,
            @NonNull PlayerState.PlayerSubscriptionType playerSubscriptionType) {
        PlayerState playerState = player.getPlayerState();

        // Do nothing if the player subscription type hasn't changed.
        if (playerState.getSubscriptionType().equals(playerSubscriptionType)) {
            return;
        }

        mDelegate.subscribePlayerStatus(player, playerSubscriptionType);
    }

    /**
     * Manages the state of any ongoing notification based on the player and connection state.
     */
    private void updateOngoingNotification() {
        PlayerState activePlayerState = getActivePlayerState();

        // Update scrobble state, if either we're currently scrobbling, or we
        // were (to catch the case where we started scrobbling a song, and the
        // user went in to settings to disable scrobbling).
        if (scrobblingEnabled || scrobblingPreviouslyEnabled) {
            scrobblingPreviouslyEnabled = scrobblingEnabled;
            Scrobble.scrobbleFromPlayerState(this, activePlayerState);
        }

        NotificationState notificationState = notificationState();

        // Compare the current state with the state when the notification was last updated.
        // If there are no changes (same song, same playing state) then there's nothing to do.
        if (notificationState.equals(ongoingNotification)) {
            return;
        }
        ongoingNotification = notificationState;

        final NotificationManagerCompat nm = NotificationManagerCompat.from(this);
        final NotificationData notificationData = new NotificationData(notificationState);
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
            final MediaMetadataCompat.Builder metaBuilder = new MediaMetadataCompat.Builder();
            metaBuilder.putString(MediaMetadata.METADATA_KEY_ARTIST, notificationState.artistName);
            metaBuilder.putString(MediaMetadata.METADATA_KEY_ALBUM, notificationState.albumName);
            metaBuilder.putString(MediaMetadata.METADATA_KEY_TITLE, notificationState.songName);
            mMediaSession.setMetadata(metaBuilder.build());

            ImageFetcher.getInstance(this).loadImage(notificationState.artworkUrl,
                    getResources().getDimensionPixelSize(android.R.dimen.notification_large_icon_width),
                    getResources().getDimensionPixelSize(android.R.dimen.notification_large_icon_height),
                    (data, bitmap) -> {
                        if (bitmap == null) {
                            bitmap = BitmapFactory.decodeResource(getResources(), R.drawable.icon_no_artwork);
                        }

                        metaBuilder.putBitmap(MediaMetadata.METADATA_KEY_ALBUM_ART, bitmap);
                        metaBuilder.putBitmap(MediaMetadata.METADATA_KEY_ART, bitmap);
                        mMediaSession.setMetadata(metaBuilder.build());
                        notificationData.builder.setLargeIcon(bitmap);
                        nm.notify(PLAYBACKSERVICE_STATUS, notificationData.builder.build());
                    });
        } else {
            Notification notification = notificationData.builder.build();
            notification.bigContentView = notificationData.expandedView;

            nm.notify(PLAYBACKSERVICE_STATUS, notification);

            ImageFetcher.getInstance(this).loadImage(this, notificationState.artworkUrl, notificationData.normalView, R.id.album,
                    getResources().getDimensionPixelSize(R.dimen.album_art_icon_normal_notification_width),
                    getResources().getDimensionPixelSize(R.dimen.album_art_icon_normal_notification_height),
                    nm, PLAYBACKSERVICE_STATUS, notification);
            ImageFetcher.getInstance(this).loadImage(this, notificationState.artworkUrl, notificationData.expandedView, R.id.album,
                    getResources().getDimensionPixelSize(R.dimen.album_art_icon_expanded_notification_width),
                    getResources().getDimensionPixelSize(R.dimen.album_art_icon_expanded_notification_height),
                    nm, PLAYBACKSERVICE_STATUS, notification);
        }
    }

    private class NotificationData {
        private final NotificationCompat.Builder builder;
        private RemoteViews normalView;
        private RemoteViews expandedView;

        /**
         * Prepare a notification builder from the supplied notification state.
         */
        @TargetApi(21)
        private NotificationData(NotificationState notificationState) {
            PendingIntent nextPendingIntent = getPendingIntent(ACTION_NEXT_TRACK);
            PendingIntent prevPendingIntent = getPendingIntent(ACTION_PREV_TRACK);
            PendingIntent playPendingIntent = getPendingIntent(ACTION_PLAY);
            PendingIntent pausePendingIntent = getPendingIntent(ACTION_PAUSE);
            PendingIntent closePendingIntent = getPendingIntent(ACTION_CLOSE);

            Intent showNowPlaying = new Intent(SqueezeService.this, NowPlayingActivity.class)
                    .setFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP | Intent.FLAG_ACTIVITY_REORDER_TO_FRONT);
            PendingIntent pIntent = PendingIntent.getActivity(SqueezeService.this, 0, showNowPlaying, 0);


            NotificationUtil.createNotificationChannel(SqueezeService.this, NOTIFICATION_CHANNEL_ID,
                    "Squeezer ongoing notification",
                    "Notifications of player and connection state",
                    NotificationManagerCompat.IMPORTANCE_LOW, false, NotificationCompat.VISIBILITY_PUBLIC);
            builder = new NotificationCompat.Builder(SqueezeService.this, NOTIFICATION_CHANNEL_ID);

            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
                builder.setContentIntent(pIntent);
                builder.setSmallIcon(R.drawable.squeezer_notification);
                builder.setVisibility(NotificationCompat.VISIBILITY_PUBLIC);
                builder.setShowWhen(false);
                builder.setContentTitle(notificationState.songName);
                builder.setContentText(notificationState.artistAlbum());
                builder.setSubText(notificationState.playerName);
                builder.setStyle(new androidx.media.app.NotificationCompat.MediaStyle()
                        .setShowActionsInCompactView(2, 3)
                        .setMediaSession(mMediaSession.getSessionToken()));

                // Don't set an ongoing notification, otherwise wearable's won't show it.
                builder.setOngoing(false);

                builder.setDeleteIntent(closePendingIntent);
                builder.addAction(new NotificationCompat.Action(R.drawable.ic_action_disconnect, "Disconnect", closePendingIntent));
                builder.addAction(new NotificationCompat.Action(R.drawable.ic_action_previous, "Previous", prevPendingIntent));
                if (notificationState.playing) {
                    builder.addAction(new NotificationCompat.Action(R.drawable.ic_action_pause, "Pause", pausePendingIntent));
                } else {
                    builder.addAction(new NotificationCompat.Action(R.drawable.ic_action_play, "Play", playPendingIntent));
                }
                builder.addAction(new NotificationCompat.Action(R.drawable.ic_action_next, "Next", nextPendingIntent));
            } else {
                normalView = new RemoteViews(SqueezeService.this.getPackageName(), R.layout.notification_player_normal);
                expandedView = new RemoteViews(SqueezeService.this.getPackageName(), R.layout.notification_player_expanded);

                builder.setOngoing(true);
                builder.setCategory(NotificationCompat.CATEGORY_SERVICE);
                builder.setSmallIcon(R.drawable.squeezer_notification);

                normalView.setImageViewBitmap(R.id.next, vectorToBitmap(R.drawable.ic_action_next));
                normalView.setOnClickPendingIntent(R.id.next, nextPendingIntent);

                expandedView.setImageViewBitmap(R.id.disconnect, vectorToBitmap(R.drawable.ic_action_disconnect));
                expandedView.setOnClickPendingIntent(R.id.disconnect, closePendingIntent);
                expandedView.setImageViewBitmap(R.id.previous, vectorToBitmap(R.drawable.ic_action_previous));
                expandedView.setOnClickPendingIntent(R.id.previous, prevPendingIntent);
                expandedView.setImageViewBitmap(R.id.next, vectorToBitmap(R.drawable.ic_action_next));
                expandedView.setOnClickPendingIntent(R.id.next, nextPendingIntent);

                builder.setContent(normalView);
                builder.setCustomBigContentView(expandedView);

                normalView.setTextViewText(R.id.trackname, notificationState.songName);
                normalView.setTextViewText(R.id.artist_album, notificationState.artistAlbum());

                expandedView.setTextViewText(R.id.trackname, notificationState.songName);
                expandedView.setTextViewText(R.id.artist_album, notificationState.artistAlbum());
                expandedView.setTextViewText(R.id.player_name, notificationState.playerName);

                if (notificationState.playing) {
                    normalView.setImageViewBitmap(R.id.pause, vectorToBitmap(R.drawable.ic_action_pause));
                    normalView.setOnClickPendingIntent(R.id.pause, pausePendingIntent);

                    expandedView.setImageViewBitmap(R.id.pause, vectorToBitmap(R.drawable.ic_action_pause));
                    expandedView.setOnClickPendingIntent(R.id.pause, pausePendingIntent);
                } else {
                    normalView.setImageViewBitmap(R.id.pause, vectorToBitmap(R.drawable.ic_action_play));
                    normalView.setOnClickPendingIntent(R.id.pause, playPendingIntent);

                    expandedView.setImageViewBitmap(R.id.pause, vectorToBitmap(R.drawable.ic_action_play));
                    expandedView.setOnClickPendingIntent(R.id.pause, playPendingIntent);
                }

                builder.setContentTitle(notificationState.songName);
                builder.setContentText(getString(R.string.notification_playing_text, notificationState.playerName));
                builder.setContentIntent(pIntent);
            }
        }
    }

    private Bitmap vectorToBitmap(@DrawableRes int vectorResource) {
        return Util.vectorToBitmap(this, vectorResource, 0xAA);
    }

    /**
     * Build current notification state based on the player and connection state.
     */
    private NotificationState notificationState() {
        NotificationState notificationState = new NotificationState();

        final Player activePlayer = mDelegate.getActivePlayer();
        notificationState.hasPlayer = (activePlayer != null);
        if (notificationState.hasPlayer) {
            final PlayerState activePlayerState = activePlayer.getPlayerState();

            notificationState.playing = activePlayerState.isPlaying();

            final CurrentPlaylistItem currentSong = activePlayerState.getCurrentSong();
            notificationState.hasSong = (currentSong != null);
            if (currentSong != null) {
                notificationState.songName = currentSong.getName();
                notificationState.albumName = currentSong.getAlbum();
                notificationState.artistName = currentSong.getArtist();
                notificationState.artworkUrl = currentSong.getIcon();
                notificationState.playerName = activePlayer.getName();
            }
        }

        return notificationState;
    }

    /**
     * @param action The action to be performed.
     * @return A new {@link PendingIntent} for {@literal action} that will update any existing
     *     intents that use the same action.
     */
    @NonNull
    private PendingIntent getPendingIntent(@NonNull String action){
        Intent intent = new Intent(this, SqueezeService.class);
        intent.setAction(action);
        return PendingIntent.getService(this, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT);
    }

    public void onEvent(ConnectionChanged event) {
        if (ConnectionState.isConnected(event.connectionState) ||
                ConnectionState.isConnectInProgress(event.connectionState)) {
            startForeground();

            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
                TelephonyManager telephonyManager = (TelephonyManager) getSystemService(TELEPHONY_SERVICE);
                telephonyManager.listen(phoneStateListener, PhoneStateListener.LISTEN_CALL_STATE);
            }
        } else {
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
                TelephonyManager telephonyManager = (TelephonyManager) getSystemService(TELEPHONY_SERVICE);
                telephonyManager.listen(phoneStateListener, PhoneStateListener.LISTEN_NONE);
            }

            mHandshakeComplete = false;
            stopForeground();
{'fraction_non_alphanumeric': 0.0827285669641307, 'fraction_numerical': 0.001076426264800861, 'mean_word_length': 2.704830513925516, 'pattern_counts': {'":': 0, '<': 43, '<?xml version=': 0, '>': 65, 'https://': 2, 'lorem ipsum': 0, 'www.': 2, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 4, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '3957510', 'n_tokens_mistral': 15210, 'n_tokens_neox': 14555, 'n_words': 3085}