diff --git a/android_app/app/src/main/java/com/health/openscale/core/OpenScale.java b/android_app/app/src/main/java/com/health/openscale/core/OpenScale.java index 0727805f..a1913c39 100644 --- a/android_app/app/src/main/java/com/health/openscale/core/OpenScale.java +++ b/android_app/app/src/main/java/com/health/openscale/core/OpenScale.java @@ -22,6 +22,7 @@ import android.arch.persistence.room.RoomDatabase; import android.content.Context; import android.content.SharedPreferences; import android.database.Cursor; +import android.database.sqlite.SQLiteDatabaseCorruptException; import android.net.Uri; import android.os.Handler; import android.preference.PreferenceManager; @@ -54,6 +55,7 @@ import com.health.openscale.gui.views.MeasurementViewSettings; import com.health.openscale.gui.views.WaterMeasurementView; import java.io.BufferedReader; +import java.io.File; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; @@ -107,7 +109,7 @@ public class OpenScale { return instance; } - public void reopenDatabase() { + public void reopenDatabase() throws SQLiteDatabaseCorruptException { if (appDB != null) { appDB.close(); } @@ -393,6 +395,56 @@ public class OpenScale { } } + public void importDatabase(Uri importFile) throws IOException { + File exportFile = context.getApplicationContext().getDatabasePath("openScale.db"); + File tmpExportFile = context.getApplicationContext().getDatabasePath("openScale_tmp.db"); + + try { + copyFile(Uri.fromFile(exportFile), Uri.fromFile(tmpExportFile)); + copyFile(importFile, Uri.fromFile(exportFile)); + + reopenDatabase(); + + if (!getScaleUserList().isEmpty()) { + selectScaleUser(getScaleUserList().get(0).getId()); + updateScaleData(); + } + } catch (SQLiteDatabaseCorruptException e) { + copyFile(Uri.fromFile(tmpExportFile), Uri.fromFile(exportFile)); + throw new IOException(e.getMessage()); + } finally { + tmpExportFile.delete(); + } + } + + public void exportDatabase(Uri exportFile) throws IOException { + File dbFile = context.getApplicationContext().getDatabasePath("openScale.db"); + + copyFile(Uri.fromFile(dbFile), exportFile); + } + + private void copyFile(Uri src, Uri dst) throws IOException { + InputStream input = context.getContentResolver().openInputStream(src); + OutputStream output = context.getContentResolver().openOutputStream(dst); + + try { + byte[] bytes = new byte[4096]; + int count; + + while ((count = input.read(bytes)) != -1){ + output.write(bytes, 0, count); + } + } finally { + if (input != null) { + input.close(); + } + if (output != null) { + output.flush(); + output.close(); + } + } + } + public void importData(Uri uri) { try { final String filename = getFilenameFromUri(uri); diff --git a/android_app/app/src/main/java/com/health/openscale/core/alarm/AlarmBackupHandler.java b/android_app/app/src/main/java/com/health/openscale/core/alarm/AlarmBackupHandler.java new file mode 100644 index 00000000..d11ff7ac --- /dev/null +++ b/android_app/app/src/main/java/com/health/openscale/core/alarm/AlarmBackupHandler.java @@ -0,0 +1,113 @@ +/* Copyright (C) 2018 olie.xdev +* +* 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 +*/ +package com.health.openscale.core.alarm; + +import android.app.AlarmManager; +import android.app.PendingIntent; +import android.content.Context; +import android.content.Intent; +import android.content.SharedPreferences; +import android.net.Uri; +import android.os.Environment; +import android.preference.PreferenceManager; +import android.util.Log; + +import com.health.openscale.core.OpenScale; + +import java.io.File; +import java.io.IOException; +import java.text.DateFormat; +import java.util.Date; + +public class AlarmBackupHandler +{ + public static final String INTENT_EXTRA_BACKUP_ALARM = "alarmBackupIntent"; + private static final int ALARM_NOTIFICATION_ID = 0x02; + + public void scheduleAlarms(Context context) + { + disableAlarm(context); + + SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context); + + if (prefs.getBoolean("autoBackup", true)) { + + String backupSchedule = prefs.getString("autoBackup_Schedule", "Monthly"); + + long intervalDayMultiplicator = 0; + + switch (backupSchedule) { + case "Daily": + intervalDayMultiplicator = 1; + break; + case "Weekly": + intervalDayMultiplicator = 7; + break; + case "Monthly": + intervalDayMultiplicator = 30; + break; + } + + PendingIntent alarmPendingIntent = getPendingAlarmIntent(context); + AlarmManager alarmMgr = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE); + alarmMgr.setInexactRepeating(AlarmManager.RTC_WAKEUP, System.currentTimeMillis(), + AlarmManager.INTERVAL_DAY * intervalDayMultiplicator, alarmPendingIntent); + } + } + + private PendingIntent getPendingAlarmIntent(Context context) + { + Intent alarmIntent = new Intent(context, ReminderBootReceiver.class); + alarmIntent.putExtra(INTENT_EXTRA_BACKUP_ALARM, true); + + return PendingIntent.getBroadcast(context, ALARM_NOTIFICATION_ID, alarmIntent, PendingIntent.FLAG_UPDATE_CURRENT); + } + + public void disableAlarm(Context context) { + AlarmManager alarmMgr = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE); + + alarmMgr.cancel(getPendingAlarmIntent(context)); + } + + public void executeBackup(Context context) { + OpenScale openScale = OpenScale.getInstance(context); + + String databaseName = "openScale.db"; + + File exportDir = new File(Environment.getExternalStorageDirectory(), PreferenceManager.getDefaultSharedPreferences(context).getString("exportDir", "openScale Backup")); + if (!Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) + return; + + SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context); + + if (!prefs.getBoolean("overwriteBackup", false)) { + databaseName = DateFormat.getDateInstance(DateFormat.SHORT).format(new Date()) + "_" + databaseName; + } + + File exportFile = new File(exportDir, databaseName); + + if (!exportDir.exists()) { + exportDir.mkdirs(); + } + + try { + openScale.exportDatabase(Uri.fromFile(exportFile)); + Log.d("AlarmBackupHandler", "openScale Auto Backup to " + exportFile); + } catch (IOException e) { + Log.e("AlarmBackupHandler", "Error while exporting database: " + e.getMessage()); + } + } +} diff --git a/android_app/app/src/main/java/com/health/openscale/core/alarm/ReminderBootReceiver.java b/android_app/app/src/main/java/com/health/openscale/core/alarm/ReminderBootReceiver.java index 7b6f3d51..088edf78 100644 --- a/android_app/app/src/main/java/com/health/openscale/core/alarm/ReminderBootReceiver.java +++ b/android_app/app/src/main/java/com/health/openscale/core/alarm/ReminderBootReceiver.java @@ -27,6 +27,8 @@ public class ReminderBootReceiver extends BroadcastReceiver { if (intent.hasExtra(AlarmHandler.INTENT_EXTRA_ALARM)) handleAlarm(context); + if (intent.hasExtra(AlarmBackupHandler.INTENT_EXTRA_BACKUP_ALARM)) handleBackupAlarm(context); + if (Intent.ACTION_BOOT_COMPLETED.equals(intent.getAction())) scheduleAlarms(context); } @@ -36,9 +38,18 @@ public class ReminderBootReceiver extends BroadcastReceiver alarmHandler.showAlarmNotification(context); } + private void handleBackupAlarm(Context context) + { + AlarmBackupHandler alarmBackupHandler = new AlarmBackupHandler(); + alarmBackupHandler.executeBackup(context); + } + private void scheduleAlarms(Context context) { AlarmHandler alarmHandler = new AlarmHandler(); + AlarmBackupHandler alarmBackupHandler = new AlarmBackupHandler(); + alarmHandler.scheduleAlarms(context); + alarmBackupHandler.scheduleAlarms(context); } } diff --git a/android_app/app/src/main/java/com/health/openscale/gui/preferences/BackupPreferences.java b/android_app/app/src/main/java/com/health/openscale/gui/preferences/BackupPreferences.java index 5833be91..c60933cc 100644 --- a/android_app/app/src/main/java/com/health/openscale/gui/preferences/BackupPreferences.java +++ b/android_app/app/src/main/java/com/health/openscale/gui/preferences/BackupPreferences.java @@ -15,32 +15,48 @@ */ package com.health.openscale.gui.preferences; +import android.content.ComponentName; +import android.content.Intent; +import android.content.SharedPreferences; import android.content.pm.PackageManager; +import android.net.Uri; import android.os.Bundle; -import android.os.Environment; +import android.preference.CheckBoxPreference; import android.preference.EditTextPreference; +import android.preference.ListPreference; +import android.preference.MultiSelectListPreference; import android.preference.Preference; import android.preference.PreferenceFragment; +import android.preference.PreferenceGroup; import android.widget.Toast; import com.health.openscale.R; import com.health.openscale.core.OpenScale; -import com.health.openscale.core.datatypes.ScaleUser; +import com.health.openscale.core.alarm.AlarmBackupHandler; +import com.health.openscale.core.alarm.ReminderBootReceiver; import com.health.openscale.gui.utils.PermissionHelper; -import java.io.File; -import java.io.FileInputStream; -import java.io.FileOutputStream; import java.io.IOException; -import java.nio.channels.FileChannel; +import java.util.ArrayList; import java.util.List; +import java.util.Set; -public class BackupPreferences extends PreferenceFragment { - private static final String PREFERENCE_KEY_EXPORT_DIR = "exportDir"; +import static android.app.Activity.RESULT_OK; + +public class BackupPreferences extends PreferenceFragment implements SharedPreferences.OnSharedPreferenceChangeListener { private static final String PREFERENCE_KEY_IMPORT_BACKUP = "importBackup"; private static final String PREFERENCE_KEY_EXPORT_BACKUP = "exportBackup"; + private static final String PREFERENCE_KEY_AUTO_BACKUP = "autoBackup"; - private EditTextPreference exportDir; + private static final int IMPORT_DATA_REQUEST = 100; + private static final int EXPORT_DATA_REQUEST = 101; + + private Preference importBackup; + private Preference exportBackup; + + private CheckBoxPreference autoBackup; + + private boolean isAutoBackupAskForPermission; @Override public void onCreate(Bundle savedInstanceState) { @@ -48,150 +64,230 @@ public class BackupPreferences extends PreferenceFragment { addPreferencesFromResource(R.xml.backup_preferences); - exportDir = (EditTextPreference) findPreference(PREFERENCE_KEY_EXPORT_DIR); - exportDir.setSummary(exportDir.getText()); - exportDir.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() { - @Override - public boolean onPreferenceChange(Preference preference, Object newValue) { - preference.setSummary((String) newValue); - return true; - } - }); + importBackup = (Preference) findPreference(PREFERENCE_KEY_IMPORT_BACKUP); + importBackup.setOnPreferenceClickListener(new onClickListenerImportBackup()); - Preference importBackup = findPreference(PREFERENCE_KEY_IMPORT_BACKUP); - importBackup.setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() { - @Override - public boolean onPreferenceClick(Preference preference) { - if (PermissionHelper.requestReadPermission(getActivity())) { - importBackup(); - } - return true; - } - }); + exportBackup = (Preference) findPreference(PREFERENCE_KEY_EXPORT_BACKUP); + exportBackup.setOnPreferenceClickListener(new onClickListenerExportBackup()); - Preference exportBackup = findPreference(PREFERENCE_KEY_EXPORT_BACKUP); - exportBackup.setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() { - @Override - public boolean onPreferenceClick(Preference preference) { - if (PermissionHelper.requestWritePermission(getActivity())) { - exportBackup(); - } - return true; - } - }); + autoBackup = (CheckBoxPreference) findPreference(PREFERENCE_KEY_AUTO_BACKUP); + autoBackup.setOnPreferenceClickListener(new onClickListenerAutoBackup()); + + initSummary(getPreferenceScreen()); + updateBackupPreferences(); } - private boolean isExternalStoragePresent() { - return Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED); + void updateBackupPreferences() { + ComponentName receiver = new ComponentName(getActivity().getApplicationContext(), ReminderBootReceiver.class); + PackageManager pm = getActivity().getApplicationContext().getPackageManager(); + + AlarmBackupHandler alarmBackupHandler = new AlarmBackupHandler(); + + isAutoBackupAskForPermission = false; + + if (autoBackup.isChecked()) { + alarmBackupHandler.scheduleAlarms(getActivity()); + + pm.setComponentEnabledSetting(receiver, PackageManager.COMPONENT_ENABLED_STATE_ENABLED, + PackageManager.DONT_KILL_APP); + } else { + alarmBackupHandler.disableAlarm(getActivity()); + + pm.setComponentEnabledSetting(receiver, PackageManager.COMPONENT_ENABLED_STATE_DISABLED, + PackageManager.DONT_KILL_APP); + } } - private File getExportDir() { - if (!isExternalStoragePresent()) { - return null; + @Override + public void onResume() + { + super.onResume(); + getPreferenceManager().getSharedPreferences().registerOnSharedPreferenceChangeListener(this); + } + + @Override + public void onPause() + { + getPreferenceManager().getSharedPreferences().unregisterOnSharedPreferenceChangeListener(this); + super.onPause(); + } + + @Override + public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) + { + updatePrefSummary(findPreference(key)); + updateBackupPreferences(); + } + + private void initSummary(Preference p) { + if (p instanceof PreferenceGroup) { + PreferenceGroup pGrp = (PreferenceGroup) p; + for (int i = 0; i < pGrp.getPreferenceCount(); i++) { + initSummary(pGrp.getPreference(i)); + } + } else { + updatePrefSummary(p); + } + } + + private void updatePrefSummary(Preference p) + { + if (p instanceof ListPreference) + { + ListPreference listPref = (ListPreference) p; + p.setSummary(listPref.getEntry()); } - return new File(Environment.getExternalStorageDirectory(), exportDir.getText()); + if (p instanceof EditTextPreference) + { + EditTextPreference editTextPref = (EditTextPreference) p; + if (p.getTitle().toString().contains("assword")) + { + p.setSummary("******"); + } + else + { + p.setSummary(editTextPref.getText()); + } + } + + if (p instanceof MultiSelectListPreference) + { + MultiSelectListPreference editMultiListPref = (MultiSelectListPreference) p; + + CharSequence[] entries = editMultiListPref.getEntries(); + CharSequence[] entryValues = editMultiListPref.getEntryValues(); + List currentEntries = new ArrayList<>(); + Set currentEntryValues = editMultiListPref.getValues(); + + for (int i = 0; i < entries.length; i++) + { + if (currentEntryValues.contains(entryValues[i].toString())) currentEntries.add(entries[i].toString()); + } + + p.setSummary(currentEntries.toString()); + } + } + + private class onClickListenerAutoBackup implements Preference.OnPreferenceClickListener { + @Override + public boolean onPreferenceClick(Preference preference) { + if (autoBackup.isChecked()) { + isAutoBackupAskForPermission = true; + + PermissionHelper.requestWritePermission(getActivity()); + } + + return true; + } + } + + private class onClickListenerImportBackup implements Preference.OnPreferenceClickListener { + @Override + public boolean onPreferenceClick(Preference preference) { + if (PermissionHelper.requestReadPermission(getActivity())) { + importBackup(); + } + + return true; + } + } + + private class onClickListenerExportBackup implements Preference.OnPreferenceClickListener { + @Override + public boolean onPreferenceClick(Preference preference) { + if (PermissionHelper.requestWritePermission(getActivity())) { + exportBackup(); + } + + return true; + } + } + + @Override + public void onActivityResult(int requestCode, int resultCode, Intent data) { + super.onActivityResult(requestCode, resultCode, data); + + if (resultCode != RESULT_OK || data == null) { + return; + } + + OpenScale openScale = OpenScale.getInstance(getActivity().getApplicationContext()); + + switch (requestCode) { + case IMPORT_DATA_REQUEST: + Uri importURI = data.getData(); + + try { + openScale.importDatabase(importURI); + Toast.makeText(getActivity().getApplicationContext(), getResources().getString(R.string.info_data_imported) + " " + importURI.getPath(), Toast.LENGTH_SHORT).show(); + } catch (IOException e) { + Toast.makeText(getActivity().getApplicationContext(), getResources().getString(R.string.error_importing) + " " + e.getMessage(), Toast.LENGTH_LONG).show(); + return; + } + break; + + case EXPORT_DATA_REQUEST: + Uri exportURI = data.getData(); + + try { + openScale.exportDatabase(exportURI); + Toast.makeText(getActivity().getApplicationContext(), getResources().getString(R.string.info_data_exported) + " " + exportURI.getPath(), Toast.LENGTH_SHORT).show(); + } catch (IOException e) { + Toast.makeText(getActivity().getApplicationContext(), getResources().getString(R.string.error_exporting) + " " + e.getMessage(), Toast.LENGTH_LONG).show(); + return; + } + break; + } } private boolean importBackup() { - File exportDir = getExportDir(); - if (exportDir == null) { - return false; - } + Intent intent = new Intent(Intent.ACTION_GET_CONTENT); + intent.addCategory(Intent.CATEGORY_OPENABLE); + intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); + intent.setType("*/*"); - File dbFile = getActivity().getDatabasePath(OpenScale.DATABASE_NAME); - File importFile = new File(exportDir, OpenScale.DATABASE_NAME); - - if (!importFile.exists()) { - Toast.makeText(getActivity(), getResources().getString(R.string.error_importing) - + " " + importFile.getPath() + " " - + getResources().getString(R.string.label_not_found), Toast.LENGTH_SHORT).show(); - return false; - } - - try { - dbFile.createNewFile(); - copyFile(importFile, dbFile); - - Toast.makeText(getActivity(), getResources().getString(R.string.info_data_imported) - + " " + importFile.getPath(), Toast.LENGTH_SHORT).show(); - } catch (IOException e) { - Toast.makeText(getActivity(), getResources().getString(R.string.error_importing) - + " " + e.getMessage(), Toast.LENGTH_SHORT).show(); - return false; - } - - OpenScale openScale = OpenScale.getInstance(getActivity()); - openScale.reopenDatabase(); - - List scaleUserList = openScale.getScaleUserList(); - - if (!scaleUserList.isEmpty()) { - openScale.selectScaleUser(scaleUserList.get(0).getId()); - openScale.updateScaleData(); - } + startActivityForResult( + Intent.createChooser(intent, getResources().getString(R.string.label_import)), + IMPORT_DATA_REQUEST); return true; } private boolean exportBackup() { - File exportDir = getExportDir(); - if (exportDir == null) { - return false; - } + Intent intent = new Intent(Intent.ACTION_CREATE_DOCUMENT); + intent.addCategory(Intent.CATEGORY_OPENABLE); + intent.addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION); + intent.setType("*/*"); - // Make sure all changes are written to the file before exporting - OpenScale.getInstance(getActivity()).reopenDatabase(); - - File dbFile = getActivity().getDatabasePath(OpenScale.DATABASE_NAME); - File file = new File(exportDir, OpenScale.DATABASE_NAME); - - if (!exportDir.exists()) { - exportDir.mkdirs(); - } - - try { - file.createNewFile(); - copyFile(dbFile, file); - - Toast.makeText(getActivity(), getResources().getString(R.string.info_data_exported) - + " " + file.getPath(), Toast.LENGTH_SHORT).show(); - } catch (IOException e) { - Toast.makeText(getActivity(), getResources().getString(R.string.error_exporting) - + " " + e.getMessage(), Toast.LENGTH_SHORT).show(); - return false; - } + startActivityForResult(intent, EXPORT_DATA_REQUEST); return true; } - private void copyFile(File src, File dst) throws IOException { - FileChannel inChannel = new FileInputStream(src).getChannel(); - FileChannel outChannel = new FileOutputStream(dst).getChannel(); - try { - inChannel.transferTo(0, inChannel.size(), outChannel); - } finally { - if (inChannel != null) - inChannel.close(); - if (outChannel != null) - outChannel.close(); - } - } - public void onMyOwnRequestPermissionsResult(int requestCode, String permissions[], int[] grantResults) { switch (requestCode) { case PermissionHelper.PERMISSIONS_REQUEST_ACCESS_READ_STORAGE: if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) { importBackup(); } else { - Toast.makeText(getActivity(), R.string.permission_not_granted, Toast.LENGTH_SHORT).show(); + Toast.makeText(getActivity().getApplicationContext(), getResources().getString(R.string.permission_not_granted), Toast.LENGTH_SHORT).show(); } break; case PermissionHelper.PERMISSIONS_REQUEST_ACCESS_WRITE_STORAGE: if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) { - exportBackup(); + if (isAutoBackupAskForPermission) { + autoBackup.setChecked(true); + } else { + exportBackup(); + } + } else { - Toast.makeText(getActivity(), R.string.permission_not_granted, Toast.LENGTH_SHORT).show(); + if (isAutoBackupAskForPermission) { + autoBackup.setChecked(false); + } + + Toast.makeText(getActivity().getApplicationContext(), getResources().getString(R.string.permission_not_granted), Toast.LENGTH_SHORT).show(); } break; } diff --git a/android_app/app/src/main/res/values/strings.xml b/android_app/app/src/main/res/values/strings.xml index 99b9d15e..a9fcb3c1 100644 --- a/android_app/app/src/main/res/values/strings.xml +++ b/android_app/app/src/main/res/values/strings.xml @@ -209,6 +209,12 @@ Percent Estimated Based on weight, height, age, gender, etc. + Auto backup + Backup schedule + Overwrite previous backup + daily + weekly + monthly Is your scale not supported? Click here to help add support for it diff --git a/android_app/app/src/main/res/values/type_auto_backup.xml b/android_app/app/src/main/res/values/type_auto_backup.xml new file mode 100644 index 00000000..cfa41c7f --- /dev/null +++ b/android_app/app/src/main/res/values/type_auto_backup.xml @@ -0,0 +1,16 @@ + + + + @string/label_daily + @string/label_weekly + @string/label_monthly + + + + Daily + Weekly + Monthly + + + + \ No newline at end of file diff --git a/android_app/app/src/main/res/xml/backup_preferences.xml b/android_app/app/src/main/res/xml/backup_preferences.xml index b57ae20d..d3c8b4ec 100644 --- a/android_app/app/src/main/res/xml/backup_preferences.xml +++ b/android_app/app/src/main/res/xml/backup_preferences.xml @@ -1,6 +1,16 @@ - + + + + + \ No newline at end of file