/* * Copyright (c) 2008, 2010, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * This code is free software; you can redistribute it and/or modify it * under the terms of the GNU General Public License version 2 only, as * published by the Free Software Foundation. Oracle designates this * particular file as subject to the "Classpath" exception as provided * by Oracle in the LICENSE file that accompanied this code. * * This code is distributed in the hope that it will be useful, but WITHOUT * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License * version 2 for more details (a copy is included in the LICENSE file that * accompanied this code). * * You should have received a copy of the GNU General Public License version * 2 along with this work; if not, write to the Free Software Foundation, * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. * * Please contact Oracle, 500 Oracle Parkway, Redwood Shores * CA 94065 USA or visit www.oracle.com if you need additional information or * have any questions. */ package com.codename1.io; import com.codename1.ui.Dialog; import com.codename1.ui.Display; import com.codename1.ui.events.ActionEvent; import com.codename1.ui.events.ActionListener; import com.codename1.ui.util.EventDispatcher; import java.io.IOException; import java.io.InputStream; import java.util.Collection; import java.util.Enumeration; import java.util.Hashtable; import java.util.Vector; /** * <p>Main entry point for managing the connection requests, this is essentially a * threaded queue that makes sure to route all connections via the network thread * while sending the callbacks through the Codename One EDT.</p> * * <p>The sample * code below fetches a page of data from the nestoria housing listing API.<br> * You can see instructions on how to display the data in the {@link com.codename1.components.InfiniteScrollAdapter} * class. You can read more about networking in Codename One {@link com.codename1.io here}</p> * <script src="https://gist.github.com/codenameone/22efe9e04e2b8986dfc3.js"></script> * * @author Shai Almog */ public class NetworkManager { /** * Indicates an unknown access point type */ public static final int ACCESS_POINT_TYPE_UNKNOWN = 1; /** * Indicates a wlan (802.11b/c/g/n) access point type */ public static final int ACCESS_POINT_TYPE_WLAN = 2; /** * Indicates an access point based on a cable */ public static final int ACCESS_POINT_TYPE_CABLE = 3; /** * Indicates a 3g network access point type */ public static final int ACCESS_POINT_TYPE_NETWORK3G = 4; /** * Indicates a 2g network access point type */ public static final int ACCESS_POINT_TYPE_NETWORK2G = 5; /** * Indicates a corporate routing server access point type (e.g. BIS etc.) */ public static final int ACCESS_POINT_TYPE_CORPORATE = 6; private static final Object LOCK = new Object(); private static final NetworkManager INSTANCE = new NetworkManager(); /** * This URL is used to check whether an Internet connection is available * @return the autoDetectURL */ public static String getAutoDetectURL() { return autoDetectURL; } /** * This URL is used to check whether an Internet connection is available * @param aAutoDetectURL the autoDetectURL to set */ public static void setAutoDetectURL(String aAutoDetectURL) { autoDetectURL = aAutoDetectURL; } private Vector pending = new Vector(); private boolean running; private int threadCount = 1; private NetworkThread[] networkThreads; private EventDispatcher errorListeners; private EventDispatcher progressListeners; private int timeout = 300000; private Hashtable threadAssignements = new Hashtable(); private Hashtable userHeaders; private boolean autoDetected; private static String autoDetectURL = "https://www.google.com/"; private NetworkManager() { } void resetAPN() { autoDetected = false; } private boolean handleException(ConnectionRequest r, Exception o) { if(errorListeners != null) { ActionEvent ev = new NetworkEvent(r, o); errorListeners.fireActionEvent(ev); return ev.isConsumed(); } return false; } /** * The number of threads * * @return the threadCount */ public int getThreadCount() { return threadCount; } /** * Thread count should never be changed when the network is running since it will have no effect. * Increasing the thread count can bring many race conditions and problems to the surface, * furthermore MIDP doesn't require support for more than one network thread hence increasing * the thread count might fail. * * @param threadCount the threadCount to set * @deprecated since the network is always running in Codename One this method is quite confusing * unfortunately fixing it will probably break working code. You should migrate the code to use * {@link #updateThreadCount(int)} */ public void setThreadCount(int threadCount) { // in auto detect mode multiple threads can break the detections if(!Util.getImplementation().shouldAutoDetectAccessPoint()) { this.threadCount = threadCount; } } /** * Sets the number of network threads and restarts the network threads * @param threadCount the new number of threads */ public void updateThreadCount(int threadCount) { this.threadCount = threadCount; shutdown(); start(); } class NetworkThread implements Runnable { private ConnectionRequest currentRequest; private Thread threadInstance; boolean stopped = false; public NetworkThread() { } public ConnectionRequest getCurrentRequest() { return currentRequest; } public void join() { try { threadInstance.join(); } catch (InterruptedException ex) { ex.printStackTrace(); } } public void start() { Util.getImplementation().startThread("Network Thread", this); } public void interrupt() { if(threadInstance != null) { threadInstance.interrupt(); } } public Thread getThreadInstance() { return threadInstance; } public void run() { threadInstance = Thread.currentThread(); while(running && !stopped) { if(pending.size() > 0) { // the synchronization here isn't essential, only for good measure synchronized(LOCK) { //double lock to prevent a potential exception if(pending.size() == 0){ continue; } currentRequest = (ConnectionRequest)pending.elementAt(0); pending.removeElementAt(0); currentRequest.prepare(); if(currentRequest.isKilled()){ continue; } } if(userHeaders != null) { Enumeration e = userHeaders.keys(); while(e.hasMoreElements()) { String key = (String)e.nextElement(); String value = (String)userHeaders.get(key); currentRequest.addRequestHeaderDontRepleace(key, value); } } if(threadAssignements.size() > 0) { String n = currentRequest.getClass().getName(); Integer threadOffset = (Integer)threadAssignements.get(n); NetworkThread[] networkThreads = NetworkManager.this.networkThreads; if(networkThreads == null) { return; } if(threadOffset != null && networkThreads[threadOffset.intValue()] != this) { synchronized(LOCK) { if(pending.size() > 0) { pending.insertElementAt(currentRequest, 1); continue; } pending.addElement(currentRequest); LOCK.notify(); try { LOCK.wait(30); } catch (InterruptedException ex) { ex.printStackTrace(); } } } } int frameRate = -1; try { // for higher priority tasks increase the thread priority, for lower // prioirty tasks decrease it. In critical priority reduce the Codename One // rendering thread speed for even faster download switch(currentRequest.getPriority()) { case ConnectionRequest.PRIORITY_CRITICAL: frameRate = Display.getInstance().getFrameRate(); Display.getInstance().setFramerate(4); Thread.currentThread().setPriority(Thread.MAX_PRIORITY - 1); break; case ConnectionRequest.PRIORITY_HIGH: Thread.currentThread().setPriority(Thread.NORM_PRIORITY + 2); break; case ConnectionRequest.PRIORITY_NORMAL: break; case ConnectionRequest.PRIORITY_LOW: Thread.currentThread().setPriority(Thread.MIN_PRIORITY + 2); break; case ConnectionRequest.PRIORITY_REDUNDANT: Thread.currentThread().setPriority(Thread.MIN_PRIORITY); break; } if(progressListeners != null) { progressListeners.fireActionEvent(new NetworkEvent(currentRequest, NetworkEvent.PROGRESS_TYPE_INITIALIZING)); } if(currentRequest.getShowOnInit() != null) { currentRequest.getShowOnInit().showModeless(); } currentRequest.performOperation(); } catch(IOException e) { if(!currentRequest.isFailSilently()) { if(!handleException(currentRequest, e)) { currentRequest.handleIOException(e); } } else { // for the record Log.e(e); } } catch(RuntimeException er) { if(!currentRequest.isFailSilently()) { if(!handleException(currentRequest, er)) { currentRequest.handleRuntimeException(er); } } else { // for the record Log.e(er); } } finally { Thread.currentThread().setPriority(Thread.NORM_PRIORITY); if(frameRate > -1) { Display.getInstance().setFramerate(frameRate); } if(progressListeners != null) { progressListeners.fireActionEvent(new NetworkEvent(currentRequest, NetworkEvent.PROGRESS_TYPE_COMPLETED)); } if(currentRequest.getDisposeOnCompletion() != null && !currentRequest.isRedirecting()) { // there may be a race condition where the dialog hasn't yet appeared but the // network request completed final ConnectionRequest finalReq = currentRequest; Display.getInstance().callSerially(new Runnable() { public void run() { Dialog dlg = finalReq.getDisposeOnCompletion(); if (dlg != null) { dlg.dispose(); } } }); } } currentRequest = null; // wakeup threads waiting for the completion of this network operation synchronized(LOCK) { LOCK.notifyAll(); } } else { synchronized(LOCK) { try { // prevent waiting when there is still a pending request // this can occur with a race condition since the synchronize // scope is limited to prevent blocking on add... if(pending.size() == 0) { LOCK.wait(); } } catch (InterruptedException ex) { ex.printStackTrace(); } } } } } } boolean hasProgressListeners() { return progressListeners != null; } void fireProgressEvent(ConnectionRequest c, int type, int length, int sentReceived) { // progressListeners might be made null by a separate thread EventDispatcher d = progressListeners; if(d != null) { NetworkEvent n = new NetworkEvent(c, type); n.setLength(length); n.setSentReceived(sentReceived); d.fireActionEvent(n); } } private NetworkThread createNetworkThread() { return new NetworkThread(); } class AutoDetectAPN extends ConnectionRequest { private Vector aps = null; private int currentAP; protected void handleErrorResponseCode(int code, String message) { retryWithDifferentAPN(); } protected void handleException(Exception err) { retryWithDifferentAPN(); } protected void readResponse(InputStream input) throws IOException { String s = Util.readToString(input); if(!s.equals("hi")) { retryWithDifferentAPN(); } } private String nextAP() { if(aps == null) { aps = new Vector(); String[] ids = getAPIds(); int idlen = ids.length; for(int iter = 0 ; iter < idlen ; iter++) { int t = getAPType(ids[iter]); if(t == ACCESS_POINT_TYPE_WLAN) { aps.insertElementAt(ids[iter ], 0); } else { if(t == ACCESS_POINT_TYPE_CORPORATE || t == ACCESS_POINT_TYPE_NETWORK3G) { aps.addElement(ids[iter]); } } } // add all the 2G networks at the end for(int iter = 0 ; iter < idlen ; iter++) { int t = getAPType(ids[iter]); if(t == ACCESS_POINT_TYPE_NETWORK2G) { aps.addElement(ids[iter]); } } } if(currentAP >= aps.size()) { return null; } String s = (String)aps.elementAt(currentAP); currentAP++; return s; } private void retryWithDifferentAPN() { String n = nextAP(); if(n == null) { return; } setCurrentAccessPoint(n); AutoDetectAPN r = new AutoDetectAPN(); r.setPost(false); r.currentAP = currentAP; r.aps = aps; r.setUrl(autoDetectURL); r.setPriority(ConnectionRequest.PRIORITY_CRITICAL); addToQueue(r); } public boolean equals(Object o) { return false; } } /** * There is no need to invoke this method since the network manager is started * implicitly. It is useful only if you explicitly stop the network manager. * Invoking this method otherwise will just do nothing. */ public void start() { if(networkThreads != null) { //throw new IllegalStateException("Network manager already initialized"); return; } running = true; networkThreads = new NetworkThread[getThreadCount()]; for(int iter = 0 ; iter < getThreadCount() ; iter++) { networkThreads[iter] = createNetworkThread(); networkThreads[iter].start(); } // we need to implement a timeout thread of our own for this case... if(!Util.getImplementation().isTimeoutSupported()) { Util.getImplementation().startThread("Timeout Thread", new Runnable() { public void run() { // detect timeout violations by polling while(running) { try { Thread.sleep(timeout / 10); } catch (InterruptedException ex) { ex.printStackTrace(); } NetworkThread[] networkThreads = NetworkManager.this.networkThreads; if(networkThreads == null) { return; } // check for timeout violations on the currently executing threads int ntlen = networkThreads.length; for(int iter = 0 ; iter < ntlen ; iter++) { ConnectionRequest c = networkThreads[iter].getCurrentRequest(); if(c != null) { int cTimeout = Math.min(timeout, c.getTimeout()); if(c.getTimeout() < 0) { cTimeout = timeout; } if(c.getTimeSinceLastActivity() > cTimeout) { // we have a timeout problem on our hands! We need to try and kill! c.kill(); networkThreads[iter].interrupt(); try { Thread.sleep(500); } catch (InterruptedException ex) { ex.printStackTrace(); } // did the attempt work? if(networkThreads[iter].getCurrentRequest() == c) { if(c.getTimeSinceLastActivity() > cTimeout) { // we need to create a whole new network thread and abandon this one! if(running) { networkThreads[iter] = createNetworkThread(); networkThreads[iter].start(); } } } } } } } } }); } } /** * Shuts down the network thread, this will trigger failures if you have network requests * @deprecated This method is for internal use only */ public void shutdown() { running = false; if(networkThreads != null) { for(NetworkThread n : networkThreads) { n.stopped = true; } } networkThreads = null; synchronized(LOCK) { LOCK.notifyAll(); } } /** * Shuts down the network thread and waits for shutdown to complete */ public void shutdownSync() { NetworkThread[] n = this.networkThreads; if(n != null) { NetworkThread t = n[0]; if(t != null) { shutdown(); t.join(); } } } /** * Returns the singleton instance of this class * * @return instance of this class */ public static NetworkManager getInstance() { return INSTANCE; } private void addSortedToQueue(ConnectionRequest request, int priority) { for(int iter = 0 ; iter < pending.size() ; iter++) { ConnectionRequest r = (ConnectionRequest)pending.elementAt(iter); if(r.getPriority() < priority) { pending.insertElementAt(request, iter); return; } } pending.addElement(request); } /** * Adds a header to the global default headers, this header will be implicitly added * to all requests going out from this point onwards. The main use case for this is * for authentication information communication via the header. * * @param key the key of the header * @param value the value of the header */ public void addDefaultHeader(String key, String value) { if(userHeaders == null) { userHeaders = new Hashtable(); } userHeaders.put(key, value); } /** * Identical to add to queue but waits until the request is processed in the queue, * this is useful for completely synchronous operations. * * @param request the request object to add */ public void addToQueueAndWait(final ConnectionRequest request) { class WaitingClass implements Runnable, ActionListener<NetworkEvent> { private boolean finishedWaiting; public void run() { while(!finishedWaiting) { try { Thread.sleep(30); } catch (InterruptedException ex) { ex.printStackTrace(); } } } public void actionPerformed(NetworkEvent e) { if(e.getError() != null) { finishedWaiting = true; removeProgressListener(this); removeErrorListener(this); return; } if(e.getConnectionRequest() == request) { if(e.getProgressType() == NetworkEvent.PROGRESS_TYPE_COMPLETED) { if(request.retrying) { request.retrying = false; return; } finishedWaiting = true; removeProgressListener(this); removeErrorListener(this); return; } } } } WaitingClass w = new WaitingClass(); addProgressListener(w); addErrorListener(w); addToQueue(request); if(Display.getInstance().isEdt()) { Display.getInstance().invokeAndBlock(w); } else { w.run(); } } /** * Adds the given network connection to the queue of execution * * @param request network request for execution */ public void addToQueue(ConnectionRequest request) { addToQueue(request, false); } /** * Kills the given request and waits until the request is killed if it is * being processed by one of the threads. This method must not be invoked from * a network thread! * @param request */ public void killAndWait(final ConnectionRequest request) { request.kill(); class KillWaitingClass implements Runnable { public void run() { for(int iter = 0 ; iter < threadCount ; iter++) { if(networkThreads[iter].currentRequest == request) { synchronized(LOCK) { while(networkThreads[iter].currentRequest == request) { try { LOCK.wait(20); } catch (InterruptedException ex) { ex.printStackTrace(); } } } } } } } Display.getInstance().invokeAndBlock(new KillWaitingClass()); } void kill9(final ConnectionRequest request) { if (request.isKilled()) { for(int iter = 0 ; iter < threadCount ; iter++) { if(networkThreads[iter].currentRequest == request) { synchronized(LOCK) { if(networkThreads[iter].currentRequest == request) { networkThreads[iter].interrupt(); networkThreads[iter].stopped = true; networkThreads[iter] = createNetworkThread(); networkThreads[iter].start(); } } } } } } /** * Adds the given network connection to the queue of execution * * @param request network request for execution */ void addToQueue(ConnectionRequest request, boolean retry) { if(!running) { start(); } if(!autoDetected) { autoDetected = true; if(Util.getImplementation().shouldAutoDetectAccessPoint()) { AutoDetectAPN r = new AutoDetectAPN(); r.setPost(false); r.setUrl(autoDetectURL); r.setPriority(ConnectionRequest.PRIORITY_CRITICAL); addToQueue(r, false); } } request.validateImpl(); synchronized(LOCK) { int i = request.getPriority(); if(!retry) { if(!request.isDuplicateSupported()) { if(pending.contains(request)) { System.out.println("Duplicate entry in the queue: " + request.getClass().getName() + ": " + request); return; } ConnectionRequest currentRequest = networkThreads[0].getCurrentRequest(); if(currentRequest != null && !currentRequest.retrying && currentRequest.equals(request)) { System.out.println("Duplicate entry detected"); return; } } } else { i = ConnectionRequest.PRIORITY_HIGH; } switch(i) { case ConnectionRequest.PRIORITY_CRITICAL: pending.insertElementAt(request, 0); ConnectionRequest currentRequest = networkThreads[0].getCurrentRequest(); if(currentRequest != null && currentRequest.getPriority() < ConnectionRequest.PRIORITY_CRITICAL) { if(currentRequest.isPausable()) { currentRequest.pause(); pending.insertElementAt(currentRequest, 1); } else { currentRequest.kill(); } } break; case ConnectionRequest.PRIORITY_HIGH: case ConnectionRequest.PRIORITY_NORMAL: case ConnectionRequest.PRIORITY_LOW: case ConnectionRequest.PRIORITY_REDUNDANT: addSortedToQueue(request, i); break; } LOCK.notify(); } } /** * Sets the timeout in milliseconds for network connections, a timeout may be "faked" * for platforms that don't support the notion of a timeout such as MIDP * * @param t the timeout duration */ public void setTimeout(int t) { if(Util.getImplementation().isTimeoutSupported()) { Util.getImplementation().setTimeout(t); } else { timeout = t; } } /** * Returns the timeout duration * * @return timeout in milliseconds */ public int getTimeout() { return timeout; } /** * Adds a generic listener to a network error that is invoked before the exception is propagated. * Notice that this doesn't apply to server error codes! * Consume the event in order to prevent it from propagating further. * * @param e callback will be invoked with the Exception as the source object */ public void addErrorListener(ActionListener<NetworkEvent> e) { if(errorListeners == null) { errorListeners = new EventDispatcher(); errorListeners.setBlocking(true); } errorListeners.addListener(e); } /** * Removes the given error listener * * @param e callback to remove */ public void removeErrorListener(ActionListener<NetworkEvent> e) { if(errorListeners == null) { return; } errorListeners.removeListener(e); } /** * Adds a listener to be notified when progress updates * * @param al action listener */ public void addProgressListener(ActionListener<NetworkEvent> al) { if(progressListeners == null) { progressListeners = new EventDispatcher(); progressListeners.setBlocking(false); } progressListeners.addListener(al); } /** * Adds a listener to be notified when progress updates * * @param al action listener */ public void removeProgressListener(ActionListener<NetworkEvent> al) { if(progressListeners == null) { return; } progressListeners.removeListener(al); Collection v = progressListeners.getListenerCollection(); if(v == null || v.size() == 0) { progressListeners = null; } } /** * Makes sure the given class (subclass of ConnectionRequest) is always assigned * to the given thread number. This is useful for a case of an application that wants * all background downloads to occur on one thread so it doesn't tie up the main * network thread (but doesn't stop like a low priority request would). * * @param requestType the class of the specific connection request * @param offset the offset of the thread starting from 0 and smaller than thread count */ public void assignToThread(Class requestType, int offset) { threadAssignements.put(requestType.getName(), new Integer(offset)); } /** * This method returns all pending ConnectioRequest connections. * @return the queue elements */ public Enumeration enumurateQueue(){ Vector elements = new Vector(); synchronized(LOCK) { Enumeration e = pending.elements(); while(e.hasMoreElements()){ elements.addElement(e.nextElement()); } } return elements.elements(); } /** * Indicates that the network queue is idle * * @return true if no network activity is in progress or pending */ public boolean isQueueIdle() { return pending == null || networkThreads == null || networkThreads[0] == null || (pending.size() == 0 && networkThreads[0].getCurrentRequest() == null); } /** * Indicates whether looking up an access point is supported by this device * * @return true if access point lookup is supported */ public boolean isAPSupported() { return Util.getImplementation().isAPSupported(); } /** * Returns the ids of the access points available if supported * * @return ids of access points */ public String[] getAPIds() { return Util.getImplementation().getAPIds(); } /** * Returns the type of the access point * * @param id access point id * @return one of the supported access point types from network manager */ public int getAPType(String id) { return Util.getImplementation().getAPType(id); } /** * Returns the user displayable name for the given access point * * @param id the id of the access point * @return the name of the access point */ public String getAPName(String id) { return Util.getImplementation().getAPName(id); } /** * Returns the id of the current access point * * @return id of the current access point */ public String getCurrentAccessPoint() { return Util.getImplementation().getCurrentAccessPoint(); } /** * Returns the id of the current access point * * @param id id of the current access point */ public void setCurrentAccessPoint(String id) { Util.getImplementation().setCurrentAccessPoint(id); } }