Skip to content
This repository has been archived by the owner on May 14, 2024. It is now read-only.

Export / Import #227

Draft
wants to merge 6 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ dependencies {
// workaround issue #73
exclude group: "com.google.android", module: "android"
}
implementation 'com.googlecode.json-simple:json-simple:1.1'
implementation "com.google.code.findbugs:jsr305:2.0.2"

androidTestImplementation "com.android.support:support-annotations:28.0.0"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ public abstract class AbstractFileChoosingActivity extends InjectionAppCompatAct
* Sends an Intent to FilePickerActivity.
*/
public final void startFileChooser() {
LOGGER.debug("Sending Intent to open FilePicker Activity.");
LOGGER.debug("Sending Intent from startFileChooser to open FilePicker Activity.");
final Intent i = new Intent(this, RaspiFilePickerActivity.class);
// Set these depending on your use case. These are the defaults.
i.putExtra(FilePickerActivity.EXTRA_ALLOW_MULTIPLE, false);
Expand All @@ -63,6 +63,19 @@ public final void startFileChooser() {
startActivityForResult(i, REQUEST_CODE_LOAD_FILE);
}

/**
* Same as above but for choosing a directory
*/
public final void startDirChooser() {
LOGGER.debug("Sending Intent from startDirChooser to open FilePicker Activity.");
final Intent i = new Intent(this, RaspiFilePickerActivity.class);
i.putExtra(FilePickerActivity.EXTRA_ALLOW_MULTIPLE, false);
i.putExtra(FilePickerActivity.EXTRA_ALLOW_CREATE_DIR, true);
i.putExtra(FilePickerActivity.EXTRA_MODE, FilePickerActivity.MODE_DIR);
i.putExtra(FilePickerActivity.EXTRA_START_PATH, Environment.getExternalStorageDirectory().getPath());
startActivityForResult(i, REQUEST_CODE_LOAD_FILE);
}


public final String getFilenameFromPath(String filePath) {
return new File(filePath).getName();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,8 @@

import de.eidottermihi.raspicheck.BuildConfig;
import de.eidottermihi.raspicheck.R;
import de.eidottermihi.rpicheck.activity.helper.ExportHelper;
import de.eidottermihi.rpicheck.activity.helper.ImportHelper;
import de.eidottermihi.rpicheck.activity.helper.LoggingHelper;
import io.freefair.android.preference.AppCompatPreferenceActivity;
import sheetrock.panda.changelog.ChangeLog;
Expand All @@ -62,6 +64,9 @@ public class SettingsActivity extends AppCompatPreferenceActivity implements
public static final String KEY_PREF_DEBUG_LOGGING = "pref_debug_log";
public static final String KEY_PREF_QUERY_SHOW_SYSTEM_TIME = "pref_query_show_system_time";

public static final String KEY_EXPORT_ALL = "export";
public static final String KEY_IMPORT_ALL = "import";

private static final String KEY_PREF_LOG = "pref_log";
private static final String KEY_PREF_CHANGELOG = "pref_changelog";
private static final String KEY_PREF_LOAD_AVG_PERIOD = "pref_load_avg";
Expand All @@ -74,11 +79,15 @@ public class SettingsActivity extends AppCompatPreferenceActivity implements
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
addPreferencesFromResource(R.xml.preferences);
// adding preference listener to log / changelog
// adding preference listener to log / changelog / export / import
Preference prefLog = findPreference(KEY_PREF_LOG);
prefLog.setOnPreferenceClickListener(this);
Preference prefChangelog = findPreference(KEY_PREF_CHANGELOG);
prefChangelog.setOnPreferenceClickListener(this);
Preference prefExport = findPreference(KEY_EXPORT_ALL);
prefExport.setOnPreferenceClickListener(this);
Preference prefImport = findPreference(KEY_IMPORT_ALL);
prefImport.setOnPreferenceClickListener(this);

findPreference("pref_app_version").setSummary(BuildConfig.VERSION_NAME);

Expand Down Expand Up @@ -166,8 +175,38 @@ public boolean onPreferenceClick(Preference preference) {
clickHandled = true;
ChangeLog cl = new ChangeLog(this);
cl.getFullLogDialog().show();
} else if (preference.getKey().equals(KEY_EXPORT_ALL)) {
clickHandled = true;
//ToDo:
// Find out how to properly create ExportHelper so that context and
// 'android.app.ActivityThread.getApplicationThread()' aren't throwing null object reference errors.
ExportHelper exportHelper = new ExportHelper();
final String allClear = exportHelper.ExportAll(this);
if (allClear == null) {
Toast.makeText(this, this.getResources().getString(R.string.export_success),
Toast.LENGTH_LONG).show();
}else{
Toast.makeText(this, allClear,
Toast.LENGTH_LONG).show();
}
} else if (preference.getKey().equals(KEY_IMPORT_ALL)) {
clickHandled = true;
//ToDo Fix the same stuff as above!
ImportHelper importHelper = new ImportHelper();
final String allClear = importHelper.ImportAll(this);
if (allClear == null) {
LOGGER.debug("Import successful");
Toast.makeText(this, this.getResources().getString(R.string.import_success),
Toast.LENGTH_LONG).show();
//Reopen the app to make the changes visible, inspired by this great answer: https://stackoverflow.com/a/39484617
Intent reopenIntent = new Intent(this, MainActivity.class);
finishAffinity();
startActivity(reopenIntent);
}else{
Toast.makeText(this, allClear,
Toast.LENGTH_LONG).show();
}
}
return clickHandled;
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,198 @@
/**
* MIT License
*
* Copyright (c) 2022 RasPi Check Contributors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package de.eidottermihi.rpicheck.activity.helper;

import android.app.Activity;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.database.Cursor;
import android.os.Environment;
import android.preference.PreferenceManager;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import org.json.simple.JSONObject;
import org.json.simple.JSONArray;

import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.util.Arrays;

import de.eidottermihi.raspicheck.BuildConfig;
import de.eidottermihi.raspicheck.R;
import de.eidottermihi.rpicheck.activity.AbstractFileChoosingActivity;
import de.eidottermihi.rpicheck.db.DeviceDbHelper;
import de.eidottermihi.rpicheck.db.RaspberryDeviceBean;

import static de.eidottermihi.rpicheck.activity.SettingsActivity.KEY_PREF_TEMPERATURE_SCALE;
import static de.eidottermihi.rpicheck.activity.SettingsActivity.KEY_PREF_QUERY_HIDE_ROOT_PROCESSES;
import static de.eidottermihi.rpicheck.activity.SettingsActivity.KEY_PREF_FREQUENCY_UNIT;
import static de.eidottermihi.rpicheck.activity.SettingsActivity.KEY_PREF_DEBUG_LOGGING;
import static de.eidottermihi.rpicheck.activity.SettingsActivity.KEY_PREF_QUERY_SHOW_SYSTEM_TIME;

@SuppressWarnings("unchecked")
public class ExportHelper extends AbstractFileChoosingActivity {
private static final Logger LOGGER = LoggerFactory.getLogger(ExportHelper.class);
private String filePath;
private Context mBaseContext;
private String errorString = null;

//Couldn't come up with any better names for the methods
public String ExportAll(Context baseContext) {
this.mBaseContext = baseContext;

/*Calling the function for choosing the folder here causes an
"java.lang.NullPointerException: Attempt to invoke virtual method 'java.lang.String android.content.Context.getPackageName()' on a null object reference"
error. I have no clue how to fix this so for now I'll just hardcode the download directory.

Ok so it turns out when I call this method, the mBase object (whatever that does) gets set to null (or not set at all),
which causes all of these problems when ContextWrapper.java tries to
return mBase.getPackageName()
I know that now but still have no clue how to fix this.

Ok so passing the base context of SettingsActivity.java fixing the context problems, but now I get
'java.lang.NullPointerException: Attempt to invoke virtual method 'android.app.ActivityThread$ApplicationThread android.app.ActivityThread.getApplicationThread()' on a null object reference'
which I don't think I can fix. At least SharedPreferences works with the mBaseContext workaround.
*/
//startDirChooser(mBaseContext);
this.filePath = String.valueOf(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS));
doExport();

return errorString;
}

private void doExport() {
LOGGER.info("Export called with filePath: {}", filePath);
//I briefly skimmed over how andOTP handles backups, which solidified my decision to use
// JSON and maybe add encryption at a later date because of the passwords/maybe keyfiles.
DeviceDbHelper deviceDb;
RaspberryDeviceBean deviceBean;
JSONArray fullJSON = new JSONArray();

//This first exports all SharedPreferences of the app, then every device and then all commands
// into a json file, which includes passwords but no keyfiles, only their path.
final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(mBaseContext);
JSONObject userSettings = new JSONObject();
userSettings.put("temp_scale", prefs.getString(KEY_PREF_TEMPERATURE_SCALE, null));
userSettings.put("hide_root", prefs.getBoolean(KEY_PREF_QUERY_HIDE_ROOT_PROCESSES, false));
userSettings.put("freq_unit", prefs.getString(KEY_PREF_FREQUENCY_UNIT, null));
userSettings.put("debug_log", prefs.getBoolean(KEY_PREF_DEBUG_LOGGING, false));
userSettings.put("sys_time", prefs.getBoolean(KEY_PREF_QUERY_SHOW_SYSTEM_TIME, false));
/*Was considering using the KEY variables as the keys for the JSON, but any change would
have made the JSON unusable so I'll stick with hardcoded names.*/

//Adding the version code just in case something changes in the future and
//it has to be imported differently (or throw out an "too old" error).
userSettings.put("version_code", BuildConfig.VERSION_CODE);


JSONArray devices = new JSONArray();
deviceDb = new DeviceDbHelper(mBaseContext);
Cursor deviceCursor = deviceDb.getFullDeviceCursor();
boolean cursorNotEnded = deviceCursor.moveToFirst();
while (cursorNotEnded) {
deviceBean = deviceDb.read(deviceCursor.getLong(0));

JSONObject device = new JSONObject();
device.put("_id", deviceBean.getId());
device.put("name", deviceBean.getName());
device.put("description", deviceBean.getDescription());
device.put("host", deviceBean.getHost());
device.put("user", deviceBean.getUser());
device.put("passwd", deviceBean.getPass());
device.put("sudo_passwd", deviceBean.getSudoPass());
device.put("ssh_port", deviceBean.getPort());
/*getCreatedAt() and getModifiedAt() returns an Java Date, which JSON does not like.
Originally intended to save it as a string, but because I can't
import it through DeviceDbHelper I'll leave it out.
device.put("created_at", String.valueOf(deviceBean.getCreatedAt()));
device.put("modified_at", String.valueOf(deviceBean.getModifiedAt()));*/
device.put("serial", deviceBean.getSerial());
device.put("auth_method", deviceBean.getAuthMethod());
device.put("keyfile_path", deviceBean.getKeyfilePath());
device.put("keyfile_pass", deviceBean.getKeyfilePass());
devices.add(device);

cursorNotEnded = deviceCursor.moveToNext();
}
deviceCursor.close();

JSONArray commands = new JSONArray();
Cursor commandCursor = deviceDb.getFullCommandCursor();
cursorNotEnded = commandCursor.moveToFirst();
while (cursorNotEnded) {
JSONObject command = new JSONObject();
//_id is not needed as it's an autoincrement field in the DB
//command.put("_id", commandCursor.getInt(0));
command.put("name", commandCursor.getString(1));
command.put("command", commandCursor.getString(2));
//noinspection SimplifiableConditionalExpression took this from CursorHelper.java
command.put("flag_output", commandCursor.getInt(3) == 1 ? true : false);
command.put("timeout", commandCursor.getInt(4));
commands.add(command);

cursorNotEnded = commandCursor.moveToNext();
}
commandCursor.close();

fullJSON.add(userSettings);
fullJSON.add(devices);
fullJSON.add(commands);

//Write file
String fileName = "rpicheck_export.json";
int fileNumber = 1;
File exportFile = new File(filePath, fileName);
//Append a number to the name instead of overwriting old exports.
// Kinda brute-force but even at 1.000.000 files it takes just takes 20 seconds (emulator).
while(exportFile.exists()) {
fileName = "rpicheck_export("+ fileNumber +").json";
exportFile = new File(filePath, fileName);
fileNumber += 1;
}

try (FileWriter writer = new FileWriter(exportFile, false)) {
writer.write(fullJSON.toJSONString());
writer.flush();
LOGGER.info("File \"{}\" successfully saved at \"{}\"", fileName, filePath);
} catch (IOException e) {
this.errorString = mBaseContext.getResources().getString(R.string.err_ioexception);
LOGGER.error(e.toString());
LOGGER.debug(Arrays.toString(e.getStackTrace()));
}
}

@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
if (requestCode == REQUEST_CODE_LOAD_FILE && resultCode == Activity.RESULT_OK) {
final String filePath = data.getData().getPath();
LOGGER.debug("Selected path: {}", filePath);
this.filePath = filePath;
doExport();
}
}
}
Loading