app/src/main/java/net/programmierecke/radiodroid2/station/live/StreamLiveInfo.java
public class StreamLiveInfo implements Parcelable {

    public StreamLiveInfo(Map<String, String> rawMetadata) {
        this.rawMetadata = rawMetadata;

        if (rawMetadata != null && rawMetadata.containsKey("StreamTitle")) {
            title = rawMetadata.get("StreamTitle");

            if (!TextUtils.isEmpty(title)) {
                String[] artistAndTrack = title.split(" - ", 2);
                artist = artistAndTrack[0];
                if (artistAndTrack.length == 2) {
                    track = artistAndTrack[1];
                }
            }
        }
    }

    public @NonNull
    String getTitle() {
        return title;
    }

    public
    boolean hasArtistAndTrack() {
        return ! (artist.isEmpty() || track.isEmpty());
    }

    public @NonNull
    String getArtist() {
        return artist;
    }

    public @NonNull
    String getTrack() {
        return track;
    }

    public Map<String, String> getRawMetadata() {
        return rawMetadata;
    }

    private String title = "";
    private String artist = "";
    private String track = "";
    private Map<String, String> rawMetadata;

    protected StreamLiveInfo(Parcel in) {
        title = in.readString();
        artist = in.readString();
        track = in.readString();
        in.readMap(rawMetadata, String.class.getClassLoader());
    }

    public static final Creator<StreamLiveInfo> CREATOR = new Creator<StreamLiveInfo>() {
        @Override
        public StreamLiveInfo createFromParcel(Parcel in) {
            return new StreamLiveInfo(in);
        }

        @Override
        public StreamLiveInfo[] newArray(int size) {
            return new StreamLiveInfo[size];
        }
    };

    @Override
    public int describeContents() {
        return 0;
    }

    @Override
    public void writeToParcel(Parcel parcel, int i) {
        parcel.writeString(title);
        parcel.writeString(artist);
        parcel.writeString(track);
        parcel.writeMap(rawMetadata);
    }
}
app/src/main/java/net/programmierecke/radiodroid2/RadioDroidApp.java
public class RadioDroidApp extends MultiDexApplication {

    private HistoryManager historyManager;
    private FavouriteManager favouriteManager;
    private RecordingsManager recordingsManager;
    private RadioAlarmManager alarmManager;
    private TvChannelManager tvChannelManager;

    private TrackHistoryRepository trackHistoryRepository;

    private MPDClient mpdClient;

    private CastHandler castHandler;

    private TrackMetadataSearcher trackMetadataSearcher;

    private ConnectionPool connectionPool;
    private OkHttpClient httpClient;

    private Interceptor testsInterceptor;

    public class UserAgentInterceptor implements Interceptor {

        private final String userAgent;

        public UserAgentInterceptor(String userAgent) {
            this.userAgent = userAgent;
        }

        @Override
        public Response intercept(Chain chain) throws IOException {
            Request originalRequest = chain.request();
            Request requestWithUserAgent = originalRequest.newBuilder()
                    .header("User-Agent", userAgent)
                    .build();
            return chain.proceed(requestWithUserAgent);
        }
    }

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

        AppCompatDelegate.setCompatVectorFromResourcesEnabled(true);

        GoogleProviderHelper.use(getBaseContext());

        connectionPool = new ConnectionPool();

        rebuildHttpClient();

        Picasso.Builder builder = new Picasso.Builder(this);
        builder.downloader(new OkHttp3Downloader(newHttpClientForPicasso()));
        Picasso picassoInstance = builder.build();
        Picasso.setSingletonInstance(picassoInstance);

        CountryCodeDictionary.getInstance().load(this);
        CountryFlagsLoader.getInstance();

        historyManager = new HistoryManager(this);
        favouriteManager = new FavouriteManager(this);
        recordingsManager = new RecordingsManager();
        alarmManager = new RadioAlarmManager(this);

        UiModeManager uiModeManager = (UiModeManager) getSystemService(UI_MODE_SERVICE);
        if (uiModeManager.getCurrentModeType() == Configuration.UI_MODE_TYPE_TELEVISION) {
            tvChannelManager = new TvChannelManager(this);
            favouriteManager.addObserver(tvChannelManager);
        }

        trackHistoryRepository = new TrackHistoryRepository(this);

        mpdClient = new MPDClient(this);

        castHandler = new CastHandler();

        trackMetadataSearcher = new TrackMetadataSearcher(httpClient);

        recordingsManager.updateRecordingsList();
    }

    public void setTestsInterceptor(Interceptor testsInterceptor) {
        this.testsInterceptor = testsInterceptor;
    }

    public void rebuildHttpClient() {
        OkHttpClient.Builder builder = newHttpClient()
                .connectTimeout(10, TimeUnit.SECONDS)
                .writeTimeout(10, TimeUnit.SECONDS)
                .readTimeout(10, TimeUnit.SECONDS)
                .addInterceptor(new UserAgentInterceptor("RadioDroid2/" + BuildConfig.VERSION_NAME));

        httpClient = builder.build();
    }

    public HistoryManager getHistoryManager() {
        return historyManager;
    }

    public FavouriteManager getFavouriteManager() {
        return favouriteManager;
    }

    public RecordingsManager getRecordingsManager() {
        return recordingsManager;
    }

    public RadioAlarmManager getAlarmManager() {
        return alarmManager;
    }

    public TrackHistoryRepository getTrackHistoryRepository() {
        return trackHistoryRepository;
    }

    public MPDClient getMpdClient() {
        return mpdClient;
    }

    public CastHandler getCastHandler() {
        return castHandler;
    }

    public TrackMetadataSearcher getTrackMetadataSearcher() {
        return trackMetadataSearcher;
    }

    public OkHttpClient getHttpClient() {
        return httpClient;
    }

    public OkHttpClient.Builder newHttpClient() {
        OkHttpClient.Builder builder = new OkHttpClient.Builder().connectionPool(connectionPool);

        if (testsInterceptor != null) {
            builder.addInterceptor(testsInterceptor);
        }

        if (!setCurrentOkHttpProxy(builder)) {
            Toast toast = Toast.makeText(this, getResources().getString(R.string.ignore_proxy_settings_invalid), Toast.LENGTH_SHORT);
            toast.show();
        }
        return Utils.enableTls12OnPreLollipop(builder);
    }

    public OkHttpClient.Builder newHttpClientWithoutProxy() {
        OkHttpClient.Builder builder = new OkHttpClient.Builder().connectionPool(connectionPool);

        if (testsInterceptor != null) {
            builder.addInterceptor(testsInterceptor);
        }

        return Utils.enableTls12OnPreLollipop(builder);
    }

    public boolean setCurrentOkHttpProxy(@NonNull OkHttpClient.Builder builder) {
        SharedPreferences sharedPref = PreferenceManager.getDefaultSharedPreferences(this);
        ProxySettings proxySettings = ProxySettings.fromPreferences(sharedPref);
        if (proxySettings != null) {
            if (!Utils.setOkHttpProxy(builder, proxySettings)) {
                // proxy settings are not valid
                return false;
            }
        }
        return true;
    }

    private OkHttpClient newHttpClientForPicasso() {
        File cache = new File(getCacheDir(), "picasso-cache");
        if (!cache.exists()) {
            //noinspection ResultOfMethodCallIgnored
            cache.mkdirs();
        }

        OkHttpClient.Builder builder = new OkHttpClient.Builder()
                .addInterceptor(new UserAgentInterceptor("RadioDroid2/" + BuildConfig.VERSION_NAME))
                .cache(new Cache(cache, Integer.MAX_VALUE));

        if (testsInterceptor != null) {
            builder.addInterceptor(testsInterceptor);
        }

        setCurrentOkHttpProxy(builder);

        return builder.build();
    }
}
app/src/main/java/net/programmierecke/radiodroid2/history/TrackHistoryEntry.java
@Entity(tableName = "track_history")
public class TrackHistoryEntry {

    @PrimaryKey(autoGenerate = true)
    public int uid;

    @ColumnInfo(name = "station_uuid")
    @NonNull
    public String stationUuid;

    @ColumnInfo(name = "station_icon_url")
    @NonNull
    public String stationIconUrl;

    @ColumnInfo(name = "track")
    @NonNull
    public String track;

    @ColumnInfo(name = "artist")
    @NonNull
    public String artist;

    @ColumnInfo(name = "title")
    @NonNull
    public String title;

    @ColumnInfo(name = "art_url")
    @Nullable
    public String artUrl;

    @ColumnInfo(name = "start_time")
    @NonNull
    public Date startTime;

    @ColumnInfo(name = "end_time")
    @NonNull
    public Date endTime;

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;

        TrackHistoryEntry that = (TrackHistoryEntry) o;

        if (uid != that.uid) return false;
        if (!stationUuid.equals(that.stationUuid)) return false;
        if (!track.equals(that.track)) return false;
        if (!artist.equals(that.artist)) return false;
        if (!title.equals(that.title)) return false;
        if (artUrl != null ? !artUrl.equals(that.artUrl) : that.artUrl != null) return false;
        if (!startTime.equals(that.startTime)) return false;
        return endTime.equals(that.endTime);
    }

    @Override
    public int hashCode() {
        int result = uid;
        result = 31 * result + stationUuid.hashCode();
        result = 31 * result + track.hashCode();
        result = 31 * result + artist.hashCode();
        result = 31 * result + title.hashCode();
        result = 31 * result + (artUrl != null ? artUrl.hashCode() : 0);
        result = 31 * result + startTime.hashCode();
        result = 31 * result + endTime.hashCode();
        return result;
    }

    public final static int MAX_HISTORY_ITEMS_IN_TABLE = 1000;
    public final static int MAX_UNKNOWN_TRACK_DURATION = 3 * 60 * 1000; // 3 minutes
}
app/src/main/java/net/programmierecke/radiodroid2/HistoryManager.java
public class HistoryManager extends StationSaveManager{
    private static final int MAXSIZE = 25;

    @Override
    protected String getSaveId(){
        return "history";
    }

    public HistoryManager(Context ctx) {
        super(ctx);
    }

    @Override
    public void add(DataRadioStation station){
        DataRadioStation stationFromHistory = getById(station.StationUuid);
        if (stationFromHistory != null) {
            listStations.remove(stationFromHistory);
            listStations.add(0, stationFromHistory);
            Save();
            return;
        }

        cutList(MAXSIZE - 1);
        super.addFront(station);
    }

    private void cutList(int count){
        if (listStations.size() > count){
            listStations = listStations.subList(0,count);
        }
    }
}
app/src/main/java/net/programmierecke/radiodroid2/station/live/ShoutcastInfo.java
public class ShoutcastInfo implements Parcelable {
    public int metadataOffset;
    public int bitrate;

    // e.g.: ice-audio-info: ice-samplerate=44100;ice-bitrate=128;ice-channels=2
    public String audioInfo;

    public String audioDesc;

    // e.g.: icy-genre:Pop / Rock
    public String audioGenre;

    public String audioName;
    public String audioHomePage;

    // e.g.: Server: Icecast 2.3.2
    public String serverName;

    public boolean serverPublic;
    public int channels;
    public int sampleRate;

    public ShoutcastInfo() {
    }

    public ShoutcastInfo(IcyHeaders icyHeaders) {
        this.bitrate = icyHeaders.bitrate;
        this.audioGenre = icyHeaders.genre;
        this.serverPublic = icyHeaders.isPublic;
        this.audioName = icyHeaders.name;
        this.audioHomePage = icyHeaders.url;
    }

    public static ShoutcastInfo Decode(Response response) {
        ShoutcastInfo info = new ShoutcastInfo();
        info.metadataOffset = parseIntWithDefault(response.header("icy-metaint"), 0);
        info.bitrate = parseIntWithDefault(response.header("icy-br"), 0);
        // e.g.: ice-audio-info: ice-samplerate=44100;ice-bitrate=128;ice-channels=2
        info.audioInfo = response.header("ice-audio-info");
        info.audioDesc = response.header("icy-description");
        // e.g.: icy-genre:Pop / Rock
        info.audioGenre = response.header("icy-genre");
        info.audioName = response.header("icy-name");
        info.audioHomePage = response.header("icy-url");
        // e.g.: Server: Icecast 2.3.2
        info.serverName = response.header("Server");
        info.serverPublic = parseIntWithDefault(response.header("icy-pub"), 0) > 0;

        if (info.audioInfo != null) {
            Map<String, String> audioInfoParams = splitAudioInfo(info.audioInfo);

            info.channels = parseIntWithDefault(audioInfoParams.get("ice-channels"), 0);
            if (info.channels == 0) {
                info.channels = parseIntWithDefault(audioInfoParams.get("channels"), 0);
            }

            info.sampleRate = parseIntWithDefault(audioInfoParams.get("ice-samplerate"), 0);
            if (info.sampleRate == 0) {
                info.sampleRate = parseIntWithDefault(audioInfoParams.get("samplerate"), 0);
            }

            if (info.bitrate == 0) {
                info.bitrate = parseIntWithDefault(audioInfoParams.get("ice-bitrate"), 0);
                if (info.bitrate == 0) {
                    info.bitrate = parseIntWithDefault(audioInfoParams.get("bitrate"), 0);
                }
            }
        }

        // info needs at least metadataoffset
        if (info.metadataOffset == 0) {
            return null;
        }

        return info;
    }

    private static Map<String, String> splitAudioInfo(String audioInfo) {
        Map<String, String> params = new LinkedHashMap<>();
        String[] pairs = audioInfo.split(";");
        for (String pair : pairs) {
            int idx = pair.indexOf("=");
            params.put(pair.substring(0, idx), pair.substring(idx + 1));
        }
        return params;
    }

    protected ShoutcastInfo(Parcel in) {
        metadataOffset = in.readInt();
        bitrate = in.readInt();
        audioInfo = in.readString();
        audioDesc = in.readString();
        audioGenre = in.readString();
        audioName = in.readString();
        audioHomePage = in.readString();
        serverName = in.readString();
        serverPublic = in.readByte() != 0;
        channels = in.readInt();
        sampleRate = in.readInt();
    }

    @Override
    public void writeToParcel(Parcel dest, int flags) {
        dest.writeInt(metadataOffset);
        dest.writeInt(bitrate);
        dest.writeString(audioInfo);
        dest.writeString(audioDesc);
        dest.writeString(audioGenre);
        dest.writeString(audioName);
        dest.writeString(audioHomePage);
        dest.writeString(serverName);
        dest.writeByte((byte) (serverPublic ? 1 : 0));
        dest.writeInt(channels);
        dest.writeInt(sampleRate);
    }

    @Override
    public int describeContents() {
        return 0;
    }

    public static final Creator<ShoutcastInfo> CREATOR = new Creator<ShoutcastInfo>() {
        @Override
        public ShoutcastInfo createFromParcel(Parcel in) {
            return new ShoutcastInfo(in);
        }

        @Override
        public ShoutcastInfo[] newArray(int size) {
            return new ShoutcastInfo[size];
        }
    };
}
app/src/main/java/net/programmierecke/radiodroid2/recording/RunningRecordingInfo.java
public class RunningRecordingInfo {
    private Recordable recordable;
    private String title;
    private String fileName;
    private FileOutputStream outputStream;
    private long bytesWritten;

    public Recordable getRecordable() {
        return recordable;
    }

    protected void setRecordable(Recordable recordable) {
        this.recordable = recordable;
    }

    public String getTitle() {
        return title;
    }

    protected void setTitle(String title) {
        this.title = title;
    }

    public String getFileName() {
        return fileName;
    }

    protected void setFileName(String fileName) {
        this.fileName = fileName;
    }

    public FileOutputStream getOutputStream() {
        return outputStream;
    }

    protected void setOutputStream(FileOutputStream outputStream) {
        this.outputStream = outputStream;
    }

    public long getBytesWritten() {
        return bytesWritten;
    }

    protected void setBytesWritten(long bytesWritten) {
        this.bytesWritten = bytesWritten;
    }
}
app/src/main/java/net/programmierecke/radiodroid2/recording/RecordingsManager.java
public class RecordingsManager {
    private final static String TAG = "Recordings";
    private DateFormat dateFormatter = new SimpleDateFormat("yyyy-MM-dd", Locale.US);
    private DateFormat timeFormatter = new SimpleDateFormat("HH-mm", Locale.US);

    private class RecordingsObservable extends Observable {
        @Override
        public synchronized boolean hasChanged() {
            return true;
        }
    }

    private Observable savedRecordingsObservable = new RecordingsObservable();

    private class RunningRecordableListener implements RecordableListener {
        private RunningRecordingInfo runningRecordingInfo;
        private boolean ended;

        private RunningRecordableListener(@NonNull RunningRecordingInfo runningRecordingInfo) {
            this.runningRecordingInfo = runningRecordingInfo;
        }

        @Override
        public void onBytesAvailable(byte[] buffer, int offset, int length) {
            try {
                runningRecordingInfo.getOutputStream().write(buffer, offset, length);
                runningRecordingInfo.setBytesWritten(runningRecordingInfo.getBytesWritten() + length);
            } catch (IOException e) {
                e.printStackTrace();
                runningRecordingInfo.getRecordable().stopRecording();
            }
        }

        @Override
        public void onRecordingEnded() {
            if (ended) {
                return;
            }

            ended = true;

            try {
                runningRecordingInfo.getOutputStream().close();
            } catch (IOException e) {
                e.printStackTrace();
            }

            RecordingsManager.this.stopRecording(runningRecordingInfo.getRecordable());
        }
    }

    private Map<Recordable, RunningRecordingInfo> runningRecordings = new HashMap<>();
    private ArrayList<DataRecording> savedRecordings = new ArrayList<>();

    public void record(@NonNull Context context, @NonNull Recordable recordable) {
        if (!recordable.canRecord()) {
            return;
        }

        if (!runningRecordings.containsKey(recordable)) {
            RunningRecordingInfo info = new RunningRecordingInfo();

            info.setRecordable(recordable);

            SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context.getApplicationContext());

            final String fileNameFormat = prefs.getString("record_name_formatting", context.getString(R.string.settings_record_name_formatting_default));

            final Map<String, String> formattingArgs = new HashMap<>(recordable.getRecordNameFormattingArgs());

            Calendar calendar = Calendar.getInstance();
            calendar.setTimeInMillis(System.currentTimeMillis());

            final Date currentTime = calendar.getTime();

            String dateStr = dateFormatter.format(currentTime);
            String timeStr = timeFormatter.format(currentTime);

            formattingArgs.put("date", dateStr);
            formattingArgs.put("time", timeStr);

            final int recordNum = prefs.getInt("record_num", 1);
            formattingArgs.put("index", Integer.toString(recordNum));

            final String recordTitle = Utils.formatStringWithNamedArgs(fileNameFormat, formattingArgs);

            info.setTitle(recordTitle);
            info.setFileName(String.format("%s.%s", recordTitle, recordable.getExtension()));

            //TODO: check available disk space here

            String filePath = RecordingsManager.getRecordDir() + "/" + info.getFileName();
            try {
                info.setOutputStream(new FileOutputStream(filePath));
            } catch (FileNotFoundException e) {
                e.printStackTrace();
                return;
            }

            recordable.startRecording(new RunningRecordableListener(info));

            runningRecordings.put(recordable, info);

            prefs.edit().putInt("record_num", recordNum + 1).apply();
        }
    }

    public void stopRecording(@NonNull Recordable recordable) {
        recordable.stopRecording();

        runningRecordings.remove(recordable);

        updateRecordingsList();
    }

    public RunningRecordingInfo getRecordingInfo(Recordable recordable) {
        return runningRecordings.get(recordable);
    }

    public Map<Recordable, RunningRecordingInfo> getRunningRecordings() {
        return Collections.unmodifiableMap(runningRecordings);
    }

    public List<DataRecording> getSavedRecordings() {
        return new ArrayList<>(savedRecordings);
    }

    public Observable getSavedRecordingsObservable() {
        return savedRecordingsObservable;
    }

    public static String getRecordDir() {
        String pathRecordings = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_MUSIC) + "/Recordings";
        File folder = new File(pathRecordings);
        if (!folder.exists()) {
            if (!folder.mkdirs()) {
                Log.e(TAG, "could not create dir:" + pathRecordings);
            }
        }
        return pathRecordings;
    }

    public void updateRecordingsList() {
        String path = getRecordDir();
        if (BuildConfig.DEBUG) {
            Log.d(TAG, "Updating recordings from " + path);
        }

        savedRecordings.clear();

        File folder = new File(path);
        File[] files = folder.listFiles();
        if (files != null) {
            for (File f : files) {
                DataRecording dr = new DataRecording();
                dr.Name = f.getName();
                dr.Time = new Date(f.lastModified());
                savedRecordings.add(dr);
            }

            Collections.sort(savedRecordings, (o1, o2) -> Long.compare(o2.Time.getTime(), o1.Time.getTime()));
        } else {
            Log.e(TAG, "Could not enumerate files in recordings directory");
        }

        savedRecordingsObservable.notifyObservers();
    }
}
app/src/main/java/net/programmierecke/radiodroid2/history/TrackHistoryRepository.java
public class TrackHistoryRepository {
    private final static int HISTORY_PAGE_SIZE = 15;

    public interface GetItemCallback {
        /**
         * It will be ran in the DB thread
         *
         * @param trackHistoryEntry fetched entry
         * @param dao               could be used to immediately do changes in the DB
         */
        void onItemFetched(@Nullable TrackHistoryEntry trackHistoryEntry, @NonNull TrackHistoryDao dao);
    }

    private final TrackHistoryDao dao;
    private final Executor queryExecutor;
    private final LiveData<PagedList<TrackHistoryEntry>> allHistoryPaged;

    // For performance reasons we don't want to enforce history limit on every insert
    private final static int TRUNCATE_FREQUENCY = 20;
    private int insertsToTruncateLeft = 0;

    public TrackHistoryRepository(Application application) {
        RadioDroidDatabase db = RadioDroidDatabase.getDatabase(application);

        dao = db.songHistoryDao();
        queryExecutor = db.getQueryExecutor();

        allHistoryPaged = new LivePagedListBuilder<>(
                dao.getAllHistoryPositional(),
                new PagedList.Config.Builder()
                        .setPageSize(HISTORY_PAGE_SIZE)
                        .setEnablePlaceholders(true)
                        .build())
                .build();
    }

    public LiveData<PagedList<TrackHistoryEntry>> getAllHistoryPaged() {
        return allHistoryPaged;
    }

    public void insert(final TrackHistoryEntry historyEntry) {
        queryExecutor.execute(() -> {
            dao.insert(historyEntry);
            if (insertsToTruncateLeft == 0) {
                insertsToTruncateLeft = TRUNCATE_FREQUENCY;
                dao.truncateHistory(MAX_HISTORY_ITEMS_IN_TABLE);
            } else {
                insertsToTruncateLeft--;
            }
        });
    }

    public void setCurrentPlayingTrackEndTime(final Date time) {
        queryExecutor.execute(() -> dao.setCurrentPlayingTrackEndTime(time));
    }

    public void setLastHistoryItemEndTimeRelative(final int deltaSeconds) {
        queryExecutor.execute(() -> dao.setLastHistoryItemEndTimeRelative(deltaSeconds));
    }

    public void setTrackArtUrl(int id, @NonNull final String artUrl) {
        queryExecutor.execute(() -> dao.setTrackArtUrl(id, artUrl));
    }

    public void getLastInsertedHistoryItem(@NonNull final GetItemCallback callback) {
        queryExecutor.execute(() -> {
            TrackHistoryEntry item = dao.getLastInsertedHistoryItem();
            callback.onItemFetched(item, dao);
        });
    }

    public void deleteHistory() {
        queryExecutor.execute(() -> {
            dao.deleteHistory();
        });
    }
}
app/src/main/java/net/programmierecke/radiodroid2/FavouriteManager.java
public class FavouriteManager extends StationSaveManager {
    @Override
    protected String getSaveId() {
        return "favourites";
    }

    public FavouriteManager(Context ctx) {
        super(ctx);

        setStationStatusListener((station, favourite) -> {
            Intent local = new Intent();
            local.setAction(DataRadioStation.RADIO_STATION_LOCAL_INFO_CHAGED);
            local.putExtra(DataRadioStation.RADIO_STATION_UUID, station.StationUuid);
            LocalBroadcastManager.getInstance(ctx).sendBroadcast(local);
        });
    }

    @Override
    public void add(DataRadioStation station) {
        if (!has(station.StationUuid)) {
            super.add(station);
        }
    }

    @Override
    public void restore(DataRadioStation station, int pos) {
        if (!has(station.StationUuid)) {
            super.restore(station, pos);
        }
    }

    @Override
    void Load() {
        super.Load();
        updateShortcuts();
    }

    @Override
    void Save() {
        super.Save();
        updateShortcuts();
    }

    public void updateShortcuts() {
        if (Build.VERSION.SDK_INT >= 25 && !BuildConfig.IS_TESTING.get()) {
            int number = min(listStations.size(), ActivityMain.MAX_DYNAMIC_LAUNCHER_SHORTCUTS);
            SetDynamicAppLauncherShortcuts setDynamicAppLauncherShortcuts = new SetDynamicAppLauncherShortcuts(number);
            for (int i = 0; i < number; i++) {
                listStations.get(i).prepareShortcut(context, setDynamicAppLauncherShortcuts);
            }
        }
    }

    @TargetApi(25)
    class SetDynamicAppLauncherShortcuts implements DataRadioStation.ShortcutReadyListener {
        ArrayList<ShortcutInfo> shortcuts;
        int expectedNumber;

        SetDynamicAppLauncherShortcuts(int expectedNumber) {
            this.expectedNumber = expectedNumber;
            shortcuts = new ArrayList<ShortcutInfo>(expectedNumber);
        }

        @Override
        public void onShortcutReadyListener(ShortcutInfo shortcut) {
            shortcuts.add(shortcut);
            if (shortcuts.size() >= expectedNumber) {
                ShortcutManager shortcutManager = context.getApplicationContext().getSystemService(ShortcutManager.class);
                shortcutManager.removeAllDynamicShortcuts();
                shortcutManager.setDynamicShortcuts(shortcuts);
            }
        }
    }
}
app/src/main/java/net/programmierecke/radiodroid2/Utils.java
public class Utils {
    private static int loadIcons = -1;

    public static int parseIntWithDefault(String number, int defaultVal) {
        try {
            return Integer.parseInt(number);
        } catch (NumberFormatException e) {
            return defaultVal;
        }
    }

    public static String getCacheFile(Context ctx, String theURI) {
        StringBuilder chaine = new StringBuilder("");
        try {
            String aFileName = theURI.toLowerCase().replace("http://", "");
            aFileName = aFileName.toLowerCase().replace("https://", "");
            aFileName = sanitizeName(aFileName);

            File file = new File(ctx.getCacheDir().getAbsolutePath() + "/" + aFileName);
            Date lastModDate = new Date(file.lastModified());

            Date now = new Date();
            long millis = now.getTime() - file.lastModified();
            long secs = millis / 1000;
            long mins = secs / 60;
            long hours = mins / 60;

            if (BuildConfig.DEBUG) {
                Log.d("UTIL", "File last modified : " + lastModDate.toString() + " secs=" + secs + "  mins=" + mins + " hours=" + hours);
            }

            if (hours < 1) {
                FileInputStream aStream = new FileInputStream(file);
                BufferedReader rd = new BufferedReader(new InputStreamReader(aStream));
                String line;
                while ((line = rd.readLine()) != null) {
                    chaine.append(line);
                }
                rd.close();
                if (BuildConfig.DEBUG) {
                    Log.d("UTIL", "used cache for:" + theURI);
                }
                return chaine.toString();
            }
            if (BuildConfig.DEBUG) {
                Log.d("UTIL", "do not use cache, because too old:" + theURI);
            }
            return null;
        } catch (Exception e) {
            Log.e("UTIL", "getCacheFile() " + e);
        }
        return null;
    }

    public static void writeFileCache(Context ctx, String theURI, String content) {
        try {
            String aFileName = theURI.toLowerCase().replace("http://", "");
            aFileName = aFileName.toLowerCase().replace("https://", "");
            aFileName = sanitizeName(aFileName);

            File f = new File(ctx.getCacheDir() + "/" + aFileName);
            FileOutputStream aStream = new FileOutputStream(f);
            aStream.write(content.getBytes("utf-8"));
            aStream.close();
        } catch (Exception e) {
            Log.e("UTIL", "writeFileCache() could not write to cache file for:" + theURI);
        }
    }

    private static String downloadFeed(OkHttpClient httpClient, Context ctx, String theURI, boolean forceUpdate, Map<String, String> dictParams) {
        Log.i("DOWN", "Url=" + theURI);
        if (!forceUpdate) {
            String cache = getCacheFile(ctx, theURI);
            if (cache != null) {
                return cache;
            }
        }
        Log.i("DOWN", "Url=" + theURI + " (not cached)");

        try {
            HttpUrl url = HttpUrl.parse(theURI);
            Request.Builder requestBuilder = new Request.Builder().url(url);

            if (dictParams != null) {
                MediaType jsonMediaType = MediaType.parse("application/json; charset=utf-8");

                Gson gson = new Gson();
                String json = gson.toJson(dictParams);

                okhttp3.RequestBody requestBody = RequestBody.create(jsonMediaType, json);

                requestBuilder.post(requestBody);
            } else {
                requestBuilder.get();
            }

            Request request = requestBuilder.build();
            okhttp3.Response response = httpClient.newCall(request).execute();

            String responseStr = response.body().string();

            writeFileCache(ctx, theURI, responseStr);
            if (BuildConfig.DEBUG) {
                Log.d("UTIL", "wrote cache file for:" + theURI);
            }
            return responseStr;
        } catch (Exception e) {
            Log.e("UTIL", "downloadFeed() " + e);
        }

        return null;
    }

    public static String downloadFeedRelative(OkHttpClient httpClient, Context ctx, String theRelativeUri, boolean forceUpdate, Map<String, String> dictParams) {
        // try current server for download
        String currentServer = RadioBrowserServerManager.getCurrentServer();
        if (currentServer == null) {
            return null;
        }

        String endpoint = RadioBrowserServerManager.constructEndpoint(currentServer, theRelativeUri);
        String result = downloadFeed(httpClient, ctx, endpoint, forceUpdate, dictParams);
        if (result != null) {
            return result;
        }

        // get a list of all servers
        String[] serverList = RadioBrowserServerManager.getServerList(false);

        // try all other servers for download
        for (String newServer : serverList) {
            if (newServer.equals(currentServer)) {
                continue;
            }

            endpoint = RadioBrowserServerManager.constructEndpoint(newServer, theRelativeUri);
            result = downloadFeed(httpClient, ctx, endpoint, forceUpdate, dictParams);
            if (result != null) {
                // set the working server as new current server
                RadioBrowserServerManager.setCurrentServer(newServer);
                return result;
            }
        }

        return null;
    }

    public static String getRealStationLink(OkHttpClient httpClient, Context ctx, String stationId) {
        Log.i("UTIL", "StationUUID:" + stationId);
        String result = Utils.downloadFeedRelative(httpClient, ctx, "json/url/" + stationId, true, null);
        if (result != null) {
            Log.i("UTIL", result);
            JSONObject jsonObj;
            try {
                jsonObj = new JSONObject(result);
                return jsonObj.getString("url");
            } catch (Exception e) {
                Log.e("UTIL", "getRealStationLink() " + e);
            }
        }
        return null;
    }

    @Deprecated
    public static DataRadioStation getStationById(OkHttpClient httpClient, Context ctx, String stationId) {
        Log.w("UTIL", "Search by id:" + stationId);
        String result = Utils.downloadFeed(httpClient, ctx, "json/stations/byid/" + stationId, true, null);
        if (result != null) {
            try {
                List<DataRadioStation> list = DataRadioStation.DecodeJson(result);
                if (list != null) {
                    if (list.size() == 1) {
                        return list.get(0);
                    }
                    Log.e("UTIL", "stations by id did have length:" + list.size());
                }
            } catch (Exception e) {
                Log.e("UTIL", "getStationByid() " + e);
            }
        }
        return null;
    }

    public static DataRadioStation getStationByUuid(OkHttpClient httpClient, Context ctx, String stationUuid) {
        Log.w("UTIL", "Search by uuid:" + stationUuid);
        String result = Utils.downloadFeedRelative(httpClient, ctx, "json/stations/byuuid/" + stationUuid, true, null);
        if (result != null) {
            try {
                List<DataRadioStation> list = DataRadioStation.DecodeJson(result);
                if (list != null) {
                    if (list.size() == 1) {
                        return list.get(0);
                    }
                    Log.e("UTIL", "stations by uuid did have length:" + list.size());
                }
            } catch (Exception e) {
                Log.e("UTIL", "getStationByUuid() " + e);
            }
        }
        return null;
    }

    public static List<DataRadioStation> getStationsByUuid(OkHttpClient httpClient, Context ctx, Iterable<String> listUUids) {
        String uuids = TextUtils.join(",", listUUids);
        Log.d("UTIL", "Search by uuid for items");
        HashMap<String, String> p = new HashMap<String, String>();
        p.put("uuids", uuids);
        String result = Utils.downloadFeedRelative(httpClient, ctx, "json/stations/byuuid", true, p);
        if (result != null) {
            try {
                List<DataRadioStation> list = DataRadioStation.DecodeJson(result);
                if (list != null) {
                    return list;
                }else{
                    Log.e("UTIL", "stations by uuid was null");
                }
            } catch (Exception e) {
                Log.e("UTIL", "getStationsByUuid() " + e);
            }
        }
        return null;
    }

    public static @Nullable
    DataRadioStation getCurrentOrLastStation(@NonNull Context ctx) {
        DataRadioStation station = PlayerServiceUtil.getCurrentStation();
        if (station == null) {
            RadioDroidApp radioDroidApp = (RadioDroidApp) ctx.getApplicationContext();
            HistoryManager historyManager = radioDroidApp.getHistoryManager();
            station = historyManager.getFirst();
        }

        return station;
    }

    public static void showMpdServersDialog(final RadioDroidApp radioDroidApp, final FragmentManager fragmentManager, @Nullable final DataRadioStation station) {
        Fragment oldFragment = fragmentManager.findFragmentByTag(PlayerSelectorDialog.FRAGMENT_TAG);
        if (oldFragment != null && oldFragment.isVisible()) {
            return;
        }

        PlayerSelectorDialog playerSelectorDialogFragment = new PlayerSelectorDialog(radioDroidApp.getMpdClient(), station);
        playerSelectorDialogFragment.show(fragmentManager, PlayerSelectorDialog.FRAGMENT_TAG);
    }

    public static void showPlaySelection(final RadioDroidApp radioDroidApp, final DataRadioStation station, final FragmentManager fragmentManager) {
        SharedPreferences sharedPref = PreferenceManager.getDefaultSharedPreferences(radioDroidApp);
        final boolean externalAvailable = sharedPref.getBoolean("play_external", false);

        CastHandler castHandler = radioDroidApp.getCastHandler();
        final boolean castAvailable = castHandler.isCastSessionAvailable();

        final boolean mpdAvailable = radioDroidApp.getMpdClient().isMpdEnabled();

        if (castAvailable && !externalAvailable && !mpdAvailable) {
            new PlayStationTask(station, radioDroidApp.getApplicationContext(),
                    url -> castHandler.playRemote(station.Name, url, station.IconUrl),
                    null)
                    .execute();
        } else if (externalAvailable || mpdAvailable) {
            showMpdServersDialog(radioDroidApp, fragmentManager, station);
        } else {
            playAndWarnIfMetered(radioDroidApp, station, PlayerType.RADIODROID, () -> play(radioDroidApp, station));
        }
    }

    public static void playAndWarnIfMetered(RadioDroidApp radioDroidApp, DataRadioStation station, PlayerType playerType, Runnable playFunc) {
        playAndWarnIfMetered(radioDroidApp, station, playerType, playFunc,
                (station1, playerType1) -> {
                    // Making sure that resuming from notification or some external event will actually resume
                    // and not issue warning a second time.
                    PlayerServiceUtil.setStation(station1);
                    PlayerServiceUtil.warnAboutMeteredConnection(playerType1);
                });
    }

    public static boolean urlIndicatesHlsStream(String streamUrl) {
        final Pattern p = Pattern.compile(".*\\.m3u8([#?\\s].*)?$");
        return p.matcher(streamUrl).matches();
    }

    public interface MeteredWarningCallback {
        void warn(DataRadioStation station, PlayerType playerType);
    }

    // TODO: Sort out the indirection when PlayerService won't need aidl and we won't need to have
    //  PlayerServiceUtil as a proxy between common code and the service.
    public static void playAndWarnIfMetered(RadioDroidApp radioDroidApp, DataRadioStation station, PlayerType playerType,
                                            Runnable playFunc, MeteredWarningCallback warningCallback) {
        SharedPreferences sharedPref = PreferenceManager.getDefaultSharedPreferences(radioDroidApp);
        final boolean warnOnMetered = sharedPref.getBoolean("warn_no_wifi", false);

        if (warnOnMetered && ConnectivityChecker.getCurrentConnectionType(radioDroidApp) == ConnectivityChecker.ConnectionType.METERED) {
            warningCallback.warn(station, playerType);
        } else {
            playFunc.run();
        }
    }

    public static void play(final RadioDroidApp radioDroidApp, final DataRadioStation station) {
        PlayerServiceUtil.play(station);
    }

    public static boolean shouldLoadIcons(final Context context) {
        switch (loadIcons) {
            case -1:
                if (PreferenceManager.getDefaultSharedPreferences(context.getApplicationContext()).getBoolean("load_icons", false)) {
                    loadIcons = 1;
                    return true;
                } else {
                    loadIcons = 0;
                    return true;
                }
            case 0:
                return false;
            case 1:
                return true;
        }
        return false;
    }

    public static String getTheme(final Context context) {
        SharedPreferences sharedPref = PreferenceManager.getDefaultSharedPreferences(context);
        return sharedPref.getString("theme_name", context.getResources().getString(R.string.theme_light));
    }

    public static int getThemeResId(final Context context) {
        String selectedTheme = getTheme(context);
        if (selectedTheme.equals(context.getResources().getString(R.string.theme_dark)))
            return R.style.MyMaterialTheme_Dark;
        else
            return R.style.MyMaterialTheme;
    }

    public static boolean isDarkTheme(final Context context) {
        return getThemeResId(context) == R.style.MyMaterialTheme_Dark;
    }

    public static int getTimePickerThemeResId(final Context context) {
        int theme;
        if (getThemeResId(context) == R.style.MyMaterialTheme_Dark)
            theme = R.style.DialogTheme_Dark;
        else
            theme = R.style.DialogTheme;
        return theme;
    }

    public static boolean useCircularIcons(final Context context) {
        SharedPreferences sharedPref = PreferenceManager.getDefaultSharedPreferences(context);
        return sharedPref.getBoolean("circular_icons", false);
    }

    // Storage Permissions
    private static String[] PERMISSIONS_STORAGE = {
            Manifest.permission.WRITE_EXTERNAL_STORAGE
    };

    public static boolean verifyStoragePermissions(Activity activity, int request_id) {
        // Check if we have write permission
        int permission = ContextCompat.checkSelfPermission(activity, Manifest.permission.WRITE_EXTERNAL_STORAGE);

        if (permission != PackageManager.PERMISSION_GRANTED) {
            // We don't have permission so prompt the user
            ActivityCompat.requestPermissions(
                    activity,
                    PERMISSIONS_STORAGE,
                    request_id
            );
            return false;
        }

        return true;
    }

    public static boolean verifyStoragePermissions(Fragment fragment, int request_id) {
        // Check if we have write permission
        int permission = ContextCompat.checkSelfPermission(fragment.requireContext(), Manifest.permission.WRITE_EXTERNAL_STORAGE);

        if (permission != PackageManager.PERMISSION_GRANTED) {
            // We don't have permission so prompt the user
            fragment.requestPermissions(PERMISSIONS_STORAGE, request_id);
            return false;
        }

        return true;
    }

    public static String getReadableBytes(double bytes) {
        String[] str = new String[]{"B", "KB", "MB", "GB", "TB"};
        for (String aStr : str) {
            if (bytes < 1024) {
                return String.format(Locale.getDefault(), "%1$,.1f %2$s", bytes, aStr);
            }
            bytes = bytes / 1024;
        }
        return String.format(Locale.getDefault(), "%1$,.1f %2$s", bytes * 1024, str[str.length - 1]);
    }

    public static String sanitizeName(String str) {
        return str.replaceAll("\\W+", "_").replaceAll("^_+", "").replaceAll("_+$", "");
    }

    public static boolean hasWifiConnection(Context context) {
        ConnectivityManager connManager = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);
        NetworkInfo mWifi = connManager.getNetworkInfo(ConnectivityManager.TYPE_WIFI);

        return mWifi.isConnected();
    }

    public static boolean hasAnyConnection(Context context) {
        ConnectivityManager connManager = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);
        NetworkInfo netInfo = connManager.getActiveNetworkInfo();
        //should check null because in airplane mode it will be null
        return (netInfo != null && netInfo.isConnected());
    }

    public static boolean bottomNavigationEnabled(Context context) {
        SharedPreferences sharedPref = PreferenceManager.getDefaultSharedPreferences(context);
        return sharedPref.getBoolean("bottom_navigation", true);
    }

    public static String formatStringWithNamedArgs(String format, Map<String, String> args) {
        StringBuilder builder = new StringBuilder(format);
        for (Map.Entry<String, String> entry : args.entrySet()) {
            final String key = "${" + entry.getKey() + "}";
            int startIdx = 0;
            while (true) {
                final int keyIdx = builder.indexOf(key, startIdx);

                if (keyIdx == -1) {
                    break;
                }

                builder.replace(keyIdx, keyIdx + key.length(), entry.getValue());
                startIdx = keyIdx + entry.getValue().length();
            }
        }

        return builder.toString();
    }

    public static int themeAttributeToColor(int themeAttributeId, Context context, int fallbackColorId) {
        TypedValue outValue = new TypedValue();
        Resources.Theme theme = context.getTheme();
        boolean wasResolved = theme.resolveAttribute(themeAttributeId, outValue, true);
        if (wasResolved) {
            return outValue.resourceId == 0 ? outValue.data : ContextCompat.getColor(context, outValue.resourceId);
        } else {
            return fallbackColorId;
        }
    }

    public static int getIconColor(Context context) {
        return themeAttributeToColor(R.attr.menuTextColorDefault, context, Color.LTGRAY);
    }

    public static int getAccentColor(Context context) {
        return themeAttributeToColor(R.attr.colorAccent, context, Color.LTGRAY);
    }

    /**
     * Add proxy to an okhttp builder.
     *
     * @return true if successful, false otherwise
     */
    public static boolean setOkHttpProxy(@NonNull OkHttpClient.Builder builder, @NonNull final ProxySettings proxySettings) {
        if (proxySettings.type == Proxy.Type.DIRECT) {
            return true;
        }
        if (TextUtils.isEmpty(proxySettings.host)) {
            return false;
        }
        if (proxySettings.port < 1 || proxySettings.port > 65535) {
            return false;
        }
        InetSocketAddress proxyAddress = InetSocketAddress.createUnresolved(proxySettings.host, proxySettings.port);
        Proxy proxy = new Proxy(proxySettings.type, proxyAddress);

        builder.proxy(proxy);

        if (!proxySettings.login.isEmpty()) {
            Authenticator proxyAuthenticator = new Authenticator() {
                @Override
                public Request authenticate(Route route, Response response) throws IOException {
                    String credential = Credentials.basic(proxySettings.login, proxySettings.password);
                    return response.request().newBuilder()
                            .header("Proxy-Authorization", credential)
                            .build();
                }
            };

            builder.authenticator(proxyAuthenticator);
        }

        return true;
    }

    public static Uri resourceToUri(Resources resources, int resID) {
        return Uri.parse(ContentResolver.SCHEME_ANDROID_RESOURCE + "://" +
                resources.getResourcePackageName(resID) + '/' +
                resources.getResourceTypeName(resID) + '/' +
                resources.getResourceEntryName(resID));
    }

    public static IconicsDrawable IconicsIcon(Context context, IIcon icon) {
        return new IconicsDrawable(context, icon).size(IconicsSize.TOOLBAR_ICON_SIZE).padding(IconicsSize.TOOLBAR_ICON_PADDING).color(IconicsColor.colorInt(getIconColor(context)));
    }

    public static String getMimeType(String url, String defaultMimeType) {
        String type = defaultMimeType;
        String extension = MimeTypeMap.getFileExtensionFromUrl(url);
        if (extension != null) {
            type = MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension);
        }
        return type;
    }

    public static OkHttpClient.Builder enableTls12OnPreLollipop(OkHttpClient.Builder client) {
        if (Build.VERSION.SDK_INT >= 16 && Build.VERSION.SDK_INT < 22) {
            try {
                TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
                trustManagerFactory.init((KeyStore)null);
                TrustManager[] tmList = trustManagerFactory.getTrustManagers();
                Log.i("OkHttpTLSCompat", "Found trustmanagers:"+tmList.length);
                X509TrustManager tm = (X509TrustManager)tmList[0];

                SSLContext sc = SSLContext.getInstance("TLSv1.2");
                sc.init(null, null, null);
                client.sslSocketFactory(new Tls12SocketFactory(sc.getSocketFactory()), tm);

                ConnectionSpec cs = new ConnectionSpec.Builder(ConnectionSpec.MODERN_TLS)
                        .tlsVersions(TlsVersion.TLS_1_2)
                        .build();

                List<ConnectionSpec> specs = new ArrayList<>();
                specs.add(cs);
                specs.add(ConnectionSpec.COMPATIBLE_TLS);
                specs.add(ConnectionSpec.CLEARTEXT);

                client.connectionSpecs(specs);
            } catch (Exception exc) {
                Log.e("OkHttpTLSCompat", "Error while setting TLS 1.2", exc);
            }
        }

        return client;
    }
}


Question: import java.util.Calendar;
import java.util.Date;
import java.util.Map;
import android.app.Notification;
import android.app.NotificationChannel;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.bluetooth.BluetoothA2dp;
import android.bluetooth.BluetoothHeadset;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.SharedPreferences;
import android.content.res.Resources;
import android.graphics.Bitmap;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.Drawable;
import android.media.AudioManager;
import android.media.ToneGenerator;
import android.media.audiofx.AudioEffect;
import android.net.wifi.WifiManager;
import android.os.Build;
import android.os.CountDownTimer;
import android.os.Handler;
import android.os.IBinder;
import android.os.Parcelable;
import android.os.PowerManager;
import android.os.RemoteException;
import android.support.v4.media.MediaMetadataCompat;
import androidx.annotation.NonNull;
import androidx.core.app.JobIntentService;
import androidx.media.app.NotificationCompat.MediaStyle;
import androidx.media.session.MediaButtonReceiver;
import android.support.v4.media.session.MediaSessionCompat;
import android.support.v4.media.session.PlaybackStateCompat;
import android.text.TextUtils;
import android.util.Log;
import android.util.TypedValue;
import android.view.KeyEvent;
import android.widget.Toast;
import androidx.core.app.NotificationCompat;
import androidx.core.app.NotificationManagerCompat;
import androidx.core.content.res.ResourcesCompat;
import androidx.core.graphics.drawable.RoundedBitmapDrawable;
import androidx.core.graphics.drawable.RoundedBitmapDrawableFactory;
import androidx.localbroadcastmanager.content.LocalBroadcastManager;
import androidx.preference.PreferenceManager;
import com.squareup.picasso.Picasso;
import com.squareup.picasso.Target;
import net.programmierecke.radiodroid2.ActivityMain;
import net.programmierecke.radiodroid2.BuildConfig;
import net.programmierecke.radiodroid2.FavouriteManager;
import net.programmierecke.radiodroid2.HistoryManager;
import net.programmierecke.radiodroid2.IPlayerService;
import net.programmierecke.radiodroid2.R;
import net.programmierecke.radiodroid2.RadioDroidApp;
import net.programmierecke.radiodroid2.Utils;
import net.programmierecke.radiodroid2.history.TrackHistoryEntry;
import net.programmierecke.radiodroid2.history.TrackHistoryRepository;
import net.programmierecke.radiodroid2.players.PlayState;
import net.programmierecke.radiodroid2.players.selector.PlayerType;
import net.programmierecke.radiodroid2.station.DataRadioStation;
import net.programmierecke.radiodroid2.station.live.ShoutcastInfo;
import net.programmierecke.radiodroid2.station.live.StreamLiveInfo;
import net.programmierecke.radiodroid2.players.RadioPlayer;
import net.programmierecke.radiodroid2.recording.RecordingsManager;
import net.programmierecke.radiodroid2.recording.RunningRecordingInfo;
import static android.content.Intent.ACTION_MEDIA_BUTTON;
                        }

                        lastPlayStartTime = System.currentTimeMillis();

                        Intent i = new Intent(AudioEffect.ACTION_OPEN_AUDIO_EFFECT_CONTROL_SESSION);
                        i.putExtra(AudioEffect.EXTRA_AUDIO_SESSION, audioSessionId);
                        i.putExtra(AudioEffect.EXTRA_PACKAGE_NAME, getPackageName());
                        itsContext.sendBroadcast(i);
                        break;
                    }
                    default: {
                        if (state != PlayState.PrePlaying) {
                            disableMediaSession();
                        }

                        if (audioSessionId > 0) {
                            if (BuildConfig.DEBUG) {
                                Log.d(TAG, "Close audio effect control session, session id=" + audioSessionId);
                            }

                            Intent i = new Intent(AudioEffect.ACTION_CLOSE_AUDIO_EFFECT_CONTROL_SESSION);
                            i.putExtra(AudioEffect.EXTRA_AUDIO_SESSION, audioSessionId);
                            i.putExtra(AudioEffect.EXTRA_PACKAGE_NAME, getPackageName());
                            itsContext.sendBroadcast(i);
                        }

                        if (state == PlayState.Idle) {
                            stop();
                        }

                        break;
                    }
                }

                if (state != PlayState.Paused && state != PlayState.Idle) {
                    startMeteredConnectionListener();
                } else {
                    stopMeteredConnectionListener();
                }

                updateNotification(state);

                final Intent intent = new Intent();
                intent.setAction(PLAYER_SERVICE_STATE_CHANGE);
                intent.putExtra(PLAYER_SERVICE_STATE_EXTRA_KEY, (Parcelable) state);
                LocalBroadcastManager.getInstance(itsContext).sendBroadcast(intent);
            }
        });
    }

    @Override
    public void onPlayerWarning(int messageId) {
        onPlayerError(messageId);
    }

    @Override
    public void onPlayerError(int messageId) {
        handler.post(() -> {
            PlayerService.this.lastErrorFromPlayer = messageId;

            toastOnUi(messageId);
            updateNotification();
        });
    }

    @Override
    public void onBufferedTimeUpdate(long bufferedMs) {

    }

    @Override
    public void foundShoutcastStream(ShoutcastInfo info, boolean isHls) {
        this.streamInfo = info;
        this.isHls = isHls;
        if (info != null) {
//            if (info.audioName != null) {
//                if (!info.audioName.trim().equals("")) {
//                    currentStationName = info.audioName.trim();
//                }
//            }

            if (BuildConfig.DEBUG) {
                Log.d(TAG, "Metadata offset:" + info.metadataOffset);
                Log.d(TAG, "Bitrate:" + info.bitrate);
                Log.d(TAG, "Name:" + info.audioName);
                Log.d(TAG, "Hls:" + isHls);
                Log.d(TAG, "Server:" + info.serverName);
                Log.d(TAG, "AudioInfo:" + info.audioInfo);
            }
        }
        sendBroadCast(PLAYER_SERVICE_META_UPDATE);
    }

    @Override
    public void foundLiveStreamInfo(final StreamLiveInfo liveInfo) {
        StreamLiveInfo oldLiveInfo = this.liveInfo;
        this.liveInfo = liveInfo;

        if (BuildConfig.DEBUG) {
            Map<String, String> rawMetadata = liveInfo.getRawMetadata();
            for (String key : rawMetadata.keySet()) {
                Log.i(TAG, "INFO:" + key + "=" + rawMetadata.get(key));
            }
        }

        if (oldLiveInfo == null || !oldLiveInfo.getTitle().equals(liveInfo.getTitle())) {
            sendBroadCast(PLAYER_SERVICE_META_UPDATE);
            updateNotification();

            Calendar calendar = Calendar.getInstance();
            Date currentTime = calendar.getTime();

            trackHistoryRepository.getLastInsertedHistoryItem((trackHistoryEntry, dao) -> {
                if (trackHistoryEntry != null && trackHistoryEntry.title.equals(liveInfo.getTitle())) {
                    // Prevent from generating several same entries when rapidly doing pause and resume.
                    trackHistoryEntry.endTime = new Date(0);
                    dao.update(trackHistoryEntry);
                } else {
                    dao.setCurrentPlayingTrackEndTime(currentTime);

Answer:                     TrackHistoryEntry newTrackHistoryEntry = new TrackHistoryEntry();