Trying the different preference activities in the ApiDemos for Android 4.0, I see in the code that some methods are deprecated in PreferencesFromCode.java, for example.
So my question is: if I use PreferenceFragment, will it work for all version or only 3.0 or 4.0 and up?
If so, what should I use that works for 2.2 and 2.3 as well?
PreferenceFragment will not work on 2.2 and 2.3 (only API level 11 and above). If you want to offer the best user experience and still support older Android versions, the best practice here seems to be to implement two PreferenceActivity classes and to decide at runtime which one to invoke. However, this method still includes calling deprecated APIs, but you can't avoid that.
So for instance, you have a preference_headers.xml:
<preference-headers xmlns:android="http://schemas.android.com/apk/res/android" >
<header android:fragment="your.package.PrefsFragment"
android:title="...">
<extra android:name="resource" android:value="preferences" />
</header>
</preference-headers>
and a standard preferences.xml (which hasn't changed much since lower API levels):
<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android" android:title="...">
...
</PreferenceScreen>
Then you need an implementation of PreferenceFragment:
public static class PrefsFragment extends PreferenceFragment {
#Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
addPreferencesFromResource(R.xml.preferences);
}
}
And finally, you need two implementations of PreferenceActivity, for API levels supporting or not supporting PreferenceFragments:
public class PreferencesActivity extends PreferenceActivity {
#Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
addPreferencesFromResource(R.xml.preferences);
addPreferencesFromResource(R.xml.other);
}
}
and:
public class OtherPreferencesActivity extends PreferenceActivity {
#Override
public void onBuildHeaders(List<Header> target) {
loadHeadersFromResource(R.xml.preference_headers, target);
}
}
At the point where you want to display the preference screen to the user, you decide which one to start:
if (Build.VERSION.SDK_INT < 11) {
startActivity(new Intent(this, PreferencesActivity.class));
} else {
startActivity(new Intent(this, OtherPreferencesActivity.class));
}
So basically, you have an xml file per fragment, you load each of these xml files manually for API levels < 11, and both Activities use the same preferences.
#Mef Your answer can be simplified even more so that you do not need both of the PreferencesActivity and OtherPreferencesActivity (having 2 PrefsActivities is a PITA).
I have found that you can put the onBuildHeaders() method into your PreferencesActivity and no errors will be thrown by Android versions prior to v11. Having the loadHeadersFromResource() inside the onBuildHeaders did not throw and exception on 2.3.6, but did on Android 1.6. After some tinkering though, I found the following code will work in all versions so that only one activity is required (greatly simplifying matters).
public class PreferencesActivity extends PreferenceActivity {
protected Method mLoadHeaders = null;
protected Method mHasHeaders = null;
/**
* Checks to see if using new v11+ way of handling PrefFragments.
* #return Returns false pre-v11, else checks to see if using headers.
*/
public boolean isNewV11Prefs() {
if (mHasHeaders!=null && mLoadHeaders!=null) {
try {
return (Boolean)mHasHeaders.invoke(this);
} catch (IllegalArgumentException e) {
} catch (IllegalAccessException e) {
} catch (InvocationTargetException e) {
}
}
return false;
}
#Override
public void onCreate(Bundle aSavedState) {
//onBuildHeaders() will be called during super.onCreate()
try {
mLoadHeaders = getClass().getMethod("loadHeadersFromResource", int.class, List.class );
mHasHeaders = getClass().getMethod("hasHeaders");
} catch (NoSuchMethodException e) {
}
super.onCreate(aSavedState);
if (!isNewV11Prefs()) {
addPreferencesFromResource(R.xml.preferences);
addPreferencesFromResource(R.xml.other);
}
}
#Override
public void onBuildHeaders(List<Header> aTarget) {
try {
mLoadHeaders.invoke(this,new Object[]{R.xml.pref_headers,aTarget});
} catch (IllegalArgumentException e) {
} catch (IllegalAccessException e) {
} catch (InvocationTargetException e) {
}
}
}
This way you only need one activity, one entry in your AndroidManifest.xml and one line when you invoke your preferences:
startActivity(new Intent(this, PreferencesActivity.class);
UPDATE Oct 2013:
Eclipse/Lint will warn you about using the deprecated method, but just ignore the warning. We are using the method only when we have to, which is whenever we do not have v11+ style preferences and must use it, which is OK. Do not be frightened about Deprecated code when you have accounted for it, Android won’t remove deprecated methods anytime soon. If it ever did occur, you won’t even need this class anymore as you would be forced to only target newer devices. The Deprecated mechanism is there to warn you that there is a better way to handle something on the latest API version, but once you have accounted for it, you can safely ignore the warning from then on. Removing all calls to deprecated methods would only result in forcing your code to only run on newer devices — thus negating the need to be backward compatible at all.
There's a newish lib that might help.
UnifiedPreference is a library for working with all versions of the
Android Preference package from API v4 and up.
Problem with previous answers is that it will stack all preferences to a single screen on pre-Honecomb devices (due to multiple calls of addPreferenceFromResource()).
If you need first screen as list and then the screen with preferences (such as using preference headers), you should use Official guide to compatible preferences
I wanted to point out that if you start at http://developer.android.com/guide/topics/ui/settings.html#PreferenceHeaders and work your way down to the section for "Supporting older versions with preference headers" it will make more sense. The guide there is very helpful and does work well. Here's an explicit example following their guide:
So start with file preference_header_legacy.xml for android systems before HoneyComb
<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android">
<Preference
android:title="OLD Test Title"
android:summary="OLD Test Summary" >
<intent
android:targetPackage="example.package"
android:targetClass="example.package.SettingsActivity"
android:action="example.package.PREFS_ONE" />
</Preference>
Next create file preference_header.xml for android systems with HoneyComb+
<preference-headers xmlns:android="http://schemas.android.com/apk/res/android">
<header
android:fragment="example.package.SettingsFragmentOne"
android:title="NEW Test Title"
android:summary="NEW Test Summary" />
</preference-headers>
Next create a preferences.xml file to hold your preferences...
<?xml version="1.0" encoding="utf-8"?>
<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android" >
<CheckBoxPreference
android:key="pref_key_auto_delete"
android:summary="#string/pref_summary_auto_delete"
android:title="#string/pref_title_auto_delete"
android:defaultValue="false" />
</PreferenceScreen>
Next create the file SettingsActivity.java
package example.project;
import java.util.List;
import android.annotation.SuppressLint;
import android.os.Build;
import android.os.Bundle;
import android.preference.PreferenceActivity;
public class SettingsActivity extends PreferenceActivity{
final static String ACTION_PREFS_ONE = "example.package.PREFS_ONE";
#SuppressWarnings("deprecation")
#Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
String action = getIntent().getAction();
if (action != null && action.equals(ACTION_PREFS_ONE)) {
addPreferencesFromResource(R.xml.preferences);
}
else if (Build.VERSION.SDK_INT < Build.VERSION_CODES.HONEYCOMB) {
// Load the legacy preferences headers
addPreferencesFromResource(R.xml.preference_header_legacy);
}
}
#SuppressLint("NewApi")
#Override
public void onBuildHeaders(List<Header> target) {
loadHeadersFromResource(R.xml.preference_header, target);
}
}
Next create the class SettingsFragmentOne.java
package example.project;
import android.annotation.SuppressLint;
import android.os.Bundle;
import android.preference.PreferenceFragment;
#SuppressLint("NewApi")
public class SettingsFragmentOne extends PreferenceFragment {
#Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
addPreferencesFromResource(R.xml.preferences);
}
}
AndroidManifest.xml, added this block between my <application> tags
<activity
android:label="#string/app_name"
android:name="example.package.SettingsActivity"
android:exported="true">
</activity>
and finally, for the <wallpaper> tag...
<wallpaper xmlns:android="http://schemas.android.com/apk/res/android"
android:description="#string/description"
android:thumbnail="#drawable/ic_thumbnail"
android:settingsActivity="example.package.SettingsActivity"
/>
I am using this library, which has an AAR in mavenCentral so you can easily include it if you are using Gradle.
compile 'com.github.machinarius:preferencefragment:0.1.1'
Related
I'm trying to learn ways to build preference pages in the Xamarin Android application.
I found a lot of examples with PreferenceFragment but it was marked as deprecated and it is difficult for me to rewrite them on the current stage.
I've created activity to represent headers. I added IntentFilter so I can access this activity from apps list in the settings menu. Also it has internal class to group some preferences together:
namespace droid.examples.Preferences
{
[Activity(Label = "Settings activity", Theme = "#style/AppTheme", Name = "droid.examples.Preferences.SettingsActivity")]
[IntentFilter(new string[] { "android.intent.action.APPLICATION_PREFERENCES" })]
public class SettingsActivity : PreferenceActivity
{
public override void OnBuildHeaders(IList<Header> target)
{
base.OnBuildHeaders(target);
LoadHeadersFromResource(Resource.Xml.preference_headers, target);
}
public class SettingsFragment : PreferenceFragmentCompat
{
public override void OnCreatePreferences(Bundle savedInstanceState, string rootKey)
{
// Load the Preferences from the XML file
SetPreferencesFromResource(Resource.Xml.app_preferences, rootKey);
}
}
}
}
My app_preferences.xml which I can't open by selecting "Prefs 1" header from preference_headers.xml:
<?xml version="1.0" encoding="utf-8" ?>
<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android">
<PreferenceCategory android:title="Category">
<CheckBoxPreference
android:key="checkbox_preference"
android:title="Developer mode"
android:summary="Allow user to see detailed messages" />
</PreferenceCategory>
</PreferenceScreen>
I have preference_headers.xml. It opens when I click on gear wheel near application name. It looks like this:
<?xml version="1.0" encoding="utf-8" ?>
<preference-headers xmlns:android="http://schemas.android.com/apk/res/android">
<header android:fragment="droid.examples.Preferences.SettingsActivity.SettingsFragment"
android:title="Prefs 1"
android:summary="An example of some preferences." />
</preference-headers>
My package name: droid.examples
I think that one problem related to the android:fragment attribute value.
What is the rules to build that value?
I suppose that it must start from 'package name'. Should it contain namespace between class name and package name?
What does $ mean in the attribute value? Is it used to mark internal class? I saw in the several places next code:
android:fragment="com.example.android.apis.preference.PreferenceWithHeaders$Prefs1Fragment"
I hope you can help me find where I made a mistakes.
Source code from GitHub
I spend a lot of time to investigate that issue and I want to make a summary.
We have to override IsValidFragment method in the SettingsActivity:
protected override bool IsValidFragment(string fragmentName)
{
return fragmentName == "droid.examples.preferences.SettingsActivity.SettingsFragment";
}
My SettingsActivity extends PreferenceActivity. Thanks to #Jeremy advice about implementation of IOnPreferenceStartFragmentCallback I find out that base class already extends it.
public abstract class PreferenceActivity ...
{
...
public virtual bool OnPreferenceStartFragment(PreferenceFragment caller, Preference pref);
...
}
So, I probably need to use PreferenceFragment instead of PreferenceFragmentCompat to make code consistent:
public class SettingsFragment : PreferenceFragment
{
public override void OnCreate(Bundle savedInstanceState)
{
base.OnCreate(savedInstanceState);
AddPreferencesFromResource(Resource.Xml.app_preferences_for_header);
}
}
Also we have to add Register attribute to our fragment:
[Register("droid.examples.preferences.SettingsActivity.SettingsFragment")]
public class SettingsFragment : PreferenceFragment
{
}
Finally I updated preference_headers.xml
<?xml version="1.0" encoding="utf-8" ?>
<preference-headers xmlns:android="http://schemas.android.com/apk/res/android">
<header android:fragment="droid.examples.preferences.SettingsActivity.SettingsFragment"
android:title="Prefs 1"
android:summary="An example of some preferences." />
</preference-headers>
android:fragment attribute value can contains '$' but '+' won't work because Register doesn't support it and we will get compilation error.
Thanks everyone who tried to help me
Looks like the string you provide is just a message to send to your parent activity. Your parent activity is responsible for instantiating the right Fragment, and performing the ceremony to render it.
The platform docs seem to indicate as such:
When a user taps a Preference with an associated Fragment, the interface method PreferenceFragmentCompat.OnPreferenceStartFragmentCallback.onPreferenceStartFragment() is called
At time of writing, there's a code snippet on that page, which I've translated for my own project more-or-less as follows:
// This has to go in the activity which hosts the PreferenceFragment
// which owns the Preference that has the `android:fragment` attribute.
using Android.Support.V7.Preferences;
using Android.Support.V4.App;
partial class MyActivity :
PreferenceFragmentCompat.IOnPreferenceStartFragmentCallback
{
Fragment GetFragmentForPrefString(string prefFragment)
{
// you implement this
}
const int fragmentContainerId = Resource.Id.whatever;
public bool OnPreferenceStartFragment(
PreferenceFragmentCompat caller, Preference pref)
{
string prefString = pref.Fragment;
var transaction = SupportFragmentManager.BeginTransaction();
transaction.Replace(fragmentContainerId,
GetFragmentForPrefString(prefString));
// you'll probably also want to add it to the back stack,
// but it's not strictly necessary I guess.
transaction.Commit();
return true;
}
}
Their sample involves the Java API method getSupportFragmentManager().getFragmentFactory() which doesn't appear to be part of the V28 Xamarin support NuGet packages. But honestly I'm not sure why that level of indirection is necessary; I'd suggest you simply implement something like
switch (prefFragmentName)
{
case "Fragment 1":
return new Fragment1();
// etc
I have a project that I originally developed using Android Studio. I decided to convert it to Xamarin (Visual Studio 2015).
After hours of porting all the code over, everything works except for my Settings activity (PreferenceActivity). I have a few PreferenceFragments that make up the settings, but all of them give me "Unable to instantiate fragment". Here is the exception I am getting:
Java.Lang.RuntimeException: Unable to start activity ComponentInfo{test.mypackagename/md50d00e677e41fc49f8b3c16e79df2b77f.SettingsActivity}: android.app.Fragment$InstantiationException: Unable to instantiate fragment test.mypackagename.GeneralPreferenceFragment: make sure class name exists, is public, and has an empty constructor that is public
I have been looking online for a solution but I just cant seem to find one. Everywhere I look they say make sure there is an empty public constructor, if its an inner class it has to be static. But I have the empty constructor and its not an inner class, its in its own file.
Here is my SettingsActivity.cs:
namespace test.mypackagename
{
public class SettingsActivity : PreferenceActivity
{
protected override void OnPostCreate(Bundle savedInstanceState)
{
base.OnPostCreate(savedInstanceState);
}
public override void OnBuildHeaders(IList<Header> target)
{
LoadHeadersFromResource(Resource.Xml.pref_headers, target);
}
}
}
Here is my GeneralPreferenceFragment.cs:
namespace test.mypackagename
{
public class GeneralPreferenceFragment : PreferenceFragment
{
public GeneralPreferenceFragment() { }
public override void OnCreate(Bundle savedInstanceState)
{
base.OnCreate(savedInstanceState);
AddPreferencesFromResource(Resource.Xml.pref_general);
}
}
}
And here is my pref_headers.xml:
<preference-headers xmlns:android="http://schemas.android.com/apk/res/android">
<header android:fragment="test.mypackagename.GeneralPreferenceFragment"
android:title="#string/pref_header_general" />
<header android:fragment="test.mypackagename.OtherPreferenceFragment1"
android:title="#string/pref_header_other1" />
<header android:fragment="test.mypackagename.OtherPreferenceFragment2"
android:title="#string/pref_header_other2" />
<header android:fragment="test.mypackagename.OtherPreferenceFragment3"
android:title="#string/pref_header_other3" />
<header android:fragment="test.mypackagename.OtherPreferenceFragment4"
android:title="#string/pref_header_other4" />
</preference-headers>
This was working fine before so Im not sure what the issue could be. Any help would be much appreciated.
I think you are running into this problem because when not using the [Register] attribute on your PreferenceFragment then its name appended with a MD5 sum by Xamarin.
So in order to actually have the namespace you expect it to in the pref_headers.xml you need to attribute your class:
[Register("test.mypackagename.GeneralPreferenceFragment")]
public class GeneralPreferenceFragment: PreferenceFragment
{
// code here
}
EDIT:
I've just tested the code and it works fine on my machine. I am not using any support packages or anything.
pref_general.xml
<?xml version="1.0" encoding="utf-8" ?>
<PreferenceScreen
xmlns:android="http://schemas.android.com/apk/res/android">
<PreferenceCategory
android:title="durr">
<CheckBoxPreference
android:key="checkbox_preference"
android:title="herp"
android:summary="derp" />
</PreferenceCategory>
</PreferenceScreen>
pref_headers.xml
<?xml version="1.0" encoding="utf-8" ?>
<preference-headers xmlns:android="http://schemas.android.com/apk/res/android">
<header android:fragment="test.mypackagename.GeneralPreferenceFragment"
android:title="general" />
</preference-headers>
SettingsActivity.cs
[Activity(Label = "SettingsActivity")]
public class SettingsActivity : PreferenceActivity
{
public override void OnBuildHeaders(IList<Header> target)
{
LoadHeadersFromResource(Resource.Xml.pref_headers, target);
}
}
GeneralPreferenceFragment.cs
[Register("test.mypackagename.GeneralPreferenceFragment")]
public class GeneralPreferenceFragment : PreferenceFragment
{
public override void OnCreate(Bundle savedInstanceState)
{
base.OnCreate(savedInstanceState);
AddPreferencesFromResource(Resource.Xml.pref_general);
}
}
This works fine and the app shows up the SettingsActivity with first a general option, after clicking on that it shows up a Title and CheckBox.
This worked even without providing any ctor to the GeneralPreferenceFragment. However, you could try add these:
public GeneralPreferenceFragment()
{
}
public GeneralPreferenceFragment(IntPtr javaRef, JniHandleOwnership transfer)
: base(javaRef, transfer)
{
}
The latter ctor is often needed when the app is coming back from background or when the Java world is invoking the class somehow.
Is there a way of handling an OnPreferenceChangeListener in API11. Simply calling Preference.setOnPreferenceChangeListener throws a lint error stating it requires API14+, however my app is designed for API11+ and I don't want to limit the number of devices it can run on further.
Update: I have checked in the development documentation and this should be available from API1
Edit: Code
import android.os.Bundle;
import android.preference.Preference;
import android.preference.Preference.OnPreferenceChangeListener;
import android.preference.PreferenceActivity;
import android.preference.SwitchPreference;
#SuppressWarnings("deprecation")
public class SettingsActivity extends PreferenceActivity {
private static final String tag = "PREFERENCES";
SwitchPreference appPower;
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
addPreferencesFromResource(R.xml.preferences_layout);
setupPreferenceElements();
LogHelper.i(tag, "Preferences Activity Started");
}
private void setupPreferenceElements() {
appPower = (SwitchPreference) getPreferenceManager().findPreference(
"power");
appPower.setChecked(SettingsManager.getPower());
appPower.setOnPreferenceChangeListener(new OnPreferenceChangeListener() {
#Override
public boolean onPreferenceChange(Preference preference,
Object newValue) {
LogHelper.i(tag, "App Power Changed to " + newValue.toString());
SettingsManager.setPower(Boolean.parseBoolean(newValue
.toString()));
return true;
}
});
}
}
android.preference.SwitchPreference was only added in API v14 (along with TwoStatePreference in case your first though was to use that instead)
If you want to continue using SwitchPreference for 14+ and support 13- you will need to:
make a folder res/xml-v14/
have your current preferences.xml with your SwitchPreference
make another folder (if needed) res/xml/ with another preferences.xml and change your SwitchPreference to a CheckBoxPreference.
This will then load the correct version depending on the API version used. You will need to check for API version in your Java code too.
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH) {
appPower = new SwitchPreference(this);
} else {
appPower = new CheckBoxPreference(this);
}
If you don't like the CheckboxPreference you can always extend Preference or extend CheckboxPreference and roll your own solution. This may actually be the best solution as it would have the advantage of being able to wrap both versions in one class where you can then support your preferred implementation, ensuring the methods you want to use are exposed and do not fail, without manual version checks type casting everywhere you use the Preference.
While doing a bit of searching, I stumbled across this library you might use. It aims to make a support SwitchPreference. I can't speak to how well it works that or if you should use it though, I've never tried it.
NOTICE: Please save yourself some time and refer to the accepted answer, no need to read all the quesiton.
You may read the rest of the question and the answer I provided for an alternative (although less sophisticated) method.
Also, you may want to take advantage of the fix for the background glitch in Android 2.X, by adding the related piece of code to your preference activity class.
Background
Being a newbie to Android coding, but somewhat experienced in other programming languages/frameworks, I was expecting my walk to Android application coding would be a rather pleasant one. It was so, until I stumbled upon this problem:
Eclipse wizard for Android projects suggested I could reach a 95% of devices if I set my minimum API to 8 (Android 2.2). I didn't need to do any fancy things with my app anyways, so I thought, "sure, why not?". Everything was okay, except occasionally I'd find several methods/classes that were deprecated in most recent API versions, and so I had to devise ways to keep using the old ways for old devices, and try to use as much as possible the new ways for newer Android versions. This is one such occasion.
After using the Eclipse wizard for creating a preference activity, I realized that the Eclipse precompiler/parser/checker(or whatever it's called) Lint, would complain about not being able to use the new ways of creating/managing preferences in older API versions. So I thought, "all right, screw the new ways. Let's do it old way and since new API versions are supposed to be backward-compatible, it should be okay", but it wasn't. Old way used methods/classes that are marked as deprecated; which, to me, means, even though they'd still work in current API, they'd stop working at some point in future releases.
So I started searching for the right way to do this, and finally hit this page: What to use instead of "addPreferencesFromResource" in a PreferenceActivity? where Garret Wilson, explains a way to use old preference screen resources in a way compatible with the new ways. It was great, and finally had the feeling I could move on with my app coding, except it wouldn't work when targeting older APIs, as it was using newer APIs code. So I had to devise a way to make it work for both old APIs and newer. After tinkering with it for a while I managed to find a way, by using precompiler(or whatever it's called) annotations and the great getClass().getMethod() along with exceptions.
Everything seemed to work flawlessly until I created a preference sub-screen. It was displaying correctly in newer Android versions, but when I tried in older ones, I could merely see a black screen. After much searching, I found this page which explains the issue: http://code.google.com/p/android/issues/detail?id=4611 This is apparently a known glitch that's been around several Android versions for a good while. I read the whole thread and found several proposed solutions to the problem, but I really didn't like entirely any of them. I, for one, prefer to avoid as much static stuff as I can, and do things programmatically. I prefer automation over repetitive work. Some solutions suggested to create sub-screens as parent screens, then adding them onto the manifest file, and calling them from the parent screen through an intent. I'd really hate having to keep track of those things: entries in manifest, separated screen resource file, intents... So that was a no-no for me. I kept looking and found a programmatic approach I liked much better... only to find that it didn't work. It consisted of iterating through the whole view tree of the preference screen and assigning a proper background to preference sub-screens, but it just didn't work because, as I later found out after much debugging, preference sub-screens views are not a child of preference screen views. I had to find a way to achieve this myself. I tried as many things as I could think of, researched and researched to no avail. I was at the verge of abandoning at several occasions, but after some two weeks of continued effort and much debugging I found a workaround, which I posted in comment #35.
Opinion
It really isn't the perfect solution/approach, and I'm aware of several of its drawbacks, but it's one that works, so I decided I would share it. Hopefully I'm not being too ridiculous in my enthusiasm to share what has taken me what I'd consider quite a lot of effort, as I'm aware it's not that great of an issue, that any experienced coder could solve. But hey, I think sharing knowledge makes me a bit better, no matter how much I brag, than an experienced coder who keeps everything to himself. Just sharing my opinion, because I can't believe nobody ever had this problem before, but I do believe many have had it and didn't bother to share their knowledge.
I present you in the answer with a proposed class to use over several versions of Android, and some suggestions on its usage. I'm open to discussion and contributions to make it a better class.
Known issues:
Parent screen Decor view background is cloned onto child screen Decor view background, which apparently isn't the normal behavior.
Status: dismissed until somebody comes up with a good reason to fix this
Crashes upon screen rotation
Status: Fixed.
Probably related to resource visibility by newer API implementation (inner class PF)
Apparently inherited classes from preferenceFragment need to have all their members static. I guess it makes sense if you're supposed to inherit every time you need to use a new fragment
If you are on the latest ADT plugin, there is an option to easily create a preference Activity that supports most older Android versions as well as all the new ones.
Right click on your project -> Other -> Android Activity
Then choose SettingsActivity
The Activity created will take take care of working with both high and low API versions since it uses if statements to choose the appropriate method of displaying the preferences.
EDIT
A good point was brought up: Phone-Sized devices, regardless of API version use the old PreferenceActivity methods.
The quickest way to get API 11+ devices to use Fragments is to remove !isXLargeTablet(context); from isSimplePreferences()
private static boolean isSimplePreferences(Context context) {
return ALWAYS_SIMPLE_PREFS
|| Build.VERSION.SDK_INT < Build.VERSION_CODES.HONEYCOMB;
}
However, now the user has more navigation to do.
This is because onBuildHeaders() is called.
To get rid of this, we will need to make our own PreferenceFragment that adds each xml resource.
#TargetApi(Build.VERSION_CODES.HONEYCOMB)
public static class AllPreferencesFragment extends PreferenceFragment{
#Override
public void onCreate (Bundle savedInstanceState){
super.onCreate(savedInstanceState);
addPreferencesFromResource(R.xml.pref_general);
// Add 'notifications' preferences, and a corresponding header.
PreferenceCategory fakeHeader = new PreferenceCategory(getActivity());
fakeHeader.setTitle(R.string.pref_header_notifications);
getPreferenceScreen().addPreference(fakeHeader);
addPreferencesFromResource(R.xml.pref_notification);
// Add 'data and sync' preferences, and a corresponding header.
fakeHeader = new PreferenceCategory(getActivity());
fakeHeader.setTitle(R.string.pref_header_data_sync);
getPreferenceScreen().addPreference(fakeHeader);
addPreferencesFromResource(R.xml.pref_data_sync);
// Bind the summaries of EditText/List/Dialog/Ringtone preferences to
// their values. When their values change, their summaries are updated
// to reflect the new value, per the Android Design guidelines.
bindPreferenceSummaryToValue(findPreference("example_text"));
bindPreferenceSummaryToValue(findPreference("example_list"));
bindPreferenceSummaryToValue(findPreference("notifications_new_message_ringtone"));
bindPreferenceSummaryToValue(findPreference("sync_frequency"));
}
}
If you can determine the screen size from outside the Activity that launches the settings, you can specify a fragment for it to launch via EXTRA_SHOW_FRAGMENT
i.putExtra(SettingsActivity.EXTRA_SHOW_FRAGMENT, "com.example.test.SettingsActivity$AllPreferencesFragment");
Or you can have the SettingsActivity determine whether or not to show this Fragment (assuming you're happy with the isXLargeTablet() method.
Change onBuildHeaders() to:
#Override
#TargetApi(Build.VERSION_CODES.HONEYCOMB)
public void onBuildHeaders(List<Header> target) {
if (!isSimplePreferences(this) && isXLargeTablet(this)) {
loadHeadersFromResource(R.xml.pref_headers, target);
}
}
Add this method:
#TargetApi(Build.VERSION_CODES.HONEYCOMB)
private void setupNewApiPhoneSizePreferences() {
if (!isXLargeTablet(this) && Build.VERSION.SDK_INT > Build.VERSION_CODES.HONEYCOMB){
getFragmentManager().beginTransaction().replace(android.R.id.content, new AllPreferencesFragment()).commit();
}
}
And in onPostCreate() add the method call.
setupNewApiPhoneSizePreferences();
This should now use non-deprecated calls from API 11 onwards.
You can use this class to display a preference screen in all Android versions from 2.X to 4.X, by feeding it with a preference screen resource.
You may use it directly by renaming it if you like, but I'd suggest you to add it to your project as is, and inherit from it, which is much cleaner if you need to work with several parent preference screens.
If you'd like to use it directly, just replace prefs value with your preference screen resource ID.
If you'd like to inherit from it, you should do it like this:
import android.os.Bundle;
public class MyPreferencesActivity extends CompatiblePreferenceActivity
{
#Override
protected void onCreate(final Bundle savedInstanceState)
{
setPrefs(R.xml.mypreferencesactivity);
super.onCreate(savedInstanceState);
}
}
ALWAYS call setPrefs(int) before calling super.onCreate(Bundle)
If, for some reason, you'd just like to take advantage of the glitch-fix and create preferences on your own, you may either just copy the glitch-fix code into your own preference activity, or inherit from the class and catch the PrefsNotSet exception as follows:
import android.os.Bundle;
public class MyPreferencesActivity extends CompatiblePreferenceActivity
{
#Override
protected void onCreate(final Bundle savedInstanceState)
{
try{
super.onCreate(savedInstanceState);
}catch(PrefsNotSetException e){};
}
}
And finally, the class:
import android.annotation.TargetApi;
import android.os.Bundle;
import android.preference.Preference;
import android.preference.PreferenceActivity;
import android.preference.PreferenceFragment;
import android.preference.PreferenceScreen;
public class CompatiblePreferenceActivity extends PreferenceActivity
{
private int prefs=0;
//Get/Set
public void setPrefs(int prefs)
{
this.prefs=prefs;
}
//Exception
protected static class PrefsNotSetException extends RuntimeException
{
private static final long serialVersionUID = 1L;
PrefsNotSetException()
{
super("\"prefs\" should be set to a valid preference resource ID.");
}
}
//Creation
#Override
protected void onCreate(final Bundle savedInstanceState)
{
super.onCreate(savedInstanceState);
if (prefs==0)
throw new PrefsNotSetException();
else
try {
getClass().getMethod("getFragmentManager");
AddResourceApi11AndGreater();
}
catch (NoSuchMethodException e) { //Api < 11
AddResourceApiLessThan11();
}
}
#SuppressWarnings("deprecation")
protected void AddResourceApiLessThan11()
{
addPreferencesFromResource(prefs);
}
#TargetApi(11)
protected void AddResourceApi11AndGreater()
{
PF.prefs=prefs;
getFragmentManager().beginTransaction().replace(
android.R.id.content, new PF()).commit();
}
#TargetApi(11)
public static class PF extends PreferenceFragment
{
private static int prefs;
#Override
public void onCreate(final Bundle savedInstanceState)
{
super.onCreate(savedInstanceState);
addPreferencesFromResource(prefs);
}
}
//Sub-screen background glitch fix
#SuppressWarnings("deprecation")
#Override
public boolean onPreferenceTreeClick(PreferenceScreen preferenceScreen,
Preference preference)
{
super.onPreferenceTreeClick(preferenceScreen, preference);
if (preference!=null)
if (preference instanceof PreferenceScreen)
if (((PreferenceScreen)preference).getDialog()!=null)
((PreferenceScreen)preference).getDialog().
getWindow().getDecorView().
setBackgroundDrawable(this.getWindow().
getDecorView().getBackground().getConstantState().
newDrawable());
return false;
}
}
Well, working with the autogenerated SettingsActivity got pretty old pretty quickly. One has to scroll up and down past boilerplate code - moreover it's full of yellow warnings and I hate yellow (deprecated warnings can't be avoided altogether though - see What to use instead of "addPreferencesFromResource" in a PreferenceActivity?, where also the matter of how to make cross API PreferenceActivity is touched also - and Was PreferenceFragment intentionally excluded from the compatibility package? for a discussion). And also you may easily get an NPE - did you know that onPostCreate() is actually onPostStart() - so findPreference() returns null in onStart().
Now there are solutions involving reflection but reflection is to be avoided (like hell it is) - and since we are not interested in pre 2 versions of android reflection can be avoided (see Is checking SDK_INT enough or is lazy loading needed for using newer android APIs ? Why?). Also there are solutions involving choosing a class at runtime - but having 2 classes sucks and is not OOP anyways (for those and other solutions see the answer to related question: PreferenceActivity Android 4.0 and earlier).
So I came up with an abstract base class, which is the correct Java and OO way of doing things (except if you need Eclair and below where you do need reflection and/or lazy loading of classes to avoid VerifyErrors), where I moved the autogenerated boilerplate code:
import android.annotation.TargetApi;
import android.content.Context;
import android.content.res.Configuration;
import android.os.Build;
import android.os.Bundle;
import android.preference.PreferenceActivity;
import android.preference.PreferenceFragment;
import java.util.List;
/**
* A {#link PreferenceActivity} that presents a set of application settings. On
* handset devices, settings are presented as a single list. On tablets,
* settings are split by category, with category headers shown to the left of
* the list of settings.
* <p>
* See <a href="http://developer.android.com/design/patterns/settings.html">
* Android Design: Settings</a> for design guidelines and the <a
* href="http://developer.android.com/guide/topics/ui/settings.html">Settings
* API Guide</a> for more information on developing a Settings UI.
*
* Defines two abstract methods that need be implemented by implementators.
*/
public abstract class BaseSettings extends PreferenceActivity {
/**
* Determines whether to always show the simplified settings UI, where
* settings are presented in a single list. When false, settings are shown
* as a master/detail two-pane view on tablets. When true, a single pane is
* shown on tablets.
*/
private static final boolean ALWAYS_SIMPLE_PREFS = false;
/**
* Helper method to determine if the device has an extra-large screen. For
* example, 10" tablets are extra-large.
*/
#TargetApi(Build.VERSION_CODES.GINGERBREAD)
private static boolean isXLargeTablet(Context context) {
return (context.getResources().getConfiguration().screenLayout &
Configuration.SCREENLAYOUT_SIZE_MASK)
>= Configuration.SCREENLAYOUT_SIZE_XLARGE;
}
/** {#inheritDoc} */
#Override
public final boolean onIsMultiPane() { // never used by us
return isXLargeTablet(this) && !isSimplePreferences(this);
}
/**
* Determines whether the simplified settings UI should be shown. This is
* true if this is forced via {#link #ALWAYS_SIMPLE_PREFS}, or the device
* doesn't have newer APIs like {#link PreferenceFragment}, or the device
* doesn't have an extra-large screen. In these cases, a single-pane
* "simplified" settings UI should be shown.
*/
private static final boolean isSimplePreferences(Context context) {
return ALWAYS_SIMPLE_PREFS
|| Build.VERSION.SDK_INT < Build.VERSION_CODES.HONEYCOMB
|| !isXLargeTablet(context);
}
#Override
protected final void onCreate(Bundle savedInstanceState) {
// disallow onCreate(), see comment in onPostCreate()
super.onCreate(savedInstanceState);
}
#Override
protected final void onStart() {
// disallow onStart(), see comment in onPostCreate()
super.onStart();
}
#Override
protected void onPostCreate(Bundle savedInstanceState) {
// onPostCreate() probably is needed because onBuildHeaders() is called
// after onCreate() ? This piece of err code should be called
// onPostStart() btw - so yeah
super.onPostCreate(savedInstanceState);
setupSimplePreferencesScreen();
// findPreference will return null if setupSimplePreferencesScreen
// hasn't run, so I disallow onCreate() and onStart()
}
/**
* Shows the simplified settings UI if the device configuration if the
* device configuration dictates that a simplified, single-pane UI should be
* shown.
*/
private void setupSimplePreferencesScreen() {
if (!isSimplePreferences(this)) {
return;
}
buildSimplePreferences();
}
/** {#inheritDoc} */
/*
* Subclasses of PreferenceActivity should implement onBuildHeaders(List) to
* populate the header list with the desired items. Doing this implicitly
* switches the class into its new "headers + fragments" mode rather than
* the old style of just showing a single preferences list (from
* http://developer
* .android.com/reference/android/preference/PreferenceActivity.html) -> IE
* this is called automatically - reads the R.xml.pref_headers and creates
* the 2 panes view - it was driving me mad - #inheritDoc my - It does not
* crash in Froyo cause isSimplePreferences is always true for
* Build.VERSION.SDK_INT < Build.VERSION_CODES.HONEYCOMB - #Override has
* nothing to do with runtime and of course on Froyo this is never called by
* the system
*/
#Override
#TargetApi(Build.VERSION_CODES.HONEYCOMB)
public final void onBuildHeaders(List<Header> target) {
if (!isSimplePreferences(this)) {
loadHeadersFromResource(getHeadersXmlID(), target);
}
}
// =========================================================================
// Abstract API
// =========================================================================
/**
* Must return an id for the headers xml file. There you define the headers
* and the corresponding PreferenceFragment for each header which you must
* of course implement. This is used in the super implementation of
* {#link #onBuildHeaders(List)}
*
* #return an id from the R file for the xml containing the headers
*/
abstract int getHeadersXmlID();
/**
* Builds a pre Honeycomb preference screen. An implementation would use the
* (deprecated)
*{#link android.preference.PreferenceActivity#addPreferencesFromResource(int)}
*/
abstract void buildSimplePreferences();
}
And a sample implementation:
public final class SettingsActivity extends BaseSettings implements
OnSharedPreferenceChangeListener {
private static final int PREF_HEADERS_XML = R.xml.pref_headers;
private static CharSequence master_enable;
private OnPreferenceChangeListener listener;
private static Preference master_pref;
private static final String TAG = SettingsActivity.class.getSimpleName();
private SharedPreferences sp;
/** Used as canvas for the simple preferences screen */
private static final int EMPTY_PREF_RESOURCE = R.xml.pref_empty;
private static int PREF_RESOURCE_SETTINGS = R.xml.pref_data_sync;
// abstract overrides
#Override
int getHeadersXmlID() {
return PREF_HEADERS_XML;
}
#Override
void buildSimplePreferences() {
// In the simplified UI, fragments are not used at all and we instead
// use the older PreferenceActivity APIs.
// THIS is a blank preferences layout - which I need so
// getPreferenceScreen() does not return null - so I can add a header -
// alternatively you can very well comment everything out apart from
// addPreferencesFromResource(R.xml.pref_data_sync);
addPreferencesFromResource(EMPTY_PREF_RESOURCE);
// Add 'data and sync' preferences, and a corresponding header.
PreferenceCategory fakeHeader = new PreferenceCategory(this);
fakeHeader.setTitle(R.string.pref_header_data_sync);
getPreferenceScreen().addPreference(fakeHeader);
addPreferencesFromResource(PREF_RESOURCE_SETTINGS);
}
// here is the work done
#Override
protected void onPostCreate(Bundle savedInstanceState) {
super.onPostCreate(savedInstanceState);
master_enable = getResources().getText(
R.string.enable_monitoring_master_pref_key);
listener = new ToggleMonitoringListener();
// DefaultSharedPreferences - register listener lest Monitor aborts
sp = PreferenceManager.getDefaultSharedPreferences(this);
sp.registerOnSharedPreferenceChangeListener(this);
master_pref = findPreference(master_enable.toString());
}
#Override
protected void onResume() {
super.onResume();
master_pref.setOnPreferenceChangeListener(listener); // no way to
// unregister, see: https://stackoverflow.com/a/20493608/281545 This
// listener reacts to *manual* updates - so no need to be active
// outside onResume()/onPause()
}
#Override
protected void onDestroy() {
// may not be called (as onDestroy() is killable), but no leak,
// see: https://stackoverflow.com/a/20493608/281545
sp.unregisterOnSharedPreferenceChangeListener(this);
super.onDestroy();
}
/**
* Toggles monitoring and sets the preference summary.Triggered on *manual*
* update of the *single* preference it is registered with, but before this
* preference is updated and saved.
*/
private static class ToggleMonitoringListener implements
OnPreferenceChangeListener {
ToggleMonitoringListener() {}
#Override
public boolean
onPreferenceChange(Preference preference, Object newValue) {
if (newValue instanceof Boolean) {
final boolean enable = (Boolean) newValue;
Monitor.enableMonitoring(preference.getContext(), enable);
final CheckBoxPreference p = (CheckBoxPreference) preference;
preference.setSummary((enable) ? p.getSummaryOn() : p
.getSummaryOff());
return true;
}
return false;
}
}
/**
* This fragment is used when the activity is showing a two-pane
* settings UI.
*/
#TargetApi(Build.VERSION_CODES.HONEYCOMB)
public final static class DataSyncPreferenceFragment extends
PreferenceFragment {
#Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
Log.w(TAG, "onCreate");
addPreferencesFromResource(PREF_RESOURCE_SETTINGS);
master_pref = findPreference(master_enable.toString());
}
}
#Override
public void onSharedPreferenceChanged(SharedPreferences sharedPreferences,
String key) {
if (master_enable == null || master_pref == null) return;
if (master_enable.toString().equals(key)) {
refreshMasterPreference();
}
}
/**
* #param key
*/
private void refreshMasterPreference() {
final Boolean isMonitoringEnabled = AccessPreferences.get(this,
master_enable.toString(), false);
Log.w(TAG, "Stored value: " + isMonitoringEnabled);
final CheckBoxPreference p = (CheckBoxPreference) master_pref;
final boolean needsRefresh = p.isChecked() != isMonitoringEnabled;
if (needsRefresh) {
p.setChecked(isMonitoringEnabled);
p.setSummary((isMonitoringEnabled) ? p.getSummaryOn() : p
.getSummaryOff());
}
}
}
So the main idea is you provide an xml for preferences with headers:
public final void onBuildHeaders(List<Header> target) {
if (!isSimplePreferences(this)) {
loadHeadersFromResource(getHeadersXmlID(), target);
}
}
where:
#Override
int getHeadersXmlID() {
return PREF_HEADERS_XML;
}
and PREF_HEADERS_XML:
<preference-headers xmlns:android="http://schemas.android.com/apk/res/android" >
<!-- These settings headers are only used on tablets. -->
<header
android:fragment=".activities.SettingsActivity$DataSyncPreferenceFragment"
android:title="#string/pref_header_data_sync" />
</preference-headers>
and setting up the simple preferences in buildSimplePreferences()
I am interested into making this into a more general API - probably including the sBindPreferenceSummaryToValueListener - so ideas welcome.
Ah, yes, the sBindPreferenceSummaryToValueListener fluff:
// FLUFF AHEAD:
// the fluff that follows is for binding preference summary to value -
// essentially wrappers around OnPreferenceChangeListener - just so
// you get an idea of the mess this autogenerated piece of, code, was
// formatter:off
/**
* A preference value change listener that updates the preference's summary
* to reflect its new value.
*/
/* private static Preference.OnPreferenceChangeListener
sBindPreferenceSummaryToValueListener =
new Preference.OnPreferenceChangeListener() {
#Override
public boolean onPreferenceChange(Preference preference,
Object value) {
String stringValue = value.toString();
if (preference instanceof ListPreference) {
// For list preferences, look up the correct display value
// in the preference's 'entries' list.
ListPreference listPreference = (ListPreference) preference;
int index = listPreference.findIndexOfValue(stringValue);
// Set the summary to reflect the new value.
preference.setSummary(index >= 0
? listPreference.getEntries()[index] : null);
} else if (preference instanceof RingtonePreference) {
// For ringtone preferences, look up the correct display
// value using RingtoneManager.
if (TextUtils.isEmpty(stringValue)) {
// Empty values correspond to 'silent' (no ringtone).
// preference.setSummary(R.string.pref_ringtone_silent);
} else {
Ringtone ringtone = RingtoneManager.getRingtone(
preference.getContext(), Uri.parse(stringValue));
if (ringtone == null) {
// Clear the summary if there was a lookup error.
preference.setSummary(null);
} else {
// Set the summary to reflect the new ringtone
// display name.
String name = ringtone
.getTitle(preference.getContext());
preference.setSummary(name);
}
}
} else if (preference instanceof CheckBoxPreference) {
boolean b = (Boolean) value;
Log.w(TAG, "::::value " + b);
final CheckBoxPreference p =(CheckBoxPreference)preference;
preference.setSummary((b) ? p.getSummaryOn() : p
.getSummaryOff());
Log.w(TAG, p.getKey() + " :: " + p.isChecked());
} else {
// For all other preferences, set the summary to the value's
// simple string representation.
preference.setSummary(stringValue);
}
return true;
}
}; */
/**
* Binds a preference's summary to its value. More specifically, when the
* preference's value is changed, its summary (line of text below the
* preference title) is updated to reflect the value. The summary is also
* immediately updated upon calling this method. The exact display format is
* dependent on the type of preference.
*
* #see #sBindPreferenceSummaryToValueListener
*/
/* private static void bindPreferenceSummaryToValue(Preference preference) {
// Set the listener to watch for value changes.
preference
.setOnPreferenceChangeListener(sBindPreferenceSummaryToValueListener);
// Trigger the listener immediately with the preference's
// current value.
sBindPreferenceSummaryToValueListener.onPreferenceChange(
preference,
PreferenceManager.getDefaultSharedPreferences(
preference.getContext()).getString(preference.getKey(), ""));
} */
I have an activity using an xml layout where a WebView is embedded. I am not using the WebView in my activity code at all, all it does is sitting there in my xml layout and being visible.
Now, when I finish the activity, I find that my activity is not being cleared from memory. (I check via hprof dump). The activity is entirely cleared though if I remove the WebView from the xml layout.
I already tried a
webView.destroy();
webView = null;
in onDestroy() of my activity, but that doesn't help much.
In my hprof dump, my activity (named 'Browser') has the following remaining GC roots (after having called destroy() on it):
com.myapp.android.activity.browser.Browser
- mContext of android.webkit.JWebCoreJavaBridge
- sJavaBridge of android.webkit.BrowserFrame [Class]
- mContext of android.webkit.PluginManager
- mInstance of android.webkit.PluginManager [Class]
I found that another developer has experienced similar thing, see the reply of Filipe Abrantes on:
http://www.curious-creature.org/2008/12/18/avoid-memory-leaks-on-android/
Indeed a very interesting post.
Recently I had a very hard time
troubleshooting a memory leak on my
Android app. In the end it turned out
that my xml layout included a WebView
component that, even if not used, was
preventing the memory from being
g-collected after screen rotations/app
restart… is this a bug of the current
implementation, or is there something
specific that one needs to do when
using WebViews
Now, unfortunately there has been no reply on the blog or the mailing list about this question yet. Therefore I am wondering, is that a bug in the SDK (maybe similar to the MapView bug as reported http://code.google.com/p/android/issues/detail?id=2181) or how to get the activity entirely off the memory with a webview embedded?
I conclude from above comments and further tests, that the problem is a bug in the SDK: when creating a WebView via XML layout, the activity is passed as the context for the WebView, not the application context. When finishing the activity, the WebView still keeps references to the activity, therefore the activity doesn't get removed from the memory.
I filed a bug report for that , see the link in the comment above.
webView = new WebView(getApplicationContext());
Note that this workaround only works for certain use cases, i.e. if you just need to display html in a webview, without any href-links nor links to dialogs, etc. See the comments below.
I have had some luck with this method:
Put a FrameLayout in your xml as a container, lets call it web_container. Then programmatically ad the WebView as mentioned above. onDestroy, remove it from the FrameLayout.
Say this is somewhere in your xml layout file e.g. layout/your_layout.xml
<FrameLayout
android:id="#+id/web_container"
android:layout_width="fill_parent"
android:layout_height="wrap_content"/>
Then after you inflate the view, add the WebView instantiated with the application context to your FrameLayout. onDestroy, call the webview's destroy method and remove it from the view hierarchy or you will leak.
public class TestActivity extends Activity {
private FrameLayout mWebContainer;
private WebView mWebView;
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.your_layout);
mWebContainer = (FrameLayout) findViewById(R.id.web_container);
mWebView = new WebView(getApplicationContext());
mWebContainer.addView(mWebView);
}
#Override
protected void onDestroy() {
super.onDestroy();
mWebContainer.removeAllViews();
mWebView.destroy();
}
}
Also FrameLayout as well as the layout_width and layout_height were arbitrarily copied from an existing project where it works. I assume another ViewGroup would work and I am certain other layout dimensions will work.
This solution also works with RelativeLayout in place of FrameLayout.
Here's a subclass of WebView that uses the above hack to seamlessly avoid memory leaks:
package com.mycompany.view;
import android.app.Activity;
import android.content.Context;
import android.content.Intent;
import android.net.Uri;
import android.util.AttributeSet;
import android.webkit.WebView;
import android.webkit.WebViewClient;
/**
* see http://stackoverflow.com/questions/3130654/memory-leak-in-webview and http://code.google.com/p/android/issues/detail?id=9375
* Note that the bug does NOT appear to be fixed in android 2.2 as romain claims
*
* Also, you must call {#link #destroy()} from your activity's onDestroy method.
*/
public class NonLeakingWebView extends WebView {
private static Field sConfigCallback;
static {
try {
sConfigCallback = Class.forName("android.webkit.BrowserFrame").getDeclaredField("sConfigCallback");
sConfigCallback.setAccessible(true);
} catch (Exception e) {
// ignored
}
}
public NonLeakingWebView(Context context) {
super(context.getApplicationContext());
setWebViewClient( new MyWebViewClient((Activity)context) );
}
public NonLeakingWebView(Context context, AttributeSet attrs) {
super(context.getApplicationContext(), attrs);
setWebViewClient(new MyWebViewClient((Activity)context));
}
public NonLeakingWebView(Context context, AttributeSet attrs, int defStyle) {
super(context.getApplicationContext(), attrs, defStyle);
setWebViewClient(new MyWebViewClient((Activity)context));
}
#Override
public void destroy() {
super.destroy();
try {
if( sConfigCallback!=null )
sConfigCallback.set(null, null);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
protected static class MyWebViewClient extends WebViewClient {
protected WeakReference<Activity> activityRef;
public MyWebViewClient( Activity activity ) {
this.activityRef = new WeakReference<Activity>(activity);
}
#Override
public boolean shouldOverrideUrlLoading(WebView view, String url) {
try {
final Activity activity = activityRef.get();
if( activity!=null )
activity.startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse(url)));
}catch( RuntimeException ignored ) {
// ignore any url parsing exceptions
}
return true;
}
}
}
To use it, just replace WebView with NonLeakingWebView in your layouts
<com.mycompany.view.NonLeakingWebView
android:layout_width="fill_parent"
android:layout_height="wrap_content"
...
/>
Then make sure to call NonLeakingWebView.destroy() from your activity's onDestroy method.
Note that this webclient should handle the common cases, but it may not be as full-featured as a regular webclient. I haven't tested it for things like flash, for example.
Based on user1668939's answer on this post (https://stackoverflow.com/a/12408703/1369016), this is how I fixed my WebView leak inside a fragment:
#Override
public void onDetach(){
super.onDetach();
webView.removeAllViews();
webView.destroy();
}
The difference from user1668939's answer is that I have not used any placeholders. Just calling removeAllViews() on the WebvView reference itself did the trick.
## UPDATE ##
If you are like me and have WebViews inside several fragments (and you do not want to repeat the above code across all of your fragments), you can use reflection to solve it. Just make your Fragments extend this one:
public class FragmentWebViewLeakFree extends Fragment{
#Override
public void onDetach(){
super.onDetach();
try {
Field fieldWebView = this.getClass().getDeclaredField("webView");
fieldWebView.setAccessible(true);
WebView webView = (WebView) fieldWebView.get(this);
webView.removeAllViews();
webView.destroy();
}catch (NoSuchFieldException e) {
e.printStackTrace();
}catch (IllegalArgumentException e) {
e.printStackTrace();
}catch (IllegalAccessException e) {
e.printStackTrace();
}catch(Exception e){
e.printStackTrace();
}
}
}
I am assuming you are calling your WebView field "webView" (and yes, your WebView reference must be a field unfortunately). I have not found another way to do it that would be independent from the name of the field (unless I loop through all the fields and check if each one is from a WebView class, which I do not want to do for performance issues).
After reading http://code.google.com/p/android/issues/detail?id=9375, maybe we could use reflection to set ConfigCallback.mWindowManager to null on Activity.onDestroy and restore it on Activity.onCreate. I'm unsure though if it requires some permissions or violates any policy. This is dependent on android.webkit implementation and it may fail on later versions of Android.
public void setConfigCallback(WindowManager windowManager) {
try {
Field field = WebView.class.getDeclaredField("mWebViewCore");
field = field.getType().getDeclaredField("mBrowserFrame");
field = field.getType().getDeclaredField("sConfigCallback");
field.setAccessible(true);
Object configCallback = field.get(null);
if (null == configCallback) {
return;
}
field = field.getType().getDeclaredField("mWindowManager");
field.setAccessible(true);
field.set(configCallback, windowManager);
} catch(Exception e) {
}
}
Calling the above method in Activity
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setConfigCallback((WindowManager)getApplicationContext().getSystemService(Context.WINDOW_SERVICE));
}
public void onDestroy() {
setConfigCallback(null);
super.onDestroy();
}
I fixed memory leak issue of frustrating Webview like this:
(I hope this may help many)
Basics:
To create a webview, a reference (say an activity) is needed.
To kill a process:
android.os.Process.killProcess(android.os.Process.myPid()); can be called.
Turning point:
By default, all activities run in same process in one application. (the process is defined by package name). But:
Different processes can be created within same application.
Solution:
If a different process is created for an activity, its context can be used to create a webview. And when this process is killed, all components having references to this activity (webview in this case) are killed and the main desirable part is :
GC is called forcefully to collect this garbage (webview).
Code for help: (one simple case)
Total two activities: say A & B
Manifest file:
<application
android:allowBackup="true"
android:icon="#drawable/ic_launcher"
android:label="#string/app_name"
android:process="com.processkill.p1" // can be given any name
android:theme="#style/AppTheme" >
<activity
android:name="com.processkill.A"
android:process="com.processkill.p2"
android:label="#string/app_name" >
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<activity
android:name="com.processkill.B"
android:process="com.processkill.p3"
android:label="#string/app_name" >
</activity>
</application>
Start A then B
A > B
B is created with webview embedded.
When backKey is pressed on activity B, onDestroy is called:
#Override
public void onDestroy() {
android.os.Process.killProcess(android.os.Process.myPid());
super.onDestroy();
}
and this kills the current process i.e. com.processkill.p3
and takes away the webview referenced to it
NOTE: Take extra care while using this kill command. (not recommended due to obvious reasons). Don't implement any static method in the activity (activity B in this case). Don't use any reference to this activity from any other (as it will be killed and no longer available).
You need to remove the WebView from the parent view before calling WebView.destroy().
WebView's destroy() comment - "This method should be called after this WebView has been removed from the view system."
You can try putting the web activity in a seperate process and exit when the activity is destroyed, if multiprocess handling is not a big effort to you.
There is an issue with "app context" workaround: crash when WebView tries to show any dialog. For example "remember the password" dialog on login/pass forms submition (any other cases?).
It could be fixed with WebView settings' setSavePassword(false) for the "remember the password" case.