diff --git a/app/build.gradle b/app/build.gradle index d2d6dfecbb..54aa94ccc0 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -190,6 +190,7 @@ dependencies { libs "MilosKozak:danars-support-lib:master@zip" implementation "com.android.support:appcompat-v7:${supportLibraryVersion}" + implementation "com.android.support:support-v13:${supportLibraryVersion}" implementation "com.android.support:support-v4:${supportLibraryVersion}" implementation "com.android.support:cardview-v7:${supportLibraryVersion}" implementation "com.android.support:recyclerview-v7:${supportLibraryVersion}" diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 213bde1916..bbe042213a 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -127,6 +127,16 @@ + + + + { - MainApp.getDbHelper().resetDatabases(); - // should be handled by Plugin-Interface and - // additional service interface and plugin registry - FoodPlugin.getPlugin().getService().resetFood(); - TreatmentsPlugin.getPlugin().getService().resetTreatments(); - }) - .create() - .show(); - return true; - case R.id.nav_export: - ImportExportPrefs.verifyStoragePermissions(this); - ImportExportPrefs.exportSharedPreferences(this); - return true; - case R.id.nav_import: - ImportExportPrefs.verifyStoragePermissions(this); - ImportExportPrefs.importSharedPreferences(this); - return true; case R.id.nav_show_logcat: LogDialog.showLogcat(this); return true; diff --git a/app/src/main/java/info/nightscout/androidaps/MainApp.java b/app/src/main/java/info/nightscout/androidaps/MainApp.java index e1cc242e7d..b16f3bc570 100644 --- a/app/src/main/java/info/nightscout/androidaps/MainApp.java +++ b/app/src/main/java/info/nightscout/androidaps/MainApp.java @@ -23,7 +23,6 @@ import org.slf4j.LoggerFactory; import java.io.File; import java.util.ArrayList; -import ch.qos.logback.classic.LoggerContext; import info.nightscout.androidaps.data.ConstraintChecker; import info.nightscout.androidaps.db.DatabaseHelper; import info.nightscout.androidaps.interfaces.PluginBase; @@ -40,6 +39,7 @@ import info.nightscout.androidaps.plugins.Insulin.InsulinOrefFreePeakPlugin; import info.nightscout.androidaps.plugins.Insulin.InsulinOrefRapidActingPlugin; import info.nightscout.androidaps.plugins.Insulin.InsulinOrefUltraRapidActingPlugin; import info.nightscout.androidaps.plugins.IobCobCalculator.IobCobCalculatorPlugin; +import info.nightscout.androidaps.plugins.Maintenance.MaintenancePlugin; import info.nightscout.androidaps.plugins.Loop.LoopPlugin; import info.nightscout.androidaps.plugins.NSClientInternal.NSClientPlugin; import info.nightscout.androidaps.plugins.NSClientInternal.NSUpload; @@ -79,6 +79,7 @@ import info.nightscout.androidaps.receivers.KeepAliveReceiver; import info.nightscout.androidaps.receivers.NSAlarmReceiver; import info.nightscout.androidaps.services.Intents; import info.nightscout.utils.FabricPrivacy; +import info.nightscout.utils.LoggerUtils; import io.fabric.sdk.android.Fabric; @@ -128,7 +129,7 @@ public class MainApp extends Application { log.info("Version: " + BuildConfig.VERSION_NAME); log.info("BuildVersion: " + BuildConfig.BUILDVERSION); - String extFilesDir = this.getLogDirectory(); + String extFilesDir = LoggerUtils.getLogDirectory(); File engineeringModeSemaphore = new File(extFilesDir, "engineering_mode"); engineeringMode = engineeringModeSemaphore.exists() && engineeringModeSemaphore.isFile(); @@ -190,6 +191,7 @@ public class MainApp extends Application { pluginsList.add(StatuslinePlugin.initPlugin(this)); pluginsList.add(PersistentNotificationPlugin.getPlugin()); pluginsList.add(NSClientPlugin.getPlugin()); + pluginsList.add(MaintenancePlugin.initPlugin(this)); pluginsList.add(sConfigBuilder = ConfigBuilderPlugin.getPlugin()); @@ -389,11 +391,6 @@ public class MainApp extends Application { return devBranch; } - public String getLogDirectory() { - LoggerContext lc = (LoggerContext) LoggerFactory.getILoggerFactory(); - return lc.getProperty("EXT_FILES_DIR"); - } - @Override public void onTerminate() { if (L.isEnabled(L.CORE)) diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/Maintenance/MaintenanceFragment.java b/app/src/main/java/info/nightscout/androidaps/plugins/Maintenance/MaintenanceFragment.java new file mode 100644 index 0000000000..cda9e3d861 --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/Maintenance/MaintenanceFragment.java @@ -0,0 +1,83 @@ +package info.nightscout.androidaps.plugins.Maintenance; + +import android.os.Bundle; +import android.support.v4.app.Fragment; +import android.support.v7.app.AlertDialog; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; + +import info.nightscout.androidaps.MainApp; +import info.nightscout.androidaps.R; +import info.nightscout.androidaps.plugins.Food.FoodPlugin; +import info.nightscout.androidaps.plugins.Treatments.TreatmentsPlugin; +import info.nightscout.utils.ImportExportPrefs; + +/** + * + */ +public class MaintenanceFragment extends Fragment { + + private Fragment f; + + @Override + public void onResume() { + super.onResume(); + + this.f = this; + } + + @Override + public void onPause() { + super.onPause(); + + this.f = null; + } + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, + Bundle savedInstanceState) { + View view = inflater.inflate(R.layout.maintenance_fragment, container, false); + + final Fragment f = this; + + view.findViewById(R.id.log_send).setOnClickListener(view1 -> MaintenancePlugin.getPlugin().sendLogs()); + + view.findViewById(R.id.log_delete).setOnClickListener(view12 -> MaintenancePlugin.getPlugin().deleteLogs()); + + view.findViewById(R.id.nav_resetdb).setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View view) { + new AlertDialog.Builder(f.getContext()) + .setTitle(R.string.nav_resetdb) + .setMessage(R.string.reset_db_confirm) + .setNegativeButton(android.R.string.cancel, null) + .setPositiveButton(android.R.string.ok, (dialog, which) -> { + MainApp.getDbHelper().resetDatabases(); + // should be handled by Plugin-Interface and + // additional service interface and plugin registry + FoodPlugin.getPlugin().getService().resetFood(); + TreatmentsPlugin.getPlugin().getService().resetTreatments(); + }) + .create() + .show(); + } + }); + + view.findViewById(R.id.nav_export).setOnClickListener(view13 -> { + // start activity for checking permissions... + ImportExportPrefs.verifyStoragePermissions(f); + ImportExportPrefs.exportSharedPreferences(f); + }); + + view.findViewById(R.id.nav_import).setOnClickListener(view14 -> { + // start activity for checking permissions... + ImportExportPrefs.verifyStoragePermissions(f); + ImportExportPrefs.importSharedPreferences(f); + }); + + + return view; + } + +} diff --git a/app/src/main/java/info/nightscout/androidaps/plugins/Maintenance/MaintenancePlugin.java b/app/src/main/java/info/nightscout/androidaps/plugins/Maintenance/MaintenancePlugin.java new file mode 100644 index 0000000000..481a65b262 --- /dev/null +++ b/app/src/main/java/info/nightscout/androidaps/plugins/Maintenance/MaintenancePlugin.java @@ -0,0 +1,237 @@ +package info.nightscout.androidaps.plugins.Maintenance; + +import android.content.Context; +import android.content.Intent; +import android.net.Uri; +import android.support.v4.content.FileProvider; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.BufferedInputStream; +import java.io.BufferedOutputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.FilenameFilter; +import java.io.IOException; +import java.util.Arrays; +import java.util.Comparator; +import java.util.Date; +import java.util.List; +import java.util.zip.ZipEntry; +import java.util.zip.ZipOutputStream; + +import info.nightscout.androidaps.R; +import info.nightscout.androidaps.interfaces.PluginBase; +import info.nightscout.androidaps.interfaces.PluginDescription; +import info.nightscout.androidaps.interfaces.PluginType; +import info.nightscout.utils.LoggerUtils; +import info.nightscout.utils.SP; + +public class MaintenancePlugin extends PluginBase { + + private static final Logger LOG = LoggerFactory.getLogger(MaintenancePlugin.class); + + private final Context ctx; + + private static MaintenancePlugin maintenancePlugin; + + public static MaintenancePlugin getPlugin() { + return maintenancePlugin; + } + + public static MaintenancePlugin initPlugin(Context ctx) { + + if (maintenancePlugin == null) { + maintenancePlugin = new MaintenancePlugin(ctx); + } + + return maintenancePlugin; + } + + public MaintenancePlugin() { + // required for testing + super(null); + this.ctx = null; + } + + MaintenancePlugin(Context ctx) { + super(new PluginDescription() + .mainType(PluginType.GENERAL) + .fragmentClass(MaintenanceFragment.class.getName()) + .alwayVisible(true) + .alwaysEnabled(true) + .pluginName(R.string.maintenance) + .shortName(R.string.maintenance_shortname) + .preferencesId(R.xml.pref_maintenance) + .description(R.string.description_maintenance) + ); + this.ctx = ctx; + } + + public void sendLogs() { + String recipient = SP.getString("key_maintenance_logs_email", "logs@androidaps.org"); + int amount = SP.getInt("key_maintenance_logs_amount", 2); + + String logDirectory = LoggerUtils.getLogDirectory(); + List logs = this.getLogfiles(logDirectory, amount); + + File zipDir = this.ctx.getExternalFilesDir("exports"); + File zipFile = new File(zipDir, this.constructName()); + + LOG.debug("zipFile: {}", zipFile.getAbsolutePath()); + File zip = this.zipLogs(zipFile, logs); + + Uri attachementUri = FileProvider.getUriForFile(this.ctx, "info.nightscout.androidaps.fileprovider", zip); + Intent emailIntent = this.sendMail(attachementUri, recipient, "Log Export"); + LOG.debug("sending emailIntent"); + ctx.startActivity(emailIntent); + } + + //todo replace this with a call on startup of the application, specifically to remove + // unnecessary garbage from the log exports + public void deleteLogs() { + String logDirectory = LoggerUtils.getLogDirectory(); + File logDir = new File(logDirectory); + + File[] files = logDir.listFiles((file, name) -> name.startsWith("AndroidAPS") + && name.endsWith(".zip")); + + Arrays.sort(files, (f1, f2) -> f1.getName().compareTo(f2.getName())); + + List delFiles = Arrays.asList(files); + int amount = SP.getInt("key_logshipper_amount", 2); + int keepIndex = amount - 1; + + if (keepIndex < delFiles.size()) { + delFiles = delFiles.subList(keepIndex, delFiles.size()); + + for (File file : delFiles) { + file.delete(); + } + } + + File exportDir = new File(logDirectory, "exports"); + + if (exportDir.exists()) { + File[] expFiles = exportDir.listFiles(); + + for (File file : expFiles) { + file.delete(); + } + exportDir.delete(); + } + } + + /** + * returns a list of log files. The number of returned logs is given via the amount + * parameter. + * + * The log files are sorted by the name descending. + * + * @param directory + * @param amount + * @return + */ + public List getLogfiles(String directory, int amount) { + LOG.debug("getting {} logs from directory {}", amount, directory); + File logDir = new File(directory); + + File[] files = logDir.listFiles((file, name) -> name.startsWith("AndroidAPS") + && (name.endsWith(".log") + || (name.endsWith(".zip") && !name.endsWith(LoggerUtils.SUFFIX)))); + + Arrays.sort(files, (f1, f2) -> f2.getName().compareTo(f1.getName())); + + List result = Arrays.asList(files); + int toIndex = amount++; + + if (toIndex > result.size()) { + toIndex = result.size(); + } + + LOG.debug("returning sublist 0 to {}", toIndex); + return result.subList(0, toIndex); + } + + public File zipLogs(File zipFile, List files) { + LOG.debug("creating zip {}", zipFile.getAbsolutePath()); + + try { + zip(zipFile, files); + } catch (IOException e) { + LOG.error("Cannot retrieve zip", e); + } + + return zipFile; + } + + /** + * construct the name of zip file which is used to export logs. + * + * The name is constructed using the following scheme: + * AndroidAPS_LOG_ + Long Time + .log.zip + * + * @return + */ + public String constructName() { + return "AndroidAPS_LOG_" + String.valueOf(new Date().getTime()) + LoggerUtils.SUFFIX; + } + + public void zip(File zipFile, List files) throws IOException { + final int BUFFER_SIZE = 2048; + + ZipOutputStream out = new ZipOutputStream(new BufferedOutputStream(new FileOutputStream(zipFile))); + + for (File file : files) { + byte data[] = new byte[BUFFER_SIZE]; + + try(FileInputStream fileInputStream = new FileInputStream( file )) { + + try(BufferedInputStream origin = new BufferedInputStream(fileInputStream, BUFFER_SIZE)) { + ZipEntry entry = new ZipEntry(file.getName()); + + out.putNextEntry(entry); + int count; + while ((count = origin.read(data, 0, BUFFER_SIZE)) != -1) { + out.write(data, 0, count); + } + + } + } + } + + out.close(); + } + + /** + * send a mail with the given file to the recipients with the given subject. + * + * the returned intent should be used to really send the mail using + * + * startActivity(Intent.createChooser(emailIntent , "Send email...")); + * + * @param attachementUri + * @param recipient + * @param subject + * @return + */ + public static Intent sendMail(Uri attachementUri, String recipient, String subject) { + LOG.debug("sending email to {} with subject {}", recipient, subject); + Intent emailIntent = new Intent(Intent.ACTION_SEND); + + emailIntent.setType("text/plain"); + emailIntent.putExtra(Intent.EXTRA_EMAIL , new String[]{recipient}); + emailIntent.putExtra(Intent.EXTRA_SUBJECT, subject); + emailIntent.putExtra(Intent.EXTRA_TEXT, ""); + + LOG.debug("put path {}", attachementUri.toString()); + emailIntent.putExtra(Intent.EXTRA_STREAM, attachementUri); + emailIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + + return emailIntent; + } + + +} diff --git a/app/src/main/java/info/nightscout/utils/ImportExportPrefs.java b/app/src/main/java/info/nightscout/utils/ImportExportPrefs.java index 22175e0281..0cef5817a9 100644 --- a/app/src/main/java/info/nightscout/utils/ImportExportPrefs.java +++ b/app/src/main/java/info/nightscout/utils/ImportExportPrefs.java @@ -3,12 +3,15 @@ package info.nightscout.utils; import android.Manifest; import android.app.Activity; import android.app.AlertDialog; +import android.content.Context; import android.content.DialogInterface; import android.content.SharedPreferences; import android.content.pm.PackageManager; import android.os.Environment; import android.preference.PreferenceManager; import android.support.v4.app.ActivityCompat; +import android.support.v4.app.Fragment; +import android.support.v4.content.ContextCompat; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -45,7 +48,6 @@ public class ImportExportPrefs { // Check if we have write permission int permission = ActivityCompat.checkSelfPermission(activity, Manifest.permission.WRITE_EXTERNAL_STORAGE); - if (permission != PackageManager.PERMISSION_GRANTED) { // We don't have permission so prompt the user ActivityCompat.requestPermissions( @@ -56,7 +58,22 @@ public class ImportExportPrefs { } } - public static void exportSharedPreferences(final Activity c) { + public static void verifyStoragePermissions(Fragment fragment) { + int permission = ContextCompat.checkSelfPermission(fragment.getContext(), + Manifest.permission.WRITE_EXTERNAL_STORAGE); + + if (permission != PackageManager.PERMISSION_GRANTED) { + // We don't have permission so prompt the user + fragment.requestPermissions(PERMISSIONS_STORAGE, REQUEST_EXTERNAL_STORAGE); + } + + } + + public static void exportSharedPreferences(final Fragment f) { + exportSharedPreferences(f.getContext()); + } + + public static void exportSharedPreferences(final Context c) { new AlertDialog.Builder(c) .setMessage(MainApp.gs(R.string.export_to) + " " + file + " ?") @@ -86,13 +103,17 @@ public class ImportExportPrefs { .show(); } - public static void importSharedPreferences(final Activity c) { - new AlertDialog.Builder(c) + public static void importSharedPreferences(final Fragment fragment) { + importSharedPreferences(fragment.getContext()); + } + + public static void importSharedPreferences(final Context context) { + new AlertDialog.Builder(context) .setMessage(MainApp.gs(R.string.import_from) + " " + file + " ?") .setPositiveButton(android.R.string.yes, new DialogInterface.OnClickListener() { public void onClick(DialogInterface dialog, int which) { - SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(c); + SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context); SharedPreferences.Editor editor = prefs.edit(); String line; String[] lineParts; @@ -113,20 +134,20 @@ public class ImportExportPrefs { } reader.close(); editor.commit(); - OKDialog.show(c, MainApp.gs(R.string.setting_imported), MainApp.gs(R.string.restartingapp), new Runnable() { + OKDialog.show(context, MainApp.gs(R.string.setting_imported), MainApp.gs(R.string.restartingapp), new Runnable() { @Override public void run() { log.debug("Exiting"); MainApp.instance().stopKeepAliveService(); MainApp.bus().post(new EventAppExit()); MainApp.closeDbHelper(); - c.finish(); +// context.finish(); System.runFinalization(); System.exit(0); } }); } catch (FileNotFoundException e) { - ToastUtils.showToastInUiThread(c, MainApp.gs(R.string.filenotfound) + " " + file); + ToastUtils.showToastInUiThread(context, MainApp.gs(R.string.filenotfound) + " " + file); log.error("Unhandled exception", e); } catch (IOException e) { log.error("Unhandled exception", e); diff --git a/app/src/main/java/info/nightscout/utils/LoggerUtils.java b/app/src/main/java/info/nightscout/utils/LoggerUtils.java new file mode 100644 index 0000000000..60d9251181 --- /dev/null +++ b/app/src/main/java/info/nightscout/utils/LoggerUtils.java @@ -0,0 +1,28 @@ +package info.nightscout.utils; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import ch.qos.logback.classic.LoggerContext; + +/** + * This class provides serveral methods for log-handling (eg. sending logs as emails). + */ +public class LoggerUtils { + + private static final Logger LOGGER = LoggerFactory.getLogger(LoggerUtils.class); + + public static String SUFFIX = ".log.zip"; + + /** + * Returns the directory, in which the logs are stored on the system. This is configured in the + * logback.xml file. + * + * @return + */ + public static String getLogDirectory() { + LoggerContext lc = (LoggerContext) LoggerFactory.getILoggerFactory(); + return lc.getProperty("EXT_FILES_DIR"); + } + +} diff --git a/app/src/main/java/info/nightscout/utils/OKDialog.java b/app/src/main/java/info/nightscout/utils/OKDialog.java index 5185049678..a432cf27fc 100644 --- a/app/src/main/java/info/nightscout/utils/OKDialog.java +++ b/app/src/main/java/info/nightscout/utils/OKDialog.java @@ -1,7 +1,9 @@ package info.nightscout.utils; import android.app.Activity; +import android.content.Context; import android.content.DialogInterface; +import android.os.Handler; import android.os.SystemClock; import android.support.v7.app.AlertDialog; import android.support.v7.view.ContextThemeWrapper; @@ -21,8 +23,12 @@ public class OKDialog { private static Logger log = LoggerFactory.getLogger(OKDialog.class); public static void show(final Activity activity, String title, String message, final Runnable runnable) { + show(activity, title, message, runnable); + } + + public static void show(final Context context, String title, String message, final Runnable runnable) { try { - AlertDialog.Builder builder = new AlertDialog.Builder(new ContextThemeWrapper(activity, R.style.AppTheme)); + AlertDialog.Builder builder = new AlertDialog.Builder(new ContextThemeWrapper(context, R.style.AppTheme)); builder.setTitle(title); builder.setMessage(message); builder.setPositiveButton(MainApp.gs(R.string.ok), new DialogInterface.OnClickListener() { @@ -30,7 +36,7 @@ public class OKDialog { dialog.dismiss(); if (runnable != null) { SystemClock.sleep(100); - activity.runOnUiThread(runnable); + runOnUiThread(runnable); } } }); @@ -41,6 +47,11 @@ public class OKDialog { } } + public static boolean runOnUiThread(Runnable theRunnable) { + final Handler mainHandler = new Handler(MainApp.instance().getApplicationContext().getMainLooper()); + return mainHandler.post(theRunnable); + } + public static void show(final Activity activity, String title, Spanned message, final Runnable runnable) { try { AlertDialog.Builder builder = new AlertDialog.Builder(new ContextThemeWrapper(activity, R.style.AppTheme)); diff --git a/app/src/main/res/layout/maintenance_fragment.xml b/app/src/main/res/layout/maintenance_fragment.xml new file mode 100644 index 0000000000..9a9a9650f5 --- /dev/null +++ b/app/src/main/res/layout/maintenance_fragment.xml @@ -0,0 +1,80 @@ + + + + +