Improve Bluetooth tethering UI usability.

- Updated hint text for BT tethering checkbox to
  "[Sharing|not sharing] this [tablet|phone]'s mobile data connection".
- Show correct hint text when user enters tethering screen.
- Show correct status after user enables tethering when Bluetooth is off.
  When BluetoothPan.setBluetoothTethering(true) is called with BT off,
  BluetoothPanProfileHandler will add a broadcast receiver to enable
  tethering after BT turns on. This happens too late to show the correct
  status when TetherSettings gets the adapter state changed event, so set
  a flag (mBluetoothEnableForTether) instead, and call setBluetoothTethering
  ourselves after the state changes to ON. Also, clear the flag if the
  adapter state changes to OFF or ERROR.
- Show correct status when user enables tethering, then disables Bluetooth,
  then returns to the tethering screen. Previously it would show
  Bluetooth tethering enabled, even though adapter state was OFF.
- Show the number of connected devices in tethering preference screen.
- Distinguish between PANU and NAP in device profiles screen, and show
  appropriate text to clarify the direction of tethering.
- Remove profiles from device profiles list when the device removes the UUID
  (e.g. Mac OS X turning NAP on/off) and after a NAP disconnection when the
  remote device only supports PANU.

Bug: 3414575
Change-Id: I2c0830876d5b9bddb293e57c4d3ca74f105911b8
diff --git a/src/com/android/settings/TetherSettings.java b/src/com/android/settings/TetherSettings.java
index 39f0535..1513d43 100644
--- a/src/com/android/settings/TetherSettings.java
+++ b/src/com/android/settings/TetherSettings.java
@@ -95,6 +95,8 @@
     private WifiManager mWifiManager;
     private WifiConfiguration mWifiConfig = null;
 
+    private boolean mBluetoothEnableForTether;
+
     @Override
     public void onCreate(Bundle icicle) {
         super.onCreate(icicle);
@@ -239,7 +241,8 @@
     private class TetherChangeReceiver extends BroadcastReceiver {
         @Override
         public void onReceive(Context content, Intent intent) {
-            if (intent.getAction().equals(ConnectivityManager.ACTION_TETHER_STATE_CHANGED)) {
+            String action = intent.getAction();
+            if (action.equals(ConnectivityManager.ACTION_TETHER_STATE_CHANGED)) {
                 // TODO - this should understand the interface types
                 ArrayList<String> available = intent.getStringArrayListExtra(
                         ConnectivityManager.EXTRA_AVAILABLE_TETHER);
@@ -250,10 +253,27 @@
                 updateState(available.toArray(new String[available.size()]),
                         active.toArray(new String[active.size()]),
                         errored.toArray(new String[errored.size()]));
-            } else if (intent.getAction().equals(Intent.ACTION_MEDIA_SHARED) ||
-                       intent.getAction().equals(Intent.ACTION_MEDIA_UNSHARED)) {
+            } else if (action.equals(Intent.ACTION_MEDIA_SHARED) ||
+                       action.equals(Intent.ACTION_MEDIA_UNSHARED)) {
                 updateState();
-            } else if (intent.getAction().equals(BluetoothAdapter.ACTION_STATE_CHANGED)) {
+            } else if (action.equals(BluetoothAdapter.ACTION_STATE_CHANGED)) {
+                if (mBluetoothEnableForTether) {
+                    switch (intent
+                            .getIntExtra(BluetoothAdapter.EXTRA_STATE, BluetoothAdapter.ERROR)) {
+                        case BluetoothAdapter.STATE_ON:
+                            mBluetoothPan.setBluetoothTethering(true);
+                            mBluetoothEnableForTether = false;
+                            break;
+
+                        case BluetoothAdapter.STATE_OFF:
+                        case BluetoothAdapter.ERROR:
+                            mBluetoothEnableForTether = false;
+                            break;
+
+                        default:
+                            // ignore transition states
+                    }
+                }
                 updateState();
             }
         }
@@ -281,6 +301,8 @@
 
         if (intent != null) mTetherChangeReceiver.onReceive(activity, intent);
         mWifiApEnabler.resume();
+
+        updateState();
     }
 
     @Override
@@ -368,22 +390,10 @@
 
     private void updateBluetoothState(String[] available, String[] tethered,
             String[] errored) {
-        ConnectivityManager cm =
-                (ConnectivityManager)getSystemService(Context.CONNECTIVITY_SERVICE);
-        int bluetoothError = ConnectivityManager.TETHER_ERROR_NO_ERROR;
-        for (String s : available) {
-            for (String regex : mBluetoothRegexs) {
-                if (s.matches(regex)) {
-                    if (bluetoothError == ConnectivityManager.TETHER_ERROR_NO_ERROR) {
-                        bluetoothError = cm.getLastTetherError(s);
-                    }
-                }
-            }
-        }
-        boolean bluetoothTethered = false;
+        int bluetoothTethered = 0;
         for (String s : tethered) {
             for (String regex : mBluetoothRegexs) {
-                if (s.matches(regex)) bluetoothTethered = true;
+                if (s.matches(regex)) bluetoothTethered++;
             }
         }
         boolean bluetoothErrored = false;
@@ -401,17 +411,19 @@
         } else if (btState == BluetoothAdapter.STATE_TURNING_ON) {
             mBluetoothTether.setEnabled(false);
             mBluetoothTether.setSummary(R.string.bluetooth_turning_on);
-        } else if (mBluetoothPan.isTetheringOn()) {
+        } else if (btState == BluetoothAdapter.STATE_ON && mBluetoothPan.isTetheringOn()) {
             mBluetoothTether.setChecked(true);
-            if (btState == BluetoothAdapter.STATE_ON) {
-                mBluetoothTether.setEnabled(true);
-                if (bluetoothTethered) {
-                    mBluetoothTether.setSummary(R.string.bluetooth_tethering_connected_subtext);
-                } else if (bluetoothErrored) {
-                    mBluetoothTether.setSummary(R.string.bluetooth_tethering_errored_subtext);
-                } else {
-                    mBluetoothTether.setSummary(R.string.bluetooth_tethering_available_subtext);
-                }
+            mBluetoothTether.setEnabled(true);
+            if (bluetoothTethered > 1) {
+                String summary = getString(
+                        R.string.bluetooth_tethering_devices_connected_subtext, bluetoothTethered);
+                mBluetoothTether.setSummary(summary);
+            } else if (bluetoothTethered == 1) {
+                mBluetoothTether.setSummary(R.string.bluetooth_tethering_device_connected_subtext);
+            } else if (bluetoothErrored) {
+                mBluetoothTether.setSummary(R.string.bluetooth_tethering_errored_subtext);
+            } else {
+                mBluetoothTether.setSummary(R.string.bluetooth_tethering_available_subtext);
             }
         } else {
             mBluetoothTether.setEnabled(true);
@@ -456,20 +468,21 @@
                 }
                 mUsbTether.setSummary("");
             }
-        } else if(preference == mBluetoothTether) {
+        } else if (preference == mBluetoothTether) {
             boolean bluetoothTetherState = mBluetoothTether.isChecked();
 
             if (bluetoothTetherState) {
                 // turn on Bluetooth first
                 BluetoothAdapter adapter = BluetoothAdapter.getDefaultAdapter();
                 if (adapter.getState() == BluetoothAdapter.STATE_OFF) {
+                    mBluetoothEnableForTether = true;
                     adapter.enable();
                     mBluetoothTether.setSummary(R.string.bluetooth_turning_on);
                     mBluetoothTether.setEnabled(false);
+                } else {
+                    mBluetoothPan.setBluetoothTethering(true);
+                    mBluetoothTether.setSummary(R.string.bluetooth_tethering_available_subtext);
                 }
-
-                mBluetoothPan.setBluetoothTethering(true);
-                mBluetoothTether.setSummary(R.string.bluetooth_tethering_available_subtext);
             } else {
                 boolean errored = false;
 
diff --git a/src/com/android/settings/bluetooth/A2dpProfile.java b/src/com/android/settings/bluetooth/A2dpProfile.java
index 96225d8..e8582f3 100644
--- a/src/com/android/settings/bluetooth/A2dpProfile.java
+++ b/src/com/android/settings/bluetooth/A2dpProfile.java
@@ -142,7 +142,7 @@
         return R.string.bluetooth_profile_a2dp;
     }
 
-    public int getDisconnectResource() {
+    public int getDisconnectResource(BluetoothDevice device) {
         return R.string.bluetooth_disconnect_a2dp_profile;
     }
 
diff --git a/src/com/android/settings/bluetooth/CachedBluetoothDevice.java b/src/com/android/settings/bluetooth/CachedBluetoothDevice.java
index 56e96b4..71a5c01 100644
--- a/src/com/android/settings/bluetooth/CachedBluetoothDevice.java
+++ b/src/com/android/settings/bluetooth/CachedBluetoothDevice.java
@@ -51,6 +51,13 @@
     private final List<LocalBluetoothProfile> mProfiles =
             new ArrayList<LocalBluetoothProfile>();
 
+    // List of profiles that were previously in mProfiles, but have been removed
+    private final List<LocalBluetoothProfile> mRemovedProfiles =
+            new ArrayList<LocalBluetoothProfile>();
+
+    // Device supports PANU but not NAP: remove PanProfile after device disconnects from NAP
+    private boolean mLocalNapRoleConnected;
+
     private boolean mVisible;
 
     private final Collection<Callback> mCallbacks = new ArrayList<Callback>();
@@ -100,8 +107,21 @@
         mProfileConnectionState.put(profile, newProfileState);
         if (newProfileState == BluetoothProfile.STATE_CONNECTED) {
             if (!mProfiles.contains(profile)) {
+                mRemovedProfiles.remove(profile);
                 mProfiles.add(profile);
+                if (profile instanceof PanProfile &&
+                        ((PanProfile) profile).isLocalRoleNap(mDevice)) {
+                    // Device doesn't support NAP, so remove PanProfile on disconnect
+                    mLocalNapRoleConnected = true;
+                }
             }
+        } else if (mLocalNapRoleConnected && profile instanceof PanProfile &&
+                ((PanProfile) profile).isLocalRoleNap(mDevice) &&
+                newProfileState == BluetoothProfile.STATE_DISCONNECTED) {
+            Log.d(TAG, "Removing PanProfile from device after NAP disconnect");
+            mProfiles.remove(profile);
+            mRemovedProfiles.add(profile);
+            mLocalNapRoleConnected = false;
         }
     }
 
@@ -391,7 +411,7 @@
         ParcelUuid[] localUuids = mLocalAdapter.getUuids();
         if (localUuids == null) return false;
 
-        mProfileManager.updateProfiles(uuids, localUuids, mProfiles);
+        mProfileManager.updateProfiles(uuids, localUuids, mProfiles, mRemovedProfiles);
 
         if (DEBUG) {
             Log.e(TAG, "updating profiles for " + mDevice.getName());
@@ -482,6 +502,10 @@
         return connectableProfiles;
     }
 
+    List<LocalBluetoothProfile> getRemovedProfiles() {
+        return mRemovedProfiles;
+    }
+
     void registerCallback(Callback callback) {
         synchronized (mCallbacks) {
             mCallbacks.add(callback);
diff --git a/src/com/android/settings/bluetooth/DeviceProfilesSettings.java b/src/com/android/settings/bluetooth/DeviceProfilesSettings.java
index 307125c..9db4baf 100644
--- a/src/com/android/settings/bluetooth/DeviceProfilesSettings.java
+++ b/src/com/android/settings/bluetooth/DeviceProfilesSettings.java
@@ -239,7 +239,7 @@
         if (TextUtils.isEmpty(name)) {
             name = context.getString(R.string.bluetooth_device);
         }
-        int disconnectMessage = profile.getDisconnectResource();
+        int disconnectMessage = profile.getDisconnectResource(device.getDevice());
         if (disconnectMessage == 0) {
             Log.w(TAG, "askDisconnect: unexpected profile " + profile);
             disconnectMessage = R.string.bluetooth_disconnect_blank;
@@ -288,6 +288,13 @@
                 refreshProfilePreference(profilePref, profile);
             }
         }
+        for (LocalBluetoothProfile profile : mCachedDevice.getRemovedProfiles()) {
+            Preference profilePref = findPreference(profile.toString());
+            if (profilePref != null) {
+                Log.d(TAG, "Removing " + profile.toString() + " from profile list");
+                mProfileContainer.removePreference(profilePref);
+            }
+        }
     }
 
     private void refreshProfilePreference(Preference profilePref, LocalBluetoothProfile profile) {
diff --git a/src/com/android/settings/bluetooth/HeadsetProfile.java b/src/com/android/settings/bluetooth/HeadsetProfile.java
index e9c52ef..13dce33 100644
--- a/src/com/android/settings/bluetooth/HeadsetProfile.java
+++ b/src/com/android/settings/bluetooth/HeadsetProfile.java
@@ -22,13 +22,11 @@
 import android.bluetooth.BluetoothProfile;
 import android.bluetooth.BluetoothUuid;
 import android.content.Context;
-import android.os.Handler;
 import android.os.ParcelUuid;
 import android.util.Log;
 
 import com.android.settings.R;
 
-import java.util.ArrayList;
 import java.util.List;
 
 /**
@@ -176,7 +174,7 @@
         return R.string.bluetooth_profile_headset;
     }
 
-    public int getDisconnectResource() {
+    public int getDisconnectResource(BluetoothDevice device) {
         return R.string.bluetooth_disconnect_headset_profile;
     }
 
diff --git a/src/com/android/settings/bluetooth/HidProfile.java b/src/com/android/settings/bluetooth/HidProfile.java
index 9185059..13d3db9 100644
--- a/src/com/android/settings/bluetooth/HidProfile.java
+++ b/src/com/android/settings/bluetooth/HidProfile.java
@@ -116,7 +116,7 @@
         return R.string.bluetooth_profile_hid;
     }
 
-    public int getDisconnectResource() {
+    public int getDisconnectResource(BluetoothDevice device) {
         return R.string.bluetooth_disconnect_hid_profile;
     }
 
diff --git a/src/com/android/settings/bluetooth/LocalBluetoothProfile.java b/src/com/android/settings/bluetooth/LocalBluetoothProfile.java
index 936231a..878a032 100644
--- a/src/com/android/settings/bluetooth/LocalBluetoothProfile.java
+++ b/src/com/android/settings/bluetooth/LocalBluetoothProfile.java
@@ -60,8 +60,9 @@
     /**
      * Returns the string resource ID for the disconnect confirmation text
      * for this profile.
+     * @param device
      */
-    int getDisconnectResource();
+    int getDisconnectResource(BluetoothDevice device);
 
     /**
      * Returns the string resource ID for the summary text for this profile
diff --git a/src/com/android/settings/bluetooth/LocalBluetoothProfileManager.java b/src/com/android/settings/bluetooth/LocalBluetoothProfileManager.java
index ee3cb66..f3143f0 100644
--- a/src/com/android/settings/bluetooth/LocalBluetoothProfileManager.java
+++ b/src/com/android/settings/bluetooth/LocalBluetoothProfileManager.java
@@ -110,7 +110,9 @@
                 BluetoothInputDevice.ACTION_CONNECTION_STATE_CHANGED);
 
         mPanProfile = new PanProfile(context);
-        addProfile(mPanProfile, PanProfile.NAME, BluetoothPan.ACTION_CONNECTION_STATE_CHANGED);
+        addPanProfile(mPanProfile, PanProfile.NAME,
+                BluetoothPan.ACTION_CONNECTION_STATE_CHANGED);
+
         Log.d(TAG, "LocalBluetoothProfileManager construction complete");
     }
 
@@ -173,6 +175,13 @@
         mProfileNameMap.put(profileName, profile);
     }
 
+    private void addPanProfile(LocalBluetoothProfile profile,
+            String profileName, String stateChangedAction) {
+        mEventManager.addProfileHandler(stateChangedAction,
+                new PanStateChangedHandler(profile));
+        mProfileNameMap.put(profileName, profile);
+    }
+
     LocalBluetoothProfile getProfileByName(String name) {
         return mProfileNameMap.get(name);
     }
@@ -190,7 +199,7 @@
      * Generic handler for connection state change events for the specified profile.
      */
     private class StateChangedHandler implements BluetoothEventManager.Handler {
-        private final LocalBluetoothProfile mProfile;
+        final LocalBluetoothProfile mProfile;
 
         StateChangedHandler(LocalBluetoothProfile profile) {
             mProfile = profile;
@@ -215,6 +224,22 @@
         }
     }
 
+    /** State change handler for NAP and PANU profiles. */
+    private class PanStateChangedHandler extends StateChangedHandler {
+
+        PanStateChangedHandler(LocalBluetoothProfile profile) {
+            super(profile);
+        }
+
+        @Override
+        public void onReceive(Context context, Intent intent, BluetoothDevice device) {
+            PanProfile panProfile = (PanProfile) mProfile;
+            int role = intent.getIntExtra(BluetoothPan.EXTRA_LOCAL_ROLE, 0);
+            panProfile.setLocalRole(device, role);
+            super.onReceive(context, intent, device);
+        }
+    }
+
     // called from DockService
     void addServiceListener(ServiceListener l) {
         mServiceListeners.add(l);
@@ -269,9 +294,14 @@
      * @param uuids of the remote device
      * @param localUuids UUIDs of the local device
      * @param profiles The list of profiles to fill
+     * @param removedProfiles list of profiles that were removed
      */
     synchronized void updateProfiles(ParcelUuid[] uuids, ParcelUuid[] localUuids,
-        Collection<LocalBluetoothProfile> profiles) {
+            Collection<LocalBluetoothProfile> profiles,
+            Collection<LocalBluetoothProfile> removedProfiles) {
+        // Copy previous profile list into removedProfiles
+        removedProfiles.clear();
+        removedProfiles.addAll(profiles);
         profiles.clear();
 
         if (uuids == null) {
@@ -280,31 +310,36 @@
 
         if (mHeadsetProfile != null) {
             if ((BluetoothUuid.isUuidPresent(localUuids, BluetoothUuid.HSP_AG) &&
-                BluetoothUuid.isUuidPresent(uuids, BluetoothUuid.HSP)) ||
-                (BluetoothUuid.isUuidPresent(localUuids, BluetoothUuid.Handsfree_AG) &&
-                BluetoothUuid.isUuidPresent(uuids, BluetoothUuid.Handsfree))) {
-                    profiles.add(mHeadsetProfile);
+                    BluetoothUuid.isUuidPresent(uuids, BluetoothUuid.HSP)) ||
+                    (BluetoothUuid.isUuidPresent(localUuids, BluetoothUuid.Handsfree_AG) &&
+                            BluetoothUuid.isUuidPresent(uuids, BluetoothUuid.Handsfree))) {
+                profiles.add(mHeadsetProfile);
+                removedProfiles.remove(mHeadsetProfile);
             }
         }
 
         if (BluetoothUuid.containsAnyUuid(uuids, A2dpProfile.SINK_UUIDS) &&
             mA2dpProfile != null) {
             profiles.add(mA2dpProfile);
+            removedProfiles.remove(mA2dpProfile);
         }
 
         if (BluetoothUuid.isUuidPresent(uuids, BluetoothUuid.ObexObjectPush) &&
             mOppProfile != null) {
             profiles.add(mOppProfile);
+            removedProfiles.remove(mOppProfile);
         }
 
         if (BluetoothUuid.isUuidPresent(uuids, BluetoothUuid.Hid) &&
             mHidProfile != null) {
             profiles.add(mHidProfile);
+            removedProfiles.remove(mHidProfile);
         }
 
         if (BluetoothUuid.isUuidPresent(uuids, BluetoothUuid.NAP) &&
             mPanProfile != null) {
             profiles.add(mPanProfile);
+            removedProfiles.remove(mPanProfile);
         }
     }
 }
diff --git a/src/com/android/settings/bluetooth/OppProfile.java b/src/com/android/settings/bluetooth/OppProfile.java
index 3f7df38..eb5900e 100644
--- a/src/com/android/settings/bluetooth/OppProfile.java
+++ b/src/com/android/settings/bluetooth/OppProfile.java
@@ -79,7 +79,7 @@
         return R.string.bluetooth_profile_opp;
     }
 
-    public int getDisconnectResource() {
+    public int getDisconnectResource(BluetoothDevice device) {
         return 0; // user must use notification to disconnect OPP transfer.
     }
 
diff --git a/src/com/android/settings/bluetooth/PanProfile.java b/src/com/android/settings/bluetooth/PanProfile.java
index 3f456e4..6cb1991 100644
--- a/src/com/android/settings/bluetooth/PanProfile.java
+++ b/src/com/android/settings/bluetooth/PanProfile.java
@@ -25,14 +25,19 @@
 
 import com.android.settings.R;
 
+import java.util.HashMap;
 import java.util.List;
 
 /**
- * PanProfile handles Bluetooth PAN profile.
+ * PanProfile handles Bluetooth PAN profile (NAP and PANU).
  */
 final class PanProfile implements LocalBluetoothProfile {
     private BluetoothPan mService;
 
+    // Tethering direction for each device
+    private final HashMap<BluetoothDevice, Integer> mDeviceRoleMap =
+            new HashMap<BluetoothDevice, Integer>();
+
     static final String NAME = "PAN";
 
     // Order of this profile in device profiles list
@@ -111,8 +116,12 @@
         return R.string.bluetooth_profile_pan;
     }
 
-    public int getDisconnectResource() {
-        return R.string.bluetooth_disconnect_pan_profile;
+    public int getDisconnectResource(BluetoothDevice device) {
+        if (isLocalRoleNap(device)) {
+            return R.string.bluetooth_disconnect_pan_nap_profile;
+        } else {
+            return R.string.bluetooth_disconnect_pan_user_profile;
+        }
     }
 
     public int getSummaryResourceForDevice(BluetoothDevice device) {
@@ -122,7 +131,11 @@
                 return R.string.bluetooth_pan_profile_summary_use_for;
 
             case BluetoothProfile.STATE_CONNECTED:
-                return R.string.bluetooth_pan_profile_summary_connected;
+                if (isLocalRoleNap(device)) {
+                    return R.string.bluetooth_pan_nap_profile_summary_connected;
+                } else {
+                    return R.string.bluetooth_pan_user_profile_summary_connected;
+                }
 
             default:
                 return Utils.getConnectionStateSummary(state);
@@ -132,4 +145,17 @@
     public int getDrawableResource(BluetoothClass btClass) {
         return R.drawable.ic_bt_network_pan;
     }
+
+    // Tethering direction determines UI strings.
+    void setLocalRole(BluetoothDevice device, int role) {
+        mDeviceRoleMap.put(device, role);
+    }
+
+    boolean isLocalRoleNap(BluetoothDevice device) {
+        if (mDeviceRoleMap.containsKey(device)) {
+            return mDeviceRoleMap.get(device) == BluetoothPan.LOCAL_NAP_ROLE;
+        } else {
+            return false;
+        }
+    }
 }