From d0e18ffb82b59d38aeaf0e552f48e734202719ab Mon Sep 17 00:00:00 2001 From: Robert Greenwalt Date: Tue, 26 Jan 2010 11:40:34 -0800 Subject: [PATCH] First pass at USB Tethering. bug:2281900 --- .../java/android/net/ConnectivityManager.java | 62 +++ .../android/net/IConnectivityManager.aidl | 8 + .../android/os/INetworkManagementService.aidl | 3 +- core/java/android/provider/Settings.java | 7 + .../android/internal/app/TetherActivity.java | 151 ++++++ core/res/AndroidManifest.xml | 4 + .../drawable-hdpi/stat_sys_tether_active.png | Bin 0 -> 1191 bytes .../res/drawable-hdpi/stat_sys_tether_usb.png | Bin 0 -> 1191 bytes .../drawable-mdpi/stat_sys_tether_active.png | Bin 0 -> 786 bytes .../res/drawable-mdpi/stat_sys_tether_usb.png | Bin 0 -> 786 bytes core/res/res/values/strings.xml | 44 +- .../android/server/ConnectivityService.java | 38 ++ .../server/NetworkManagementService.java | 17 +- .../server/connectivity/Tethering.java | 483 ++++++++++++++++++ 14 files changed, 807 insertions(+), 10 deletions(-) create mode 100644 core/java/com/android/internal/app/TetherActivity.java create mode 100755 core/res/res/drawable-hdpi/stat_sys_tether_active.png create mode 100755 core/res/res/drawable-hdpi/stat_sys_tether_usb.png create mode 100644 core/res/res/drawable-mdpi/stat_sys_tether_active.png create mode 100644 core/res/res/drawable-mdpi/stat_sys_tether_usb.png create mode 100644 services/java/com/android/server/connectivity/Tethering.java diff --git a/core/java/android/net/ConnectivityManager.java b/core/java/android/net/ConnectivityManager.java index 30799ec9f3b87..d435df584286a 100644 --- a/core/java/android/net/ConnectivityManager.java +++ b/core/java/android/net/ConnectivityManager.java @@ -115,6 +115,24 @@ public class ConnectivityManager public static final String ACTION_BACKGROUND_DATA_SETTING_CHANGED = "android.net.conn.BACKGROUND_DATA_SETTING_CHANGED"; + /** + * Broadcast Action: A tetherable connection has come or gone + * TODO - finish the doc + * @hide + */ + public static final String ACTION_TETHER_STATE_CHANGED = + "android.net.conn.TETHER_STATE_CHANGED"; + + /** + * @hide + */ + public static final String EXTRA_AVAILABLE_TETHER_COUNT = "availableCount"; + + /** + * @hide + */ + public static final String EXTRA_ACTIVE_TETHER_COUNT = "activeCount"; + /** * The Default Mobile data connection. When active, all data traffic * will use this connection by default. Should not coexist with other @@ -338,4 +356,48 @@ public class ConnectivityManager } mService = service; } + + /** + * {@hide} + */ + public String[] getTetherableIfaces() { + try { + return mService.getTetherableIfaces(); + } catch (RemoteException e) { + return new String[0]; + } + } + + /** + * {@hide} + */ + public String[] getTetheredIfaces() { + try { + return mService.getTetheredIfaces(); + } catch (RemoteException e) { + return new String[0]; + } + } + + /** + * {@hide} + */ + public boolean tether(String iface) { + try { + return mService.tether(iface); + } catch (RemoteException e) { + return false; + } + } + + /** + * {@hide} + */ + public boolean untether(String iface) { + try { + return mService.untether(iface); + } catch (RemoteException e) { + return false; + } + } } diff --git a/core/java/android/net/IConnectivityManager.aidl b/core/java/android/net/IConnectivityManager.aidl index 9f59ccede478c..caa3f2bbc772a 100644 --- a/core/java/android/net/IConnectivityManager.aidl +++ b/core/java/android/net/IConnectivityManager.aidl @@ -50,4 +50,12 @@ interface IConnectivityManager boolean getBackgroundDataSetting(); void setBackgroundDataSetting(boolean allowBackgroundData); + + boolean tether(String iface); + + boolean untether(String iface); + + String[] getTetherableIfaces(); + + String[] getTetheredIfaces(); } diff --git a/core/java/android/os/INetworkManagementService.aidl b/core/java/android/os/INetworkManagementService.aidl index e4ec098eeb265..f48f45f21f165 100644 --- a/core/java/android/os/INetworkManagementService.aidl +++ b/core/java/android/os/INetworkManagementService.aidl @@ -140,7 +140,8 @@ interface INetworkManagementService * Attaches a PPP server daemon to the specified TTY with the specified * local/remote addresses. */ - void attachPppd(String tty, String localAddr, String remoteAddr); + void attachPppd(String tty, String localAddr, String remoteAddr, String dns1Addr, + String dns2Addr); /** * Detaches a PPP server daemon from the specified TTY. diff --git a/core/java/android/provider/Settings.java b/core/java/android/provider/Settings.java index 71280057d3ea6..bacaf439bafe0 100644 --- a/core/java/android/provider/Settings.java +++ b/core/java/android/provider/Settings.java @@ -2943,6 +2943,13 @@ public final class Settings { */ public static final String MOUNT_UMS_NOTIFY_ENABLED = "mount_ums_notify_enabled"; + /** + * Whether or not a notification is displayed when a Tetherable interface is detected. + * (0 = false, 1 = true) + * @hide + */ + public static final String TETHER_NOTIFY = "tether_notify"; + /** * If nonzero, ANRs in invisible background processes bring up a dialog. * Otherwise, the process will be silently killed. diff --git a/core/java/com/android/internal/app/TetherActivity.java b/core/java/com/android/internal/app/TetherActivity.java new file mode 100644 index 0000000000000..2b93dbc8c082e --- /dev/null +++ b/core/java/com/android/internal/app/TetherActivity.java @@ -0,0 +1,151 @@ +/* + * Copyright (C) 2007 Google Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.internal.app; + +import android.app.AlertDialog; +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.DialogInterface; +import android.content.Intent; +import android.content.IntentFilter; +import android.net.ConnectivityManager; +import android.os.Bundle; +import android.os.Handler; +import android.os.IMountService; +import android.os.Message; +import android.os.RemoteException; +import android.os.ServiceManager; +import android.widget.Toast; +import android.util.Log; + +/** + * This activity is shown to the user for him/her to connect/disconnect a Tether + * connection. It will display notification when a suitable connection is made + * to allow the tether to be setup. A second notification will be show when a + * tether is active, allowing the user to manage tethered connections. + */ +public class TetherActivity extends AlertActivity implements + DialogInterface.OnClickListener { + + private static final int POSITIVE_BUTTON = AlertDialog.BUTTON1; + + /* Used to detect when the USB cable is unplugged, so we can call finish() */ + private BroadcastReceiver mTetherReceiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + if (intent.getAction() == ConnectivityManager.ACTION_TETHER_STATE_CHANGED) { + handleTetherStateChanged(intent); + } + } + }; + + private boolean mWantTethering; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + // determine if we advertise tethering or untethering + ConnectivityManager cm = + (ConnectivityManager)getSystemService(Context.CONNECTIVITY_SERVICE); + if (cm.getTetheredIfaces().length > 0) { + mWantTethering = false; + } else if (cm.getTetherableIfaces().length > 0) { + mWantTethering = true; + } else { + finish(); + return; + } + + // Set up the "dialog" + if (mWantTethering == true) { + mAlertParams.mIconId = com.android.internal.R.drawable.ic_dialog_usb; + mAlertParams.mTitle = getString(com.android.internal.R.string.tether_title); + mAlertParams.mMessage = getString(com.android.internal.R.string.tether_message); + mAlertParams.mPositiveButtonText = + getString(com.android.internal.R.string.tether_button); + mAlertParams.mPositiveButtonListener = this; + mAlertParams.mNegativeButtonText = + getString(com.android.internal.R.string.tether_button_cancel); + mAlertParams.mNegativeButtonListener = this; + } else { + mAlertParams.mIconId = com.android.internal.R.drawable.ic_dialog_usb; + mAlertParams.mTitle = getString(com.android.internal.R.string.tether_stop_title); + mAlertParams.mMessage = getString(com.android.internal.R.string.tether_stop_message); + mAlertParams.mPositiveButtonText = + getString(com.android.internal.R.string.tether_stop_button); + mAlertParams.mPositiveButtonListener = this; + mAlertParams.mNegativeButtonText = + getString(com.android.internal.R.string.tether_stop_button_cancel); + mAlertParams.mNegativeButtonListener = this; + } + setupAlert(); + } + + @Override + protected void onResume() { + super.onResume(); + + registerReceiver(mTetherReceiver, new IntentFilter( + ConnectivityManager.ACTION_TETHER_STATE_CHANGED)); + } + + @Override + protected void onPause() { + super.onPause(); + + unregisterReceiver(mTetherReceiver); + } + + /** + * {@inheritDoc} + */ + public void onClick(DialogInterface dialog, int which) { + + if (which == POSITIVE_BUTTON) { + ConnectivityManager connManager = + (ConnectivityManager)getSystemService(Context.CONNECTIVITY_SERVICE); + // start/stop tethering + if (mWantTethering) { + if (!connManager.tether("ppp0")) { + showTetheringError(); + } + } else { + if (!connManager.untether("ppp0")) { + showUnTetheringError(); + } + } + } + // No matter what, finish the activity + finish(); + } + + private void handleTetherStateChanged(Intent intent) { + finish(); + } + + private void showTetheringError() { + Toast.makeText(this, com.android.internal.R.string.tether_error_message, + Toast.LENGTH_LONG).show(); + } + + private void showUnTetheringError() { + Toast.makeText(this, com.android.internal.R.string.tether_stop_error_message, + Toast.LENGTH_LONG).show(); + } + +} diff --git a/core/res/AndroidManifest.xml b/core/res/AndroidManifest.xml index 665088a4c5baf..1406b66459b66 100644 --- a/core/res/AndroidManifest.xml +++ b/core/res/AndroidManifest.xml @@ -1239,6 +1239,10 @@ + + diff --git a/core/res/res/drawable-hdpi/stat_sys_tether_active.png b/core/res/res/drawable-hdpi/stat_sys_tether_active.png new file mode 100755 index 0000000000000000000000000000000000000000..4c14c0743768b6426bbc22d343f2a884f36bf6fa GIT binary patch literal 1191 zcmV;Y1X%ltP)-#Tz=_E;l|51X!J3voQ&$sNSrltY|Y-3}?Cp*1f50R0P zs_%9DQI?mNhuUm5v)yiQV9m4upV*&>h=}E+q@-V)o0|@gJH}RGNls4gXPs$9K|w(Q zLG1dI>U27s#N>j5H;9dm{lS1iR8$l&1Ni*>6jEG3YilbVkw||W8|T2e za89or3k?l5FePIx(%jq(MMXvM{QL}yi;Kc`I2WUn!7DHXguKJ87L~%nLMSaQrRR!@ z3OG7Cg8lt{VSA-b&?|L9P@)_3swUrA}schqKuA?LS9}T@yl#BYYrVk zz~0~AHOD1qpvGUh{n*$TgolT_^c@%&@C}$2k5mFCx6jDPaN{69z7Vjtw>RPqFLiZw zP+3_?&+F^!u)4ac2n;<8$Ke0x=kU~4jbr=@8XFs-tE=m)?R;KZTJpMMBAgNm7swA@ ztE#Hp4DmmCAD&}QRT8B+d;*3cK#5Z#z^bdOVRv_z-tFw{P=@ds7@oOYS5{Ws3>Ov_ zT>9`Fb1ExqD2$*S92~ffjj9Ap(JOiKudS^?OiT>)_V!YHW@g5PUp&ci41>V{wY9aR z(b3VgYq%IC{t0=d8ZYMN=BTV;B?^@|YW;A>Fn4x#c0yQK7*!jSlanwzJF8l;2<@1h zFT6}oPs8BgAhfr)(vr0-zNb!i~=jX}h@$pe}PdPgQGueLa{= zCP++7r1r_l32bd`QH;Z2igIyrLC1b}b|##X*f=_mz%ny4C2!?4M)I1P8fa;0fvl`7 zmv>iJSJ2ed1UEM~nsOye{GNfOr>9F=>U_2k0)qU*oVdNcP4$@8Bz}2$sq@M)EoJrb z@iA2v7@sgK@F~e}6|9ze3>Aw)B)BtHm6erI8~@ibj*wU4myW26kB?JymkYMh6Z`u5 zeBLoPjb!a@85A%jJIp3;+x6ZBf(J?@Isx002ovPDHLk FV1m*nFSq~z literal 0 HcmV?d00001 diff --git a/core/res/res/drawable-hdpi/stat_sys_tether_usb.png b/core/res/res/drawable-hdpi/stat_sys_tether_usb.png new file mode 100755 index 0000000000000000000000000000000000000000..4c14c0743768b6426bbc22d343f2a884f36bf6fa GIT binary patch literal 1191 zcmV;Y1X%ltP)-#Tz=_E;l|51X!J3voQ&$sNSrltY|Y-3}?Cp*1f50R0P zs_%9DQI?mNhuUm5v)yiQV9m4upV*&>h=}E+q@-V)o0|@gJH}RGNls4gXPs$9K|w(Q zLG1dI>U27s#N>j5H;9dm{lS1iR8$l&1Ni*>6jEG3YilbVkw||W8|T2e za89or3k?l5FePIx(%jq(MMXvM{QL}yi;Kc`I2WUn!7DHXguKJ87L~%nLMSaQrRR!@ z3OG7Cg8lt{VSA-b&?|L9P@)_3swUrA}schqKuA?LS9}T@yl#BYYrVk zz~0~AHOD1qpvGUh{n*$TgolT_^c@%&@C}$2k5mFCx6jDPaN{69z7Vjtw>RPqFLiZw zP+3_?&+F^!u)4ac2n;<8$Ke0x=kU~4jbr=@8XFs-tE=m)?R;KZTJpMMBAgNm7swA@ ztE#Hp4DmmCAD&}QRT8B+d;*3cK#5Z#z^bdOVRv_z-tFw{P=@ds7@oOYS5{Ws3>Ov_ zT>9`Fb1ExqD2$*S92~ffjj9Ap(JOiKudS^?OiT>)_V!YHW@g5PUp&ci41>V{wY9aR z(b3VgYq%IC{t0=d8ZYMN=BTV;B?^@|YW;A>Fn4x#c0yQK7*!jSlanwzJF8l;2<@1h zFT6}oPs8BgAhfr)(vr0-zNb!i~=jX}h@$pe}PdPgQGueLa{= zCP++7r1r_l32bd`QH;Z2igIyrLC1b}b|##X*f=_mz%ny4C2!?4M)I1P8fa;0fvl`7 zmv>iJSJ2ed1UEM~nsOye{GNfOr>9F=>U_2k0)qU*oVdNcP4$@8Bz}2$sq@M)EoJrb z@iA2v7@sgK@F~e}6|9ze3>Aw)B)BtHm6erI8~@ibj*wU4myW26kB?JymkYMh6Z`u5 zeBLoPjb!a@85A%jJIp3;+x6ZBf(J?@Isx002ovPDHLk FV1m*nFSq~z literal 0 HcmV?d00001 diff --git a/core/res/res/drawable-mdpi/stat_sys_tether_active.png b/core/res/res/drawable-mdpi/stat_sys_tether_active.png new file mode 100644 index 0000000000000000000000000000000000000000..2d0da4c8ec1b35742584997bb6239721939cd3fb GIT binary patch literal 786 zcmV+t1MU2YP)P000>X1^@s6#OZ}&0000PbVXQnQ*UN; zcVTj606}DLVr3vnZDD6+Qe|Oed2z{QJOBU!v`IukRCwCNRy}AUQ4pRr1|*;{Xu!)I z1frlpC5@zzfFJ~}iHJ=~BZ`&SdDun4>JY6&w6U;BB}Oa`tPVnoOA{eMg~aL%2P%?i zp@~1Yv)szAzSUeHcLT$F^WMyUGxO%nTWT_yzduBoz6Zzp3&k5B8W^T zQ%NKe^ae(w(cDK44-et^_!v;9R4S380Cce5@2hz0Tme%@rwWF{;T&&mZNbse5u=AA z$|OCspFL;MY`8+~cDrG1ZH>{x5#>etToTWMV8|p21OlMf>*4hD6vE*!+}_>_9{y4g zilUYp%_t{K@X8V z9uF9eMuu^YYg`bn%o1z65QJ4i=2i&KxyGzB#bOce?(X3F`Wh@23(x*lk;Gy#==FLG z<9s%f?d@%d$K#;UXrNZB@c~gulWHLdw+l5pr z#W0s<)5z;xySuxSm6esLtkdZrlgWToDut`7D>y$the#yC?B(-$NG6leXf)=k5FM>l zDije!tyc3u{c-1Au zZ@%$Pr;~{aDH@G3XNBM-!u}P000>X1^@s6#OZ}&0000PbVXQnQ*UN; zcVTj606}DLVr3vnZDD6+Qe|Oed2z{QJOBU!v`IukRCwCNRy}AUQ4pRr1|*;{Xu!)I z1frlpC5@zzfFJ~}iHJ=~BZ`&SdDun4>JY6&w6U;BB}Oa`tPVnoOA{eMg~aL%2P%?i zp@~1Yv)szAzSUeHcLT$F^WMyUGxO%nTWT_yzduBoz6Zzp3&k5B8W^T zQ%NKe^ae(w(cDK44-et^_!v;9R4S380Cce5@2hz0Tme%@rwWF{;T&&mZNbse5u=AA z$|OCspFL;MY`8+~cDrG1ZH>{x5#>etToTWMV8|p21OlMf>*4hD6vE*!+}_>_9{y4g zilUYp%_t{K@X8V z9uF9eMuu^YYg`bn%o1z65QJ4i=2i&KxyGzB#bOce?(X3F`Wh@23(x*lk;Gy#==FLG z<9s%f?d@%d$K#;UXrNZB@c~gulWHLdw+l5pr z#W0s<)5z;xySuxSm6esLtkdZrlgWToDut`7D>y$the#yC?B(-$NG6leXf)=k5FM>l zDije!tyc3u{c-1Au zZ@%$Pr;~{aDH@G3XNBM-!u}change network connectivity Allows an application to change - the state network connectivity. + the state of network connectivity. + + + change tethered connectivity + + Allows an application to change + the state of tethered network connectivity. change background data usage setting @@ -2200,4 +2206,40 @@ Used by AccessibilityService to announce the purpose of the view. --> favorite + + + + + + USB tethering available + + Select \"Tether\" if you want to share your phone\'s data connection with your computer. + + Tether + + Cancel + + There is a problem tethering. + + USB tethering available + + Select to tether your computer to your phone. + + Untether + + Select to untether your computer. + + + + + Disconnect tethering + + You have been sharing your phone\'s cellular data connection with your computer. Select \"Disconnect\" to disconnect USB tethering. + + Disconnect + + Cancel + + We\'ve encountered a problem turning off Tethering. Please try again. + diff --git a/services/java/com/android/server/ConnectivityService.java b/services/java/com/android/server/ConnectivityService.java index aa4956f1573ee..42590160d99a4 100644 --- a/services/java/com/android/server/ConnectivityService.java +++ b/services/java/com/android/server/ConnectivityService.java @@ -43,6 +43,8 @@ import android.util.Log; import com.android.internal.telephony.Phone; +import com.android.server.connectivity.Tethering; + import java.io.FileDescriptor; import java.io.PrintWriter; import java.util.ArrayList; @@ -62,6 +64,9 @@ public class ConnectivityService extends IConnectivityManager.Stub { private static final String NETWORK_RESTORE_DELAY_PROP_NAME = "android.telephony.apn-restore"; + + private Tethering mTethering; + /** * Sometimes we want to refer to the individual network state * trackers separately, and sometimes we just want to treat them @@ -308,6 +313,8 @@ public class ConnectivityService extends IConnectivityManager.Stub { continue; } } + + mTethering = new Tethering(mContext); } @@ -784,6 +791,13 @@ public class ConnectivityService extends IConnectivityManager.Stub { "ConnectivityService"); } + // TODO Make this a special check when it goes public + private void enforceTetherChangePermission() { + mContext.enforceCallingOrSelfPermission( + android.Manifest.permission.CHANGE_NETWORK_STATE, + "ConnectivityService"); + } + /** * Handle a {@code DISCONNECTED} event. If this pertains to the non-active * network, we ignore it. If it is for the active network, we send out a @@ -1368,4 +1382,28 @@ public class ConnectivityService extends IConnectivityManager.Stub { } } } + + // javadoc from interface + public boolean tether(String iface) { + enforceTetherChangePermission(); + return mTethering.tether(iface); + } + + // javadoc from interface + public boolean untether(String iface) { + enforceTetherChangePermission(); + return mTethering.untether(iface); + } + + // TODO - move iface listing, queries, etc to new module + // javadoc from interface + public String[] getTetherableIfaces() { + enforceAccessPermission(); + return mTethering.getTetherableIfaces(); + } + + public String[] getTetheredIfaces() { + enforceAccessPermission(); + return mTethering.getTetheredIfaces(); + } } diff --git a/services/java/com/android/server/NetworkManagementService.java b/services/java/com/android/server/NetworkManagementService.java index b34b50aae63dd..d41aacfd81775 100644 --- a/services/java/com/android/server/NetworkManagementService.java +++ b/services/java/com/android/server/NetworkManagementService.java @@ -334,9 +334,9 @@ class NetworkManagementService extends INetworkManagementService.Stub { mContext.enforceCallingOrSelfPermission( android.Manifest.permission.CHANGE_NETWORK_STATE, "NetworkManagementService"); try { - String cmd = "tether dns set "; + String cmd = "tether dns set"; for (String s : dns) { - cmd += InetAddress.getByName(s).toString() + " "; + cmd += " " + InetAddress.getByName(s).getHostAddress(); } mConnector.doCommand(cmd); } catch (UnknownHostException e) { @@ -373,14 +373,16 @@ class NetworkManagementService extends INetworkManagementService.Stub { return mConnector.doListCommand("list_ttys", NetdResponseCode.TtyListResult); } - public void attachPppd(String tty, String localAddr, String remoteAddr) - throws IllegalStateException { + public void attachPppd(String tty, String localAddr, String remoteAddr, String dns1Addr, + String dns2Addr) throws IllegalStateException { try { mContext.enforceCallingOrSelfPermission( android.Manifest.permission.CHANGE_NETWORK_STATE, "NetworkManagementService"); - mConnector.doCommand(String.format("pppd attach %s %s %s", tty, - InetAddress.getByName(localAddr).toString(), - InetAddress.getByName(localAddr).toString())); + mConnector.doCommand(String.format("pppd attach %s %s %s %s %s", tty, + InetAddress.getByName(localAddr).getHostAddress(), + InetAddress.getByName(remoteAddr).getHostAddress(), + InetAddress.getByName(dns1Addr).getHostAddress(), + InetAddress.getByName(dns2Addr).getHostAddress())); } catch (UnknownHostException e) { throw new IllegalStateException("Error resolving addr", e); } @@ -392,4 +394,3 @@ class NetworkManagementService extends INetworkManagementService.Stub { mConnector.doCommand(String.format("pppd detach %s", tty)); } } - diff --git a/services/java/com/android/server/connectivity/Tethering.java b/services/java/com/android/server/connectivity/Tethering.java new file mode 100644 index 0000000000000..f6853839a66f0 --- /dev/null +++ b/services/java/com/android/server/connectivity/Tethering.java @@ -0,0 +1,483 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.server.connectivity; + +import android.app.Notification; +import android.app.NotificationManager; +import android.app.PendingIntent; +import android.content.BroadcastReceiver; +import android.content.ContentResolver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.content.res.Resources; +import android.net.ConnectivityManager; +import android.net.INetworkManagementEventObserver; +import android.os.IBinder; +import android.os.INetworkManagementService; +import android.os.RemoteException; +import android.os.ServiceManager; +import android.provider.Settings; +import android.util.Log; + +import java.util.ArrayList; +/** + * @hide + */ +public class Tethering extends INetworkManagementEventObserver.Stub { + + private Notification mTetheringNotification; + private Context mContext; + private final String TAG = "Tethering"; + + private boolean mPlaySounds = false; + + private ArrayList mAvailableIfaces; + private ArrayList mActiveIfaces; + + private ArrayList mActiveTtys; + + private BroadcastReceiver mStateReceiver; + + public Tethering(Context context) { + Log.d(TAG, "Tethering starting"); + mContext = context; + + // register for notifications from NetworkManagement Service + IBinder b = ServiceManager.getService(Context.NETWORKMANAGEMENT_SERVICE); + INetworkManagementService service = INetworkManagementService.Stub.asInterface(b); + try { + service.registerObserver(this); + } catch (RemoteException e) { + Log.e(TAG, "Error registering observer :" + e); + } + + mAvailableIfaces = new ArrayList(); + mActiveIfaces = new ArrayList(); + mActiveTtys = new ArrayList(); + + // TODO - remove this hack after real USB connections are detected. + IntentFilter filter = new IntentFilter(); + filter.addAction(Intent.ACTION_UMS_DISCONNECTED); + filter.addAction(Intent.ACTION_UMS_CONNECTED); + mStateReceiver = new UMSStateReceiver(); + mContext.registerReceiver(mStateReceiver, filter); + } + + public synchronized void interfaceLinkStatusChanged(String iface, boolean link) { + Log.d(TAG, "interfaceLinkStatusChanged " + iface + ", " + link); + } + + public synchronized void interfaceAdded(String iface) { + if (mActiveIfaces.contains(iface)) { + Log.e(TAG, "active iface (" + iface + ") reported as added, ignoring"); + return; + } + if (mAvailableIfaces.contains(iface)) { + Log.e(TAG, "available iface (" + iface + ") readded, ignoring"); + return; + } + mAvailableIfaces.add(iface); + Log.d(TAG, "interfaceAdded :" + iface); + sendTetherStateChangedBroadcast(); + } + + public synchronized void interfaceRemoved(String iface) { + if (mActiveIfaces.contains(iface)) { + Log.d(TAG, "removed an active iface (" + iface + ")"); + untether(iface); + } + if (mAvailableIfaces.contains(iface)) { + mAvailableIfaces.remove(iface); + Log.d(TAG, "interfaceRemoved " + iface); + sendTetherStateChangedBroadcast(); + } + } + + public synchronized boolean tether(String iface) { + Log.d(TAG, "Tethering " + iface); + + if (!mAvailableIfaces.contains(iface)) { + Log.e(TAG, "Tried to Tether an unavailable iface :" + iface + ", ignoring"); + return false; + } + if (mActiveIfaces.contains(iface)) { + Log.e(TAG, "Tried to Tether an already Tethered iface :" + iface + ", ignoring"); + return false; + } + + IBinder b = ServiceManager.getService(Context.NETWORKMANAGEMENT_SERVICE); + INetworkManagementService service = INetworkManagementService.Stub.asInterface(b); + + if (mActiveIfaces.size() == 0) { + try { + service.setIpForwardingEnabled(true); + } catch (Exception e) { + Log.e(TAG, "Error in setIpForwardingEnabled(true) :" + e); + return false; + } + + try { + // TODO - don't hardcode this - though with non-conf values (un-routable) + // maybe it's not a big deal + service.startTethering("169.254.2.1", "169.254.2.64"); + } catch (Exception e) { + Log.e(TAG, "Error in startTethering :" + e); + try { + service.setIpForwardingEnabled(false); + } catch (Exception ee) {} + return false; + } + + try { + // TODO - maybe use the current connection's dns servers for this + String[] dns = new String[2]; + dns[0] = new String("8.8.8.8"); + dns[1] = new String("4.2.2.2"); + service.setDnsForwarders(dns); + } catch (Exception e) { + Log.e(TAG, "Error in setDnsForwarders :" + e); + try { + service.stopTethering(); + } catch (Exception ee) {} + try { + service.setIpForwardingEnabled(false); + } catch (Exception ee) {} + } + } + + try { + service.tetherInterface(iface); + } catch (Exception e) { + Log.e(TAG, "Error in tetherInterface :" + e); + if (mActiveIfaces.size() == 0) { + try { + service.stopTethering(); + } catch (Exception ee) {} + try { + service.setIpForwardingEnabled(false); + } catch (Exception ee) {} + } + return false; + } + + try { + // TODO - use the currently active external iface + service.enableNat (iface, "rmnet0"); + } catch (Exception e) { + Log.e(TAG, "Error in enableNat :" + e); + try { + service.untetherInterface(iface); + } catch (Exception ee) {} + if (mActiveIfaces.size() == 0) { + try { + service.stopTethering(); + } catch (Exception ee) {} + try { + service.setIpForwardingEnabled(false); + } catch (Exception ee) {} + } + return false; + } + mAvailableIfaces.remove(iface); + mActiveIfaces.add(iface); + Log.d(TAG, "Tethered " + iface); + sendTetherStateChangedBroadcast(); + return true; + } + + public synchronized boolean untether(String iface) { + Log.d(TAG, "Untethering " + iface); + + if (mAvailableIfaces.contains(iface)) { + Log.e(TAG, "Tried to Untether an available iface :" + iface); + return false; + } + if (!mActiveIfaces.contains(iface)) { + Log.e(TAG, "Tried to Untether an inactive iface :" + iface); + return false; + } + + IBinder b = ServiceManager.getService(Context.NETWORKMANAGEMENT_SERVICE); + INetworkManagementService service = INetworkManagementService.Stub.asInterface(b); + + // none of these errors are recoverable - ie, multiple calls won't help + // and the user can't do anything. Basically a reboot is required and probably + // the device is misconfigured or something bad has happend. + // Because of this, we should try to unroll as much state as we can. + try { + service.disableNat(iface, "rmnet0"); + } catch (Exception e) { + Log.e(TAG, "Error in disableNat :" + e); + } + try { + service.untetherInterface(iface); + } catch (Exception e) { + Log.e(TAG, "Error untethering " + iface + ", :" + e); + } + mActiveIfaces.remove(iface); + mAvailableIfaces.add(iface); + + if (mActiveIfaces.size() == 0) { + Log.d(TAG, "no active tethers - turning down dhcp/ipforward"); + try { + service.stopTethering(); + } catch (Exception e) { + Log.e(TAG, "Error in stopTethering :" + e); + } + try { + service.setIpForwardingEnabled(false); + } catch (Exception e) { + Log.e(TAG, "Error in setIpForwardingEnabled(false) :" + e); + } + } + sendTetherStateChangedBroadcast(); + Log.d(TAG, "Untethered " + iface); + return true; + } + + private void sendTetherStateChangedBroadcast() { + Intent broadcast = new Intent(ConnectivityManager.ACTION_TETHER_STATE_CHANGED); + broadcast.addFlags(Intent.FLAG_RECEIVER_REPLACE_PENDING); + broadcast.putExtra(ConnectivityManager.EXTRA_AVAILABLE_TETHER_COUNT, + mAvailableIfaces.size()); + broadcast.putExtra(ConnectivityManager.EXTRA_ACTIVE_TETHER_COUNT, mActiveIfaces.size()); + mContext.sendBroadcast(broadcast); + + // for USB we only have the one, so don't have to deal with additional + if (mAvailableIfaces.size() > 0) { + // Check if the user wants to be bothered + boolean tellUser = (Settings.Secure.getInt(mContext.getContentResolver(), + Settings.Secure.TETHER_NOTIFY, 0) == 1); + + if (tellUser) { + showTetherAvailableNotification(); + } + } else if (mActiveIfaces.size() > 0) { + showTetheredNotification(); + } else { + clearNotification(); + } + } + + private void showTetherAvailableNotification() { + NotificationManager notificationManager = (NotificationManager)mContext. + getSystemService(Context.NOTIFICATION_SERVICE); + if (notificationManager == null) { + return; + } + + Intent intent = new Intent(); + intent.setClass(mContext, com.android.internal.app.TetherActivity.class); + + intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + PendingIntent pi = PendingIntent.getActivity(mContext, 0, intent, 0); + + Resources r = Resources.getSystem(); + CharSequence title = r.getText(com.android.internal.R.string. + tether_available_notification_title); + CharSequence message = r.getText(com.android.internal.R.string. + tether_available_notification_message); + + if(mTetheringNotification == null) { + mTetheringNotification = new Notification(); + mTetheringNotification.when = 0; + } + mTetheringNotification.icon = com.android.internal.R.drawable.stat_sys_tether_usb; + + boolean playSounds = false; + //playSounds = SystemProperties.get("persist.service.mount.playsnd", "1").equals("1"); + if (playSounds) { + mTetheringNotification.defaults |= Notification.DEFAULT_SOUND; + } else { + mTetheringNotification.defaults &= ~Notification.DEFAULT_SOUND; + } + + mTetheringNotification.flags = Notification.FLAG_ONGOING_EVENT; + mTetheringNotification.tickerText = title; + mTetheringNotification.setLatestEventInfo(mContext, title, message, pi); + + notificationManager.notify(mTetheringNotification.icon, mTetheringNotification); + + } + + private void showTetheredNotification() { + NotificationManager notificationManager = (NotificationManager)mContext. + getSystemService(Context.NOTIFICATION_SERVICE); + if (notificationManager == null) { + return; + } + + Intent intent = new Intent(); + intent.setClass(mContext, com.android.internal.app.TetherActivity.class); + + intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + PendingIntent pi = PendingIntent.getActivity(mContext, 0, intent, 0); + + Resources r = Resources.getSystem(); + CharSequence title = r.getText(com.android.internal.R.string. + tether_stop_notification_title); + CharSequence message = r.getText(com.android.internal.R.string. + tether_stop_notification_message); + + if(mTetheringNotification == null) { + mTetheringNotification = new Notification(); + mTetheringNotification.when = 0; + } + mTetheringNotification.icon = com.android.internal.R.drawable.stat_sys_tether_usb; + + boolean playSounds = false; + //playSounds = SystemProperties.get("persist.service.mount.playsnd", "1").equals("1"); + if (playSounds) { + mTetheringNotification.defaults |= Notification.DEFAULT_SOUND; + } else { + mTetheringNotification.defaults &= ~Notification.DEFAULT_SOUND; + } + + mTetheringNotification.flags = Notification.FLAG_ONGOING_EVENT; + mTetheringNotification.tickerText = title; + mTetheringNotification.setLatestEventInfo(mContext, title, message, pi); + + notificationManager.notify(mTetheringNotification.icon, mTetheringNotification); + } + + private void clearNotification() { + NotificationManager notificationManager = (NotificationManager)mContext. + getSystemService(Context.NOTIFICATION_SERVICE); + if (notificationManager != null && mTetheringNotification != null) { + notificationManager.cancel(mTetheringNotification.icon); + mTetheringNotification = null; + } + } + + + + +// TODO - remove this hack after we get proper USB detection + private class UMSStateReceiver extends BroadcastReceiver { + public void onReceive(Context content, Intent intent) { + if (intent.getAction().equals(Intent.ACTION_UMS_CONNECTED)) { + Tethering.this.handleTtyConnect(); + } else if (intent.getAction().equals(Intent.ACTION_UMS_DISCONNECTED)) { + Tethering.this.handleTtyDisconnect(); + } + } + } + + private synchronized void handleTtyConnect() { + Log.d(TAG, "handleTtyConnect"); + // for each of the available Tty not already supported by a ppp session, + // create a ppp session + // TODO - this should be data-driven rather than hard coded. + String[] allowedTtys = new String[1]; + allowedTtys[0] = new String("ttyGS0"); + + IBinder b = ServiceManager.getService(Context.NETWORKMANAGEMENT_SERVICE); + INetworkManagementService service = INetworkManagementService.Stub.asInterface(b); + + String[] availableTtys; + try { + availableTtys = service.listTtys(); + } catch (RemoteException e) { + Log.e(TAG, "error listing Ttys :" + e); + return; + } + + for (String tty : availableTtys) { + for (String pattern : allowedTtys) { + if (tty.matches(pattern)) { + synchronized (this) { + if (!mActiveTtys.contains(tty)) { + // TODO - don't hardcode this + try { + // local, remote, dns + service.attachPppd(tty, "169.254.1.128", "169.254.1.1", + "169.254.1.128", "0.0.0.0"); + } catch (Exception e) { + Log.e(TAG, "error calling attachPppd: " + e); + return; + } + Log.d(TAG, "started Pppd on tty " + tty); + mActiveTtys.add(tty); + // TODO - remove this after we detect the new iface + interfaceAdded("ppp0"); + } + } + } + } + } + } + + private synchronized void handleTtyDisconnect() { + Log.d(TAG, "handleTtyDisconnect"); + + // TODO - this should be data-driven rather than hard coded. + String[] allowedTtys = new String[1]; + allowedTtys[0] = new String("ttyGS0"); + + IBinder b = ServiceManager.getService(Context.NETWORKMANAGEMENT_SERVICE); + INetworkManagementService service = INetworkManagementService.Stub.asInterface(b); + + String[] availableTtys; + try { + availableTtys = service.listTtys(); + } catch (RemoteException e) { + Log.e(TAG, "error listing Ttys :" + e); + return; + } + + for (String tty : availableTtys) { + for (String pattern : allowedTtys) { + if (tty.matches(pattern)) { + synchronized (this) { + if (mActiveTtys.contains(tty)) { + try { + service.detachPppd(tty); + } catch (Exception e) { + Log.e(TAG, "error calling detachPppd on " + tty + " :" + e); + } + mActiveTtys.remove(tty); + // TODO - remove this after we detect the new iface + interfaceRemoved("ppp0"); + return; + } + } + } + } + } + } + + public synchronized String[] getTetheredIfaces() { + int size = mActiveIfaces.size(); + String[] result = new String[size]; + size -= 1; + for (int i=0; i< size; i++) { + result[i] = mActiveIfaces.get(i); + } + return result; + } + + public synchronized String[] getTetherableIfaces() { + int size = mAvailableIfaces.size(); + String[] result = new String[size]; + size -= 1; + for (int i=0; i< size; i++) { + result[i] = mActiveIfaces.get(i); + } + return result; + } +}