diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 73df543f1f..35e672da9d 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -703,6 +703,12 @@
+
+
200000) {
+ if (builder.length() > MAX_LOG_PACKAGE_SIZE) {
JoH.static_toast_long(this, "Could not package up all logs, using most recent");
+ builder.append("\n\nOnly the most recent logs have been included to limit the file size.\n");
break;
}
}
- startActivity(new Intent(getApplicationContext(), SendFeedBack.class).putExtra("generic_text", builder.toString()));
+
+ builder.insert(0, JoH.getDeviceDetails() + "\n" + JoH.getVersionDetails() + "\n" + getBestCollectorHardwareName() + "\n===\n" + "\nLog data:\n"); // Adds device, version and collector details before the log.
+ builder.append("\n\nCaptured: " + JoH.dateTimeText(JoH.tsl())); // Adds date and time of capture after the log.
+
+ return builder.toString();
}
// View model container - accessible binding methods must be declared public
@@ -636,5 +651,3 @@ public void onBindBinding(ViewDataBinding binding, int bindingVariable, @LayoutR
}
}
-
-
diff --git a/app/src/main/java/com/eveningoutpost/dexdrip/utilitymodels/Constants.java b/app/src/main/java/com/eveningoutpost/dexdrip/utilitymodels/Constants.java
index 160d9e8346..49cd91fc46 100644
--- a/app/src/main/java/com/eveningoutpost/dexdrip/utilitymodels/Constants.java
+++ b/app/src/main/java/com/eveningoutpost/dexdrip/utilitymodels/Constants.java
@@ -58,6 +58,8 @@ public class Constants {
static final int NIGHTSCOUT_ERROR_NOTIFICATION_ID = 2001;
public static final int HEALTH_CONNECT_RESPONSE_ID = 2002;
+ public static final int ZXING_CAM_REQ_CODE = 49374;
+ public static final int ZXING_FILE_REQ_CODE = 49375; // This is created by just incrementing the existing camera scan code from the zxing package
public static final int SENSORY_EXPIRY_NOTIFICATION_ID = 2003;
diff --git a/app/src/main/java/com/eveningoutpost/dexdrip/utilitymodels/SaveLogs.java b/app/src/main/java/com/eveningoutpost/dexdrip/utilitymodels/SaveLogs.java
new file mode 100644
index 0000000000..a8a806ef68
--- /dev/null
+++ b/app/src/main/java/com/eveningoutpost/dexdrip/utilitymodels/SaveLogs.java
@@ -0,0 +1,110 @@
+package com.eveningoutpost.dexdrip.utilitymodels;
+
+import android.Manifest;
+import android.app.Activity;
+import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.os.Bundle;
+import android.os.Environment;
+import android.view.View;
+import android.widget.TextView;
+
+import com.eveningoutpost.dexdrip.BaseAppCompatActivity;
+import com.eveningoutpost.dexdrip.R;
+import com.eveningoutpost.dexdrip.models.JoH;
+import com.eveningoutpost.dexdrip.models.UserError;
+
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+
+import static com.eveningoutpost.dexdrip.utils.FileUtils.makeSureDirectoryExists;
+
+import androidx.core.app.ActivityCompat;
+import androidx.core.content.ContextCompat;
+
+// Saves xDrip logs to storage.
+// SendFeedBack sends logs to the lead developer.
+// This does the same thing for saving logs to storage.
+// Navid200
+// July 2024
+
+public class SaveLogs extends BaseAppCompatActivity {
+
+ private static final String TAG = "save logs";
+ private String LOG_FILE_PATH = "/Download/xDrip-export"; // Path to where we save the log file
+ private String LOG_FILE_NAME = "xDrip-log.txt"; // Log file name
+ private final static int MY_PERMISSIONS_REQUEST_STORAGE = 104;
+ private String log_data = "";
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.activity_save_logs);
+
+ Intent intent = getIntent();
+ if (intent != null) {
+ final Bundle bundle = intent.getExtras();
+ if (bundle != null) {
+ final String str2 = bundle.getString("generic_text");
+ if (str2 != null) {
+ log_data = str2;
+ ((TextView) findViewById(R.id.yourSaveText)).setText(log_data.length() > 300 ? "\n\nAttached " + log_data.length() + " characters of log data. (hidden)\n\n" : log_data);
+ }
+ }
+ }
+ }
+
+ public void closeActivity(View myview) {
+ finish();
+ }
+
+ public void saveLogs(View myview) {
+ if (saveLogsToStorage(log_data)) {
+ UserError.Log.e(TAG, "Saved log file to /Downloads/xDrip-export/xDrip-log.txt");
+ } else {
+ UserError.Log.e(TAG, "Could not write log file");
+ }
+ log_data = "";
+ closeActivity(null); // Let's close the menu
+ }
+
+ public boolean saveLogsToStorage(String contents) {
+ if (isStorageWritable(this, MY_PERMISSIONS_REQUEST_STORAGE)) {
+ try {
+ final StringBuilder sb = new StringBuilder();
+ sb.append(Environment.getExternalStorageDirectory().getAbsolutePath());
+ sb.append(LOG_FILE_PATH);
+ final String dir = sb.toString();
+ makeSureDirectoryExists(dir);
+ final String pathPlusFileName = dir + "/" + LOG_FILE_NAME;
+ final File myExternalFile = new File(pathPlusFileName);
+ FileOutputStream fos = new FileOutputStream(myExternalFile);
+ fos.write(contents.getBytes());
+ fos.close();
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ return true;
+ } else {
+ JoH.static_toast_long("getString(R.string.sdcard_not_writable_cannot_save)");
+ return false;
+ }
+ }
+
+ public static boolean isStorageWritable(Activity context, int request_code) { // Get write permission if not & return false. Return true if yes and not tied up.
+ if (ContextCompat.checkSelfPermission(context,
+ android.Manifest.permission.WRITE_EXTERNAL_STORAGE)
+ != PackageManager.PERMISSION_GRANTED) {
+ ActivityCompat.requestPermissions(context,
+ new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE},
+ request_code);
+ UserError.Log.e(TAG, "Did not have write permission, but should have it now");
+ return false;
+ }
+ String state = Environment.getExternalStorageState();
+ return Environment.MEDIA_MOUNTED.equals(state);
+ }
+
+}
+
diff --git a/app/src/main/java/com/eveningoutpost/dexdrip/utilitymodels/SendFeedBack.java b/app/src/main/java/com/eveningoutpost/dexdrip/utilitymodels/SendFeedBack.java
index 7f05385904..7b6c080af4 100644
--- a/app/src/main/java/com/eveningoutpost/dexdrip/utilitymodels/SendFeedBack.java
+++ b/app/src/main/java/com/eveningoutpost/dexdrip/utilitymodels/SendFeedBack.java
@@ -78,7 +78,7 @@ protected void onCreate(Bundle savedInstanceState) {
final String str2 = bundle.getString("generic_text");
if (str2 != null) {
log_data = str2;
- ((EditText) findViewById(R.id.yourText)).setText(log_data.length() > 300 ? "\n\nPlease describe what you think these logs may show? Explain the problem if there is one.\n\nAttached " + log_data.length() + " characters of log data. (hidden)\n\n" : log_data);
+ ((EditText) findViewById(R.id.yourText)).setText(log_data.length() > 300 ? "\n\nPlease describe what you think these logs may show. Explain the problem if there is one.\n\nAttached " + log_data.length() + " characters of log data. (hidden)\n\n" : log_data);
type_of_message = "Log Push";
myrating.setVisibility(View.GONE);
ratingtext.setVisibility(View.GONE);
@@ -177,7 +177,7 @@ public void sendFeedback(View myview) {
try {
final RequestBody formBody = new FormEncodingBuilder()
.add("contact", contact.getText().toString())
- .add("body", JoH.getDeviceDetails() + "\n" + JoH.getVersionDetails() + "\n" + getBestCollectorHardwareName() + "\n===\n\n" + yourtext.getText().toString() + " \n\n===\nType: " + type_of_message + "\nLog data:\n\n" + log_data + "\n\n\nSent: " + JoH.dateTimeText(JoH.tsl()))
+ .add("body",yourtext.getText().toString() + " \n\n===\nType: " + type_of_message + "\nLog data:\n\n" + log_data) // Adding "Your text" and type to the log
.add("rating", String.valueOf(myrating.getRating()))
.add("type", type_of_message)
.build();
diff --git a/app/src/main/java/com/eveningoutpost/dexdrip/utils/Preferences.java b/app/src/main/java/com/eveningoutpost/dexdrip/utils/Preferences.java
index b3a2a518bb..d86da4ce00 100644
--- a/app/src/main/java/com/eveningoutpost/dexdrip/utils/Preferences.java
+++ b/app/src/main/java/com/eveningoutpost/dexdrip/utils/Preferences.java
@@ -17,6 +17,8 @@
import android.content.SharedPreferences;
import android.content.pm.PackageManager;
import android.content.res.Configuration;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
import android.graphics.Color;
import android.media.Ringtone;
import android.media.RingtoneManager;
@@ -124,12 +126,20 @@
import com.eveningoutpost.dexdrip.webservices.XdripWebService;
import com.eveningoutpost.dexdrip.xDripWidget;
import com.eveningoutpost.dexdrip.xdrip;
+import com.google.zxing.BinaryBitmap;
+import com.google.zxing.MultiFormatReader;
+import com.google.zxing.NotFoundException;
+import com.google.zxing.RGBLuminanceSource;
+import com.google.zxing.Result;
+import com.google.zxing.common.HybridBinarizer;
import com.google.zxing.integration.android.IntentIntegrator;
import com.google.zxing.integration.android.IntentResult;
import com.nightscout.core.barcode.NSBarcodeConfig;
import net.tribe7.common.base.Joiner;
+import java.io.FileNotFoundException;
+import java.io.InputStream;
import java.lang.reflect.Method;
import java.net.URI;
import java.text.DecimalFormat;
@@ -170,6 +180,13 @@ public class Preferences extends BasePreferenceActivity implements SearchPrefere
private static AllPrefsFragment pFragment;
private BroadcastReceiver mibandStatusReceiver;
+ // The following three variables enable us to create a common state from the input,
+ // whether we scan from camera or a file, and continue with the same following
+ // set of commands to avoid code duplication.
+ private volatile String scanFormat = null; // The format of the scan
+ private volatile String scanContents = null; // Text content of the scan coming either from camera or file
+ private volatile byte[] scanRawBytes = null; // Raw bytes of the scan
+
private void refreshFragments() {
refreshFragments(null);
}
@@ -341,7 +358,11 @@ public static Boolean getBooleanPreferenceViaContextWithoutException(Context con
@Override
- public void onActivityResult(int requestCode, int resultCode, Intent data) {
+ protected synchronized void onActivityResult(int requestCode, int resultCode, Intent data) {
+ // Let's reset variables just to be sure
+ scanFormat = null;
+ scanContents = null;
+ scanRawBytes = null;
if (requestCode == Constants.HEALTH_CONNECT_RESPONSE_ID) {
if (HealthConnectEntry.enabled()) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
@@ -352,22 +373,64 @@ public void onActivityResult(int requestCode, int resultCode, Intent data) {
}
}
+ if (requestCode == Constants.ZXING_FILE_REQ_CODE) { // If we are scanning an image file, not using the camera
+ // The core of the following section, selecting the file, converting it into a bitmap, and then to a bitstream, is from:
+ // https://stackoverflow.com/questions/55427308/scaning-qrcode-from-image-not-from-camera-using-zxing
+ if (data == null || data.getData() == null) {
+ Log.e("TAG", "No file was selected");
+ return;
+ }
+ Uri uri = data.getData();
+ try {
+ InputStream inputStream = getContentResolver().openInputStream(uri);
+ Bitmap bitmap = BitmapFactory.decodeStream(inputStream);
+ if (bitmap == null) {
+ Log.e("TAG", "uri is not a bitmap," + uri.toString());
+ return;
+ }
+ int width = bitmap.getWidth(), height = bitmap.getHeight();
+ int[] pixels = new int[width * height];
+ bitmap.getPixels(pixels, 0, width, 0, 0, width, height);
+ bitmap.recycle();
+ bitmap = null;
+ RGBLuminanceSource source = new RGBLuminanceSource(width, height, pixels);
+ BinaryBitmap bBitmap = new BinaryBitmap(new HybridBinarizer(source));
+ MultiFormatReader reader = new MultiFormatReader();
+ try {
+ Result result = reader.decode(bBitmap);
+ scanFormat = result.getBarcodeFormat().toString();
+ scanContents = result.getText(); // The text content of the scanned file
+ scanRawBytes = result.getRawBytes();
+ } catch (NotFoundException e) {
+ Log.e("TAG", "decode exception", e);
+ }
+ } catch (FileNotFoundException e) {
+ Log.e("TAG", "can not open file" + uri.toString(), e);
+ }
+ } else if (requestCode == Constants.ZXING_CAM_REQ_CODE) { // If we are scanning from camera
+ IntentResult scanResult = IntentIntegrator.parseActivityResult(requestCode, resultCode, data);
+ scanFormat = scanResult.getFormatName();
+ scanContents = scanResult.getContents(); // The text content of the scan from camera
+ scanRawBytes = scanResult.getRawBytes();
+ }
+ // We now have scan format, scan text content, and scan raw bytes in the corresponding variables.
+ // Everything after this is applied whether we scanned with camera or from a file.
- IntentResult scanResult = IntentIntegrator.parseActivityResult(requestCode, resultCode, data);
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this);
- if (scanResult == null || scanResult.getContents() == null) {
+ if (scanContents == null) { // If we have no scan content
+ UserError.Log.d(TAG, "No scan results ");
return;
}
- if (scanResult.getFormatName().equals("QR_CODE")) {
- final String scanresults = scanResult.getContents();
- if (QRcodeUtils.hasDecoderMarker(scanresults)) {
- installxDripPlusPreferencesFromQRCode(prefs, scanresults);
+ if (scanFormat.equals("QR_CODE")) { // The scan is a QR code
+
+ if (QRcodeUtils.hasDecoderMarker(scanContents)) {
+ installxDripPlusPreferencesFromQRCode(prefs, scanContents);
return;
}
try {
- if (BlueJay.processQRCode(scanResult.getRawBytes())) {
+ if (BlueJay.processQRCode(scanRawBytes)) {
refreshFragments();
return;
}
@@ -376,7 +439,7 @@ public void onActivityResult(int requestCode, int resultCode, Intent data) {
}
- final NSBarcodeConfig barcode = new NSBarcodeConfig(scanresults);
+ final NSBarcodeConfig barcode = new NSBarcodeConfig(scanContents);
if (barcode.hasMongoConfig()) {
if (barcode.getMongoUri().isPresent()) {
SharedPreferences.Editor editor = prefs.edit();
@@ -427,9 +490,9 @@ public void onActivityResult(int requestCode, int resultCode, Intent data) {
editor.putBoolean("cloud_storage_mqtt_enable", false);
editor.apply();
}
- } else if (scanResult.getFormatName().equals("CODE_128")) {
- Log.d(TAG, "Setting serial number to: " + scanResult.getContents());
- prefs.edit().putString("share_key", scanResult.getContents()).apply();
+ } else if (scanFormat.equals("CODE_128")) {
+ Log.d(TAG, "Setting serial number to: " + scanContents);
+ prefs.edit().putString("share_key", scanContents).apply();
}
refreshFragments();
}
@@ -1006,6 +1069,7 @@ public void onCreate(Bundle savedInstanceState) {
addPreferencesFromResource(R.xml.pref_data_sync);
setupBarcodeConfigScanner();
setupBarcodeShareScanner();
+ setupQrFromFile();
bindPreferenceSummaryToValue(findPreference("cloud_storage_mongodb_uri"));
bindPreferenceSummaryToValue(findPreference("cloud_storage_mongodb_collection"));
bindPreferenceSummaryToValue(findPreference("cloud_storage_mongodb_device_status_collection"));
@@ -2853,6 +2917,16 @@ public boolean onPreferenceClick(Preference preference) {
});
}
+ private void setupQrFromFile() {
+ findPreference("qr_code_from_file").setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() {
+ @Override
+ public boolean onPreferenceClick(Preference preference) { // Listener for scanning QR code from file
+ new QrCodeFromFile(getActivity()).scanFile();
+ return true;
+ }
+ });
+ }
+
private void refresh_extra_items() {
try {
if (this.prefs == null) return;
diff --git a/app/src/main/java/com/eveningoutpost/dexdrip/utils/QrCodeFromFile.java b/app/src/main/java/com/eveningoutpost/dexdrip/utils/QrCodeFromFile.java
new file mode 100644
index 0000000000..c275d5dc76
--- /dev/null
+++ b/app/src/main/java/com/eveningoutpost/dexdrip/utils/QrCodeFromFile.java
@@ -0,0 +1,67 @@
+package com.eveningoutpost.dexdrip.utils;
+
+import android.app.Activity;
+import android.content.Intent;
+
+import com.eveningoutpost.dexdrip.models.UserError;
+import com.eveningoutpost.dexdrip.utilitymodels.Constants;
+
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * This is a helper class to manage QR code scan from file and
+ * return results to the instantiating activity to complement the existing scan from camera function.
+ * The scan from file portion reference: https://stackoverflow.com/questions/55427308/scaning-qrcode-from-image-not-from-camera-using-zxing
+ */
+
+public class QrCodeFromFile {
+ private static final String TAG = QrCodeFromFile.class.getSimpleName();
+
+ private Activity activity;
+ private Collection desiredBarcodeFormats;
+
+
+ public QrCodeFromFile(Activity activity) {
+ this.activity = activity;
+ }
+
+ public QrCodeFromFile setDesiredBarcodeFormats(Collection desiredBarcodeFormats) {
+ this.desiredBarcodeFormats = desiredBarcodeFormats;
+ return this;
+ }
+
+ public final void initiateFileScan() {
+ UserError.Log.e(TAG, "Navid_ initiate scan");
+
+ // TODO Replace startActivityForResult with Androidx Activity Result APIs
+ this.activity.startActivityForResult(this.createFileScanIntent(), Constants.ZXING_FILE_REQ_CODE);
+
+ }
+
+ public Intent createFileScanIntent() {
+ Intent pickIntent = new Intent(Intent.ACTION_PICK);
+ pickIntent.setDataAndType( android.provider.MediaStore.Images.Media.EXTERNAL_CONTENT_URI, "image/*");
+
+ return pickIntent;
+ }
+
+ private static List list(String... values) {
+ return Collections.unmodifiableList(Arrays.asList(values));
+ }
+
+ public void scanFile() { // Copied (and slightly modified) from AndroidBarcode.scan()
+ UserError.Log.e(TAG, "Navid_ scanFile ");
+ actuallyStartScanFile();
+ }
+
+ private void actuallyStartScanFile() {
+ UserError.Log.e(TAG, "Navid_ actuallyScan ");
+ new QrCodeFromFile(activity)
+ .setDesiredBarcodeFormats(list("QR_CODE", "CODE_128"))
+ .initiateFileScan();
+ }
+
+}
diff --git a/app/src/main/res/layout/activity_event_log.xml b/app/src/main/res/layout/activity_event_log.xml
index 71e522b53e..a65bb8d699 100644
--- a/app/src/main/res/layout/activity_event_log.xml
+++ b/app/src/main/res/layout/activity_event_log.xml
@@ -139,6 +139,17 @@
android:layout_weight="1"
android:onClick="uploadEventLogs"
android:text="@string/upload_logs"
+ android:textAllCaps="false"
+ android:textAlignment="center" />
+
+
diff --git a/app/src/main/res/layout/activity_save_logs.xml b/app/src/main/res/layout/activity_save_logs.xml
new file mode 100644
index 0000000000..810e9cc6d6
--- /dev/null
+++ b/app/src/main/res/layout/activity_save_logs.xml
@@ -0,0 +1,46 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/layout/activity_send_feed_back.xml b/app/src/main/res/layout/activity_send_feed_back.xml
index edd5602d2a..fc6711e583 100644
--- a/app/src/main/res/layout/activity_send_feed_back.xml
+++ b/app/src/main/res/layout/activity_send_feed_back.xml
@@ -9,18 +9,23 @@
android:paddingTop="@dimen/activity_vertical_margin"
tools:context="com.eveningoutpost.dexdrip.utilitymodels.SendFeedBack">
-
+
+
+ android:inputType="textMultiLine"
+ android:text="" />
+ android:layout_below="@+id/yourText" />
Auto configure
Auto configure using a barcode.
+ Camera
+ Scan a QR code with your mobile (recommended)
+ Image file
+ You can save a QR code as an image file (such as .png or .jpeg) on your mobile. Then, use this option to scan the QR code from the saved file.
Scan Share Barcode
Or scan the barcode on the share receiver
@@ -130,6 +134,7 @@
xDrip+ Update Available
Share Settings via QR code
Send Feedback to Jamorham
+ Save Logs
Parakeet Map
Display Preferences
@@ -414,7 +419,9 @@
Please enter your question or comments here.\n\nIf you supply an email address you may get a response.\n\n
Optional contact info here, e.g. email
Send Message
+ Save Logs
Please indicate what you think of the app generally
+ Warning!\nThe logs may contain confidential information such as passwords or user IDs.
Preferences saved in Internal storage/Download
Couldn\'t write to SD card - check permissions?
Loaded Preferences! - Restarting
diff --git a/app/src/main/res/xml/pref_data_sync.xml b/app/src/main/res/xml/pref_data_sync.xml
index 711934a994..e5e99bcc47 100644
--- a/app/src/main/res/xml/pref_data_sync.xml
+++ b/app/src/main/res/xml/pref_data_sync.xml
@@ -8,9 +8,18 @@
android:title="@string/data_sync">
+ android:title="@string/auto_configure_title">
+
+
+