I've written an iOS app that I want to port to android. This app is a front-end for a web-based timekeeping system at work. Unfortunately, the system doesn't have an API, so I'm screen-scraping it using xpath queries in javascript. In iOS, I only have to load this page once because I get full control over when instances of my UIWebView get destroyed.
The iOS app only has 3 use cases, each which break into separate Android
activities:
Login
View a list of all reported times
Report a new time.
Using a naive approach, my android views (including the WebView I need to use to interact with the timekeeping system) will be destroyed and recreated when I switch between views. If I have to reload the WebView every time I switch activities, my app will appear very slow.
Is it possible to share a WebView instance between multiple
activities? I know I can set the launchMode to singleInstance, but ideally, it would be nice to allow separate instances so that the back button would function normally.
Per a suggestion on from Eric Burke at STL Mobile Dev:
See if your Android Application can create the WebView. You won't actually display the WebView anywhere, it's just created for interaction with the web site. The WebView instance's lifecycle is then managed by your Application rather than an Activity.
I was able to share my WebView between my Activity instances. I specified a custom Application in my manifest, and created the WebView inside the Application. The Application is a Context and it lives as long as the app does, so all Activity objects can share the WebView that way.
The way I solved this problem is little different. It pivots around couple of things.
WebViews can be created with application context.
There is something called MutableContextWrapper, we can flip the underlying context inside it.
So, we create the web view in the pool with mutable context wrapper with appContext and when an activity context asks for it, we flip the context and return it. We flip the context to appContext when we return the webView back.
Something like this
// Has a single web view, and crashes if a request to obtain comes in and the
// cached instance is already allocated.
public class SingularWebViewPool implements WebViewPool {
private WebView mCachedInstance;
private volatile int mBorrower;
private final int mSignature;
private final Context mAppCtx;
public SingularWebViewPool(Context appCtx) {
this.mAppCtx = appCtx;
this.mCachedInstance = new WebView(appCtx);
// We will match this every time, we flip to app context to ensure a different web view is
// not handed over to us.
this.mSignature = mCachedInstance.hashCode();
}
#Override
#NonNull
public WebView obtain(#NonNull Context activity) {
if (mCachedInstance != null) {
Context ctx = mCachedInstance.getContext();
if (ctx instanceof MutableContextWrapper) {
((MutableContextWrapper) ctx).setBaseContext(activity);
} else
// We should not reach here!
throw new IllegalStateException("Cached web view stored without a mutable context wrapper.");
WebView temp = mCachedInstance;
mCachedInstance = null;
this.mBorrower = activity.hashCode();
return temp;
} else
throw new IllegalStateException("Pool not having a cached web view instance when obtain() was called.");
}
#Override
public boolean release(#NonNull WebView webView, #NonNull Context borrower) {
// Validate the last borrower.
if (borrower.hashCode() != this.mBorrower) {
return false;
}
Context ctx = webView.getContext();
if (ctx instanceof MutableContextWrapper) {
((MutableContextWrapper) ctx).setBaseContext(mAppCtx);
} else
throw new IllegalStateException("Cached web view stored without a mutable context wrapper.");
// match the signature.
if (mSignature != mCachedInstance.hashCode()) {
throw new IllegalStateException("A different web view is released other than what we have given out.");
}
mCachedInstance = webView;
mBorrower = 0;
return true;
}
}
Related
I have a WebView in the layout xml of my MainActivity, to which I setWebViewClient(new WebViewClient()), followed by loadUrl(...) in onCreate.
Most of the time the app runs fine and the Web content is displayed correctly.
But in some cases, opening the app causes a crash. I've noticed that it happens when the app scheduled a PendingIntent broadcast with AlarmManager, which triggers a Notification whose contentIntent is a PendingIntent.getActivity set to launch MainActivity.
But it happens only in the case when the user has removed the app from the stack of active apps in the meantime (Notification is visible, not yet clicked, and stack if apps cleared. So, app process probably stopped?).
Seemingly no other system modifications in between (in particular no app/system update, no playing around with user profiles or Chrome app.)
Stack trace:
java.lang.RuntimeException:
at android.webkit.WebViewDelegate.getPackageId (WebViewDelegate.java:164)
at yj.a (PG:16)
at xH.run (PG:14)
at java.lang.Thread.run (Thread.java:764)
Occurs with Android 7.0 thru 9. Also, seems to have started to occur when I upgraded target SDK to 28.
I don't use explicitly a WebViewDelegate. It must be internal system code (hence the obfuscation).
By reading the source code of AOSP, it seems that the WebView fails to retrieve the package to which it belongs -- but why sometimes only!?
Any help appreciated! Thanks.
It has taken weeks of investigation on and off, but I've finally found why I'm seeing this issue. For me, it was just because I'd overridden the getResources() method in my application scope to use the current activity. Something like this:
public class MyApplication extends MultiDexApplication {
private static MyApplication sInstance = null;
private WeakReference<Activity> mCurrentActivity;
public static MyApplication getInstance() {
return sInstance;
}
public void setCurrentActivity(Activity activity) {
mCurrentActivity = new WeakReference<>(activity);
}
public Activity getCurrentActivity() {
return mCurrentActivity == null ? null : mCurrentActivity.get();
}
#Override
public Resources getResources() {
// This is a very BAD thing to do
Activity activity = getCurrentActivity();
if (activity != null) {
return activity.getResources();
}
return super.getResources();
}
}
This was done as a shortcut as I often wanted to get strings that were activity-specific, so I was calling MyApplication.getInstance().getResources().getString(). I now know this was a bad thing to do - removing my override of this method instantly fixed it.
So the key takeaway from this for me is that when the WebView is initialising, it MUST be able to get hold of the application context, so that the resources passed into WebViewDelegate.getPackageId() are at the application level - the activity context isn't enough, and causes this error.
As a side note - I wasn't even trying to add a WebView to my application. I was only actually using the following:
String userAgent = WebSettings.getDefaultUserAgent(this);
I was then passing this value into a custom media player that I'm using. Passing "this" as either application or activity scope always failed, due to my override.
Looking through documentation,you can see that error is thrown when package can't be found.Check your syntax ,package name and try again.
https://android.googlesource.com/platform/frameworks/base/+/master/core/java/android/webkit/WebViewDelegate.java (Line 164)
/**
* Returns the package id of the given {#code packageName}.
*/
public int getPackageId(Resources resources, String packageName) {
SparseArray<String> packageIdentifiers =
resources.getAssets().getAssignedPackageIdentifiers();
for (int i = 0; i < packageIdentifiers.size(); i++) {
final String name = packageIdentifiers.valueAt(i);
if (packageName.equals(name)) {
return packageIdentifiers.keyAt(i);
}
}
throw new RuntimeException("Package not found: " + packageName);
}
I am trying to build a Flutter app that does the following:
1) Run an alarm manager every minute (even when app is in background/closed).
2) When the alarm manager's onReceive method is called, get the users location.
3) Store this location in a SQL/SQF database.
Basically, I have all the code working. However, I'd prefer to do step 2 and 3 with two different plugins, as to create more modularity. But to do so, I need to have an instance of a FlutterView, such that I can do a "MethodChannel(flutterView, CHANNEL).invokeMethod(methods, args);" from Android.
(In android/Java, onReceive method) I have tried to get the FlutterView from the context, like so:
private FlutterView viewFromAppContext(Context context) {
Application app = (Application) context.getApplicationContext();
if (!(app instanceof FlutterApplication)) {
Log.i(TAG, "viewFromAppContext app not a FlutterApplication");
return null;
}
FlutterApplication flutterApp = (FlutterApplication) app;
Activity activity = flutterApp.getCurrentActivity();
if (activity == null) {
Log.i(TAG, "viewFromAppContext activity is null");
return null;
}
if (!(activity instanceof FlutterActivity)) {
Log.i(TAG, "viewFromAppContext activity is not a FlutterActivity");
return null;
}
FlutterActivity flutterActivity = (FlutterActivity) activity;
return flutterActivity.getFlutterView();
}
However, when I try to do this when the app is in the background, the activity is null.
Is it possible to create a new activity and/or flutterview in this scenario (which can direct to my 'setMethodCallHandler' method in dart)?
I think what you are looking for is a FlutterNativeView called with the second param set to true; it is not easy to follow, slightly divergent from the linked git repo and is engineered well in excess of your needs, but Ben Konyi has a working example of this. I think you might enjoy checking out some of the ways he kept the other plugins working and the service working, if not the methodchannel impl (with three callback domains).
I am trying to use a Singleton to share a large data object between Activities. But when I open the new Activity, the singleton comes up as empty. It seems to me that the Singleton should be the same no matter where in the Application I call if from.
It seems like the Scope of the Singleton is being limited to the individual Activity. Working around this is making my App very complicated. I must be doing something wrong. I even tried instantiating them in an extended Application class... Google says I should not have to use that though...
Can someone please point out where I am going wrong? i.e. Why does this singletom not contain the same data in each Activity?
I call it from an Activity with...
DataLog dataLog = DataLog.getInstance(this);
I have...
public class DataLog extends ArrayList<String> implements Serializable {
private static final long serialVersionUID = 0L;
private static DataLog sInstance;
private static Context mContext;
public static DataLog getInstance(Context context) {
mContext = context.getApplicationContext();
prefs = PreferenceManager.getDefaultSharedPreferences(mContext);
if (sInstance == null) {
sInstance = new DataLog();
}
return sInstance;
}
private DataLog() {
}
public boolean add(String entry) {
super.add(entry);
return true;
}
public void add(int index, String entry) {
if (index > 0)
super.add(index, entry);
else
super.add(entry);
}
public void clear() {
super.clear();
}
...
}
Its highly advisable to avoid singleton for sharing large data sets in android.
Singletons are used for short life-cycle objects.
Switch to SharedPrefferences, SQLite DB's or file storing. You are not the only to have experienced this behavior, and the reason lies in the nature of android Activities and the system itself(managing activities and its data).
Here is an example why singleton is bad for your case:
You stored important data in it. The user knows that he can close the app on home button to call someone or whatever)maybe someone called him when he was in your app), and that when he opens your app he will come back at the same place with everything in order. (this is expected behavior from users and android apps). The system can easily kill your process and all static variables in it for memory maintenance, app inactivity etc...result=data lost. Thus its not safe to use it.
I have an Activity which has 3 tabs which are Fragments.
Featured
Up Coming
Favorites
Right now my implement when i click a particular tab it downloads the content from the internet and displays it.
When i was browsing through the Google Play App. I found that when i went into the App section All the content across the tabs Featured - Top Free - Top Paid etc was already there and only the images was lazy loading.
I am trying figure out how this can be implemented.
In my app i have a Activity wich has 4 Tabs wich are Fragments..
I solved your explained Problem by Using a Singleton. I load all relevant information from sqlite database and pass it into an ArrayList in the Singletion..
So i can access the content from every Fragment...
public class Singleton {
private static Singleton instance = null;
public ArrayList<MyObject> myObjectList;
protected Singleton() {
// Exists only to defeat instantiation.
}
public static Singleton getInstance() {
if(instance == null) {
instance = new Singleton();
myObjectList = new ArrayList<MyObject>();
}
return instance;
}
}
This is a feature of ViewPager
With the release of Gingerbread, I have been experimenting with some of the new API's, one of them being StrictMode.
I noticed that one of the warnings is for getSharedPreferences().
This is the warning:
StrictMode policy violation; ~duration=1949 ms: android.os.StrictMode$StrictModeDiskReadViolation: policy=23 violation=2
and it's being given for a getSharedPreferences() call being made on the UI thread.
Should SharedPreferences access and changes really be made off the UI thread?
I'm glad you're already playing with it!
Some things to note: (in lazy bullet form)
if this is the worst of your problems, your app's probably in a good spot. :) Writes are generally slower than reads, though, so be sure you're using SharedPreferenced$Editor.apply() instead of commit(). apply() is new in GB and async (but always safe, careful of lifecycle transitions). You can use reflection to conditionally call apply() on GB+ and commit() on Froyo or below. I'll be doing a blogpost with sample code of how to do this.
Regarding loading, though...
once loaded, SharedPreferences are singletons and cached process-wide. so you want to get it loaded as early as possible so you have it in memory before you need it. (assuming it's small, as it should be if you're using SharedPreferences, a simple XML file...) You don't want to fault it in the future time some user clicks a button.
but whenever you call context.getSharedPreferences(...), the backing XML file is stat'd to see if it's changed, so you'll want to avoid those stats during UI events anyway. A stat should normally be fast (and often cached), but yaffs doesn't have much in the way of concurrency (and a lot of Android devices run on yaffs... Droid, Nexus One, etc.) so if you avoid disk, you avoid getting stuck behind other in-flight or pending disk operations.
so you'll probably want to load the SharedPreferences during your onCreate() and re-use the same instance, avoiding the stat.
but if you don't need your preferences anyway during onCreate(), that loading time is stalling your app's start-up unnecessarily, so it's generally better to have something like a FutureTask<SharedPreferences> subclass that kicks off a new thread to .set() the FutureTask subclasses's value. Then just lookup your FutureTask<SharedPreferences>'s member whenever you need it and .get() it. I plan to make this free behind the scenes in Honeycomb, transparently. I'll try to release some sample code which
shows best practices in this area.
Check the Android Developers blog for upcoming posts on StrictMode-related subjects in the coming week(s).
Accessing the shared preferences can take quite some time because they are read from flash storage. Do you read a lot? Maybe you could use a different format then, e.g. a SQLite database.
But don't fix everything you find using StrictMode. Or to quote the documentation:
But don't feel compelled to fix everything that StrictMode finds. In particular, many cases of disk access are often necessary during the normal activity lifecycle. Use StrictMode to find things you did by accident. Network requests on the UI thread are almost always a problem, though.
One subtlety about Brad's answer: even if you load the SharedPreferences in onCreate(), you should probably still read values on the background thread because getString() etc. block until reading the shared file preference in finishes (on a background thread):
public String getString(String key, String defValue) {
synchronized (this) {
awaitLoadedLocked();
String v = (String)mMap.get(key);
return v != null ? v : defValue;
}
}
edit() also blocks in the same way, although apply() appears to be safe on the foreground thread.
(BTW sorry to put this down here. I would have put this as a comment to Brad's answer, but I just joined and don't have enough reputation to do so.)
I know this is an old question but I want to share my approach. I had long reading times and used a combination of shared preferences and the global application class:
ApplicationClass:
public class ApplicationClass extends Application {
private LocalPreference.Filter filter;
public LocalPreference.Filter getFilter() {
return filter;
}
public void setFilter(LocalPreference.Filter filter) {
this.filter = filter;
}
}
LocalPreference:
public class LocalPreference {
public static void saveLocalPreferences(Activity activity, int maxDistance, int minAge,
int maxAge, boolean showMale, boolean showFemale) {
Filter filter = new Filter();
filter.setMaxDistance(maxDistance);
filter.setMinAge(minAge);
filter.setMaxAge(maxAge);
filter.setShowMale(showMale);
filter.setShowFemale(showFemale);
BabysitApplication babysitApplication = (BabysitApplication) activity.getApplication();
babysitApplication.setFilter(filter);
SecurePreferences securePreferences = new SecurePreferences(activity.getApplicationContext());
securePreferences.edit().putInt(Preference.FILER_MAX_DISTANCE.toString(), maxDistance).apply();
securePreferences.edit().putInt(Preference.FILER_MIN_AGE.toString(), minAge).apply();
securePreferences.edit().putInt(Preference.FILER_MAX_AGE.toString(), maxAge).apply();
securePreferences.edit().putBoolean(Preference.FILER_SHOW_MALE.toString(), showMale).apply();
securePreferences.edit().putBoolean(Preference.FILER_SHOW_FEMALE.toString(), showFemale).apply();
}
public static Filter getLocalPreferences(Activity activity) {
BabysitApplication babysitApplication = (BabysitApplication) activity.getApplication();
Filter applicationFilter = babysitApplication.getFilter();
if (applicationFilter != null) {
return applicationFilter;
} else {
Filter filter = new Filter();
SecurePreferences securePreferences = new SecurePreferences(activity.getApplicationContext());
filter.setMaxDistance(securePreferences.getInt(Preference.FILER_MAX_DISTANCE.toString(), 20));
filter.setMinAge(securePreferences.getInt(Preference.FILER_MIN_AGE.toString(), 15));
filter.setMaxAge(securePreferences.getInt(Preference.FILER_MAX_AGE.toString(), 50));
filter.setShowMale(securePreferences.getBoolean(Preference.FILER_SHOW_MALE.toString(), true));
filter.setShowFemale(securePreferences.getBoolean(Preference.FILER_SHOW_FEMALE.toString(), true));
babysitApplication.setFilter(filter);
return filter;
}
}
public static class Filter {
private int maxDistance;
private int minAge;
private int maxAge;
private boolean showMale;
private boolean showFemale;
public int getMaxDistance() {
return maxDistance;
}
public void setMaxDistance(int maxDistance) {
this.maxDistance = maxDistance;
}
public int getMinAge() {
return minAge;
}
public void setMinAge(int minAge) {
this.minAge = minAge;
}
public int getMaxAge() {
return maxAge;
}
public void setMaxAge(int maxAge) {
this.maxAge = maxAge;
}
public boolean isShowMale() {
return showMale;
}
public void setShowMale(boolean showMale) {
this.showMale = showMale;
}
public boolean isShowFemale() {
return showFemale;
}
public void setShowFemale(boolean showFemale) {
this.showFemale = showFemale;
}
}
}
MainActivity (activity that get called first in your application):
LocalPreference.getLocalPreferences(this);
Steps explained:
The main activity calls getLocalPreferences(this) -> this will read your preferences, set the filter object in your application class and returns it.
When you call the getLocalPreferences() function again somewhere else in the application it first checks if it's not available in the application class which is a lot faster.
NOTE: ALWAYS check if an application wide variable is different from NULL, reason -> http://www.developerphil.com/dont-store-data-in-the-application-object/
The application object will not stay in memory forever, it will get killed. Contrary to popular belief, the app won’t be restarted from scratch. Android will create a new Application object and start the activity where the user was before to give the illusion that the application was never killed in the first place.
If I didn't check on null I would allow a nullpointer to be thrown when calling for example getMaxDistance() on the filter object (if the application object was swiped from the memory by Android)
SharedPreferences class does some reads & writes within XML files on disk, so just like any other IO operation it could be blocking. The amount of data currently stored in SharedPreferences affects the time and resource consumed by the API calls. For minimal amounts of data it's a matter of a few milliseconds (sometimes even less than a millisecond) to get/put data. But from the point of view of an expert it could be important to improve the performance by doing the API calls in background. For an asynchronous SharedPreferences I suggest checking out the Datum library.
i do not see any reason to read them from a background thread. but to write it i would. at startup time the shared preference file is loaded into memory so its fast to access, but to write things can take a bit of time so we can use apply the write async. that should be the difference between commit and apply methods of shared prefs.