I've come across an unexpected error caused by calling URL.setURLStreamHandlerFactory(factory);
in an Android application that is being updated.
public class ApplicationRoot extends Application {
static {
/* Add application support for custom URI protocols. */
final URLStreamHandlerFactory factory = new URLStreamHandlerFactory() {
@Override
public URLStreamHandler createURLStreamHandler(final String protocol) {
if (ExternalProtocol.PROTOCOL.equals(protocol)) {
return new ExternalProtocol();
}
if (ArchiveProtocol.PROTOCOL.equals(protocol)) {
return new ArchiveProtocol();
}
return null;
}
};
URL.setURLStreamHandlerFactory(factory);
}
}
Intro:
Here is my situation: I'm maintaining a non-market application used in an enterprise fashion. My business sells tablets with pre-installed applications that are developed and maintained by the business. These pre-installed applications are not part of the ROM; they are installed as typical Unknown Source applications. We do not perform updates through the Play Store or any other market. Rather, application updates are controlled by a custom Update Manager application, which communicates directly with our servers to perform OTA updates.
Problem:
This Update Manager application, which I am maintaining, occasionally needs to update itself. Immediately after the application updates itself, it restarts by way of the android.intent.action.PACKAGE_REPLACED
broadcast, which I register for in the AndroidManifest. However, upon restart of the application immediately after the update, I occasionally receive this Error
java.lang.Error: Factory already set
at java.net.URL.setURLStreamHandlerFactory(URL.java:112)
at com.xxx.xxx.ApplicationRoot.<clinit>(ApplicationRoot.java:37)
at java.lang.Class.newInstanceImpl(Native Method)
at java.lang.Class.newInstance(Class.java:1208)
at android.app.Instrumentation.newApplication(Instrumentation.java:996)
at android.app.Instrumentation.newApplication(Instrumentation.java:981)
at android.app.LoadedApk.makeApplication(LoadedApk.java:511)
at android.app.ActivityThread.handleReceiver(ActivityThread.java:2625)
at android.app.ActivityThread.access$1800(ActivityThread.java:172)
at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1384)
at android.os.Handler.dispatchMessage(Handler.java:102)
at android.os.Looper.loop(Looper.java:146)
at android.app.ActivityThread.main(ActivityThread.java:5653)
at java.lang.reflect.Method.invokeNative(Native Method)
at java.lang.reflect.Method.invoke(Method.java:515)
at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:1291)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:1107)
at dalvik.system.NativeStart.main(Native Method)
Note that the majority of the time, the application restarts properly. However, every once in a while, I get the above error. I'm perplexed because the only place I call setURLStreamHandlerFactory
is here, and it is done in a static
block, which I presume - although correct me if I'm wrong - is only called once, when the ApplicationRoot
class if first loaded. However, it would seem that it is getting called twice, resulting in the above error.
Question:
What in the blazing sams is going on? My only guess at this point is that the VM/process for the updated application is the same as the previously installed application that is being updated, so when the static
block for the new ApplicationRoot
gets called, the URLStreamHandlerFactory
set by the old ApplicationRoot
is still "active". Is this possible? How can I avoid this situation? Seeing that it doesn't always happen, it seems to be a race condition of some sort; maybe within Android's APK installation routine? Thanks,
Edit:
Additional code as requested. Here is the manifest portion dealing with the Broadcast
<receiver android:name=".OnSelfUpdate" >
<intent-filter>
<action android:name="android.intent.action.PACKAGE_REPLACED" />
<data android:scheme="package" />
</intent-filter>
</receiver>
And the BroadcastReceiver
itself
public class OnSelfUpdate extends BroadcastReceiver {
@Override
public void onReceive(final Context context, final Intent intent) {
/* Get the application(s) updated. */
final int uid = intent.getIntExtra(Intent.EXTRA_UID, 0);
final PackageManager packageManager = context.getPackageManager();
final String[] packages = packageManager.getPackagesForUid(uid);
if (packages != null) {
final String thisPackage = context.getPackageName();
for (final String pkg : packages) {
/* Check to see if this application was updated. */
if (pkg.equals(thisPackage)) {
final Intent intent = new Intent(context, MainActivity.class);
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
context.startActivity(intent);
break;
}
}
}
}
}
Static blocks are executed when the class is loaded - if the class is reloaded for some reason (for example when it has been updated) it will be executed again.
In your case it means that the URLStreamHandlerFactory
you set the previous time it was loaded will remain.
This isn't really an issue unless you've updated the URLStreamHandlerFactory
.
There are two ways of fixing this:
Catch the Error
and continue on your merry way, ignoring the fact that you're still using the old factory.
Implement a very simple wrapper that delegates to another URLStreamHandlerFactory
that you can replace and that you won't have to change. You'll run into the same issue here with the wrapper though so you need to catch the Error
on that one or combine it with option 3.
Keep track of whether or not you've already installed the handler using a system property.
Code:
public static void maybeInstall(URLStreamHandlerFactory factory) {
if(System.getProperty("com.xxx.streamHandlerFactoryInstalled") == null) {
URL.setURLStreamHandlerFactory(factory);
System.setProperty("com.xxx.streamHandlerFactoryInstalled", "true");
}
}
URLStreamHandlerFactory
once - it makes little sense to me TBH.Code:
public static void forcefullyInstall(URLStreamHandlerFactory factory) {
try {
// Try doing it the normal way
URL.setURLStreamHandlerFactory(factory);
} catch (final Error e) {
// Force it via reflection
try {
final Field factoryField = URL.class.getDeclaredField("factory");
factoryField.setAccessible(true);
factoryField.set(null, factory);
} catch (NoSuchFieldException | IllegalAccessException e1) {
throw new Error("Could not access factory field on URL class: {}", e);
}
}
}
The field name is factory
on Oracle's JRE
, might be different on Android.
AFAIK you can/should not restart the JVM. Furthermore, as you've already found out, you can't set URLStreamHandlerFactory
twice in a JVM for a single application.
Your application should try to set the factory only when it isn't:
try {
URL.setURLStreamHandlerFactory(factory);
} catch (Error e) {
e.printStackTrace();
}
If your application updates also include updating the factory, you could try killing the process your app resides in but I don't it's a good idea to do so, even worse - it might not even work.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With