/*   This file is part of My Expenses.
 *   My Expenses 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.
 *
 *   My Expenses 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 My Expenses.  If not, see <http://www.gnu.org/licenses/>.
 */

package org.totschnig.myexpenses;

import android.app.Activity;
import android.app.ActivityManager;
import android.app.Application;
import android.content.ContentResolver;
import android.content.ContentUris;
import android.content.ContentValues;
import android.content.Context;
import android.content.SharedPreferences;
import android.content.SharedPreferences.OnSharedPreferenceChangeListener;
import android.content.res.Configuration;
import android.database.ContentObserver;
import android.database.Cursor;
import android.net.Uri;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.v4.provider.DocumentFile;
import android.support.v7.app.AppCompatDelegate;
import android.support.v7.preference.PreferenceManager;
import android.text.TextUtils;
import android.util.Log;

import com.android.calendar.CalendarContractCompat;
import com.android.calendar.CalendarContractCompat.Calendars;
import com.android.calendar.CalendarContractCompat.Events;

import org.acra.ACRA;
import org.acra.config.ACRAConfiguration;
import org.totschnig.myexpenses.di.AppComponent;
import org.totschnig.myexpenses.di.AppModule;
import org.totschnig.myexpenses.di.DaggerAppComponent;
import org.totschnig.myexpenses.model.Template;
import org.totschnig.myexpenses.preference.PrefKey;
import org.totschnig.myexpenses.preference.SharedPreferencesCompat;
import org.totschnig.myexpenses.provider.DatabaseConstants;
import org.totschnig.myexpenses.provider.DbUtils;
import org.totschnig.myexpenses.provider.TransactionProvider;
import org.totschnig.myexpenses.service.DailyAutoBackupScheduler;
import org.totschnig.myexpenses.service.PlanExecutor;
import org.totschnig.myexpenses.util.AcraHelper;
import org.totschnig.myexpenses.util.LicenceHandler;
import org.totschnig.myexpenses.util.Result;
import org.totschnig.myexpenses.util.Utils;
import org.totschnig.myexpenses.widget.AbstractWidget;
import org.totschnig.myexpenses.widget.AccountWidget;
import org.totschnig.myexpenses.widget.TemplateWidget;

import java.io.File;
import java.util.Locale;
import java.util.UUID;

import javax.inject.Inject;

public class MyApplication extends Application implements
    OnSharedPreferenceChangeListener {

  public AppComponent getAppComponent() {
    return appComponent;
  }

  private AppComponent appComponent;
  @Inject
  LicenceHandler licenceHandler;
  @Inject @Nullable
  ACRAConfiguration acraConfiguration;
  private static boolean instrumentationTest = false;
  private static String testId;
  public static final String PLANNER_CALENDAR_NAME = "MyExpensesPlanner";
  public static final String PLANNER_ACCOUNT_NAME = "Local Calendar";
  public static final String INVALID_CALENDAR_ID = "-1";
  private SharedPreferences mSettings;
  private static MyApplication mSelf;

  public static final String BACKUP_DB_FILE_NAME = "BACKUP";
  public static final String BACKUP_PREF_FILE_NAME = "BACKUP_PREF";

  public static final String KEY_NOTIFICATION_ID = "notification_id";
  public static final String KEY_OPERATION_TYPE = "operationType";

  public static final String CONTRIB_SECRET = "RANDOM_SECRET";
  public static final String CALENDAR_FULL_PATH_PROJECTION = "ifnull("
      + Calendars.ACCOUNT_NAME + ",'') || '/' ||" + "ifnull("
      + Calendars.ACCOUNT_TYPE + ",'') || '/' ||" + "ifnull(" + Calendars.NAME
      + ",'')";

  private long mLastPause = 0;
  public final static String TAG = "MyExpenses";

  private boolean isLocked;

  public static void setInstrumentationTest(boolean instrumentationTest) {
    MyApplication.instrumentationTest = instrumentationTest;
  }

  public static boolean isInstrumentationTest() {
    return instrumentationTest;
  }

  public boolean isLocked() {
    return isLocked;
  }

  public void setLocked(boolean isLocked) {
    this.isLocked = isLocked;
  }

  public static final String FEEDBACK_EMAIL = "support@myexpenses.mobi";
  // public static int BACKDOOR_KEY = KeyEvent.KEYCODE_CAMERA;

  /**
   * we cache value of planner calendar id, so that we can handle changes in
   * value
   */
  private String mPlannerCalendarId = "-1";
  /**
   * we store the systemLocale if the user wants to come back to it after having
   * tried a different locale;
   */
  private Locale systemLocale = Locale.getDefault();

  @Override
  public void onCreate() {
    super.onCreate();
    AppCompatDelegate.setCompatVectorFromResourcesEnabled(true);
    //Maybe prevents occasional crashes on Gingerbread
    //https://code.google.com/p/android/issues/detail?id=81083
    try {Class.forName("android.os.AsyncTask");} catch(Throwable ignore) {}
    mSelf = this;
    if (!ACRA.isACRASenderServiceProcess()) {
      // sets up mSettings
      getSettings().registerOnSharedPreferenceChangeListener(this);
      licenceHandler.init(this);
      initPlanner();
      registerWidgetObservers();
    }
  }

  @Override
  protected void attachBaseContext(Context base) {
    super.attachBaseContext(base);
    appComponent = DaggerAppComponent.builder()
        .appModule(new AppModule(this)).build();
    appComponent.inject(this);
    if (acraConfiguration != null) {
      ACRA.init(this, acraConfiguration);
    }
  }

  private void registerWidgetObservers() {
    final ContentResolver r = getContentResolver();
    WidgetObserver mTemplateObserver = new WidgetObserver(TemplateWidget.class);
    for (Uri uri : TemplateWidget.OBSERVED_URIS) {
      r.registerContentObserver(uri, true, mTemplateObserver);
    }
    WidgetObserver mAccountObserver = new WidgetObserver(AccountWidget.class);
    for (Uri uri : AccountWidget.OBSERVED_URIS) {
      r.registerContentObserver(uri, true, mAccountObserver);
    }
  }

  public static MyApplication getInstance() {
    return mSelf;
  }

  public SharedPreferences getSettings() {
    if (mSettings == null) {
      mSettings = instrumentationTest ? getSharedPreferences(getTestId(), Context.MODE_PRIVATE) :
          PreferenceManager.getDefaultSharedPreferences(this);
    }
    return mSettings;
  }

  public static String getTestId() {
    if (testId == null) {
      testId = UUID.randomUUID().toString();
    }
    return testId;
  }

  public static void cleanUpAfterTest() {
    mSelf.deleteDatabase(testId);
    mSelf.getSettings().edit().clear().commit();
    new File(new File(mSelf.getFilesDir().getParentFile().getPath() + "/shared_prefs/"),
        testId + ".xml").delete();
  }

  public static int getThemeId() {
    return getThemeId("");
  }

  public static int getThemeIdEditDialog() {
    return getThemeId("EditDialog");
  }

  public static int getThemeIdTranslucent() {
    return getThemeId("Translucent");
  }

  public LicenceHandler getLicenceHandler() {
    return licenceHandler;
  }

  public enum ThemeType {
    dark, light
  }

  public static ThemeType getThemeType() {
    try {
      return ThemeType.valueOf(PrefKey.UI_THEME_KEY.getString(ThemeType.dark.name()));
    } catch (IllegalArgumentException e) {
      return ThemeType.dark;
    }
  }

  private static int getThemeId(String subStyle) {
    int fontScale;
    try {
      fontScale = PrefKey.UI_FONTSIZE.getInt(0);
    } catch (Exception e) {
      // in a previous version, the same key was holding an integer
      fontScale = 0;
      PrefKey.UI_FONTSIZE.remove();
    }
    String style = getThemeType() == ThemeType.light ? "ThemeLight" : "ThemeDark";
    if (!TextUtils.isEmpty(subStyle)) {
      style += "." + subStyle;
    }
    String resolve = style;
    if (fontScale > 0 && fontScale < 4) {
      resolve = style + ".s" + fontScale;
    }
    int resId = mSelf.getResources().getIdentifier(resolve, "style", mSelf.getPackageName());
    if (resId==0) {
      //try style without font scaling as fallback
      resId = mSelf.getResources().getIdentifier(style, "style", mSelf.getPackageName());
      if (resId==0) throw new RuntimeException(style + " is not defined");
    }
    return resId;
  }

  @Override
  public void onConfigurationChanged(Configuration newConfig) {
    super.onConfigurationChanged(newConfig);
    systemLocale = newConfig.locale;
  }

  public void setLanguage() {
    String language = PrefKey.UI_LANGUAGE.getString("default");
    Locale l;
    if (language.equals("default")) {
      l = systemLocale;
    } else if (language.contains("-")) {
      String[] parts = language.split("-");
      l = new Locale(parts[0], parts[1]);
    } else {
      l = new Locale(language);
    }
    setLanguage(l);
  }

  public void setLanguage(Locale locale) {
    if (!Locale.getDefault().equals(locale)) {
      Locale.setDefault(locale);
      Configuration config = new Configuration();
      config.locale = locale;
      config.fontScale = getResources().getConfiguration().fontScale;
      getResources().updateConfiguration(config,
          getResources().getDisplayMetrics());
    }
  }

  public static DocumentFile requireBackupFile(@NonNull DocumentFile appDir) {
    return Utils.timeStampedFile(appDir, "backup", "application/zip", false);
  }

  public static File getBackupDbFile(File backupDir) {
    return new File(backupDir, BACKUP_DB_FILE_NAME);
  }

  public static File getBackupPrefFile(File backupDir) {
    return new File(backupDir, BACKUP_PREF_FILE_NAME);
  }

  public long getLastPause() {
    return mLastPause;
  }

  public void setLastPause(Activity ctx) {
    if (!isLocked()) {
      // if we are dealing with an activity called from widget that allows to
      // bypass password protection, we do not reset last pause
      // otherwise user could gain unprotected access to the app
      boolean isDataEntryEnabled = PrefKey.PROTECTION_ENABLE_DATA_ENTRY_FROM_WIDGET
          .getBoolean(false);
      boolean isStartFromWidget = ctx.getIntent().getBooleanExtra(
          AbstractWidget.EXTRA_START_FROM_WIDGET_DATA_ENTRY, false);
      if (!isDataEntryEnabled || !isStartFromWidget) {
        this.mLastPause = System.nanoTime();
      }
    }
  }

  public void resetLastPause() {
    this.mLastPause = 0;
  }

  /**
   * @param ctx
   *          Activity that should be password protected, can be null if called
   *          from widget provider
   * @return true if password protection is set, and we have paused for at least
   *         {@link PrefKey#PROTECTION_DELAY_SECONDS} seconds unless we are called
   *         from widget or from an activity called from widget and passwordless
   *         data entry from widget is allowed sets isLocked as a side effect
   */
  public boolean shouldLock(Activity ctx) {
    boolean isStartFromWidget = ctx == null
        || ctx.getIntent().getBooleanExtra(
            AbstractWidget.EXTRA_START_FROM_WIDGET_DATA_ENTRY, false);
    boolean isProtected = isProtected();
    long lastPause = getLastPause();
    boolean isPostDelay = System.nanoTime() - lastPause > (PrefKey.PROTECTION_DELAY_SECONDS
        .getInt(15) * 1000000000L);
    boolean isDataEntryEnabled = PrefKey.PROTECTION_ENABLE_DATA_ENTRY_FROM_WIDGET
        .getBoolean(false);
    if (isProtected && isPostDelay
        && (!isDataEntryEnabled || !isStartFromWidget)) {
      setLocked(true);
      return true;
    }
    return false;
  }

  public boolean isProtected() {
    return PrefKey.PERFORM_PROTECTION.getBoolean(false);
  }

  /**
   * @param calendarId id of calendar in system calendar content provider
   * @return verifies if the passed in calendarid exists and is the one stored
   *         in {@link PrefKey#PLANNER_CALENDAR_PATH}
   */
  private boolean checkPlannerInternal(String calendarId) {
    ContentResolver cr = getContentResolver();
    Cursor c = cr.query(Calendars.CONTENT_URI,
        new String[] { CALENDAR_FULL_PATH_PROJECTION + " AS path" },
        Calendars._ID + " = ?", new String[] { calendarId }, null);
    boolean result = true;
    if (c == null)
      return false;
    else {
      if (c.moveToFirst()) {
        String found = DbUtils.getString(c, 0);
        String expected = PrefKey.PLANNER_CALENDAR_PATH.getString("");
        if (!found.equals(expected)) {
          Log.w(TAG, String.format(
              "found calendar, but path did not match; expected %s ; got %s",
              expected, found));
          result = false;
        }
      } else {
        Log.i(TAG, "configured calendar has been deleted: " + calendarId);
        result = false;
      }
      c.close();
      return result;
    }
  }

  public String checkPlanner() {
    mPlannerCalendarId = PrefKey.PLANNER_CALENDAR_ID.getString(INVALID_CALENDAR_ID);
    if (!mPlannerCalendarId.equals(INVALID_CALENDAR_ID) && !checkPlannerInternal(mPlannerCalendarId)) {
      removePlanner();
      return
          INVALID_CALENDAR_ID;
    }
    return mPlannerCalendarId;
  }

  public void removePlanner() {
    SharedPreferencesCompat.apply(mSettings.edit()
        .remove(PrefKey.PLANNER_CALENDAR_ID.getKey())
        .remove(PrefKey.PLANNER_CALENDAR_PATH.getKey())
        .remove(PrefKey.PLANNER_LAST_EXECUTION_TIMESTAMP.getKey()));
  }

  /**
   * check if we already have a calendar in Account {@link #PLANNER_ACCOUNT_NAME}
   * of type {@link CalendarContractCompat#ACCOUNT_TYPE_LOCAL} with name
   * {@link #PLANNER_ACCOUNT_NAME} if yes use it, otherwise create it
   * 
   * @return true if we have configured a useable calendar
   * @param persistToSharedPref if true id of the created calendar is stored in preferences
   */
  public String createPlanner(boolean persistToSharedPref) {
    Uri.Builder builder = Calendars.CONTENT_URI.buildUpon();
    String plannerCalendarId;
    builder.appendQueryParameter(Calendars.ACCOUNT_NAME, PLANNER_ACCOUNT_NAME);
    builder.appendQueryParameter(Calendars.ACCOUNT_TYPE,
        CalendarContractCompat.ACCOUNT_TYPE_LOCAL);
    builder.appendQueryParameter(CalendarContractCompat.CALLER_IS_SYNCADAPTER,
        "true");
    Uri calendarUri = builder.build();
    Cursor c = getContentResolver().query(calendarUri,
        new String[] { Calendars._ID }, Calendars.NAME + " = ?",
        new String[] { PLANNER_CALENDAR_NAME }, null);
    if (c == null) {
      AcraHelper.report(new Exception(
              "Searching for planner calendar failed, Calendar app not installed?"));
      return INVALID_CALENDAR_ID;
    }
    if (c.moveToFirst()) {
      plannerCalendarId = String.valueOf(c.getLong(0));
      Log.i(TAG, "found a preexisting calendar: " + plannerCalendarId);
      c.close();
    } else {
      c.close();
      ContentValues values = new ContentValues();
      values.put(Calendars.ACCOUNT_NAME, PLANNER_ACCOUNT_NAME);
      values.put(Calendars.ACCOUNT_TYPE,
          CalendarContractCompat.ACCOUNT_TYPE_LOCAL);
      values.put(Calendars.NAME, PLANNER_CALENDAR_NAME);
      values.put(Calendars.CALENDAR_DISPLAY_NAME,
          getString(R.string.plan_calendar_name));
      values.put(Calendars.CALENDAR_COLOR,
          getResources().getColor(R.color.appDefault));
      values.put(Calendars.CALENDAR_ACCESS_LEVEL, Calendars.CAL_ACCESS_OWNER);
      values.put(Calendars.OWNER_ACCOUNT, "private");
      Uri uri;
      try {
        uri = getContentResolver().insert(calendarUri, values);
      } catch (IllegalArgumentException e) {
        AcraHelper.report(e);
        return INVALID_CALENDAR_ID;
      }
      if (uri == null) {
        AcraHelper.report(new Exception(
            "Inserting planner calendar failed, uri is null"));
        return INVALID_CALENDAR_ID;
      }
      plannerCalendarId = uri.getLastPathSegment();
      if (plannerCalendarId == null || plannerCalendarId.equals("0")) {
        AcraHelper.report(new Exception(String.format(Locale.US,
                "Inserting planner calendar failed, last path segment is %s", plannerCalendarId)));
        return INVALID_CALENDAR_ID;
      }
      Log.i(TAG, "successfully set up new calendar: " + plannerCalendarId);
    }
    if (persistToSharedPref) {
      // onSharedPreferenceChanged should now trigger initPlanner
      PrefKey.PLANNER_CALENDAR_ID.putString(plannerCalendarId);
    }
    return plannerCalendarId;
  }

  /**
   * call PlanExecutor, which will 1) set up the planner calendar 2) execute
   * plans 3) reschedule execution through alarm
   */
  public void initPlanner() {
    Log.i(TAG, "initPlanner called, setting plan executor to run in 1 minute");
    PlanExecutor.setAlarm(this, System.currentTimeMillis() + 60000);
  }

  public static String[] buildEventProjection() {
    String[] projection = new String[android.os.Build.VERSION.SDK_INT >= 16 ? 10
        : 8];
    projection[0] = Events.DTSTART;
    projection[1] = Events.DTEND;
    projection[2] = Events.RRULE;
    projection[3] = Events.TITLE;
    projection[4] = Events.ALL_DAY;
    projection[5] = Events.EVENT_TIMEZONE;
    projection[6] = Events.DURATION;
    projection[7] = Events.DESCRIPTION;
    if (android.os.Build.VERSION.SDK_INT >= 16) {
      projection[8] = Events.CUSTOM_APP_PACKAGE;
      projection[9] = Events.CUSTOM_APP_URI;
    }
    return projection;
  }

  /**
   * @param eventCursor
   *          must have been populated with a projection built by
   *          {@link #buildEventProjection()}
   * @param eventValues ContentValues where the extracted data is copied to
   */
  public static void copyEventData(Cursor eventCursor, ContentValues eventValues) {
    eventValues.put(Events.DTSTART, DbUtils.getLongOrNull(eventCursor, 0));
    //older Android versions have populated both dtend and duration
    //restoring those on newer versions leads to IllegalArgumentexception
    Long dtEnd = DbUtils.getLongOrNull(eventCursor, 1);
    String dtDuration = dtEnd != null ? null : eventCursor.getString(6);
    eventValues.put(Events.DTEND, dtEnd);
    eventValues.put(Events.RRULE, eventCursor.getString(2));
    eventValues.put(Events.TITLE, eventCursor.getString(3));
    eventValues.put(Events.ALL_DAY, eventCursor.getInt(4));
    eventValues.put(Events.EVENT_TIMEZONE, eventCursor.getString(5));
    eventValues.put(Events.DURATION, dtDuration);
    eventValues.put(Events.DESCRIPTION, eventCursor.getString(7));
    if (android.os.Build.VERSION.SDK_INT >= 16) {
      eventValues.put(Events.CUSTOM_APP_PACKAGE, eventCursor.getString(8));
      eventValues.put(Events.CUSTOM_APP_URI, eventCursor.getString(9));
    }
  }

  private boolean insertEventAndUpdatePlan(ContentValues eventValues,
      long templateId) {
    Uri uri = getContentResolver().insert(Events.CONTENT_URI, eventValues);
    long planId = ContentUris.parseId(uri);
    Log.i(TAG, "event copied with new id: " + planId);
    ContentValues planValues = new ContentValues();
    planValues.put(DatabaseConstants.KEY_PLANID, planId);
    int updated = getContentResolver().update(
        ContentUris.withAppendedId(Template.CONTENT_URI, templateId),
        planValues, null, null);
    return updated > 0;
  }

  @Override
  public void onSharedPreferenceChanged(SharedPreferences sharedPreferences,
      String key) {
    if (!key.equals(PrefKey.AUTO_BACKUP_DIRTY.getKey())) {
      markDataDirty();
    }
    // TODO: move to TaskExecutionFragment
    if (!key.equals(PrefKey.PLANNER_CALENDAR_ID.getKey())) {
      return;
    }
    String oldValue = mPlannerCalendarId;
    boolean safeToMovePlans = true;
    String newValue = sharedPreferences.getString(
        PrefKey.PLANNER_CALENDAR_ID.getKey(), "-1");
    if (oldValue.equals(newValue)) {
      return;
    }
    mPlannerCalendarId = newValue;
    if (!newValue.equals("-1")) {
      // if we cannot verify that the oldValue has the correct path
      // we will not risk mangling with an unrelated calendar
      if (!oldValue.equals("-1") && !checkPlannerInternal(oldValue))
        safeToMovePlans = false;
      ContentResolver cr = getContentResolver();
      // we also store the name and account of the calendar,
      // to protect against cases where a user wipes the data of the calendar
      // provider
      // and then accidentally we link to the wrong calendar
      Uri uri = ContentUris.withAppendedId(Calendars.CONTENT_URI,
          Long.parseLong(mPlannerCalendarId));
      Cursor c = cr.query(uri, new String[] { CALENDAR_FULL_PATH_PROJECTION
          + " AS path" }, null, null, null);
      if (c != null && c.moveToFirst()) {
        String path = c.getString(0);
        Log.i(TAG, "storing calendar path : " + path);
        PrefKey.PLANNER_CALENDAR_PATH.putString(path);
      } else {
        AcraHelper.report(new IllegalStateException(
            "could not retrieve configured calendar"));
        mPlannerCalendarId = "-1";
        PrefKey.PLANNER_CALENDAR_PATH.remove();
        PrefKey.PLANNER_CALENDAR_ID.putString("-1");
      }
      if (c != null) {
        c.close();
      }
      if (mPlannerCalendarId.equals("-1")) {
        return;
      }
      if (oldValue.equals("-1")) {
        initPlanner();
      } else if (safeToMovePlans) {
        ContentValues eventValues = new ContentValues();
        eventValues.put(Events.CALENDAR_ID, Long.parseLong(newValue));
        Cursor planCursor = cr.query(Template.CONTENT_URI, new String[] {
            DatabaseConstants.KEY_ROWID, DatabaseConstants.KEY_PLANID },
            DatabaseConstants.KEY_PLANID + " IS NOT null", null, null);
        if (planCursor != null) {
          if (planCursor.moveToFirst()) {
            do {
              long templateId = planCursor.getLong(0);
              long planId = planCursor.getLong(1);
              Uri eventUri = ContentUris.withAppendedId(Events.CONTENT_URI,
                  planId);

              Cursor eventCursor = cr.query(eventUri, buildEventProjection(),
                  Events.CALENDAR_ID + " = ?", new String[] { oldValue }, null);
              if (eventCursor != null) {
                if (eventCursor.moveToFirst()) {
                  // Log.i("DEBUG",
                  // DatabaseUtils.dumpCursorToString(eventCursor));
                  copyEventData(eventCursor, eventValues);
                  if (insertEventAndUpdatePlan(eventValues, templateId)) {
                    Log.i(TAG, "updated plan id in template:" + templateId);
                    int deleted = cr.delete(eventUri, null, null);
                    Log.i(TAG, "deleted old event: " + deleted);
                  }
                }
                eventCursor.close();
              }
            } while (planCursor.moveToNext());
          }
          planCursor.close();
        }
      }
    } else {
      PrefKey.PLANNER_CALENDAR_PATH.remove();
    }
  }

  private class WidgetObserver extends ContentObserver {
    /**
       * 
       */
    private Class<? extends AbstractWidget<?>> mProvider;

    WidgetObserver(Class<? extends AbstractWidget<?>> provider) {
      super(null);
      mProvider = provider;
    }

    @Override
    public void onChange(boolean selfChange) {
      AbstractWidget.updateWidgets(mSelf, mProvider);
    }
  }

  public int getMemoryClass() {
    ActivityManager am = (ActivityManager) getSystemService(ACTIVITY_SERVICE);
    return am.getMemoryClass();
  }

  /**
   * 1.check if a planner is configured. If no, nothing to do 2.check if the
   * configured planner exists on the device 2.1 if yes go through all events
   * and look for them based on UUID added to description recreate events that
   * we did not find (2.2 if no, user should have been asked to select a target
   * calendar where we will store the recreated events)
   *
   * @return Result with success true
   */
  public Result restorePlanner() {
    ContentResolver cr = getContentResolver();
    String TAG = "restorePlanner";
    String calendarId = PrefKey.PLANNER_CALENDAR_ID.getString("-1");
    String calendarPath = PrefKey.PLANNER_CALENDAR_PATH.getString("");
    Log.d(TAG, String.format(
        "restore plans to calendar with id %s and path %s", calendarId,
        calendarPath));
    int restoredPlansCount = 0;
    if (!(calendarId.equals("-1") || calendarPath.equals(""))) {
      Cursor c = cr.query(Calendars.CONTENT_URI,
          new String[] { Calendars._ID }, CALENDAR_FULL_PATH_PROJECTION
              + " = ?", new String[] { calendarPath }, null);
      if (c != null) {
        if (c.moveToFirst()) {
          mPlannerCalendarId = c.getString(0);
          Log.d(TAG, String.format("restorePlaner: found calendar with id %s",
              mPlannerCalendarId));
          PrefKey.PLANNER_CALENDAR_ID.putString(mPlannerCalendarId);
          ContentValues planValues = new ContentValues(), eventValues = new ContentValues();
          eventValues.put(Events.CALENDAR_ID,
              Long.parseLong(mPlannerCalendarId));
          Cursor planCursor = cr.query(Template.CONTENT_URI, new String[] {
              DatabaseConstants.KEY_ROWID, DatabaseConstants.KEY_PLANID,
              DatabaseConstants.KEY_UUID }, DatabaseConstants.KEY_PLANID
              + " IS NOT null", null, null);
          if (planCursor != null) {
            if (planCursor.moveToFirst()) {
              do {
                long templateId = planCursor.getLong(0);
                long oldPlanId = planCursor.getLong(1);
                String uuid = planCursor.getString(2);
                Cursor eventCursor = cr
                    .query(Events.CONTENT_URI, new String[] { Events._ID },
                        Events.CALENDAR_ID + " = ? AND " + Events.DESCRIPTION
                            + " LIKE ?", new String[] { mPlannerCalendarId,
                            "%" + uuid + "%" }, null);
                if (eventCursor != null) {
                  if (eventCursor.moveToFirst()) {
                    long newPlanId = eventCursor.getLong(0);
                    Log.d(TAG, String.format(
                        "Looking for event with uuid %s: found id %d. "
                            + "Original event had id %d", uuid, newPlanId,
                        oldPlanId));
                    if (newPlanId != oldPlanId) {
                      planValues.put(DatabaseConstants.KEY_PLANID, newPlanId);
                      int updated = cr.update(ContentUris.withAppendedId(
                          Template.CONTENT_URI, templateId), planValues, null,
                          null);
                      if (updated > 0) {
                        Log.i(TAG, "updated plan id in template:" + templateId);
                        restoredPlansCount++;
                      }
                    } else {
                      restoredPlansCount++;
                    }
                    continue;
                  }
                  eventCursor.close();
                }
                Log.d(
                    TAG,
                    String
                        .format(
                            "Looking for event with uuid %s did not find, now reconstructing from cache",
                            uuid));
                eventCursor = cr.query(TransactionProvider.EVENT_CACHE_URI,
                    buildEventProjection(), Events.DESCRIPTION + " LIKE ?",
                    new String[] { "%" + uuid + "%" }, null);
                boolean found = false;
                if (eventCursor != null) {
                  if (eventCursor.moveToFirst()) {
                    found = true;
                    copyEventData(eventCursor, eventValues);
                    if (insertEventAndUpdatePlan(eventValues, templateId)) {
                      Log.i(TAG, "updated plan id in template:" + templateId);
                      restoredPlansCount++;
                    }
                  }
                  eventCursor.close();
                }
                if (!found) {
                  //need to set eventId to null
                  planValues.putNull(DatabaseConstants.KEY_PLANID);
                  getContentResolver().update(
                      ContentUris.withAppendedId(Template.CONTENT_URI, templateId),
                      planValues, null, null);
                }
              } while (planCursor.moveToNext());
            }
            planCursor.close();
          }
        }
        c.close();
      }
    }
    return new Result(true, R.string.restore_calendar_success,
        restoredPlansCount);
  }

  public static String getMarketPrefix() {
    switch(BuildConfig.FLAVOR) {
      case "amazon":
        return "amzn://apps/android?p=";
      default:
        return "market://details?id=";
    }
  }

  public static String getMarketSelfUri() {
    if (BuildConfig.FLAVOR.equals("blackberry")) {
      return "appworld://content/54472888";
    } else {
      return getMarketPrefix() + "org.totschnig.myexpenses";
    }
  }

  public static void markDataDirty() {
    boolean persistedDirty =  PrefKey.AUTO_BACKUP_DIRTY.getBoolean(true);
    if (!persistedDirty) {
      PrefKey.AUTO_BACKUP_DIRTY.putBoolean(true);
      DailyAutoBackupScheduler.updateAutoBackupAlarms(mSelf);
    }
  }
}
