I have a 2-step database migration in my app, using Room. The migration had to be done in two parts (v.2 -> 3 -> 4) because complex database operations using newly added tables had to be done between versions 3 and 4.
I have a SharedPreferences key-value pair to save the information when the user migrates. This is to ensure that the app won't try to migrate from 2 to 3 after it has already migrated to version 4.
The migration from 2 to 3 is meant to be run only when the user updates the app from the old version to the new version.
This has worked in every single one of our test devices, but Google Play's data is giving us a lot of crash reports for IllegalStateException, stating that the database tried to downgrade itself. This should NOT happen, should the SharedPreferences check work correctly.
For reference. Here's some of the migration code:
if(!hasMigrated){
if(NeedMigration.needMigrate(MyApplication.getAppContext())){
..
try{
//instantiate migration 2 to 3
pdb=PreDatabase.getInstance(this);
dRepo = new DayRepo(pdb, this, dayInstance);
//db operations
createDays();
//store into SharedPrefs that we already migrated from 3
NeedMigration.setHasMigrated(MyApplication.getAppContext());
}catch(IllegalStateException e){
Log.e("Database", "Already at version 4");
..
return;
}
..
//instantiate migration from 3 to 4 (mig is in db.build)
mdb = MyDatabase.getInstance(this);
mRepo = Repository.fromContext(this);
..
} else {
/*
If the user already has updated and the database exists in its newest state, skip straight to MyDatabase.java
instantiation and load Day objects from database.
*/
instantiateDatabase();
}
..
hasMigrated=true;
}
The hasMigrated boolean is just a static boolean so that the app won't try to instantiate the database again during runtime. If NeedMigration.needMigrate returns false, it instead just instantiates the database at the current version.
Here is the NeedMigrate class:
private static final String MIG_PREFS= "migration_preferences";
private static final String MIG_HAS_UPDATED ="has_updated_to_four";
//if yes (true) then yes need migrate
//if false then no! no migrate
public static boolean needMigrate(Context activity){
//compare versioncode with sharedprefs
final SharedPreferences preferences = activity.getSharedPreferences(MIG_PREFS, Activity.MODE_PRIVATE);
final boolean needmig = preferences.getBoolean(MIG_HAS_UPDATED, true);
return needmig;
}
public static void setHasMigrated(Context activity){
final SharedPreferences prefs = activity.getSharedPreferences(MIG_PREFS, Activity.MODE_PRIVATE);
//check if value exists
SharedPreferences.Editor edit = prefs.edit();
//false because if needMigrate is false then we dont need migrate
edit.putBoolean(MIG_HAS_UPDATED, false);
edit.commit();
}
Again let me clarify that this works perfectly fine on my test devices, but we're getting a lot of crash reports from Google implying that the database tried to run at the previous version:
java.lang.IllegalStateException:
at androidx.room.RoomOpenHelper.onUpgrade (RoomOpenHelper.java:101)
at androidx.room.RoomOpenHelper.onDowngrade (RoomOpenHelper.java:113)
at androidx.sqlite.db.framework.FrameworkSQLiteOpenHelper$OpenHelper.onDowngrade
(FrameworkSQLiteOpenHelper.java:144) at
android.database.sqlite.SQLiteOpenHelper.getDatabaseLocked
(SQLiteOpenHelper.java:257) at
android.database.sqlite.SQLiteOpenHelper.getWritableDatabase
(SQLiteOpenHelper.java:2) at
androidx.sqlite.db.framework.FrameworkSQLiteOpenHelper$OpenHelper.getWritableSupportDatabase
(FrameworkSQLiteOpenHelper.java:96) at
androidx.sqlite.db.framework.FrameworkSQLiteOpenHelper.getWritableDatabase
(FrameworkSQLiteOpenHelper.java:54) at
androidx.room.RoomDatabase.query (RoomDatabase.java:256) at
androidx.room.util.DBUtil.query (DBUtil.java:54) at
fi......controllers.database_controller.MealGroupDao_..PreDatabase_Impl.getMealGroupsInDay
(MealGroupDao_..PreDatabase_Impl.java:157) at
fi.....DayRepo.lambda$populateDayCache$0 (DayRepo.java:301) at
fi.....controllers.database_controller.repos.-$$Lambda$DayRepo$gQGLM0kHLZLeLseqjFzeMTTO2fw.run
(-.java:8) at java.util.concurrent.ThreadPoolExecutor.runWorker
(ThreadPoolExecutor.java:1167) at
java.util.concurrent.ThreadPoolExecutor$Worker.run
(ThreadPoolExecutor.java:641) at java.lang.Thread.run
(Thread.java:764)
We just got a review saying "Can't open app"and I suspect that this is the reason.
Why is this SharedPreference check ignored, or does it just return a wrong value ? Is this a context-related issue or perhaps related to the fact that the NeedMigration class is static?
I have tried:
Changing context from activity to application context
moving the setHasMigrated call to happen before the database 2 to 3 update (DID NOT WORK...)
Adding try/catch blocks in a lot of places
Related
I am learning more about SharedPreferences and would like to understand how exactly everything is working. I have watched a decent amount of videos but I still have some questions.
Upon a user logging in, I generate a random session ID using UUID. I then assign the user their session ID and save a session by passing the UserModel into a SessionManagement instance that handles SharedPreferences.
userModel.setSessionId(UUID.randomUUID().toString());
SessionManagement sessionManagement = new SessionManagement(LoginActivity.this);
sessionManagement.saveSession(userModel);
When the user closes/opens the app, onStart() is called. It creates another instance of SessionManagement and checks if the session is null using getSession() to determine whether they're logged in or not.
SessionManagement sessionManagement = new SessionManagement(LoginActivity.this);
if (sessionManagement.getSession() != null) {
// go to some activity
}
And here is what SessionManagement constructor looks like:
private SharedPreferences sharedPreferences;
private final SharedPreferences.Editor editor;
private MasterKey masterKey;
//private String SHARED_PREF_NAME = "session";
private final String SESSION_KEY = "session_user";
private final String SESSION_USERNAME = "session_username";
public SessionManagement(Context context) {
try {
masterKey = new MasterKey.Builder(context)
.setKeyScheme(MasterKey.KeyScheme.AES256_GCM)
.build();
} catch (GeneralSecurityException | IOException e) {
e.printStackTrace();
}
try {
sharedPreferences = EncryptedSharedPreferences.create(
context,
"secret_shared_prefs",
masterKey,
EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM
);
} catch (GeneralSecurityException | IOException e) {
e.printStackTrace();
}
//sharedPreferences = context.getSharedPreferences(SHARED_PREF_NAME, Context.MODE_PRIVATE);
editor = sharedPreferences.edit();
}
My question now is, if I am just checking whether the session is null or not, how does SharedPreferences know that the sessionID corresponds to the user that initialized it in step 1?
What are the ways that people work around weak/exposed session ID's that a SharedPreferences implementation can protect against?
Is my implementation/flow correct?
Is it safe to save the sessionID to a user model?
I appreciate any help I can get with this topic!
I use SharedPreferences a lot but did not yet use EncryptedSharedPreferences. I think this is only necessary, if you have data that deserves protection in a very strict way (like passwords or similar). But then you might probably want to use Google Identity or similar.
If you use private SharedPreferences, by calling context.getSharedPreferences(.., Context.MODE_PRIVATE) the OS already makes sure your data is only accessable from your app. Unless there is a bug in the system or the device is hacked, no other party has access to your data.
Now to answer your question, what do you use the session ID for?
For me it looks like you try to implement a web application inside an Android app. If this is the case because you are used to build web apps and have no more specific reason, then just forget about session handling and implement the app assuming there is only one user.
If you want it because you are communicating with a web service or similar, then let that service do the session handling, if possible.
If you really need that session, then the answer to your question is:
With your example the system doesn't know the SharedPrefrences you are calling is valid for the current session. The values are always the same, independent on who is currently logged in.
What you can do, is to choose a name for your SharedPreferences that corresponds with your logged in user (e.g. the hash of the username/mail address or similar).
So you always load the SharedPreferences based on some user identification, just like that:
context.getSharedPreferences(username.hashCode(), Context.MODE_PRIVATE)
There are two points to consider, if you do so:
You might generate junk data, if a user logs in once, therefore generates a SharedPreferences file, and never logs in again. So it might be appropriate to save the users last activity and clean up from time to time.
If you have lot's of users logging in, you might want to consider a database or some other storage type, like json files or whatever, as it's much easier to handle and clean them up.
sharedPrefences is like a repo. So write our data with string types. You can select your data is private or public. And remove your data when your deleted app.
Maybe your help this link:
https://www.geeksforgeeks.org/shared-preferences-in-android-with-examples/
I found a problem with Shared Preferences which happens only for some devices and only with Android 9 (Pie) o above.
To encrypt my database I create a key and store it to my sharedPreferences.
SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context);
String content = sharedPreferences.getString(-nameOfTheKey-, "");
if (content == null || content.isEmpty()){
// Create and store the new key
content = restoreApplicationKey(context);
}
As you can see, first I check for the availability then I create if not exist or empty. This key isn't created in any other place. That is the only point.
When I try to first install the app on my device, the condition return false because the key-value is already present in my SharedPreferences. This is impossibile!
I found online that the solution is to set allowBackup="false" into the Manifest and reinstall the app but I don't understand this behavior. How is it possible that the kay-value is pre setted?
Also, before setting allowBackup to false, I noticed that if forcing the initialization and uninstall the app, after the reinstall the key is still present but the value is changed. Every time with the same string but different from the saved one. All others key are fine, except this one.
As I said, the above code is the only entry point and it's called only at the app launch. Also the restoreApplicationKey(context) is called only at this point.
I have a Native Android App in the Store. This App has its Settings stored in the SharedPreferences. Now I am working on an Update for the App. The Update is a Xamarin Cross-Platform App. App Name, Package Name, and those things are the same as in the Native App. But I can't get the values from SharedPreferences now. Did I make something wrong? I guess it's because of a different Application Context. But I don't know why.
Here is how the Native App is loading the Values:
final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(getApplicationContext());
final Set<String> keySet = prefs.getAll().keySet();
for (final String key : keySet) {
if (key.startsWith(DatabaseSetting.DATABASE_SETTING_KEY_PREFIX)) {
//Do stuff
}
}
After updating the App I can not see any keys in the prefs.
C# Xamarin Code to also load these prefs:
ISharedPreferences sharedPrefs = PreferenceManager.GetDefaultSharedPreferences(Application.Context);
foreach (string key in sharedPrefs.All.Keys)
{
if(key.StartsWith(DATABASE_SETTING_KEY_PREFIX))
}
The App was updated without uninstalling the old one. Just deployed the new one.
Is it giving you a nullpoint?
Are you storing app context (Android.App.Application.Context) or class context(Context)?
Are there any other exceptions thrown that you possibly aren't catching?
I know there must be douzens of answers to this question out there, but either i cant't find them or I don't understand them.
The Question:
How do I get my app to exactly start as it was left?
F.e. dynamicly added checkBoxes shouldn't dissapear!
There is no "out of the box" way of doing it. You could save the current state of your Activity in some way (More on persistence)
Then you need to be able to rebuild the desired state of the persisted state in your Activity lifecycle
You could save and load with the shared preferences for example:
public void saveState(YourState state) {
SharedPreferences sharedPreferences = app.getSharedPreferences(R.string.preference_file_key, Context.MODE_PRIVATE)
sharedPreferences.edit()
.putString("CustomAtt", state.getCustomAtt())
}
public YourState loadState() {
SharedPreferences sharedPreferences = app.getSharedPreferences(R.string.preference_file_key, Context.MODE_PRIVATE)
String customAtt = sharedPreferences.getString("CustomAtt", "DefaultValue")
return new YoutState(customAtt)
}
And use it like this
#Override
protected void onCreate(Bundle savedInstanceState) {
YourState state = loadState();
// Rebuild your activity based on state
someView.setText(state.getCustomAtt())
}
You can store such values in SharedPreferences.
https://developer.android.com/training/basics/data-storage/shared-preferences.html
It is using key-value approach for saving. So you can save some values and read it from SharedPreferences whenever you want to.
This is the best approach for small data, that can be used on the app launch. So you can quit your app and the data is still present - so can be read on the next app launch.
Or save the condition of your program to a text file, so that the program can "translate" it back into conditions before it stops, or what I do not recommend, it saves every object created with ObjectOutputStream.
I have a blank-ish Android Project and what I want to do is take the user to a different "page/screen" if it is their first time only.
I know the logic for this but since I'm new to Android, I'm unsure of how to code this.
Below are the steps I believe I need to take in order to accomplish this:
App loads. If Local storage contains setting "FirstTimeUser", then it is not their first time using the app. Show the MainActivity page. If FirstTimeUser setting does not exist, it is their first time using the app (or they have uninstalled and reinstalled it), so instead, show WelcomeActivity page.
After viewing Welcome activity page, create FirstTimeUser setting and set to False.
But how do I code this for an Android app?
Use shared preferences as shown:
//declare as global
SharedPreferences prefs = null;
//and in your onCreate method:
prefs = getSharedPreferences("packageNameHere", MODE_PRIVATE);
if (prefs.getBoolean("firstrun", true)) {
//do stuff here if first run
//make sure to flag the boolean as false
prefs.edit().putBoolean("firstrun", false).commit();
}
else{
//if not first run, do something else
}
There is something called "SharedPreferences" that i believe your looking for.
http://developer.android.com/reference/android/content/SharedPreferences.Editor.html