/*
* @copyright 2012 Philip Warner
* @license GNU General Public License
*
* This file is part of Book Catalogue.
*
* Book Catalogue 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.
*
* Book Catalogue 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 Book Catalogue. If not, see <http://www.gnu.org/licenses/>.
*/
package com.eleybourn.bookcatalogue;
import android.app.Activity;
import android.app.Application;
import android.app.Notification;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.content.res.Configuration;
import android.content.res.Resources;
import android.database.sqlite.SQLiteDatabase;
import android.os.Build;
import com.eleybourn.bookcatalogue.booklist.BooklistPreferencesActivity;
import com.eleybourn.bookcatalogue.utils.Logger;
import com.eleybourn.bookcatalogue.utils.Terminator;
import com.eleybourn.bookcatalogue.utils.Utils;
import org.acra.ACRA;
import org.acra.ErrorReporter;
import org.acra.ReportingInteractionMode;
import org.acra.annotation.ReportsCrashes;
import org.acra.collector.CrashReportData;
import org.acra.sender.ReportSenderException;
import java.lang.ref.WeakReference;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.Locale;
import static org.acra.ReportField.ANDROID_VERSION;
import static org.acra.ReportField.APP_VERSION_CODE;
import static org.acra.ReportField.APP_VERSION_NAME;
import static org.acra.ReportField.BUILD;
import static org.acra.ReportField.CUSTOM_DATA;
import static org.acra.ReportField.PHONE_MODEL;
import static org.acra.ReportField.STACK_TRACE;
import static org.acra.ReportField.USER_APP_START_DATE;
import static org.acra.ReportField.USER_COMMENT;
import static org.acra.ReportField.USER_CRASH_DATE;
/**
* BookCatalogue Application implementation. Useful for making globals available
* and for being a central location for logically application-specific objects such
* as preferences.
*
* @author Philip Warner
*
*/
@ReportsCrashes(formKey = "", // will not be used
mailTo = "philip.warner@rhyme.com.au,eleybourn@gmail.com",
mode = ReportingInteractionMode.DIALOG,
customReportContent = { USER_COMMENT, USER_APP_START_DATE, USER_CRASH_DATE, APP_VERSION_NAME, APP_VERSION_CODE, ANDROID_VERSION, PHONE_MODEL, CUSTOM_DATA, STACK_TRACE },
//optional, displayed as soon as the crash occurs, before collecting data which can take a few seconds
resToastText = R.string.crash_toast_text,
resNotifTickerText = R.string.crash_notif_ticker_text,
resNotifTitle = R.string.crash_notif_title,
resNotifText = R.string.crash_notif_text,
resNotifIcon = android.R.drawable.stat_notify_error, // optional. default is a warning sign
resDialogText = R.string.crash_dialog_text,
resDialogIcon = android.R.drawable.ic_dialog_info, //optional. default is a warning sign
resDialogTitle = R.string.crash_dialog_title, // optional. default is your application name
resDialogCommentPrompt = R.string.crash_dialog_comment_prompt, // optional. when defined, adds a user text field input with this text resource as a label
resDialogOkToast = R.string.crash_dialog_ok_toast // optional. displays a Toast message when the user accepts to send a report.
)
public class BookCatalogueApp extends Application {
/** Not sure this is a good idea. Stores the Application context once created */
public static Context context = null;
/** Flag indicating the collation we use in the current database is case-sensitive */
private static Boolean mCollationCaseSensitive = null;
/** Used to sent notifications regarding tasks */
private static NotificationManager mNotifier;
private static BcQueueManager mQueueManager = null;
/** The locale used at startup; so that we can revert to system locale if we want to */
private static Locale mInitialLocale = null;
/** User-specified default locale */
private static Locale mPreferredLocale = null;
/**
* Constructor.
*/
public BookCatalogueApp() {
super();
mInitialLocale = Locale.getDefault();
}
/**
* There seems to be something fishy in creating locales from full names (like en_AU),
* so we split it and process it manually.
*
* @param name Locale name (eg. 'en_AU')
*
* @return Locale corresponding to passed name
*/
public static Locale localeFromName(String name) {
String[] parts;
if (name.contains("_")) {
parts = name.split("_");
} else {
parts = name.split("-");
}
Locale l;
if (parts.length == 1) {
l = new Locale(parts[0]);
} else if (parts.length == 2) {
l = new Locale(parts[0], parts[1]);
} else {
l = new Locale(parts[0], parts[1], parts[2]);
}
return l;
}
public class BcReportSender extends org.acra.sender.EmailIntentSender {
public BcReportSender(Context ctx) {
super(ctx);
}
@Override
public void send(CrashReportData report) throws ReportSenderException {
//report.put(USER_COMMENT, report.get(USER_COMMENT) + "\n\n" + Tracker.getEventsInfo());
super.send(report);
}
}
/**
* Most real initialization should go here, since before this point, the App is still
* 'Under Construction'.
*/
@Override
public void onCreate() {
// Don't rely on the the context until now...
BookCatalogueApp.context = this.getApplicationContext();
// Get the preferred locale as soon as possible
try {
// Save the original locale
mInitialLocale = Locale.getDefault();
// See if user has set a preference
String prefLocale = getAppPreferences().getString(BookCataloguePreferences.PREF_APP_LOCALE, null);
//prefLocale = "ru";
// If we have a preference, set it
if (prefLocale != null && !prefLocale.equals("")) {
mPreferredLocale = localeFromName(prefLocale);
applyPreferredLocaleIfNecessary(getBaseContext().getResources());
}
} catch (Exception e) {
// Not much we can do...we want locale set early, but not fatal if it fails.
Logger.logError(e);
}
Terminator.init();
// The following line triggers the initialization of ACRA
ACRA.init(this);
BcReportSender bcSender = new BcReportSender(this);
ErrorReporter.getInstance().setReportSender(bcSender);
// Save the app signer
ErrorReporter.getInstance().putCustomData("Signed-By", Utils.signedBy(this));
// Create the notifier
mNotifier = (NotificationManager)getSystemService(NOTIFICATION_SERVICE);
// Start the queue manager
if (mQueueManager == null)
mQueueManager = new BcQueueManager(this.getApplicationContext());
super.onCreate();
if (Build.VERSION.SDK_INT < 16) {
//
// Avoid possible bug in SQLite which resuts in database being closed without an explicit call.
// Based on the grepcode Android sources, it looks like this bug was fixed an/or addressed in
// 4.1.1, but not in 4.0.4.
//
// See:
//
// https://code.google.com/p/android/issues/detail?id=4282
// http://darutk-oboegaki.blogspot.com.au/2011/03/sqlitedatabase-is-closed-automatically.html
//
// a pdf of the second link is in 'support' folder.
//
CatalogueDBAdapter dbh = new CatalogueDBAdapter(this);
dbh.open();
SQLiteDatabase db = dbh.getDb().getUnderlyingDatabase();
db.acquireReference();
if (Build.VERSION.SDK_INT < 8) {
//
// RELEASE: REMOVE THIS CODE When MinSDK becomes 8!
//
// Android 2.1 has a very nasty bug that can cause un-closed SQLiteStatements to dereference the
// database when they have not referenced it.. SQLiteStatements can fail to be released in a timely
// fashion when the screen is rotated, which will then result in an attempt to acess a closed closable.
// ... so for Android 2.1...we take 1000 references and hope the user won't rotate the screen 1000
// times while background tasks are running.
//
// We have made the best efforts to avoid this bug, this is just insurance.
//
// The key instance where this happens is if the GetListTask in BooksOnBookshelf is aborted due to
// a screen rotation; the onFinish() method is never called, so the statements are not deleted.
//
// We have added finalize() code to SynchronizedStatement so that IF it is called first (not
// guaranteed by Java spec) it will close the SQLiteStatement and try to avoid this issue.
//
for(int i = 0; i < 1000; i++)
db.acquireReference();
}
dbh.close();
}
// Watch the preferences and handle changes as necessary
//BookCataloguePreferences ap = getPreferences();
SharedPreferences p = BookCataloguePreferences.getSharedPreferences();
p.registerOnSharedPreferenceChangeListener(mPrefsListener);
}
/**
* Shared Preferences Listener
*
* Currently it just handles Locale changes and propagates it to any listeners.
*/
private SharedPreferences.OnSharedPreferenceChangeListener mPrefsListener = new SharedPreferences.OnSharedPreferenceChangeListener() {
@Override
public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) {
if (key.equals(BookCataloguePreferences.PREF_APP_LOCALE)) {
String prefLocale = getAppPreferences().getString(BookCataloguePreferences.PREF_APP_LOCALE, null);
//prefLocale = "ru";
// If we have a preference, set it
if (prefLocale != null && !prefLocale.equals("")) {
mPreferredLocale = localeFromName(prefLocale);
} else {
mPreferredLocale = getSystemLocal();
}
applyPreferredLocaleIfNecessary(getBaseContext().getResources());
notifyLocaleChanged();
}
}
};
/**
* Send a message to all registered OnLocaleChangedListeners, and cleanup any dead references.
*/
private void notifyLocaleChanged() {
ArrayList<WeakReference<OnLocaleChangedListener>> toRemove = new ArrayList<WeakReference<OnLocaleChangedListener>>();
for (WeakReference<OnLocaleChangedListener> ref : mOnLocaleChangedListeners) {
OnLocaleChangedListener l = ref.get();
if (l == null)
toRemove.add(ref);
else
try { l.onLocaleChanged(); } catch (Exception e) { /* Ignore */ }
}
for(WeakReference<OnLocaleChangedListener> ref: toRemove) {
mOnLocaleChangedListeners.remove(ref);
}
}
/**
* Add a new OnLocaleChangedListener, and cleanup any dead references.
*/
public static void registerOnLocaleChangedListener(OnLocaleChangedListener listener) {
ArrayList<WeakReference<OnLocaleChangedListener>> toRemove = new ArrayList<WeakReference<OnLocaleChangedListener>>();
boolean alreadyAdded = false;
for(WeakReference<OnLocaleChangedListener> ref: mOnLocaleChangedListeners) {
OnLocaleChangedListener l = ref.get();
if (l == null)
toRemove.add(ref);
else if (l == listener)
alreadyAdded = true;
}
if (!alreadyAdded)
mOnLocaleChangedListeners.add(new WeakReference<OnLocaleChangedListener>(listener));
for(WeakReference<OnLocaleChangedListener> ref: toRemove) {
mOnLocaleChangedListeners.remove(ref);
}
}
/**
* Remove the passed OnLocaleChangedListener, and cleanup any dead references.
*/
public static void unregisterOnLocaleChangedListener(OnLocaleChangedListener listener) {
ArrayList<WeakReference<OnLocaleChangedListener>> toRemove = new ArrayList<WeakReference<OnLocaleChangedListener>>();
for(WeakReference<OnLocaleChangedListener> ref: mOnLocaleChangedListeners) {
OnLocaleChangedListener l = ref.get();
if ( (l == null) || (l == listener) )
toRemove.add(ref);
}
for(WeakReference<OnLocaleChangedListener> ref: toRemove) {
mOnLocaleChangedListeners.remove(ref);
}
}
/** Set of OnLocaleChangedListeners */
private static HashSet<WeakReference<OnLocaleChangedListener>> mOnLocaleChangedListeners = new HashSet<WeakReference<OnLocaleChangedListener>>();
/**
* Interface definition
*/
public static interface OnLocaleChangedListener {
public void onLocaleChanged();
}
/**
* Check if sqlite collation is case sensitive; cache the result.
* This bug was introduced in ICS and present in 4.0-4.0.3, at least that meant that
* UNICODE collation became CS. We now use a LOCALIZED Collation, but still check if CI.
*
* @param db Any sqlite database connection
*
* @return Flag indicating 'Collate <our-collation>' is broken.
*/
public static boolean isCollationCaseSensitive(SQLiteDatabase db) {
if (mCollationCaseSensitive == null)
mCollationCaseSensitive = CollationCaseSensitive.isCaseSensitive(db);
return mCollationCaseSensitive;
}
// /**
// * Currently the QueueManager is implemented as a service. This is not clearly necessary
// * but has the huge advantage of making a 'context' object available in the Service
// * implementation.
// *
// * By binding it here, the service will not die when the last Activity is closed. We
// * could call StartService to keep it awake indefinitely also, but we do want the binding
// * object...so we bind it.
// */
// private void startQueueManager() {
// doBindService();
// }
//
// /**
// * Points to the bound service, once it is started.
// */
// private static BcQueueManager mBoundService = null;
//
// /**
// * Utility routine to get the current QueueManager.
// *
// * @return QueueManager object
// */
// public static BcQueueManager getQueueManager() {
// return mBoundService;
// }
/**
* Utility routine to get the current QueueManager.
*
* @return QueueManager object
*/
public static BcQueueManager getQueueManager() {
return mQueueManager;
}
/**
* Wrapper to reduce explicit use of the 'context' member.
*
* @param resId Resource ID
*
* @return Localized resource string
*/
public final static String getResourceString(int resId) {
return context.getString(resId);
}
/**
* Wrapper to reduce explicit use of the 'context' member.
*
* @param resId Resource ID
*
* @return Localized resource string
*/
public final static String getResourceString(int resId, Object...objects) {
return context.getString(resId, objects);
}
/**
* Utility routine to return as BookCataloguePreferences object.
*
* @return Application preferences object.
*/
public static BookCataloguePreferences getAppPreferences() {
return new BookCataloguePreferences();
}
public static boolean isBackgroundImageDisabled() {
return getAppPreferences().getBoolean(BookCataloguePreferences.PREF_DISABLE_BACKGROUND_IMAGE, false);
}
// /**
// * Code based on Google sample code to bind the service.
// */
// private ServiceConnection mConnection = new ServiceConnection() {
// public void onServiceConnected(ComponentName className, IBinder service) {
// // This is called when the connection with the service has been
// // established, giving us the service object we can use to
// // interact with the service. Because we have bound to a explicit
// // service that we know is running in our own process, we can
// // cast its IBinder to a concrete class and directly access it.
// mBoundService = (BcQueueManager)((QueueManager.QueueManagerBinder)service).getService();
//
// // Tell the user about this for our demo.
// //Toast.makeText(BookCatalogueApp.this, "Connected", Toast.LENGTH_SHORT).show();
// }
//
// public void onServiceDisconnected(ComponentName className) {
// // This is called when the connection with the service has been
// // unexpectedly disconnected -- that is, its process crashed.
// // Because it is running in our same process, we should never
// // see this happen.
// mBoundService = null;
// //Toast.makeText(BookCatalogueApp.this, "Disconnected", Toast.LENGTH_SHORT).show();
// }
// };
//
// /** Indicates service has been bound. Really. */
// boolean mIsBound;
//
// /**
// * Establish a connection with the service. We use an explicit
// * class name because we want a specific service implementation that
// * we know will be running in our own process (and thus won't be
// * supporting component replacement by other applications).
// */
// void doBindService() {
// bindService(new Intent(BookCatalogueApp.this, BcQueueManager.class), mConnection, Context.BIND_AUTO_CREATE);
// mIsBound = true;
// }
// /**
// * Detach existiing service connection.
// */
// void doUnbindService() {
// if (mIsBound) {
// unbindService(mConnection);
// mIsBound = false;
// }
// }
/**
* Return the Intent that will be used by the notifications manager when a notification
* is clicked; should bring the app to the foreground.
*/
public static Intent getAppToForegroundIntent(Context c) {
Intent i = new Intent (c, StartupActivity.class );
i.setAction("android.intent.action.MAIN");
i.addCategory(Intent.CATEGORY_LAUNCHER);
// No idea what to do with this!
i.putExtra("bringFg", true);
return i;
}
// /**
// * Used by the Manifest-based startup activity to determine the desired first activity for the user.
// *
// * @return Intent for preference-based startup activity.
// */
// public Intent getStartupIntent() {
// BookCataloguePreferences prefs = getAppPreferences();
//
// Intent i;
// if (prefs.getStartInMyBook()) {
// i = new Intent(this, BookCatalogue.class);
// } else {
// i = new Intent(this, MainMenu.class);
// }
// return i;
// }
public static void startPreferencesActivity(Activity a) {
Intent i = new Intent(a, BooklistPreferencesActivity.class);
a.startActivity(i);
}
/**
* Show a notification while this app is running.
*
* @param title
* @param message
*/
public static void showNotification(int id, String title, String message, Intent i) {
// In this sample, we'll use the same text for the ticker and the expanded notification
CharSequence text = message; //getText(R.string.local_service_started);
// Set the icon, scrolling text and timestamp
Notification notification = new Notification(R.drawable.ic_stat_logo, text, System.currentTimeMillis());
// Auto-cancel the notification
notification.flags |= Notification.FLAG_AUTO_CANCEL;
// The PendingIntent to launch our activity if the user selects this notification
PendingIntent contentIntent = PendingIntent.getActivity(context, 0, i, 0);
// Set the info for the views that show in the notification panel.
notification.setLatestEventInfo(context, title, //getText(R.string.local_service_label),
text, contentIntent);
// Send the notification.
mNotifier.notify(id, notification);
}
/**
* Get the current preferred locale, or null
*
* @return locale, or null
*/
public static Locale getPreferredLocale() {
return mPreferredLocale;
}
/**
* Set the current preferred locale in the passed resources.
*
* @param res Resources to use
* @return true if it was actually changed
*/
public static boolean applyPreferredLocaleIfNecessary(Resources res) {
if (mPreferredLocale == null)
return false;
if (res.getConfiguration().locale.equals(mPreferredLocale))
return false;
Locale.setDefault(mPreferredLocale);
Configuration config = new Configuration();
config.locale = mPreferredLocale;
res.updateConfiguration(config, res.getDisplayMetrics());
return true;
}
/**
* Monitor configuration changes (like rotation) to make sure we reset the
* locale.
*
* @param newConfig
*/
@Override
public void onConfigurationChanged(Configuration newConfig)
{
super.onConfigurationChanged(newConfig);
if (mPreferredLocale != null)
{
applyPreferredLocaleIfNecessary(getBaseContext().getResources());
}
}
/** List of supported locales */
private static ArrayList<String> mSupportedLocales = null;
/**
* Get the list of supported locale names
*
* @return ArrayList of locale names
*/
public static ArrayList<String> getSupportedLocales() {
if (mSupportedLocales == null) {
mSupportedLocales = new ArrayList<String>();
mSupportedLocales.add("de_DE");
mSupportedLocales.add("en_AU");
mSupportedLocales.add("es_ES");
mSupportedLocales.add("fr_FR");
mSupportedLocales.add("it_IT");
mSupportedLocales.add("nl_NL");
mSupportedLocales.add("ru_RU");
mSupportedLocales.add("tr_TR");
mSupportedLocales.add("el_GR");
}
return mSupportedLocales;
}
public static Locale getSystemLocal() {
return mInitialLocale;
}
}