ActionBarCompat: Hide ActionBar before activity is created (bug?) - android

So I was using the ActionBarSherlock and decided to switch to the new ActionBarCompat. With ABS, hiding the ActionBar was possible using the way described in this post:
How to hide action bar before activity is created, and then show it again?
But, with the ActionBarCompat the app crashes on API14, because when you set android:windowActionBar as false the getSupportActionBar() method returns null, even if you have declared the getWindow().requestFeature(Window.FEATURE_ACTION_BAR); into the onCreate() method.
Funny thing is that if you call getActionBar() instead, you get the object and everything works fine.
So, is that a bug or am I missing something? Any ideas are welcome!
styles.xml file:
<style name="Theme.MyApp" parent="#style/Theme.AppCompat.Light.DarkActionBar">
<item name="android:windowActionBar">false</item>
<item name="android:windowTitleSize">0dp</item>
</style>
MyActivity.java file:
...
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// Get the action bar feature. This feature is disabled by default into the theme
// for specific reasons.
getWindow().requestFeature(Window.FEATURE_ACTION_BAR);
...
// By default the action bar is hidden.
getSupportActionBar().hide();
}

I got stuck with the same problem and, it seems to me, found a reason of this strange behavior. I looked through source of support library and got this:
Appcompat checks a value of mHasActionBar variable before creating new action bar in ActionBarActivityDelegate
final ActionBar getSupportActionBar() {
// The Action Bar should be lazily created as mHasActionBar or mOverlayActionBar
// could change after onCreate
if (mHasActionBar || mOverlayActionBar) {
if (mActionBar == null) {
mActionBar = createSupportActionBar();
...
And we can change its value by calling supportRequestWindowFeature(int featureId) which is delegated by ActionBarActivity to a ActionBarActivityDelegate.
There are base delegate class ActionBarDelegateBase and its descendants ActionBarDelegateHC, ActionBarActivityDelegateICS, ActionBarActivityJB, one of which is chosen according to a version of running android. And method supportRequestWindowFeature is actually works fine almost in all of them, but it's overridden in ActionBarActivityDelegateICS like that
#Override
public boolean supportRequestWindowFeature(int featureId) {
return mActivity.requestWindowFeature(featureId);
}
So it has no effect on the variable mHasActionBar, that's why getSupportActionBar() returns null.
We almost there. I came to two different solutions.
First way
import source project of appcompat from git
change overridden method in ActionBarActivityDelegateICS.java to something like this
#Override
public boolean supportRequestWindowFeature(int featureId) {
boolean result = mActivity.requestWindowFeature(featureId);
if (result) {
switch (featureId) {
case WindowCompat.FEATURE_ACTION_BAR:
mHasActionBar = true;
case WindowCompat.FEATURE_ACTION_BAR_OVERLAY:
mOverlayActionBar = true;
}
}
return result;
}
place this line in activity's onCreate method before getSupportActionBar()
supportRequestWindowFeature(WindowCompat.FEATURE_ACTION_BAR);
Second way
import project of appcompat from android SDK (which is with empty src directory)
add this method to your activity
private void requestFeature() {
try {
Field fieldImpl = ActionBarActivity.class.getDeclaredField("mImpl");
fieldImpl.setAccessible(true);
Object impl = fieldImpl.get(this);
Class<?> cls = Class.forName("android.support.v7.app.ActionBarActivityDelegate");
Field fieldHasActionBar = cls.getDeclaredField("mHasActionBar");
fieldHasActionBar.setAccessible(true);
fieldHasActionBar.setBoolean(impl, true);
} catch (NoSuchFieldException e) {
Log.e(LOG_TAG, e.getLocalizedMessage(), e);
} catch (IllegalAccessException e) {
Log.e(LOG_TAG, e.getLocalizedMessage(), e);
} catch (IllegalArgumentException e) {
Log.e(LOG_TAG, e.getLocalizedMessage(), e);
} catch (ClassNotFoundException e) {
Log.e(LOG_TAG, e.getLocalizedMessage(), e);
}
}
call requestFeature() in onCreate method of your activity like this
if (Build.VERSION.SDK_INT >= 11) {
requestFeature();
}
supportRequestWindowFeature(WindowCompat.FEATURE_ACTION_BAR);
I used the second way. That's all.

I use this to hide ActionBar in AppCompat:
style.xml
<style name="Theme.AppCompat.Light.NoActionBar" parent="#style/Theme.AppCompat.Light">
<item name="android:windowNoTitle">true</item>
</style>
AndroidManifest.xml:
<activity
android:name=".Splash"
android:label="#string/title_activity_splash"
android:theme="#style/Theme.AppCompat.Light.NoActionBar">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>

I don`t know if I understood exactly your question, but here I go.
I think that you need to use both: getSupportActionBar() to old versions and getActionBar() to newest versions. It`s not a bug.
You need to verify the device version before use the methods.
I hope I was able to help.

Does your activity extends ActionBarActivity? Probably it doesn't and that's why it's running with the getActionbar() method.

Related

DialogFragment overwrite style when returning to foreground

I am stuck trying to overwrite the style in a DialogFragment when the fragment is shown after it was backgrounded at least once.
This is the relevant code:
My styles.xml:
<style name="Theme.MyDialog.Default">
<item name="android:windowBackground">#drawable/my_background</item>
</style>
<style name="Theme.MyDialog.NoAnimation"
parent="Theme.MyDialog.Default">
<item name="android:windowEnterAnimation">#null</item>
</style>
My Fragment relevant portion of the code:
private boolean wasPaused;
#Override
public void onStart() {
super.onStart();
if (wasPaused) {
// when this dialog re-enters foreground, do not animate it
super.setStyle(DialogFragment.STYLE_NO_FRAME, R.style.Theme_MyDialog_NoAnimation);
}
}
#Override
public void onStop() {
wasPaused = true;
super.onStop();
}
The style is never R.style.Theme_MyDialog_NoAnimation even after the dialog fragment comes back into foreground after being backgrounded. The debug statements show that the super.setStyle(...) does happen but my guess is it occurs after the view is already created. Can the style be overwritten after the app was paused?
UPDATE: see my comment which includes the answer below.
Answering my own question...
Based on javadoc comments at https://android.googlesource.com/platform/frameworks/base/+/refs/heads/master/core/java/android/app/DialogFragment.java
Calling setStyle(int, int) after the fragment's Dialog is created will have no effect.
Hopefully this will help someone else.

Android :: How to change theme from other apk programmatically

I have a apps named: preference, and i want to make themes for this apps. My AndroidManifest.xml of preference is:
<application
android:allowBackup="true"
android:icon="#drawable/ic_launcher"
android:label="#string/app_name"
android:theme="#style/AppTheme" >
<activity
android:name="com.example.preference.MainActivity"
android:label="#string/app_name" >
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
And in the MainActivity.java: I received theme change button click by:
themeChange = (Button) findViewById(R.id.themeChange);
themeChange.setOnClickListener(this);
I have a theme application which has PinkTheme (styles.xml) and the package name is com.example.theme.
<resources xmlns:android="http://schemas.android.com/apk/res/android">
<style name="AppBaseTheme" parent="android:Theme.Light">
<!--
Theme customizations available in newer API levels can go in
res/values-vXX/styles.xml, while customizations related to
backward-compatibility can go here.
-->
</style>
<style name="PinkTheme" parent="AppBaseTheme" >
<item name="android:textColor">#FF69B4</item>
<item name="android:typeface">monospace</item>
<item name="android:textSize">40sp</item>
<item name="android:windowBackground">#008000</item>
</style>
</resources>
And the onClick() of my preference is:
public void onClick(View v) {
// TODO Auto-generated method stub
switch(v.getId()){
case R.id.themeChange:
Resources res = null;
try {
res = getPackageManager().getResourcesForApplication("com.example.theme");
} catch (NameNotFoundException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
if(null != res) {
int sId = res.getIdentifier("com.example.theme:style/PinkTheme", null, null);
Theme themeObject = res.newTheme(); // theme object
themeObject.applyStyle(sId, true); // Place new attribute values into the theme
getTheme().setTo(themeObject);
}
break;
default:
break;
}
}
I have received theme Object of the theme package and try to setTheme of the preference application. But its not working. Please help me that: how to set theme from others application to my current application programmatically.
If you want to change settings in one application from another, then probably the easiest way to do it is to put a theme ID into an intent and send that intent to your MainActivity (or to an IntentService) where the receiving app can process the data. For example when the intent arrives, you can process it like you do a click event in your "onClick" logic.
For example, an app can create an intent like this:
Intent intent = new Intent("android.intent.action.MAIN");
intent.setComponent(new ComponentName("your.package", "your.package.component"));
intent.putExtra("theme_id", "theme_1");
startActivity(intent);
Then in your activity, use getIntent().getStringExtra("theme_id") to get the data passed to your app.
You would need to use the application's resources to load the theme. I have answered this in another thread here: https://stackoverflow.com/a/41948943/1048340
Here is an example where the package "com.example.theme" is installed and we use the app's resources and theme style in another app:
public class MainActivity extends AppCompatActivity {
Resources resources;
#Override protected void onCreate(Bundle savedInstanceState) {
int themeResId = getResources().getIdentifier("AppTheme", "style", "com.example.theme");
if (themeResId != 0) {
setTheme(themeResId);
}
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
}
#Override public Resources getResources() {
if (resources == null) {
try {
resources = getPackageManager().getResourcesForApplication("com.example.theme");
} catch (PackageManager.NameNotFoundException e) {
resources = super.getResources();
}
}
return resources;
}
}
See here for a working project on GitHub.
This can lead to problems because all layouts, drawables, strings, etc. will be loaded from the other application's resources. Therefore, you should avoid using a theme from another package and instead copy the resources and theme to your own project.

How to make globally background of the Action Bar

I'm a noob in android development and I need to set the background color of the ActionBar globally. The whole application will be the same ActionBar background. Can I make this by using AndroidManifest.xml, if yes how? Thanks!
To make global variables...
Make a new class and add your variables (Add whatever value you need in this class)
package com.srr.yourpackage;
public class GlobalVariables extends Application{
private int someVar;
public int getSomeVar(){
return someVar;
}
public void setSomeVar(int a){
someVar = a;
}
}
Add the reference of this file to your Manifest inside the application tag.
<application
android:allowBackup="true"
android:debuggable="true"
android:icon="#drawable/icon"
android:label="#string/app_name"
android:name =".GlobalVariables">
<activity.....
</application>
Use it inside any of your classes.
public class yourNewClass extends Activity
{
GlobalVariables globalVar;
protected void onCreate(Bundle savedInstanceState) {
// TODO Auto-generated method stub
super.onCreate(savedInstanceState);
setContentView(R.layout.yourLayout);
globalVar = (GlobalVariables) getApplication();
int yourInt = globalVar.getSomeVar(); //Call upon anything you need in the activity of your choice.
}
}
Or if you only need just Action Bar, use this topic
I'd rather prefer to make a theme and customize each activity, or the application in the manifest file. When I develop apps, I use this tool to generate the UI:
Android Action Bar Style Generator

PreferenceActivity Android 4.0 and earlier

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'

Memory leak in WebView

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.

Categories

Resources