/*******************************************************************************
* 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 android.app.AlertDialog;
import android.app.Dialog;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.SharedPreferences;
import android.content.SharedPreferences.Editor;
import android.content.pm.ActivityInfo;
import android.os.Bundle;
import android.os.Message;
import android.util.Log;
import android.view.View;
import com.actionbarsherlock.app.SherlockFragmentActivity;
import com.actionbarsherlock.view.Menu;
import com.actionbarsherlock.view.MenuItem;
import com.digitallizard.bbcnewsreader.ServiceManager.MessageReceiver;
import com.digitallizard.bbcnewsreader.data.DatabaseHandler;
import com.digitallizard.bbcnewsreader.fragments.ArticleFragment;
import com.digitallizard.bbcnewsreader.fragments.FrontpageFragment;
import com.digitallizard.bbcnewsreader.fragments.FrontpageFragment.FrontPageClickHandler;
public class ReaderActivity extends SherlockFragmentActivity implements MessageReceiver, FrontPageClickHandler {
/* constants */
public static final String AD_PUB_ID = "a14f0749c716805";
static final int ACTIVITY_CHOOSE_CATEGORIES = 1;
public static final int DISPLAY_MODE_HANDSET = 0;
public static final int DISPLAY_MODE_TABLET_LANDSCAPE = 1;
public static final String PREFS_FILE_NAME = "com.digitallizard.bbcnewsreader_preferences";
public static final int DEFAULT_ITEM_LOAD_LIMIT = 4;
public static final int DEFAULT_CLEAR_OUT_AGE = 4;
public static final boolean DEFAULT_LOAD_IN_BACKGROUND = true;
public static final boolean DEFAULT_RTC_WAKEUP = true;
public static final String DEFAULT_LOAD_INTERVAL = "1_hour";
public static final boolean DEFAULT_DISPLAY_FULL_ERROR = false;
public static final int DEFAULT_CATEGORY_UPDATE_VERSION = 0;
public static final int CURRENT_CATEGORY_UPDATE_VERSION = 0;
public static final String PREFKEY_LOAD_IN_BACKGROUND = "loadInBackground";
public static final String PREFKEY_RTC_WAKEUP = "rtcWakeup";
public static final String PREFKEY_LOAD_INTERVAL = "loadInterval";
public static final String PREFKEY_CATEGORY_UPDATE_VERSION = "categoryUpdateVersion";
public static final int ERROR_TYPE_GENERAL = 0;
public static final int ERROR_TYPE_INTERNET = 1;
public static final int ERROR_TYPE_FATAL = 2;
public static final byte[] NO_THUMBNAIL_URL_CODE = new byte[] { 127 };
/* variables */
private DatabaseHandler database;
private ServiceManager service;
private SharedPreferences settings;
private int currentDisplayMode;
private boolean loadInProgress;
private long lastLoadTime;
private boolean errorWasFatal;
private boolean errorDuringThisLoad;
private boolean firstRun;
private Dialog errorDialog;
private Dialog firstRunDialog;
private Dialog backgroundLoadDialog;
private Menu menu;
public void handleMessage(Message msg) {
// decide what to do with the message
switch (msg.what) {
case ResourceService.MSG_CLIENT_REGISTERED:
// start a load if we haven't loaded within half an hour
// TODO make the load time configurable
long difference = System.currentTimeMillis() - (lastLoadTime * 1000); // the time since the last load
if (lastLoadTime == 0 || difference > (60 * 60 * 1000)) {
// don't load if this is the first run
if(!firstRun) {
loadData(); // trigger a load
}
}
break;
case ResourceService.MSG_ERROR:
Bundle bundle = msg.getData(); // retrieve the data
errorOccured(bundle.getInt(ResourceService.KEY_ERROR_TYPE), bundle.getString(ResourceService.KEY_ERROR_MESSAGE),
bundle.getString(ResourceService.KEY_ERROR_ERROR));
break;
case ResourceService.MSG_NOW_LOADING:
loadBegun();
break;
case ResourceService.MSG_FULL_LOAD_COMPLETE:
fullLoadComplete();
break;
case ResourceService.MSG_RSS_LOAD_COMPLETE:
rssLoadComplete();
break;
case ResourceService.MSG_UPDATE_LOAD_PROGRESS:
int totalItems = msg.getData().getInt("totalItems");
int itemsLoaded = msg.getData().getInt("itemsDownloaded");
updateLoadProgress(totalItems, itemsLoaded);
break;
}
}
public void onItemClick(int id) {
// either transition to the article view activity or update the article fragment
if (currentDisplayMode == DISPLAY_MODE_HANDSET) {
// launch the article activity
Intent intent = new Intent(this, ArticleActivity.class);
intent.putExtra(ArticleActivity.EXTRA_KEY_ITEM_ID, id);
startActivity(intent);
}
else if (currentDisplayMode == DISPLAY_MODE_TABLET_LANDSCAPE) {
// display the article
ArticleFragment article = (ArticleFragment) getSupportFragmentManager().findFragmentById(R.id.articleFragment);
article.displayArticle(id);
// findViewById(R.id.articleFragment)
}
}
public void onCategoryClick(String title) {
// either transition to the article view activity or update the article fragment
if (currentDisplayMode == DISPLAY_MODE_HANDSET) {
// launch the category activity
Intent intent = new Intent(this, CategoryActivity.class);
intent.putExtra(CategoryActivity.EXTRA_CATEGORY_TITLE, title);
startActivity(intent);
}
}
void errorOccured(int type, String msg, String error) {
// check if we need to fill in the error messages
if (msg == null) {
msg = "null";
}
if (error == null) {
error = "null";
}
// check if we need to shutdown after displaying the message
if (type == ERROR_TYPE_FATAL) {
errorWasFatal = true;
}
// show a user friendly message or just the error
if (settings.getBoolean("displayFullError", DEFAULT_DISPLAY_FULL_ERROR)) {
showErrorDialog("Error: " + error);
}
else {
// display a user friendly message
if (type == ERROR_TYPE_FATAL) {
showErrorDialog("Fatal error:\n" + msg + "\nPlease try resetting the app.");
Log.e("BBC News Reader", "Fatal error: " + msg);
Log.e("BBC News Reader", error);
}
else if (type == ERROR_TYPE_GENERAL) {
showErrorDialog("Error:\n" + msg);
Log.e("BBC News Reader", "Error: " + msg);
Log.e("BBC News Reader", error);
}
else if (type == ERROR_TYPE_INTERNET) {
// only allow one internet error per load
if (!errorDuringThisLoad) {
errorDuringThisLoad = true;
showErrorDialog("Please check your internet connection.");
}
Log.e("BBC News Reader", "Error: " + msg);
Log.e("BBC News Reader", error);
}
}
}
void showErrorDialog(String error) {
// only show the error dialog if one isn't already visible
if (errorDialog == null) {
AlertDialog.Builder builder = new AlertDialog.Builder(this);
builder.setMessage(error);
builder.setCancelable(false);
builder.setPositiveButton("Close", new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int id) {
closeErrorDialog();
}
});
errorDialog = builder.create();
errorDialog.show();
}
}
void closeErrorDialog() {
errorDialog = null; // destroy the dialog
// see if we need to end the program
if (errorWasFatal) {
// crash out
// Log.e("BBC News Reader", "Oops something broke. We'll crash now.");
System.exit(1); // closes the app with an error code
}
}
void showFirstRunDialog() {
AlertDialog.Builder builder = new AlertDialog.Builder(this);
String message = "Choose the categories you are interested in. \n\n"
+ "The fewer categories enabled the lower data usage and the faster loading will be.";
builder.setMessage(message);
builder.setCancelable(false);
builder.setPositiveButton("Choose", new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int id) {
closeFirstRunDialog();
// show the category chooser
showCategoryChooser();
}
});
firstRunDialog = builder.create();
firstRunDialog.show();
}
void closeFirstRunDialog() {
firstRunDialog = null; // destroy the dialog
}
void showBackgroundLoadDialog() {
AlertDialog.Builder builder = new AlertDialog.Builder(this);
String message = "Load news in the background? \n\n" + "This could increase data usage but will reduce load times.\n\n"
+ "If you wish to use the widget, this should be switched on.";
builder.setMessage(message);
builder.setCancelable(false);
builder.setPositiveButton("Yes", new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int id) {
closeBackgroundLoadDialog();
firstRun = false; // we have finished the first run
// save the selected option
Editor editor = settings.edit();
editor.putBoolean("loadInBackground", true);
editor.commit();
}
});
builder.setNegativeButton("No", new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int id) {
closeBackgroundLoadDialog();
firstRun = false; // we have finished the first run
// save the selected option
Editor editor = settings.edit();
editor.putBoolean("loadInBackground", false);
editor.commit();
}
});
backgroundLoadDialog = builder.create();
backgroundLoadDialog.show();
}
void closeBackgroundLoadDialog() {
backgroundLoadDialog = null;
}
void updateLoadProgress(int totalItems, int itemsLoaded) {
setStatusText("Preloading " + itemsLoaded + " of " + totalItems + " items");
}
void setLastLoadTime(long time) {
lastLoadTime = time; // store the time
// display the new time to the user
// check if the time is set
if (!loadInProgress) {
if (lastLoadTime == 0) {
// say we have never loaded
setStatusText("Never updated.");
}
else {
// set the text to show date and time
String status = "Updated ";
// find out time since last load in milliseconds
long difference = System.currentTimeMillis() - (time * 1000); // the time since the last load
// if within 1 hour, display minutes
if (difference < (1000 * 60 * 60)) {
int minutesAgo = (int) Math.floor((difference / 1000) / 60);
if (minutesAgo == 0) {
status += "just now";
}
else if (minutesAgo == 1) {
status += minutesAgo + " min ago";
}
else {
status += minutesAgo + " mins ago";
}
}
else {
// if we are within 24 hours, display hours
if (difference < (1000 * 60 * 60 * 24)) {
int hoursAgo = (int) Math.floor(((difference / 1000) / 60) / 60);
if (hoursAgo == 1) {
status += hoursAgo + " hour ago";
}
else {
status += hoursAgo + " hours ago";
}
}
else {
// if we are within 2 days, display yesterday
if (difference < (1000 * 60 * 60 * 48)) {
status += "yesterday";
}
else {
// we have not updated recently
status += "ages ago";
// TODO more formal message?
}
}
}
setStatusText(status);
}
}
}
void setStatusText(String text) {
// set the text to the action bar
getSupportActionBar().setTitle(text);
}
void loadBegun() {
loadInProgress = true; // flag the data as being loaded
// show the loading image on the button
// TODO update the load button
// tell the user what is going on
setStatusText("Loading feeds...");
}
void loadData() {
// check we aren't currently loading news
if (!loadInProgress) {
// TODO display old news as old
// tell the service to load the data
service.sendMessageToService(ResourceService.MSG_LOAD_DATA);
errorDuringThisLoad = false;
// hide the refresh button and show the stop button
menu.findItem(R.id.menuItemRefresh).setVisible(false);
menu.findItem(R.id.menuItemStop).setVisible(true);
}
}
void stopDataLoad() {
// check we are actually loading news
if (loadInProgress) {
errorDuringThisLoad = false;
// send a message to the service to stop it loading the data
service.sendMessageToService(ResourceService.MSG_STOP_DATA_LOAD);
// hide the stop button and show the refresh button
menu.findItem(R.id.menuItemRefresh).setVisible(true);
menu.findItem(R.id.menuItemStop).setVisible(false);
}
}
void fullLoadComplete() {
// check we are actually loading news
if (loadInProgress) {
loadInProgress = false;
// hide the stop button and show the refresh button
menu.findItem(R.id.menuItemRefresh).setVisible(true);
menu.findItem(R.id.menuItemStop).setVisible(false);
// report the loaded status
setLastLoadTime(settings.getLong("lastLoadTime", 0)); // set the time as unix time
// tell the database to delete old items
database.clearOld();
}
}
void rssLoadComplete() {
// check we are actually loading news
if (loadInProgress) {
// tell the user what is going on
setStatusText("Loading items...");
}
}
void showCategoryChooser() {
// launch the category chooser activity
Intent intent = new Intent(this, CategoryChooserActivity.class);
startActivityForResult(intent, ACTIVITY_CHOOSE_CATEGORIES);
}
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
loadInProgress = false;
lastLoadTime = 0;
// load the preferences system
settings = getSharedPreferences(PREFS_FILE_NAME, MODE_PRIVATE);
loadSettings();
// load the database, init it if required
database = new DatabaseHandler(this);
firstRun = false;
if (!database.isCreated()) {
// add the categories and set the version to prevent this happening twice
database.addCategoriesFromXml();
Editor editor = settings.edit();
editor.putInt(PREFKEY_CATEGORY_UPDATE_VERSION, CURRENT_CATEGORY_UPDATE_VERSION);
editor.commit();
// proceed with the first run
firstRun = true;
showFirstRunDialog();
}
// check if an update is required, if the stored category version is less than the current one
if(settings.getInt(PREFKEY_CATEGORY_UPDATE_VERSION, DEFAULT_CATEGORY_UPDATE_VERSION)
< CURRENT_CATEGORY_UPDATE_VERSION) {
// run an update
database.updateCategoriesFromXml();
// set the preference value to the current version
Editor editor = settings.edit();
editor.putInt(PREFKEY_CATEGORY_UPDATE_VERSION, CURRENT_CATEGORY_UPDATE_VERSION);
editor.commit();
}
// bind the service
service = new ServiceManager(this, this);
service.doBindService();
Eula.show(this); // show the eula
// determine which display mode is currently active
if (getResources().getBoolean(R.bool.screen_xlarge)) {
currentDisplayMode = DISPLAY_MODE_TABLET_LANDSCAPE;
}
else {
currentDisplayMode = DISPLAY_MODE_HANDSET;
}
// do specific configuration for various screen sizes
if (currentDisplayMode == DISPLAY_MODE_TABLET_LANDSCAPE) {
// force landscape
setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE);
}
// hide the up button
this.getSupportActionBar().setDisplayHomeAsUpEnabled(false);
// create the ui
this.setContentView(R.layout.reader_activity);
}
@Override
public void onResume() {
super.onResume();
// update the last loaded display
setLastLoadTime(lastLoadTime);
// TODO update display more often?
}
@Override
protected void onDestroy() {
// unbind from the service
service.doUnbindService();
super.onDestroy(); // pass the destroy command to the super
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
super.onCreateOptionsMenu(menu);
// inflate the menu
getSupportMenuInflater().inflate(R.menu.main_menu, menu);
this.menu = menu; // store a reference for later
return true; // we have made the menu so we can return true
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
if (item.getItemId() == R.id.menuItemCategories) {
// launch the category chooser activity
showCategoryChooser();
return true;
}
else if (item.getItemId() == R.id.menuItemSettings) {
// show the settings menu
Intent intent = new Intent(this, SettingsActivity.class);
startActivity(intent);
return true;
}
else if (item.getItemId() == R.id.menuItemRefresh) {
loadData();
return true;
}
else if (item.getItemId() == R.id.menuItemStop) {
stopDataLoad();
return true;
}
else {
return super.onOptionsItemSelected(item);
}
}
@Override
public void onActivityResult(int requestCode, int resultCode, Intent data) {
// wait for activities to send us result data
switch (requestCode) {
case ACTIVITY_CHOOSE_CATEGORIES:
// check the request was a success
if (resultCode == RESULT_OK) {
// refresh the display
loadData(); // make sure selected categories are loaded
FrontpageFragment frontpage = (FrontpageFragment) getSupportFragmentManager().findFragmentById(R.id.frontpageFragment);
frontpage.createNewsDisplay(getLayoutInflater(), frontpage.getView()); // reload the ui
// check for a first run
if (firstRun) {
showBackgroundLoadDialog();
}
}
break;
}
}
void loadSettings() {
// check the settings file exists
if (settings != null) {
// load values from the settings
setLastLoadTime(settings.getLong("lastLoadTime", 0)); // sets to zero if not in preferences
}
}
public void refreshClicked(View item) {
// start the load if we are not loading
if (!loadInProgress) {
loadData();
}
else {
stopDataLoad();
}
}
public int getCurrentDisplayMode() {
return currentDisplayMode;
}
}