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" /> + +