Android cursorwindow memory error in a specific fragment
Asked Answered
J

0

7

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?

Joli answered 27/4, 2016 at 1:50 Comment(4)
can you post the line number where this error is occurring ?Seriatim
There is no line number. The message just comes up in logcat, but nothing actually breaks or stops working as a result of this...just the UI slows downJoli
Have you tried to run your code in a low end device? Maybe it could throw an exception on a specific code line.Ellie
I havent. I dont have access to any other devices for testing, and it doesnt work in an emulator because I cant simulate sensor data (I dont think?)Joli

© 2022 - 2024 — McMap. All rights reserved.