I had a problem which I already solved but I still wants to know WHY
the solution solved it.
I wrote an android app that had a sqlite db after a couple of times I debugged it
The oncreate method in the db didnt got called (even though everything worked fine before)
After I changed the db version number from 1 to 2 everything worked fine again
Even though I uninstalled the app through the app manager and also removed the cache and
The local database information.
My question is as follows - does the local database data is saved somewhere else?
In case it doesn't - Why did it worked only after I upgraded the version number
not even when I erased all the app related data?
/**
* A class to handle sqlite reads/writes of user related data to be collected
*/
public class UserDataManager extends SQLiteOpenHelper {
// Class Variables
private final String TAG = UserDataManager.class.getSimpleName();
// Database Version
private static final int DATABASE_VERSION = 1;
// Database Name
public static final String DATABASE_NAME = "tmc";
// Tables
private static final String TABLE_USER = "user";
// Tables and table columns names
private String CREATE_USER_TABLE;
private static final String COLUMN_USER_ID = "user_id";
private static final String COLUMN_USER_MAIL = "email";
private static final String COLUMN_USER_ACTIVE = "user_active";
private static final String COLUMN_USER_NAME = "name";
private static final String COLUMN_USER_PASSWORD = "password";
private static final String COLUMN_USER_PHONE_NUMBER = "phone_number";
/**
* Class constructor
*
* #param context
* The context to run in
*/
public UserDataManager(Context context) {
super(context, DATABASE_NAME, null, DATABASE_VERSION);
}
// Creating Tables
#Override
public void onCreate(SQLiteDatabase db) {
CREATE_USER_TABLE = "CREATE TABLE IF NOT EXISTS " + TABLE_USER + " ("
+ COLUMN_USER_ID + " INTEGER PRIMARY KEY NOT NULL, "
+ COLUMN_USER_MAIL + " VARCHAR(64) NOT NULL, "
+ COLUMN_USER_NAME + " VARCHAR(64) NOT NULL, "
+ COLUMN_USER_PASSWORD + " VARCHAR(64) NOT NULL, "
+ COLUMN_USER_PHONE_NUMBER + " VARCHAR(64) NOT NULL, "
+ COLUMN_USER_ACTIVE + " INT NOT NULL);";
// create the tables
db.execSQL(CREATE_USER_TABLE);
}
// Upgrading database
#Override
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
// Drop older table if existed
db.execSQL("DROP TABLE IF EXISTS " + TABLE_USER);
// Create tables again
onCreate(db);
}
/**
* Adding a user to the database
*
* #param userId
* The created user id
* #param userName
* The user name
* #param userEmail
* The user email
* #param userPassword
* The user password
* #param userPhoneNumber
* The user phone number
* #param isActive
* Set to 1 if the user is active 0 otherwise
* #return True if the user added successfully false otherwise
*/
public boolean AddUser(int userId, String userName, String userEmail,
String userPassword, String userPhoneNumber, boolean isActive) {
// method variables
long rowId;
boolean pass = false;
int active = isActive ? 1 : 0;
SQLiteDatabase db = null;
ContentValues row = null;
// try to add the user to the db
try {
row = new ContentValues();
db = this.getWritableDatabase();
db.delete(TABLE_USER, null, null);
row.put(COLUMN_USER_ID, userId);
row.put(COLUMN_USER_NAME, userName);
row.put(COLUMN_USER_MAIL, userEmail);
row.put(COLUMN_USER_PASSWORD, userPassword);
row.put(COLUMN_USER_CAR_NUMBER, userPhoneNumber);
row.put(COLUMN_USER_ACTIVE, active);
rowId = db.insert(TABLE_USER, null, row);
if (rowId > -1) {
pass = true;
}
} catch (SQLException exception) {
Log.e(TAG, exception.getMessage());
} finally {
if (db != null) {
// close database connection
db.close();
}
}
return pass;
}
/**
* Get the current registered user
*
* #return The id of the column of the registered user
*/
public int GetRegisteredUserId() {
// method variables
int columnIndex = -1;
int userId = -1;
SQLiteDatabase db = null;
Cursor cursor = null;
// try to get the user from the database
try {
db = this.getReadableDatabase();
cursor = db.query(TABLE_USER, new String[] { COLUMN_USER_ID },
null, null, null, null, null);
if (cursor != null) {
boolean moved = cursor.moveToFirst();
if (moved) {
columnIndex = cursor.getColumnIndex(COLUMN_USER_ID);
if (columnIndex > -1) {
userId = cursor.getInt(columnIndex);
}
}
}
} catch (SQLException exception) {
Log.e(TAG, exception.getMessage());
} finally {
if (cursor != null)
// release cursor
cursor.close();
if (db != null)
// close database connection
db.close();
}
return userId;
}
/**
* Get the current user email
*
* #return The id of the column of the registered user
*/
public String GetRegisteredUserEmail() {
// method variables
int columnIndex = -1;
String userEmail = null;
SQLiteDatabase db = null;
Cursor cursor = null;
// try to get the user from the database
try {
db = this.getReadableDatabase();
cursor = db.query(TABLE_USER, new String[] { COLUMN_USER_MAIL },
null, null, null, null, null);
if (cursor != null) {
boolean moved = cursor.moveToFirst();
if (moved) {
columnIndex = cursor.getColumnIndex(COLUMN_USER_MAIL);
if (columnIndex > -1) {
userEmail = cursor.getString(columnIndex);
}
}
}
} catch (SQLException exception) {
Log.e(TAG, exception.getMessage());
} finally {
if (cursor != null)
// release cursor
cursor.close();
if (db != null)
// close database connection
db.close();
}
return userEmail;
}
/**
* Get the current user password
*
* #return The password of the current logged user
*/
public String GetRegisteredUserPassword() {
// method variables
int columnIndex = -1;
String userPassword = null;
SQLiteDatabase db = null;
Cursor cursor = null;
// try to get the user from the database
try {
db = this.getReadableDatabase();
cursor = db.query(TABLE_USER,
new String[] { COLUMN_USER_PASSWORD }, null, null, null,
null, null);
if (cursor != null) {
boolean moved = cursor.moveToFirst();
if (moved) {
columnIndex = cursor.getColumnIndex(COLUMN_USER_PASSWORD);
if (columnIndex > -1) {
userPassword = cursor.getString(columnIndex);
}
}
}
} catch (SQLException exception) {
Log.e(TAG, exception.getMessage());
} finally {
if (cursor != null)
// release cursor
cursor.close();
if (db != null)
// close database connection
db.close();
}
return userPassword;
}
/**
* Get number of rows in the user table
*
* #return the number of the rows in the user table (How many users are
* saved in the DB)
*/
public int GetRowCount() {
// method variables
int rowsCount = 0;
SQLiteDatabase db = null;
Cursor cursor = null;
// try to get the user from the database
try {
db = this.getReadableDatabase();
cursor = db.query(TABLE_USER, null, null, null, null, null, null);
if (cursor != null) {
boolean moved = cursor.moveToFirst();
if (moved) {
do {
rowsCount++;
} while (cursor.moveToNext());
}
}
} catch (SQLException exception) {
Log.e(TAG, exception.getMessage());
} finally {
if (cursor != null)
// release cursor
cursor.close();
if (db != null)
// close database connection
db.close();
}
return rowsCount;
}
/**
* Remove a user from the database
*
* #param userId
* The user id
*/
public void LogoutUser() {
// method variables
SQLiteDatabase db = null;
// try to remove a user from the database
try {
db = this.getWritableDatabase();
onUpgrade(db, DATABASE_VERSION, DATABASE_VERSION);
} catch (SQLException exception) {
Log.e(TAG, exception.getMessage());
} finally {
if (db != null) {
// close database connection
db.close();
}
}
}
/**
* Set a user to be active or not
*
* #param isActive
* 1 if the cigarette is active 0 otherwise
* #return True if the cigarette active field has changed false otherwise
*/
public boolean SetUserActive(boolean isActive) {
// method variables
int rowsAffected;
int active = isActive ? 1 : 0;
long userId;
String userIdString;
boolean pass = true;
SQLiteDatabase db = null;
ContentValues values = null;
// try to remove a device from the database
try {
userId = GetRegisteredUserId();
if (userId > -1) {
userIdString = String.valueOf(userId);
db = this.getWritableDatabase();
values = new ContentValues();
values.put(COLUMN_USER_ACTIVE, active);
rowsAffected = db.update(TABLE_USER, values, COLUMN_USER_ID
+ " = ?", new String[] { userIdString });
if (rowsAffected != 1) {
pass = false;
}
}
} catch (SQLException exception) {
Log.e(TAG, exception.getMessage());
} finally {
if (db != null) {
// close database connection
db.close();
}
}
return pass;
}
}
Notes -
1. Please note that my device is rooted and so after inserting the data to the db im changing the permissions on the db file for 777 so I can pull it from the phone to see whats in it (i.e. did the query pass or not)
2. The error that is being thrown is "android.database.sqlite.SQLiteException: no such table: user "
Chocolate chips cookies will be granted for any answer... =)
Why did it worked only after I upgraded the version number not even when I erased all the app related data?
As soon as you start working with either of getReadableDatabase() ,getWriteableDatabase() or any other SQLiteHelper class code. The first method calls is onCreate(SQLiteDatabase db) which creates Database under your application database path
/data/data/PACKAGE_NAME/databases/tmc (in your case).
If you modify your Database structure in SQliteHelper the first method get called is onUpgrage() which checks whether Database_Version get modified or not. If it's then it execute onUpgrade() with series of DROP TABLE IF EXIST followed by onCreate() which again create your database with new structure under your application path by replacing your previous database file.
Clearing Cached data using Application Manager indeed clear database and cached data of that application. But SQLiteHelper did check for Database_Version with old and new one. If new one is greater than old one. It does call onUpgrage() followed by onCreate().
When you intent to use Database with Android Application it get store under /data/data/PACKAGE_NAME/databases/tmc with application process security. Unable to access database file unless you have rooted Android device in which you already have.
One can create Developer Options or anything you like just to pull database from your application process to SD Card for unrooted devices.
Copy database file from application process path to SD Card for unrooted devices.
try {
File sd = Environment.getExternalStorageDirectory();
File data = Environment.getDataDirectory();
if (sd.canWrite()) {
String currentDBPath = "/data/data/" + getPackageName() + "/databases/ZnameDB"; //Your DATABASE_NAME
String backupDBPath = "ZnameDB_Dev.db"; //DATABASE_COPY_NAME UNDER SDCARD
File currentDB = new File(currentDBPath);
File backupDB = new File(sd, backupDBPath);
if (currentDB.exists()) {
FileChannel src = new FileInputStream(currentDB).getChannel();
FileChannel dst = new FileOutputStream(backupDB).getChannel();
dst.transferFrom(src, 0, src.size());
src.close();
dst.close();
Toast.makeText(SettingsActivity.this, "Database Transfered!", Toast.LENGTH_SHORT).show();
}
}
} catch (Exception e) {
Log.e(TAG, e.toString());
}
Answering your first question, all data is stored under YOUR_PACKAGE/databases/DATABASE.db only.
If you erase app through app manager all data is removed, just package remains. If you uninstall your app everything is cleared including package folder. Even if you set for your app install location to external SD card, database is stored internally anyway.
From documentation:
The .apk file is saved on the external storage, but all private user
data, databases, optimized .dex files, and extracted native code are
saved on the internal device memory.
SQLiteOpenHelper logic is simple:
checks if DB exists, if not new DB is created
retrieve DB version, initial value is 0, that's why in your app minimum value for DB version is 1 to call onCreate() method at least once
if version is equal to 0, onCreate() is called, or
if version is different than the one provided by your code onUpgrade() or onDowngrade() is called
So, whenever you upgrade your scheme, version number MUST be increased, there is no argue about that, in order to allow your app work properly.
Now, in your specific case I can only guess. I would say that erasing your package was not entirely successful and bits of data was left, especially if you mentioned that you did some manual modification on DB file. Maybe it has something to do with Android version running on your device but you didn't mentioned which one is it.
That's all. I hope my answer is satisfying.
Can you exec PRAGMA user_version; in your adb to get the db version? According to the source code of SQLiteOpenHelper, SQLite.getVersion() equals to SQLiteOpenHelper.mNewVersion, so onCreate() method won't be invoked. When you chmod 777 on db file, the user_version will be modified too.
Assuming the databases are not deleted while uninstalling app, this seems plausible. The databases are stored here DDMS/data/data/PACKAGE_NAME/databases/YOUR_DB_FILE. You can only see this if your phone is rooted.
Please check whether this assumption is true and correct me if I am wrong.
Thanks
Related
I'm using an external database inside my android application and it directly embeds inside the apk package after compiling.
As I want to implement in app purchase in order to access some of its data I don't want to leave it without any encryption.
I used Sqlcipher library but it makes the app too big and slow.
Isn't there any other way to do this? For example an algorithm to encrypt the strings so I put the encrypted text in database and decrypt it inside application code?
The following is an example App that Encrypts part of the data that can then be turned on. It is based upon the code in my comment.
Stage 1 - The Master Database.
To start with you need the database that is to be the basis of the encrypted database (i.e. the MASTER database that IS NOT include in the App, it's use is to create the Encrypted database (or databases, perhaps a library, each database with a unique password/secret key if you wanted greater security)) in part consider this (as is used throughout the example) :-
As you can see this one will work by having a table called FreeData and another called PaidData. The tables definitions are the same EXCEPT that for the PaidData there is no ID column (the intention of this method is to decrypt the rows in the PaidData into the FreeData when/if the requested and the SecretKey (password) is valid.).
So The FreeData table looks like :-
The PaidData table looks like :-
So the only difference between the tables is the actual data contained within and that the id column is missing.
The id's will be generated when the encrypted data is extracted from the PaidData table, decrypted and the inserted into the FreeData table. Thus just one decryption is required to get access to the data.
Stage 2 - Generating the Encrypted Database for distribution with the App
This is done by a App just for this purpose using the EncryptDecrypt class very similar to the one at Encrypt data in SQLite
as per EncryptDecrypt.java
class EncryptDecrypt {
private Cipher cipher;
private static SecretKeySpec secretKeySpec;
private static IvParameterSpec ivParameterSpec;
private boolean do_encrypt = true;
/**
* Construct EncryptDecrypt instance that does not check user login-in
* mode, thus the assumption is that this user is NOT the special user
* NOUSER that doesn't require a password to login; this constructor
* is designed to ONLY be used when a user has been added by NOUSER,
* and to then encrypt the data using the enccryptForced method solely
* to encrypt any existing card data for the new user that has a password.
*
* #param context The context, required for database usage (user)
* #param skey The secret key to be used to encrypt/decrypt
*/
EncryptDecrypt(Context context, String skey) {
//DBUsersMethods users = new DBUsersMethods(context);
String saltasString = "there is no dark side of the moon it is all dark.";
String paddedskey = (skey + saltasString).substring(0,16);
secretKeySpec = new SecretKeySpec(paddedskey.getBytes(),"AES/CBC/PKCS5Padding");
ivParameterSpec = new IvParameterSpec((saltasString.substring(0,16)).getBytes());
try {
cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
} catch (Exception e){
//e.printStackTrace();
}
}
/**
* Normal encryption routine that will not encrypt data if the user is
* the special case NOUSER (i.e LOGIN mode is NOUSER), otherwise data
* is encrypted.
*
* #Param toEncrypt The string to be encrypted
* #return The encryted (or not if NOUSER) data as a string
*/
String encrypt(String toEncrypt) {
if (!do_encrypt) {
return toEncrypt;
}
byte[] encrypted;
try {
cipher.init(Cipher.ENCRYPT_MODE,secretKeySpec,ivParameterSpec);
encrypted = cipher.doFinal(toEncrypt.getBytes());
} catch (Exception e) {
//e.printStackTrace();
return null;
}
return Base64.encodeToString(encrypted, Base64.DEFAULT);
}
/**
* Encryption, irrespective of the USER type, noting that this should
* only be used in conjunction with an EncryptDecrypt instance created
* using the 2nd/extended constructor
*
* #param toEncrypt The string to be encrypted
* #return The encrypted data as a string
*/
String encryptForced(String toEncrypt) {
byte[] encrypted;
try {
cipher.init(Cipher.ENCRYPT_MODE,secretKeySpec,ivParameterSpec);
encrypted = cipher.doFinal(toEncrypt.getBytes());
} catch (Exception e) {
//e.printStackTrace();
return null;
}
return Base64.encodeToString(encrypted,Base64.DEFAULT);
}
/**
* Decrypt an encrypted string
* #param toDecrypt The encrypted string to be decrypted
* #return The decrypted string
*/
String decrypt(String toDecrypt) {
if (!do_encrypt) {
return toDecrypt;
}
byte[] decrypted;
try {
cipher.init(Cipher.DECRYPT_MODE,secretKeySpec,ivParameterSpec);
decrypted = cipher.doFinal(Base64.decode(toDecrypt,Base64.DEFAULT));
} catch (Exception e) {
//e.printStackTrace();
return null;
}
return new String(decrypted);
}
}
As this was designed for user login and multiple users where a salt was part of the database the salt has been hard coded using :- String saltasString = "there is no dark side of the moon it is all dark.";, The phrase can be changed as long as it is at least 16 characters in length (only the first 16 bytes are used).
A class is used to cater for potential flexibility/expansion where multiple tables can be specified or not for encryption and for multiple columns that can be encrypted, copied asis or skipped (e.g. id's would probably be skipped (in the example it's not even defined as a column).).
This class is TableColumnConvertList.java and is :-
public class TableColumnConvertList {
private ArrayList<TableEntry> tables;
public TableColumnConvertList() {
this.tables = new ArrayList<>();
}
public String[] getTables() {
String[] tableList = new String[tables.size()];
int ix = 0;
for (TableEntry te: this.tables) {
tableList[ix++] = te.getSourceTableName();
}
return tableList;
}
public String[] getTableColumnNamesToEncrypt(String tableName) {
String[] rv = null;
for(TableEntry te: this.tables) {
if (te.getSourceTableName().equals(tableName)) {
rv = new String[te.getColumnNamesToEncrypt().size()];
int ix=0;
for (String s: te.getColumnNamesToEncrypt()) {
rv[ix++] = s;
}
}
}
return rv;
}
public String[] getTableColumnNamesToCopyAsis(String tableName) {
String[] rv = null;
for (TableEntry te: this.tables) {
if (te.getSourceTableName().equals(tableName)) {
rv = new String[te.getColumnNamesToCopyAsis().size()];
int ix=0;
for (String s: te.getColumnNamesToCopyAsis()) {
rv[ix++] = s;
}
}
}
return rv;
}
public String[] getTableColumnNamesToSkip(String tableName) {
String[] rv = null;
for (TableEntry te: this.tables) {
if (te.sourceTableName.equals(tableName)) {
rv = new String[te.getColumnNamesToSkip().size()];
int ix =0;
for (String s: te.getColumnNamesToSkip()) {
rv[ix++] = s;
}
}
}
return rv;
}
public void addTable(
String sourceTableName,
String destinationTableName,
String[] columnNamesToEncrypt,
String[] columnNamesToCopyAsis,
String[] columnNamesToSkip
) {
tables.add(
new TableEntry(
sourceTableName,
destinationTableName,
columnNamesToEncrypt,
columnNamesToCopyAsis,
columnNamesToSkip
)
);
}
private class TableEntry {
private String sourceTableName;
private String destinationTableName;
private ArrayList<String> columnNamesToEncrypt;
private ArrayList<String> columnNamesToCopyAsis;
private ArrayList<String> columnNamesToSkip;
private TableEntry() {}
private TableEntry(String sourceTableName,
String destinationTableName,
String[] columnNamesToEncrypt,
String[] columnNamesToCopyAsis,
String[] columnNamesToSkip
) {
this.sourceTableName = sourceTableName;
this.destinationTableName = destinationTableName;
this.columnNamesToEncrypt = new ArrayList<>();
if (columnNamesToEncrypt != null && columnNamesToEncrypt.length > 0) {
for (String s: columnNamesToEncrypt) {
addColumn(s);
}
}
}
private void addColumn(String s) {
this.columnNamesToEncrypt.add(s);
}
private String getSourceTableName() {
return sourceTableName;
}
public String getDestinationTableName() {
return destinationTableName;
}
public void setSourceTableName(String sourceTableName) {
this.sourceTableName = sourceTableName;
}
public void setDestinationTableName(String destinationTableName) {
this.destinationTableName = destinationTableName;
}
private ArrayList<String> getColumnNamesToEncrypt() {
return columnNamesToEncrypt;
}
public void setColumnNamesToEncrypt(ArrayList<String> columnNamesToEncrypt) {
this.columnNamesToEncrypt = columnNamesToEncrypt;
}
private ArrayList<String> getColumnNamesToCopyAsis() {
return columnNamesToCopyAsis;
}
public void setColumnNamesToCopyAsis(ArrayList<String> columnNamesToCopyAsis) {
this.columnNamesToCopyAsis = columnNamesToCopyAsis;
}
public ArrayList<String> getColumnNamesToSkip() {
return columnNamesToSkip;
}
public void setColumnNamesToSkip(ArrayList<String> columnNamesToSkip) {
this.columnNamesToSkip = columnNamesToSkip;
}
}
}
The rest of this basic App, at present, is all in a single activity that uses two input's (EditTexts) :-
The secret key used to encrypt
The database name (file name) of the encrypted database.
Code in the App prevents using the same name as the base database, which needs to be copied into the assets folder.
and a Button, to initiate the Encryption if the input is good (to a fashion aka with limited validation).
This the layout xml activiy_main.xml is :-
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:padding="10dp"
tools:context=".MainActivity">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Database EncryptTool" />
<EditText
android:id="#+id/secretkey"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:hint="Secret Key to use to Encrypt the Database."
>
</EditText>
<EditText
android:id="#+id/databasename"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="MyDatabase"
android:hint="Database Name"
>
</EditText>
<Button
android:id="#+id/encrypt"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="ENCRYPT"
android:visibility="gone"
>
</Button>
</LinearLayout>
MainActivity.java where the work is done is :-
public class MainActivity extends AppCompatActivity {
public static final String ASSETDB_NAME = "basedb.db";
public static final int ASSETDB_NOT_FOUND = -10;
public static final int ASSETFILE_OPEN_ERROR = -11;
public static final int ASSETDB_OPEN_ERROR = -12;
public static final int ASSETDB_COPY_ERROR = -13;
public static final int ASSETDB_FLUSH_ERROR = -14;
public static final int ASSETDB_CLOSE_ERROR = -15;
public static final int ASSETFILE_CLOSE_ERROR = -16;
public static final int ASSETDB_CREATED_SUCCESSFULLY = 0;
public static final int BUFFERSIZE = 1024 * 4;
EditText mSecretKey, mDBName;
Button mEncryptButton;
TableColumnConvertList mTCCL = new TableColumnConvertList();
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mDBName = this.findViewById(R.id.databasename);
mSecretKey = this.findViewById(R.id.secretkey);
mEncryptButton = this.findViewById(R.id.encrypt);
//<<<<<<<<< set what data to encrypt i.e. table(s) and the column(s) in the table >>>>>>>>>
mTCCL.addTable(
"PaidData",
"FreeData",
new String[]{"theData"},
new String[]{},
new String[]{"id"}
);
if (getDBFromAsset() >= 0) {
mEncryptButton.setVisibility(View.VISIBLE);
mEncryptButton.setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View v) {
if (mDBName.getText().toString().length() < 1) {
Toast.makeText(
v.getContext(),
"The Database Name cannot be blank.",
Toast.LENGTH_LONG
).show();
mDBName.requestFocus();
return;
}
if (mDBName.getText().toString().equals(ASSETDB_NAME)) {
Toast.makeText(
v.getContext(),
"Database Name cannot be "
+ ASSETDB_NAME
+ ". Please change the name.",
Toast.LENGTH_LONG
).show();
mDBName.requestFocus();
return;
}
if (mSecretKey.getText().toString().length() < 1) {
Toast.makeText(
v.getContext(),
"The Secret Key cannot be blank.",
Toast.LENGTH_LONG
).show();
mSecretKey.requestFocus();
return;
}
if (createEncryptedDatabase(mTCCL,
mDBName.getText().toString(),
mSecretKey.getText().toString()
) == 0) {
Toast.makeText(v.getContext(),"Successfully Encrypted Database " + mDBName + " using Secret Key " + mSecretKey,Toast.LENGTH_LONG).show();
}
}
});
}
}
private boolean checkIfDataBaseExists(String databaseName) {
File dbFile = new File(this.getDatabasePath(databaseName).getPath());
if (dbFile.exists()) {
return true;
} else {
if (!dbFile.getParentFile().exists()) {
dbFile.getParentFile().mkdirs();
}
}
return false;
}
private boolean checkIfAssetDBExists() {
try {
InputStream is = this.getAssets().open(ASSETDB_NAME);
is.close();
return true;
} catch (IOException e) {
return false;
}
}
private int getDBFromAsset() {
int rv = ASSETDB_NOT_FOUND;
File dbFile = new File(this.getDatabasePath(ASSETDB_NAME).getPath());
InputStream is;
FileOutputStream os;
int read_length;
byte[] buffer = new byte[BUFFERSIZE];
if (!checkIfAssetDBExists()) {
return ASSETDB_NOT_FOUND;
}
if (checkIfDataBaseExists(ASSETDB_NAME)) {
dbFile.delete();
}
try {
rv = ASSETFILE_OPEN_ERROR;
is = this.getAssets().open(ASSETDB_NAME);
rv = ASSETDB_OPEN_ERROR;
os = new FileOutputStream(dbFile);
rv = ASSETDB_COPY_ERROR;
while ((read_length = is.read(buffer)) > 0) {
os.write(buffer,0,read_length);
}
rv = ASSETDB_FLUSH_ERROR;
os.flush();
rv = ASSETDB_CLOSE_ERROR;
os.close();
rv = ASSETFILE_CLOSE_ERROR;
is.close();
rv = ASSETDB_CREATED_SUCCESSFULLY;
} catch (IOException e) {
e.printStackTrace();
}
return rv;
}
private int createEncryptedDatabase(TableColumnConvertList tableColumnConvertList, String databaseName, String key) {
File copiedAssetDB = new File(this.getDatabasePath(ASSETDB_NAME).getPath());
File encryptedDB = new File(this.getDatabasePath(databaseName).getPath());
if (encryptedDB.exists()) {
encryptedDB.delete();
}
try {
byte[] buffer = new byte[BUFFERSIZE];
int read_length;
InputStream is = new FileInputStream(copiedAssetDB);
OutputStream os = new FileOutputStream(encryptedDB);
while ((read_length = is.read(buffer)) > 0) {
os.write(buffer,0,read_length);
}
os.flush();
os.close();
is.close();
} catch (IOException e) {
e.printStackTrace();
return -1;
}
SQLiteDatabase db = SQLiteDatabase.openDatabase(encryptedDB.getPath(),null,SQLiteDatabase.OPEN_READWRITE);
EncryptDecrypt ed = new EncryptDecrypt(this,key);
int errorcount = 0;
db.beginTransaction();
for (String t: tableColumnConvertList.getTables()) {
ContentValues cv = new ContentValues();
String[] columnsToEncrypt = tableColumnConvertList.getTableColumnNamesToEncrypt(t);
String[] columnOriginalValues = new String[columnsToEncrypt.length];
Cursor c = db.query(true,t,columnsToEncrypt,null,null,null,null,null, null);
int totalRows = c.getCount();
int updatedRows = 0;
while (c.moveToNext()) {
cv.clear();
int ovix=0;
StringBuilder whereClause = new StringBuilder();
for (String s: c.getColumnNames()) {
for (String ec: columnsToEncrypt ) {
if (s.equals(ec)) {
cv.put(s,ed.encrypt(c.getString(c.getColumnIndex(s))));
columnOriginalValues[ovix++] = c.getString(c.getColumnIndex(s));
if (whereClause.length() > 0) {
whereClause.append(" AND ");
}
whereClause.append(s).append("=?");
}
}
}
updatedRows += db.update(t,cv,whereClause.toString(),columnOriginalValues);
}
c.close();
Log.d("ENCRYPTRESULT","Read " + totalRows + " DISTINCT ROWS. Updated " + updatedRows);
errorcount += totalRows - updatedRows;
}
if (errorcount == 0) {
db.setTransactionSuccessful();
} else {
Toast.makeText(
this,
"Errors encountered Encrypting Database. Rolled back (not changed)",
Toast.LENGTH_LONG
).show();
}
db.endTransaction();
return errorcount;
}
}
Of importance is this line/code :-
TableColumnConvertList mTCCL = new TableColumnConvertList();
..........
//<<<<<<<<< set what data to encrypt i.e. table(s) and the column(s) in the table >>>>>>>>>
mTCCL.addTable(
"PaidData",
"FreeData",
new String[]{"theData"},
new String[]{},
new String[]{"id"}
);
This adds a table to the List of tables to be encrypted. It's parameters are :-
The name of the table that is to be included in Encryption.
the name of the table into which the encrypted data is to be stored.
Note this functionality is not present but could be added. As such the value is ignored.
The list of columns that are to be encrypted.
The list of columns that are to be copied asis.
This functionality is not present but could be added. As such the list is ignored.
The list of columns that are to be skipped (e.g. id columns).
Although coded, the functionality is not present. As such the list is ignored.
What the App does.
The final result is an database as per the database in the assets folder (named basedb.db) that has the data in the theData column of the PaidData table encrypted, but the the FreeData table is unchanged. This database could then be copied (e.g. using device explorer) and then included as an asset in the App that is to be distributed. That App could include a reversal of the Encryption using the secret key and the decryption part of the EncryptDecrypt class.
e.g.
The FreeData table :-
The PaidData table :-
When the App is started if copies the database (hard coded as basedb.db) from the assets folder it it exists and makes the Encrypt button visible.
If the Encrypt button isn't visible then the asset file was not located. So it's time to correct the issue (provide the correct database file).
Note as this is just a demo many checks/options that could/should be done or added are skipped for brevity.
If the Encrypt button appears then encryption is just a matter of hitting the button.
After hitting the button createEncryptedDatabase method is called.
This creates a copy, this will be the encrypted database, of the database copied from the assets folder by copying the file to it's new name (as per the given database name which MUST be different to the asset's file name).
Using the copied database it queries the table(s) defined in mTCCL (an instance of the TableColumnConvertList class) .
The query will extract data only for the columns that have been specified as those to be encrypted. The query only obtain distinct rows (i.e if multiple rows exist that has the same data in the columns then only one of the rows is extracted).
For each extracted row :-
The commonly used ContentValues instance is cleared.
The whereClause StringBuilder is cleared.
Each column in the Cursor is checked to see if it is a column, defined in the table being processed (it should be as only column t be encrypted are extracted).
if not then it is skipped.
The original value is saved in the appropriate element of the string array columnOriginalValues (this to be used as the bind parameters for the WHERE clause of the UPDATE)
An element of the ContentValues instance is added with the current column name and the encrypted data.
This is done as per cv.put(s,ed.encrypt(c.getString(c.getColumnIndex(s))));
If the length of the whereClause is greater than 0 then AND is added to the whereClause, then the column name suffixed with =? is added to the whereClause being built.
After all columns have been processed the SQLiteDatabase update method is called to update the columns setting the values to the encrypted values WHERE all the column match the original data.
After all rows have been processed the Cursor is closed and the next table processed.
If after all tables have been processed then error count is 0 then the transaction is set as successful, otherwise a message is Toasted Errors encountered Encrypting Database. Rolled back (not changed).
The transaction is then ended (if not set as successful then the data is not updated but rolled back).
The database will be in the data/data/package_name/databases folder e.g. :-
Example Decryption App for Answer 1
Note that the database (MyDatabase) was copied from data/data/package_name/databases of the App from the previous answer (i.e. the encrypted database) into the assets folder of this app
The following is a very basic App that initially only has the Free Data, but as an Edit Text and a Button that allows the Paid Data to be decrypted and retrieved. The available data (Free Data initially) is listed in a ListView; after decryption the PaidData having been copied to the FreeData is then avialable and listed.
Notes
the data can be decrypted numerous times and each such successful attempt will add and display more rows.
EncryptDEcrypt.java is identical to the one used in the Encrypt tool.
The Database Helper is :-
public class DBHelper extends SQLiteOpenHelper {
public static final String DBNAME = "MyDatabase";
public static final int DBVERSION = 1;
public static final String TBL_FREEDATA = "FreeData";
public static final String COL_FREEDATA_ID = "id";
public static final String COL_THEDATA = "theData";
SQLiteDatabase mDB;
public DBHelper(Context context) {
super(context, DBNAME, null, DBVERSION);
loadDBFromAssets(context);
mDB = this.getWritableDatabase();
}
#Override
public void onCreate(SQLiteDatabase db) {
}
#Override
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
}
public long insertFreeDataRow(String theData) {
ContentValues cv = new ContentValues();
cv.put(COL_THEDATA,theData);
return mDB.insert(TBL_FREEDATA,null,cv);
}
public Cursor getAllAvialableData() {
return mDB.query(TBL_FREEDATA,new
String[]{"*",COL_FREEDATA_ID + " AS " + BaseColumns._ID},
null,null,null,null,null
);
}
public void decryptAndLoadPaidData(Context context, String secretKey) {
EncryptDecrypt ed = new EncryptDecrypt(context,secretKey);
mDB.beginTransaction();
Cursor c = mDB.query("PaidData",null,null,null,null,null,null);
while (c.moveToNext()) {
String decrypted_data = ed.decrypt(c.getString(c.getColumnIndex(COL_THEDATA)));
if (decrypted_data != null) {
insertFreeDataRow(decrypted_data);
} else {
Toast.makeText(context,"Naughty, that's not the password.",Toast.LENGTH_LONG).show();
}
}
c.close();
mDB.setTransactionSuccessful();
mDB.endTransaction();
}
private boolean loadDBFromAssets(Context context) {
File dbFile = new File(context.getDatabasePath(DBNAME).getPath());
byte[] buffer = new byte[1024 * 4];
int read_length = 0;
if (dbFile.exists()) return true;
if (!dbFile.getParentFile().exists()) {
dbFile.getParentFile().mkdirs();
}
try {
InputStream assetdb = context.getAssets().open(DBNAME);
OutputStream realdb = new FileOutputStream(dbFile);
while ((read_length = assetdb.read(buffer)) > 0) {
realdb.write(buffer,0,read_length);
}
realdb.flush();
realdb.close();
assetdb.close();
} catch (IOException e) {
e.printStackTrace();
return false;
}
return true;
}
}
MainActivity.java is :-
public class MainActivity extends AppCompatActivity {
ListView mListView;
EditText mSecretKeyInput;
Button mDecrypt;
SimpleCursorAdapter mSCA;
Cursor mAllTheData;
DBHelper mDBhlpr;
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mListView = this.findViewById(R.id.list);
mSecretKeyInput = this.findViewById(R.id.secretKey);
mDecrypt = this.findViewById(R.id.decrypt);
mDBhlpr = new DBHelper(this);
manageListView();
manageDecryptButton();
}
private void manageListView() {
mAllTheData = mDBhlpr.getAllAvialableData();
if (mSCA == null) {
mSCA = new SimpleCursorAdapter(
this,android.R.layout.simple_list_item_1,mAllTheData,new String[]{DBHelper.COL_THEDATA},new int[]{android.R.id.text1},0);
mListView.setAdapter(mSCA);
} else {
mSCA.swapCursor(mAllTheData);
}
}
private void manageDecryptButton() {
mDecrypt.setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View v) {
if (mSecretKeyInput.getText().toString().length() > 0) {
mDBhlpr.decryptAndLoadPaidData(v.getContext(),mSecretKeyInput.getText().toString());
manageListView();
}
}
});
}
}
Result
When first run the App only shows the Free Data as per :-
If the correct password/secret key is input and the Get Paid Data button is pressed then the extra data is added :-
If an incorrect password is provided, then the data is not loaded and a toast appears indicating the wrong password.
I use code below to test if a column exists:
public static boolean isColumnExists(String tableName, String columnName) {
Cursor cursor = null;
try {
SQLiteDatabase db = getDatabase();
cursor = db.rawQuery("SELECT * FROM " + tableName + " LIMIT 0", null);
String[] cloNames = cursor.getColumnNames();
if (cloNames != null) {
for (String temp : cloNames) {
if (columnName.equalsIgnoreCase(temp)) {
return true;
}
}
}
} catch (Exception e) {
e.printStackTrace();
} finally {
if (null != cursor && !cursor.isClosed()) {
cursor.close();
}
}
return false;
}
The column hello2 doesn't exist in initial state, after adding column to database, the following test still tells that the column doesn't exist, and the second try will cause an error about duplicate column, which is not correct.
if (!isColumnExists("PositionCache", "hello2")) {
// First try will insert column to database
getDatabase().execSQL("alter table PositionCache add hello2 Integer default 0");
}
if (!isColumnExists("PositionCache", "hello2")) {
// Second try will give and error about duplicate column of hello2
getDatabase().execSQL("alter table PositionCache add hello2 Integer default 0");
}
I need to know the reason about such an abnormal phenomenon.
If I change SELECT * FROM to select * from in method isColumnExists then everything become normal.
I believe the reason is that SQLite (I strongly suspect the Cursor, so more correctly the Android SQLite aspect of the SDK) is cacheing data (perhaps because the underlying data is never retrieved from the Database as there is no need to get the data (as far as the Cursor is concerned)).
I've tried various checks including putting breakpoints in, checking the result of getColumnnames, and making the method non-static.
As soon as I add an alternative check using the PRAGMA table_info(*table_name*); then the column exists.
As such I'd suggest using the following :-
public static boolean isColumnExistsOld(String tableName, String columnName) {
Cursor csr = getDatabase().rawQuery("PRAGMA table_info(" + tableName + ")",null);
while(csr.moveToNext()) {
if (csr.getString(csr.getColumnIndex("name")).equalsIgnoreCase(columnName)) {
return true;
}
}
return false;
/*
Cursor cursor = null;
try {
SQLiteDatabase db = getDatabase();
cursor = db.rawQuery("SELECT * FROM " + tableName + " LIMIT 1", null);
cursor.moveToFirst();
String[] cloNames = cursor.getColumnNames();
if (cloNames != null) {
for (String temp : cloNames) {
if (columnName.equalsIgnoreCase(temp)) {
return true;
}
}
}
} catch (Exception e) {
e.printStackTrace();
} finally {
if (null != cursor && !cursor.isClosed()) {
cursor.close();
}
}
boolean rv = colfound;
return false;
*/
}
Note your code has been left in but commented out.
I believe that evaluating forces the cache to be refreshed (i.e. I tried this an yep it does dynamically change to include the column).
I am creating app in which i register user and store user's information in database,so i have created database and storing value in databse but i don't know how to fetch data from database and show in textview?Using below query to fetch data but it has error.What is correct way?
public void insertEntry(String fname, String lname, String gen,String weight)
{
SQLiteDatabase db = dbHelper.getWritableDatabase();
ContentValues values = new ContentValues();
values.put("firstname", fname);
values.put("lastname", lname);
values.put("gender", gen);
values.put("weight",weight);
}
public Cursor fetchData()
{
SQLiteDatabase mDB = dbHelper.getWritableDatabase();
return mDB.rawQuery("SELECT * FROM Register WHERE firstname=? lastname =?" , null);
}
Using this to set fetched value on textview in different activity
Cursor name = sqliteDataBase.fetchData();
tv_name.setText((CharSequence) name);
Try this,
try {
SQLiteDatabase mDB = dbHelper.getReadableDatabase();
String selectQuery = "SELECT * FROM Register WHERE firstname= "+first+" lastname =" + last + ";";
cursor = db.rawQuery(selectQuery, null);
if (cursor != null && cursor.getCount() > 0) {
if (cursor.moveToFirst()) {
String firstName = cursor.getString(cursor.getColumnIndex("firstname")));
}
}
} catch(Exception e) {
e.printSTackTrace();
} finally {
if (cursor != null) {
cursor.close();
}
}
public String fecthResults()
{
String name = null;
SQLiteDatabase sqLiteDatabase = getReadableDatabase();
Cursor cursor = sqLiteDatabase.query(TABLE_NAME, null, null, null, null, null, null);
if (cursor.moveToFirst()) {
do {
name = cursor.getString(cursor.getColumnIndex("firstname")
} while (cursor.moveToNext());
cursor.close();
}
sqLiteDatabase.close();
return name;
Get the name and then set it directly to a texView
A Cursor is an interface that provides random read-write access to the result set returned by the query. It contains multiple rows of data and this data can be easily processed by help of For loop.
Lets take an example to understand the process. Suppose, you need to access data from a column name "col_1" and show the data in TextView. The For loop for the process will be as follows.
for (cursor.moveToNext()) {
textView.setText(cursor.getString(cursor.getColumnIndex("col_1")));
}
Now, if we need only one value (from only one record or tuple) then, we can opt out the For loop and change the code as shown below.
if (cursor.moveToFirst()) {
textView.setText(cursor.getString(cursor.getColumnIndex("col_1")));
}
Always remember to close the cursor after using it.
To close the cursor, use the following line of code.
cursor.close();
For more information, please visit the following links:
https://developer.android.com/reference/android/database/Cursor.html
https://developer.android.com/training/basics/data-storage/databases.html#ReadDbRow
The application am working on will initially have one database with a table, say tbl_usr which will have only one record. Basically we are trying to keep one user per device. When the user logs in from the device with an auth code, his details will be fetched from server and stored in database. Next time if he tries to enter different auth code, which is valid but is not in table then he will not be allowed to proceed. Below is a common DBHelper class.
But whatever approach am trying, I am getting databaselocked exception, when tried for the 2nd time login. I've referred various links where in it was suggested to use different instance of database within method, but still it comes with error. Below is my Helper class
public class DBaseHelper extends SQLiteOpenHelper {
private static String CREATE_TABLE;
private static final String DATABASE_NAME="IPDB";
private static String UserMessage="";
private int tableType=0;
private ContentValues cValues;
private Cursor cursor;
public enum TableTypes{
Table1
};
public DBaseHelper(Context context){
super(context,context.getExternalFilesDir(null).getAbsolutePath()+"/"+DATABASE_NAME,null,1);
}
#Override
public void onCreate(SQLiteDatabase db){
TableTypes tableTypes=TableTypes.values()[tableType];
switch (tableTypes){
case Table1:
CREATE_TABLE="CREATE TABLE IF NOT EXISTS tbl_usr....";
break;
default:
break;
}
db.execSQL(CREATE_TABLE);
db.close();
System.out
.println("onCreate Method Done.");
}
#Override
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion){
/*db.execSQL("DROP TABLE IF EXISTS "+LOGIN_TABLE);*/
onCreate(db);
}
/*this is the method which gets called from other class Like*/
/*helper.insertRecord(tableParams);*/
public HashMap<String,String> insertRecord(HashMap<String,String> dbaseParams){
HashMap<String,String> response=new HashMap<String,String>();
tableType=Integer.parseInt(dbaseParams.get("tableType"));
cValues = new ContentValues();
String TableName="";
TableTypes tableTypes=TableTypes.values()[tableType];
switch (tableTypes){
case Table1:
String AuthCode=dbParams.get("AuthCode");
/*if user exists then check if its the same user*/
if( CheckUserRecordExists(AuthCode) && empty(UserMessage) ){
response.put("isSuccess","true");
return response;
}
else {
if (!empty(UserMessage)) {
response.put("isSuccess", "false");
response.put("message",UserMessage);
return response;
}
/*add new user
Fill cValues declared above*/
TableName = "Table1";
}
break;
default:
break;
}
SQLiteDatabase dataBase = getWritableDatabase();
/*insert data into database*/
try {
dataBase.beginTransaction();
long rowID = dataBase.insertOrThrow(TableName, null, cValues);
dataBase.setTransactionSuccessful();
}
catch(Exception ex){
ex.printStackTrace();
}
finally {
dataBase.close();
}
response.put("isSuccess", "true");
return response;
}
private boolean CheckUserRecordExists(String authCode){
UserMessage="";
SQLiteDatabase dataBase=getReadableDatabase();
/*Exception here when comes for 2nd time after new installation*/
cursor = dataBase.query("Table1", new String[]{"COUNT(*)"}, null, null, null, null, null);
cursor.moveToFirst();
int iCount=cursor.getInt(0);
/*check if any record exist*/
if(iCount>0){
dataBase.close();
if(!cursor.isClosed()) cursor.close();
/*check if the code entered matches with the record existing*/
if(!CheckIsDataAlreadyInDBorNot("Table1","Auth_Code",authCode))
{
UserMessage="Invalid login!";
return false;
}
else return true;
}
else{
dataBase.close();
if(!cursor.isClosed()) cursor.close();
return false;
}
}
private boolean CheckIsDataAlreadyInDBorNot( String TableName,
String dbfield, String fieldValue) {
/*checking if user is same user*/
SQLiteDatabase dataBase=getReadableDatabase();
String[] columns = { dbfield };
String selection = dbfield + " =?";
String[] selectionArgs = { fieldValue };
String limit = "1";
Cursor cursor = dataBase.query(TableName, columns, selection, selectionArgs, null, null, null, limit);
boolean exists = (cursor.getCount() > 0);
cursor.close();
dataBase.close();
return exists;
}
public static boolean empty( final String s ) {
return s == null || s.trim().isEmpty();
}
}
I know its a huge code, but logic is simple. But the problem is database lock. Could someone let me know how I can make sure that database is always in valid state on each operation?
You have beginTransaction() but no matching calls to endTransaction(). An ongoing transaction keeps the database in a locked state and also keeps the internal reference count nonzero, so close() does not yet actually close the database.
The conventional pattern for transactional operations is
db.beginTransaction();
try {
// db operations that can throw
db.setTransactionSuccessful();
} finally {
db.endTransaction();
}
Also in your onCreate() you should not be closing the database since you don't own it.
I'm running an update query that compiles. But when I test the results, there seem to be no changes. Everything looks good to me, but clearly something is wrong. Can anyone see what I am missing. Pretty new to SQLite, so apologies if it's something simple. Thanks!
public static Boolean updateHealth (int unitVal, int oldValue, int newValue) {
String unit = Integer.toString(unitVal);
String oldVal = Integer.toString(oldValue);
String newVal = Integer.toString(newValue);
System.err.printf("old val: %s, new val: %s\n", oldVal, newVal);
SQLiteDatabase db = myDBOpenHelper.getWritableDatabase();
String where = UNIT_COLUMN + " = " + unit + " AND " + HEALTH_COLUMN + " = " + oldVal;
Cursor cursor = db.query(UNITS_TABLE, new String[] {UNIT_COLUMN}, where, null, null, null, null);
if (cursor != null) {
/* the record doesn't exist, cancel the operation */
return false;
}
ContentValues updatedValues = new ContentValues();
updatedValues.put(HEALTH_COLUMN, newVal);
/* SQL query clauses */
String whereArgs[] = null;
db.update(UNITS_TABLE, updatedValues, where, whereArgs);
return true;
}
The cursor is not null when no row is retrieved. So you have to replace the line if (cursor != null) { by if(!cursor.moveToNext()) {
By the way, you don't need to query the database before updating. You can do the update, see how many rows have been affected and return true if the number of affected rows is > 0, false otherwise. The number of affected rows is returned by the method update.