diff --git a/CHANGES.txt b/CHANGES.txt index 332e388..9fb7a6a 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -1,5 +1,9 @@ Change log for AWARE-Light +4.2.1.beta + +- New feature to export the aware-light database files to the device's storage. + 4.1.6.beta - Fixed a crash that occurred when enabling accessibility before starting a study. @@ -9,11 +13,11 @@ Change log for AWARE-Light - Logging if the application is terminated by user actions or system actions to aware_log table 4.1.4.beta -- Screen reader function improved preformance +- Screen reader function improved performance 4.1.3.light.bundle - Fixed bug for random EMA triggered condition -- Screen reader function improved preformance +- Screen reader function improved performance 4.1.2.light.bundle - Screen reader table allows longer text to be stored diff --git a/aware-core/aware.gradle b/aware-core/aware.gradle index 2300e88..e11d646 100644 --- a/aware-core/aware.gradle +++ b/aware-core/aware.gradle @@ -4,8 +4,8 @@ buildscript { mqtt_libs = '1.2.1' ion_libs = "2.+" google_libs = "17.0.0" - version_code = 6 - version_readable = "4.1." + version_code + "." + "beta" + version_code = 1 + version_readable = "4.2." + version_code + "." + "beta" compile_sdk = 28 target_sdk = 28 minimum_sdk = 24 diff --git a/aware-core/src/main/java/com/aware/Applications.java b/aware-core/src/main/java/com/aware/Applications.java index 8f48329..ea8a40e 100644 --- a/aware-core/src/main/java/com/aware/Applications.java +++ b/aware-core/src/main/java/com/aware/Applications.java @@ -41,8 +41,11 @@ import org.json.JSONException; import java.util.ArrayList; +import java.util.HashSet; import java.util.Iterator; import java.util.List; +import java.util.Set; + import android.graphics.Rect; /** @@ -95,7 +98,7 @@ public class Applications extends AccessibilityService { ArrayList contentBuffer = new ArrayList(); - ArrayList textBuffer = new ArrayList(); + Set textBuffer = new HashSet<>(); int TEXT_BUFFER_LIMIT = 100; @@ -170,12 +173,9 @@ private String getApplicationName(String package_name) { public void onAccessibilityEvent(AccessibilityEvent event) { if (event.getPackageName() == null) return; - - - - // if text buffer is full then send all contents in the content buffer - if (textBuffer.size() == TEXT_BUFFER_LIMIT){ - + // if content buffer is full then send all contents in the content buffer + if (contentBuffer.size() == TEXT_BUFFER_LIMIT){ + // Log.d("Screen_Text", "==========LIMIT REACH============"); for (ContentValues content: contentBuffer){ getContentResolver().insert(ScreenText_Provider.ScreenTextData.CONTENT_URI, content); @@ -186,6 +186,8 @@ public void onAccessibilityEvent(AccessibilityEvent event) { } textBuffer.clear(); contentBuffer.clear(); + + // Log.d("Screen_Text", "==========CLEAN BUFFER============"); } @@ -225,15 +227,6 @@ else if (criteria == 1 && app_names.contains(curr_app)) { // package in exclusiv // Get the current foreground app String currentForegroundApp = event.getPackageName().toString(); - // Check if the foreground app has changed - if (!currentForegroundApp.equals(previousForegroundApp)) { - // Clear the text buffer - textBuffer.clear(); - - // Update the previous foreground app - previousForegroundApp = currentForegroundApp; - } - // Get text tree AccessibilityNodeInfo mNodeInfo = event.getSource(); textTree(mNodeInfo); @@ -261,14 +254,38 @@ else if (criteria == 1 && app_names.contains(curr_app)) { // package in exclusiv int hashedText = currScreenText.hashCode(); - // Add to content: get rid of the duplicate text - if (!textBuffer.contains(hashedText)) { - textBuffer.add(hashedText); - contentBuffer.add(screenText); + // Check if the foreground app has changed + if (!currentForegroundApp.equals(previousForegroundApp)) { +// // Log.d("Screen_Text", "==========App Switch============"); + if (!textBuffer.contains(hashedText)) { + textBuffer.add(hashedText); + contentBuffer.add(screenText); +// // Log.d("Screen_Text", "Add ContentText: " + currScreenText); + } + + + // Log.d("Screen_Text", "Current ContentBuffer Size: " + contentBuffer.size()); + + for (ContentValues content: contentBuffer){ + getContentResolver().insert(ScreenText_Provider.ScreenTextData.CONTENT_URI, content); - // Log the current screen text for debugging - if (Aware.DEBUG){ - Log.d("AWARE::ScreenText", "Current Screen Text: " + currScreenText); + if (awareSensor != null) awareSensor.onScreentext(content); + Intent screen_text_data = new Intent(ScreenText.ACTION_SCREENTEXT_DETECT); + sendBroadcast(screen_text_data); + } + + textBuffer.clear(); + contentBuffer.clear(); + // Log.d("Screen_Text", "==========CLEAN BUFFER============"); + // Update the previous foreground app + previousForegroundApp = currentForegroundApp; + + } else { + // Add to content: get rid of the duplicate text + if (!textBuffer.contains(hashedText)) { + textBuffer.add(hashedText); + contentBuffer.add(screenText); + // Log.d("Screen_Text", "Add ContentText: " + currScreenText); } } diff --git a/aware-core/src/main/res/values/strings.xml b/aware-core/src/main/res/values/strings.xml index c0c0328..ce83c27 100644 --- a/aware-core/src/main/res/values/strings.xml +++ b/aware-core/src/main/res/values/strings.xml @@ -33,6 +33,7 @@ Sensors needed Check for study updates Sync data + Export data Join a study Quit study Sign Up! diff --git a/aware-phone/build.gradle b/aware-phone/build.gradle index 30d6360..415a33c 100755 --- a/aware-phone/build.gradle +++ b/aware-phone/build.gradle @@ -134,6 +134,7 @@ dependencies { api(project(":com.aware.plugin.sensortag")) api(project(":com.aware.plugin.sentimental")) implementation 'com.android.support:support-v4:28.0.0' + implementation 'androidx.annotation:annotation:1.2.0' } repositories { diff --git a/aware-phone/src/main/java/com/aware/phone/ui/Aware_Light_Client.java b/aware-phone/src/main/java/com/aware/phone/ui/Aware_Light_Client.java index 8336e3d..712bccb 100644 --- a/aware-phone/src/main/java/com/aware/phone/ui/Aware_Light_Client.java +++ b/aware-phone/src/main/java/com/aware/phone/ui/Aware_Light_Client.java @@ -35,6 +35,10 @@ import android.view.ViewGroup; import android.widget.ListAdapter; import android.widget.Toast; +import android.view.View; +import android.widget.ListView; +import androidx.annotation.NonNull; +import androidx.core.app.ActivityCompat; import com.aware.Applications; import com.aware.Aware; @@ -63,6 +67,27 @@ import static com.aware.Aware.TAG; import static com.aware.Aware.setNotificationProperties; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; + +import android.Manifest; +import android.content.Intent; +import android.net.Uri; +import android.os.Bundle; +import android.os.Environment; +import android.view.View; +import android.widget.Button; +import android.widget.Toast; +import androidx.annotation.NonNull; +import androidx.appcompat.app.AppCompatActivity; +import androidx.core.app.ActivityCompat; +import androidx.core.content.ContextCompat; +import androidx.documentfile.provider.DocumentFile; + /** * */ @@ -71,7 +96,8 @@ public class Aware_Light_Client extends Aware_Activity { public static boolean permissions_ok; private static Hashtable listSensorType; private static SharedPreferences prefs; - + public static final int REQUEST_CODE_OPEN_DIRECTORY = 1000; + public static final int REQUEST_CODE_STORAGE_PERMISSION = 1001; private static final ArrayList REQUIRED_PERMISSIONS = new ArrayList<>(); private static final Hashtable optionalSensors = new Hashtable<>(); @@ -334,6 +360,129 @@ protected void onProgressUpdate(Preference... values) { } } + /** + * Checks if the application is running on an emulator. + * + * @return true if the application is running on an emulator, false otherwise. + */ + public static boolean isEmulator() { + // Check various properties to determine if the current environment is an emulator. + return Build.FINGERPRINT.startsWith("generic") + || Build.FINGERPRINT.startsWith("unknown") + || Build.MODEL.contains("google_sdk") + || Build.MODEL.contains("Emulator") + || Build.MODEL.contains("Android SDK built for x86") + || Build.MANUFACTURER.contains("Genymotion") + || (Build.BRAND.startsWith("generic") && Build.DEVICE.startsWith("generic")) + || "google_sdk".equals(Build.PRODUCT); + } + + /** + * Retrieves the appropriate directory for the application's database file storage based on + * several conditions such as running environment (emulator or real device) and configuration settings. + * + * @return The File object pointing to the appropriate directory for database storage. + */ + protected File getDatabaseFileFolder(){ + Context context = this; // Use the current instance to access the context methods. + + File dataDirectory; + + if (context.getResources().getBoolean(com.aware.R.bool.internalstorage)) { + // Use the internal storage directory assigned to the app which is private. + dataDirectory = context.getFilesDir(); + } else if (!context.getResources().getBoolean(com.aware.R.bool.standalone)) { + // Use a directory on the external storage that remains after the app is uninstalled. + dataDirectory = new File(Environment.getExternalStoragePublicDirectory("AWARE").toString()); + } else { + // Decide the storage location based on whether the environment is an emulator. + if (isEmulator()) { + // Use internal storage for emulators for simplicity. + dataDirectory = context.getFilesDir(); + } else { + // Use the external app-specific directory that is removed when the app is uninstalled. + dataDirectory = new File(ContextCompat.getExternalFilesDirs(context, null)[0] + "/AWARE"); + } + } + + return dataDirectory; + } + + /** + * This method checks if the result corresponds to the REQUEST_CODE_OPEN_DIRECTORY and if + * the result code indicates success. If so, it proceeds to export files from a specified + * internal directory to the selected external directory. + */ + @Override + protected void onActivityResult(int requestCode, int resultCode, Intent data) { + super.onActivityResult(requestCode, resultCode, data); + + // Check if the result comes from the correct request and has a successful result code + if (requestCode == REQUEST_CODE_OPEN_DIRECTORY && resultCode == RESULT_OK) { + Uri treeUri = data.getData(); + DocumentFile pickedDir = DocumentFile.fromTreeUri(this, treeUri); + + // Retrieve the directory for storing database files + File externalAppDirectory = getDatabaseFileFolder(); + + + // Log.d("export_data", "External AWARE directory path: " + externalAppDirectory.getAbsolutePath()); + + File[] files = externalAppDirectory.listFiles(); + + if (files != null && files.length > 0) { + for (final File file : files) { + // Log.d("export_data", "Processing file: " + file.getAbsolutePath()); + try { + DocumentFile newFile = pickedDir.createFile("application/octet-stream", file.getName()); + if (newFile != null) { + try (InputStream in = new FileInputStream(file); + OutputStream out = getContentResolver().openOutputStream(newFile.getUri())) { + byte[] buffer = new byte[1024]; + int length; + while ((length = in.read(buffer)) > 0) { + out.write(buffer, 0, length); + } + out.flush(); + } + runOnUiThread(new Runnable() { + @Override + public void run() { + Toast.makeText(Aware_Light_Client.this, "Exported: " + file.getName(), Toast.LENGTH_SHORT).show(); + } + }); + } else { + throw new IOException("Failed to create document file for: " + file.getName()); + } + } catch (final IOException e) { + // Log.e("export_data", "Failed to export: " + file.getName(), e); + runOnUiThread(new Runnable() { + @Override + public void run() { + Toast.makeText(Aware_Light_Client.this, "Failed to export: " + file.getName(), Toast.LENGTH_SHORT).show(); + } + }); + } + } + runOnUiThread(new Runnable() { + @Override + public void run() { + Toast.makeText(Aware_Light_Client.this, "All files exported successfully", Toast.LENGTH_LONG).show(); + } + }); + } else { + // Log.d("export_data", "No files found to export."); + runOnUiThread(new Runnable() { + @Override + public void run() { + Toast.makeText(Aware_Light_Client.this, "No files available to export.", Toast.LENGTH_SHORT).show(); + } + }); + } + // Log.d("export_data", "Output folder URI: " + pickedDir.getUri().toString()); + } + } + @Override protected void onResume() { diff --git a/aware-phone/src/main/java/com/aware/phone/ui/prefs/ExportDataPref.java b/aware-phone/src/main/java/com/aware/phone/ui/prefs/ExportDataPref.java new file mode 100644 index 0000000..7f2980b --- /dev/null +++ b/aware-phone/src/main/java/com/aware/phone/ui/prefs/ExportDataPref.java @@ -0,0 +1,177 @@ +package com.aware.phone.ui.prefs; +import android.content.Context; +import android.content.Intent; +import android.net.Uri; +import android.os.Build; +import android.os.Environment; +import android.preference.Preference; +import android.util.AttributeSet; +import android.util.Log; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.Button; +import android.widget.Toast; +import com.aware.phone.R; +import androidx.core.app.ActivityCompat; +import androidx.core.content.ContextCompat; +import androidx.documentfile.provider.DocumentFile; +import android.app.Activity; +import android.content.pm.PackageManager; +import com.aware.phone.ui.Aware_Light_Client; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; + +public class ExportDataPref extends Preference { + public static final int REQUEST_CODE_OPEN_DIRECTORY = 1000; + public static final int REQUEST_CODE_STORAGE_PERMISSION = 1001; + + public ExportDataPref(Context context, AttributeSet attrs) { + super(context, attrs); + setLayoutResource(R.layout.pref_export_data); + } + + @Override + protected View onCreateView(ViewGroup parent) { + super.onCreateView(parent); + LayoutInflater inflater = (LayoutInflater) getContext().getSystemService( + Context.LAYOUT_INFLATER_SERVICE); + View view = inflater.inflate(R.layout.pref_export_data, parent, false); + + view.findViewById(R.id.btn_export_study_data).setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + Toast.makeText(getContext(), "Exporting data...", Toast.LENGTH_SHORT).show(); + Log.d("export_data", "click on the btn"); + handleExportAction(); + } + + + }); + + return view; + } + private void handleExportAction() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { + Log.d("export_data", "API version is above 28"); + Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT_TREE); + intent.addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION | Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION); + ((Activity) getContext()).startActivityForResult(intent, REQUEST_CODE_OPEN_DIRECTORY); + } else { + + Log.d("export_data", "API version is below 28"); + if (ContextCompat.checkSelfPermission(getContext(), android.Manifest.permission.WRITE_EXTERNAL_STORAGE) + == PackageManager.PERMISSION_GRANTED) { + performExportToPublicDirectory(); + } else { + ActivityCompat.requestPermissions( + (Activity) getContext(), + new String[]{android.Manifest.permission.WRITE_EXTERNAL_STORAGE}, + REQUEST_CODE_STORAGE_PERMISSION); + } + } + } + + /** + * Checks if the application is running on an emulator. + * + * @return true if the application is running on an emulator, false otherwise. + */ + public static boolean isEmulator() { + return Build.FINGERPRINT.startsWith("generic") + || Build.FINGERPRINT.startsWith("unknown") + || Build.MODEL.contains("google_sdk") + || Build.MODEL.contains("Emulator") + || Build.MODEL.contains("Android SDK built for x86") + || Build.MANUFACTURER.contains("Genymotion") + || (Build.BRAND.startsWith("generic") && Build.DEVICE.startsWith("generic")) + || "google_sdk".equals(Build.PRODUCT); + } + + /** + * Retrieves the appropriate directory for the application's database file storage based on + * several conditions such as running environment (emulator or real device) and configuration settings. + * + * @return The File object pointing to the appropriate directory for database storage. + */ + protected File getDatabaseFileFolder(){ + Context context = getContext(); + // Correct path to the external app-specific directory + + File dataDirectory; + if (context.getResources().getBoolean(com.aware.R.bool.internalstorage)) { + + dataDirectory = context.getFilesDir(); + } else if (!context.getResources().getBoolean(com.aware.R.bool.standalone)) { + // sdcard/AWARE/ (shareable, does not delete when uninstalling) + dataDirectory = new File(Environment.getExternalStoragePublicDirectory("AWARE").toString()); + } else { + if (isEmulator()) { + dataDirectory = context.getFilesDir(); + } else { + // sdcard/Android//AWARE/ (not shareable, deletes when uninstalling package) + dataDirectory = new File(ContextCompat.getExternalFilesDirs(context, null)[0] + "/AWARE"); + } + } + + return dataDirectory; + } + + /** + * Exports all files from the application-specific directory to a public directory. (Below Android API 28) + * This method attempts to export all files located in the app's private database file folder + * to a designated public directory within the device's "Downloads" directory. + */ + private void performExportToPublicDirectory() { + File dataDirectory = getDatabaseFileFolder(); + + if (!dataDirectory.exists() && !dataDirectory.mkdirs()) { + // Log.e("export_data", "Failed to create directory: " + dataDirectory.getAbsolutePath()); + + return; + } + + File[] files = dataDirectory.listFiles(); + if (files == null || files.length == 0) { + // Log.e("export_data", "No files found in directory: " + dataDirectory.getAbsolutePath()); + + return; + } + + // Log.d("export_data", "Resource folder path: \n" + dataDirectory.getAbsolutePath()); + + // Setting the export destination to Download/AWARE + File exportDestination = new File(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS), "AWARE"); + if (!exportDestination.exists() && !exportDestination.mkdirs()) { + // Log.e("export_data", "Failed to create export directory: " + exportDestination.getAbsolutePath()); + + return; + } + + for (final File file : files) { + + File newFile = new File(exportDestination, file.getName()); + // Log.d("export_data", "Exporting File path: " + file.getAbsolutePath() + " to " + newFile.getAbsolutePath()); + try (InputStream in = new FileInputStream(file); + OutputStream out = new FileOutputStream(newFile)) { + byte[] buffer = new byte[1024]; + int length; + while ((length = in.read(buffer)) > 0) { + out.write(buffer, 0, length); + } + out.flush(); + } catch (IOException e) { + Log.e("export_data", "Failed to export: " + file.getName(), e); + } + } + + Toast.makeText(getContext(), "Data saved at Downloads/AWARE", Toast.LENGTH_SHORT).show(); + + } + +} diff --git a/aware-phone/src/main/res/layout/pref_export_data.xml b/aware-phone/src/main/res/layout/pref_export_data.xml new file mode 100644 index 0000000..beb83bc --- /dev/null +++ b/aware-phone/src/main/res/layout/pref_export_data.xml @@ -0,0 +1,15 @@ + + + +