I have an android app that uses a service to collect sensor data every 5ms and inserts it into a sqlite table. In a typical session there will be about 40mins of recordings. All of that code seems to be working fine.
I have a strange problem where if the user navigates to a particular fragment I get a CursorWindow: Window is full: requested allocation XXX
error. I'm not sure what is special about this specific fragment that is causing that error, and only happens with this one fragment
The fragment in question contains a button, which when clicked will do a number of things:
- Creates some directories on external storage
- Copies all of the sensor data from the temporary table and inserts it into a more permanent table
- Creates a copy of the entire
.db
file to external storage - Takes the contents of a table and writes it to a CSV file
- Queries another table for sensor data, and writes all of that sensor data to another CSV file
- Uses media scanner to scan all of the files in the export directory so they can be accessed via MTP
The fragment code looks like this (a lot of the catch blocks have been left out for brevity - they mostly just log information):
public class SaveFragment extends Fragment implements View.OnClickListener {
Button saveButton;
MainActivity mainActivity;
DBHelper dbHelper;
Boolean subjectDataExists;
MediaScanner mediaScanner;
static ProgressDialog dialog;
public SaveFragment() {
// Required empty public constructor
}
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.fragment_save, container, false);
//Get save button view
saveButton = (Button) view.findViewById(R.id.saveButton);
saveButton.setOnClickListener(this);
//Get DBHelper
dbHelper = DBHelper.getInstance(getActivity(), new DatabaseHandler());
//Check if sensor data has been recorded
subjectDataExists = dbHelper.checkSubjectDataExists(Short.parseShort(dbHelper.getTempSubInfo("subNum")));
// Inflate the layout for this fragment
return view;
}
@Override
public void onClick(View v) {
//Alert dialog for saving/quitting
AlertDialog.Builder alertDialogBuilder = new AlertDialog.Builder(mainActivity);
if (subjectDataExists) {
alertDialogBuilder.setTitle("Save and quit?");
alertDialogBuilder.setMessage("Are you sure you want to save the data and quit the current session?");
} else {
alertDialogBuilder.setTitle("Quit?");
alertDialogBuilder.setMessage("Are you sure you want to quit the current session? \n\n No data will be saved.");
}
alertDialogBuilder.setPositiveButton("Yes", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int id) {
//Save if sensor data exists, otherwise quit
if (subjectDataExists) {
new ExportDatabaseCSVTask().execute();
} else {
quitSession();
}
}
});
alertDialogBuilder.setNegativeButton("No", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int id) {
dialog.cancel();
}
});
AlertDialog quitAlertDialog = alertDialogBuilder.create();
quitAlertDialog.show();
}
//Quit the current session and go back to login screen
private void quitSession(){
Intent intent = new Intent(getActivity(), LoginActivity.class);
intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
startActivity(intent);
getActivity().finishAffinity();
}
//Message handler class for database progress updates
private static class DatabaseHandler extends Handler {
@Override
public void handleMessage (Message msg){
Double progressPercent = (Double) msg.obj;
Integer progressValue = 40 + (int) Math.ceil(progressPercent/2);
dialog.setProgress(progressValue);
}
}
//Async class for CSV export task
public class ExportDatabaseCSVTask extends AsyncTask<String, Integer, Boolean> {
@Override
protected void onPreExecute() {
//show a progress dialog
}
protected Boolean doInBackground(final String... args) {
//Create directories for the output csv files
String pathToExternalStorage = Environment.getExternalStorageDirectory().toString();
File exportDir = new File(pathToExternalStorage, "/Data");
File subjectDataDir = new File(exportDir, "/subjects");
publishProgress(5);
//The sleep is here just so the progress updates in the dialog are visually slower
SystemClock.sleep(100);
if (!exportDir.exists()) {
Boolean created = exportDir.mkdirs();
}
publishProgress(10);
SystemClock.sleep(100);
if (!subjectDataDir.exists()) {
Boolean created = subjectDataDir.mkdirs();
}
publishProgress(15);
SystemClock.sleep(100);
//If all directories have been created successfully
if (exportDir.exists() && subjectDataDir.exists()) {
try {
//Copy temp subject and sensor data to persistent db tables
dbHelper.copyTempData();
publishProgress(20);
SystemClock.sleep(200);
//Backup the SQL DB file
File data = Environment.getDataDirectory();
String currentDBPath = "//data//com.example.app//databases//" + DBHelper.DATABASE_NAME;
File currentDB = new File(data, currentDBPath);
File destDB = new File(exportDir, DBHelper.DATABASE_NAME);
publishProgress(25);
SystemClock.sleep(100);
if (exportDir.canWrite()) {
if (currentDB.exists()) {
FileChannel src = new FileInputStream(currentDB).getChannel();
FileChannel dst = new FileOutputStream(destDB).getChannel();
dst.transferFrom(src, 0, src.size());
src.close();
dst.close();
}
}
publishProgress(35);
SystemClock.sleep(300);
//Export subjects table/tracking sheet
File trackingSheet = new File(exportDir, "trackingSheet.csv");
try{
dbHelper.exportTrackingSheet(trackingSheet);
} catch (SQLException | IOException e){
}
publishProgress(40);
SystemClock.sleep(300);
//Export individual subject data
String subNum = dbHelper.getTempSubInfo("subNum");
File subjectFile = new File(subjectDataDir, subNum + ".csv");
try{
dbHelper.exportSubjectData(subjectFile, subNum);
} catch (SQLException | IOException e){
}
publishProgress(90);
SystemClock.sleep(300);
//Scan all files for MTP
List<String> fileList = getListFiles(exportDir);
String[] allFiles = new String[fileList.size()];
allFiles = fileList.toArray(allFiles);
mediaScanner = new MediaScanner();
try{
mediaScanner.scanFile(getContext(), allFiles, null, mainActivity.logger);
} catch (Exception e) {
}
publishProgress(100);
SystemClock.sleep(400);
return true;
} catch (SQLException | IOException e) {
} else {
//Directories don't exist
if (!exportDir.exists()) {
} else if (!subjectDataDir.exists()) {
return false;
}
}
public void onProgressUpdate(Integer ... progress){
dialog.setProgress(progress[0]);
if (progress[0] == 100){
dialog.setMessage("Quitting...");
}
}
protected void onPostExecute(final Boolean success) {
if (dialog.isShowing()) {
dialog.dismiss();
}
if (success) {
//Restart app and go back to login screen
quitSession();
}
}
//Recursive file lister for MTP
private List<String> getListFiles(File parentDir) {
ArrayList<String> inFiles = new ArrayList<>();
File[] files = parentDir.listFiles();
//Loop through everything in base directory, including folders
for (File file : files) {
if (file.isDirectory()) {
//Recursively add files from subdirectories
inFiles.addAll(getListFiles(file));
} else {
inFiles.add(file.getAbsolutePath());
}
}
return inFiles;
}
}
}
After a lot of sensor data has been recorded, I get the error every time the user navigates to the fragment. But when the button is clicked, I get the error continuously every 2 seconds.
The sensor recording service code can be found in my other question: Android sending messages between fragment and service
This fragment calls a number of methods from my DBHelper class (which is set up as a singleton):
public class DBHelper extends SQLiteOpenHelper {
SQLiteDatabase db;
CSVWriter csvWrite;
Cursor curCSV;
static Handler messageHandler;
private static DBHelper sInstance;
public static synchronized DBHelper getInstance(Context context) {
if (sInstance == null) {
sInstance = new DBHelper(context.getApplicationContext());
Log.d(TAG, "New DBHelper created");
}
return sInstance;
}
public static synchronized DBHelper getInstance(Context context, Handler handler) {
if (sInstance == null) {
sInstance = new DBHelper(context.getApplicationContext());
Log.d(TAG, "New DBHelper created");
}
messageHandler = handler;
return sInstance;
}
private DBHelper(Context context) {
super(context, DATABASE_NAME, null, DATABASE_VERSION);
db = this.getWritableDatabase();
}
public boolean checkSubjectDataExists(Short subNum) throws SQLException {
//Check if sensor data, for this subject, exists in the temp data table
String query = "SELECT * FROM " + DATA_TABLE_NAME_TEMP + " WHERE " + DATA_SUBJECT + "=" + subNum;
Cursor c = db.rawQuery(query, null);
boolean exists = (c.getCount() > 0);
c.close();
return exists;
}
public void copyTempData() throws SQLException{
String copySubjectSQL = "INSERT INTO " + SUBJECTS_TABLE_NAME + " SELECT * FROM " + SUBJECTS_TABLE_NAME_TEMP;
db.execSQL(copySubjectSQL);
String copyDataSQL = "INSERT INTO " + DATA_TABLE_NAME + " SELECT * FROM " + DATA_TABLE_NAME_TEMP;
db.execSQL(copyDataSQL);
}
public void exportTrackingSheet(File outputFile) throws SQLException, IOException {
csvWrite = new CSVWriter(new FileWriter(outputFile));
curCSV = db.rawQuery("SELECT * FROM " + SUBJECTS_TABLE_NAME, null);
csvWrite.writeNext(curCSV.getColumnNames());
while (curCSV.moveToNext()) {
String arrStr[] = {curCSV.getString(0), curCSV.getString(1), curCSV.getString(2),
curCSV.getString(3), curCSV.getString(4), curCSV.getString(5), curCSV.getString(6)};
csvWrite.writeNext(arrStr);
}
csvWrite.close();
curCSV.close();
}
public void exportSubjectData(File outputFile, String subNum) throws IOException, SQLException {
csvWrite = new CSVWriter(new FileWriter(outputFile));
curCSV = db.rawQuery("SELECT * FROM " + DATA_TABLE_NAME + " WHERE id = " + subNum, null);
csvWrite.writeNext(curCSV.getColumnNames());
Integer writeCounter = 0;
Integer numRows = curCSV.getCount();
while (curCSV.moveToNext()) {
writeCounter++;
String arrStr[] = {curCSV.getString(0), curCSV.getString(1), curCSV.getString(2),
curCSV.getString(3), curCSV.getString(4), curCSV.getString(5),
curCSV.getString(6), curCSV.getString(7), curCSV.getString(8),
curCSV.getString(9), curCSV.getString(10), curCSV.getString(11),
curCSV.getString(12), curCSV.getString(13), curCSV.getString(14),
curCSV.getString(15), curCSV.getString(16), curCSV.getString(17),
curCSV.getString(18), curCSV.getString(19), curCSV.getString(20),
curCSV.getString(21), curCSV.getString(22), curCSV.getString(23),
curCSV.getString(24), curCSV.getString(25)};
csvWrite.writeNext(arrStr);
if ((writeCounter % 1000) == 0){
csvWrite.flush();
}
Double progressPercent = Math.ceil(((float) writeCounter / (float) numRows)*100);
Message msg = Message.obtain();
msg.obj = progressPercent;
msg.setTarget(messageHandler);
msg.sendToTarget();
}
csvWrite.close();
curCSV.close();
}
}
The DBHelper and any SQL connections are closed in onDestroy
of my main activity
My media scanner class is also pretty straightforward:
public class MediaScanner {
protected void scanFile(final Context context, String[] files, String[] mimeTypes, final Logger logger) {
MediaScannerConnection.scanFile(context, files, mimeTypes,
new MediaScannerConnection.OnScanCompletedListener() {
@Override
public void onScanCompleted(String path, Uri uri) {
//Log some info
}
}
);
}
}
Can anyone see anything special about my fragment code that is causing this cursor window error?