public class DatabaseHelper extends SQLiteOpenHelper {
    public static final DateTimeFormatter DB_DATE_FORMATTER = DateTimeFormat.forPattern("yyyy-MM-dd");
    public static final DateTimeFormatter DB_TIME_FORMATTER = DateTimeFormat.forPattern("HH:mm");
    public static final String DBT_EVENT = "[Event]",
            DBC_EVENT_ROW_ID = "[ROWID]",
            DBC_EVENT_DATE = "[Date]",
            DBC_EVENT_TIME = "[Time]",
            DBC_EVENT_TYPE = "[TypeKey]",
            DBC_EVENT_SUBTYPE = "[SubTypeKey]",
            DBC_EVENT_DESC = "[Description]";
    private static final String TAG = "DatabaseHelper";
    private static final String DATABASE_NAME = "Events.db";
    private static final int DATABASE_VERSION = 6;
    private static final String SQL_CREATE_DBT_EVENT =
            "CREATE TABLE " + DBT_EVENT + " (" +
                    DBC_EVENT_DATE + " DATE NOT NULL, " +
                    DBC_EVENT_TIME + " TIME NOT NULL, " +
                    DBC_EVENT_TYPE + " INTEGER NOT NULL DEFAULT 5, " +
                    DBC_EVENT_SUBTYPE + " INTEGER NOT NULL DEFAULT 0, " +
                    DBC_EVENT_DESC + " TEXT (1000) );";
    private static final String SQL_CREATE_DBI_EVENT_DATE =
            "CREATE INDEX [EventDateIndex] ON " + DBT_EVENT + " (" + DBC_EVENT_DATE + " ASC);";
    private static final String SQL_CREATE_DBI_EVENT_TYPE =
            "CREATE INDEX [EventTypeIndex] ON " + DBT_EVENT + " (" + DBC_EVENT_TYPE + " ASC);";

    private Context mContext = null;

    public DatabaseHelper(Context context) {
        super(context, DATABASE_NAME, null, DATABASE_VERSION);
        mContext = context;
    }

    @Override
    public void onCreate(SQLiteDatabase db) {
        try {
            db.beginTransaction();

            db.execSQL(SQL_CREATE_DBT_EVENT);
            db.execSQL(SQL_CREATE_DBI_EVENT_DATE);
            db.execSQL(SQL_CREATE_DBI_EVENT_TYPE);

            //db.execSQL("CREATE TABLE [EventType] ([Name] TEXT (50) PRIMARY KEY UNIQUE NOT NULL, [Custom] BOOLEAN (0) NOT NULL DEFAULT (1), [Order] INTEGER NOT NULL DEFAULT (99), [Parent] TEXT (50) REFERENCES [EventType] ([Name]) ON DELETE CASCADE ON UPDATE CASCADE);");
            /*db.execSQL("INSERT INTO [EventType] ([Name], [Custom], [Order], [Parent]) VALUES " +
                            "('Food', 0, 10, NULL)," +
                            "('Drink', 0, 11, NULL)," +
                            "('Activity', 0, 20, NULL)," +
                            "('Exercise', 0, 21, NULL)," +
                            "('Supplements', 0, 30, NULL)," +
                            "('Medication', 0, 31, NULL)," +
                            "('Breakfast', 0, 10, 'Food')," +
                            "('Launch', 0, 11, 'Food')," +
                            "('Dinner', 0, 12, 'Food')," +
                            "('Snack', 0, 13, 'Food')," +
                            "('Coffee', 0, 10, 'Drink')," +
                            "('Tea', 0, 11, 'Drink')," +
                            "('Water', 0, 12, 'Drink')," +
                            "('Alcohol', 0, 13, 'Drink')," +
                            "('Fruit Juice', 0, 14, 'Drink')," +
                            "('Energy Drink', 0, 15, 'Drink')," +
                            "('Soda', 0, 16, 'Drink');");*/

            Log.d(TAG, "Database " + DATABASE_NAME + " created.");
            db.setTransactionSuccessful();
        } finally {
            db.endTransaction();
        }
    }

    @Override
    public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
        switch (oldVersion) {
            case 1:
                try {
                    db.beginTransaction();

                    db.execSQL("ALTER TABLE " + DBT_EVENT + " ADD COLUMN " + DBC_EVENT_TYPE + " INTEGER NOT NULL DEFAULT 5;");
                    db.execSQL("ALTER TABLE " + DBT_EVENT + " ADD COLUMN " + DBC_EVENT_SUBTYPE + " INTEGER NOT NULL DEFAULT 0;");
                    db.execSQL("DROP INDEX IF EXISTS [EventTypeIndex];");
                    db.execSQL("CREATE INDEX [EventTypeIndex] ON " + DBT_EVENT + " (" + DBC_EVENT_TYPE + " ASC);");
                    migrate1To2Helper1(db, "[Type]", DBC_EVENT_TYPE, "FOOD", 0);
                    migrate1To2Helper1(db, "[Type]", DBC_EVENT_TYPE, "DRINK", 1);
                    migrate1To2Helper1(db, "[Type]", DBC_EVENT_TYPE, "MED", 2);
                    migrate1To2Helper1(db, "[Title]", DBC_EVENT_SUBTYPE, "Food", 0);
                    migrate1To2Helper1(db, "[Title]", DBC_EVENT_SUBTYPE, "Drink", 0);
                    migrate1To2Helper1(db, "[Title]", DBC_EVENT_SUBTYPE, "Breakfast", 1);

                    Log.d(TAG, "Database version updated from 1 to 2.");
                    db.setTransactionSuccessful();
                } catch (SQLException e) {
                    Log.e(TAG, "An exception occurred while database updating from 1 to 2.", e);
                    throw e;
                } finally {
                    if (db.inTransaction()) {
                        db.endTransaction();
                    }
                }
            case 2:
                try {
                    db.beginTransaction();

                    db.execSQL("ALTER TABLE " + DBT_EVENT + " RENAME TO [EVENT1];");
                    db.execSQL("DROP INDEX [EventDateIndex];");
                    db.execSQL("DROP INDEX [EventTypeIndex];");

                    db.execSQL(SQL_CREATE_DBT_EVENT);
                    db.execSQL(SQL_CREATE_DBI_EVENT_DATE);
                    db.execSQL(SQL_CREATE_DBI_EVENT_TYPE);

                    db.execSQL("INSERT INTO " + DBT_EVENT + " SELECT [Date], [Time], [TypeKey], [SubTypeKey], [Description] FROM [EVENT1];");
                    db.execSQL("DROP TABLE [EVENT1];");

                    Log.d(TAG, "Database version updated from 2 to 3.");
                    db.setTransactionSuccessful();
                } catch (SQLException e) {
                    Log.e(TAG, "An exception occurred while database updating from 2 to 3.", e);
                    throw e;
                } finally {
                    if (db.inTransaction()) {
                        db.endTransaction();
                    }
                }
            case 3:
            case 4:
                try {
                    db.beginTransaction();
                    ContentValues values = new ContentValues();
                    values.put(DBC_EVENT_TYPE, 0);
                    db.update(DBT_EVENT, values, DBC_EVENT_TYPE + " == ?", new String[]{Integer.toString(-1),});
                    db.setTransactionSuccessful();
                } catch (SQLException e) {
                    Log.e(TAG, "An exception occurred while database updating from 2 to 4.", e);
                    throw e;
                } finally {
                    if (db.inTransaction()) {
                        db.endTransaction();
                    }
                }
                //db.execSQL("ALTER TABLE [Event]....;");
                //db.delete(DBT_EVENT, null, null); // DELETE EVERYTHING!!!
                break;
            case 5:
                // Settings keys has been changed. And this is the easiest way to apply this for the previous versions.
                SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(mContext);
                SharedPreferences.Editor editor = preferences.edit();
                if (preferences.contains("general_clock_mode")) {
                    editor.putString(PreferenceKeys.KEY_GENERAL_CLOCK_MODE_STRING,
                            preferences.getString("general_clock_mode", "-1"));
                    editor.remove("general_clock_mode");
                }
                if (preferences.contains("notifications_active")) {
                    editor.putBoolean(PreferenceKeys.KEY_NOTIFICATIONS_ACTIVE_BOOL,
                            preferences.getBoolean("notifications_active", true));
                    editor.remove("notifications_active");
                }
                if (preferences.contains("notifications_daily_remainder")) {
                    editor.putBoolean(PreferenceKeys.KEY_NOTIFICATIONS_DAILY_REMAINDER_BOOL,
                            preferences.getBoolean("notifications_daily_remainder", true));
                    editor.remove("notifications_daily_remainder");
                }
                if (preferences.contains("notifications_daily_remainder_time")) {
                    editor.putString(PreferenceKeys.KEY_NOTIFICATIONS_DAILY_REMAINDER_TIME_STRING,
                            preferences.getString("notifications_daily_remainder_time", "19:00"));
                    editor.remove("notifications_daily_remainder_time");
                }
                editor.apply();
                break;
            default:
                break;
        }
    }

    @Override
    public void onConfigure(SQLiteDatabase db) {
        super.onConfigure(db);

        if (!db.isReadOnly()) {
            db.execSQL("PRAGMA foreign_keys = on;");
        }
    }

    private void migrate1To2Helper1(SQLiteDatabase db, String oldColumn, String newColumn, String oldValue, int newValue) {
        ContentValues values = new ContentValues();
        values.put(newColumn, newValue);
        db.update(DBT_EVENT, values, oldColumn + " == ?", new String[]{oldValue,});
    }
} @SuppressWarnings({"WeakerAccess", "unused"})
public class EventHelper {
    private static final String TAG = "EventHelper";

    public static Event parse(@NonNull final Cursor cursor) {
        return new Event(
                cursor.getLong(0), // ROWID
                LocalDate.parse(cursor.getString(1), DatabaseHelper.DB_DATE_FORMATTER), // DATE
                LocalTime.parse(cursor.getString(2), DatabaseHelper.DB_TIME_FORMATTER), // TIME
                cursor.getInt(3), // TYPE
                cursor.getInt(4), // SUBTYPE
                cursor.getString(5) // DESCRIPTION
        );
    }

    public static String[] getDatabaseColumns() {
        return new String[]{
                DatabaseHelper.DBC_EVENT_ROW_ID,
                DatabaseHelper.DBC_EVENT_DATE,
                DatabaseHelper.DBC_EVENT_TIME,
                DatabaseHelper.DBC_EVENT_TYPE,
                DatabaseHelper.DBC_EVENT_SUBTYPE,
                DatabaseHelper.DBC_EVENT_DESC
        };
    }

    private static ContentValues getContentValues(@NonNull final Event event) {
        return getContentValues(event, false);
    }

    private static ContentValues getContentValues(@NonNull final Event event,
                                                  final boolean includeID) {
        final ContentValues values = new ContentValues(includeID ? 6 : 5);

        if (includeID) {
            values.put(DatabaseHelper.DBC_EVENT_ROW_ID, event.getID());
        }

        values.put(DatabaseHelper.DBC_EVENT_DATE, event.getDate().toString(DatabaseHelper.DB_DATE_FORMATTER));
        values.put(DatabaseHelper.DBC_EVENT_TIME, event.getTime().toString(DatabaseHelper.DB_TIME_FORMATTER));
        values.put(DatabaseHelper.DBC_EVENT_TYPE, event.getType());
        values.put(DatabaseHelper.DBC_EVENT_SUBTYPE, event.getSubType());
        values.put(DatabaseHelper.DBC_EVENT_DESC, event.getDescription());

        return values;
    }

    public static boolean insert(@NonNull final Context context,
                                 @NonNull final Event event) throws SQLiteException {
        DatabaseHelper databaseHelper = new DatabaseHelper(context);

        try {
            SQLiteDatabase db = databaseHelper.getWritableDatabase();
            return insert(db, event);
        } finally {
            databaseHelper.close();
        }
    }

    public static boolean insert(@NonNull final SQLiteDatabase writableDatabase,
                                 @NonNull final Event event) throws SQLiteException {
        long id = writableDatabase.insert(DatabaseHelper.DBT_EVENT, DatabaseHelper.DBC_EVENT_DESC,
                EventHelper.getContentValues(event, event.getID() > 0));

        if (id >= 0) {
            event.setID(id);
            return true;
        }

        return false;
    }

    public static boolean update(@NonNull final Context context,
                                 @NonNull final Event event) throws SQLiteException {
        DatabaseHelper databaseHelper = new DatabaseHelper(context);

        try {
            SQLiteDatabase db = databaseHelper.getWritableDatabase();
            return update(db, event);
        } finally {
            databaseHelper.close();
        }
    }

    public static boolean update(@NonNull final SQLiteDatabase writableDatabase,
                                 @NonNull final Event event) throws SQLiteException {
        return writableDatabase.update(DatabaseHelper.DBT_EVENT, EventHelper.getContentValues(event),
                DatabaseHelper.DBC_EVENT_ROW_ID + " == ?",
                new String[]{Long.toString(event.getID())}) > 0;
    }

    public static boolean delete(@NonNull final Context context,
                                 @NonNull final Event event) throws SQLiteException {
        DatabaseHelper databaseHelper = new DatabaseHelper(context);

        try {
            SQLiteDatabase db = databaseHelper.getWritableDatabase();
            return delete(db, event);
        } finally {
            databaseHelper.close();
        }
    }

    public static boolean delete(@NonNull final SQLiteDatabase writableDatabase,
                                 @NonNull final Event event) throws SQLiteException {
        return writableDatabase.delete(DatabaseHelper.DBT_EVENT,
                DatabaseHelper.DBC_EVENT_ROW_ID + " == ?",
                new String[]{Long.toString(event.getID())}) > 0;
    }

    @Nullable
    public static Event getEventByID(@NonNull final Context context,
                                     final long id) throws SQLiteException {
        DatabaseHelper databaseHelper = new DatabaseHelper(context);

        try {
            SQLiteDatabase db = databaseHelper.getReadableDatabase();
            return getEventByID(db, id);
        } finally {
            databaseHelper.close();
        }
    }

    @Nullable
    public static Event getEventByID(@NonNull final SQLiteDatabase readableDatabase,
                                     final long id) throws SQLiteException {
        Cursor cursor = null;
        try {
            cursor = readableDatabase.query(DatabaseHelper.DBT_EVENT, getDatabaseColumns(),
                    DatabaseHelper.DBC_EVENT_ROW_ID + " == ?", new String[]{Long.toString(id)},
                    null, null, null);

            if (cursor.moveToFirst()) {
                return parse(cursor);
            }
        } finally {
            if (null != cursor) {
                cursor.close();
            }
        }

        return null;
    }

    @Nullable
    public static ArrayList<Event> getEventByDate(@NonNull final Context context,
                                                  @NonNull final LocalDate date) throws SQLiteException {
        DatabaseHelper databaseHelper = new DatabaseHelper(context);

        try {
            SQLiteDatabase db = databaseHelper.getReadableDatabase();
            return getEventByDate(db, date);
        } finally {
            databaseHelper.close();
        }
    }

    @Nullable
    public static ArrayList<Event> getEventByDate(@NonNull final SQLiteDatabase readableDatabase,
                                                  @NonNull final LocalDate date) throws SQLiteException {
        Cursor cursor = null;
        try {
            cursor = readableDatabase.query(DatabaseHelper.DBT_EVENT, EventHelper.getDatabaseColumns(),
                    DatabaseHelper.DBC_EVENT_DATE + " = ?", new String[]{date.toString(DatabaseHelper.DB_DATE_FORMATTER)},
                    null, null, DatabaseHelper.DBC_EVENT_TIME + "," + DatabaseHelper.DBC_EVENT_ROW_ID);

            ArrayList<Event> list;
            if (cursor.moveToFirst()) {
                Log.d(TAG, MessageFormat.format("There are {0,number,integer} records on {1}.", cursor.getCount(), date));
                list = new ArrayList<>(cursor.getCount());

                do {
                    list.add(parse(cursor));
                } while (cursor.moveToNext());

                return list;
            }
        } finally {
            if (null != cursor) {
                cursor.close();
            }
        }

        return null;
    }

    @Nullable
    public static ArrayList<Event> getEventByDateRange(@NonNull final Context context,
                                                       @NonNull final LocalDate startDate,
                                                       @NonNull final LocalDate endDate)
            throws SQLiteException, IllegalArgumentException {
        DatabaseHelper databaseHelper = new DatabaseHelper(context);

        try {
            SQLiteDatabase db = databaseHelper.getReadableDatabase();
            return getEventByDateRange(db, startDate, endDate);
        } finally {
            databaseHelper.close();
        }
    }

    @Nullable
    public static ArrayList<Event> getEventByDateRange(@NonNull final SQLiteDatabase readableDatabase,
                                                       @NonNull final LocalDate startDate,
                                                       @NonNull final LocalDate endDate)
            throws SQLiteException, IllegalArgumentException {
        if (startDate.isAfter(endDate)) {
            throw new IllegalArgumentException("startDate must be <= endDate");
        }

        Cursor cursor = null;
        try {
            cursor = readableDatabase.query(DatabaseHelper.DBT_EVENT, EventHelper.getDatabaseColumns(),
                    DatabaseHelper.DBC_EVENT_DATE + " >= ? AND " + DatabaseHelper.DBC_EVENT_DATE + " <= ?",
                    new String[]{startDate.toString(DatabaseHelper.DB_DATE_FORMATTER), endDate.toString(DatabaseHelper.DB_DATE_FORMATTER)},
                    null, null, DatabaseHelper.DBC_EVENT_DATE + "," + DatabaseHelper.DBC_EVENT_TIME + "," + DatabaseHelper.DBC_EVENT_ROW_ID);

            ArrayList<Event> list;
            if (cursor.moveToFirst()) {
                Log.d(TAG, MessageFormat.format("There are {0,number,integer} records between {1} and {2}.", cursor.getCount(), startDate, endDate));
                list = new ArrayList<>(cursor.getCount());

                do {
                    list.add(parse(cursor));
                } while (cursor.moveToNext());

                return list;
            }
        } finally {
            if (null != cursor) {
                cursor.close();
            }
        }

        return null;
    }

    public static int hasEventToday(@NonNull final Context context) throws SQLiteException {
        return getEventCountByDate(context, LocalDate.now());
    }

    public static int hasEventToday(@NonNull final SQLiteDatabase readableDatabase)
            throws SQLiteException {
        return getEventCountByDate(readableDatabase, LocalDate.now());
    }

    public static int getEventCountByDate(@NonNull final Context context,
                                          @NonNull final LocalDate date) throws SQLiteException {
        DatabaseHelper databaseHelper = new DatabaseHelper(context);

        try {
            final SQLiteDatabase db = databaseHelper.getReadableDatabase();
            return getEventCountByDate(db, date);
        } finally {
            databaseHelper.close();
        }
    }

    public static int getEventCountByDate(@NonNull final SQLiteDatabase readableDatabase,
                                          @NonNull final LocalDate date) throws SQLiteException {
        Cursor cursor = null;
        try {
            cursor = readableDatabase.query(DatabaseHelper.DBT_EVENT, new String[]{"COUNT(*)",},
                    DatabaseHelper.DBC_EVENT_DATE + " = ?", new String[]{date.toString(DatabaseHelper.DB_DATE_FORMATTER)},
                    null, null, null);

            if (cursor.moveToFirst()) {
                return cursor.getInt(0);
            }
        } finally {
            if (null != cursor) {
                cursor.close();
            }
        }

        return 0;
    }

    public static int getEventCountByDateRange(@NonNull final Context context,
                                               @NonNull final LocalDate startDate,
                                               @NonNull final LocalDate endDate) throws SQLiteException {
        DatabaseHelper databaseHelper = new DatabaseHelper(context);

        try {
            SQLiteDatabase db = databaseHelper.getReadableDatabase();
            return getEventCountByDateRange(db, startDate, endDate);
        } finally {
            databaseHelper.close();
        }
    }

    public static int getEventCountByDateRange(@NonNull final SQLiteDatabase readableDatabase,
                                               @NonNull final LocalDate startDate,
                                               @NonNull final LocalDate endDate) throws SQLiteException {
        Cursor cursor = null;
        try {
            cursor = readableDatabase.query(DatabaseHelper.DBT_EVENT, new String[]{"COUNT(*)",},
                    DatabaseHelper.DBC_EVENT_DATE + " >= ? AND " + DatabaseHelper.DBC_EVENT_DATE + " <= ?",
                    new String[]{startDate.toString(DatabaseHelper.DB_DATE_FORMATTER), endDate.toString(DatabaseHelper.DB_DATE_FORMATTER)},
                    null, null, null);

            if (cursor.moveToFirst()) {
                return cursor.getInt(0);
            }
        } finally {
            if (null != cursor) {
                cursor.close();
            }
        }

        return 0;
    }

    @Nullable
    public static Cursor getCursorOfDescriptionsByPartial(@NonNull final Context context,
                                                          @NonNull final String partialDescription) throws SQLiteException {
        LocalDate aMonthBefore = LocalDate.now().minusMonths(1);

        final DatabaseHelper dbHelper = new DatabaseHelper(context);
        final SQLiteDatabase readableDatabase = dbHelper.getReadableDatabase();

        return readableDatabase.query(DatabaseHelper.DBT_EVENT,
                new String[]{
                        DatabaseHelper.DBC_EVENT_ROW_ID + " _id",
                        DatabaseHelper.DBC_EVENT_DATE,
                        DatabaseHelper.DBC_EVENT_TIME,
                        DatabaseHelper.DBC_EVENT_TYPE,
                        DatabaseHelper.DBC_EVENT_SUBTYPE,
                        DatabaseHelper.DBC_EVENT_DESC
                },
                DatabaseHelper.DBC_EVENT_DATE + " >= ? AND " + DatabaseHelper.DBC_EVENT_DESC + " LIKE ? COLLATE NOCASE",
                new String[]{aMonthBefore.toString(DatabaseHelper.DB_DATE_FORMATTER), partialDescription + "%"},
                DatabaseHelper.DBC_EVENT_DESC + " COLLATE NOCASE", null,
                DatabaseHelper.DBC_EVENT_DATE + " DESC," + DatabaseHelper.DBC_EVENT_TIME + "," + DatabaseHelper.DBC_EVENT_ROW_ID);
    }
} public class BackupFragment extends Fragment implements View.OnClickListener {
    public static final String TAG = "BackupFragment";
    private static final String KEY_FROM_DATE_SERIALIZABLE = "FROM DATE";
    private static final String KEY_TO_DATE_SERIALIZABLE = "TO DATE";
    private static final String KEY_SELECTED_FORMAT_INT = "FORMAT";
    private static final int REQUEST_EXTERNAL_STORAGE = 30;

    protected OnFragmentInteractionListener mListener;

    protected GridLayout mGridLayout;
    private TextView tvFromDatePicker, tvToDatePicker;
    private Spinner spFormats;

    protected LocalDate mFromDate, mToDate;

    protected DatabaseHelper mDatabaseHelper;
    protected ProgressDialog mProgressDialog;
    protected BackupAsyncTask mAsyncTask = null;

    public static BackupFragment newInstance() {
        return new BackupFragment();
    }

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setRetainInstance(true);
        setHasOptionsMenu(true);

        mDatabaseHelper = new DatabaseHelper(getContext());

        if (null != savedInstanceState) {
            mFromDate = (LocalDate) savedInstanceState.getSerializable(KEY_FROM_DATE_SERIALIZABLE);
            mToDate = (LocalDate) savedInstanceState.getSerializable(KEY_TO_DATE_SERIALIZABLE);
        } else {
            mFromDate = getFirstDate();
            mToDate = LocalDate.now();
        }
    }

    @Override
    public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
        mGridLayout = (GridLayout) inflater.inflate(R.layout.fragment_backup_gridlayout, container, false);

        tvFromDatePicker = mGridLayout.findViewById(R.id.tvFromDatePicker);
        tvToDatePicker = mGridLayout.findViewById(R.id.tvToDatePicker);
        spFormats = mGridLayout.findViewById(R.id.spFormats);

        ResourcesHelper.setLeftCompoundDrawable(mGridLayout, R.id.tvFrom, R.drawable.calendar_today);
        ResourcesHelper.setLeftCompoundDrawable(mGridLayout, R.id.tvTo, R.drawable.calendar);
        ResourcesHelper.setLeftCompoundDrawable(mGridLayout, R.id.tvType, R.drawable.file);

        tvFromDatePicker.setOnClickListener(this);
        tvToDatePicker.setOnClickListener(this);

        tvFromDatePicker.setText(DateTimeHelper.convertLocalDateToString(mFromDate));
        tvToDatePicker.setText(DateTimeHelper.convertLocalDateToString(mToDate));

        if (null != savedInstanceState) {
            spFormats.setSelection(savedInstanceState.getInt(KEY_SELECTED_FORMAT_INT, 0));
        }

        if (null != mAsyncTask) {
            // To start progress dialog again.
            mAsyncTask.onPreExecute();
        }

        return mGridLayout;
    }

    @Override
    public void onSaveInstanceState(@NonNull Bundle outState) {
        super.onSaveInstanceState(outState);

        outState.putSerializable(KEY_FROM_DATE_SERIALIZABLE, mFromDate);
        outState.putSerializable(KEY_TO_DATE_SERIALIZABLE, mToDate);
        outState.putInt(KEY_SELECTED_FORMAT_INT, spFormats.getSelectedItemPosition());
    }

    @Override
    public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
        inflater.inflate(R.menu.menu_export_fragment, menu);

        if (!Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) {
            menu.findItem(R.id.action_save).setEnabled(false);
        }

        super.onCreateOptionsMenu(menu, inflater);
    }

    @Override
    public boolean onOptionsItemSelected(MenuItem item) {
        switch (item.getItemId()) {
            case R.id.action_save:
                if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
                    if (getActivity().checkSelfPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE)
                            != PackageManager.PERMISSION_GRANTED) {
                        getActivity().requestPermissions(
                                new String[]{
                                        Manifest.permission.WRITE_EXTERNAL_STORAGE
                                }, REQUEST_EXTERNAL_STORAGE);

                        return true;
                    }
                }

                try {
                    mAsyncTask = (BackupAsyncTask) new BackupToJSON(this, BackupAsyncTask.TO_EXTERNAL).execute();
                } catch (BackupException e) {
                    if (BuildConfig.CRASHLYTICS_ENABLED) {
                        Crashlytics.logException(e);
                    }
                    Log.e(TAG, "Save to external storage unsuccessful.", e);
                }

                return true;
            case R.id.action_share:
                try {
                    mAsyncTask = (BackupAsyncTask) new BackupToJSON(this, BackupAsyncTask.TO_SHARE).execute();
                } catch (BackupException e) {
                    if (BuildConfig.CRASHLYTICS_ENABLED) {
                        Crashlytics.logException(e);
                    }
                    Log.e(TAG, "Share unsuccessful.", e);
                }

                return true;
        }

        return super.onOptionsItemSelected(item);
    }

    @Override
    public void onAttach(Context context) {
        super.onAttach(context);
        if (context instanceof OnFragmentInteractionListener) {
            mListener = (OnFragmentInteractionListener) context;
        } else {
            throw new RuntimeException(context.toString()
                    + " must implement OnFragmentInteractionListener");
        }
    }

    @Override
    public void onDetach() {
        super.onDetach();
        mListener = null;
    }

    @Override
    public void onClick(final View v) {
        LocalDate date;
        if (v.getId() == R.id.tvFromDatePicker) {
            date = mFromDate;
        } else if (v.getId() == R.id.tvToDatePicker) {
            date = mToDate;
        } else {
            return;
        }

        DatePickerDialog datePicker = new FixedDatePickerDialog(getContext(),
                new DatePickerDialog.OnDateSetListener() {
                    @Override
                    public void onDateSet(DatePicker view, int year, int monthOfYear, int dayOfMonth) {
                        LocalDate newDate = new LocalDate(year, monthOfYear + 1, dayOfMonth);
                        Log.d(TAG, MessageFormat.format("date selected {0}", newDate));

                        if (v.getId() == R.id.tvFromDatePicker) {
                            mFromDate = newDate;
                            tvFromDatePicker.setText(DateTimeHelper.convertLocalDateToString(newDate));

                            if (mFromDate.isAfter(mToDate)) {
                                mToDate = mFromDate;
                                tvToDatePicker.setText(DateTimeHelper.convertLocalDateToString(newDate));
                            }
                        } else {
                            mToDate = newDate;
                            tvToDatePicker.setText(DateTimeHelper.convertLocalDateToString(newDate));

                            if (mToDate.isBefore(mFromDate)) {
                                mFromDate = mToDate;
                                tvFromDatePicker.setText(DateTimeHelper.convertLocalDateToString(newDate));
                            }
                        }
                    }
                }, date.getYear(), date.getMonthOfYear() - 1, date.getDayOfMonth()
        );

        datePicker.setButton(DatePickerDialog.BUTTON_POSITIVE, getString(android.R.string.ok), datePicker);
        datePicker.setButton(DatePickerDialog.BUTTON_NEGATIVE, getString(android.R.string.cancel), datePicker);

        datePicker.show();
    }

    @NonNull
    private LocalDate getFirstDate() {
        SQLiteDatabase db = null;
        Cursor cursor = null;
        try {
            db = mDatabaseHelper.getReadableDatabase();
            cursor = db.query(DatabaseHelper.DBT_EVENT,
                    new String[]{DatabaseHelper.DBC_EVENT_DATE,},
                    null, null, null, null, DatabaseHelper.DBC_EVENT_DATE, "1");

            if (cursor.moveToFirst()) {
                return LocalDate.parse(cursor.getString(0), DatabaseHelper.DB_DATE_FORMATTER);
            }
        } catch (SQLiteException e) {
            if (BuildConfig.CRASHLYTICS_ENABLED) {
                Crashlytics.logException(e);
            }
            Log.e(TAG, "Content cannot be prepared probably a DB issue.", e);
        } catch (Exception e) {
            if (BuildConfig.CRASHLYTICS_ENABLED) {
                Crashlytics.logException(e);
            }
            Log.e(TAG, "Content cannot be prepared.", e);
        } finally {
            if (null != cursor) {
                cursor.close();
            }

            if (null != db && db.isOpen()) {
                db.close();
            }
        }

        return LocalDate.now();
    }

    @Override
    public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
        if (requestCode == REQUEST_EXTERNAL_STORAGE
                && grantResults.length > 0
                && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
            try {
                switch (spFormats.getSelectedItemPosition()) {
                    case 0: // JSON
                        mAsyncTask = (BackupAsyncTask) new BackupToJSON(this, BackupAsyncTask.TO_EXTERNAL).execute();
                        break;
                    default:
                        throw new BackupException(getContext(), R.string.backup_unimplemented_destination);

                }

            } catch (BackupException e) {
                if (BuildConfig.CRASHLYTICS_ENABLED) {
                    Crashlytics.logException(e);
                }
                Log.e(TAG, "Save to external storage unsuccessful.", e);
            }
        } else {
            Toast.makeText(getContext(), R.string.backup_no_permission, Toast.LENGTH_LONG).show();
        }
    }

    public boolean isWiFiConnected() {
        WifiManager wifiMgr = (WifiManager) getContext().getApplicationContext().getSystemService(Context.WIFI_SERVICE);

        if (wifiMgr != null && wifiMgr.isWifiEnabled()) {

            WifiInfo wifiInfo = wifiMgr.getConnectionInfo();

            return !(wifiInfo != null && wifiInfo.getNetworkId() == -1);

        } else {
            return false;
        }
    }

    public interface OnFragmentInteractionListener {
        void onBackupComplete(Uri uri, LocalDate startDate, LocalDate endDate);

        void onShareComplete(Uri uri, LocalDate startDate, LocalDate endDate);
    }

} public class ResourcesHelper {
    @NonNull
    public static Resources getEngResources(@NonNull final Context context) {
        return getResources(context, Locale.ENGLISH);
    }

    @NonNull
    public static Resources getResources(@NonNull final Context context, @NonNull final Locale locale) {
        Resources standardResources = context.getResources();

        AssetManager assets = standardResources.getAssets();
        DisplayMetrics metrics = standardResources.getDisplayMetrics();
        Configuration config = new Configuration(standardResources.getConfiguration());
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
            config.setLocales(LocaleList.forLanguageTags(Locale.ENGLISH.toLanguageTag()));
        } else {
            //noinspection deprecation
            config.locale = locale;
        }

        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
            return context.createConfigurationContext(config).getResources();
        } else {
            //noinspection deprecation
            return new Resources(assets, metrics, config);
        }
    }

    public static void setLeftCompoundDrawable(@NonNull final View parent, @IdRes final int textView, @DrawableRes final int drawable) {
        setLeftCompoundDrawable(parent.getContext(), (TextView) parent.findViewById(textView), drawable);
    }

    public static void setLeftCompoundDrawable(@NonNull final Context context, @NonNull final TextView textView, @DrawableRes final int drawable) {
        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
            textView.setCompoundDrawablesWithIntrinsicBounds(
                    VectorDrawableCompat.create(context.getResources(), drawable, context.getTheme()), null, null, null);
        } else {
            textView.setCompoundDrawablesWithIntrinsicBounds(drawable, 0, 0, 0);
        }
    }
} public class Event implements Parcelable {
    public static final int TYPE_FOOD = 0;
    public static final int TYPE_DRINK = 1;
    public static final int TYPE_MED = 2;
    public static final int TYPE_SUPP = 3;
    public static final int TYPE_EXC = 4;
    public static final int TYPE_OTHER = 5;
    public static final Creator<Event> CREATOR = new Creator<Event>() {
        @Override
        public Event createFromParcel(Parcel in) {
            return new Event(in);
        }

        @Override
        public Event[] newArray(int size) {
            return new Event[size];
        }
    };
    private long mID;
    private LocalDate mDate;
    private LocalTime mTime;
    private int mType;
    private int mSubType;
    private String mDescription;

    public Event() {
        setID(-1);
    }

    public Event(long id, LocalDate date, LocalTime time, int type, int subType, String description) {
        setID(id);
        setDate(date);
        setTime(time);
        setType(type);
        setSubType(subType);
        setDescription(description);
    }

    protected Event(Parcel in) {
        mID = in.readLong();
        mDate = (LocalDate) in.readSerializable();
        mTime = (LocalTime) in.readSerializable();
        mType = in.readInt();
        mSubType = in.readInt();
        mDescription = in.readString();
    }

    public long getID() {
        return mID;
    }

    public void setID(long ID) {
        this.mID = ID;
    }

    public LocalDate getDate() {
        return mDate;
    }

    public void setDate(LocalDate date) {
        this.mDate = date;
    }

    public LocalTime getTime() {
        return mTime;
    }

    public void setTime(LocalTime time) {
        this.mTime = time;
    }

    public int getType() {
        return mType;
    }

    public void setType(int type) {
        this.mType = type;
    }

    public int getSubType() {
        return mSubType;
    }

    public void setSubType(int subType) {
        this.mSubType = subType;
    }

    public String getDescription() {
        return mDescription;
    }

    public void setDescription(String description) {
        this.mDescription = description;
    }

    @Override
    public int describeContents() {
        return 0;
    }

    @Override
    public void writeToParcel(Parcel dest, int flags) {
        dest.writeLong(mID);
        dest.writeSerializable(mDate);
        dest.writeSerializable(mTime);
        dest.writeInt(mType);
        dest.writeInt(mSubType);
        dest.writeString(mDescription);
    }

    @Override
    public boolean equals(final Object obj) {
        if (null != obj && obj instanceof Event) {
            final Event event = (Event) obj;
            if (getID() == event.getID()
                    && getType() == event.getType()
                    && getSubType() == event.getSubType()
                    && getDate().equals(event.getDate())
                    && getTime().equals(event.getTime())
                    && getDescription().equals(event.getDescription()))
                return true;
        }

        return false;
    }

    @Override
    public String toString() {
        return MessageFormat.format("[ID]={0,number}, [Date]={1}, [Time]={2}, [Type]={3,number}, [SubType]={4,number}, [Description]={5}",
                getID(),
                getDate().toString(DatabaseHelper.DB_DATE_FORMATTER),
                getTime().toString(DatabaseHelper.DB_TIME_FORMATTER),
                getType(),
                getSubType(),
                getDescription());
    }
} public static final String KEY_APP_ID = "Application ID"; public static final String KEY_BACKUP_FILE_DRIVE_ID_STRING = "Backup Settings - Drive ID"; public static final String KEY_BACKUP_LAST_BACKUP_TIMESTAMP_LONG = "Backup Settings - Last Backup"; public static final String KEY_GENERAL_CLOCK_MODE_STRING = "General Settings - Clock Mode"; // -1, 0, 1 public static final String KEY_NOTIFICATIONS_ACTIVE_BOOL = "Notification Settings - Active"; public static final String KEY_NOTIFICATIONS_DAILY_REMAINDER_BOOL = "Notification Settings - Daily Remainder - Active"; public static final String KEY_NOTIFICATIONS_DAILY_REMAINDER_TIME_STRING = "Notification Settings - Daily Remainder - Time"; // 19:00
import android.content.SharedPreferences;
import android.content.res.Resources;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.os.Build;
import android.support.annotation.NonNull;
import android.support.v7.preference.PreferenceManager;
import android.util.JsonWriter;
import android.util.Log;
import com.canyapan.dietdiaryapp.R;
import com.canyapan.dietdiaryapp.db.DatabaseHelper;
import com.canyapan.dietdiaryapp.db.EventHelper;
import com.canyapan.dietdiaryapp.fragments.BackupFragment;
import com.canyapan.dietdiaryapp.helpers.ResourcesHelper;
import com.canyapan.dietdiaryapp.models.Event;
import com.firebase.jobdispatcher.JobParameters;
import com.firebase.jobdispatcher.JobService;
import com.google.android.gms.auth.api.signin.GoogleSignIn;
import com.google.android.gms.auth.api.signin.GoogleSignInAccount;
import com.google.android.gms.common.api.Scope;
import com.google.android.gms.drive.Drive;
import com.google.android.gms.drive.DriveClient;
import com.google.android.gms.drive.DriveContents;
import com.google.android.gms.drive.DriveFile;
import com.google.android.gms.drive.DriveFolder;
import com.google.android.gms.drive.DriveId;
import com.google.android.gms.drive.DriveResourceClient;
import com.google.android.gms.drive.MetadataChangeSet;
import com.google.android.gms.drive.metadata.CustomPropertyKey;
import com.google.android.gms.tasks.Continuation;
import com.google.android.gms.tasks.OnFailureListener;
import com.google.android.gms.tasks.OnSuccessListener;
import com.google.android.gms.tasks.Task;
import com.google.android.gms.tasks.Tasks;
import com.jaredrummler.android.device.DeviceName;
import org.apache.commons.io.IOUtils;
import org.joda.time.DateTime;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.security.NoSuchAlgorithmException;
import java.text.MessageFormat;
import java.util.HashSet;
import java.util.Set;
import java.util.zip.CRC32;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;
import static com.canyapan.dietdiaryapp.preference.PreferenceKeys.KEY_APP_ID;
import static com.canyapan.dietdiaryapp.preference.PreferenceKeys.KEY_BACKUP_FILE_DRIVE_ID_STRING;
import static com.canyapan.dietdiaryapp.preference.PreferenceKeys.KEY_BACKUP_LAST_BACKUP_TIMESTAMP_LONG;
import static com.canyapan.dietdiaryapp.preference.PreferenceKeys.KEY_GENERAL_CLOCK_MODE_STRING;
import static com.canyapan.dietdiaryapp.preference.PreferenceKeys.KEY_NOTIFICATIONS_ACTIVE_BOOL;
import static com.canyapan.dietdiaryapp.preference.PreferenceKeys.KEY_NOTIFICATIONS_DAILY_REMAINDER_BOOL;
import static com.canyapan.dietdiaryapp.preference.PreferenceKeys.KEY_NOTIFICATIONS_DAILY_REMAINDER_TIME_STRING;
        OutputStreamWriter os = null;
        JsonWriter writer = null;
        try {
            os = new OutputStreamWriter(new FileOutputStream(outputFile, false), "UTF-8");
            os.write('\uFEFF'); // Unicode character, U+FEFF BYTE ORDER MARK (BOM) | https://en.wikipedia.org/wiki/Byte_order_mark

            writer = new JsonWriter(os);
            writer.setIndent("  ");

            writer.beginObject();

            writer.name("App").value("DietDiaryApp");
            writer.name("Ver").value(1);

            writeDeviceProperties(writer);
            writeSettings(writer);
            writeEvents(writer);

            writer.endObject();

            Log.d(TAG, "Backup file created.");
        } catch (IOException e) {
            Log.e(TAG, "Backup file creation failed.");
            throw e;
        } finally {
            if (null != writer) {
                try {
                    writer.flush();
                    writer.close();

                } catch (IOException e) {
                    Log.w(TAG, "JSON stream cannot be closed.", e);
                }
            } else if (null != os) { // Writer will close this normally.
                try {
                    os.flush();
                    os.close();

                } catch (IOException e) {
                    Log.w(TAG, "Output stream cannot be closed.", e);
                }
            }
        }
    }

    private void compressBackupFile(@NonNull final File outputFile, @NonNull final File inputFile) throws IOException, NoSuchAlgorithmException {
        if (!inputFile.exists()) {
            throw new IOException("Backup file is gone."); // :S
        }

        InputStream inputStream = null;
        ZipOutputStream zipOutputStream = null;

        try {
            inputStream = new FileInputStream(inputFile);
            zipOutputStream = new ZipOutputStream(new FileOutputStream(outputFile, false));

            zipOutputStream.setLevel(6);

            final ZipEntry zipEntry = new ZipEntry(BACKUP_FILE_NAME);
            zipEntry.setSize(inputFile.length());

            zipOutputStream.putNextEntry(zipEntry);

            CRC32 crc32 = new CRC32();
            crc32.reset();

            final byte[] buffer = new byte[1024];

            int len;
            while ((len = inputStream.read(buffer)) > 0) {
                zipOutputStream.write(buffer, 0, len);
                crc32.update(buffer, 0, len);
            }

            zipEntry.setCrc(crc32.getValue());

            zipOutputStream.closeEntry();
            zipOutputStream.finish();

            Log.d(TAG, "Compressed.");
        } catch (Exception e) {
            Log.e(TAG, "Compression failed.");
            throw e;
        } finally {
            if (null != zipOutputStream) {
                try {
                    zipOutputStream.flush();
                    zipOutputStream.close();
                } catch (IOException e) {
                    Log.w(TAG, "Compressed file stream cannot be closed.", e);
                }
            }

            if (null != inputStream) {
                try {
                    inputStream.close();
                } catch (IOException e) {
                    Log.w(TAG, "Input stream cannot be closed.", e);
                }
            }
        }
    }

    private void writeDeviceProperties(@NonNull final JsonWriter writer) throws IOException {
        // Write device properties
        writer.name("Device")
                .beginObject()
                .name("Name").value(DeviceName.getDeviceName())
                .name("Manufacturer").value(Build.MANUFACTURER)
                .name("Brand").value(Build.BRAND)
                .name("Product").value(Build.PRODUCT)
                .name("Model").value(Build.MODEL)
                .endObject();
    }

    private void writeSettings(@NonNull final JsonWriter writer) throws IOException {
        // Write settings
        writer.name("Settings").beginObject();

        if (mSharedPreferences.contains(KEY_GENERAL_CLOCK_MODE_STRING)) {