/** * Copyright (c) 2012 Todoroo Inc * * See the file "LICENSE" for the full license governing this code. */ package com.todoroo.astrid.service; import java.io.File; import java.util.List; import android.Manifest; import android.app.Activity; import android.app.AlarmManager; import android.app.AlertDialog; import android.app.PendingIntent; import android.content.ContentValues; import android.content.Context; import android.content.DialogInterface; import android.content.DialogInterface.OnClickListener; import android.content.Intent; import android.content.pm.PackageInfo; import android.content.pm.PackageManager; import android.database.sqlite.SQLiteException; import android.media.AudioManager; import android.util.Log; import android.widget.Toast; import com.crittercism.app.Crittercism; import com.timsu.astrid.R; import com.todoroo.andlib.data.DatabaseDao.ModelUpdateListener; import com.todoroo.andlib.data.TodorooCursor; import com.todoroo.andlib.service.Autowired; import com.todoroo.andlib.service.ContextManager; import com.todoroo.andlib.service.DependencyInjectionService; import com.todoroo.andlib.service.ExceptionService; import com.todoroo.andlib.sql.Criterion; import com.todoroo.andlib.sql.Query; import com.todoroo.andlib.utility.AndroidUtilities; import com.todoroo.andlib.utility.DateUtilities; import com.todoroo.andlib.utility.DialogUtilities; import com.todoroo.andlib.utility.Preferences; import com.todoroo.astrid.actfm.sync.ActFmPreferenceService; import com.todoroo.astrid.actfm.sync.ActFmSyncThread; import com.todoroo.astrid.actfm.sync.AstridNewSyncMigrator; import com.todoroo.astrid.activity.BeastModePreferences; import com.todoroo.astrid.backup.BackupConstants; import com.todoroo.astrid.backup.BackupService; import com.todoroo.astrid.backup.TasksXmlImporter; import com.todoroo.astrid.core.PluginServices; import com.todoroo.astrid.dao.Database; import com.todoroo.astrid.dao.MetadataDao.MetadataCriteria; import com.todoroo.astrid.dao.TagDataDao; import com.todoroo.astrid.dao.TaskAttachmentDao; import com.todoroo.astrid.dao.TaskDao; import com.todoroo.astrid.dao.TaskListMetadataDao; import com.todoroo.astrid.dao.UserActivityDao; import com.todoroo.astrid.dao.WaitingOnMeDao; import com.todoroo.astrid.data.Metadata; import com.todoroo.astrid.data.TagData; import com.todoroo.astrid.data.Task; import com.todoroo.astrid.gcal.CalendarStartupReceiver; import com.todoroo.astrid.gtasks.GtasksMetadata; import com.todoroo.astrid.gtasks.GtasksPreferenceService; import com.todoroo.astrid.gtasks.sync.GtasksSyncService; import com.todoroo.astrid.opencrx.OpencrxCoreUtils; import com.todoroo.astrid.reminders.ReengagementService; import com.todoroo.astrid.reminders.ReminderStartupReceiver; import com.todoroo.astrid.service.abtesting.ABChooser; import com.todoroo.astrid.service.abtesting.ABTestInvoker; import com.todoroo.astrid.service.abtesting.ABTests; import com.todoroo.astrid.subtasks.SubtasksMetadata; import com.todoroo.astrid.tags.TaskToTagMetadata; import com.todoroo.astrid.ui.TaskListFragmentPager; import com.todoroo.astrid.utility.AstridPreferences; import com.todoroo.astrid.utility.Constants; import com.todoroo.astrid.widget.TasksWidget.WidgetUpdateService; /** * Service which handles jobs that need to be run when Astrid starts up. * * @author Tim Su <tim@todoroo.com> * */ public class StartupService { static { AstridDependencyInjector.initialize(); } public StartupService() { DependencyInjectionService.getInstance().inject(this); } // --- application startup @Autowired ExceptionService exceptionService; @Autowired UpgradeService upgradeService; @Autowired TaskService taskService; @Autowired TaskDao taskDao; @Autowired TagDataDao tagDataDao; @Autowired UserActivityDao userActivityDao; @Autowired TaskAttachmentDao taskAttachmentDao; @Autowired TaskListMetadataDao taskListMetadataDao; @Autowired WaitingOnMeDao waitingOnMeDao; @Autowired MetadataService metadataService; @Autowired Database database; @Autowired GtasksPreferenceService gtasksPreferenceService; @Autowired ActFmPreferenceService actFmPreferenceService; @Autowired GtasksSyncService gtasksSyncService; @Autowired ABChooser abChooser; @Autowired ABTests abTests; @Autowired ABTestInvoker abTestInvoker; /** * bit to prevent multiple initializations */ private static boolean hasStartedUp = false; /** * Call to skip initialization steps (i.e. if only a notification screen is needed) */ public synchronized static void bypassInitialization() { hasStartedUp = true; } /** Called when this application is started up */ public synchronized void onStartupApplication(final Activity context) { if(hasStartedUp || context == null) return; // sets up context manager ContextManager.setContext(context); if(!StatisticsService.dontCollectStatistics()) { Crittercism.init(context.getApplicationContext(), Constants.CRITTERCISM_APP_ID); } try { database.openForWriting(); checkForMissingColumns(); } catch (SQLiteException e) { handleSQLiteError(context, e); return; } // show notification if reminders are silenced if(context instanceof Activity) { AudioManager audioManager = (AudioManager)context.getSystemService( Context.AUDIO_SERVICE); if(!Preferences.getBoolean(R.string.p_rmd_enabled, true)) Toast.makeText(context, R.string.TLA_notification_disabled, Toast.LENGTH_LONG).show(); else if(audioManager.getStreamVolume(AudioManager.STREAM_NOTIFICATION) == 0) Toast.makeText(context, R.string.TLA_notification_volume_low, Toast.LENGTH_LONG).show(); } // read current version int latestSetVersion = 0; try { latestSetVersion = AstridPreferences.getCurrentVersion(); } catch (Exception e) { exceptionService.reportError("astrid-startup-version-read", e); //$NON-NLS-1$ } if (latestSetVersion == 0) { if (Preferences.getLong(AstridPreferences.P_FIRST_LAUNCH, -1) < 0) { Preferences.setLong(AstridPreferences.P_FIRST_LAUNCH, DateUtilities.now()); } } BeastModePreferences.assertHideUntilSectionExists(context, latestSetVersion); int version = 0; String versionName = "0"; //$NON-NLS-1$ try { PackageManager pm = context.getPackageManager(); PackageInfo pi = pm.getPackageInfo(Constants.PACKAGE, PackageManager.GET_META_DATA); version = pi.versionCode; versionName = pi.versionName; } catch (Exception e) { exceptionService.reportError("astrid-startup-package-read", e); //$NON-NLS-1$ } Log.i("astrid", "Astrid Startup. " + latestSetVersion + //$NON-NLS-1$ //$NON-NLS-2$ " => " + version); //$NON-NLS-1$ databaseRestoreIfEmpty(context); // invoke upgrade service boolean justUpgraded = latestSetVersion != version; if(justUpgraded && version > 0) { if(latestSetVersion > 0) { upgradeService.performUpgrade(context, latestSetVersion); } else { Preferences.setBoolean(AstridNewSyncMigrator.PREF_SYNC_MIGRATION, true); // New installs should have this flag set up front } AstridPreferences.setCurrentVersion(version); AstridPreferences.setCurrentVersionName(versionName); } upgradeService.performSecondaryUpgrade(context); final int finalLatestVersion = latestSetVersion; abTests.externalInit(context); abChooser.makeChoicesForAllTests(latestSetVersion == 0, taskService.getUserActivationStatus()); abTestInvoker.reportAcquisition(); initializeDatabaseListeners(); ActFmSyncThread.initializeSyncComponents(taskDao, tagDataDao, userActivityDao, taskAttachmentDao, taskListMetadataDao, waitingOnMeDao); // perform startup activities in a background thread new Thread(new Runnable() { public void run() { // start widget updating alarm AlarmManager am = (AlarmManager)context.getSystemService(Context.ALARM_SERVICE); Intent intent = new Intent(context, WidgetUpdateService.class); PendingIntent pendingIntent = PendingIntent.getService(context, 0, intent, PendingIntent.FLAG_CANCEL_CURRENT); am.setInexactRepeating(AlarmManager.RTC, 0, Constants.WIDGET_UPDATE_INTERVAL, pendingIntent); ReengagementService.scheduleReengagementAlarm(context); taskService.cleanup(); // if sync ongoing flag was set, clear it gtasksPreferenceService.stopOngoing(); actFmPreferenceService.stopOngoing(); OpencrxCoreUtils.INSTANCE.stopOngoing(); // perform initialization ReminderStartupReceiver.startReminderSchedulingService(context); BackupService.scheduleService(context); gtasksSyncService.initialize(); // get and display update messages if (finalLatestVersion != 0) new UpdateMessageService(context).processUpdates(); new PremiumUnlockService().checkForPremium(); checkForSubtasksUse(); checkForSwipeListsUse(); checkForVoiceRemindersUse(); } }).start(); AstridPreferences.resetPreferencesFromAbTests(latestSetVersion); AstridPreferences.setPreferenceDefaults(); CalendarStartupReceiver.scheduleCalendarAlarms(context, false); // This needs to be after set preference defaults for the purposes of ab testing // check for task killers if(!Constants.OEM) showTaskKillerHelp(context); hasStartedUp = true; } private void initializeDatabaseListeners() { // This listener makes sure that when a tag's name is created or changed, // the corresponding metadata will also update tagDataDao.addListener(new ModelUpdateListener<TagData>() { @Override public void onModelUpdated(TagData model, boolean outstandingEntries) { ContentValues values = model.getSetValues(); Metadata m = new Metadata(); if (values != null) { if (values.containsKey(TagData.NAME.name)) { m.setValue(TaskToTagMetadata.TAG_NAME, model.getValue(TagData.NAME)); PluginServices.getMetadataService().update(Criterion.and(MetadataCriteria.withKey(TaskToTagMetadata.KEY), TaskToTagMetadata.TAG_UUID.eq(model.getValue(TagData.UUID))), m); } } } }); } /** * @param context * @param e error that was raised */ public static void handleSQLiteError(Context context, final SQLiteException e) { if (context instanceof Activity) { Activity activity = (Activity) context; DialogUtilities.okDialog(activity, activity.getString(R.string.DB_corrupted_title), 0, activity.getString(R.string.DB_corrupted_body), null); } e.printStackTrace(); } private void checkForMissingColumns() { // For some reason these properties are missing for some users. // Make them exist! try { TodorooCursor<Task> tasks = taskService.query(Query.select(Task.UUID, Task.USER_ID, Task.USER).limit(1)); try { System.err.println(tasks.getCount()); } finally { tasks.close(); } } catch (SQLiteException e) { database.tryAddColumn(Task.TABLE, Task.UUID, "'0'"); //$NON-NLS-1$ database.tryAddColumn(Task.TABLE, Task.USER_ID, "0"); //$NON-NLS-1$ database.tryAddColumn(Task.TABLE, Task.USER, null); } } /** * If database exists, no tasks but metadata, and a backup file exists, restore it */ private void databaseRestoreIfEmpty(Context context) { try { if(AstridPreferences.getCurrentVersion() >= UpgradeService.V3_0_0 && !context.getDatabasePath(database.getName()).exists()) { // we didn't have a database! restore latest file File directory = BackupConstants.defaultExportDirectory(); if(!directory.exists()) return; File[] children = directory.listFiles(); AndroidUtilities.sortFilesByDateDesc(children); if(children.length > 0) { StatisticsService.sessionStart(context); TasksXmlImporter.importTasks(context, children[0].getAbsolutePath(), null); StatisticsService.reportEvent(StatisticsConstants.LOST_TASKS_RESTORED); } } } catch (Exception e) { Log.w("astrid-database-restore", e); //$NON-NLS-1$ } } private static final String PREF_SUBTASKS_CHECK = "subtasks_check_stat"; //$NON-NLS-1$ private void checkForSubtasksUse() { if (!Preferences.getBoolean(PREF_SUBTASKS_CHECK, false)) { if (taskService.countTasks() > 3) { StatisticsService.reportEvent(StatisticsConstants.SUBTASKS_HAS_TASKS); checkMetadataStat(Criterion.and(MetadataCriteria.withKey(SubtasksMetadata.METADATA_KEY), SubtasksMetadata.ORDER.gt(0)), StatisticsConstants.SUBTASKS_ORDER_USED); checkMetadataStat(Criterion.and(MetadataCriteria.withKey(SubtasksMetadata.METADATA_KEY), SubtasksMetadata.INDENT.gt(0)), StatisticsConstants.SUBTASKS_INDENT_USED); checkMetadataStat(Criterion.and(MetadataCriteria.withKey(GtasksMetadata.METADATA_KEY), GtasksMetadata.INDENT.gt(0)), StatisticsConstants.GTASKS_INDENT_USED); } Preferences.setBoolean(PREF_SUBTASKS_CHECK, true); } } private static final String PREF_SWIPE_CHECK = "swipe_check_stat"; //$NON-NLS-1$ private void checkForSwipeListsUse() { if (!Preferences.getBoolean(PREF_SWIPE_CHECK, false)) { if (Preferences.getBoolean(R.string.p_swipe_lists_enabled, false) && Preferences.getBoolean(TaskListFragmentPager.PREF_SHOWED_SWIPE_HELPER, false)) { StatisticsService.reportEvent(StatisticsConstants.SWIPE_USED); } Preferences.setBoolean(PREF_SWIPE_CHECK, true); } } private static final String PREF_VOICE_REMINDERS_CHECK = "voice_reminders_check"; //$NON-NLS-1$ private void checkForVoiceRemindersUse() { if (!Preferences.getBoolean(PREF_VOICE_REMINDERS_CHECK, false)) { if (Preferences.getBoolean(R.string.p_voiceRemindersEnabled, false)) { StatisticsService.reportEvent(StatisticsConstants.VOICE_REMINDERS_ENABLED); Preferences.setBoolean(PREF_VOICE_REMINDERS_CHECK, true); } } } private void checkMetadataStat(Criterion criterion, String statistic) { TodorooCursor<Metadata> sort = metadataService.query(Query.select(Metadata.ID).where(criterion).limit(1)); try { if (sort.getCount() > 0) StatisticsService.reportEvent(statistic); } finally { sort.close(); } } private static final String P_TASK_KILLER_HELP = "taskkiller"; //$NON-NLS-1$ /** * Show task killer helper * @param context */ private static void showTaskKillerHelp(final Context context) { if(!Preferences.getBoolean(P_TASK_KILLER_HELP, false)) return; // search for task killers. if they exist, show the help! PackageManager pm = context.getPackageManager(); List<PackageInfo> apps = pm .getInstalledPackages(PackageManager.GET_PERMISSIONS); outer: for (PackageInfo app : apps) { if(app == null || app.requestedPermissions == null) continue; if(app.packageName.startsWith("com.android")) //$NON-NLS-1$ continue; for (String permission : app.requestedPermissions) { if (Manifest.permission.RESTART_PACKAGES.equals(permission)) { CharSequence appName = app.applicationInfo.loadLabel(pm); OnClickListener listener = new OnClickListener() { @Override public void onClick(DialogInterface arg0, int arg1) { Preferences.setBoolean(P_TASK_KILLER_HELP, true); } }; new AlertDialog.Builder(context) .setTitle(R.string.DLG_information_title) .setMessage(context.getString(R.string.task_killer_help, appName)) .setIcon(android.R.drawable.ic_dialog_alert) .setPositiveButton(R.string.task_killer_help_ok, listener) .show(); break outer; } } } } }