OSDN Git Service

Make the Power Control widget more responsive.
authorBrad Fitzpatrick <bradfitz@android.com>
Mon, 22 Mar 2010 22:58:01 +0000 (15:58 -0700)
committerBrad Fitzpatrick <bradfitz@android.com>
Tue, 23 Mar 2010 01:07:17 +0000 (18:07 -0700)
BUG=2535155

Change-Id: Id3049fa9ba850c05140d7374065717b854a5d721

src/com/android/settings/widget/SettingsAppWidgetProvider.java

index 005fc13..2fd618c 100644 (file)
@@ -50,7 +50,7 @@ public class SettingsAppWidgetProvider extends AppWidgetProvider {
             new ComponentName("com.android.settings",
                     "com.android.settings.widget.SettingsAppWidgetProvider");
 
-    private static LocalBluetoothManager mLocalBluetoothManager = null;
+    private static LocalBluetoothManager sLocalBluetoothManager = null;
 
     private static final int BUTTON_WIFI = 0;
     private static final int BUTTON_BRIGHTNESS = 1;
@@ -58,9 +58,16 @@ public class SettingsAppWidgetProvider extends AppWidgetProvider {
     private static final int BUTTON_GPS = 3;
     private static final int BUTTON_BLUETOOTH = 4;
 
+    // This widget keeps track of two sets of states:
+    // "3-state": STATE_DISABLED, STATE_ENABLED, STATE_INTERMEDIATE
+    // "5-state": STATE_DISABLED, STATE_ENABLED, STATE_TURNING_ON, STATE_TURNING_OFF, STATE_UNKNOWN
     private static final int STATE_DISABLED = 0;
     private static final int STATE_ENABLED = 1;
-    private static final int STATE_INTERMEDIATE = 2;
+    private static final int STATE_TURNING_ON = 2;
+    private static final int STATE_TURNING_OFF = 3;
+    private static final int STATE_UNKNOWN = 4;
+    private static final int STATE_INTERMEDIATE = 5;
+
 
     /**
      * Minimum and maximum brightnesses.  Don't go to 0 since that makes the display unusable
@@ -69,6 +76,270 @@ public class SettingsAppWidgetProvider extends AppWidgetProvider {
     private static final int MAXIMUM_BACKLIGHT = android.os.Power.BRIGHTNESS_ON;
     private static final int DEFAULT_BACKLIGHT = (int) (android.os.Power.BRIGHTNESS_ON * 0.4f);
 
+    private static final StateTracker sWifiState = new WifiStateTracker();
+    private static final StateTracker sBluetoothState = new BluetoothStateTracker();
+
+    /**
+     * The state machine for Wifi and Bluetooth toggling, tracking
+     * reality versus the user's intent.
+     *
+     * This is necessary because reality moves relatively slowly
+     * (turning on &amp; off radio drivers), compared to user's
+     * expectations.
+     */
+    private abstract static class StateTracker {
+        // Is the state in the process of changing?
+        private boolean mInTransition = false;
+        private Boolean mActualState = null;  // initially not set
+        private Boolean mIntendedState = null;  // initially not set
+
+        // Did a toggle request arrive while a state update was
+        // already in-flight?  If so, the mIntendedState needs to be
+        // requested when the other one is done, unless we happened to
+        // arrive at that state already.
+        private boolean mDeferredStateChangeRequestNeeded = false;
+
+        /**
+         * User pressed a button to change the state.  Something
+         * should immediately appear to the user afterwards, even if
+         * we effectively do nothing.  Their press must be heard.
+         */
+        public final void toggleState(Context context) {
+            int currentState = getTriState(context);
+            boolean newState = false;
+            switch (currentState) {
+                case STATE_ENABLED:
+                    newState = false;
+                    break;
+                case STATE_DISABLED:
+                    newState = true;
+                    break;
+                case STATE_INTERMEDIATE:
+                    if (mIntendedState != null) {
+                        newState = !mIntendedState;
+                    }
+                    break;
+            }
+            mIntendedState = newState;
+            if (mInTransition) {
+                // We don't send off a transition request if we're
+                // already transitioning.  Makes our state tracking
+                // easier, and is probably nicer on lower levels.
+                // (even though they should be able to take it...)
+                mDeferredStateChangeRequestNeeded = true;
+            } else {
+                mInTransition = true;
+                boolean showToast = newState;  // only show Toast on the up transition
+                requestStateChange(context, newState, showToast);
+            }
+        }
+
+        /**
+         * Update internal state from a broadcast state change.
+         */
+        public abstract void onActualStateChange(Context context, Intent intent);
+
+        /**
+         * Sets the value that we're now in.  To be called from onActualStateChange.
+         *
+         * @param newState one of STATE_DISABLED, STATE_ENABLED, STATE_TURNING_ON,
+         *                 STATE_TURNING_OFF, STATE_UNKNOWN
+         */
+        protected final void setCurrentState(Context context, int newState) {
+            boolean wasInTransition = mInTransition;
+            switch (newState) {
+                case STATE_DISABLED:
+                    mInTransition = false;
+                    mActualState = false;
+                    break;
+                case STATE_ENABLED:
+                    mInTransition = false;
+                    mActualState = true;
+                    break;
+                case STATE_TURNING_ON:
+                    mInTransition = true;
+                    mActualState = false;
+                    break;
+                case STATE_TURNING_OFF:
+                    mInTransition = true;
+                    mActualState = true;
+                    break;
+            }
+
+            if (wasInTransition && !mInTransition) {
+                if (mDeferredStateChangeRequestNeeded) {
+                    Log.v(TAG, "processing deferred state change");
+                    if (mActualState != null && mIntendedState != null &&
+                        mIntendedState.equals(mActualState)) {
+                        Log.v(TAG, "... but intended state matches, so no changes.");
+                    } else if (mIntendedState != null) {
+                        mInTransition = true;
+                        requestStateChange(context, mIntendedState, false /* no toast */);
+                    }
+                    mDeferredStateChangeRequestNeeded = false;
+                }
+            }
+        }
+
+
+        /**
+         * If we're in a transition mode, this returns true if we're
+         * transitioning towards being enabled.
+         */
+        public final boolean isTurningOn() {
+            return mIntendedState != null && mIntendedState;
+        }
+
+        /**
+         * Returns simplified 3-state value from underlying 5-state.
+         *
+         * @param context
+         * @return STATE_ENABLED, STATE_DISABLED, or STATE_INTERMEDIATE
+         */
+        public final int getTriState(Context context) {
+            switch (getActualState(context)) {
+                case STATE_DISABLED:
+                    return STATE_DISABLED;
+                case STATE_ENABLED:
+                    return STATE_ENABLED;
+                default:
+                    return STATE_INTERMEDIATE;
+            }
+        }
+
+        /**
+         * Gets underlying actual state.
+         *
+         * @param context
+         * @return STATE_ENABLED, STATE_DISABLED, STATE_ENABLING, STATE_DISABLING,
+         *         or or STATE_UNKNOWN.
+         */
+        public abstract int getActualState(Context context);
+
+        /**
+         * Actually make the desired change to the underlying radio
+         * API.
+         */
+        protected abstract void requestStateChange(Context context,
+                                                   boolean desiredState, boolean withToast);
+    }
+
+    /**
+     * Subclass of StateTracker to get/set Wifi state.
+     */
+    private static final class WifiStateTracker extends StateTracker {
+        @Override
+        public int getActualState(Context context) {
+            WifiManager wifiManager = (WifiManager) context.getSystemService(Context.WIFI_SERVICE);
+            if (wifiManager != null) {
+                return wifiStateToFiveState(wifiManager.getWifiState());
+            }
+            return STATE_UNKNOWN;
+        }
+
+        @Override
+        protected void requestStateChange(Context context,
+                                          boolean desiredState, boolean withToast) {
+            WifiManager wifiManager = (WifiManager) context.getSystemService(Context.WIFI_SERVICE);
+            if (wifiManager == null) {
+                Log.d(TAG, "No wifiManager.");
+                return;
+            }
+            if (withToast) {
+                Toast.makeText(context, R.string.gadget_toggle_wifi, Toast.LENGTH_SHORT).show();
+            }
+            wifiManager.setWifiEnabled(desiredState);
+        }
+
+        @Override
+        public void onActualStateChange(Context context, Intent intent) {
+            if (!WifiManager.WIFI_STATE_CHANGED_ACTION.equals(intent.getAction())) {
+                return;
+            }
+            int wifiState = intent.getIntExtra(WifiManager.EXTRA_WIFI_STATE, -1);
+            setCurrentState(context, wifiStateToFiveState(wifiState));
+        }
+
+        /**
+         * Converts WifiManager's state values into our
+         * Wifi/Bluetooth-common state values.
+         */
+        private static int wifiStateToFiveState(int wifiState) {
+            switch (wifiState) {
+                case WifiManager.WIFI_STATE_DISABLED:
+                    return STATE_DISABLED;
+                case WifiManager.WIFI_STATE_ENABLED:
+                    return STATE_ENABLED;
+                case WifiManager.WIFI_STATE_DISABLING:
+                    return STATE_TURNING_OFF;
+                case WifiManager.WIFI_STATE_ENABLING:
+                    return STATE_TURNING_ON;
+                default:
+                    return STATE_UNKNOWN;
+            }
+        }
+    }
+
+    /**
+     * Subclass of StateTracker to get/set Bluetooth state.
+     */
+    private static final class BluetoothStateTracker extends StateTracker {
+
+        @Override
+        public int getActualState(Context context) {
+            if (sLocalBluetoothManager == null) {
+                sLocalBluetoothManager = LocalBluetoothManager.getInstance(context);
+                if (sLocalBluetoothManager == null) {
+                    return STATE_UNKNOWN;  // On emulator?
+                }
+            }
+            return bluetoothStateToFiveState(sLocalBluetoothManager.getBluetoothState());
+        }
+
+        @Override
+        protected void requestStateChange(Context context,
+                                          boolean desiredState, boolean withToast) {
+            if (sLocalBluetoothManager == null) {
+                Log.d(TAG, "No LocalBluetoothManager");
+                return;
+            }
+            if (withToast) {
+                Toast.makeText(context,
+                               R.string.gadget_toggle_bluetooth, Toast.LENGTH_SHORT).show();
+            }
+            sLocalBluetoothManager.setBluetoothEnabled(desiredState);
+        }
+
+        @Override
+        public void onActualStateChange(Context context, Intent intent) {
+            if (!BluetoothAdapter.ACTION_STATE_CHANGED.equals(intent.getAction())) {
+                return;
+            }
+            int bluetoothState = intent.getIntExtra(BluetoothAdapter.EXTRA_STATE, -1);
+            setCurrentState(context, bluetoothStateToFiveState(bluetoothState));
+        }
+
+        /**
+         * Converts BluetoothAdapter's state values into our
+         * Wifi/Bluetooth-common state values.
+         */
+        private static int bluetoothStateToFiveState(int bluetoothState) {
+            switch (bluetoothState) {
+                case BluetoothAdapter.STATE_OFF:
+                    return STATE_DISABLED;
+                case BluetoothAdapter.STATE_ON:
+                    return STATE_ENABLED;
+                case BluetoothAdapter.STATE_TURNING_ON:
+                    return STATE_TURNING_ON;
+                case BluetoothAdapter.STATE_TURNING_OFF:
+                    return STATE_TURNING_OFF;
+                default:
+                    return STATE_UNKNOWN;
+            }
+        }
+    }
+
+
     @Override
     public void onUpdate(Context context, AppWidgetManager appWidgetManager,
             int[] appWidgetIds) {
@@ -142,29 +413,53 @@ public class SettingsAppWidgetProvider extends AppWidgetProvider {
      * @param context
      */
     private static void updateButtons(RemoteViews views, Context context) {
-        switch (getWifiState(context)) {
+        switch (sWifiState.getTriState(context)) {
             case STATE_DISABLED:
-                views.setImageViewResource(R.id.img_wifi, R.drawable.ic_appwidget_settings_wifi_off);
-                views.setImageViewResource(R.id.ind_wifi, R.drawable.appwidget_settings_ind_off_l);
+                views.setImageViewResource(R.id.img_wifi,
+                                           R.drawable.ic_appwidget_settings_wifi_off);
+                views.setImageViewResource(R.id.ind_wifi,
+                                           R.drawable.appwidget_settings_ind_off_l);
                 break;
             case STATE_ENABLED:
-                views.setImageViewResource(R.id.img_wifi, R.drawable.ic_appwidget_settings_wifi_on);
-                views.setImageViewResource(R.id.ind_wifi, R.drawable.appwidget_settings_ind_on_l);
+                views.setImageViewResource(R.id.img_wifi,
+                                           R.drawable.ic_appwidget_settings_wifi_on);
+                views.setImageViewResource(R.id.ind_wifi,
+                                           R.drawable.appwidget_settings_ind_on_l);
                 break;
             case STATE_INTERMEDIATE:
-                views.setImageViewResource(R.id.img_wifi, R.drawable.ic_appwidget_settings_wifi_off);
-                views.setImageViewResource(R.id.ind_wifi, R.drawable.appwidget_settings_ind_mid_l);
+                // In the transitional state, the bottom green bar
+                // shows the tri-state (on, off, transitioning), but
+                // the top dark-gray-or-bright-white logo shows the
+                // user's intent.  This is much easier to see in
+                // sunlight.
+                if (sWifiState.isTurningOn()) {
+                    views.setImageViewResource(R.id.img_wifi,
+                                               R.drawable.ic_appwidget_settings_wifi_on);
+                    views.setImageViewResource(R.id.ind_wifi,
+                                               R.drawable.appwidget_settings_ind_mid_l);
+                } else {
+                    views.setImageViewResource(R.id.img_wifi,
+                                               R.drawable.ic_appwidget_settings_wifi_off);
+                    views.setImageViewResource(R.id.ind_wifi,
+                                               R.drawable.appwidget_settings_ind_off_l);
+                }
                 break;
         }
         if (getBrightnessMode(context)) {
-            views.setImageViewResource(R.id.img_brightness, R.drawable.ic_appwidget_settings_brightness_auto);
-            views.setImageViewResource(R.id.ind_brightness, R.drawable.appwidget_settings_ind_on_r);
+            views.setImageViewResource(R.id.img_brightness,
+                                       R.drawable.ic_appwidget_settings_brightness_auto);
+            views.setImageViewResource(R.id.ind_brightness,
+                                       R.drawable.appwidget_settings_ind_on_r);
         } else if (getBrightness(context)) {
-            views.setImageViewResource(R.id.img_brightness, R.drawable.ic_appwidget_settings_brightness_on);
-            views.setImageViewResource(R.id.ind_brightness, R.drawable.appwidget_settings_ind_on_r);
+            views.setImageViewResource(R.id.img_brightness,
+                                       R.drawable.ic_appwidget_settings_brightness_on);
+            views.setImageViewResource(R.id.ind_brightness,
+                                       R.drawable.appwidget_settings_ind_on_r);
         } else {
-            views.setImageViewResource(R.id.img_brightness, R.drawable.ic_appwidget_settings_brightness_off);
-            views.setImageViewResource(R.id.ind_brightness, R.drawable.appwidget_settings_ind_off_r);
+            views.setImageViewResource(R.id.img_brightness,
+                                       R.drawable.ic_appwidget_settings_brightness_off);
+            views.setImageViewResource(R.id.ind_brightness,
+                                       R.drawable.appwidget_settings_ind_off_r);
         }
         if (getSync(context)) {
             views.setImageViewResource(R.id.img_sync, R.drawable.ic_appwidget_settings_sync_on);
@@ -180,18 +475,36 @@ public class SettingsAppWidgetProvider extends AppWidgetProvider {
             views.setImageViewResource(R.id.img_gps, R.drawable.ic_appwidget_settings_gps_off);
             views.setImageViewResource(R.id.ind_gps, R.drawable.appwidget_settings_ind_off_c);
         }
-        switch (getBluetoothState(context)) {
+        switch (sBluetoothState.getTriState(context)) {
             case STATE_DISABLED:
-                views.setImageViewResource(R.id.img_bluetooth, R.drawable.ic_appwidget_settings_bluetooth_off);
-                views.setImageViewResource(R.id.ind_bluetooth, R.drawable.appwidget_settings_ind_off_c);
+                views.setImageViewResource(R.id.img_bluetooth,
+                                           R.drawable.ic_appwidget_settings_bluetooth_off);
+                views.setImageViewResource(R.id.ind_bluetooth,
+                                           R.drawable.appwidget_settings_ind_off_c);
                 break;
             case STATE_ENABLED:
-                views.setImageViewResource(R.id.img_bluetooth, R.drawable.ic_appwidget_settings_bluetooth_on);
-                views.setImageViewResource(R.id.ind_bluetooth, R.drawable.appwidget_settings_ind_on_c);
+                views.setImageViewResource(R.id.img_bluetooth,
+                                           R.drawable.ic_appwidget_settings_bluetooth_on);
+                views.setImageViewResource(R.id.ind_bluetooth,
+                                           R.drawable.appwidget_settings_ind_on_c);
                 break;
             case STATE_INTERMEDIATE:
-                views.setImageViewResource(R.id.img_bluetooth, R.drawable.ic_appwidget_settings_bluetooth_off);
-                views.setImageViewResource(R.id.ind_bluetooth, R.drawable.appwidget_settings_ind_mid_c);
+                // In the transitional state, the bottom green bar
+                // shows the tri-state (on, off, transitioning), but
+                // the top dark-gray-or-bright-white logo shows the
+                // user's intent.  This is much easier to see in
+                // sunlight.
+                if (sBluetoothState.isTurningOn()) {
+                    views.setImageViewResource(R.id.img_bluetooth,
+                                               R.drawable.ic_appwidget_settings_bluetooth_on);
+                    views.setImageViewResource(R.id.ind_bluetooth,
+                                               R.drawable.appwidget_settings_ind_mid_c);
+                } else {
+                    views.setImageViewResource(R.id.img_bluetooth,
+                                               R.drawable.ic_appwidget_settings_bluetooth_off);
+                    views.setImageViewResource(R.id.ind_bluetooth,
+                                               R.drawable.appwidget_settings_ind_off_c);
+                }
                 break;
         }
     }
@@ -223,11 +536,15 @@ public class SettingsAppWidgetProvider extends AppWidgetProvider {
     @Override
     public void onReceive(Context context, Intent intent) {
         super.onReceive(context, intent);
-        if (intent.hasCategory(Intent.CATEGORY_ALTERNATIVE)) {
+        if (WifiManager.WIFI_STATE_CHANGED_ACTION.equals(intent.getAction())) {
+            sWifiState.onActualStateChange(context, intent);
+        } else if (BluetoothAdapter.ACTION_STATE_CHANGED.equals(intent.getAction())) {
+            sBluetoothState.onActualStateChange(context, intent);
+        } else if (intent.hasCategory(Intent.CATEGORY_ALTERNATIVE)) {
             Uri data = intent.getData();
             int buttonId = Integer.parseInt(data.getSchemeSpecificPart());
             if (buttonId == BUTTON_WIFI) {
-                toggleWifi(context);
+                sWifiState.toggleState(context);
             } else if (buttonId == BUTTON_BRIGHTNESS) {
                 toggleBrightness(context);
             } else if (buttonId == BUTTON_SYNC) {
@@ -235,50 +552,15 @@ public class SettingsAppWidgetProvider extends AppWidgetProvider {
             } else if (buttonId == BUTTON_GPS) {
                 toggleGps(context);
             } else if (buttonId == BUTTON_BLUETOOTH) {
-                toggleBluetooth(context);
+                sBluetoothState.toggleState(context);
             }
         }
+
         // State changes fall through
         updateWidget(context);
     }
 
     /**
-     * Gets the state of Wi-Fi
-     *
-     * @param context
-     * @return STATE_ENABLED, STATE_DISABLED, or STATE_INTERMEDIATE
-     */
-    private static int getWifiState(Context context) {
-        WifiManager wifiManager = (WifiManager) context.getSystemService(Context.WIFI_SERVICE);
-        int wifiState = wifiManager.getWifiState();
-        if (wifiState == WifiManager.WIFI_STATE_DISABLED) {
-            return STATE_DISABLED;
-        } else if (wifiState == WifiManager.WIFI_STATE_ENABLED) {
-            return STATE_ENABLED;
-        } else {
-            return STATE_INTERMEDIATE;
-        }
-    }
-
-    /**
-     * Toggles the state of Wi-Fi
-     *
-     * @param context
-     */
-    private void toggleWifi(Context context) {
-        WifiManager wifiManager = (WifiManager) context.getSystemService(Context.WIFI_SERVICE);
-        int wifiState = getWifiState(context);
-        if (wifiState == STATE_ENABLED) {
-            wifiManager.setWifiEnabled(false);
-            Toast.makeText(context, R.string.gadget_toggle_wifi, Toast.LENGTH_SHORT).show();
-        } else if (wifiState == STATE_DISABLED) {
-            wifiManager.setWifiEnabled(true);
-            Toast.makeText(context, R.string.gadget_toggle_wifi, Toast.LENGTH_SHORT).show();
-        }
-        // If wifi is in intermediate state, don't do anything.
-    }
-
-    /**
      * Gets the state of background data.
      *
      * @param context
@@ -459,44 +741,4 @@ public class SettingsAppWidgetProvider extends AppWidgetProvider {
             Log.d(TAG, "toggleBrightness: " + e);
         }
     }
-
-    /**
-     * Gets state of bluetooth
-     *
-     * @param context
-     * @return STATE_ENABLED, STATE_DISABLED, or STATE_INTERMEDIATE
-     */
-    private static int getBluetoothState(Context context) {
-        if (mLocalBluetoothManager == null) {
-            mLocalBluetoothManager = LocalBluetoothManager.getInstance(context);
-            if (mLocalBluetoothManager == null) {
-                return STATE_INTERMEDIATE; // On emulator?
-            }
-        }
-        int state = mLocalBluetoothManager.getBluetoothState();
-        if (state == BluetoothAdapter.STATE_OFF) {
-            return STATE_DISABLED;
-        } else if (state == BluetoothAdapter.STATE_ON) {
-            return STATE_ENABLED;
-        } else {
-            return STATE_INTERMEDIATE;
-        }
-    }
-
-    /**
-     * Toggles the state of bluetooth
-     *
-     * @param context
-     */
-    private void toggleBluetooth(Context context) {
-        int state = getBluetoothState(context);
-        if (state == STATE_ENABLED) {
-            mLocalBluetoothManager.setBluetoothEnabled(false);
-            Toast.makeText(context, R.string.gadget_toggle_bluetooth, Toast.LENGTH_SHORT).show();
-        } else if (state == STATE_DISABLED) {
-            mLocalBluetoothManager.setBluetoothEnabled(true);
-            Toast.makeText(context, R.string.gadget_toggle_bluetooth, Toast.LENGTH_SHORT).show();
-        }
-        // If bluetooth is in intermediate state, don't do anything.
-    }
 }