I've developed an app for Android which stores car filling information in an SQLite database. Everything seems to work fine on my 4.0.3 phone and my 4.0.3 AVD, but I would like it to also work in Jelly Bean and newer versions. The app really doesn't do anything very complicated. When I fire the app up in the emulated Jelly Bean device I get a FATAL EXCEPTION error:
09-05 01:45:40.311: W/dalvikvm(1062): threadid=1: thread exiting with uncaught exception >(group=0x414c4700) 09-05 01:45:40.331: E/AndroidRuntime(1062): FATAL EXCEPTION: main 09-05 01:45:40.331: E/AndroidRuntime(1062): java.lang.RuntimeException: Unable to start >activity ComponentInfo{ard.util.fueltracker/ard.util.fueltracker.TitleScreenActivity}: >java.lang.IllegalStateException: attempt to re-open an already-closed object: >SQLiteDatabase: /data/data/ard.util.fueltracker/databases/vehicleDatabase 09-05 01:45:40.331: E/AndroidRuntime(1062): at >android.app.ActivityThread.performLaunchActivity(ActivityThread.java:2211)
In my initial searches in Google I thought I had Instance issues somehow, so I followed Approach #1 here (http://www.androiddesignpatterns.com/2012/05/correctly-managing-your-sqlite-database.html), but this doesn't seem to make a difference. Still works in ICS with the changes too.
Here's the code from my TitleScreenActivity:
package ard.util.fueltracker;
import java.util.List;
import ard.util.fueltracker.util.SystemUiHider;
import android.annotation.SuppressLint;
import android.annotation.TargetApi;
import android.app.Activity;
import android.os.Build;
import android.os.Bundle;
import android.os.Handler;
import android.view.MotionEvent;
import android.view.View;
import android.view.Window;
import android.view.WindowManager;
import android.content.Intent;
import android.widget.AdapterView;
import android.widget.AdapterView.OnItemSelectedListener;
import android.widget.ArrayAdapter;
import android.widget.Button;
import android.widget.Spinner;
import android.widget.Toast;
/**
* An example full-screen activity that shows and hides the system UI (i.e.
* status bar and navigation/system bar) with user interaction.
*
* @see SystemUiHider
*/
@SuppressLint("NewApi")
public class TitleScreenActivity extends Activity implements OnItemSelectedListener {
/**
* Whether or not the system UI should be auto-hidden after
* {@link #AUTO_HIDE_DELAY_MILLIS} milliseconds.
*/
private static final boolean AUTO_HIDE = false;
/**
* If {@link #AUTO_HIDE} is set, the number of milliseconds to wait after
* user interaction before hiding the system UI.
*/
private static final int AUTO_HIDE_DELAY_MILLIS = 3000;
/**
* If set, will toggle the system UI visibility upon interaction. Otherwise,
* will show the system UI visibility upon interaction.
*/
private static final boolean TOGGLE_ON_CLICK = false;
/**
* The flags to pass to {@link SystemUiHider#getInstance}.
*/
//private static final int HIDER_FLAGS = SystemUiHider.FLAG_HIDE_NAVIGATION;
private static final int HIDER_FLAGS = 0;
/**
* The instance of the {@link SystemUiHider} for this activity.
*/
private SystemUiHider mSystemUiHider;
// Spinner element
Spinner spinner;
// Buttons
Button btnAdd;
Button btnLogo;
@SuppressLint("NewApi")
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
getWindow().requestFeature(Window.FEATURE_ACTION_BAR); //new
getActionBar().hide(); //new
getWindow().setFlags(
WindowManager.LayoutParams.FLAG_FULLSCREEN,WindowManager.LayoutParams.FLAG_FULLSCREEN);
setContentView(R.layout.activity_title_screen);
// Spinner element
spinner = (Spinner) findViewById(R.id.titleSelectionSpinner);
// buttons
btnAdd = (Button) findViewById(R.id.button_go);
btnLogo = (Button) findViewById(R.id.button_ard_logo);
// Spinner click listener
spinner.setOnItemSelectedListener(this);
// Check for Add New Vehicle entry to create menu option in drop down list
checkAddNewVehicle();
// Loading spinner data from database
loadSpinnerData();
//"Go" button actions
btnAdd.setOnClickListener(new View.OnClickListener() {
public void onClick(View arg0) {
//Starting a new Intent
Intent screenAddVehicle = new Intent(getApplicationContext(), AddVehicleActivity.class);
Intent screenRecordViewing = new Intent(getApplicationContext(), RecordViewing.class);
String SpinnerChoice = spinner.getSelectedItem().toString();
//Sending data to another Activity
//store value of vehicle label for Record Viewing screen
screenRecordViewing.putExtra("vehicleLabel", SpinnerChoice);
//interpret Spinner choice as Menu items
//"Add New Vehicle" always at Position 0
if (SpinnerChoice == spinner.getItemAtPosition(0).toString()) {
startActivity(screenAddVehicle);
} else {
//display value of selection if not "Add New Vehicle"
//Toast.makeText(spinner.getContext(), "Selection:" + SpinnerChoice, Toast.LENGTH_LONG).show();
//go to Record Viewing screen
startActivity(screenRecordViewing);
}
}
});
//"Logo" button - display program copyright
btnLogo.setOnClickListener(new View.OnClickListener() {
public void onClick(View arg0) {
Toast.makeText(getApplicationContext(), "FuelTracker is Copyright 2013 by Authentic Ruby Designs", Toast.LENGTH_LONG).show();
}
});
final View controlsView = findViewById(R.id.fullscreen_content_controls);
final View contentView = findViewById(R.id.fullscreen_content);
// Set up an instance of SystemUiHider to control the system UI for
// this activity.
mSystemUiHider = SystemUiHider.getInstance(this, contentView,
HIDER_FLAGS);
mSystemUiHider.setup();
mSystemUiHider
.setOnVisibilityChangeListener(new SystemUiHider.OnVisibilityChangeListener() {
// Cached values.
int mControlsHeight;
int mShortAnimTime;
@Override
@TargetApi(Build.VERSION_CODES.HONEYCOMB_MR2)
public void onVisibilityChange(boolean visible) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB_MR2) {
// If the ViewPropertyAnimator API is available
// (Honeycomb MR2 and later), use it to animate the
// in-layout UI controls at the bottom of the
// screen.
if (mControlsHeight == 0) {
mControlsHeight = controlsView.getHeight();
}
if (mShortAnimTime == 0) {
mShortAnimTime = getResources().getInteger(
android.R.integer.config_shortAnimTime);
}
controlsView
.animate()
.translationY(visible ? 0 : mControlsHeight)
.setDuration(mShortAnimTime);
} else {
// If the ViewPropertyAnimator APIs aren't
// available, simply show or hide the in-layout UI
// controls.
controlsView.setVisibility(visible ? View.VISIBLE
: View.GONE);
}
if (visible && AUTO_HIDE) {
// Schedule a hide().
delayedHide(AUTO_HIDE_DELAY_MILLIS);
}
}
});
// Set up the user interaction to manually show or hide the system UI.
contentView.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
if (TOGGLE_ON_CLICK) {
mSystemUiHider.toggle();
} else {
mSystemUiHider.show();
}
}
});
// Upon interacting with UI controls, delay any scheduled hide()
// operations to prevent the jarring behavior of controls going away
// while interacting with the UI.
findViewById(R.id.button_go).setOnTouchListener(
mDelayHideTouchListener);
}
@Override
protected void onPostCreate(Bundle savedInstanceState) {
super.onPostCreate(savedInstanceState);
// Trigger the initial hide() shortly after the activity has been
// created, to briefly hint to the user that UI controls
// are available.
//delayedHide(100);
}
/**
* Touch listener to use for in-layout UI controls to delay hiding the
* system UI. This is to prevent the jarring behavior of controls going away
* while interacting with activity UI.
*/
View.OnTouchListener mDelayHideTouchListener = new View.OnTouchListener() {
@Override
public boolean onTouch(View view, MotionEvent motionEvent) {
if (AUTO_HIDE) {
delayedHide(AUTO_HIDE_DELAY_MILLIS);
}
return false;
}
};
Handler mHideHandler = new Handler();
Runnable mHideRunnable = new Runnable() {
@Override
public void run() {
mSystemUiHider.hide();
}
};
/**
* Schedules a call to hide() in [delay] milliseconds, canceling any
* previously scheduled calls.
*/
private void delayedHide(int delayMillis) {
mHideHandler.removeCallbacks(mHideRunnable);
mHideHandler.postDelayed(mHideRunnable, delayMillis);
}
@Override
public void onItemSelected(AdapterView<?> arg0, View arg1, int arg2,
long arg3) {
// TODO Auto-generated method stub
}
@Override
public void onNothingSelected(AdapterView<?> arg0) {
// TODO Auto-generated method stub
}
/**
* Function to check that Add New Vehicle is the first label in the DB table
*/
private void checkAddNewVehicle() {
// database handler
//DatabaseHandler db = new DatabaseHandler(getApplicationContext());
DatabaseHandler db = DatabaseHandler.getInstance(getApplicationContext());
// table data
List<String> labels = db.getAllLabels();
//Check for "Add New Vehicle" entry at first row of table
if (labels.isEmpty() ) {
db.insertLabel("Add New Vehicle");
}
}
/**
* Function to load the spinner data from SQLite database
* */
private void loadSpinnerData() {
// database handler
//DatabaseHandler db = new DatabaseHandler(getApplicationContext());
DatabaseHandler db = DatabaseHandler.getInstance(getApplicationContext());
// Spinner Drop down elements
List<String> labels = db.getAllLabels();
// Creating adapter for spinner
ArrayAdapter<String> dataAdapter = new ArrayAdapter<String>(this,
android.R.layout.simple_spinner_item, labels);
// Drop down layout style - list view with radio button
dataAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
// attaching data adapter to spinner
spinner.setAdapter(dataAdapter);
}
}
And here's my DatabaseHandler class:
package ard.util.fueltracker;
import java.text.DecimalFormat;
import java.util.ArrayList;
import java.util.List;
import android.content.ContentValues;
import android.content.Context;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteOpenHelper;
import android.widget.TableLayout;
public class DatabaseHandler extends SQLiteOpenHelper {
//create static instance
private static DatabaseHandler mInstance = null;
// Database Version
private static final int DATABASE_VERSION = 1;
// Database Name
private static final String DATABASE_NAME = "vehicleDatabase";
// Labels table name
private static final String TABLE_LABELS = "labels";
// Fuel Data table name
private static final String TABLE_FUELDATA = "fuel_data";
// Labels Table Columns names
private static final String KEY_ID = "id";
private static final String KEY_NAME = "name";
// Fuel Data Tables Columns names
private static final String KEY_ENTRY = "entry_id";
private static final String FIELD_VEHICLEID = "vehicle_id";
private static final String FIELD_DATE = "date";
private static final String FIELD_FTYPE = "fuel_type";
private static final String FIELD_BRAND = "brand";
private static final String FIELD_PRICE = "price";
private static final String FIELD_KMS = "kms";
private static final String FIELD_LITRES = "litres";
private static final String FIELD_LPER = "l_per";
private static final String FIELD_MPG = "mpg";
// Fuel Data Columns for display - query shorthand
private static final String[] fuelDataDisplayCols = { FIELD_DATE, FIELD_FTYPE, FIELD_BRAND, FIELD_PRICE,
FIELD_KMS, FIELD_LITRES, FIELD_LPER, FIELD_MPG };
public static DatabaseHandler getInstance(Context ctx) {
//Use the application context to avoid leaking Activity's context
if (mInstance == null) {
mInstance = new DatabaseHandler(ctx.getApplicationContext());
}
return mInstance;
}
private DatabaseHandler(Context context) {
super(context, DATABASE_NAME, null, DATABASE_VERSION);
}
// Creating Tables
@Override
public void onCreate(SQLiteDatabase db) {
// Category table create query
String CREATE_CATEGORIES_TABLE = "CREATE TABLE " + TABLE_LABELS + "("
+ KEY_ID + " INTEGER PRIMARY KEY," + KEY_NAME + " TEXT)";
db.execSQL(CREATE_CATEGORIES_TABLE);
// Fuel Data table create query
String CREATE_FUELDATA_TABLE = "CREATE TABLE " + TABLE_FUELDATA + "("
+ KEY_ENTRY + " INTEGER PRIMARY KEY," + FIELD_VEHICLEID + " INTEGER,"
+ FIELD_DATE + " TEXT," + FIELD_FTYPE + " TEXT," + FIELD_BRAND +
" TEXT," + FIELD_PRICE + " REAL," + FIELD_KMS + " REAL,"
+ FIELD_LITRES + " REAL," + FIELD_LPER + " REAL," + FIELD_MPG +
" REAL)";
db.execSQL(CREATE_FUELDATA_TABLE);
db.close();
}
// Upgrading database
@Override
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
// Drop older table if existed
db.execSQL("DROP TABLE IF EXISTS " + TABLE_LABELS);
db.execSQL("DROP TABLE IF EXISTS " + TABLE_FUELDATA);
// Create tables again
onCreate(db);
}
/**
* Inserting new label into Labels table
* */
public void insertLabel(String label){
SQLiteDatabase db = this.getWritableDatabase();
ContentValues values = new ContentValues();
values.put(KEY_NAME, label);
// Inserting Row
db.insert(TABLE_LABELS, null, values);
db.close(); // Closing database connection
}
/**
* Inserting entry data into Fuel Data table
*/
//public void insertFuelData(int vehicleID, String date, String ftype, String brand,
// float price, float kms, float litres) {
public void insertFuelData(ArrayList<String> fuelEntryData) {
SQLiteDatabase dbWrite = this.getWritableDatabase();
ArrayList<String> fuelEntry = fuelEntryData;
ContentValues values = new ContentValues();
//turn array values into individual field values with correct datatype
int vehicleID = getVehicleID(fuelEntry.get(0));
String date = fuelEntry.get(1);
String ftype = fuelEntry.get(2);
String brand = fuelEntry.get(3);
double price = Double.parseDouble((fuelEntry.get(4)));
double kms = Double.parseDouble((fuelEntry.get(5)));
double litres = Double.parseDouble((fuelEntry.get(6)));
//values for calculations
double lper;
double mpg;
//Format calculations to round to one significant digit
DecimalFormat df = new DecimalFormat("###.#");
//calculate Liters/100Kilometres
lper = (100 / (kms / litres));
//calculate U.S. Miles Per Gallon
mpg = (kms / litres) * 2.35;
//Prepare data and Insert row into table
values.put(FIELD_VEHICLEID, vehicleID);
values.put(FIELD_DATE, date);
values.put(FIELD_FTYPE, ftype);
values.put(FIELD_BRAND, brand);
values.put(FIELD_PRICE, price);
values.put(FIELD_KMS, kms);
values.put(FIELD_LITRES, litres);
values.put(FIELD_LPER, df.format(lper));
values.put(FIELD_MPG, df.format(mpg));
dbWrite.insert(TABLE_FUELDATA, null, values);
dbWrite.close();
}
/**
* Getting all labels
* returns list of labels
* */
public List<String> getAllLabels(){
List<String> labels = new ArrayList<String>();
// Select All Query
String selectQuery = "SELECT * FROM " + TABLE_LABELS;
SQLiteDatabase db = this.getReadableDatabase();
Cursor cursor = db.rawQuery(selectQuery, null);
// looping through all rows and adding to list
if (cursor.moveToFirst()) {
do {
labels.add(cursor.getString(1));
} while (cursor.moveToNext());
}
// closing connection
cursor.close();
db.close();
// returning lables
return labels;
}
/**
* Get entries based on vehicle id
*/
public List<List<String>> getFuelData(int vehicleID) {
//List of Lists - each row/entry of data is a separate List
List<List<String>> fuelDataTable = new ArrayList<List<String>>();
List<String> fuelDataRow = new ArrayList<String>();
//Select Query
String selectQuery = "SELECT * FROM " + TABLE_FUELDATA +
" WHERE vehicle_id = ? ";
SQLiteDatabase db = this.getReadableDatabase();
Cursor cursor = db.rawQuery(selectQuery, new String[] { String.valueOf(vehicleID) });
// looping through all rows and 9 columns (not incl key) and adding to list
if (cursor.moveToFirst()) {
do {
for(int i = 1; i < 10; i++) {
//doesn't work with non-string values
//fuelDataRow.add(cursor.getString(i));
}
//add each row in its entirety to the List; multi-dimenionsal list
fuelDataTable.add(fuelDataRow);
//empty fuelDataRow
fuelDataRow.clear();
} while (cursor.moveToNext());
}
// closing connection
cursor.close();
db.close();
//return data
return fuelDataTable;
}
public List<String> getFuelDataRows(int vehicleID) {
List<String> fuelDataTable = new ArrayList<String>();
SQLiteDatabase db = this.getReadableDatabase();
//Get data from database table
Cursor c = db.query(TABLE_FUELDATA, fuelDataDisplayCols, " vehicle_id=? ", new String[] { String.valueOf(vehicleID) }, null, null, FIELD_DATE);
//Insert Header row into Array
fuelDataTable.add("Date");
fuelDataTable.add("Type");
fuelDataTable.add("Brand");
fuelDataTable.add("Price");
fuelDataTable.add("KMs");
fuelDataTable.add("Litres");
fuelDataTable.add("L/100");
fuelDataTable.add("MPG");
//Go to beginning of Cursor data and loop through
c.moveToFirst();
while (!c.isAfterLast()) {
//add each cell in the row to List array
fuelDataTable.add(c.getString(c.getColumnIndex(FIELD_DATE)));
fuelDataTable.add(c.getString(c.getColumnIndex(FIELD_FTYPE)));
fuelDataTable.add(c.getString(c.getColumnIndex(FIELD_BRAND)));
fuelDataTable.add(String.valueOf(c.getDouble(c.getColumnIndex(FIELD_PRICE))));
fuelDataTable.add(String.valueOf(c.getDouble(c.getColumnIndex(FIELD_KMS))));
fuelDataTable.add(String.valueOf(c.getDouble(c.getColumnIndex(FIELD_LITRES))));
fuelDataTable.add(String.valueOf(c.getDouble(c.getColumnIndex(FIELD_LPER))));
fuelDataTable.add(String.valueOf(c.getDouble(c.getColumnIndex(FIELD_MPG))));
c.moveToNext();
}
// Make sure to close the cursor
c.close();
db.close();
return fuelDataTable;
}
public int getVehicleID(String label) {
// set variables
SQLiteDatabase dbReader = this.getReadableDatabase();
String vLabel = label;
//Query String
String selectQuery = "SELECT " + KEY_ID + " FROM " + TABLE_LABELS +
" WHERE name = ? ";
Cursor c = dbReader.rawQuery(selectQuery, new String[] { vLabel });
//avoid out of bounds exception
c.moveToFirst();
//extract value as integer
int vID = c.getInt(c.getColumnIndex(KEY_ID));
c.close();
return vID;
}
}
Some of it probably looks ugly - this is my first app project and Eclipse auto-populated some of the methods upon creation - but it is working OK in Ice Cream Sandwich.
Thanks for any clues.
db.close()
is not needed inpublic void onCreate()
. This will probably fix your problem.