/*******************************************************************************
* BBC News Reader
* Released under the BSD License. See README or LICENSE.
* Copyright (c) 2011, Digital Lizard (Oscar Key, Thomas Boby)
* All rights reserved.
******************************************************************************/
package com.digitallizard.bbcnewsreader;
import java.util.ArrayList;
import org.mcsoxford.rss.RSSItem;
import android.app.AlarmManager;
import android.app.PendingIntent;
import android.app.Service;
import android.appwidget.AppWidgetManager;
import android.content.BroadcastReceiver;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.SharedPreferences;
import android.content.SharedPreferences.Editor;
import android.content.SharedPreferences.OnSharedPreferenceChangeListener;
import android.net.ConnectivityManager;
import android.net.NetworkInfo;
import android.os.Binder;
import android.os.Bundle;
import android.os.Handler;
import android.os.IBinder;
import android.os.Message;
import android.os.Messenger;
import android.os.RemoteException;
import android.util.Log;
import com.digitallizard.bbcnewsreader.data.DatabaseHandler;
import com.digitallizard.bbcnewsreader.resource.web.WebManager;
import com.digitallizard.bbcnewsreader.widget.ReaderWidget;
public class ResourceService extends Service implements ResourceInterface {
/* variables */
public boolean loadInProgress; // a flag to tell the activity if there is a load in progress
ArrayList<Messenger> clients = new ArrayList<Messenger>(); // holds references to all of our clients
final Messenger messenger = new Messenger(new IncomingHandler()); // the messenger used for communication
BroadcastReceiver broadcastReceiver;
DatabaseHandler database; // the database
RSSManager rssManager;
WebManager webManager;
SharedPreferences settings;
OnSharedPreferenceChangeListener settingsChangedListener;
int totalItemsToDownload;
int itemsDownloaded;
/* command definitions */
public static final int MSG_REGISTER_CLIENT = 1;
public static final int MSG_UNREGISTER_CLIENT = 2;
public static final int MSG_CLIENT_REGISTERED = 3; // returned to a client when registered
public static final int MSG_LOAD_DATA = 4; // sent to request a data load
public static final int MSG_LOAD_ARTICLE = 11;
public static final int MSG_LOAD_THUMB = 12;
public static final int MSG_LOAD_IMAGE = 13;
public static final int MSG_STOP_DATA_LOAD = 9; // sent to stop data loading
public static final int MSG_CATEGORY_LOADED = 6; // sent when a category has loaded
public static final int MSG_ARTICLE_LOADED = 15; // article loaded
public static final int MSG_THUMB_LOADED = 14; // thumbnail loaded
public static final int MSG_NOW_LOADING = 16;
public static final int MSG_FULL_LOAD_COMPLETE = 8; // sent when all the data has been loaded
public static final int MSG_RSS_LOAD_COMPLETE = 10;
public static final int MSG_UPDATE_LOAD_PROGRESS = 18;
public static final int MSG_ERROR = 7; // help! An error occurred
public static final String KEY_ERROR_TYPE = "type";
public static final String KEY_ERROR_MESSAGE = "message";
public static final String KEY_ERROR_ERROR = "error";
public static final String KEY_CATEGORY = "category";
public static final String KEY_ITEM_ID = "itemId";
public static final String ACTION_LOAD = "com.digitallizard.bbcnewsreader.action.LOAD_NEWS";
// the handler class to process new messages
class IncomingHandler extends Handler {
@Override
public void handleMessage(Message msg) {
// decide what to do with the message
switch (msg.what) {
case MSG_REGISTER_CLIENT:
clients.add(msg.replyTo); // add a reference to the client to our list
sendMsg(msg.replyTo, MSG_CLIENT_REGISTERED, null);
break;
case MSG_UNREGISTER_CLIENT:
unregisterClient(msg.replyTo);
break;
case MSG_LOAD_DATA:
loadData(); // start of the loading of data
break;
case MSG_LOAD_ARTICLE:
loadArticle(msg.getData().getInt(KEY_ITEM_ID));
break;
case MSG_LOAD_THUMB:
loadThumbnail(msg.getData().getInt("itemId"));
break;
case MSG_LOAD_IMAGE:
// TODO load specific image
break;
case MSG_STOP_DATA_LOAD:
stopDataLoad();
break;
default:
super.handleMessage(msg); // we don't know what to do, lets hope that the super class knows
}
}
}
public class ResourceBinder extends Binder {
ResourceService getService() {
return ResourceService.this;
}
}
public synchronized void setDatabase(DatabaseHandler db) {
this.database = db;
}
public synchronized DatabaseHandler getDatabase() {
return database;
}
public synchronized void setWebManager(WebManager manager) {
this.webManager = manager;
}
public synchronized WebManager getWebManager() {
return this.webManager;
}
void loadData() {
// check if the device is online
if (isOnline()) {
// report to the gui that a load has been activated
sendMsgToAll(MSG_NOW_LOADING, null);
// set the flag saying that we are loading
loadInProgress = true;
// retrieve the active category urls
String[][] enabledCategories = getDatabase().getEnabledCategories();
String[] urls = enabledCategories[0];
String[] names = enabledCategories[1];
// start the RSS Manager
rssManager.load(names, urls);
}
else {
// report that there is no internet connection
reportError(ReaderActivity.ERROR_TYPE_INTERNET, "There is no internet connection.", null);
}
}
void loadArticle(int id) {
String url = database.getUrl(id); // get the url of the item
webManager.loadNow(url, WebManager.ITEM_TYPE_HTML, id); // tell the webmanager to load this
}
void loadThumbnail(int id) {
String url = database.getThumbnailUrl(id); // get the url of the item
if (url == null) {
database.addThumbnail(id, ReaderActivity.NO_THUMBNAIL_URL_CODE);// Set thumbnail to no thumbnail
// report that the thumbnail has been loaded so it can be displayed
Bundle bundle = new Bundle();
bundle.putInt("id", id);
sendMsgToAll(MSG_THUMB_LOADED, bundle);
}
else {
webManager.loadNow(url, WebManager.ITEM_TYPE_THUMB, id); // tell the webmanager to load this
}
}
void loadImage(int id) {
// TODO add specific image loading
}
void stopDataLoad() {
// stop the data loading
rssManager.stopLoading();
getWebManager().stopDownload();
// the stopping of loading will be reported by the managers...
}
void updateLastLoadTime() {
// store the new time in the preferences file
Editor editor = settings.edit();
long time = (long) Math.floor(System.currentTimeMillis() / 1000); // unix time of now
editor.putLong("lastLoadTime", time);
editor.commit();
}
void sendMsg(Messenger client, int what, Bundle bundle) {
try {
// create a message according to parameters
Message msg = Message.obtain(null, what);
if (bundle != null) {
msg.setData(bundle);
}
client.send(msg); // send the message
} catch (RemoteException e) {
// We are probably shutting down, but report it anyway
Log.e("ERROR", "Unable to send message to client: " + e.getMessage());
}
}
void sendMsg(int clientId, int what, Bundle bundle) {
// simply call the main sendMessage but with an actual client
sendMsg(clients.get(clientId), what, bundle);
}
void sendMsgToAll(int what, Bundle bundle) {
// loop through and send the message to all the clients
for (int i = 0; i < clients.size(); i++) {
sendMsg(i, what, bundle);
}
}
private void unregisterClient(Messenger client) {
// remove our reference to the client
clients.remove(client);
// if we have no more clients and a load is not in progress, shutdown
if(clients.isEmpty() && !loadInProgress) {
stopSelf();
}
}
boolean isOnline() {
ConnectivityManager manager = (ConnectivityManager) getSystemService(Context.CONNECTIVITY_SERVICE);
NetworkInfo info = manager.getActiveNetworkInfo();
// check that there is an active network
if (info != null) {
return info.isConnected();
}
else {
return false;
}
}
/**
* Called when an RSS feed has loaded
*
* @param item
* The item that has been loaded
*/
public synchronized void categoryRssLoaded(RSSItem[] items, String category) {
// clear the priorities for this category to prevent old items hanging around
database.clearPriorities(category);
// insert the items into the database
for (int i = 0; i < items.length; i++) {
// check there are some thumbnails
String thumbUrl = null;
if (items[i].getThumbnails().size() == 2) {
thumbUrl = items[i].getThumbnails().get(1).toString();
}
getDatabase().insertItem(items[i].getTitle(), items[i].getDescription(), category, items[i].getPubDate(), items[i].getLink().toString(),
thumbUrl, i);
}
// send a message to the gui to tell it that we have loaded the category
Bundle bundle = new Bundle();
bundle.putString(KEY_CATEGORY, category);
sendMsgToAll(MSG_CATEGORY_LOADED, bundle);
}
public synchronized void reportError(int type, String msg, String error) {
// an error has occurred, send a message to the gui
// this will display something useful to the user
Bundle bundle = new Bundle();
bundle.putInt(KEY_ERROR_TYPE, type);
bundle.putString(KEY_ERROR_MESSAGE, msg);
bundle.putString(KEY_ERROR_ERROR, error);
sendMsgToAll(MSG_ERROR, bundle);
}
public synchronized void rssLoadComplete(boolean successful) {
// check if the load was successful before continuing
if (!successful) {
fullLoadComplete(false); // end the load here, it was not successful
return; // bail
}
updateLastLoadTime(); // save last load time
// tell the gui
sendMsgToAll(MSG_RSS_LOAD_COMPLETE, null);
// add unloaded items to the download queue
totalItemsToDownload = 0;
itemsDownloaded = 0;
// query the database to find out which items to load
int itemLoadLimit = settings.getInt("itemLoadLimit", ReaderActivity.DEFAULT_ITEM_LOAD_LIMIT); // the limit for the number of items to load
Integer[][] items = database.getUndownloaded(itemLoadLimit);
// load the undownloaded articles
Integer[] htmlIds = items[DatabaseHandler.COLUMN_UNDOWNLOADED_ARTICLES];
for (int t = 0; t < htmlIds.length; t++) {
String url = database.getUrl(htmlIds[t]);
webManager.addToQueue(url, WebManager.ITEM_TYPE_HTML, htmlIds[t]);
}
// load the undownloaded thumbnails
Integer[] thumbIds = items[DatabaseHandler.COLUMN_UNDOWNLOADED_ARTICLES];
for (int t = 0; t < thumbIds.length; t++) {
String url = database.getThumbnailUrl(thumbIds[t]);
// check if there is a thumbnail url, if so load it
if (url == null) {
database.addThumbnail(thumbIds[t], ReaderActivity.NO_THUMBNAIL_URL_CODE);// Set thumbnail to no thumbnail
// report that the thumbnail has been loaded so it can be displayed
Bundle bundle = new Bundle();
bundle.putInt("id", thumbIds[t]);
sendMsgToAll(MSG_THUMB_LOADED, bundle);
}
else {
webManager.addToQueue(url, WebManager.ITEM_TYPE_THUMB, thumbIds[t]);
}
}
// set the items to download
totalItemsToDownload = htmlIds.length + thumbIds.length;
reportItemsToDownload();
// if we didn't have to add anything, report the load as fully complete
if (webManager.isQueueEmpty()) {
fullLoadComplete(true);
}
// update the widget, if the load was successful
if (successful) {
AppWidgetManager widgetManager = AppWidgetManager.getInstance(this);
ComponentName provider = new ComponentName(this, ReaderWidget.class);
int[] ids = widgetManager.getAppWidgetIds(provider);
// only broadcast an update request if there are some active widgets
if (ids.length > 0) {
Intent intent = new Intent(AppWidgetManager.ACTION_APPWIDGET_UPDATE);
intent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_IDS, ids);
sendBroadcast(intent);
}
}
}
public synchronized void fullLoadComplete(boolean successful) {
// set the flag to false
loadInProgress = false;
// if we have clients, tell them the load is complete, else shutdown
if(!clients.isEmpty()) {
// send a message saying that we have loaded
sendMsgToAll(MSG_FULL_LOAD_COMPLETE, null);
} else {
stopSelf();
}
}
public synchronized void itemDownloadComplete(boolean specific, int itemId, int type, Object download) {
// choose what to do depending on the type of object
if (type == WebManager.ITEM_TYPE_HTML) {
byte[] html = (byte[]) download;
database.addHtml(itemId, html);
// if this item was specifically requested we need to report that it has been loaded
if (specific) {
Bundle bundle = new Bundle();
bundle.putInt(KEY_ITEM_ID, itemId);
sendMsgToAll(MSG_ARTICLE_LOADED, bundle); // tell every client about the load
}
}
if (type == WebManager.ITEM_TYPE_IMAGE) {
byte[] image = (byte[]) download;
database.addImage(itemId, image);
// report that this thumbnail has been loading to the ui
Bundle bundle = new Bundle();
bundle.putInt(KEY_ITEM_ID, itemId);
sendMsgToAll(MSG_THUMB_LOADED, bundle);
}
if (type == WebManager.ITEM_TYPE_THUMB) {
byte[] thumb = (byte[]) download;
database.addThumbnail(itemId, thumb);
// report that the thumbnail has been loaded so it can be displayed
Bundle bundle = new Bundle();
bundle.putInt(KEY_ITEM_ID, itemId);
sendMsgToAll(MSG_THUMB_LOADED, bundle);
}
if (!specific) {
// increment the number of items that have been loaded
incrementItemsToDownload();
}
}
void reportItemsToDownload() {
// check if a load is in progress before sending this signal
if (loadInProgress) {
Bundle bundle = new Bundle();
bundle.putInt("totalItems", totalItemsToDownload);
bundle.putInt("itemsDownloaded", itemsDownloaded);
sendMsgToAll(MSG_UPDATE_LOAD_PROGRESS, bundle);
}
}
void incrementItemsToDownload() {
itemsDownloaded++;
reportItemsToDownload();
}
void updateSettings() {
// get the alarm manager to allow triggering of loads in the future
AlarmManager alarmManager = (AlarmManager) getSystemService(ALARM_SERVICE);
// produce the intent to trigger a load
Intent intent = new Intent(ACTION_LOAD);
PendingIntent pendingIntent = PendingIntent.getService(this, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT);
// PendingIntent pendingIntent = PendingIntent.getBroadcast(this, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT);
// register alarms for background loading
if (settings.getBoolean(ReaderActivity.PREFKEY_LOAD_IN_BACKGROUND, ReaderActivity.DEFAULT_LOAD_IN_BACKGROUND)) {
// background loading is switched on, register an alarm to trigger loads, first work out the interval
String loadIntervalString = settings.getString(ReaderActivity.PREFKEY_LOAD_INTERVAL, ReaderActivity.DEFAULT_LOAD_INTERVAL);
long loadInterval;
if (loadIntervalString.equals("15_mins")) {
loadInterval = AlarmManager.INTERVAL_FIFTEEN_MINUTES;
}
else if (loadIntervalString.equals("30_mins")) {
loadInterval = AlarmManager.INTERVAL_HALF_HOUR;
}
else if (loadIntervalString.equals("1_hour")) {
loadInterval = AlarmManager.INTERVAL_HOUR;
}
else if (loadIntervalString.equals("half_day")) {
loadInterval = AlarmManager.INTERVAL_HALF_DAY;
}
else {
loadInterval = AlarmManager.INTERVAL_HOUR;
}
// work out the starting time of the alarm
long startingTime = System.currentTimeMillis() + loadInterval; // now plus the interval
// register an alarm to start loads, depending on rtc wakeup
if (settings.getBoolean(ReaderActivity.PREFKEY_RTC_WAKEUP, ReaderActivity.DEFAULT_RTC_WAKEUP)) {
alarmManager.setInexactRepeating(AlarmManager.RTC_WAKEUP, startingTime, loadInterval, pendingIntent);
}
else {
alarmManager.setInexactRepeating(AlarmManager.RTC, startingTime, loadInterval, pendingIntent);
}
}
else {
// background loading is switched off, cancel alarms
alarmManager.cancel(pendingIntent);
}
}
@Override
public void onCreate() {
// init variables
loadInProgress = false;
// load various key components
if (settings == null) {
// load in the settings
settings = getSharedPreferences(ReaderActivity.PREFS_FILE_NAME, MODE_PRIVATE); // load settings in read/write form
}
if (database == null) {
// load the database
setDatabase(new DatabaseHandler(this));
// create tables in the database if needed
if (!getDatabase().isCreated()) {
getDatabase().addCategoriesFromXml();
}
}
if (getWebManager() == null) {
// load the web manager
setWebManager(new WebManager(this));
}
if (rssManager == null) {
// load the rss manager
rssManager = new RSSManager(this);
}
// register to receive alerts when a load is required
broadcastReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
if (intent.getAction().equals("com.digitallizard.bbcnewsreader.action.LOAD_NEWS")) {
loadData(); // load the news
}
}
};
this.registerReceiver(broadcastReceiver, new IntentFilter(ACTION_LOAD));
// load in the settings
updateSettings();
// register a change listener on the settings
settingsChangedListener = new OnSharedPreferenceChangeListener() {
public void onSharedPreferenceChanged(SharedPreferences preferences, String key) {
// update the settings
updateSettings();
}
};
settings.registerOnSharedPreferenceChangeListener(settingsChangedListener);
}
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
// check if we have been told to load news on start
if (intent != null) {
if (intent.getAction().equals(ACTION_LOAD)) {
// start a load
loadData();
}
}
// we want to continue running until explicitly stopped, so return sticky.
return START_STICKY;
}
@Override
public void onDestroy() {
// unregister receivers
this.unregisterReceiver(broadcastReceiver);
if (settings != null && settingsChangedListener != null) {
settings.unregisterOnSharedPreferenceChangeListener(settingsChangedListener);
}
super.onDestroy();
}
@Override
public IBinder onBind(Intent intent) {
return messenger.getBinder();
}
}