Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

setURLStreamHandlerFactory and "java.lang.Error: Factory already set"

Tags:

java

android

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;
                }
            }
        }
    }

}
like image 777
pathfinderelite Avatar asked May 15 '15 19:05

pathfinderelite


2 Answers

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:

  1. Catch the Error and continue on your merry way, ignoring the fact that you're still using the old factory.

  2. 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.

  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");
    }
}
  1. Force replacement using reflection. I have absolutely no idea why you can only set the 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.

like image 137
Raniz Avatar answered Oct 22 '22 00:10

Raniz


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.

like image 40
Simas Avatar answered Oct 22 '22 01:10

Simas