Prevent PreferenceFragment from writing default preferences to SharedPreferences - android

Current behavior of PreferenceFragment: Upon first display on screen PreferenceFragment writes to associated SharedPreferences all default values defined in PreferenceScreen XML resource. I tested this couple times and PreferenceFragment as well as PreferenceActivity writes all preferences defaults to SharedPreferences when user opens Settings activity, even if he immediately close it without touching anything.
Problem: When in the next version of my app I decide to change some default user preferences, they will not apply to the devices where user at least once opened app preferences, because PreferenceFragment wrote all default values to SharedPreferences. I know that I can reapply new default values by overwriting all values in SharedPreferences, not only default, but user chosen too. But resetting user preferences in app update is completely unacceptable. So the problem is that we cant distinguish when some particular preference was set explicity by user or its just default preference written by PreferenceFragment upon first display on screen.
What I want: If the user explicity set some preference, whatever he has chosen, I should not touch this with my updated app defaults, even if user choice is coincides with my old default. But if user was not explicity chose preference I want that my new default preferences start working for him with app update.
So: How to prevent write of default preferences values by PreferenceFragment to associated SharedPreferences?

After studying sources I found a way to achieve requested behavior.
The only place where real write to SharedPreferences occurs it's bunch of persist[Type] methods in Preference class. And subclasses of Preference usually call persist[Type] method only in single internal method, that has similar structure across all subclasses. For example, method from TwoStatePreference, that is superclass of ChekBoxPreference and SwitchPreference:
public void setChecked(boolean checked) {
boolean changed = this.mChecked != checked;
if(changed || !this.mCheckedSet) {
this.mChecked = checked;
this.mCheckedSet = true;
this.persistBoolean(checked);
if(changed) {
this.notifyDependencyChange(this.shouldDisableDependents());
this.notifyChanged();
}
}
}
Next, setChecked method of TwoStatePreference is called in about five other methods, and two of these calls can produce default value to be committed to SharedPreferences. Here is first:
#Override
protected void onSetInitialValue(boolean restoreValue, Object defaultValue) {
setChecked(restoreValue ? getPersistedBoolean(mChecked)
: (Boolean) defaultValue);
}
And second:
#Override
protected void onRestoreInstanceState(Parcelable state) {
if (state == null || !state.getClass().equals(SavedState.class)) {
// Didn't save state for us in onSaveInstanceState
super.onRestoreInstanceState(state);
return;
}
SavedState myState = (SavedState) state;
super.onRestoreInstanceState(myState.getSuperState());
setChecked(myState.checked);
}
And here is the solution, custom class, that subclasses SwitchPreferenceCompat and preventing commit in two above calls:
public class MySwitchPref extends SwitchPreferenceCompat
{
private boolean mAllowPersist;
#Override
protected boolean persistBoolean(boolean value) {
if (mAllowPersist) {
return super.persistBoolean(value);
}
return false;
}
#Override
protected void onSetInitialValue(boolean restoreValue,
Object defaultValue) {
mAllowPersist = false;
super.onSetInitialValue(restoreValue, defaultValue);
mAllowPersist = true;
}
#Override
protected void onRestoreInstanceState(Parcelable state) {
mAllowPersist = false;
super.onRestoreInstanceState(state);
mAllowPersist = true;
}
public MySwitchPref(Context context, AttributeSet attrs,
int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
}
public MySwitchPref(Context context, AttributeSet attrs,
int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
public MySwitchPref(Context context, AttributeSet attrs) {
super(context, attrs);
}
public MySwitchPref(Context context) {
super(context);
}
}
You should replace your SwitchPreferenceCompat declaration in xml PreferenceScreen to this subclass and all should work, I have tested it. And if you use other then SwitchPreference types of preferences, you as well should similarly subclass and override their behavior.
Beware: This solution relies upon internal realization of current, com.android.support:preference-v7:23.4.0 library. It might change with future releases, so if you use other library version, or use non support implementation you should look in to source, and ensure there is no other calls to persist values in SharedPreferences except that two, that I overriden. As well if you use other subclasses of Preference, not only SwitchPreference, you should check for other calls to persist values in SharedPreferences.

Related

Default Shared Preferences : Source not found

I use Default Shared Preferences to save some data in them.
I have created a class to handle all this :
public class PreferencesHandler {
private Context mContext;
public PreferencesHandler (Context context) {
this.mContext = context;
}
public void setBoolean(String name, boolean value) {
PreferenceManager.getDefaultSharedPreferences(mContext).edit()
.putBoolean(name, value).commit();
}
public void setInt(String name, int value) {
PreferenceManager.getDefaultSharedPreferences(mContext).edit()
.putInt(name, value).commit();
}
public boolean getBoolean(String name, boolean defaultValue) {
return PreferenceManager.getDefaultSharedPreferences(mContext)
.getBoolean(name, defaultValue);
}
public int getInt(String name, int defaultValue) {
return PreferenceManager.getDefaultSharedPreferences(mContext).getInt(
name, defaultValue);
}
}
How you see I created a Handler class for easier access.
Also I have a Methods class where all my methods are stored.
And in that class I have a method which uses this handler class :
public class Methods {
private Context mContext;
public Methods(Context context) {
this.mContext = context;
}
public void process (PreferencesHandler pref, String prefName, int default) {
int mInt= pref.getInt(prefName, default);
//The rest doesn't matters
}
}
I call this method in my activity class.
With the debugger I found out that the error is in the Handler, when it executes the return line, it opens the PreferenceManager.class with the text "Source not found".
It says that the JAR file android.jar has no source attachment.
What should I do to fix this?
The problem is probably that the Activity that you use to call the constructor of PreferencesHandler gets destroyed, and then you try to use that PreferencesHandler in a new instance of that activity.
If this is the problem then this change in both constructors should fix that:
mContext = context.getApplicationContext();
As for the source code of PreferenceManager.class: use the Android SDK manager to install the Android sources and restart your IDE.
In Eclipse the Android SDK Manager can be found here: Window -> Android SDK Manager

MODE_MULTI_PROCESS for SharedPreferences isn't working

I have a SyncAdapter running on its own process separately from the main app process.
I'm using a static wrapper class around my SharedPreferences that creates a static object on process load (Application's onCreate) like so:
myPrefs = context.getSharedPreferences(MY_FILE_NAME, Context.MODE_MULTI_PROCESS | Context.MODE_PRIVATE);
The wrapper has get and set methods, like so:
public static String getSomeString() {
return myPrefs.getString(SOME_KEY, null);
}
public static void setSomeString(String str) {
myPrefs.edit().putString(SOME_KEY, str).commit();
}
Both SyncAdapter and app uses this wrapper class to edit and get from the prefs, this works sometimes but a lot of times I see the SyncAdapter getting old/missing prefs on accesses to the prefs, while the main app sees the recent changes properly.
According to the docs I think the MODE_MULTI_PROCESS flag should work as I expect it to, allowing both processes to see latest changes, but it doesn't work.
Update:
Per x90's suggestion, I've tried refraining from using a static SharedPreferences object and instead calling getSharedPreferences on each get/set method.
This caused a new issue, where the prefs file gets deleted (!!!) on multi-process simultaneous access.
i.e. I see in the logcat:
(process 1): getName => "Name"
(process 2): getName => null
(process 1): getName => null
and from that point all the prefs saved on the SharedPreferences object were deleted.
This is probably a result of another warning I see in the log:
W/FileUtils(21552): Failed to chmod(/data/data/com.my_company/shared_prefs/prefs_filename.xml): libcore.io.ErrnoException: chmod failed: ENOENT (No such file or directory)
P.S this is not a deterministic issue, I saw the above logs after a crash happened, but couldn't recreate yet on the same device, and until now it didn't seem to happen on other devices.
ANOTHER UPDATE:
I've filed a bug report on this, after writing a small testing method to confirm this is indeed an Android issue, star it at https://code.google.com/p/android/issues/detail?id=66625
I gave a very quick look at Google's code and apparently Context.MODE_MULTI_PROCESS is not an actual way to ensure process-safety of SharedPreferences.
SharedPreferences itself is not process-safe. (That's probably why SharedPreferences documentation says "currently this class does not support use across multiple processes. This will be added later.")
MODE_MULTI_PROCESS just works in conjunction with every Context.getSharedPreferences(String name, int mode) call: when you retrieve an instance of SharedPreferences specifying the MODE_MULTI_PROCESS flag android will reload the preferences file to be up to date with any (eventual) concurrent modification that occurred to it. If you then keep that instance as a class (static or not) member, the preference file won't be reloaded again.
Using Context.getSharedPreferences(...) every time you want to write or read into preferences is not process-safe either, but I guess it's probably the closest that you can get to it at the moment.
If you don't actually need to read the same preference from the different processes, then a workaround could be to use different preferences files for the different processes.
Had exactly the same problem and my solution was to write a ContentProvider based replacement for the SharedPreferences. It works 100% multiprocess.
I made it a library for all of us. Here is the result:
https://github.com/grandcentrix/tray
I just ran into the same problem. I switched my app to run the service in a separate process and realized sharedPreferences was all broken.
Two things:
1) Are you using Editor.apply() or .commit()? I was using .apply(). I started checking my preference file either after the activity or the service made changes to it and realized whenever one would make a change, it would create a new file with only the newly changed value. I.E., a value written from the activity would be erased when a new value was written/changed from the service and vice versa. I switched to .commit() everywhere and this is no longer the case! From the documentation: "Note that when two editors are modifying preferences at the same time, the last one to call apply wins.
2) SharedPreferencesListener doesn't appear to work across processes even after switching to .commit(). You'll have to use Messenger Handlers or Broadcast Intents to notify of a change. When you look at the documentation for the SharedPreferences class it even says "Note: currently this class does not support use across multiple processes. This will be added later." http://developer.android.com/reference/android/content/SharedPreferences.html
In that respect we're lucky we even have the MODE_MULTI_PROCESS flag working to read/write from the same SharedPreferences across different processes.
MODE_MULTI_PROCESS for SharedPreferences is depreciated now (android M -API level 23-onward).It was not process safe.
MODE_MULTI_PROCESS was deprecated in API level 23. You can solve this problem with ContentProvider. DPreference uses a ContentProvider wrappering sharepreference. It has a better performance than using sqlite implmented.
https://github.com/DozenWang/DPreference
Because MODE_MULTI_PROCESS is not currently supported, I haven't found any way to work with Shared Preferences between processes other than working around it.
I know people are sharing the libraries they wrote to address this, but I actually used a third-party library I found on another thread that implements SQLLite in lieu of the Shared Preferences:
https://github.com/hamsterready/dbpreferences
However, what was important to me that I haven't found addressed in other solutions was maintaining the automatic UI generation already built into Preference Fragment - better to be able to specify your elements in XML and call addPreferencesFromResource(R.xml.preferences) than have to build your UI from scratch.
So, to make this work, I subclassed each of the Preference elements I needed (in my case just Preference, SwitchPreference, and EditTextPreference), and overrode a few methods from the base classes to include saving to an instance of DatabaseSharedPreferences taken from the above library.
For example, below I subclass EditTextPreference and get the preference key from the base class. I then override the persist and getPersisted methods in Preference base class. I then override onSetInitialValue, setText, and getText in the EditText base class.
public class EditTextDBPreference extends EditTextPreference {
private DatabaseBasedSharedPreferences mDBPrefs;
private String mKey;
private String mText;
public EditTextDBPreference(Context context) {
super(context);
init(context);
}
public EditTextDBPreference(Context context, AttributeSet attrs) {
super(context, attrs);
init(context);
}
public EditTextDBPreference(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init(context);
}
#TargetApi(Build.VERSION_CODES.LOLLIPOP)
public EditTextDBPreference(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
init(context);
}
private void init(Context context)
{
mDBPrefs = new DatabaseBasedSharedPreferences(context);
mKey = super.getKey();
}
public DatabaseBasedSharedPreferences getSharedDBPreferences()
{
if (mDBPrefs == null) {
return null;
}
return mDBPrefs;
}
#Override
protected boolean persistBoolean(boolean value) {
if (mKey != null)
mDBPrefs.putBoolean(mKey,value);
return super.persistBoolean(value);
}
#Override
protected boolean persistFloat(float value) {
if (mKey != null)
mDBPrefs.putFloat(mKey, value);
return super.persistFloat(value);
}
#Override
protected boolean persistInt(int value) {
if (mKey != null)
mDBPrefs.putInt(mKey, value);
return super.persistInt(value);
}
#Override
protected boolean persistLong(long value) {
if (mKey != null)
mDBPrefs.putLong(mKey, value);
return super.persistLong(value);
}
#Override
protected boolean persistString(String value) {
if (mKey != null)
mDBPrefs.putString(mKey, value);
return super.persistString(value);
}
#Override
protected boolean getPersistedBoolean(boolean defaultReturnValue) {
if (mKey == null)
return false;
return mDBPrefs.getBoolean(mKey, defaultReturnValue);
}
#Override
protected float getPersistedFloat(float defaultReturnValue) {
if (mKey == null)
return -1f;
return mDBPrefs.getFloat(mKey, defaultReturnValue);
}
#Override
protected int getPersistedInt(int defaultReturnValue) {
if (mKey == null)
return -1;
return mDBPrefs.getInt(mKey, defaultReturnValue);
}
#Override
protected long getPersistedLong(long defaultReturnValue) {
if (mKey == null)
return (long)-1.0;
return mDBPrefs.getLong(mKey, defaultReturnValue);
}
#Override
protected String getPersistedString(String defaultReturnValue) {
if (mKey == null)
return null;
return mDBPrefs.getString(mKey, defaultReturnValue);
}
#Override
public void setKey(String key) {
super.setKey(key);
mKey = key;
}
#Override
protected void onSetInitialValue(boolean restoreValue, Object defaultValue) {
setText(restoreValue ? getPersistedString(mText) : (String) defaultValue);
}
#Override
public void setText(String text) {
final boolean wasBlocking = shouldDisableDependents();
boolean textChanged = false;
if (mText != null && !mText.equals(text))
textChanged = true;
mText = text;
persistString(text);
if (textChanged) {
// NOTE: This is a an external class in my app that I use to send a broadcast to other processes that preference settings have changed
BASettingsActivity.SendSettingsUpdate(getContext());
}
final boolean isBlocking = shouldDisableDependents();
if (isBlocking != wasBlocking) {
notifyDependencyChange(isBlocking);
}
}
#Override
public String getText() {
return mText;
}
Then you simply specify the new element in your preferences.xml file, and voila!
You now get the process interoperability of SQLLite and the UI auto-generation of PreferenceFragment!
<com.sampleproject.EditTextDBPreference
android:key="#string/pref_key_build_number"
android:title="#string/build_number"
android:enabled="false"
android:selectable="false"
android:persistent="false"
android:shouldDisableView="false"/>
I know this is an old post, but still a problem that many folks have tried to resolve with a ContentProvider, but that is a problematic approach, as it can cause ANRs, crashes, and general UI performance problems if read/write calls are not done on a background thread.
I don't mean this to be a plug, however, I wrote a library that implements the SharedPreference interface that is process-safe, but doesn't utilize ContentProvider (it uses FileObserver to sync data between processes). https://github.com/pablobaxter/Harmony
My hope is that this solves the issue many folks are having with trying to use SharedPreferences in multiple processes, and still keep their apps performant.

NumberFormatException with IntEditTextPreference

In my android application, I'm using a class called 'IntEditTextPreference'. This class is used when I want a user to introduce a preference as an integer.
But it has a problem. When the user leaves the field empty and press "ok", an NumberFormatException is thrown.
What could I do to avoid the user to press "ok" when the field is empty?
Thanks!
public class IntEditTextPreference extends EditTextPreference
{
public IntEditTextPreference(Context context)
{
super(context);
}
public IntEditTextPreference(Context context, AttributeSet attrs)
{
super(context, attrs);
}
public IntEditTextPreference(Context context, AttributeSet attrs, int defStyle)
{
super(context, attrs, defStyle);
}
#Override
protected String getPersistedString(String defaultReturnValue)
{
return String.valueOf(getPersistedInt(-1));
}
#Override
protected boolean persistString(String value)
{
return persistInt(Integer.valueOf(value));
}
}
You should probably still have a try/catch block around it to catch NumberFormatException. But there are many ways to do this. One way is you can use the buttons setClickable method to false and then true when the text is not null and is integer using onTextChangedListener. Or you could simply let it be clickable but check for empty string or non-integer when the button is clicked and use a warning message toast/alert/label to let the user know they have incorrect field before allowing the button to do anything else. Hope this helps!
In general if you are using a browser-based-application, you would use JavaScript/AJAX to display a button when the input is valid. This is already handled at client side.
To avoid the NumberFormatException, simply add a try-catch-block around the Integer.valueOf(value) statement.
Basically it depends on your client-framework. There might be better framework-specific solutions. Which one do you use?

How to cancel a ListPreference click?

I have 3 preference settings:
- An EditTextPreference to enter a web address
- An EditTextPreference to enter a port
- A ListPreference to show some elements from the web page (e.g. http://www.igs-ip.net:2101) defined by the previous 2 settings.
To make this viable, I was thinking to validate the web connection on the click of the 3rd setting. So far, I was able to catch the click to dynamically fill the ListPreference:
ListPreferenceDynamic dlp = (ListPreferenceDynamic)findPreference(strKey);
dlp.setOnClickListner(new ListPreferenceDynamicOnClickListener()
{
public void onClick(ListPreferenceDynamic preference)
{
String[] astr = astrOpenWebPageAndGetInfo(strAddress, strPort);
if (astr != null)
{
preference.setEntries(astr);
preference.setEntryValues(astr);
}
}
});
My problem now is to find a way to prevent to show the ListPreference dialog when there is a problem, let say, with the internet address. I would like only to show a Toast to explain the problem without showing an empty ListPreference dialog.
A bit late but maybe still useful to some of you:
I figured that the OnPreferenceChangeListener can deal with that. Just register one and return true if you want accept the new value or return false if you want to cancel the click:
yourPreference.setOnPreferenceChangeListener((preference, newValue) -> {
if(condition){
showToast();
return false; //cancel the click
}
else return true; //accept the click
});
The parameter newValue determines the new would-be value of your preference.
I was actually after this.
But the ChangeListener triggers after a new value is picked, right?
So it's not really canceling the list which is what I'm after.
I did found a solution which was making a custom ListPreference class and conditionally handle the onClick.
That way if it's set as not active throught the setActive method the list will not show after clicking the option.
public class CustomListPreference extends ListPreference {
private boolean isActive;
public CustomListPreference(Context context, AttributeSet attrs) {
super(context, attrs);
}
public CustomListPreference(Context context) {
super(context);
}
public void setActive(boolean isActive) {
this.isActive = isActive;
}
public boolean getActive() {
return this.isActive;
}
#Override
public void onClick() {
if (isActive) super.onClick();
}
}

How to set the values of PreferenceActivity(R.xml.preferences) dynamically loading it from SharedPreferences?

I have a MyPreferenceActivity that extends PreferenceActivity like this :
public class MyPreferenceActivity extends PreferenceActivity{
#Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
addPreferencesFromResource(R.xml.preferences);
}
}
I also have custom shared preferences that R.xml.preferences are tied to by individual preference classes...for e.g.
preferences.xml has
<!--EditTextPreference-->
<com.myapp.preferences.PrimaryNumberPreference
android:key="PREFS_PRIMARY_NUMBER"
android:title="#string/primary_number_preference_title"
android:summary="#string/primary_number_preference_summary"
android:dialogTitle="#string/primary_number_preference_dialog_title"
android:dialogMessage="#string/primary_number_preference_dialog_message"
/>
PrimaryNumberPreference.java :
public class PrimaryNumberPreference extends EditTextPreference {
Context ctx = null;
public PrimaryNumberPreference(Context context) {
super(context);
this.ctx = context;
}
public PrimaryNumberPreference(Context context, AttributeSet attrs) {
super(context, attrs);
this.ctx = context;
}
public PrimaryNumberPreference(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
this.ctx = context;
}
#Override
protected void onDialogClosed(boolean positiveResult) {
super.onDialogClosed(positiveResult);
if (positiveResult){
customPreferenceibrary.setPrefsPrimaryNumber(getText());
}
}
}
OK so all seems to be in place. Now what I want is everytime the PreferenceActivity is loaded, it should fetch the current sharedpreference values that are stored in their respective key and prepopulate the elements of the PreferenceActivity...
Eg: PrimaryNumber shared preference is set to 1234 when my app is installed. Now if I go to MyPreferenceActivity and browse the PrimaryNumber EditTextPreference, the text box should be prepopulated by 1234.
Something(pseudo code) like :
MyPreferenceActivity.PrimaryNumberPreference.Val = getSharedPreferences(prefsPrimaryNumber)
How can I do that?
EDIT
Please let me know if the question is unclear and needs better explanantion. I am sure the answer is a simple implementation of something very standard in android. All PreferenceActivity elements pick the current value to be displayed from the stored SharedPreferences only, right?
I think I see two potential problems:
Problem One
If you have a custom preference such as the PrimaryNumberPreference.java you need to manually save the preference when the dialog closes. Is that what you are doing in customPreferenceibrary.setPrefsPrimaryNumber(getText()); ??
If they are not committed in this function then I think they need to be. (Or committed somewhere else in onDialogClosed)
Problem Two
Assuming you are commiting the preferences in setPrefsPrimaryNumber, I think the setPrefsPrimaryNumber needs to receive two paramenters,
The preference name
The actual value
I dont think it is enough just to have the key name set in XML (android:key="PREFS_PRIMARY_NUMBER is not enough)
Hope this helps.
I figured out the answer with the help of suggestions provided by #Mel and Its very easy.
In the class that extends the corresponding PreferenceActivity element(e.g. shown below) just add this.setValue in the constructor of that preference element class. Thats it!
public class selectColorPreference extends ListPreference{
Context ctx = null;
private String error = null;
private int prefsRemoteSMSAccess;
MyPreferencesLibrary myPreferencesLibrary;
public selectColorPreference(Context context) {
super(context);
this.ctx = context;
myPreferencesLibrary = new MyPreferencesLibrary(ctx);
this.setValue(myPreferencesLibrary.getSharedPreferenceValueForColor());
}
}

Categories

Resources