/****************************************************************************************
* Copyright (c) 2009 Edu Zamora <edu.zasu@gmail.com> *
* Copyright (c) 2009 Casey Link <unnamedrambler@gmail.com> *
* Copyright (c) 2014 Timothy Rae <perceptualchaos2@gmail.com> *
* *
* 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.ichi2.anki;
import android.app.Application;
import android.content.Context;
import android.content.SharedPreferences;
import android.content.res.Configuration;
import android.content.res.Resources;
import android.os.Environment;
import android.preference.PreferenceManager;
import android.util.Log;
import android.view.ViewConfiguration;
import com.ichi2.anki.dialogs.AnkiDroidCrashReportDialog;
import com.ichi2.anki.exception.StorageAccessException;
import com.ichi2.compat.CompatHelper;
import com.ichi2.utils.LanguageUtil;
import org.acra.ACRA;
import org.acra.ACRAConfigurationException;
import org.acra.ReportField;
import org.acra.ReportingInteractionMode;
import org.acra.annotation.ReportsCrashes;
import org.acra.sender.HttpSender;
import org.json.JSONException;
import org.json.JSONObject;
import java.util.Locale;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import timber.log.Timber;
/**
* Application class.
*/
@ReportsCrashes(
reportDialogClass = AnkiDroidCrashReportDialog.class,
httpMethod = HttpSender.Method.PUT,
reportType = HttpSender.Type.JSON,
formUri = "https://ankidroid.org/acra/report",
mode = ReportingInteractionMode.DIALOG,
resDialogCommentPrompt = R.string.empty_string,
resDialogTitle = R.string.feedback_title,
resDialogText = R.string.feedback_default_text,
resToastText = R.string.feedback_auto_toast_text,
resDialogPositiveButtonText = R.string.feedback_report,
additionalSharedPreferences = {"com.ichi2.anki"},
excludeMatchingSharedPreferencesKeys = {"username","hkey"},
customReportContent = {
ReportField.REPORT_ID,
ReportField.APP_VERSION_CODE,
ReportField.APP_VERSION_NAME,
ReportField.PACKAGE_NAME,
ReportField.FILE_PATH,
ReportField.PHONE_MODEL,
ReportField.ANDROID_VERSION,
ReportField.BUILD,
ReportField.BRAND,
ReportField.PRODUCT,
ReportField.TOTAL_MEM_SIZE,
ReportField.AVAILABLE_MEM_SIZE,
ReportField.BUILD_CONFIG,
ReportField.CUSTOM_DATA,
ReportField.STACK_TRACE,
ReportField.STACK_TRACE_HASH,
//ReportField.INITIAL_CONFIGURATION,
ReportField.CRASH_CONFIGURATION,
//ReportField.DISPLAY,
ReportField.USER_COMMENT,
ReportField.USER_APP_START_DATE,
ReportField.USER_CRASH_DATE,
//ReportField.DUMPSYS_MEMINFO,
//ReportField.DROPBOX,
ReportField.LOGCAT,
//ReportField.EVENTSLOG,
//ReportField.RADIOLOG,
//ReportField.IS_SILENT,
ReportField.INSTALLATION_ID,
//ReportField.USER_EMAIL,
//ReportField.DEVICE_FEATURES,
ReportField.ENVIRONMENT,
//ReportField.SETTINGS_SYSTEM,
//ReportField.SETTINGS_SECURE,
//ReportField.SETTINGS_GLOBAL,
ReportField.SHARED_PREFERENCES,
ReportField.APPLICATION_LOG,
ReportField.MEDIA_CODEC_LIST,
ReportField.THREAD_DETAILS
//ReportField.USER_IP
},
logcatArguments = { "-t", "100", "-v", "time", "ActivityManager:I", "SQLiteLog:W", AnkiDroidApp.TAG + ":D", "*:S" }
)
public class AnkiDroidApp extends Application {
public static final String XML_CUSTOM_NAMESPACE = "http://arbitrary.app.namespace/com.ichi2.anki";
public static final String FEEDBACK_REPORT_ASK = "2";
public static final String FEEDBACK_REPORT_NEVER = "1";
public static final String FEEDBACK_REPORT_ALWAYS = "0";
// Tag for logging messages.
public static final String TAG = "AnkiDroid";
// Singleton instance of this class.
private static AnkiDroidApp sInstance;
// Constants for gestures
public static int sSwipeMinDistance = -1;
public static int sSwipeThresholdVelocity = -1;
private static int DEFAULT_SWIPE_MIN_DISTANCE;
private static int DEFAULT_SWIPE_THRESHOLD_VELOCITY;
/**
* The latest package version number that included important changes to the database integrity check routine. All
* collections being upgraded to (or after) this version must run an integrity check as it will contain fixes that
* all collections should have.
*/
public static final int CHECK_DB_AT_VERSION = 40;
/**
* The latest package version number that included changes to the preferences that requires handling. All
* collections being upgraded to (or after) this version must update preferences.
*/
public static final int CHECK_PREFERENCES_AT_VERSION = 20500225;
/**
* On application creation.
*/
@Override
public void onCreate() {
super.onCreate();
// Get preferences
SharedPreferences preferences = getSharedPrefs(this);
// Initialize crash reporting module
ACRA.init(this);
// Setup logging and crash reporting
if (BuildConfig.DEBUG) {
// Enable verbose error logging and do method tracing to put the Class name as log tag
Timber.plant(new Timber.DebugTree());
// Disable crash reporting
setAcraReportingMode(FEEDBACK_REPORT_NEVER);
preferences.edit().putString("reportErrorMode", FEEDBACK_REPORT_NEVER).commit();
// Use a wider logcat filter incase crash reporting manually re-enabled
String [] logcatArgs = { "-t", "300", "-v", "long", "ACRA:S"};
ACRA.getConfig().setLogcatArguments(logcatArgs);
} else {
// Disable verbose error logging and use fixed log tag "AnkiDroid"
Timber.plant(new ProductionCrashReportingTree());
// Enable or disable crash reporting based on user setting
setAcraReportingMode(preferences.getString("reportErrorMode", FEEDBACK_REPORT_ASK));
}
Timber.tag(TAG);
sInstance = this;
setLanguage(preferences.getString(Preferences.LANGUAGE, ""));
// Configure WebView to allow file scheme pages to access cookies.
CompatHelper.getCompat().enableCookiesForFileSchemePages();
// Prepare Cookies to be synchronized between RAM and permanent storage.
CompatHelper.getCompat().prepareWebViewCookies(this.getApplicationContext());
// Set good default values for swipe detection
final ViewConfiguration vc = ViewConfiguration.get(this);
DEFAULT_SWIPE_MIN_DISTANCE = vc.getScaledPagingTouchSlop();
DEFAULT_SWIPE_THRESHOLD_VELOCITY = vc.getScaledMinimumFlingVelocity();
// Create the AnkiDroid directory if missing. Send exception report if inaccessible.
if (CollectionHelper.hasStorageAccessPermission(this)) {
try {
String dir = CollectionHelper.getCurrentAnkiDroidDirectory(this);
CollectionHelper.initializeAnkiDroidDirectory(dir);
} catch (StorageAccessException e) {
Timber.e(e, "Could not initialize AnkiDroid directory");
String defaultDir = CollectionHelper.getDefaultAnkiDroidDirectory();
if (isSdCardMounted() && CollectionHelper.getCurrentAnkiDroidDirectory(this).equals(defaultDir)) {
// Don't send report if the user is using a custom directory as SD cards trip up here a lot
sendExceptionReport(e, "AnkiDroidApp.onCreate");
}
}
}
}
@Override
public void onConfigurationChanged(Configuration newConfig) {
super.onConfigurationChanged(newConfig);
// Preserve the language from the settings, e.g. when the device is rotated
setLanguage(getSharedPrefs(this).getString(Preferences.LANGUAGE, ""));
}
/**
* Convenience method for accessing Shared preferences
*
* @param context Context to get preferences for.
* @return A SharedPreferences object for this instance of the app.
*/
public static SharedPreferences getSharedPrefs(Context context) {
return PreferenceManager.getDefaultSharedPreferences(context);
}
public static AnkiDroidApp getInstance() {
return sInstance;
}
public static String getCacheStorageDirectory() {
return sInstance.getCacheDir().getAbsolutePath();
}
public static Resources getAppResources() {
return sInstance.getResources();
}
public static boolean isSdCardMounted() {
return Environment.MEDIA_MOUNTED.equals(Environment.getExternalStorageState());
}
public static void sendExceptionReport(Throwable e, String origin) {
sendExceptionReport(e, origin, null);
}
public static void sendExceptionReport(Throwable e, String origin, String additionalInfo) {
//CustomExceptionHandler.getInstance().uncaughtException(null, e, origin, additionalInfo);
SharedPreferences prefs = getSharedPrefs(getInstance());
// Only send report if we have not sent an identical report before
try {
JSONObject sentReports = new JSONObject(prefs.getString("sentExceptionReports", "{}"));
String hash = getExceptionHash(e);
if (sentReports.has(hash)) {
Timber.i("The exception report with hash %s has already been sent from this device", hash);
return;
} else {
sentReports.put(hash, true);
prefs.edit().putString("sentExceptionReports", sentReports.toString()).apply();
}
} catch (JSONException e1) {
Timber.i(e1, "Could not get cache of sent exception reports");
}
ACRA.getErrorReporter().putCustomData("origin", origin);
ACRA.getErrorReporter().putCustomData("additionalInfo", additionalInfo);
ACRA.getErrorReporter().handleException(e);
}
private static String getExceptionHash(Throwable th) {
final StringBuilder res = new StringBuilder();
Throwable cause = th;
while (cause != null) {
final StackTraceElement[] stackTraceElements = cause.getStackTrace();
for (final StackTraceElement e : stackTraceElements) {
res.append(e.getClassName());
res.append(e.getMethodName());
}
cause = cause.getCause();
}
return Integer.toHexString(res.toString().hashCode());
}
/**
* Sets the user language.
*
* @param localeCode The locale code of the language to set
*/
public static void setLanguage(String localeCode) {
Configuration config = getInstance().getResources().getConfiguration();
Locale newLocale = LanguageUtil.getLocale(localeCode);
config.locale = newLocale;
getInstance().getResources().updateConfiguration(config, getInstance().getResources().getDisplayMetrics());
}
public static boolean initiateGestures(SharedPreferences preferences) {
Boolean enabled = preferences.getBoolean("gestures", false);
if (enabled) {
int sensitivity = preferences.getInt("swipeSensitivity", 100);
if (sensitivity != 100) {
float sens = 100.0f/sensitivity;
sSwipeMinDistance = (int) (DEFAULT_SWIPE_MIN_DISTANCE * sens + 0.5f);
sSwipeThresholdVelocity = (int) (DEFAULT_SWIPE_THRESHOLD_VELOCITY * sens + 0.5f);
} else {
sSwipeMinDistance = DEFAULT_SWIPE_MIN_DISTANCE;
sSwipeThresholdVelocity = DEFAULT_SWIPE_THRESHOLD_VELOCITY;
}
}
return enabled;
}
/**
* Set the reporting mode for ACRA based on the value of the reportErrorMode preference
* @param value value of reportErrorMode preference
*/
public void setAcraReportingMode(String value) {
SharedPreferences.Editor editor = ACRA.getACRASharedPreferences().edit();
// Set the ACRA disable value
if (value.equals(FEEDBACK_REPORT_NEVER)) {
editor.putBoolean("acra.disable", true);
} else {
editor.putBoolean("acra.disable", false);
// Switch between auto-report via toast and manual report via dialog
try {
if (value.equals(FEEDBACK_REPORT_ALWAYS)) {
ACRA.getConfig().setMode(ReportingInteractionMode.TOAST);
ACRA.getConfig().setResToastText(R.string.feedback_auto_toast_text);
} else if (value.equals(FEEDBACK_REPORT_ASK)) {
ACRA.getConfig().setMode(ReportingInteractionMode.DIALOG);
ACRA.getConfig().setResToastText(R.string.feedback_manual_toast_text);
}
} catch (ACRAConfigurationException e) {
Timber.e("Could not set ACRA report mode");
}
}
editor.commit();
}
/**
* Get the url for the feedback page
* @return
*/
public static String getFeedbackUrl() {
if (isCurrentLanguage("ja")) {
return sInstance.getResources().getString(R.string.link_help_ja);
} else {
return sInstance.getResources().getString(R.string.link_help);
}
}
/**
* Get the url for the manual
* @return
*/
public static String getManualUrl() {
if (isCurrentLanguage("ja")) {
return sInstance.getResources().getString(R.string.link_manual_ja);
} else {
return sInstance.getResources().getString(R.string.link_manual);
}
}
/**
* Check whether l is the currently set language code
* @param l ISO2 language code
* @return
*/
private static boolean isCurrentLanguage(String l) {
String pref = getSharedPrefs(sInstance).getString(Preferences.LANGUAGE, "");
return pref.equals(l) || pref.equals("") && Locale.getDefault().getLanguage().equals(l);
}
/** A tree which logs necessary data for crash reporting. */
public static class ProductionCrashReportingTree extends Timber.HollowTree {
private static final ThreadLocal<String> NEXT_TAG = new ThreadLocal<>();
private static final Pattern ANONYMOUS_CLASS = Pattern.compile("\\$\\d+$");
@Override public void e(String message, Object... args) {
Log.e(TAG, createTag() + "/ " + formatString(message, args)); // Just add to the log.
}
@Override public void e(Throwable t, String message, Object... args) {
Log.e(TAG, createTag() + "/ " + formatString(message, args), t); // Just add to the log.
}
@Override public void w(String message, Object... args) {
Log.w(TAG, createTag() + "/ " + formatString(message, args)); // Just add to the log.
}
@Override public void w(Throwable t, String message, Object... args) {
Log.w(TAG, createTag() + "/ " + formatString(message, args), t); // Just add to the log.
}
@Override public void i(String message, Object... args) {
// Skip createTag() to improve performance. message should be descriptive enough without it
Log.i(TAG, formatString(message, args)); // Just add to the log.
}
@Override public void i(Throwable t, String message, Object... args) {
// Skip createTag() to improve performance. message should be descriptive enough without it
Log.i(TAG, formatString(message, args), t); // Just add to the log.
}
// Ignore logs below INFO level --> Non-overridden methods go to HollowTree
static String formatString(String message, Object... args) {
// If no varargs are supplied, treat it as a request to log the string without formatting.
try {
return args.length == 0 ? message : String.format(message, args);
} catch (Exception e) {
return message;
}
}
private static String createTag() {
String tag = NEXT_TAG.get();
if (tag != null) {
NEXT_TAG.remove();
return tag;
}
StackTraceElement[] stackTrace = new Throwable().getStackTrace();
if (stackTrace.length < 6) {
throw new IllegalStateException(
"Synthetic stacktrace didn't have enough elements: are you using proguard?");
}
tag = stackTrace[5].getClassName();
Matcher m = ANONYMOUS_CLASS.matcher(tag);
if (m.find()) {
tag = m.replaceAll("");
}
return tag.substring(tag.lastIndexOf('.') + 1);
}
}
}