package com.felkertech.cumulustv.model; import android.content.ComponentName; import android.content.ContentResolver; import android.content.Context; import android.database.Cursor; import android.media.tv.TvContract; import android.net.Uri; import android.os.Build; import android.support.annotation.NonNull; import android.util.Log; import com.felkertech.cumulustv.fileio.AbstractFileParser; import com.felkertech.cumulustv.fileio.FileParserFactory; import com.felkertech.cumulustv.fileio.HttpFileParser; import com.felkertech.cumulustv.fileio.M3uParser; import com.felkertech.cumulustv.plugins.CumulusChannel; import com.felkertech.cumulustv.plugins.JsonContainer; import com.felkertech.cumulustv.receivers.GoogleDriveBroadcastReceiver; import com.felkertech.cumulustv.services.CumulusJobService; import com.felkertech.cumulustv.utils.ActivityUtils; import com.felkertech.cumulustv.utils.DriveSettingsManager; import com.felkertech.settingsmanager.SettingsManager; import com.google.android.media.tv.companionlibrary.model.Channel; import com.google.android.media.tv.companionlibrary.model.InternalProviderData; import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; import java.io.IOException; import java.io.InputStream; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Set; /** * Created by N on 7/14/2015. * This is a JSON object that stores all relevant user-input data for channels */ public class ChannelDatabase { private static final String TAG = ChannelDatabase.class.getSimpleName(); private static final boolean DEBUG = true; public static final String KEY = "JSONDATA"; private static final String KEY_CHANNELS = "channels"; private static final String KEY_MODIFIED = "modified"; private JSONObject mJsonObject; private JSONArray mTemporaryObjects; private SettingsManager mSettingsManager; protected HashMap<String, Long> mDatabaseHashMap; protected List<JsonChannel> mJsonChannelsList; private static ChannelDatabase mChannelDatabase; public static ChannelDatabase getInstance(Context context) { if (mChannelDatabase == null) { mChannelDatabase = new ChannelDatabase(context); mChannelDatabase.initializeHashMap(context); try { mChannelDatabase.readJsonListings(); } catch (JSONException e) { e.printStackTrace(); } } return mChannelDatabase; } protected ChannelDatabase(final Context context) { mSettingsManager = new SettingsManager(context); try { DriveSettingsManager sp = new DriveSettingsManager(context); String spData = sp.getString(KEY, getDefaultJsonString()); if (spData.isEmpty()) { spData = getDefaultJsonString(); } mJsonObject = new JSONObject(spData); if (!mJsonObject.has(KEY_MODIFIED)) { mJsonObject.put(KEY_MODIFIED, 0L); save(); } resetPossibleGenres(); // This will try to use the newest API data } catch (final JSONException e) { throw new MalformedChannelDataException(e.getMessage()); } } public JSONArray getJSONArray() throws JSONException { return mJsonObject.getJSONArray(KEY_CHANNELS); } public List<JsonChannel> getJsonChannels() throws JSONException { if (mJsonChannelsList == null) { JSONArray channels = getJSONArray(); final List<JsonChannel> channelList = new ArrayList<>(); // Add all the normal channels for (int i = 0; i < channels.length(); i++) { ChannelDatabaseFactory.parseType(channels.getJSONObject(i), new ChannelDatabaseFactory.ChannelParser() { @Override public void ifJsonChannel(JsonChannel entry) { channelList.add(entry); } @Override public void ifJsonListing(JsonListing entry) { } }); } Log.d(TAG, "There are " + channelList.size() + " items"); // Add temporary channels to list if (mTemporaryObjects != null) { for (int i = 0; i < mTemporaryObjects.length(); i++) { ChannelDatabaseFactory.parseType(mTemporaryObjects.getJSONObject(i), new ChannelDatabaseFactory.ChannelParser() { @Override public void ifJsonChannel(JsonChannel entry) { if (!channelList.contains(entry)) { channelList.add(entry); } } @Override public void ifJsonListing(JsonListing entry) { } }); } Log.d(TAG, "Plus more " + channelList.size() + " channels"); } if (mSettingsManager.getBoolean("SORT_BY_NUMBER")) { Collections.sort(channelList); // Optionally reorder } mJsonChannelsList = channelList; } return mJsonChannelsList; } public List<Channel> getChannels() throws JSONException { List<JsonChannel> jsonChannelList = getJsonChannels(); List<Channel> channelList = new ArrayList<>(); for (int i = 0; i < jsonChannelList.size(); i++) { JsonChannel jsonChannel = jsonChannelList.get(i); Channel channel = jsonChannel.toChannel(); channelList.add(channel); } return channelList; } public List<Channel> getChannels(InternalProviderData providerData) throws JSONException { List<JsonChannel> jsonChannelList = getJsonChannels(); List<Channel> channelList = new ArrayList<>(); for (int i = 0; i < jsonChannelList.size(); i++) { JsonChannel jsonChannel = jsonChannelList.get(i); Channel channel = jsonChannel.toChannel(providerData); channelList.add(channel); } return channelList; } public boolean channelNumberExists(String number) { try { List<JsonChannel> jsonChannelList = getJsonChannels(); for (JsonChannel jsonChannel : jsonChannelList) { if (jsonChannel.getNumber().equals(number)) { return true; } } } catch (JSONException ignored) { } return false; } public boolean channelExists(CumulusChannel channel) { try { List<JsonChannel> jsonChannelList = getJsonChannels(); for (JsonChannel jsonChannel : jsonChannelList) { if (jsonChannel.equals(channel) || jsonChannel.getMediaUrl().equals(channel.getMediaUrl())) { return true; } } } catch (JSONException ignored) { } return false; } public JsonChannel findChannelByMediaUrl(String mediaUrl) { try { List<JsonChannel> jsonChannelList = getJsonChannels(); for (JsonChannel jsonChannel : jsonChannelList) { if (jsonChannel.getMediaUrl() != null) { if (jsonChannel.getMediaUrl().equals(mediaUrl)) { return jsonChannel; } } } } catch (JSONException ignored) { } return null; } public String[] getChannelNames() { List<String> strings = new ArrayList<>(); try { List<JsonChannel> jsonChannelList = getJsonChannels(); for (JsonChannel jsonChannel : jsonChannelList) { strings.add(jsonChannel.getNumber() + " " + jsonChannel.getName()); } } catch (JSONException ignored) { } return strings.toArray(new String[strings.size()]); } public void add(CumulusChannel channel) throws JSONException { if (mJsonObject != null) { mJsonObject.getJSONArray(KEY_CHANNELS).put(channel.toJson()); save(); } } public void add(JsonListing listing) throws JSONException { if (mJsonObject != null) { JSONArray channels = mJsonObject.getJSONArray(KEY_CHANNELS); mJsonObject.getJSONArray(KEY_CHANNELS).put(listing.toJson()); Log.d(TAG, channels.toString()); save(); } } public void update(CumulusChannel channel) throws JSONException { /*if (channel.getGenresString().isEmpty()) { channel = new CumulusChannel.Builder(channel) .setGenres(TvContract.Programs.Genres.FAMILY_KIDS) .build(); }*/ final CumulusChannel channel1 = channel; if(!channelExists(channel)) { add(channel); } else { final JSONArray channels = getJSONArray(); final int[] i = new int[1]; for (i[0] = 0; i[0] < channels.length(); i[0]++) { ChannelDatabaseFactory.parseType(channels.getJSONObject(i[0]), new ChannelDatabaseFactory.ChannelParser() { @Override public void ifJsonChannel(JsonChannel entry) { try { if (entry.getMediaUrl().equals(channel1.getMediaUrl())) { if (DEBUG) { Log.d(TAG, "Remove " + i[0] + " and put at " + i[0] + ": " + channel1.toString()); } channels.put(i[0], channel1.toJson()); mJsonObject.getJSONArray(KEY_CHANNELS).put(i[0], channel1.toJson()); save(); return; } } catch (JSONException ignored) { } } @Override public void ifJsonListing(JsonListing entry) { } }); } } } public void delete(final JsonContainer container) throws JSONException { final JSONArray channels = getJSONArray(); final int[] i = new int[1]; for (i[0] = 0; i[0] < channels.length(); i[0]++) { ChannelDatabaseFactory.parseType(channels.getJSONObject(i[0]), new ChannelDatabaseFactory.ChannelParser() { @Override public void ifJsonChannel(JsonChannel entry) { try { if (entry.equals(container)) { if (DEBUG) { Log.d(TAG, "Remove " + i[0] + " and put at " + i[0] + ": " + entry.toString()); } // channels.put(i[0], entry.toJson()); mJsonObject.getJSONArray(KEY_CHANNELS).remove(i[0]); save(); return; } } catch (JSONException ignored) { } } @Override public void ifJsonListing(JsonListing entry) { if (entry.equals(container)) { if (DEBUG) { Log.d(TAG, "Remove " + i[0] + " and put at " + i[0] + ": " + entry.toString()); } try { // channels.put(i[0], entry.toJson()); mJsonObject.getJSONArray(KEY_CHANNELS).remove(i[0]); save(); return; } catch (JSONException e) { e.printStackTrace(); } } } }); } } public void save() { try { setLastModified(); mSettingsManager.setString(KEY, toString()); initializeHashMap(mSettingsManager.getContext()); mJsonChannelsList = null; readJsonListings(); // Tell the system that there's been changes. ActivityUtils.onChannelsChanged(mSettingsManager.getContext()); } catch (JSONException e) { e.printStackTrace(); } } @Override public String toString() { return mJsonObject.toString(); } public String toM3u() { StringBuilder builder = new StringBuilder(); builder.append(M3uParser.Constants.HEADER_TAG + "\n"); try { List<JsonChannel> jsonChannelList = getJsonChannels(); for (int i = 0; i< jsonChannelList.size(); i++) { JsonChannel jsonChannel = jsonChannelList.get(i); builder.append(M3uParser.Constants.CHANNEL_TAG); builder.append(" " + M3uParser.Constants.CH_NUMBER + "=\"") .append(jsonChannel.getNumber()).append("\""); if (jsonChannel.hasLogo()) { builder.append(" " + M3uParser.Constants.CH_LOGO + "=\"") .append(jsonChannel.getLogo()).append("\""); } if (jsonChannel.isAudioOnly()) { builder.append(" " + M3uParser.Constants.CH_AUDIO_ONLY + "=1"); } if (jsonChannel.getEpgUrl() != null) { builder.append(" " + M3uParser.Constants.CH_EPG_URL + "=\"") .append(jsonChannel.getEpgUrl()).append("\""); } builder.append(" " + M3uParser.Constants.CH_GENRES + "=\"") .append(jsonChannel.getGenresString()).append("\""); if (jsonChannel.getPluginSource() != null) { builder.append(" " + M3uParser.Constants.CH_PLUGIN + "=\"") .append(jsonChannel.getPluginSource()).append("\""); } if (jsonChannel.hasSplashscreen()) { builder.append(" " + M3uParser.Constants.CH_SPLASH + "=\"") .append(jsonChannel.getSplashscreen()).append("\""); } builder.append(", ").append(jsonChannel.getName()).append("\n"); // Add URL builder.append(jsonChannel.getMediaUrl()).append("\n"); } } catch (JSONException e) { e.printStackTrace(); } return builder.toString(); } public long getLastModified() throws JSONException { return mJsonObject.getLong("modified"); } private void setLastModified() throws JSONException { if(mJsonObject != null) { mJsonObject.put("modified", System.currentTimeMillis()); } } public HashMap<String, Long> getHashMap() { return mDatabaseHashMap; } public JsonChannel getChannelFromRowId(@NonNull Long rowId) { if (mDatabaseHashMap == null || rowId < 0) { return null; } Set<String> mediaUrlSet = mDatabaseHashMap.keySet(); for (String mediaUrl : mediaUrlSet) { if (mDatabaseHashMap.get(mediaUrl).equals(rowId)) { return findChannelByMediaUrl(mediaUrl); } } return null; } public void resetPossibleGenres() throws JSONException { JSONArray genres = new JSONArray(); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP_MR1) { genres.put(TvContract.Programs.Genres.ANIMAL_WILDLIFE); genres.put(TvContract.Programs.Genres.ARTS); genres.put(TvContract.Programs.Genres.COMEDY); genres.put(TvContract.Programs.Genres.DRAMA); genres.put(TvContract.Programs.Genres.EDUCATION); genres.put(TvContract.Programs.Genres.ENTERTAINMENT); genres.put(TvContract.Programs.Genres.FAMILY_KIDS); genres.put(TvContract.Programs.Genres.GAMING); genres.put(TvContract.Programs.Genres.LIFE_STYLE); genres.put(TvContract.Programs.Genres.MOVIES); genres.put(TvContract.Programs.Genres.MUSIC); genres.put(TvContract.Programs.Genres.NEWS); genres.put(TvContract.Programs.Genres.PREMIER); genres.put(TvContract.Programs.Genres.SHOPPING); genres.put(TvContract.Programs.Genres.SPORTS); genres.put(TvContract.Programs.Genres.TECH_SCIENCE); genres.put(TvContract.Programs.Genres.TRAVEL); } else { genres.put(TvContract.Programs.Genres.ANIMAL_WILDLIFE); genres.put(TvContract.Programs.Genres.COMEDY); genres.put(TvContract.Programs.Genres.DRAMA); genres.put(TvContract.Programs.Genres.EDUCATION); genres.put(TvContract.Programs.Genres.FAMILY_KIDS); genres.put(TvContract.Programs.Genres.GAMING); genres.put(TvContract.Programs.Genres.MOVIES); genres.put(TvContract.Programs.Genres.NEWS); genres.put(TvContract.Programs.Genres.SHOPPING); genres.put(TvContract.Programs.Genres.SPORTS); genres.put(TvContract.Programs.Genres.TRAVEL); } mJsonObject.put("possibleGenres", genres); } /** * Creates a link between the database Uris and the JSONChannels * @param context The application's context for the {@link ContentResolver}. */ protected void initializeHashMap(final Context context) { new Thread(new Runnable() { @Override public void run() { ContentResolver contentResolver = context.getContentResolver(); Uri channelsUri = TvContract.buildChannelsUriForInput( ActivityUtils.TV_INPUT_SERVICE.flattenToString()); Cursor cursor = contentResolver.query(channelsUri, null, null, null, null); mDatabaseHashMap = new HashMap<>(); Log.d(TAG, "Initialize CD HashMap"); if (cursor != null) { while (cursor.moveToNext()) { try { InternalProviderData ipd = new InternalProviderData( cursor.getBlob(cursor.getColumnIndex( TvContract.Channels.COLUMN_INTERNAL_PROVIDER_DATA))); String mediaUrl = ipd.getVideoUrl(); long rowId = cursor.getLong(cursor.getColumnIndex(TvContract.Channels._ID)); Log.d(TAG, "Try to match " + mediaUrl + " " + rowId); for (JsonChannel jsonChannel : getJsonChannels()) { if (jsonChannel.getMediaUrl().equals(mediaUrl)) { mDatabaseHashMap.put(jsonChannel.getMediaUrl(), rowId); } } } catch (InternalProviderData.ParseException e) { e.printStackTrace(); } catch (JSONException ignored) { } } cursor.close(); } } }).start(); } public void eraseData() { mSettingsManager.setString(ChannelDatabase.KEY, getDefaultJsonString()); Log.d(TAG, "Erasing data"); try { mJsonObject.put(KEY_CHANNELS, new JSONArray()); Log.d(TAG, getJSONArray().toString()); } catch (JSONException e) { e.printStackTrace(); } } /** * Adds temporary channels to the database, which live solely in memory and are wiped when the * app memory is deleted. These can interface with {@link JsonListing} objects to add channels * that are displayed but not saved. * * @param temp The channel to add. * @throws JSONException */ public void addTemporaryChannel(JsonContainer temp) throws JSONException { if (mTemporaryObjects == null) { mTemporaryObjects = new JSONArray(); } for (int i = 0; i < mTemporaryObjects.length(); i++) { if (mTemporaryObjects.get(i).equals(temp)) { return; } } mTemporaryObjects.put(temp.toJson()); } /** * Scans through user data to find all json listings and add them to a temporary object in * memory. */ protected void readJsonListings() throws JSONException { JSONArray jsonArray = getJSONArray(); for (int i = 0; i < jsonArray.length(); i++) { ChannelDatabaseFactory.parseType(jsonArray.getJSONObject(i), new ChannelDatabaseFactory.ChannelParser() { @Override public void ifJsonChannel(JsonChannel entry) { } @Override public void ifJsonListing(JsonListing entry) { Log.d(TAG, "Json listing " + entry.getUrl()); new HttpFileParser(entry.getUrl(), new AbstractFileParser.FileLoader() { @Override public void onFileLoaded(InputStream inputStream) { if (inputStream == null) { return; } try { M3uParser.TvListing listing = M3uParser.parse(inputStream); if (listing == null) { return; } List<M3uParser.M3uTvChannel> channels = listing.channels; for (M3uParser.M3uTvChannel c : channels) { Log.d(TAG, "Reading " + c.url + " from JSON Listing"); addTemporaryChannel(c.toJsonChannel()); } // Make sure these get loaded mJsonChannelsList = null; } catch (IOException | JSONException e) { e.printStackTrace(); } } }); } }); } } public static String[] getAllGenres() { if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP_MR1) { return new String[] { TvContract.Programs.Genres.ANIMAL_WILDLIFE, TvContract.Programs.Genres.ARTS, TvContract.Programs.Genres.COMEDY, TvContract.Programs.Genres.DRAMA, TvContract.Programs.Genres.EDUCATION, TvContract.Programs.Genres.ENTERTAINMENT, TvContract.Programs.Genres.FAMILY_KIDS, TvContract.Programs.Genres.GAMING, TvContract.Programs.Genres.LIFE_STYLE, TvContract.Programs.Genres.MOVIES, TvContract.Programs.Genres.MUSIC, TvContract.Programs.Genres.NEWS, TvContract.Programs.Genres.PREMIER, TvContract.Programs.Genres.SHOPPING, TvContract.Programs.Genres.SPORTS, TvContract.Programs.Genres.TECH_SCIENCE, TvContract.Programs.Genres.TRAVEL, }; } return new String[] { TvContract.Programs.Genres.ANIMAL_WILDLIFE, TvContract.Programs.Genres.COMEDY, TvContract.Programs.Genres.DRAMA, TvContract.Programs.Genres.EDUCATION, TvContract.Programs.Genres.FAMILY_KIDS, TvContract.Programs.Genres.GAMING, TvContract.Programs.Genres.MOVIES, TvContract.Programs.Genres.NEWS, TvContract.Programs.Genres.SHOPPING, TvContract.Programs.Genres.SPORTS, TvContract.Programs.Genres.TRAVEL, }; } protected static String getDefaultJsonString() { try { JSONObject jsonObject = new JSONObject(); jsonObject.put(KEY_CHANNELS, new JSONArray()); jsonObject.put(KEY_MODIFIED, 0); return jsonObject.toString(); } catch (JSONException e) { e.printStackTrace(); } throw new RuntimeException("Default JSON String cannot be created"); } public static String getNonNullChannelLogo(CumulusChannel jsonChannel) { if (jsonChannel.hasLogo()) { return jsonChannel.getLogo(); } return "https://raw.githubusercontent.com/Fleker/CumulusTV/master/app/src/main/res/drawab" + "le-xhdpi/c_banner_3_2.jpg"; } public static class MalformedChannelDataException extends RuntimeException { public MalformedChannelDataException(String reason) { super(reason); } } }