Java tutorial
/* * Copyright (C) 2011,2012 Southern Storm Software, Pty Ltd. * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ package com.southernstorm.tvguide; import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.InputStream; import java.io.InputStreamReader; import java.io.IOException; import java.io.OutputStreamWriter; import java.io.UnsupportedEncodingException; import java.net.MalformedURLException; import java.net.URI; import java.net.URISyntaxException; import java.util.ArrayList; import java.util.Calendar; import java.util.Collections; import java.util.Date; import java.util.GregorianCalendar; import java.util.List; import java.util.Map; import java.util.Random; import java.util.TreeMap; import java.util.zip.GZIPInputStream; import org.apache.http.Header; import org.apache.http.HttpEntity; import org.apache.http.HttpResponse; import org.apache.http.HttpStatus; import org.apache.http.client.methods.HttpGet; import org.apache.http.impl.client.DefaultHttpClient; import org.apache.http.impl.cookie.DateParseException; import org.apache.http.impl.cookie.DateUtils; import org.xmlpull.v1.XmlPullParser; import org.xmlpull.v1.XmlPullParserException; import org.xmlpull.v1.XmlPullParserFactory; import org.xmlpull.v1.XmlSerializer; import android.content.Context; import android.content.SharedPreferences; import android.content.pm.ApplicationInfo; import android.content.pm.PackageInfo; import android.content.pm.PackageManager; import android.content.pm.PackageManager.NameNotFoundException; import android.content.res.XmlResourceParser; import android.net.ConnectivityManager; import android.net.NetworkInfo; import android.os.AsyncTask; import android.util.Xml; /** * Cache management and network fetching for channel data. */ public class TvChannelCache extends ExternalMediaHandler { private String serviceName; private String serviceUrl; private File httpCacheDir; private File iconCacheDir; private Random rand; private boolean debug; private boolean debugLoaded; private List<TvChannel> activeChannels; private Map<String, TvChannel> channels; private String region; private Map<String, List<String>> regionTree; private Map<String, ArrayList<String>> commonIds; private List<TvNetworkListener> networkListeners; private List<TvChannelChangedListener> channelListeners; private boolean embeddedLoaded = false; private boolean sdLoaded = false; private boolean mainListLoaded = false; private boolean mainListFetching = false; private boolean mainListUnchanged = false; private boolean haveDataForDecls = false; private String lastSelectedChannel; private static TvChannelCache instance = null; private Calendar mainListLastMod = null; private Calendar mainListLastFetched = null; private TvChannelCache() { this.serviceName = ""; this.rand = new Random(System.currentTimeMillis()); this.activeChannels = new ArrayList<TvChannel>(); this.channels = new TreeMap<String, TvChannel>(); this.commonIds = new TreeMap<String, ArrayList<String>>(); this.regionTree = new TreeMap<String, List<String>>(); this.networkListeners = new ArrayList<TvNetworkListener>(); this.channelListeners = new ArrayList<TvChannelChangedListener>(); } /** * Gets the global instance of the channel cache. * * @return the global instance */ public static TvChannelCache getInstance() { if (instance == null) { instance = new TvChannelCache(); instance.setServiceName("OzTivo"); instance.setServiceUrl("http://xml.oztivo.net/xmltv/datalist.xml.gz"); } return instance; } /** * Gets the current region. * * @return the region, or null if none set */ public String getRegion() { return region; } /** * Sets the current region. * * @param region the region to set */ public void setRegion(String region) { if (this.region == null || !this.region.equals(region)) { this.region = region; // Save the region in the settings. SharedPreferences.Editor editor = getContext().getSharedPreferences("TVGuideActivity", 0).edit(); editor.putString("region", region); editor.commit(); // Reload the channel list. loadChannels(); } } @Override public void addContext(Context context) { super.addContext(context); if (region == null) { // Load the region from the settings for the first time. SharedPreferences prefs = context.getSharedPreferences("TVGuideActivity", 0); region = prefs.getString("region", ""); if (region != null && region.equals("")) region = null; } // Determine if the application was built as debug or release to set the debug flag. if (!debugLoaded) { debugLoaded = true; debug = false; PackageManager manager = context.getPackageManager(); try { PackageInfo info = manager.getPackageInfo(context.getPackageName(), 0); if ((info.applicationInfo.flags & ApplicationInfo.FLAG_DEBUGGABLE) != 0) debug = true; } catch (NameNotFoundException e) { } } // Load the channels. if (channels.size() == 0 && region != null) loadChannels(); } public void addContext(Context context, boolean forceMainListRefresh) { addContext(context); if (forceMainListRefresh && mainListLoaded && !mainListFetching && mainListLastFetched != null) { Calendar now = new GregorianCalendar(); long diff = now.getTimeInMillis() - mainListLastFetched.getTimeInMillis(); if (diff > (60 * 60 * 1000)) { // 1 hour mainListLoaded = false; loadChannels(); } } } /** * Gets the list of active channels. * * @return the channels */ public List<TvChannel> getActiveChannels() { return activeChannels; } /** * Gets the list of all channels in the current region, hidden or shown. * * @return the channels */ public List<TvChannel> getAllChannelsInRegion() { List<TvChannel> allChannels = new ArrayList<TvChannel>(); for (TvChannel channel : channels.values()) { if (channel.getHiddenState() == TvChannel.HIDDEN_BY_REGION) { String region = channel.getRegion(); if (region == null || !regionMatch(region)) continue; } if (haveDataForDecls && !channel.hasDataFor()) continue; // No data for the channel on the server, so block it. allChannels.add(channel); } Collections.sort(allChannels); return allChannels; } /** * Gets the channel with a specific identifier. * * @param id the identifier * @return the channel, or null if not found */ public TvChannel getChannel(String id) { if (id == null || id.length() == 0) return null; else return channels.get(id); } /** * Gets the name of the service to cache channel data underneath. * * @return the service name, or the empty string if no service */ public String getServiceName() { return serviceName; } /** * Sets the name of the service to cache channel data underneath. * * @param serviceName the service name, or the empty string if no service */ public void setServiceName(String serviceName) { if (serviceName == null) serviceName = ""; if (!this.serviceName.equals(serviceName)) { this.serviceName = serviceName; if (isMediaUsable()) reloadService(); } } /** * Gets the main channel list URL for this service. * * @return the url */ public String getServiceUrl() { return serviceUrl; } /** * Sets the main channel list URL for this service. * * @param url the url */ public void setServiceUrl(String url) { this.serviceUrl = url; } /** * Gets the channel identifier of the last channel that was selected in * the programme view. * * @return the channel id */ public String getLastSelectedChannel() { return lastSelectedChannel; } /** * Sets the channel identifier of the last channel that was selected in * the programme view. * * @param channelId the channel id */ public void setLastSelectedChannel(String channelId) { lastSelectedChannel = channelId; } /** * Open the XMLTV data file in the cache for a specific channel and date. * * The data on the SD card is stored as gzip'ed XML. The stream returned * by this function will unzip the data as it is read. * * @param channel the channel, or null for the main channel list file * @param date the date corresponding to the requested data * @return an input stream, or null if the data is not present */ public InputStream openChannelData(TvChannel channel, Calendar date) { File file = dataFile(channel, date, ".xml.gz"); if (file == null || !file.exists()) return null; try { FileInputStream fileStream = new FileInputStream(file); try { return new GZIPInputStream(fileStream); } catch (IOException e) { fileStream.close(); return null; } } catch (IOException e) { return null; } } /** * Gets the last-modified date for a specific channel and date combination. * * @param channel the channel, or null for the main channel list file * @param date the date * @return the last-modified date, or null if the data is not in the cache */ public Calendar channelDataLastModified(TvChannel channel, Calendar date) { File file = dataFile(channel, date, ".cache"); if (file == null || !file.exists()) return null; try { FileInputStream input = new FileInputStream(file); try { InputStreamReader reader = new InputStreamReader(input, "utf-8"); try { String line; while ((line = readLine(reader)) != null) { if (line.startsWith("Last-Modified: ")) { String lastModified = line.substring(15); Date parsedDate = DateUtils.parseDate(lastModified); GregorianCalendar result = new GregorianCalendar(); result.setTime(parsedDate); return result; } } } catch (DateParseException e) { } finally { reader.close(); } } finally { input.close(); } } catch (IOException e) { } return null; } /** * Determine if data for a specific channel and date is available in the cache, * and is up to date with respect to the server. * * @param channel the channel * @param date the date to look for * @return true if data is available, false if not */ public boolean hasChannelData(TvChannel channel, Calendar date) { File file = dataFile(channel, date, ".xml.gz"); if (file == null || !file.exists()) return false; Calendar dayLastMod = channel.dayLastModified(date); if (dayLastMod == null || !isNetworkingAvailable()) return true; // Assume the local copy is up to date. Calendar fileLastMod = channelDataLastModified(channel, date); if (fileLastMod == null) return true; return sameTimeNoTimezone(dayLastMod, fileLastMod); } private static boolean sameTimeNoTimezone(Calendar d1, Calendar d2) { if (d1.get(Calendar.DAY_OF_MONTH) != d2.get(Calendar.DAY_OF_MONTH)) return false; if (d1.get(Calendar.MONTH) != d2.get(Calendar.MONTH)) return false; if (d1.get(Calendar.YEAR) != d2.get(Calendar.YEAR)) return false; if (d1.get(Calendar.HOUR_OF_DAY) != d2.get(Calendar.HOUR_OF_DAY)) return false; if (d1.get(Calendar.MINUTE) != d2.get(Calendar.MINUTE)) return false; return d1.get(Calendar.SECOND) == d2.get(Calendar.SECOND); } /** * Expire old entries in the cache. */ public void expire() { if (httpCacheDir == null) return; String[] entries = httpCacheDir.list(); GregorianCalendar today = new GregorianCalendar(); int todayYear = today.get(Calendar.YEAR); int todayMonth = today.get(Calendar.MONTH) + 1; int todayDay = today.get(Calendar.DAY_OF_MONTH); for (int index = 0; index < entries.length; ++index) { // Look for files that end in ".xml.gz" or ".cache". String name = entries[index]; int suffixLength; if (name.endsWith(".xml.gz")) suffixLength = 7; else if (name.endsWith(".cache")) suffixLength = 6; else continue; if ((name.length() - suffixLength) < 10) continue; // Extract the date in the format YYYY-MM-DD from the name // and determine if it is less than today. int posn = name.length() - suffixLength - 10; int year = Utils.parseField(name, posn, 4); int month = Utils.parseField(name, posn + 5, 2); int day = Utils.parseField(name, posn + 8, 2); if (year > todayYear) continue; if (year == todayYear) { if (month > todayMonth) continue; if (month == todayMonth) { if (day >= todayDay) continue; } } // Delete the file as it is older than today. File file = new File(httpCacheDir, name); if (debug) System.out.println("expiring " + file.getPath()); file.delete(); } } /** * Clear the entire contents of the cache. */ public void clear() { if (httpCacheDir == null) return; String[] entries = httpCacheDir.list(); for (int index = 0; index < entries.length; ++index) { // Look for files that end in ".xml.gz" or ".cache". String name = entries[index]; int suffixLength; if (name.endsWith(".xml.gz")) suffixLength = 7; else if (name.endsWith(".cache")) suffixLength = 6; else continue; if ((name.length() - suffixLength) < 10) continue; // Delete the file. File file = new File(httpCacheDir, name); if (debug) System.out.println("deleting " + file.getPath()); file.delete(); } } /** * Reloads the service in response to a service name or media change. */ private void reloadService() { // If the service name is empty, then there is no need for a cache. if (serviceName.length() == 0) { unloadService(); return; } // Create the cache directory if it doesn't already exist. File cacheDir = getCacheDir(); if (cacheDir == null) { unloadService(); return; } File serviceCacheDir = new File(cacheDir, serviceName); httpCacheDir = new File(serviceCacheDir, "http"); httpCacheDir.mkdirs(); if (!httpCacheDir.exists()) { unloadService(); return; } iconCacheDir = new File(serviceCacheDir, "icons"); iconCacheDir.mkdirs(); if (!iconCacheDir.exists()) iconCacheDir = null; // We have the http directory, so we can continue. // Reload the channel list using the hidden-vs-shown data on the SD card. if (!sdLoaded && !channels.isEmpty()) loadChannels(); } /** * Unloads the service in response to the media being unmounted, usually * because the SD card has been mounted via USB by another computer. */ private void unloadService() { httpCacheDir = null; iconCacheDir = null; sdLoaded = false; // Reload channel hidden-vs-shown list when SD card re-inserted. } @Override protected void mediaUsableChanged() { if (isMediaUsable()) reloadService(); else unloadService(); } /** * Get the name of the data file corresponding to a particular * channel and date. * * @param channel the channel, or null for the main channel list file * @param date the date to fetch * @param extension the file extension, ".xml.gz" or ".cache" * @return the filename encapsulated in a File object, or null if no cache */ private File dataFile(TvChannel channel, Calendar date, String extension) { if (httpCacheDir == null) return null; else if (channel == null) return new File(httpCacheDir, "channels" + extension); StringBuilder name = new StringBuilder(); int year = date.get(Calendar.YEAR); int month = date.get(Calendar.MONTH) + 1; int day = date.get(Calendar.DAY_OF_MONTH); name.append(channel.getId()); name.append('_'); name.append((char) ('0' + ((year / 1000) % 10))); name.append((char) ('0' + ((year / 100) % 10))); name.append((char) ('0' + ((year / 10) % 10))); name.append((char) ('0' + (year % 10))); name.append('-'); name.append((char) ('0' + ((month / 10) % 10))); name.append((char) ('0' + (month % 10))); name.append('-'); name.append((char) ('0' + ((day / 10) % 10))); name.append((char) ('0' + (day % 10))); name.append(extension); return new File(httpCacheDir, name.toString()); } private static String readLine(InputStreamReader reader) throws IOException { StringBuilder builder = new StringBuilder(1024); int ch; while ((ch = reader.read()) != -1 && ch != '\n') builder.append((char) ch); if (ch == -1 && builder.length() == 0) return null; return builder.toString(); } private class RequestInfo { public TvChannel channel; public Calendar date; public Calendar primaryDate; public URI uri; public File cacheFile; public File dataFile; public String etag; public String lastModified; public String userAgent; public boolean success; public boolean notFound; public RequestInfo next; public boolean isChannelListFetch() { return channel == null && date == null; } public boolean isChannelDataFetch() { return channel != null && date != null; } public boolean isChannelIconFetch() { return channel != null && date == null; } public boolean isSameFetch(RequestInfo info) { return isSameFetch(info.channel, info.date); } public boolean isSameFetch(TvChannel channel, Calendar date) { if (this.channel == null) { if (channel != null) return false; } else if (this.channel != channel) { return false; } if (this.date == null) return date == null; else if (date == null) return false; else return this.date.equals(date); } public void updateFromResponse(HttpResponse response) { Header header = response.getFirstHeader("ETag"); if (header != null) etag = header.getValue(); header = response.getFirstHeader("Last-Modified"); if (header != null) lastModified = header.getValue(); } }; private RequestInfo requestQueue; private TvChannel currentRequestChannel; private Calendar currentRequestDate; private Calendar currentRequestPrimaryDate; private boolean requestsActive; private static Calendar lastRequestTime = null; /** * Background asynchronous task for downloading data from the Internet. */ private class DownloadAsyncTask extends AsyncTask<RequestInfo, Integer, RequestInfo> { private boolean fetch(RequestInfo info) { try { // OzTivo requires that there be at least 1 second between data requests. // Icons are fetched from elsewhere so we can fetch them immediately. if (info.date != null) { Calendar currentTime = new GregorianCalendar(); if (lastRequestTime == null) { lastRequestTime = currentTime; } else { long diff = currentTime.getTimeInMillis() - lastRequestTime.getTimeInMillis(); if (diff >= 0 && diff < 1000) { try { Thread.sleep(diff); } catch (InterruptedException e) { } } lastRequestTime = currentTime; } } // Start the request. DefaultHttpClient client = new DefaultHttpClient(); HttpGet request = new HttpGet(info.uri); request.setHeader("User-Agent", info.userAgent); if (info.etag != null) request.setHeader("If-None-Match", info.etag); if (info.lastModified != null) request.setHeader("If-Modified-Since", info.lastModified); request.setHeader("Accept-Encoding", "gzip"); HttpResponse response = client.execute(request); int status = response.getStatusLine().getStatusCode(); if (status == HttpStatus.SC_OK) { // Successful response with new data. Copy it to the cache. info.updateFromResponse(response); HttpEntity entity = response.getEntity(); InputStream content = entity.getContent(); try { FileOutputStream output = new FileOutputStream(info.dataFile); byte[] buffer = new byte[2048]; try { int length; while ((length = content.read(buffer, 0, 2048)) > 0) output.write(buffer, 0, length); } finally { output.close(); } } catch (IOException e) { return false; } finally { content.close(); } return true; } else if (status == HttpStatus.SC_NOT_MODIFIED) { // Data has not changed since the last request. info.updateFromResponse(response); return true; } else if (status == HttpStatus.SC_NOT_FOUND) { // Explicit 404 Not Found from the server. info.notFound = true; return false; } else { // Request failed for some other reason. return false; } } catch (UnsupportedEncodingException e) { return false; } catch (MalformedURLException e) { return false; } catch (IOException e) { return false; } } protected RequestInfo doInBackground(RequestInfo... requests) { RequestInfo info = requests[0]; if (info.cacheFile != null && info.cacheFile.exists()) { // Read ETag/Last-Modified data from the ".cache" file. try { FileInputStream input = new FileInputStream(info.cacheFile); try { InputStreamReader reader = new InputStreamReader(input, "utf-8"); try { String line; while ((line = readLine(reader)) != null) { if (line.startsWith("ETag: ")) info.etag = line.substring(6); else if (line.startsWith("Last-Modified: ")) info.lastModified = line.substring(15); } } finally { reader.close(); } } finally { input.close(); } } catch (IOException e) { } } info.success = fetch(info); if (!info.success) { // Something failed during the request - delete the cache files // before handing the result back to the main thread. if (info.cacheFile != null) info.cacheFile.delete(); info.dataFile.delete(); } else if (info.cacheFile != null) { // Write ETag/Last-Modified data to the ".cache" file. try { FileOutputStream output = new FileOutputStream(info.cacheFile); try { OutputStreamWriter writer = new OutputStreamWriter(output, "utf-8"); try { if (info.etag != null) { writer.write("ETag: "); writer.write(info.etag); writer.write("\n"); } if (info.lastModified != null) { writer.write("Last-Modified: "); writer.write(info.lastModified); writer.write("\n"); } } finally { writer.close(); } } finally { output.close(); } } catch (IOException e) { } } return info; } protected void onProgressUpdate(Integer... progress) { // Progress reporting not used by this task. } protected void onPostExecute(RequestInfo info) { if (debug) { if (info.success) System.out.println("fetched to " + info.dataFile.getPath()); else System.out.println("fetch failed"); } reportRequestResult(info); startNextRequest(); } }; /** * Fetch bulk data for all channels. * * @param numDays the number of days to fetch * @return true if data has been scheduled to be fetched, false if nothing needs downloading */ public boolean bulkFetch(int numDays) { boolean fetched = false; boolean requestsWereActive = requestsActive; Calendar today = new GregorianCalendar(); for (TvNetworkListener listener : networkListeners) listener.setCancelable(); for (int index = 0; index < activeChannels.size(); ++index) { TvChannel channel = activeChannels.get(index); for (int day = 0; day < numDays; ++day) { Calendar date = (Calendar) today.clone(); date.add(Calendar.DAY_OF_MONTH, day); if (!hasChannelData(channel, date)) { fetch(channel, date, today); fetched = true; } } } if (!fetched && !requestsWereActive) { for (TvNetworkListener listener : networkListeners) listener.endNetworkRequests(); } return fetched; } /** * Fetches the guide data for a specific date and time. * * @param channel the channel * @param date the date to request */ public void fetch(TvChannel channel, Calendar date) { fetch(channel, date, date); } /** * Fetches the guide data for a specific date and time, as part of a multi-day request. * At least two days worth of data are needed to show 6:00am one day to 6:00am the next. * The first day is the "primary" and typically must be fetched from the server. * The second day's data is optional and an error will not be reported to the user * if it is not available. * * @param channel the channel * @param date the date to request * @param primaryDate the primary date for multi-day requests */ public void fetch(TvChannel channel, Calendar date, Calendar primaryDate) { // Bail out if the cache is unusable or there is no network. if (httpCacheDir == null || !isNetworkingAvailable()) return; // If the channels use datafor declarations, then the date must be // amongst the channel's allowable dates to proceed with the fetch. if (haveDataForDecls && !channel.hasDataFor(date)) return; // Determine the base URL to use. OzTivo rules specify that a // url should be chosen randomly from the list of base urls. // http://www.oztivo.net/twiki/bin/view/TVGuide/StaticXMLGuideAPI List<String> baseUrls = channel.getBaseUrls(); if (baseUrls.isEmpty()) return; String baseUrl; if (baseUrls.size() >= 2) baseUrl = baseUrls.get(rand.nextInt(baseUrls.size())); else baseUrl = baseUrls.get(0); // Generate the URI for the request. StringBuilder requestUrl = new StringBuilder(); int year = date.get(Calendar.YEAR); int month = date.get(Calendar.MONTH) + 1; int day = date.get(Calendar.DAY_OF_MONTH); requestUrl.append(baseUrl); if (!baseUrl.endsWith("/")) requestUrl.append('/'); requestUrl.append(channel.getId()); requestUrl.append('_'); requestUrl.append((char) ('0' + ((year / 1000) % 10))); requestUrl.append((char) ('0' + ((year / 100) % 10))); requestUrl.append((char) ('0' + ((year / 10) % 10))); requestUrl.append((char) ('0' + (year % 10))); requestUrl.append('-'); requestUrl.append((char) ('0' + ((month / 10) % 10))); requestUrl.append((char) ('0' + (month % 10))); requestUrl.append('-'); requestUrl.append((char) ('0' + ((day / 10) % 10))); requestUrl.append((char) ('0' + (day % 10))); requestUrl.append(".xml.gz"); URI uri; try { uri = new URI(requestUrl.toString()); } catch (URISyntaxException e) { return; } // Create the request info block. RequestInfo info = new RequestInfo(); info.channel = channel; info.date = date; info.primaryDate = primaryDate; info.uri = uri; info.cacheFile = dataFile(channel, date, ".cache"); info.dataFile = dataFile(channel, date, ".xml.gz"); info.etag = null; info.lastModified = null; info.userAgent = getContext().getResources().getString(R.string.user_agent); info.success = false; // Queue up the request to fetch the data from the network. addRequestToQueue(info); } /** * Fetches the main channel list from the server. */ private void fetchChannelList() { // Bail out if the cache is unusable or there is no network. if (httpCacheDir == null || !isNetworkingAvailable()) return; // Create the request info block. RequestInfo info = new RequestInfo(); info.channel = null; info.date = null; info.primaryDate = null; try { info.uri = new URI(serviceUrl); } catch (URISyntaxException e) { return; } info.cacheFile = dataFile(null, null, ".cache"); info.dataFile = dataFile(null, null, ".xml.gz"); info.etag = null; info.lastModified = null; info.userAgent = getContext().getResources().getString(R.string.user_agent); info.success = false; // Queue up the request to fetch the data from the network. addRequestToQueue(info); } /** * Fetches a channel icon from the network. * * @param channel the channel * @param uri the URI of the icon's location on the network * @param file the local file to cache the icon data in */ private void fetchIcon(TvChannel channel, String uri, File file) { // Bail out if the cache is unusable or there is no network. if (iconCacheDir == null || !isNetworkingAvailable()) return; // Parse the URI. URI uriObject; try { uriObject = new URI(uri); } catch (URISyntaxException e) { return; } // Create the request info block. Null date indicates that this is an icon fetch. RequestInfo info = new RequestInfo(); info.channel = channel; info.date = null; info.primaryDate = null; info.uri = uriObject; info.cacheFile = null; info.dataFile = file; info.etag = null; info.lastModified = null; info.userAgent = getContext().getResources().getString(R.string.user_agent); info.success = false; // Queue up the request to fetch the data from the network. addRequestToQueue(info); } /** * Adds a request to the queue of files to be downloaded. * * @param info the request to be added */ private void addRequestToQueue(RequestInfo info) { // Ignore the request if it is already on the queue. RequestInfo current = requestQueue; RequestInfo prev = null; while (current != null) { if (current.isSameFetch(info) && current.isChannelDataFetch()) { // Upgrade the existing request to a primary day request if necessary. if (info.primaryDate.equals(info.date)) current.primaryDate = current.date; return; } prev = current; current = current.next; } if (info.isSameFetch(currentRequestChannel, currentRequestDate) && info.isChannelDataFetch()) { if (info.primaryDate.equals(info.date)) currentRequestPrimaryDate = currentRequestDate; return; } // Add the request to the end of the queue. info.next = null; if (prev != null) prev.next = info; else requestQueue = info; // If we don't have a request currently in progress, then start it. if (!requestsActive) startNextRequest(); } /** * Start downloading the next request on the queue. */ private void startNextRequest() { for (;;) { RequestInfo info = requestQueue; if (info == null) { currentRequestChannel = null; currentRequestDate = null; currentRequestPrimaryDate = null; if (requestsActive) { requestsActive = false; for (TvNetworkListener listener : networkListeners) listener.endNetworkRequests(); } break; } requestQueue = info.next; info.next = null; if (info.isChannelDataFetch() && hasChannelData(info.channel, info.date)) { // A previous request on the queue already fetched this data. for (TvNetworkListener listener : networkListeners) listener.dataAvailable(info.channel, info.date, info.primaryDate); continue; } currentRequestChannel = info.channel; currentRequestDate = info.date; currentRequestPrimaryDate = info.primaryDate; if (debug) System.out.println("fetching " + info.uri.toString()); requestsActive = true; for (TvNetworkListener listener : networkListeners) { if (info.isChannelDataFetch()) listener.setCurrentNetworkRequest(info.channel, info.date, info.primaryDate); else if (info.isChannelListFetch()) listener.setCurrentNetworkListRequest(); else listener.setCurrentNetworkIconRequest(info.channel); } new DownloadAsyncTask().execute(info); break; } } private void reportRequestResult(RequestInfo info) { for (TvNetworkListener listener : networkListeners) { if (info.date == null) continue; else if (info.success) listener.dataAvailable(info.channel, info.date, currentRequestPrimaryDate); else listener.requestFailed(info.channel, info.date, currentRequestPrimaryDate); } if (info.isChannelIconFetch() && info.success) { info.channel.setIconFile(info.dataFile.getPath()); for (TvChannelChangedListener channelListener : channelListeners) channelListener.channelsChanged(); } else if (info.isChannelListFetch()) { // Main channel list has been fetched - reload the channels. mainListFetching = false; if (info.success) { Calendar lastmod = channelDataLastModified(null, null); if (lastmod != null && mainListLastMod != null && lastmod.equals(mainListLastMod)) mainListUnchanged = true; // Break infinite loop: don't try to fetch again. loadChannels(); } } } public void addNetworkListener(TvNetworkListener listener) { networkListeners.add(listener); } public void removeNetworkListener(TvNetworkListener listener) { networkListeners.remove(listener); } public void addChannelChangedListener(TvChannelChangedListener listener) { channelListeners.add(listener); } public void removeChannelChangedListener(TvChannelChangedListener listener) { channelListeners.remove(listener); } /** * Cancels all pending requests. */ public void cancelRequests() { requestQueue = null; } /** * Loads or reloads channel information. */ public void loadChannels() { // Load the channels from the embedded resources first. if (!embeddedLoaded) { if (region == null || getContext() == null) return; Calendar start = new GregorianCalendar(); XmlResourceParser parser = getContext().getResources().getXml(R.xml.channels); loadChannelsFromXml(parser); parser.close(); embeddedLoaded = true; if (debug) { double time = ((new GregorianCalendar()).getTimeInMillis() - start.getTimeInMillis()) / 1000.0; System.out.println("time to parse embedded channel list: " + time); } } // Load the hidden-vs-shown state from the SD card. if (!sdLoaded && isMediaUsable()) { File serviceDir = new File(getFilesDir(), serviceName); File file = new File(serviceDir, "channels.xml"); if (file.exists()) { Calendar start = new GregorianCalendar(); try { FileInputStream fileStream = new FileInputStream(file); try { XmlPullParserFactory factory = XmlPullParserFactory.newInstance(); XmlPullParser parser = factory.newPullParser(); parser.setInput(fileStream, null); loadChannelsFromXml(parser); } catch (XmlPullParserException e) { // Ignore - just stop parsing at the first error. } finally { fileStream.close(); } } catch (IOException e) { } if (debug) { double time = ((new GregorianCalendar()).getTimeInMillis() - start.getTimeInMillis()) / 1000.0; System.out.println("time to parse SD config channel list: " + time); } } sdLoaded = true; } // Load the server's channel list to refresh data-for declarations. if (!mainListLoaded) { InputStream inputStream = null; Calendar lastmod = channelDataLastModified(null, null); Calendar now = new GregorianCalendar(); mainListLastFetched = now; if (mainListUnchanged || lastmod == null || (now.getTimeInMillis() - lastmod.getTimeInMillis()) < (24 * 60 * 60 * 1000)) inputStream = openChannelData(null, null); // Reuse previous list if less than 24 hours old mainListUnchanged = false; if (inputStream != null) { Calendar start = new GregorianCalendar(); try { try { XmlPullParserFactory factory = XmlPullParserFactory.newInstance(); XmlPullParser parser = factory.newPullParser(); parser.setInput(inputStream, null); loadChannelsFromXml(parser); } catch (XmlPullParserException e) { // Ignore - just stop parsing at the first error. } finally { inputStream.close(); } } catch (IOException e) { } mainListLoaded = true; if (debug) { double time = ((new GregorianCalendar()).getTimeInMillis() - start.getTimeInMillis()) / 1000.0; System.out.println("time to parse network channel list: " + time); } } else if (!mainListFetching && isNetworkingAvailable()) { // Fetch the channel list for the first time. mainListFetching = true; mainListLastMod = lastmod; fetchChannelList(); } } // Rebuild the active channel list. activeChannels.clear(); for (TvChannel channel : channels.values()) { if (channel.getHiddenState() == TvChannel.HIDDEN_BY_REGION) { String region = channel.getRegion(); if (region == null || !regionMatch(region)) continue; } else if (channel.getHiddenState() == TvChannel.HIDDEN) { continue; } if (haveDataForDecls && !channel.hasDataFor()) continue; // No data for the channel on the server, so block it. activeChannels.add(channel); if (channel.iconNeedsFetching()) fetchIcon(channel, channel.getIconSource(), new File(channel.getIconFile())); } Collections.sort(activeChannels); // Notify interested parties that the active channel list has changed. for (TvChannelChangedListener listener : channelListeners) listener.channelsChanged(); } /** * Loads channel information from an XML stream. There may be three types of streams: * 1. channels.xml from the embedded resources; 2. channels.xml from the SD card which * defines which channels are shown and hidden; 3. channel list from the server. * * @param parser XML stream to load the channels from */ private void loadChannelsFromXml(XmlPullParser parser) { String id = null; String parent; try { int eventType = parser.getEventType(); while (eventType != XmlPullParser.END_DOCUMENT) { if (eventType == XmlPullParser.START_TAG) { String name = parser.getName(); if (name.equals("channel")) { // Parse the contents of a <channel> element. id = parser.getAttributeValue(null, "id"); TvChannel channel = channels.get(id); if (channel == null) { channel = new TvChannel(); channel.setId(id); channel.setName(id); } String hidden = parser.getAttributeValue(null, "hidden-state"); if (hidden != null) { if (hidden.equals("hide")) channel.setHiddenState(TvChannel.HIDDEN); else if (hidden.equals("show")) channel.setHiddenState(TvChannel.NOT_HIDDEN); else if (hidden.equals("by-region")) channel.setHiddenState(TvChannel.HIDDEN_BY_REGION); } String region = parser.getAttributeValue(null, "region"); if (region != null) { channel.setRegion(region); channel.setHiddenState(TvChannel.HIDDEN_BY_REGION); channel.setDefaultHiddenState(TvChannel.HIDDEN_BY_REGION); } loadChannel(channel, parser); channels.put(id, channel); } else if (name.equals("region")) { // Parse the contents of a <region> element. id = parser.getAttributeValue(null, "id"); parent = parser.getAttributeValue(null, "parent"); if (id != null && parent != null) { if (!regionTree.containsKey(id)) regionTree.put(id, new ArrayList<String>()); if (!regionTree.get(id).contains(parent)) regionTree.get(id).add(parent); } } else if (name.equals("other-parent")) { // Secondary parent for the current region. parent = Utils.getContents(parser, name); if (!regionTree.containsKey(id)) regionTree.put(id, new ArrayList<String>()); if (!regionTree.get(id).contains(parent)) regionTree.get(id).add(parent); } } eventType = parser.next(); } } catch (XmlPullParserException e) { // Ignore - just stop parsing at the first error. } catch (IOException e) { } } private void loadChannel(TvChannel channel, XmlPullParser parser) throws XmlPullParserException, IOException { String commonId = parser.getAttributeValue(null, "common-id"); if (commonId != null && channel.getCommonId() == null) { // Keep track of all channels with the same common identifier in a shared list. // We use this to migrate bookmarks across regions. channel.setCommonId(commonId); ArrayList<String> list = commonIds.get(commonId); if (list != null) { list.add(channel.getId()); } else { list = new ArrayList<String>(); list.add(channel.getId()); commonIds.put(commonId, list); } channel.setOtherChannelsList(list); } int eventType = parser.next(); boolean hadNumbers = channel.getNumbers() != null; boolean hadDataFor = channel.hasDataFor(); while (eventType != XmlPullParser.END_DOCUMENT) { if (eventType == XmlPullParser.START_TAG) { String name = parser.getName(); if (name.equals("datafor")) { if (hadDataFor) { // Loading new datafor declarations to replace previous list. channel.clearDataFor(); hadDataFor = false; } String lastmod = parser.getAttributeValue(null, "lastmodified"); String dateStr = Utils.getContents(parser, name); if (lastmod != null && dateStr != null) { // Parse the date in the format YYYY-MM-DD. int year = Utils.parseField(dateStr, 0, 4); int month = Utils.parseField(dateStr, 5, 2); int day = Utils.parseField(dateStr, 8, 2); FastCalendar date = new FastCalendar(year, month - 1, day); channel.addDataFor(date, Utils.parseDateTimeFast(lastmod)); haveDataForDecls = true; } } else if (name.equals("base-url")) { // Ignored for now. } else if (name.equals("display-name")) { channel.setName(Utils.getContents(parser, name)); } else if (name.equals("icon") && channel.getIconResource() == 0 && channel.getIconSource() == null) { String src = parser.getAttributeValue(null, "src"); if (src != null) { int index = src.lastIndexOf('/'); if (index >= 0) { String filename = src.substring(index + 1); int resource = IconFactory.getInstance().getChannelIconResource(filename); if (resource != 0) { channel.setIconResource(resource); } else if (iconCacheDir != null) { String iconFile = iconCacheDir + "/" + filename; channel.setIconFile(iconFile); channel.setIconSource(src); } } } } else if (name.equals("number") && !hadNumbers) { String system = parser.getAttributeValue(null, "system"); String currentNumbers = channel.getNumbers(); if (!system.equals("digital")) { if (currentNumbers == null) { // Hide Pay TV only channels for now, and mark them to convert // programmes into the local timezone. channel.setHiddenState(TvChannel.HIDDEN); channel.setDefaultHiddenState(TvChannel.HIDDEN); channel.setConvertTimezone(true); String number = Utils.getContents(parser, name); channel.setNumbers(number); channel.setPrimaryChannelNumber(Integer.valueOf(number)); } } else { String number = Utils.getContents(parser, name); if (currentNumbers == null) { channel.setNumbers(number); channel.setPrimaryChannelNumber(Integer.valueOf(number)); } else { channel.setNumbers(currentNumbers + ", " + number); } } } else if (name.equals("convert-timezone")) { channel.setConvertTimezone(true); } } else if (eventType == XmlPullParser.END_TAG && parser.getName().equals("channel")) { break; } eventType = parser.next(); } List<String> baseUrls = new ArrayList<String>(); baseUrls.add("http://www.oztivo.net/xmltv/"); baseUrls.add("http://xml.oztivo.net/xmltv/"); channel.setBaseUrls(baseUrls); } /** * Saves the hidden-vs-shown states of all channels to the SD card. */ public void saveChannelHiddenStates() { if (!isMediaUsable()) return; File serviceDir = new File(getFilesDir(), serviceName); serviceDir.mkdirs(); File file = new File(serviceDir, "channels.xml"); try { FileOutputStream fileStream = new FileOutputStream(file); XmlSerializer serializer = Xml.newSerializer(); serializer.setOutput(fileStream, "UTF-8"); serializer.startDocument(null, Boolean.valueOf(true)); serializer.setFeature("http://xmlpull.org/v1/doc/features.html#indent-output", true); serializer.startTag(null, "tv"); for (TvChannel channel : channels.values()) { if (channel.getHiddenState() == channel.getDefaultHiddenState()) continue; // Don't save channels that have their default hidden state. serializer.startTag(null, "channel"); serializer.attribute(null, "id", channel.getId()); if (channel.getHiddenState() == TvChannel.HIDDEN) serializer.attribute(null, "hidden-state", "hide"); else if (channel.getHiddenState() == TvChannel.NOT_HIDDEN) serializer.attribute(null, "hidden-state", "show"); else serializer.attribute(null, "hidden-state", "by-region"); serializer.endTag(null, "channel"); } serializer.endTag(null, "tv"); serializer.endDocument(); fileStream.close(); } catch (IOException e) { } } private boolean regionMatch(String r) { if (r.equals(region)) return true; List<String> testRegions = new ArrayList<String>(); testRegions.add(region); return regionMatch(r, testRegions); } private boolean regionMatch(String r, List<String> regions) { for (String region : regions) { if (r.equals(region)) return true; List<String> testRegions = regionTree.get(region); if (testRegions != null && regionMatch(r, testRegions)) return true; } return false; } /** * Determine if networking is available at the present time. * * @return true if networking is available, false if not (e.g. airplane mode). */ public boolean isNetworkingAvailable() { Context context = getContext(); if (context == null) return false; ConnectivityManager manager = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE); if (manager == null) return false; NetworkInfo info = manager.getActiveNetworkInfo(); if (info == null) return false; return info.isAvailable() && info.isConnected(); } public enum LastActivity { DefaultActivity, ProgrammeListActivity, BookmarkListActivity } private LastActivity lastActivity = LastActivity.DefaultActivity; public LastActivity getLastActivity() { return lastActivity; } public void setLastActivity(LastActivity activity) { lastActivity = activity; } }