1

I've used protocol handlers in the past overriding the default http handler and creating my own custom handlers and I was thinking the approach still works on Android. I am trying to override any http or https URL requested by my Android app and pass it to a custom handler under certain situations. However I still would like to access web resources in other cases. How do I retrieve the default http/https protocol handlers? I'm trying something like the following to load the default handler before putting my override in place:

static URLStreamHandler handler;
static {
    Class<?> handlerClass;
    try {
        handlerClass = Class.forName("net.www.protocol.http.Handler");
    } catch (ClassNotFoundException e) {
        throw new RuntimeException("Error loading clas for default http handler.", e);
    }
    Object handlerInstance;
    try {
        handlerInstance = handlerClass.newInstance();
    } catch (InstantiationException e) {
        throw new RuntimeException("Error instantiating default http handler.", e);
    } catch (IllegalAccessException e) {
        throw new RuntimeException("Error accessing default http handler.", e);
    }
    if (! (handlerInstance instanceof URLStreamHandler)) {
        throw new RuntimeException("Wrong class type, " + handlerInstance.getClass().getName());
    } else {
        handler = (URLStreamHandler) handlerInstance;
    }
}

My override logic works as follows:

URL.setURLStreamHandlerFactory(new URLStreamHandlerFactory() {
    public URLStreamHandler createURLStreamHandler(String protocol) {
        URLStreamHandler urlStreamHandler = new URLStreamHandler() {
            protected URLConnection openConnection(URL url) throws IOException {
                return new URLConnection(url) {
                    public void connect() throws IOException {
                        Log.i(getClass().getName(), "Global URL override!!! URL load requested " + url);
                    }
                };
            }
        };
        return shouldHandleURL(url) ? urlStreamHandler : handler;
    }
});

The override works but I cannot load the default in cases where I want normal URL connection behavior. Trying to clear my StreamHandlerFactory as follows:

URL.setURLStreamHandlerFactory(null);

Throws an error:

java.lang.Error: Factory already set
at java.net.URL.setURLStreamHandlerFactory(URL.java:112)
Cliff
  • 10,586
  • 7
  • 61
  • 102

2 Answers2

0

The only way I've been able to resolve my issue is by setting the streamHandler and StramHandler factory to null using reflection through the private fields. It's yucky but it works. This is my temporary solution (I was hoping for something less yucky):

private static class APIURLStreamHandlerFactory implements URLStreamHandlerFactory {
    public URLStreamHandler createURLStreamHandler(String protocol) {
        return new URLStreamHandler() {
            protected URLConnection openConnection(URL url) throws IOException {

                if (! shouldHandle(url)) {
                    Field streamHandlerMapField = getURLPrivateField("streamHandlers");
                    try { Map handlerMap = (Map) streamHandlerMapField.get(url); handlerMap.clear(); }
                    catch (IllegalAccessException e) { throw new Error("Could not access private field streamHandler",e); }
                    unregisterSelf();
                    invokeInstancePrivateMethod(url, "setupStreamHandler");
                    URLStreamHandler originalHandler = getPrivateUrlStreamHandler(url);
                    Method openConnectionMethod = getPrivateMethod(originalHandler, "openConnection", URL.class);
                    openConnectionMethod.setAccessible(true);
                    try { return (URLConnection) openConnectionMethod.invoke(originalHandler, url); }
                    catch (IllegalAccessException e) { throw new Error("Could not access openConnection on URL", e); }
                    catch (InvocationTargetException e) { throw new RuntimeException("Exception while invoking openConnection on URL", e); }
                    finally { registerSelf(); }
                }

                return new APIURLConnection(url, registeredServiceRouter);
            }
        };
    }

    private static Method getPrivateMethod(Object object, String methodName, Class... parameterTypes) {
        try { return object.getClass().getDeclaredMethod(methodName, parameterTypes); }
        catch (NoSuchMethodException e) { throw new Error("Could not find method " + methodName, e); }
    }

    private static boolean shouldHandle(URL url) {
        //Logic to decide which requests to handle
    }

    private static URLStreamHandler getPrivateUrlStreamHandler(URL url) {
        URLStreamHandler originalHandler;
        try { originalHandler = (URLStreamHandler) getURLPrivateField("streamHandler").get(url); }
        catch (IllegalAccessException e) { throw new Error("Could not access streamHandler field on URL",e); }
        return originalHandler;
    }

    private static Object invokeInstancePrivateMethod(Object objectInstance, String methodName) {
        try {
            Method urlPrivateMethod = getURLPrivateMethod(methodName);
            urlPrivateMethod.setAccessible(true);
            return urlPrivateMethod.invoke(objectInstance);
        }
        catch (IllegalAccessException e) { throw new Error("Cannot access metehod " + methodName + " on instance type " + objectInstance.getClass().getName(), e); }
        catch (InvocationTargetException e) { throw new RuntimeException("Exception while invoking method " + methodName + " on type " + objectInstance.getClass().getName(),e); }
    }

    private static Method getURLPrivateMethod(String methodName) {
        try { return URL.class.getDeclaredMethod(methodName); }
        catch (NoSuchMethodException e) { throw new Error("Method " + methodName + " not found on class URL"); }
    }

    @TargetApi(Build.VERSION_CODES.KITKAT)
    private static void resetStreamHandlerFactory() {
        try { getURLPrivateField("streamHandlerFactory").set(null, null); }
        catch (IllegalAccessException e) { throw new Error("Could not access factory field on URL class: {}", e); }
    }

    @NonNull
    private static Field getURLPrivateField(String field) {
        final Field privateField;
        try { privateField = URL.class.getDeclaredField(field); }
        catch (NoSuchFieldException e) { throw new Error("No such field " + field + " in class URL"); }
        privateField.setAccessible(true);
        return privateField;
    }
}
Cliff
  • 10,586
  • 7
  • 61
  • 102
0

I found this in java.net.URL

else if (protocol.equals("http")) {
        try {
            String name = "com.android.okhttp.HttpHandler";
            streamHandler = (URLStreamHandler) Class.forName(name).newInstance();
        } catch (Exception e) {
            throw new AssertionError(e);
        }
    } 

It would appear com.android.okhttp.HttpHandler would be the stream handler you would want to return for default behaviour

Here are the other defaults:

if (protocol.equals("file")) {
        streamHandler = new FileHandler();
    } else if (protocol.equals("ftp")) {
        streamHandler = new FtpHandler();
    } else if (protocol.equals("http")) {
        try {
            String name = "com.android.okhttp.HttpHandler";
            streamHandler = (URLStreamHandler) Class.forName(name).newInstance();
        } catch (Exception e) {
            throw new AssertionError(e);
        }
    } else if (protocol.equals("https")) {
        try {
            String name = "com.android.okhttp.HttpsHandler";
            streamHandler = (URLStreamHandler) Class.forName(name).newInstance();
        } catch (Exception e) {
            throw new AssertionError(e);
        }
    } else if (protocol.equals("jar")) {
        streamHandler = new JarHandler();
    }
    if (streamHandler != null) {
        streamHandlers.put(protocol, streamHandler);
    }

PS: I've been trying to solve this for the past couple hours and your post was the only one I could find wanting to do a similar thing. Hopefully this helps.

Joel B
  • 1
  • Thanks Joel! I'd seen that logic myself and decided to set the factory to make sure all of the regular logic was invoked. However I want to avoid relying on a specific class or field that could change based on device and/or API version. Reading through source was how I realized there was a guard around resetting and how I saw which field needed to be set. – Cliff Feb 17 '16 at 14:54