16

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

}
pathfinderelite
  • 3,047
  • 1
  • 27
  • 30

3 Answers3

10

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.

ItamarG3
  • 4,092
  • 6
  • 31
  • 44
Raniz
  • 10,882
  • 1
  • 32
  • 64
  • Thanks, #2 seems promising. – pathfinderelite May 29 '15 at 10:46
  • For me, only solution #4 worked. Because my code is loaded via my class loader, the factory was set in the JDK URL class but when trying to use it from another class loader I had unknown protocol. – Anthony Jul 04 '23 at 14:35
2

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.

Community
  • 1
  • 1
Simas
  • 43,548
  • 10
  • 88
  • 116
  • *An Error is a subclass of Throwable that indicates serious problems that a reasonable application should not try to catch* From [doc](http://docs.oracle.com/javase/7/docs/api/java/lang/Error.html). I'm uncomfortable catching an `Error`, as the JVM may remain in an abnormal state. Then again, it's not as abnormal as a crashed application :). In any case, thank you, but this doesn't really answer my question of why it's happening. – pathfinderelite May 22 '15 at 20:43
  • @pathfinderelite You can always check if the error thrown is the one you expect it to be otherwise re-throw it or you could instead get the static variable via reflection and check if it's null. – Simas May 22 '15 at 20:46
  • Unfortunately, passively ignoring the situation has another, potentially more significant, problem. If the updated application implements the URLStreamHandlerFactory differently than the previous version, the new factory will not get set. The old factory would still be in-place. This also begs the question, which `ExternalProtocol` and `ArchiveProtocol` classes would the old factory use? The ones from the previous version or the updated version? – pathfinderelite May 22 '15 at 21:33
0

Following configuration (fork) worked for me :

<plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-surefire-plugin</artifactId>
                <configuration>
                    <forkCount>1</forkCount>
                    <reuseForks>false</reuseForks>
                    <argLine>--add-exports java.base/sun.nio.ch=ALL-UNNAMED</argLine>
                </configuration>
 </plugin>
Tohid Makari
  • 1,700
  • 3
  • 15
  • 29