From 1d9160e5c52b8a5be283022e2d71a6c91cc1fa8d Mon Sep 17 00:00:00 2001 From: Navid Date: Fri, 26 Jul 2024 18:08:53 -0400 Subject: [PATCH 1/3] Scan QR code from image file --- .../dexdrip/utilitymodels/Constants.java | 2 + .../dexdrip/utils/Preferences.java | 98 ++++++++++++++++--- .../dexdrip/utils/QrCodeFromFile.java | 67 +++++++++++++ app/src/main/res/values/strings.xml | 4 + app/src/main/res/xml/pref_data_sync.xml | 19 +++- 5 files changed, 176 insertions(+), 14 deletions(-) create mode 100644 app/src/main/java/com/eveningoutpost/dexdrip/utils/QrCodeFromFile.java 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/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/values/strings.xml b/app/src/main/res/values/strings.xml index 0c3743c9e7..7ca42d43dd 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -39,6 +39,10 @@ Cloud Storage Auto configure Auto configure using a barcode. + Scanner + 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 diff --git a/app/src/main/res/xml/pref_data_sync.xml b/app/src/main/res/xml/pref_data_sync.xml index 711934a994..ebebd683a4 100644 --- a/app/src/main/res/xml/pref_data_sync.xml +++ b/app/src/main/res/xml/pref_data_sync.xml @@ -8,9 +8,24 @@ android:title="@string/data_sync"> + android:title="@string/auto_configure_title"> + + + + + Date: Wed, 31 Jul 2024 19:47:51 -0400 Subject: [PATCH 2/3] Save logs locally --- app/src/main/AndroidManifest.xml | 6 + .../dexdrip/EventLogActivity.java | 27 +++-- .../dexdrip/utilitymodels/SaveLogs.java | 110 ++++++++++++++++++ .../dexdrip/utilitymodels/SendFeedBack.java | 4 +- .../main/res/layout/activity_event_log.xml | 12 ++ .../main/res/layout/activity_save_logs.xml | 46 ++++++++ .../res/layout/activity_send_feed_back.xml | 26 +++-- app/src/main/res/values/strings.xml | 3 + 8 files changed, 213 insertions(+), 21 deletions(-) create mode 100644 app/src/main/java/com/eveningoutpost/dexdrip/utilitymodels/SaveLogs.java create mode 100644 app/src/main/res/layout/activity_save_logs.xml 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/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/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" /> + +