View Javadoc
1   /*
2    * Copyright (C) 2013 4th Line GmbH, Switzerland
3    *
4    * The contents of this file are subject to the terms of either the GNU
5    * Lesser General Public License Version 2 or later ("LGPL") or the
6    * Common Development and Distribution License Version 1 or later
7    * ("CDDL") (collectively, the "License"). You may not use this file
8    * except in compliance with the License. See LICENSE.txt for more
9    * information.
10   *
11   * This program is distributed in the hope that it will be useful,
12   * but WITHOUT ANY WARRANTY; without even the implied warranty of
13   * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
14   */
15  
16  package org.fourthline.cling.android;
17  
18  import android.content.BroadcastReceiver;
19  import android.content.Context;
20  import android.content.Intent;
21  import android.content.IntentFilter;
22  import android.net.ConnectivityManager;
23  import android.net.NetworkInfo;
24  import android.net.wifi.WifiManager;
25  import org.fourthline.cling.UpnpServiceConfiguration;
26  import org.fourthline.cling.model.ModelUtil;
27  import org.fourthline.cling.protocol.ProtocolFactory;
28  import org.fourthline.cling.transport.Router;
29  import org.fourthline.cling.transport.RouterException;
30  import org.fourthline.cling.transport.RouterImpl;
31  import org.fourthline.cling.transport.spi.InitializationException;
32  import org.seamless.util.Exceptions;
33  
34  import java.util.logging.Level;
35  import java.util.logging.Logger;
36  
37  /**
38   * Monitors all network connectivity changes, switching the router accordingly.
39   *
40   * @author Michael Pujos
41   * @author Christian Bauer
42   */
43  public class AndroidRouter extends RouterImpl {
44  
45      final private static Logger log = Logger.getLogger(Router.class.getName());
46  
47      final private Context context;
48  
49      final private WifiManager wifiManager;
50      protected WifiManager.MulticastLock multicastLock;
51      protected WifiManager.WifiLock wifiLock;
52      protected NetworkInfo networkInfo;
53      protected BroadcastReceiver broadcastReceiver;
54  
55      public AndroidRouter(UpnpServiceConfiguration configuration,
56                           ProtocolFactory protocolFactory,
57                           Context context) throws InitializationException {
58          super(configuration, protocolFactory);
59  
60          this.context = context;
61          this.wifiManager = ((WifiManager) context.getSystemService(Context.WIFI_SERVICE));
62          this.networkInfo = NetworkUtils.getConnectedNetworkInfo(context);
63  
64          // Only register for network connectivity changes if we are not running on emulator
65          if (!ModelUtil.ANDROID_EMULATOR) {
66              this.broadcastReceiver = createConnectivityBroadcastReceiver();
67              context.registerReceiver(broadcastReceiver, new IntentFilter("android.net.conn.CONNECTIVITY_CHANGE"));
68          }
69      }
70  
71      protected BroadcastReceiver createConnectivityBroadcastReceiver() {
72  		return new ConnectivityBroadcastReceiver();
73  	}
74  
75      @Override
76      protected int getLockTimeoutMillis() {
77          return 15000;
78      }
79  
80      @Override
81      public void shutdown() throws RouterException {
82          super.shutdown();
83          unregisterBroadcastReceiver();
84      }
85  
86      @Override
87      public boolean enable() throws RouterException {
88          lock(writeLock);
89          try {
90              boolean enabled;
91              if ((enabled = super.enable())) {
92                  // Enable multicast on the WiFi network interface,
93                  // requires android.permission.CHANGE_WIFI_MULTICAST_STATE
94                  if (isWifi()) {
95                      setWiFiMulticastLock(true);
96                      setWifiLock(true);
97                  }
98              }
99              return enabled;
100         } finally {
101             unlock(writeLock);
102         }
103     }
104 
105     @Override
106     public boolean disable() throws RouterException {
107         lock(writeLock);
108         try {
109             // Disable multicast on WiFi network interface,
110             // requires android.permission.CHANGE_WIFI_MULTICAST_STATE
111             if (isWifi()) {
112                 setWiFiMulticastLock(false);
113                 setWifiLock(false);
114             }
115             return super.disable();
116         } finally {
117             unlock(writeLock);
118         }
119     }
120 
121     public NetworkInfo getNetworkInfo() {
122         return networkInfo;
123     }
124 
125     public boolean isMobile() {
126         return NetworkUtils.isMobile(networkInfo);
127     }
128 
129     public boolean isWifi() {
130         return NetworkUtils.isWifi(networkInfo);
131     }
132 
133     public boolean isEthernet() {
134         return NetworkUtils.isEthernet(networkInfo);
135     }
136 
137     public boolean enableWiFi() {
138         log.info("Enabling WiFi...");
139         try {
140             return wifiManager.setWifiEnabled(true);
141         } catch (Throwable t) {
142             // workaround (HTC One X, 4.0.3)
143             //java.lang.SecurityException: Permission Denial: writing com.android.providers.settings.SettingsProvider
144             // uri content://settings/system from pid=4691, uid=10226 requires android.permission.WRITE_SETTINGS
145             //	at android.os.Parcel.readException(Parcel.java:1332)
146             //	at android.os.Parcel.readException(Parcel.java:1286)
147             //	at android.net.wifi.IWifiManager$Stub$Proxy.setWifiEnabled(IWifiManager.java:1115)
148             //	at android.net.wifi.WifiManager.setWifiEnabled(WifiManager.java:946)
149             log.log(Level.WARNING, "SetWifiEnabled failed", t);
150             return false;
151         }
152     }
153 
154     public void unregisterBroadcastReceiver() {
155         if (broadcastReceiver != null) {
156             context.unregisterReceiver(broadcastReceiver);
157             broadcastReceiver = null;
158         }
159     }
160 
161     protected void setWiFiMulticastLock(boolean enable) {
162         if (multicastLock == null) {
163             multicastLock = wifiManager.createMulticastLock(getClass().getSimpleName());
164         }
165 
166         if (enable) {
167             if (multicastLock.isHeld()) {
168                 log.warning("WiFi multicast lock already acquired");
169             } else {
170                 log.info("WiFi multicast lock acquired");
171                 multicastLock.acquire();
172             }
173         } else {
174             if (multicastLock.isHeld()) {
175                 log.info("WiFi multicast lock released");
176                 multicastLock.release();
177             } else {
178                 log.warning("WiFi multicast lock already released");
179             }
180         }
181     }
182 
183     protected void setWifiLock(boolean enable) {
184         if (wifiLock == null) {
185             wifiLock = wifiManager.createWifiLock(WifiManager.WIFI_MODE_FULL_HIGH_PERF, getClass().getSimpleName());
186         }
187 
188         if (enable) {
189             if (wifiLock.isHeld()) {
190                 log.warning("WiFi lock already acquired");
191             } else {
192                 log.info("WiFi lock acquired");
193                 wifiLock.acquire();
194             }
195         } else {
196             if (wifiLock.isHeld()) {
197                 log.info("WiFi lock released");
198                 wifiLock.release();
199             } else {
200                 log.warning("WiFi lock already released");
201             }
202         }
203     }
204 
205     /**
206      * Can be overriden by subclasses to do additional work.
207      *
208      * @param oldNetwork <code>null</code> when first called by constructor.
209      */
210     protected void onNetworkTypeChange(NetworkInfo oldNetwork, NetworkInfo newNetwork) throws RouterException {
211         log.info(String.format("Network type changed %s => %s",
212             oldNetwork == null ? "" : oldNetwork.getTypeName(),
213             newNetwork == null ? "NONE" : newNetwork.getTypeName()));
214 
215         if (disable()) {
216             log.info(String.format(
217                 "Disabled router on network type change (old network: %s)",
218                 oldNetwork == null ? "NONE" : oldNetwork.getTypeName()
219             ));
220         }
221 
222         networkInfo = newNetwork;
223         if (enable()) {
224             // Can return false (via earlier InitializationException thrown by NetworkAddressFactory) if
225             // no bindable network address found!
226             log.info(String.format(
227                 "Enabled router on network type change (new network: %s)",
228                 newNetwork == null ? "NONE" : newNetwork.getTypeName()
229             ));
230         }
231     }
232 
233     /**
234      * Handles errors when network has been switched, during reception of
235      * network switch broadcast. Logs a warning by default, override to
236      * change this behavior.
237      */
238     protected void handleRouterExceptionOnNetworkTypeChange(RouterException ex) {
239         Throwable cause = Exceptions.unwrap(ex);
240         if (cause instanceof InterruptedException) {
241             log.log(Level.INFO, "Router was interrupted: " + ex, cause);
242         } else {
243             log.log(Level.WARNING, "Router error on network change: " + ex, ex);
244         }
245     }
246 
247     class ConnectivityBroadcastReceiver extends BroadcastReceiver {
248 
249         @Override
250         public void onReceive(Context context, Intent intent) {
251 
252             if (!intent.getAction().equals(ConnectivityManager.CONNECTIVITY_ACTION))
253                 return;
254 
255             displayIntentInfo(intent);
256 
257             NetworkInfo newNetworkInfo = NetworkUtils.getConnectedNetworkInfo(context);
258 
259             // When Android switches WiFI => MOBILE, sometimes we may have a short transition
260             // with no network: WIFI => NONE, NONE => MOBILE
261             // The code below attempts to make it look like a single WIFI => MOBILE
262             // transition, retrying up to 3 times getting the current network.
263             //
264             // Note: this can block the UI thread for up to 3s
265             if (networkInfo != null && newNetworkInfo == null) {
266                 for (int i = 1; i <= 3; i++) {
267                     try {
268                         Thread.sleep(1000);
269                     } catch (InterruptedException e) {
270                         return;
271                     }
272                     log.warning(String.format(
273                         "%s => NONE network transition, waiting for new network... retry #%d",
274                         networkInfo.getTypeName(), i
275                     ));
276                     newNetworkInfo = NetworkUtils.getConnectedNetworkInfo(context);
277                     if (newNetworkInfo != null)
278                         break;
279                 }
280             }
281 
282             if (isSameNetworkType(networkInfo, newNetworkInfo)) {
283                 log.info("No actual network change... ignoring event!");
284             } else {
285                 try {
286                     onNetworkTypeChange(networkInfo, newNetworkInfo);
287                 } catch (RouterException ex) {
288                     handleRouterExceptionOnNetworkTypeChange(ex);
289                 }
290             }
291         }
292 
293         protected boolean isSameNetworkType(NetworkInfo network1, NetworkInfo network2) {
294             if (network1 == null && network2 == null)
295                 return true;
296             if (network1 == null || network2 == null)
297                 return false;
298             return network1.getType() == network2.getType();
299         }
300 
301         protected void displayIntentInfo(Intent intent) {
302             boolean noConnectivity = intent.getBooleanExtra(ConnectivityManager.EXTRA_NO_CONNECTIVITY, false);
303             String reason = intent.getStringExtra(ConnectivityManager.EXTRA_REASON);
304             boolean isFailover = intent.getBooleanExtra(ConnectivityManager.EXTRA_IS_FAILOVER, false);
305 
306             NetworkInfo currentNetworkInfo = (NetworkInfo) intent.getParcelableExtra(ConnectivityManager.EXTRA_NETWORK_INFO);
307             NetworkInfo otherNetworkInfo = (NetworkInfo) intent.getParcelableExtra(ConnectivityManager.EXTRA_OTHER_NETWORK_INFO);
308 
309             log.info("Connectivity change detected...");
310             log.info("EXTRA_NO_CONNECTIVITY: " + noConnectivity);
311             log.info("EXTRA_REASON: " + reason);
312             log.info("EXTRA_IS_FAILOVER: " + isFailover);
313             log.info("EXTRA_NETWORK_INFO: " + (currentNetworkInfo == null ? "none" : currentNetworkInfo));
314             log.info("EXTRA_OTHER_NETWORK_INFO: " + (otherNetworkInfo == null ? "none" : otherNetworkInfo));
315             log.info("EXTRA_EXTRA_INFO: " + intent.getStringExtra(ConnectivityManager.EXTRA_EXTRA_INFO));
316         }
317 
318     }
319 
320 }