org.y20k.transistor.core.Station.java Source code

Java tutorial

Introduction

Here is the source code for org.y20k.transistor.core.Station.java

Source

/**
 * Station.java
 * Implements the Station class
 * A Station handles station-related data, e.g. the name and the streaming URL
 * <p>
 * This file is part of
 * TRANSISTOR - Radio App for Android
 * <p>
 * Copyright (c) 2015-17 - Y20K.org
 * Licensed under the MIT-License
 * http://opensource.org/licenses/MIT
 */

package org.y20k.transistor.core;

import android.app.Activity;
import android.content.ContentValues;
import android.content.Context;
import android.content.Intent;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.net.Uri;
import android.os.Bundle;
import android.os.Parcel;
import android.os.Parcelable;
import android.support.annotation.NonNull;
import android.support.v4.content.LocalBroadcastManager;
import android.util.Log;
import android.util.Xml;

import org.xmlpull.v1.XmlPullParser;
import org.xmlpull.v1.XmlPullParserException;
import org.y20k.transistor.helpers.LogHelper;
import org.y20k.transistor.helpers.SingletonProperties;
import org.y20k.transistor.helpers.StorageHelper;
import org.y20k.transistor.helpers.TransistorKeys;
import org.y20k.transistor.sqlcore.StationsDbContract;
import org.y20k.transistor.sqlcore.StationsDbHelper;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileOutputStream;
import java.io.FileReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Locale;
import java.util.Scanner;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
 * Station class
 */
public final class Station implements Comparable<Station>, Parcelable {

    /* Define log tag */
    private static final String LOG_TAG = Station.class.getSimpleName();

    /**
     * Station  ID , the primary key in DB, and it's UNIQUE integer
     */
    public long _ID;
    /**
     * Station  UNIQUE ID , used to identify station in DB, and should be UNIQUE and it can be string value
     */
    public String UNIQUE_ID;
    /**
     * Station Title
     */
    public String TITLE;
    /**
     * Station Sub Title
     */
    public String SUBTITLE;

    /**
     * This is image link (this should be external http link or in storage file link to image)
     */
    public String IMAGE_PATH;
    /**
     * This is small image link (icon) (this should be external http link or in storage file link to image)
     */
    public String SMALL_IMAGE_PATH;

    /**
     * This is local image name (after cache to storage it should be with that name)
     */
    public String IMAGE_FILE_NAME;
    /**
     * This is local small image name (icon)  (after cache to storage it should be with that name)
     */
    public String SMALL_IMAGE_FILE_NAME;

    /**
     * Station Stream URI (Mandatory)
     */
    public String StreamURI;

    /**
     * Station CONTENT TYPE (value auto detected / or can be read from xml metadata - if it's imported using xml file)
     */
    public String CONTENT_TYPE;

    /**
     * Station DESCRIPTION (metadata) - string value and not have any formats
     */
    public String DESCRIPTION;

    /**
     * Station DESCRIPTION (metadata) - string value and not have any formats
     */
    public int RATING;

    /**
     * Station COMMA SEPARATED TAGS (metadata)
     */
    public String COMMA_SEPARATED_TAGS;

    /**
     * Station CATEGORY
     */
    public String CATEGORY;

    /**
     * Station Html Description , with HTML formal, it will be visible inside in-app WebView
     * with default header\styles located in \assets\webViewStyleDefaults.html
     */
    public String MarkdownDescription;

    /**
     * Station IS FAVOURITE (user favourite)
     */
    public int IS_FAVOURITE;

    /**
     * Station is Thump Up by user (not used yet) (feature not used yet)
     */
    public String THUMP_UP_STATUS;

    private File mStationImageFile;
    private File mStationSmallImageFile;
    private Bitmap mStationImage;

    //inserted stations if user import XML file
    private ArrayList<Station> mInsertedStations = new ArrayList<Station>();

    /* Supported xml import file content types */
    private static final String[] CONTENT_TYPES_IMPORT_XML = { "application/xml" };

    /* Supported audio file content types */
    private static final String[] CONTENT_TYPES_MPEG = { "audio/mpeg" };
    private static final String[] CONTENT_TYPES_OGG = { "audio/ogg", "application/ogg" };
    private static final String[] CONTENT_TYPES_AAC = { "audio/aac", "audio/aacp" };

    /* Supported playlist content types */
    private static final String[] CONTENT_TYPES_PLS = { "audio/x-scpls" };
    private static final String[] CONTENT_TYPES_M3U = { "audio/x-mpegurl", "application/vnd.apple.mpegurl",
            "audio/mpegurl" };

    /* Regular expression to extract content-type and charset from header string */
    private static final Pattern CONTENT_TYPE_PATTERN = Pattern.compile("([^;]*)(; ?charset=([^;]+))?");

    /* Main class variables */
    public String mPlaylistFileContent;
    private boolean mPlayback;
    private Bundle mStationFetchResults;

    /* Constructor when given folder and remote location */
    public Station(File folder, URL fileLocation, Activity mActivity) throws IOException, XmlPullParserException {
        MainConstructor(folder, fileLocation, mActivity);
    }

    private void MainConstructor(File folder, URL fileLocation, Activity mActivity)
            throws XmlPullParserException, IOException {
        // create results bundle
        mStationFetchResults = new Bundle();

        // determine content type of remote file
        ContentType contentType = getContentType(fileLocation);
        ContentType resultContentType = contentType;

        LogHelper.v(LOG_TAG, "Content type of given file is " + contentType);

        //is XML import file
        if (isXMLFile(contentType)) {
            //READ THE XML FILE ITEMS
            readXmlEntries(fileLocation, mActivity);
            // set playback state
            mPlayback = false;
            return;
        }
        // content type is raw audio file
        else if (isAudioFile(contentType)) {
            // use raw audio file for station data
            StreamURI = fileLocation.toString().trim();
            TITLE = detactStationName(fileLocation);
            // save results
            mStationFetchResults.putParcelable(TransistorKeys.RESULT_STREAM_TYPE, contentType);
            mStationFetchResults.putBoolean(TransistorKeys.RESULT_FETCH_ERROR, false);
        }

        // content type is playlist
        else if (isPlaylist(contentType)) {
            // download and parse station data from playlist file
            mPlaylistFileContent = downloadPlaylistFile(fileLocation);

            // parse result of downloadPlaylistFile
            if (parse(mPlaylistFileContent, this) && StreamURI != null) {
                TITLE = detactStationName(fileLocation);
                // save results
                mStationFetchResults.putParcelable(TransistorKeys.RESULT_PLAYLIST_TYPE, contentType);
                resultContentType = getContentType(Uri.parse(StreamURI));
                mStationFetchResults.putParcelable(TransistorKeys.RESULT_STREAM_TYPE, resultContentType);
                mStationFetchResults.putString(TransistorKeys.RESULT_FILE_CONTENT, mPlaylistFileContent);
                mStationFetchResults.putBoolean(TransistorKeys.RESULT_FETCH_ERROR, false);

            } else {
                // save error flag and file content in results
                mStationFetchResults.putParcelable(TransistorKeys.RESULT_PLAYLIST_TYPE, contentType);
                mStationFetchResults.putString(TransistorKeys.RESULT_FILE_CONTENT,
                        "\n[File probably does not contain a valid streaming URL.]");
                mStationFetchResults.putBoolean(TransistorKeys.RESULT_FETCH_ERROR, true);
            }

            // content type is none of the above
        } else if (contentType != null) {
            // save results and return
            mStationFetchResults.putParcelable(TransistorKeys.RESULT_STREAM_TYPE, contentType);
            mStationFetchResults.putBoolean(TransistorKeys.RESULT_FETCH_ERROR, true);
            return;
            // no content type
        } else {
            // save error flag in results and return
            mStationFetchResults.putBoolean(TransistorKeys.RESULT_FETCH_ERROR, true);
            return;
        }

        //---This part of code will run if it's not import from XML---

        // set playback state
        mPlayback = false;

        // set Transistor's image file object
        final String uNIQUE_ID = fileLocation.toString().replaceAll("[:/]", "_");
        IMAGE_FILE_NAME = uNIQUE_ID + ".png";
        SMALL_IMAGE_FILE_NAME = uNIQUE_ID + "_small" + ".png";

        //add station to DB
        // strip out problematic characters
        UNIQUE_ID = uNIQUE_ID;
        CONTENT_TYPE = resultContentType.getTypeString();
        IMAGE_PATH = getFavIconUrlString(StreamURI); //default to fav icon
        SMALL_IMAGE_FILE_NAME = IMAGE_PATH; //default to fav icon
        AddStationItemToDb(this, mActivity); //save to DB
    }

    /* Constructor when given folder and file on sd card */
    public Station(File folder, Uri fileLocation, Activity mActivity) {

        // create results bundle
        mStationFetchResults = new Bundle();

        // read local file and put result into mPlaylistFileContent
        File localFile = new File(fileLocation.getPath());
        if (localFile.exists()) {
            mPlaylistFileContent = readPlaylistFile(localFile);
        } else {
            LogHelper.v(LOG_TAG, "File does not exist " + localFile);
        }

        // parse the raw content of playlist file (mPlaylistFileContent)
        if (parse(mPlaylistFileContent, this) && StreamURI != null) {
            URL streamURL = null;
            try {
                MainConstructor(folder, streamURL, mActivity);
                streamURL = new URL(StreamURI);
            } catch (MalformedURLException e) {
                e.printStackTrace();
            } catch (XmlPullParserException e) {
                e.printStackTrace();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }

    }

    private String readPlaylistFile(File playlistFile) {

        try (BufferedReader br = new BufferedReader(new FileReader(playlistFile))) {
            String line;
            int counter = 0;
            StringBuilder sb = new StringBuilder("");

            // read until last line reached or until line five
            while ((line = br.readLine()) != null && counter < 5) {
                sb.append(line);
                sb.append("\n");
                counter++;
            }

            // set mPlaylistFileContent and return String
            mPlaylistFileContent = sb.toString();
            return sb.toString();

        } catch (IOException e) {
            LogHelper.e(LOG_TAG, "Unable to read playlist file: " + playlistFile.toString());
            // set mPlaylistFileContent and return null
            mPlaylistFileContent = "[IO error. Unable to read playlist file: " + playlistFile.toString() + "]";
            return null;
        }

    }

    public Station() {
        //nothing for now, this will create empty station object
    }

    //v3
    private void readXmlEntries(URL fileLocation, Activity mActivity) throws XmlPullParserException, IOException {
        InputStream stream = null;
        try {
            HttpURLConnection conn = (HttpURLConnection) fileLocation.openConnection();
            conn.setReadTimeout(10000 /* milliseconds */);
            conn.setConnectTimeout(15000 /* milliseconds */);
            conn.setRequestMethod("GET");
            conn.setDoInput(true);
            // Starts the query
            conn.connect();
            stream = conn.getInputStream();
            readXmlElementsFromInputStream(mActivity, stream);
        } finally {
            // close InputStream after the app is
            // finished using it.
            if (stream != null) {
                stream.close();
            }
        }
    }

    public void readXmlElementsFromInputStream(Activity mActivity, InputStream stream)
            throws XmlPullParserException, IOException {
        //parse
        XmlPullParser parser = Xml.newPullParser();
        parser.setFeature(XmlPullParser.FEATURE_PROCESS_NAMESPACES, false);
        parser.setInput(stream, null);
        parser.nextTag();

        List entries = new ArrayList();

        parser.require(XmlPullParser.START_TAG, null, "channels");
        while (parser.next() != XmlPullParser.END_TAG) {
            if (parser.getEventType() != XmlPullParser.START_TAG) {
                continue;
            }
            String name = parser.getName();
            // Starts by looking for the entry tag
            if (name.equals("entry")) {
                parser.require(XmlPullParser.START_TAG, null, "entry");

                Station stationItem = new Station();
                //reset some variables
                stationItem.MarkdownDescription = "";

                while (parser.next() != XmlPullParser.END_TAG) {
                    if (parser.getEventType() != XmlPullParser.START_TAG) {
                        continue;
                    }
                    String tagName = parser.getName();
                    if (tagName.equals("unique_id")) {
                        parser.require(XmlPullParser.START_TAG, null, "unique_id");
                        stationItem.UNIQUE_ID = readXmlElementText(parser);
                        stationItem.IMAGE_FILE_NAME = stationItem.UNIQUE_ID + ".png";
                        stationItem.SMALL_IMAGE_FILE_NAME = stationItem.UNIQUE_ID + "_small.png";
                    } else if (tagName.equals("title")) {
                        parser.require(XmlPullParser.START_TAG, null, "title");
                        stationItem.TITLE = readXmlElementText(parser);
                    } else if (tagName.equals("subtitle")) {
                        parser.require(XmlPullParser.START_TAG, null, "subtitle");
                        stationItem.SUBTITLE = readXmlElementText(parser);
                    } else if (tagName.equals("image")) {
                        parser.require(XmlPullParser.START_TAG, null, "image");
                        stationItem.IMAGE_PATH = readXmlElementText(parser);
                    } else if (tagName.equals("uri")) {
                        parser.require(XmlPullParser.START_TAG, null, "uri");
                        stationItem.StreamURI = readXmlElementText(parser);
                    } else if (tagName.equals("content_type")) {
                        parser.require(XmlPullParser.START_TAG, null, "content_type");
                        stationItem.CONTENT_TYPE = readXmlElementText(parser);
                    } else if (tagName.equals("rating")) {
                        parser.require(XmlPullParser.START_TAG, null, "rating");
                        String ratingVal = readXmlElementText(parser);
                        stationItem.RATING = getIntegerRating(ratingVal);
                    } else if (tagName.equals("comma_separated_tags")) {
                        parser.require(XmlPullParser.START_TAG, null, "comma_separated_tags");
                        stationItem.COMMA_SEPARATED_TAGS = readXmlElementText(parser);
                    } else if (tagName.equals("category")) {
                        parser.require(XmlPullParser.START_TAG, null, "category");
                        stationItem.CATEGORY = readXmlElementText(parser);
                    } else if (tagName.equals("markdown_description")) {
                        parser.require(XmlPullParser.START_TAG, null, "markdown_description");
                        stationItem.MarkdownDescription = readXmlElementText(parser);
                    } else if (tagName.equals("small_image_URL")) {
                        parser.require(XmlPullParser.START_TAG, null, "small_image_URL");
                        stationItem.SMALL_IMAGE_PATH = readXmlElementText(parser);
                    } else if (tagName.equals("description")) {
                        parser.require(XmlPullParser.START_TAG, null, "description");
                        stationItem.DESCRIPTION = readXmlElementText(parser);
                    } else {
                        skipXmlTagParse(parser);
                    }
                }
                if (stationItem.UNIQUE_ID != null && !stationItem.UNIQUE_ID.isEmpty()
                        && stationItem.StreamURI != null && !stationItem.StreamURI.isEmpty()) {

                    //get content type of station streamUrl
                    ContentType itemCnt = getContentType(Uri.parse(stationItem.StreamURI));

                    //check 3.1.2 station URL (if playlist then extract first station URL
                    if (isPlaylist(itemCnt)) {
                        // download and parse station data from playlist file
                        String itemPlaylistFileContent = downloadPlaylistFile(new URL(stationItem.StreamURI));
                        // parse result of downloadPlaylistFile and fill streamUrl and Title/subtitle
                        if (parse(itemPlaylistFileContent, stationItem)) {
                            //get content type after updating the streamUrl
                            itemCnt = getContentType(Uri.parse(stationItem.StreamURI));
                        } else {
                            LogHelper.e(LOG_TAG, "\n[File probably does not contain a valid streaming URL."
                                    + stationItem.StreamURI + "]");
                            continue; //continue and don't save this station to DB
                        }
                    }

                    //update content type of station and override the provided one if available
                    if (itemCnt.type != null && !itemCnt.type.isEmpty()) {
                        stationItem.CONTENT_TYPE = itemCnt.type;
                    }

                    //add default Image URL
                    if (stationItem.IMAGE_PATH == null || stationItem.IMAGE_PATH.isEmpty()) {
                        IMAGE_PATH = getFavIconUrlString(stationItem.StreamURI); //default to fav icon
                    }
                    if (stationItem.SMALL_IMAGE_PATH == null || stationItem.SMALL_IMAGE_PATH.isEmpty()) {
                        SMALL_IMAGE_PATH = IMAGE_PATH; //default AS IMAGE_PATH
                    }
                    //add station to db
                    AddStationItemToDb(stationItem, mActivity);

                    //for reference and to inform the adaptor we need the list of mInsertedStations
                    mInsertedStations.add(stationItem);
                }
            } else {
                skipXmlTagParse(parser);
            }
        }
    }

    /**
     * Get rating - default value is 0 and acceptable values are from 0 to 5
     *
     * @param str rating number value
     * @return int from 0 to 5 - default value is 0
     */

    public int getIntegerRating(String str) {
        try {
            int rating = Integer.parseInt(str);
            return (rating > 5 || rating < 1) ? 0 : rating;
        } catch (NumberFormatException nfe) {
            return 0;
        }
    }

    //v2
    private void skipXmlTagParse(XmlPullParser parser) throws XmlPullParserException, IOException {
        if (parser.getEventType() != XmlPullParser.START_TAG) {
            throw new IllegalStateException();
        }
        int depth = 1;
        while (depth != 0) {
            switch (parser.next()) {
            case XmlPullParser.END_TAG:
                depth--;
                break;
            case XmlPullParser.START_TAG:
                depth++;
                break;
            }
        }
    }

    //v2
    private String readXmlElementText(XmlPullParser parser) throws IOException, XmlPullParserException {
        String result = "";
        if (parser.next() == XmlPullParser.TEXT) {
            result = parser.getText();
            parser.nextTag();
        }
        if (result != null && !result.isEmpty()) {
            result = result.trim();
        }
        return result;
    }

    //v2 - check the content type of file is XML
    private boolean isXMLFile(ContentType contentType) {
        if (contentType != null) {
            for (String[] array : new String[][] { CONTENT_TYPES_IMPORT_XML }) {
                if (Arrays.asList(array).contains(contentType.type)) {
                    return true;
                }
            }
        }
        return false;
    }

    /* Constructor used by CREATOR */
    protected Station(Parcel in) {
        TITLE = in.readString();
        StreamURI = in.readString();
        mStationFetchResults = in.readBundle(Bundle.class.getClassLoader());
        mPlayback = in.readByte() != 0; // true if byte != 0

        _ID = in.readLong();
        UNIQUE_ID = in.readString();
        SUBTITLE = in.readString();
        IMAGE_PATH = in.readString();
        IMAGE_FILE_NAME = in.readString();
        SMALL_IMAGE_FILE_NAME = in.readString();
        CONTENT_TYPE = in.readString();
        DESCRIPTION = in.readString();
        RATING = in.readInt();
        COMMA_SEPARATED_TAGS = in.readString();
        CATEGORY = in.readString();
        MarkdownDescription = in.readString();
        SMALL_IMAGE_PATH = in.readString();
        IS_FAVOURITE = in.readInt();
        THUMP_UP_STATUS = in.readString();

        LogHelper.v(LOG_TAG, "Station re-created from parcel. State of playback is: " + mPlayback);
    }

    @Override
    public void writeToParcel(Parcel dest, int flags) {
        dest.writeString(TITLE);
        dest.writeString(StreamURI);
        dest.writeBundle(mStationFetchResults);
        dest.writeByte((byte) (mPlayback ? 1 : 0)); // if mPlayback == true, byte == 1
        dest.writeLong(_ID);
        dest.writeString(UNIQUE_ID);
        dest.writeString(SUBTITLE);
        dest.writeString(IMAGE_PATH);
        dest.writeString(IMAGE_FILE_NAME);
        dest.writeString(SMALL_IMAGE_FILE_NAME);
        dest.writeString(CONTENT_TYPE);
        dest.writeString(DESCRIPTION);
        dest.writeInt(RATING);
        dest.writeString(COMMA_SEPARATED_TAGS);
        dest.writeString(CATEGORY);
        dest.writeString(MarkdownDescription);
        dest.writeString(SMALL_IMAGE_PATH);
        dest.writeInt(IS_FAVOURITE);
        dest.writeString(THUMP_UP_STATUS);
    }

    /* CREATOR for Collection object used to do parcel related operations */
    public static final Creator<Station> CREATOR = new Creator<Station>() {
        @Override
        public Station createFromParcel(Parcel in) {
            return new Station(in);
        }

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

    /* Downloads remote playlist file */
    private String downloadPlaylistFile(URL fileLocation) {

        LogHelper.v(LOG_TAG, "Downloading... " + fileLocation.toString());

        try (BufferedReader br = new BufferedReader(new InputStreamReader(fileLocation.openStream()))) {

            String line;
            int counter = 0;
            StringBuilder sb = new StringBuilder("");

            // read until last last reached or until line five
            while ((line = br.readLine()) != null && counter < 5) {
                sb.append(line);
                sb.append("\n");
                counter++;
            }

            if (sb.length() == 0) {
                LogHelper.e(LOG_TAG, "Input stream was empty: " + fileLocation.toString());
            }

            // set mPlaylistFileContent and return String
            mPlaylistFileContent = sb.toString();
            return sb.toString();

        } catch (IOException e) {
            LogHelper.e(LOG_TAG, "Unable to get playlist file from server: " + fileLocation.toString());
            // set mPlaylistFileContent and return null
            mPlaylistFileContent = "[HTTP error. Unable to get playlist file from server: "
                    + fileLocation.toString() + "]";
            return null;
        }
    }

    /* Returns content type for given Uri */
    private ContentType getContentType(Uri streamUri) {
        if (streamUri == null) {
            return null;
        }
        try {
            // determine content type of remote file
            URL streamURL = new URL(streamUri.toString());
            return getContentType(streamURL);

        } catch (MalformedURLException e) {
            e.printStackTrace();
            return null;
        }
    }

    /* Returns content type for given URL */
    private ContentType getContentType(URL fileLocation) {
        try {
            HttpURLConnection connection = (HttpURLConnection) fileLocation.openConnection();
            connection.setConnectTimeout(5000);
            connection.setReadTimeout(5000);
            String contentTypeHeader = connection.getContentType();

            if (contentTypeHeader != null) {
                Matcher matcher = CONTENT_TYPE_PATTERN
                        .matcher(contentTypeHeader.trim().toLowerCase(Locale.ENGLISH));
                if (matcher.matches()) {
                    ContentType contentType = new ContentType();
                    String contentTypeString = matcher.group(1);
                    String charsetString = matcher.group(3);
                    if (contentTypeString != null) {
                        contentType.type = contentTypeString.trim();
                    }
                    if (charsetString != null) {
                        contentType.charset = charsetString.trim();
                    }
                    return contentType;
                }
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
        return null;
    }

    /* Determines if given content type is a playlist */
    private boolean isPlaylist(ContentType contentType) {
        if (contentType != null) {
            for (String[] array : new String[][] { CONTENT_TYPES_PLS, CONTENT_TYPES_M3U }) {
                if (Arrays.asList(array).contains(contentType.type)) {
                    return true;
                }
            }
        }
        return false;
    }

    /* Determines if given content type is an audio file */
    private boolean isAudioFile(ContentType contentType) {
        if (contentType != null) {
            for (String[] array : new String[][] { CONTENT_TYPES_MPEG, CONTENT_TYPES_OGG, CONTENT_TYPES_AAC }) {
                if (Arrays.asList(array).contains(contentType.type)) {
                    return true;
                }
            }
        }
        return false;
    }

    /* Determines name of station based on the location URL */
    private String detactStationName(URL fileLocationUrl) {

        String stationName;
        String fileLocation = fileLocationUrl.toString();
        int stationNameStart = fileLocation.lastIndexOf('/') + 1;
        int stationNameEnd = fileLocation.lastIndexOf('.');

        // try to cut out station name from given URL
        if (stationNameStart < stationNameEnd) {
            stationName = fileLocation.substring(stationNameStart, stationNameEnd);
        } else {
            stationName = fileLocation;
        }

        return stationName;
    }

    /* download Image File from url */
    private Bitmap downloadImageFile(String theImageExternalUrl) throws MalformedURLException {
        //check if there is image URL availabe in object
        URL fileLocation;
        if (theImageExternalUrl != null && !theImageExternalUrl.isEmpty()) {
            fileLocation = new URL(theImageExternalUrl);
            // download favicon
            LogHelper.v(LOG_TAG, "Downloading channelimage: " + fileLocation.toString());
            try (InputStream in = fileLocation.openStream()) {
                return BitmapFactory.decodeStream(in);
            } catch (IOException e) {
                LogHelper.e(LOG_TAG, "Error downloading: " + fileLocation.toString());
            }
        }

        //if due to any reason ,
        // the download not complete then try to download favicon
        String faviconLocation = getFavIconUrlString(StreamURI);
        //try download favicon
        LogHelper.v(LOG_TAG, "Downloading favicon: " + faviconLocation);
        try (InputStream in = new URL(faviconLocation).openStream()) {
            return BitmapFactory.decodeStream(in);
        } catch (IOException e) {
            LogHelper.e(LOG_TAG, "Error downloading: " + faviconLocation);
        }

        //if all failed then return null , app should use the default image
        return null;
    }

    @NonNull
    private String getFavIconUrlString(String inpotUrl) throws MalformedURLException {
        URL fileLocation;//Image path not found
        //Then try get image from fav icon of the host site
        fileLocation = new URL(inpotUrl);
        String host = fileLocation.getHost();

        // strip subdomain and add www if necessary
        if (!host.startsWith("www")) {
            int index = host.indexOf(".");
            host = "www" + host.substring(index);
        }

        // get favicon location
        return "http://" + host + "/favicon.ico";
    }

    /* Parses string representation of mStationPlaylistFile */
    private boolean parse(String fileContent, Station theStation) {

        theStation.mPlaylistFileContent = fileContent;

        // check for null
        if (fileContent == null) {
            return false;
        }

        // prepare scanner
        Scanner in = new Scanner(fileContent);
        String line;

        while (in.hasNextLine()) {

            // get a line from file content
            line = in.nextLine();

            // M3U: found station name
            if (line.contains("#EXTINF:-1,")) {
                if (theStation.TITLE == null || theStation.TITLE.isEmpty()) {
                    theStation.TITLE = line.substring(11).trim();
                }
                if (theStation.SUBTITLE == null || theStation.SUBTITLE.isEmpty()) {
                    theStation.SUBTITLE = line.substring(11).trim();
                }
                // M3U: found stream URL
            } else if (line.startsWith("http")) {
                theStation.StreamURI = line.trim();
            }

            // PLS: found station name
            else if (line.startsWith("Title1=")) {
                if (theStation.TITLE == null || theStation.TITLE.isEmpty()) {
                    theStation.TITLE = line.substring(7).trim();
                }
                if (theStation.SUBTITLE == null || theStation.SUBTITLE.isEmpty()) {
                    theStation.SUBTITLE = line.substring(7).trim();
                }
                // PLS: found stream URL
            } else if (line.startsWith("File1=http")) {
                theStation.StreamURI = line.substring(6).trim();
            }

        }

        in.close();

        if (theStation.StreamURI == null || theStation.StreamURI == "") {
            LogHelper.e(LOG_TAG, "Unable to parse: " + fileContent);
            return false;
        }

        // try to construct name of station from remote mStationPlaylistFile name
        if ((theStation.TITLE == null || theStation.TITLE.isEmpty()) && theStation.StreamURI != null
                && theStation.StreamURI != "") {
            try {
                theStation.TITLE = detactStationName(new URL(theStation.StreamURI));
            } catch (MalformedURLException e) {
                e.printStackTrace();
                LogHelper.e(LOG_TAG, "Unable to parse: " + fileContent);
                return false;
            }
        } else if (theStation.TITLE == null || theStation.TITLE.isEmpty()) {
            theStation.TITLE = "New Station";
        }

        // file content string parsed successfully
        return true;

    }

    /* Writes station image as png to storage */
    public void writeImageFile(File folder, Bitmap downloadedImage, String imgFileName) {
        String fileLocation = folder.toString() + "/" + imgFileName;
        LogHelper.v(LOG_TAG, "Saving channel image : " + fileLocation.toString());
        File mStationImageFile = new File(fileLocation);
        // write image to storage
        try (FileOutputStream out = new FileOutputStream(mStationImageFile)) {
            downloadedImage.compress(Bitmap.CompressFormat.PNG, 100, out);
        } catch (IOException e) {
            LogHelper.e(LOG_TAG, "Unable to save station image: " + downloadedImage.toString());
        }
    }

    /* Getter for download error flag */
    public Bundle getStationFetchResults() {
        return mStationFetchResults;
    }

    /* Getter for playback state - if this station is currently selected in player */
    public boolean getPlaybackState() {
        //check the global variable (singleton application variable)
        String currentRunningStationid = SingletonProperties.getInstance().CurrentStation_ID;
        if (currentRunningStationid != null && currentRunningStationid != ""
                && currentRunningStationid == String.valueOf(_ID)) {
            Log.v(LOG_TAG + "debug", "getPlaybackState ,playback = " + String.valueOf(true) + " - StationID = "
                    + String.valueOf(_ID) + " - currentRunningStationid=" + currentRunningStationid);
            return true;
        }
        Log.v(LOG_TAG + "debug", "getPlaybackState ,playback = " + String.valueOf(false) + " - StationID = "
                + String.valueOf(_ID) + " - currentRunningStationid=" + currentRunningStationid);
        return false;//mPlayback;
    }

    /* Getter for URL of stream */
    public Uri getStreamUri() {
        return Uri.parse(StreamURI);
    }

    /* Setter for image file object of station */
    public File getStationImageFileReference(File folder) {
        if (IMAGE_FILE_NAME != null && IMAGE_FILE_NAME != "") {
            // construct location of png image file from station name and folder
            String fileLocation = folder.toString() + "/" + IMAGE_FILE_NAME;
            mStationImageFile = new File(fileLocation);
        } else {
            // construct location of png image file from station name and folder
            String fileLocation = folder.toString() + "/" + UNIQUE_ID + ".png";
            mStationImageFile = new File(fileLocation);
        }
        return mStationImageFile;
    }

    /* return cached image File  */
    public File getStationImage(final Context cntxt) {
        //try get the image from file cache
        StorageHelper storageHelper = new StorageHelper(cntxt);
        final File folder = storageHelper.getCollectionDirectory();
        if (IMAGE_FILE_NAME != null && IMAGE_FILE_NAME != "") {
            // construct location of png image file from station name and folder
            String fileLocation = folder.toString() + "/" + IMAGE_FILE_NAME;
            mStationImageFile = new File(fileLocation);
        } else {
            // construct location of png image file from station name and folder
            String fileLocation = folder.toString() + "/" + UNIQUE_ID + ".png";
            mStationImageFile = new File(fileLocation);
        }
        if (mStationImageFile.exists()) {
            //file already exists, then return it from file system
            return mStationImageFile;
        } else {
            //file not exists in cache, then
            //and download image in background
            final String sIMAGE_PATH = IMAGE_PATH;
            final String sIMAGE_FILE_NAME = IMAGE_FILE_NAME;
            AsyncSaveDownloadToDesk(cntxt, folder, sIMAGE_PATH, sIMAGE_FILE_NAME);
        }

        //return null (to force app use the URL of image/ or default image)
        return null;
    }

    /* return cached SMALL image File  (icon) */
    public File getStationSmallImage(final Context cntxt) {
        //try get the image from file cache
        StorageHelper storageHelper = new StorageHelper(cntxt);
        final File folder = storageHelper.getCollectionDirectory();
        if (SMALL_IMAGE_FILE_NAME != null && !SMALL_IMAGE_FILE_NAME.isEmpty()) {
            // construct location of png image file from station name and folder
            String fileLocation = folder.toString() + "/" + SMALL_IMAGE_FILE_NAME;
            mStationSmallImageFile = new File(fileLocation);
        } else {
            // construct location of png image file from station name and folder
            String fileLocation = folder.toString() + "/" + UNIQUE_ID + "_small.png";
            mStationSmallImageFile = new File(fileLocation);
        }
        if (mStationSmallImageFile.exists()) {
            //file already exists, then get it from file system
            return mStationSmallImageFile;
        } else {
            //return null (will load the file from URL or default image)
            //and download it in background
            final String sSMALL_IMAGE_PATH = SMALL_IMAGE_PATH;
            final String sSMALL_IMAGE_FILE_NAME = SMALL_IMAGE_FILE_NAME;

            AsyncSaveDownloadToDesk(cntxt, folder, sSMALL_IMAGE_PATH, sSMALL_IMAGE_FILE_NAME);

            //return the large image instead if it's available (better than just return the default 'null'
            //if large image also not available , it will return null (to force app use the URL of image/ or default image)
            return getStationImage(cntxt);
        }
    }

    /*
    Download image from URL to desk to be cached for next time use
     */
    private void AsyncSaveDownloadToDesk(final Context cntxt, final File folder, final String sImagePath,
            final String sImageFileName) {
        Thread prepareThread = new Thread() {
            @Override
            public void run() {
                syncSaveDownloadToDesk(cntxt, sImagePath, folder, sImageFileName);
            }

            @Override
            public State getState() {
                return super.getState();
            }
        };
        prepareThread.start();
    }

    public void syncSaveDownloadToDesk(Context cntxt, String sImagePath, File folder, String sImageFileName) {
        boolean downloadDoneSuccessfully = false;
        try {
            //load all stations and ensure images are cached
            final StationsDbHelper mDbHelper = new StationsDbHelper(cntxt);
            //try download the file
            Bitmap downloadedImage = downloadImageFile(sImagePath);
            //Save Image to desk
            if (downloadedImage != null) {
                writeImageFile(folder, downloadedImage, sImageFileName);
                downloadDoneSuccessfully = true;
            }
        } catch (MalformedURLException e) {
            e.printStackTrace();
        } finally {
            // send local broadcast to inform all that image of this channel has been updated
            if (downloadDoneSuccessfully) {
                Intent i = new Intent();
                i.setAction(TransistorKeys.ACTION_COLLECTION_CHANGED);
                i.putExtra(TransistorKeys.EXTRA_COLLECTION_CHANGE, TransistorKeys.STATION_CHANGED_IMAGE);
                i.putExtra(TransistorKeys.EXTRA_STATION, Station.this);
                i.putExtra(TransistorKeys.EXTRA_STATION_DB_ID, _ID);
                LocalBroadcastManager.getInstance(cntxt.getApplicationContext()).sendBroadcast(i);
            }
        }
    }

    /* Setter for playback state */
    public void setPlaybackState(boolean playback) {
        Log.v(LOG_TAG + "debug", "setPlaybackState ,playback = " + String.valueOf(playback) + " - StationID = "
                + String.valueOf(_ID));
        String currentRunningStationid = SingletonProperties.getInstance().CurrentStation_ID;
        if (playback) {
            //set this station as current playing
            SingletonProperties.getInstance().CurrentStation_ID = String.valueOf(_ID);
            LogHelper.v(LOG_TAG, "CurrentStation_ID set to = " + String.valueOf(_ID));
        } else if (currentRunningStationid != null && currentRunningStationid != ""
                && currentRunningStationid == String.valueOf(_ID)) {
            //remove this station from currentRunningStation_id var only if it's already there
            SingletonProperties.getInstance().CurrentStation_ID = null;
            LogHelper.v(LOG_TAG, "CurrentStation_ID set to = null");
        }
    }

    @Override
    public int compareTo(@NonNull Station otherStation) {
        // Compares two stations: returns "1" if name if this station is greater than name of given station
        return TITLE.compareToIgnoreCase(otherStation.TITLE);
    }

    @Override
    public String toString() {
        return "Station [Name=" + TITLE + ", Uri=" + StreamURI + "]";
    }

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

    /**
     * Container class representing the content-type and charset string
     * received from the response header of an HTTP server.
     */
    public class ContentType implements Parcelable {
        String type;
        String charset;

        /* Constructor (default) */
        public ContentType() {
        }

        /* Constructor used by CREATOR */
        protected ContentType(Parcel in) {
            type = in.readString();
            charset = in.readString();
        }

        /* CREATOR for ContentType object used to do parcel related operations */
        public final Creator<ContentType> CREATOR = new Creator<ContentType>() {
            @Override
            public ContentType createFromParcel(Parcel in) {
                return new ContentType(in);
            }

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

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

        @Override
        public void writeToParcel(Parcel dest, int flags) {
            dest.writeString(type);
            dest.writeString(charset);
        }

        @Override
        public String toString() {
            return "ContentType{type='" + type + "'" + ", charset='" + charset + "'}";
        }

        public String getTypeString() {
            return type;
        }
    }

    /* add station data to DB SQLite */
    public static void AddStationItemToDb(Station stationItem, Activity mActivity) {
        //db test
        StationsDbHelper mDbHelper = new StationsDbHelper(mActivity);
        // Gets the data repository in write mode
        SQLiteDatabase db = mDbHelper.getWritableDatabase();

        // Filter results WHERE "title" = 'My Title'
        String selection = StationsDbContract.StationEntry.COLUMN_UNIQUE_ID + " = ?";
        String[] selectionArgs = { stationItem.UNIQUE_ID };

        String[] projection = { StationsDbContract.StationEntry._ID,
                StationsDbContract.StationEntry.COLUMN_UNIQUE_ID };
        String sortOrder = StationsDbContract.StationEntry.COLUMN_UNIQUE_ID + " DESC";
        Cursor cursor = db.query(StationsDbContract.StationEntry.TABLE_NAME, // The table to query
                projection, // The columns to return
                selection, // The columns for the WHERE clause
                selectionArgs, // The values for the WHERE clause
                null, // don't group the rows
                null, // don't filter by row groups
                sortOrder);
        if (cursor.getCount() == 0) {
            //record not found
            // Create a new map of values, where column names are the keys
            ContentValues values = new ContentValues();
            values.put(StationsDbContract.StationEntry.COLUMN_NAME_TITLE, stationItem.TITLE);
            values.put(StationsDbContract.StationEntry.COLUMN_UNIQUE_ID, stationItem.UNIQUE_ID);
            values.put(StationsDbContract.StationEntry.COLUMN_NAME_SUBTITLE, stationItem.SUBTITLE);
            values.put(StationsDbContract.StationEntry.COLUMN_DESCRIPTION, stationItem.DESCRIPTION);
            values.put(StationsDbContract.StationEntry.COLUMN_IMAGE_PATH, stationItem.IMAGE_PATH);
            values.put(StationsDbContract.StationEntry.COLUMN_IMAGE_FILE_NAME, stationItem.IMAGE_FILE_NAME);
            values.put(StationsDbContract.StationEntry.COLUMN_SMALL_IMAGE_FILE_NAME,
                    stationItem.SMALL_IMAGE_FILE_NAME);
            values.put(StationsDbContract.StationEntry.COLUMN_URI, stationItem.StreamURI);
            values.put(StationsDbContract.StationEntry.COLUMN_CONTENT_TYPE, stationItem.CONTENT_TYPE);
            values.put(StationsDbContract.StationEntry.COLUMN_RATING, stationItem.RATING);
            values.put(StationsDbContract.StationEntry.COLUMN_IS_FAVOURITE, 0); //default
            values.put(StationsDbContract.StationEntry.COLUMN_COMMA_SEPARATED_TAGS,
                    stationItem.COMMA_SEPARATED_TAGS);
            values.put(StationsDbContract.StationEntry.COLUMN_CATEGORY, stationItem.CATEGORY);
            values.put(StationsDbContract.StationEntry.COLUMN_MARKDOWN_DESCRIPTION,
                    stationItem.MarkdownDescription);
            values.put(StationsDbContract.StationEntry.COLUMN_SMALL_IMAGE_URL, stationItem.SMALL_IMAGE_PATH);

            // Insert the new row, returning the primary key value of the new row
            long newRowId = db.insert(StationsDbContract.StationEntry.TABLE_NAME, null, values);
            stationItem._ID = newRowId;
        } //todo: , else then update the existing with new data

        db.close();

    }

    public ArrayList<Station> getInsertedStations() {
        return mInsertedStations;
    }
}