diff options
author | John Spurlock <jspurlock@google.com> | 2014-07-07 08:37:56 -0400 |
---|---|---|
committer | John Spurlock <jspurlock@google.com> | 2014-07-12 18:41:57 -0400 |
commit | 486b78e42652466f6241eb87d5bed60040db7a25 (patch) | |
tree | 6d11449bd70d79355d489558f8145e3655e0e672 /packages | |
parent | 030f0b2340877b0a466ae112466a88456fcb5a0e (diff) | |
download | frameworks_base-486b78e42652466f6241eb87d5bed60040db7a25.zip frameworks_base-486b78e42652466f6241eb87d5bed60040db7a25.tar.gz frameworks_base-486b78e42652466f6241eb87d5bed60040db7a25.tar.bz2 |
QS: Introduce bluetooth control panel.
- Factor out common detail item panel view, share with Wifi.
- Add an empty state (large icon + text)
- Implement connect / disconnect for supported BT profiles.
- Wire up "scanning" state, but still waiting on asset.
- Add BT controller info to dump.
Bug:16235253
Change-Id: Icf854cafba962fe4b63767d7206e309d80b7b87b
Diffstat (limited to 'packages')
18 files changed, 1067 insertions, 128 deletions
diff --git a/packages/SystemUI/res/drawable/ic_qs_bluetooth_detail_empty.xml b/packages/SystemUI/res/drawable/ic_qs_bluetooth_detail_empty.xml new file mode 100644 index 0000000..aa0b369 --- /dev/null +++ b/packages/SystemUI/res/drawable/ic_qs_bluetooth_detail_empty.xml @@ -0,0 +1,28 @@ +<!-- +Copyright (C) 2014 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. +--> +<vector xmlns:android="http://schemas.android.com/apk/res/android" > + <size + android:width="56dp" + android:height="56dp"/> + + <viewport + android:viewportWidth="48.0" + android:viewportHeight="48.0"/> + + <path + android:fill="@color/qs_detail_empty" + android:pathData="M35.4,15.4L24.0,4.0l-2.0,0.0l0.0,15.2L12.8,10.0L10.0,12.8L21.2,24.0L10.0,35.2l2.8,2.8l9.2,-9.2L22.0,44.0l2.0,0.0l11.4,-11.4L26.8,24.0L35.4,15.4zM26.0,11.7l3.8,3.8L26.0,19.2L26.0,11.7zM29.8,32.6L26.0,36.3l0.0,-7.5L29.8,32.6z"/> +</vector> diff --git a/packages/SystemUI/res/drawable/ic_qs_cancel.xml b/packages/SystemUI/res/drawable/ic_qs_cancel.xml new file mode 100644 index 0000000..de72f13 --- /dev/null +++ b/packages/SystemUI/res/drawable/ic_qs_cancel.xml @@ -0,0 +1,28 @@ +<!-- +Copyright (C) 2014 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. +--> +<vector xmlns:android="http://schemas.android.com/apk/res/android" > + <size + android:width="24dp" + android:height="24dp"/> + + <viewport + android:viewportWidth="48.0" + android:viewportHeight="48.0"/> + + <path + android:fill="#FFFFFFFF" + android:pathData="M24.0,4.0C12.9,4.0 4.0,12.9 4.0,24.0s8.9,20.0 20.0,20.0c11.1,0.0 20.0,-8.9 20.0,-20.0S35.1,4.0 24.0,4.0zM34.0,31.2L31.2,34.0L24.0,26.8L16.8,34.0L14.0,31.2l7.2,-7.2L14.0,16.8l2.8,-2.8l7.2,7.2l7.2,-7.2l2.8,2.8L26.8,24.0L34.0,31.2z"/> +</vector> diff --git a/packages/SystemUI/res/drawable/ic_qs_wifi_detail_empty.xml b/packages/SystemUI/res/drawable/ic_qs_wifi_detail_empty.xml new file mode 100644 index 0000000..16fa30b --- /dev/null +++ b/packages/SystemUI/res/drawable/ic_qs_wifi_detail_empty.xml @@ -0,0 +1,28 @@ +<!-- +Copyright (C) 2014 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. +--> +<vector xmlns:android="http://schemas.android.com/apk/res/android" > + <size + android:width="56dp" + android:height="56dp"/> + + <viewport + android:viewportWidth="48.0" + android:viewportHeight="48.0"/> + + <path + android:pathData="M24.0,4.0C15.0,4.0 6.7,7.0 0.0,12.0l24.0,32.0l24.0,-32.0C41.3,7.0 33.0,4.0 24.0,4.0z" + android:fill="@color/qs_detail_empty" /> +</vector> diff --git a/packages/SystemUI/res/layout/qs_detail_item.xml b/packages/SystemUI/res/layout/qs_detail_item.xml index c5eaed9..55139fb 100644 --- a/packages/SystemUI/res/layout/qs_detail_item.xml +++ b/packages/SystemUI/res/layout/qs_detail_item.xml @@ -17,9 +17,9 @@ <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="@dimen/qs_detail_item_height" - android:gravity="center_vertical" android:background="@drawable/btn_borderless_rect" android:clickable="true" + android:gravity="center_vertical" android:orientation="horizontal" > <ImageView @@ -29,9 +29,10 @@ android:layout_marginEnd="12dp" /> <LinearLayout - android:layout_width="match_parent" + android:layout_width="0dp" android:layout_height="wrap_content" android:layout_marginStart="20dp" + android:layout_weight="1" android:orientation="vertical" > <TextView @@ -48,4 +49,13 @@ android:textAppearance="@style/TextAppearance.QS.DetailItemSecondary" /> </LinearLayout> + <ImageView + android:id="@android:id/icon2" + style="@style/QSBorderlessButton" + android:layout_width="48dp" + android:layout_height="48dp" + android:clickable="true" + android:scaleType="center" + android:src="@drawable/ic_qs_cancel" /> + </LinearLayout>
\ No newline at end of file diff --git a/packages/SystemUI/res/layout/qs_detail_items.xml b/packages/SystemUI/res/layout/qs_detail_items.xml new file mode 100644 index 0000000..b64005f --- /dev/null +++ b/packages/SystemUI/res/layout/qs_detail_items.xml @@ -0,0 +1,49 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + Copyright (C) 2014 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. +--> +<!-- extends FrameLayout --> +<com.android.systemui.qs.QSDetailItems xmlns:android="http://schemas.android.com/apk/res/android" + android:layout_width="match_parent" + android:layout_height="match_parent" > + + <LinearLayout + android:id="@android:id/list" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:orientation="vertical" /> + + <LinearLayout + android:id="@android:id/empty" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_gravity="center" + android:gravity="center_horizontal" + android:orientation="vertical" > + + <ImageView + android:id="@android:id/icon" + android:layout_width="56dp" + android:layout_height="56dp" /> + + <TextView + android:id="@android:id/title" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginTop="20dp" + android:textAppearance="@style/TextAppearance.QS.DetailEmpty" /> + </LinearLayout> + +</com.android.systemui.qs.QSDetailItems>
\ No newline at end of file diff --git a/packages/SystemUI/res/values/colors.xml b/packages/SystemUI/res/values/colors.xml index 4cc0bb5..bae7ed7 100644 --- a/packages/SystemUI/res/values/colors.xml +++ b/packages/SystemUI/res/values/colors.xml @@ -41,6 +41,7 @@ <color name="qs_tile_divider">#29ffffff</color><!-- 16% white --> <color name="qs_tile_text">#B3FFFFFF</color><!-- 70% white --> <color name="qs_subhead">#66FFFFFF</color><!-- 40% white --> + <color name="qs_detail_empty">#24B0BEC5</color><!-- 14% blue grey 200--> <color name="data_usage_secondary">#99FFFFFF</color><!-- 60% white --> <color name="data_usage_graph_track">#33FFFFFF</color><!-- 20% white --> <color name="status_bar_clock_color">#33FFFFFF</color> diff --git a/packages/SystemUI/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml index 48670fb..b0f2133 100644 --- a/packages/SystemUI/res/values/strings.xml +++ b/packages/SystemUI/res/values/strings.xml @@ -486,6 +486,8 @@ <string name="quick_settings_bluetooth_multiple_devices_label">Bluetooth (<xliff:g id="number">%d</xliff:g> Devices)</string> <!-- QuickSettings: Bluetooth (Off) [CHAR LIMIT=NONE] --> <string name="quick_settings_bluetooth_off_label">Bluetooth Off</string> + <!-- QuickSettings: Bluetooth detail panel, text when there are no items [CHAR LIMIT=NONE] --> + <string name="quick_settings_bluetooth_detail_empty_text">No paired devices available</string> <!-- QuickSettings: Brightness [CHAR LIMIT=NONE] --> <string name="quick_settings_brightness_label">Brightness</string> <!-- QuickSettings: Rotation Unlocked [CHAR LIMIT=NONE] --> @@ -522,6 +524,8 @@ <string name="quick_settings_wifi_no_network">No Network</string> <!-- QuickSettings: Wifi (Off) [CHAR LIMIT=NONE] --> <string name="quick_settings_wifi_off_label">Wi-Fi Off</string> + <!-- QuickSettings: Wifi detail panel, text when there are no items [CHAR LIMIT=NONE] --> + <string name="quick_settings_wifi_detail_empty_text">No saved networks available</string> <!-- QuickSettings: Remote display [CHAR LIMIT=NONE] --> <string name="quick_settings_remote_display_no_connection_label">Cast screen</string> <!-- QuickSettings: Brightness dialog title [CHAR LIMIT=NONE] --> @@ -538,6 +542,8 @@ <string name="quick_settings_done">Done</string> <!-- QuickSettings: Control panel: Label for connected device. [CHAR LIMIT=NONE] --> <string name="quick_settings_connected">Connected</string> + <!-- QuickSettings: Control panel: Label for connecting device. [CHAR LIMIT=NONE] --> + <string name="quick_settings_connecting">Connecting...</string> <!-- QuickSettings: Tethering. [CHAR LIMIT=NONE] --> <string name="quick_settings_tethering_label">Tethering</string> <!-- QuickSettings: Hotspot. [CHAR LIMIT=NONE] --> diff --git a/packages/SystemUI/res/values/styles.xml b/packages/SystemUI/res/values/styles.xml index f99b68b..7da6c22 100644 --- a/packages/SystemUI/res/values/styles.xml +++ b/packages/SystemUI/res/values/styles.xml @@ -179,6 +179,11 @@ <item name="android:gravity">center</item> </style> + <style name="TextAppearance.QS.DetailEmpty"> + <item name="android:textSize">14sp</item> + <item name="android:textColor">@color/qs_subhead</item> + </style> + <style name="TextAppearance.QS.Subhead"> <item name="android:textSize">14sp</item> <item name="android:fontFamily">sans-serif-medium</item> diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSDetailItems.java b/packages/SystemUI/src/com/android/systemui/qs/QSDetailItems.java new file mode 100644 index 0000000..24c1378 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/qs/QSDetailItems.java @@ -0,0 +1,209 @@ +/* + * Copyright (C) 2014 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.systemui.qs; + +import android.content.Context; +import android.os.Handler; +import android.os.Looper; +import android.os.Message; +import android.text.TextUtils; +import android.util.AttributeSet; +import android.util.Log; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.FrameLayout; +import android.widget.ImageView; +import android.widget.LinearLayout; +import android.widget.TextView; + +import com.android.systemui.R; + +/** + * Quick settings common detail view with line items. + */ +public class QSDetailItems extends FrameLayout { + private static final String TAG = "QSDetailItems"; + private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG); + + private final Context mContext; + private final H mHandler = new H(); + + private String mTag; + private Callback mCallback; + private boolean mItemsVisible = true; + private LinearLayout mItems; + private View mEmpty; + private TextView mEmptyText; + private ImageView mEmptyIcon; + + public QSDetailItems(Context context, AttributeSet attrs) { + super(context, attrs); + mContext = context; + mTag = TAG; + } + + public static QSDetailItems convertOrInflate(Context context, View convert, ViewGroup parent) { + if (convert instanceof QSDetailItems) { + return (QSDetailItems) convert; + } + return (QSDetailItems) LayoutInflater.from(context).inflate(R.layout.qs_detail_items, + parent, false); + } + + @Override + protected void onFinishInflate() { + super.onFinishInflate(); + mItems = (LinearLayout) findViewById(android.R.id.list); + mItems.setVisibility(GONE); + mEmpty = findViewById(android.R.id.empty); + mEmpty.setVisibility(GONE); + mEmptyText = (TextView) mEmpty.findViewById(android.R.id.title); + mEmptyIcon = (ImageView) mEmpty.findViewById(android.R.id.icon); + } + + public void setTagSuffix(String suffix) { + mTag = TAG + "." + suffix; + } + + public void setEmptyState(int icon, int text) { + mEmptyIcon.setImageResource(icon); + mEmptyText.setText(text); + } + + @Override + protected void onAttachedToWindow() { + super.onAttachedToWindow(); + if (DEBUG) Log.d(mTag, "onAttachedToWindow"); + } + + @Override + protected void onDetachedFromWindow() { + super.onDetachedFromWindow(); + if (DEBUG) Log.d(mTag, "onDetachedFromWindow"); + mCallback = null; + } + + public void setCallback(Callback callback) { + mHandler.removeMessages(H.SET_CALLBACK); + mHandler.obtainMessage(H.SET_CALLBACK, callback).sendToTarget(); + } + + public void setItems(Item[] items) { + mHandler.removeMessages(H.SET_ITEMS); + mHandler.obtainMessage(H.SET_ITEMS, items).sendToTarget(); + } + + public void setItemsVisible(boolean visible) { + mHandler.removeMessages(H.SET_ITEMS_VISIBLE); + mHandler.obtainMessage(H.SET_ITEMS_VISIBLE, visible ? 1 : 0, 0).sendToTarget(); + } + + private void handleSetCallback(Callback callback) { + mCallback = callback; + } + + private void handleSetItems(Item[] items) { + final int itemCount = items != null ? items.length : 0; + mEmpty.setVisibility(itemCount == 0 ? VISIBLE : GONE); + mItems.setVisibility(itemCount == 0 ? GONE : VISIBLE); + for (int i = mItems.getChildCount() - 1; i >= itemCount; i--) { + mItems.removeViewAt(i); + } + for (int i = 0; i < itemCount; i++) { + bind(items[i], mItems.getChildAt(i)); + } + } + + private void handleSetItemsVisible(boolean visible) { + if (mItemsVisible == visible) return; + mItemsVisible = visible; + for (int i = 0; i < mItems.getChildCount(); i++) { + mItems.getChildAt(i).setVisibility(mItemsVisible ? VISIBLE : INVISIBLE); + } + } + + private void bind(final Item item, View view) { + if (view == null) { + view = LayoutInflater.from(mContext).inflate(R.layout.qs_detail_item, this, false); + mItems.addView(view); + } + view.setVisibility(mItemsVisible ? VISIBLE : INVISIBLE); + final ImageView iv = (ImageView) view.findViewById(android.R.id.icon); + iv.setImageResource(item.icon); + final TextView title = (TextView) view.findViewById(android.R.id.title); + title.setText(item.line1); + final TextView summary = (TextView) view.findViewById(android.R.id.summary); + final boolean twoLines = !TextUtils.isEmpty(item.line2); + summary.setVisibility(twoLines ? VISIBLE : GONE); + summary.setText(twoLines ? item.line2 : null); + view.setMinimumHeight(mContext.getResources() .getDimensionPixelSize( + twoLines ? R.dimen.qs_detail_item_height_twoline : R.dimen.qs_detail_item_height)); + view.setOnClickListener(new OnClickListener() { + @Override + public void onClick(View v) { + if (mCallback != null) { + mCallback.onDetailItemClick(item); + } + } + }); + final ImageView disconnect = (ImageView) view.findViewById(android.R.id.icon2); + disconnect.setVisibility(item.canDisconnect ? VISIBLE : GONE); + disconnect.setOnClickListener(new OnClickListener() { + @Override + public void onClick(View v) { + if (mCallback != null) { + mCallback.onDetailItemDisconnect(item); + } + } + }); + } + + private class H extends Handler { + private static final int SET_ITEMS = 1; + private static final int SET_CALLBACK = 2; + private static final int SET_ITEMS_VISIBLE = 3; + + public H() { + super(Looper.getMainLooper()); + } + + @Override + public void handleMessage(Message msg) { + if (msg.what == SET_ITEMS) { + handleSetItems((Item[]) msg.obj); + } else if (msg.what == SET_CALLBACK) { + handleSetCallback((QSDetailItems.Callback) msg.obj); + } else if (msg.what == SET_ITEMS_VISIBLE) { + handleSetItemsVisible(msg.arg1 != 0); + } + } + } + + public static class Item { + public int icon; + public String line1; + public String line2; + public Object tag; + public boolean canDisconnect; + } + + public interface Callback { + void onDetailItemClick(Item item); + void onDetailItemDisconnect(Item item); + } +} diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSPanel.java b/packages/SystemUI/src/com/android/systemui/qs/QSPanel.java index 36cd388..449cc1d 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/QSPanel.java +++ b/packages/SystemUI/src/com/android/systemui/qs/QSPanel.java @@ -168,6 +168,12 @@ public class QSPanel extends ViewGroup { fireToggleStateChanged(state); } } + @Override + public void onScanStateChanged(boolean state) { + if (mDetailRecord == r) { + fireScanStateChanged(state); + } + } }); final View.OnClickListener click = new View.OnClickListener() { @Override @@ -195,6 +201,7 @@ public class QSPanel extends ViewGroup { if (mDetailRecord != null) return; // already showing something in detail r.detailAdapter = r.tile.getDetailAdapter(); if (r.detailAdapter == null) return; + mDetailRecord = r; r.detailView = r.detailAdapter.createDetailView(mContext, r.detailView, mDetailContent); if (r.detailView == null) throw new IllegalStateException("Must return detail view"); mDetailDoneButton.setOnClickListener(new OnClickListener() { @@ -211,7 +218,6 @@ public class QSPanel extends ViewGroup { mDetailRecord.tile.mHost.startSettingsActivity(settingsIntent); } }); - mDetailRecord = r; mDetailContent.removeAllViews(); mDetail.bringToFront(); mDetailContent.addView(r.detailView); @@ -312,6 +318,12 @@ public class QSPanel extends ViewGroup { } } + private void fireScanStateChanged(boolean state) { + if (mCallback != null) { + mCallback.onScanStateChanged(state); + } + } + private class H extends Handler { private static final int SHOW_DETAIL = 1; private static final int SET_TILE_VISIBILITY = 2; @@ -344,5 +356,6 @@ public class QSPanel extends ViewGroup { public interface Callback { void onShowingDetail(QSTile.DetailAdapter detail); void onToggleStateChanged(boolean state); + void onScanStateChanged(boolean state); } } diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSTile.java b/packages/SystemUI/src/com/android/systemui/qs/QSTile.java index 62c9d9f..fc08cf4 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/QSTile.java +++ b/packages/SystemUI/src/com/android/systemui/qs/QSTile.java @@ -50,7 +50,7 @@ import java.util.Objects; */ public abstract class QSTile<TState extends State> implements Listenable { protected final String TAG = "QSTile." + getClass().getSimpleName(); - protected static final boolean DEBUG = false; + protected static final boolean DEBUG = Log.isLoggable("QSTile", Log.DEBUG); protected final Host mHost; protected final Context mContext; @@ -129,6 +129,10 @@ public abstract class QSTile<TState extends State> implements Listenable { mHandler.obtainMessage(H.TOGGLE_STATE_CHANGED, state ? 1 : 0, 0).sendToTarget(); } + public void fireScanStateChanged(boolean state) { + mHandler.obtainMessage(H.SCAN_STATE_CHANGED, state ? 1 : 0, 0).sendToTarget(); + } + // call only on tile worker looper private void handleSetCallback(Callback callback) { @@ -166,6 +170,12 @@ public abstract class QSTile<TState extends State> implements Listenable { } } + private void handleScanStateChanged(boolean state) { + if (mCallback != null) { + mCallback.onScanStateChanged(state); + } + } + protected void handleUserSwitch(int newUserId) { handleRefreshState(null); } @@ -178,6 +188,7 @@ public abstract class QSTile<TState extends State> implements Listenable { private static final int SHOW_DETAIL = 5; private static final int USER_SWITCH = 6; private static final int TOGGLE_STATE_CHANGED = 7; + private static final int SCAN_STATE_CHANGED = 8; private H(Looper looper) { super(looper); @@ -208,6 +219,9 @@ public abstract class QSTile<TState extends State> implements Listenable { } else if (msg.what == TOGGLE_STATE_CHANGED) { name = "handleToggleStateChanged"; handleToggleStateChanged(msg.arg1 != 0); + } else if (msg.what == SCAN_STATE_CHANGED) { + name = "handleScanStateChanged"; + handleScanStateChanged(msg.arg1 != 0); } } catch (Throwable t) { final String error = "Error in " + name; @@ -221,6 +235,7 @@ public abstract class QSTile<TState extends State> implements Listenable { void onStateChanged(State state); void onShowDetail(boolean show); void onToggleStateChanged(boolean state); + void onScanStateChanged(boolean state); } public interface Host { diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/BluetoothTile.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/BluetoothTile.java index 7431e69..1250d21 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tiles/BluetoothTile.java +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/BluetoothTile.java @@ -16,24 +16,33 @@ package com.android.systemui.qs.tiles; -import android.bluetooth.BluetoothAdapter.BluetoothStateChangeCallback; +import android.content.Context; import android.content.Intent; import android.provider.Settings; import android.text.TextUtils; +import android.view.View; +import android.view.ViewGroup; import com.android.systemui.R; +import com.android.systemui.qs.QSDetailItems; +import com.android.systemui.qs.QSDetailItems.Item; import com.android.systemui.qs.QSTile; import com.android.systemui.statusbar.policy.BluetoothController; +import com.android.systemui.statusbar.policy.BluetoothController.PairedDevice; + +import java.util.Set; /** Quick settings tile: Bluetooth **/ public class BluetoothTile extends QSTile<QSTile.BooleanState> { private static final Intent BLUETOOTH_SETTINGS = new Intent(Settings.ACTION_BLUETOOTH_SETTINGS); private final BluetoothController mController; + private final BluetoothDetailAdapter mDetailAdapter; public BluetoothTile(Host host) { super(host); mController = host.getBluetoothController(); + mDetailAdapter = new BluetoothDetailAdapter(); } @Override @@ -42,6 +51,11 @@ public class BluetoothTile extends QSTile<QSTile.BooleanState> { } @Override + public DetailAdapter getDetailAdapter() { + return mDetailAdapter; + } + + @Override protected BooleanState newTileState() { return new BooleanState(); } @@ -63,7 +77,10 @@ public class BluetoothTile extends QSTile<QSTile.BooleanState> { @Override protected void handleSecondaryClick() { - mHost.startSettingsActivity(BLUETOOTH_SETTINGS); + if (!mState.value) { + mController.setBluetoothEnabled(true); + } + showDetail(true); } @Override @@ -83,7 +100,8 @@ public class BluetoothTile extends QSTile<QSTile.BooleanState> { state.label = mController.getLastDeviceName(); } else if (connecting) { state.iconId = R.drawable.ic_qs_bluetooth_connecting; - stateContentDescription = mContext.getString(R.string.accessibility_desc_connecting); + stateContentDescription = + mContext.getString(R.string.accessibility_desc_connecting); state.label = mContext.getString(R.string.quick_settings_bluetooth_label); } else { state.iconId = R.drawable.ic_qs_bluetooth_on; @@ -106,5 +124,100 @@ public class BluetoothTile extends QSTile<QSTile.BooleanState> { public void onBluetoothStateChange(boolean enabled, boolean connecting) { refreshState(); } + @Override + public void onBluetoothPairedDevicesChanged() { + mUiHandler.post(new Runnable() { + @Override + public void run() { + mDetailAdapter.updateItems(); + } + }); + } }; + + private final class BluetoothDetailAdapter implements DetailAdapter, QSDetailItems.Callback { + private QSDetailItems mItems; + + @Override + public int getTitle() { + return R.string.quick_settings_bluetooth_label; + } + + @Override + public Boolean getToggleState() { + return mState.value; + } + + @Override + public Intent getSettingsIntent() { + return BLUETOOTH_SETTINGS; + } + + @Override + public void setToggleState(boolean state) { + mController.setBluetoothEnabled(state); + showDetail(false); + } + + @Override + public View createDetailView(Context context, View convertView, ViewGroup parent) { + mItems = QSDetailItems.convertOrInflate(context, convertView, parent); + mItems.setTagSuffix("Bluetooth"); + mItems.setEmptyState(R.drawable.ic_qs_bluetooth_detail_empty, + R.string.quick_settings_bluetooth_detail_empty_text); + mItems.setCallback(this); + updateItems(); + setItemsVisible(mState.value); + return mItems; + } + + public void setItemsVisible(boolean visible) { + if (mItems == null) return; + mItems.setItemsVisible(visible); + } + + private void updateItems() { + if (mItems == null) return; + Item[] items = null; + final Set<PairedDevice> devices = mController.getPairedDevices(); + if (devices != null) { + items = new Item[devices.size()]; + int i = 0; + for (PairedDevice device : devices) { + final Item item = new Item(); + item.icon = R.drawable.ic_qs_bluetooth_on; + item.line1 = device.name; + if (device.state == PairedDevice.STATE_CONNECTED) { + item.icon = R.drawable.ic_qs_bluetooth_connected; + item.line2 = mContext.getString(R.string.quick_settings_connected); + item.canDisconnect = true; + } else if (device.state == PairedDevice.STATE_CONNECTING) { + item.icon = R.drawable.ic_qs_bluetooth_connecting; + item.line2 = mContext.getString(R.string.quick_settings_connecting); + } + item.tag = device; + items[i++] = item; + } + } + mItems.setItems(items); + } + + @Override + public void onDetailItemClick(Item item) { + if (item == null || item.tag == null) return; + final PairedDevice device = (PairedDevice) item.tag; + if (device != null && device.state == PairedDevice.STATE_DISCONNECTED) { + mController.connect(device); + } + } + + @Override + public void onDetailItemDisconnect(Item item) { + if (item == null || item.tag == null) return; + final PairedDevice device = (PairedDevice) item.tag; + if (device != null) { + mController.disconnect(device); + } + } + } } diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/WifiTile.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/WifiTile.java index 2876607..900c7b2 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tiles/WifiTile.java +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/WifiTile.java @@ -21,15 +21,12 @@ import android.content.Intent; import android.content.res.Resources; import android.provider.Settings; import android.util.Log; -import android.view.LayoutInflater; import android.view.View; -import android.view.View.OnClickListener; import android.view.ViewGroup; -import android.widget.ImageView; -import android.widget.LinearLayout; -import android.widget.TextView; import com.android.systemui.R; +import com.android.systemui.qs.QSDetailItems; +import com.android.systemui.qs.QSDetailItems.Item; import com.android.systemui.qs.QSTile; import com.android.systemui.qs.QSTileView; import com.android.systemui.qs.SignalTileView; @@ -40,7 +37,6 @@ import com.android.systemui.statusbar.policy.NetworkController.NetworkSignalChan /** Quick settings tile: Wifi **/ public class WifiTile extends QSTile<QSTile.SignalState> { private static final Intent WIFI_SETTINGS = new Intent(Settings.ACTION_WIFI_SETTINGS); - private static final int MAX_ITEMS = 4; // TODO temporary visual restriction private final NetworkController mController; private final WifiDetailAdapter mDetailAdapter; @@ -66,7 +62,6 @@ public class WifiTile extends QSTile<QSTile.SignalState> { if (listening) { mController.addNetworkSignalChangedCallback(mCallback); mController.addAccessPointCallback(mDetailAdapter); - mController.scanForAccessPoints(); } else { mController.removeNetworkSignalChangedCallback(mCallback); mController.removeAccessPointCallback(mDetailAdapter); @@ -108,7 +103,7 @@ public class WifiTile extends QSTile<QSTile.SignalState> { boolean wifiNotConnected = (cb.wifiSignalIconId > 0) && (cb.enabledDesc == null); boolean enabledChanging = state.enabled != cb.enabled; if (enabledChanging) { - mDetailAdapter.postUpdateItems(); + mDetailAdapter.setItemsVisible(cb.enabled); fireToggleStateChanged(cb.enabled); } state.enabled = cb.enabled; @@ -204,9 +199,9 @@ public class WifiTile extends QSTile<QSTile.SignalState> { }; private final class WifiDetailAdapter implements DetailAdapter, - NetworkController.AccessPointCallback { + NetworkController.AccessPointCallback, QSDetailItems.Callback { - private LinearLayout mItems; + private QSDetailItems mItems; private AccessPoint[] mAccessPoints; @Override @@ -232,66 +227,67 @@ public class WifiTile extends QSTile<QSTile.SignalState> { @Override public View createDetailView(Context context, View convertView, ViewGroup parent) { - if (convertView != null) return convertView; - mItems = new LinearLayout(context); - mItems.setOrientation(LinearLayout.VERTICAL); + if (DEBUG) Log.d(TAG, "createDetailView convertView=" + (convertView != null)); + mAccessPoints = null; + mController.scanForAccessPoints(); + fireScanStateChanged(true); + mItems = QSDetailItems.convertOrInflate(context, convertView, parent); + mItems.setTagSuffix("Wifi"); + mItems.setCallback(this); + mItems.setEmptyState(R.drawable.ic_qs_wifi_detail_empty, + R.string.quick_settings_wifi_detail_empty_text); updateItems(); + setItemsVisible(mState.enabled); return mItems; } @Override public void onAccessPointsChanged(final AccessPoint[] accessPoints) { - mUiHandler.post(new Runnable() { - @Override - public void run() { - mAccessPoints = accessPoints; - updateItems(); - } - }); + mAccessPoints = accessPoints; + updateItems(); + if (accessPoints != null && accessPoints.length > 0) { + fireScanStateChanged(false); + } } - public void postUpdateItems() { - mUiHandler.post(new Runnable() { - @Override - public void run() { - updateItems(); - } - }); + @Override + public void onDetailItemClick(Item item) { + if (item == null || item.tag == null) return; + final AccessPoint ap = (AccessPoint) item.tag; + if (!ap.isConnected) { + mController.connect(ap); + } + showDetail(false); + } + + @Override + public void onDetailItemDisconnect(Item item) { + // noop + } + + public void setItemsVisible(boolean visible) { + if (mItems == null) return; + mItems.setItemsVisible(visible); } private void updateItems() { if (mItems == null) return; - mItems.removeAllViews(); - if (mAccessPoints == null || mAccessPoints.length == 0 || !mState.enabled) return; - for (int i = 0; i < mAccessPoints.length; i++) { - final AccessPoint ap = mAccessPoints[i]; - if (ap == null) continue; - final View item = LayoutInflater.from(mContext).inflate(R.layout.qs_detail_item, - mItems, false); - final ImageView iv = (ImageView) item.findViewById(android.R.id.icon); - iv.setImageResource(ap.iconId); - final TextView title = (TextView) item.findViewById(android.R.id.title); - title.setText(ap.ssid); - final TextView summary = (TextView) item.findViewById(android.R.id.summary); - if (ap.isConnected) { - item.setMinimumHeight(mContext.getResources() - .getDimensionPixelSize(R.dimen.qs_detail_item_height_twoline)); - summary.setText(R.string.quick_settings_connected); - } else { - summary.setVisibility(View.GONE); - } - item.setOnClickListener(new OnClickListener() { - @Override - public void onClick(View v) { - if (!ap.isConnected) { - mController.connect(ap); - } - showDetail(false); + Item[] items = null; + if (mAccessPoints != null) { + items = new Item[mAccessPoints.length]; + for (int i = 0; i < mAccessPoints.length; i++) { + final AccessPoint ap = mAccessPoints[i]; + final Item item = new Item(); + item.tag = ap; + item.icon = ap.iconId; + item.line1 = ap.ssid; + if (ap.isConnected) { + item.line2 = mContext.getString(R.string.quick_settings_connected); } - }); - mItems.addView(item); - if (mItems.getChildCount() == MAX_ITEMS) break; + items[i] = item; + } } + mItems.setItems(items); } }; } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBar.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBar.java index 04f9c72..9d7d933 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBar.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBar.java @@ -2316,7 +2316,12 @@ public class PhoneStatusBar extends BaseStatusBar implements DemoMode, mGestureRec.dump(fd, pw, args); } - mNetworkController.dump(fd, pw, args); + if (mNetworkController != null) { + mNetworkController.dump(fd, pw, args); + } + if (mBluetoothController != null) { + mBluetoothController.dump(fd, pw, args); + } } private String hunStateToString(Entry entry) { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarHeaderView.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarHeaderView.java index 52d3cd3..33d1b15 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarHeaderView.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarHeaderView.java @@ -63,6 +63,8 @@ public class StatusBarHeaderView extends RelativeLayout implements View.OnClickL private View mSignalCluster; private View mSettingsButton; private View mQsDetailHeader; + private TextView mQsDetailHeaderTitle; + private Switch mQsDetailHeaderSwitch; private View mEmergencyCallsOnly; private TextView mBatteryLevel; @@ -120,6 +122,8 @@ public class StatusBarHeaderView extends RelativeLayout implements View.OnClickL mSettingsButton.setOnClickListener(this); mQsDetailHeader = findViewById(R.id.qs_detail_header); mQsDetailHeader.setAlpha(0); + mQsDetailHeaderTitle = (TextView) mQsDetailHeader.findViewById(android.R.id.title); + mQsDetailHeaderSwitch = (Switch) mQsDetailHeader.findViewById(android.R.id.toggle); mEmergencyCallsOnly = findViewById(R.id.header_emergency_calls_only); mBatteryLevel = (TextView) findViewById(R.id.battery_level); loadDimens(); @@ -550,10 +554,22 @@ public class StatusBarHeaderView extends RelativeLayout implements View.OnClickL }); } + @Override + public void onScanStateChanged(final boolean state) { + post(new Runnable() { + @Override + public void run() { + handleScanStateChanged(state); + } + }); + } + private void handleToggleStateChanged(boolean state) { - final Switch headerSwitch = (Switch) - mQsDetailHeader.findViewById(android.R.id.toggle); - headerSwitch.setChecked(state); + mQsDetailHeaderSwitch.setChecked(state); + } + + private void handleScanStateChanged(boolean state) { + // TODO - waiting on framework asset } private void handleShowingDetail(final QSTile.DetailAdapter detail) { @@ -561,18 +577,14 @@ public class StatusBarHeaderView extends RelativeLayout implements View.OnClickL transition(mDateTime, !showingDetail); transition(mQsDetailHeader, showingDetail); if (showingDetail) { - final TextView headerTitle = (TextView) - mQsDetailHeader.findViewById(android.R.id.title); - headerTitle.setText(detail.getTitle()); - final Switch headerSwitch = (Switch) - mQsDetailHeader.findViewById(android.R.id.toggle); + mQsDetailHeaderTitle.setText(detail.getTitle()); final Boolean toggleState = detail.getToggleState(); if (toggleState == null) { - headerSwitch.setVisibility(INVISIBLE); + mQsDetailHeaderSwitch.setVisibility(INVISIBLE); mQsDetailHeader.setClickable(false); } else { - headerSwitch.setVisibility(VISIBLE); - headerSwitch.setChecked(toggleState); + mQsDetailHeaderSwitch.setVisibility(VISIBLE); + mQsDetailHeaderSwitch.setChecked(toggleState); mQsDetailHeader.setClickable(true); mQsDetailHeader.setOnClickListener(new OnClickListener() { @Override diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/BluetoothController.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/BluetoothController.java index 8e9fb30..cbdd138 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/BluetoothController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/BluetoothController.java @@ -16,6 +16,8 @@ package com.android.systemui.statusbar.policy; +import java.util.Set; + public interface BluetoothController { void addStateChangedCallback(Callback callback); void removeStateChangedCallback(Callback callback); @@ -26,8 +28,32 @@ public interface BluetoothController { boolean isBluetoothConnecting(); String getLastDeviceName(); void setBluetoothEnabled(boolean enabled); + Set<PairedDevice> getPairedDevices(); + void connect(PairedDevice device); + void disconnect(PairedDevice device); public interface Callback { void onBluetoothStateChange(boolean enabled, boolean connecting); + void onBluetoothPairedDevicesChanged(); + } + + public static final class PairedDevice { + public static int STATE_DISCONNECTED = 0; + public static int STATE_CONNECTING = 1; + public static int STATE_CONNECTED = 2; + public static int STATE_DISCONNECTING = 3; + + public String id; + public String name; + public int state = STATE_DISCONNECTED; + public Object tag; + + public static String stateToString(int state) { + if (state == STATE_DISCONNECTED) return "STATE_DISCONNECTED"; + if (state == STATE_CONNECTING) return "STATE_CONNECTING"; + if (state == STATE_CONNECTED) return "STATE_CONNECTED"; + if (state == STATE_DISCONNECTING) return "STATE_DISCONNECTING"; + return "UNKNOWN"; + } } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/BluetoothControllerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/BluetoothControllerImpl.java index 379b509..f021623 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/BluetoothControllerImpl.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/BluetoothControllerImpl.java @@ -16,49 +16,91 @@ package com.android.systemui.statusbar.policy; +import static android.bluetooth.BluetoothAdapter.ERROR; +import static com.android.systemui.statusbar.policy.BluetoothUtil.connectionStateToString; +import static com.android.systemui.statusbar.policy.BluetoothUtil.deviceToString; +import static com.android.systemui.statusbar.policy.BluetoothUtil.profileStateToString; +import static com.android.systemui.statusbar.policy.BluetoothUtil.profileToString; +import static com.android.systemui.statusbar.policy.BluetoothUtil.uuidToProfile; +import static com.android.systemui.statusbar.policy.BluetoothUtil.uuidToString; +import static com.android.systemui.statusbar.policy.BluetoothUtil.uuidsToString; + import android.bluetooth.BluetoothAdapter; import android.bluetooth.BluetoothDevice; +import android.bluetooth.BluetoothManager; +import android.bluetooth.BluetoothProfile; +import android.bluetooth.BluetoothProfile.ServiceListener; import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; +import android.os.ParcelUuid; +import android.util.ArrayMap; +import android.util.ArraySet; +import android.util.Log; +import android.util.SparseBooleanArray; + +import com.android.systemui.statusbar.policy.BluetoothUtil.Profile; +import java.io.FileDescriptor; +import java.io.PrintWriter; import java.util.ArrayList; -import java.util.HashSet; import java.util.Set; -public class BluetoothControllerImpl extends BroadcastReceiver implements BluetoothController { - private static final String TAG = "StatusBar.BluetoothController"; +public class BluetoothControllerImpl implements BluetoothController { + private static final String TAG = "BluetoothController"; + private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG); + private final Context mContext; private final ArrayList<Callback> mCallbacks = new ArrayList<Callback>(); - private final Set<BluetoothDevice> mBondedDevices = new HashSet<BluetoothDevice>(); private final BluetoothAdapter mAdapter; + private final Receiver mReceiver = new Receiver(); + private final ArrayMap<BluetoothDevice, DeviceInfo> mDeviceInfo = new ArrayMap<>(); private boolean mEnabled; private boolean mConnecting; private BluetoothDevice mLastDevice; public BluetoothControllerImpl(Context context) { - mAdapter = BluetoothAdapter.getDefaultAdapter(); - - IntentFilter filter = new IntentFilter(); - filter.addAction(BluetoothAdapter.ACTION_STATE_CHANGED); - filter.addAction(BluetoothAdapter.ACTION_CONNECTION_STATE_CHANGED); - filter.addAction(BluetoothDevice.ACTION_BOND_STATE_CHANGED); - filter.addAction(BluetoothDevice.ACTION_ALIAS_CHANGED); - context.registerReceiver(this, filter); - - final BluetoothAdapter adapter = BluetoothAdapter.getDefaultAdapter(); - if (adapter != null) { - handleAdapterStateChange(adapter.getState()); + mContext = context; + final BluetoothManager bluetoothManager = + (BluetoothManager) context.getSystemService(Context.BLUETOOTH_SERVICE); + mAdapter = bluetoothManager.getAdapter(); + if (mAdapter == null) { + Log.w(TAG, "Default BT adapter not found"); + return; } - fireCallbacks(); + + mReceiver.register(); + setAdapterState(mAdapter.getState()); updateBondedBluetoothDevices(); } + public void dump(FileDescriptor fd, PrintWriter pw, String[] args) { + pw.println("BluetoothController state:"); + pw.print(" mAdapter="); pw.println(mAdapter); + pw.print(" mEnabled="); pw.println(mEnabled); + pw.print(" mConnecting="); pw.println(mConnecting); + pw.print(" mLastDevice="); pw.println(mLastDevice); + pw.print(" mCallbacks.size="); pw.println(mCallbacks.size()); + pw.print(" mDeviceInfo.size="); pw.println(mDeviceInfo.size()); + for (int i = 0; i < mDeviceInfo.size(); i++) { + final BluetoothDevice device = mDeviceInfo.keyAt(i); + final DeviceInfo info = mDeviceInfo.valueAt(i); + pw.print(" "); pw.print(deviceToString(device)); + pw.print('('); pw.print(uuidsToString(device)); pw.print(')'); + pw.print(" "); pw.println(infoToString(info)); + } + } + + private static String infoToString(DeviceInfo info) { + return info == null ? null : ("connectionState=" + + connectionStateToString(info.connectionState) + ",bonded=" + info.bonded); + } + public void addStateChangedCallback(Callback cb) { mCallbacks.add(cb); - fireCallback(cb); + fireStateChange(cb); } @Override @@ -99,64 +141,191 @@ public class BluetoothControllerImpl extends BroadcastReceiver implements Blueto return mAdapter != null; } - public Set<BluetoothDevice> getBondedBluetoothDevices() { - return mBondedDevices; + @Override + public ArraySet<PairedDevice> getPairedDevices() { + final ArraySet<PairedDevice> rt = new ArraySet<>(); + for (int i = 0; i < mDeviceInfo.size(); i++) { + final BluetoothDevice device = mDeviceInfo.keyAt(i); + final DeviceInfo info = mDeviceInfo.valueAt(i); + if (!info.bonded) continue; + final PairedDevice paired = new PairedDevice(); + paired.id = device.getAddress(); + paired.tag = device; + paired.name = device.getAliasName(); + paired.state = connectionStateToPairedDeviceState(info.connectionState); + rt.add(paired); + } + return rt; + } + + private static int connectionStateToPairedDeviceState(int state) { + if (state == BluetoothAdapter.STATE_CONNECTED) return PairedDevice.STATE_CONNECTED; + if (state == BluetoothAdapter.STATE_CONNECTING) return PairedDevice.STATE_CONNECTING; + if (state == BluetoothAdapter.STATE_DISCONNECTING) return PairedDevice.STATE_DISCONNECTING; + return PairedDevice.STATE_DISCONNECTED; } @Override - public String getLastDeviceName() { - return mLastDevice != null ? mLastDevice.getAliasName() - : mBondedDevices.size() == 1 ? mBondedDevices.iterator().next().getAliasName() - : null; + public void connect(final PairedDevice pd) { + connect(pd, true); } @Override - public void onReceive(Context context, Intent intent) { - final String action = intent.getAction(); + public void disconnect(PairedDevice pd) { + connect(pd, false); + } - if (action.equals(BluetoothAdapter.ACTION_STATE_CHANGED)) { - handleAdapterStateChange( - intent.getIntExtra(BluetoothAdapter.EXTRA_STATE, BluetoothAdapter.ERROR)); - } - if (action.equals(BluetoothAdapter.ACTION_CONNECTION_STATE_CHANGED)) { - mConnecting = intent.getIntExtra(BluetoothAdapter.EXTRA_CONNECTION_STATE, -1) - == BluetoothAdapter.STATE_CONNECTING; - mLastDevice = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE); + private void connect(PairedDevice pd, final boolean connect) { + if (mAdapter == null || pd == null || pd.tag == null) return; + final BluetoothDevice device = (BluetoothDevice) pd.tag; + final String action = connect ? "connect" : "disconnect"; + if (DEBUG) Log.d(TAG, action + " " + deviceToString(device)); + final SparseBooleanArray profiles = new SparseBooleanArray(); + for (ParcelUuid uuid : device.getUuids()) { + final int profile = uuidToProfile(uuid); + if (profile == 0) { + Log.w(TAG, "Device " + deviceToString(device) + " has an unsupported uuid: " + + uuidToString(uuid)); + continue; + } + final int profileState = mAdapter.getProfileConnectionState(profile); + if (DEBUG && !profiles.get(profile)) Log.d(TAG, "Profile " + profileToString(profile) + + " state = " + profileStateToString(profileState)); + final boolean connected = profileState == BluetoothProfile.STATE_CONNECTED; + if (connect != connected) { + profiles.put(profile, true); + } } - if (action.equals(BluetoothDevice.ACTION_ALIAS_CHANGED)) { - mLastDevice = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE); + for (int i = 0; i < profiles.size(); i++) { + final int profile = profiles.keyAt(i); + mAdapter.getProfileProxy(mContext, new ServiceListener() { + @Override + public void onServiceConnected(int profile, BluetoothProfile proxy) { + if (DEBUG) Log.d(TAG, "onServiceConnected " + profileToString(profile)); + final Profile p = BluetoothUtil.getProfile(proxy); + if (p == null) { + Log.w(TAG, "Unable get get Profile for " + profileToString(profile)); + } else { + final boolean ok = connect ? p.connect(device) : p.disconnect(device); + if (DEBUG) Log.d(TAG, action + " " + profileToString(profile) + " " + + (ok ? "succeeded" : "failed")); + } + } + + @Override + public void onServiceDisconnected(int profile) { + if (DEBUG) Log.d(TAG, "onServiceDisconnected " + profileToString(profile)); + } + }, profile); } - fireCallbacks(); - updateBondedBluetoothDevices(); + } + + @Override + public String getLastDeviceName() { + return mLastDevice != null ? mLastDevice.getAliasName() : null; } private void updateBondedBluetoothDevices() { - mBondedDevices.clear(); - - BluetoothAdapter adapter = BluetoothAdapter.getDefaultAdapter(); - if (adapter != null) { - Set<BluetoothDevice> devices = adapter.getBondedDevices(); - if (devices != null) { - for (BluetoothDevice device : devices) { - if (device.getBondState() != BluetoothDevice.BOND_NONE) { - mBondedDevices.add(device); - } + if (mAdapter == null) return; + final Set<BluetoothDevice> bondedDevices = mAdapter.getBondedDevices(); + for (DeviceInfo info : mDeviceInfo.values()) { + info.bonded = false; + } + int bondedCount = 0; + BluetoothDevice lastBonded = null; + if (bondedDevices != null) { + for (BluetoothDevice bondedDevice : bondedDevices) { + final boolean bonded = bondedDevice.getBondState() != BluetoothDevice.BOND_NONE; + updateInfo(bondedDevice).bonded = bonded; + if (bonded) { + bondedCount++; + lastBonded = bondedDevice; } } } + if (mLastDevice == null && bondedCount == 1) { + mLastDevice = lastBonded; + } + firePairedDevicesChanged(); + } + + private void firePairedDevicesChanged() { + for (Callback cb : mCallbacks) { + cb.onBluetoothPairedDevicesChanged(); + } + } + + private void setAdapterState(int adapterState) { + final boolean enabled = adapterState == BluetoothAdapter.STATE_ON; + if (mEnabled == enabled) return; + mEnabled = enabled; + fireStateChange(); } - private void handleAdapterStateChange(int adapterState) { - mEnabled = (adapterState == BluetoothAdapter.STATE_ON); + private void setConnecting(boolean connecting) { + if (mConnecting == connecting) return; + mConnecting = connecting; + fireStateChange(); } - private void fireCallbacks() { + private void fireStateChange() { for (Callback cb : mCallbacks) { - fireCallback(cb); + fireStateChange(cb); } } - private void fireCallback(Callback cb) { + private void fireStateChange(Callback cb) { cb.onBluetoothStateChange(mEnabled, mConnecting); } + + private final class Receiver extends BroadcastReceiver { + public void register() { + final IntentFilter filter = new IntentFilter(); + filter.addAction(BluetoothAdapter.ACTION_STATE_CHANGED); + filter.addAction(BluetoothAdapter.ACTION_CONNECTION_STATE_CHANGED); + filter.addAction(BluetoothDevice.ACTION_BOND_STATE_CHANGED); + filter.addAction(BluetoothDevice.ACTION_ALIAS_CHANGED); + mContext.registerReceiver(this, filter); + } + + @Override + public void onReceive(Context context, Intent intent) { + final String action = intent.getAction(); + final BluetoothDevice device = intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE); + if (action.equals(BluetoothAdapter.ACTION_STATE_CHANGED)) { + setAdapterState(intent.getIntExtra(BluetoothAdapter.EXTRA_STATE, ERROR)); + if (DEBUG) Log.d(TAG, "ACTION_STATE_CHANGED " + mEnabled); + } else if (action.equals(BluetoothAdapter.ACTION_CONNECTION_STATE_CHANGED)) { + final DeviceInfo info = updateInfo(device); + final int state = intent.getIntExtra(BluetoothAdapter.EXTRA_CONNECTION_STATE, + ERROR); + if (state != ERROR) { + info.connectionState = state; + } + mLastDevice = device; + if (DEBUG) Log.d(TAG, "ACTION_CONNECTION_STATE_CHANGED " + + connectionStateToString(state) + " " + deviceToString(device)); + setConnecting(info.connectionState == BluetoothAdapter.STATE_CONNECTING); + } else if (action.equals(BluetoothDevice.ACTION_ALIAS_CHANGED)) { + updateInfo(device); + mLastDevice = device; + } else if (action.equals(BluetoothDevice.ACTION_BOND_STATE_CHANGED)) { + if (DEBUG) Log.d(TAG, "ACTION_BOND_STATE_CHANGED " + device); + // we'll update all bonded devices below + } + updateBondedBluetoothDevices(); + } + } + + private DeviceInfo updateInfo(BluetoothDevice device) { + DeviceInfo info = mDeviceInfo.get(device); + info = info != null ? info : new DeviceInfo(); + mDeviceInfo.put(device, info); + return info; + } + + private static class DeviceInfo { + int connectionState = BluetoothAdapter.STATE_DISCONNECTED; + boolean bonded; // per getBondedDevices + } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/BluetoothUtil.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/BluetoothUtil.java new file mode 100644 index 0000000..1b4be85 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/BluetoothUtil.java @@ -0,0 +1,226 @@ +/* + * Copyright (C) 2014 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.systemui.statusbar.policy; + +import android.bluetooth.BluetoothA2dp; +import android.bluetooth.BluetoothA2dpSink; +import android.bluetooth.BluetoothAdapter; +import android.bluetooth.BluetoothDevice; +import android.bluetooth.BluetoothHeadset; +import android.bluetooth.BluetoothHeadsetClient; +import android.bluetooth.BluetoothInputDevice; +import android.bluetooth.BluetoothMap; +import android.bluetooth.BluetoothPan; +import android.bluetooth.BluetoothProfile; +import android.bluetooth.BluetoothUuid; +import android.os.ParcelUuid; +import android.text.TextUtils; + +public class BluetoothUtil { + + public static String profileToString(int profile) { + if (profile == BluetoothProfile.HEADSET) return "HEADSET"; + if (profile == BluetoothProfile.A2DP) return "A2DP"; + if (profile == BluetoothProfile.AVRCP_CONTROLLER) return "AVRCP_CONTROLLER"; + return "UNKNOWN"; + } + + public static String profileStateToString(int state) { + if (state == BluetoothProfile.STATE_CONNECTED) return "STATE_CONNECTED"; + if (state == BluetoothProfile.STATE_CONNECTING) return "STATE_CONNECTING"; + if (state == BluetoothProfile.STATE_DISCONNECTED) return "STATE_DISCONNECTED"; + if (state == BluetoothProfile.STATE_DISCONNECTING) return "STATE_DISCONNECTING"; + return "STATE_UNKNOWN"; + } + + public static String uuidToString(ParcelUuid uuid) { + if (BluetoothUuid.AudioSink.equals(uuid)) return "AudioSink"; + if (BluetoothUuid.AudioSource.equals(uuid)) return "AudioSource"; + if (BluetoothUuid.AdvAudioDist.equals(uuid)) return "AdvAudioDist"; + if (BluetoothUuid.HSP.equals(uuid)) return "HSP"; + if (BluetoothUuid.HSP_AG.equals(uuid)) return "HSP_AG"; + if (BluetoothUuid.Handsfree.equals(uuid)) return "Handsfree"; + if (BluetoothUuid.Handsfree_AG.equals(uuid)) return "Handsfree_AG"; + if (BluetoothUuid.AvrcpController.equals(uuid)) return "AvrcpController"; + if (BluetoothUuid.AvrcpTarget.equals(uuid)) return "AvrcpTarget"; + if (BluetoothUuid.ObexObjectPush.equals(uuid)) return "ObexObjectPush"; + if (BluetoothUuid.Hid.equals(uuid)) return "Hid"; + if (BluetoothUuid.Hogp.equals(uuid)) return "Hogp"; + if (BluetoothUuid.PANU.equals(uuid)) return "PANU"; + if (BluetoothUuid.NAP.equals(uuid)) return "NAP"; + if (BluetoothUuid.BNEP.equals(uuid)) return "BNEP"; + if (BluetoothUuid.PBAP_PSE.equals(uuid)) return "PBAP_PSE"; + if (BluetoothUuid.MAP.equals(uuid)) return "MAP"; + if (BluetoothUuid.MNS.equals(uuid)) return "MNS"; + if (BluetoothUuid.MAS.equals(uuid)) return "MAS"; + return uuid != null ? uuid.toString() : null; + } + + public static String connectionStateToString(int connectionState) { + if (connectionState == BluetoothAdapter.STATE_DISCONNECTED) return "STATE_DISCONNECTED"; + if (connectionState == BluetoothAdapter.STATE_CONNECTED) return "STATE_CONNECTED"; + if (connectionState == BluetoothAdapter.STATE_DISCONNECTING) return "STATE_DISCONNECTING"; + if (connectionState == BluetoothAdapter.STATE_CONNECTING) return "STATE_CONNECTING"; + return "ERROR"; + } + + public static String deviceToString(BluetoothDevice device) { + return device == null ? null : (device.getAddress() + '[' + device.getAliasName() + ']'); + } + + public static String uuidsToString(BluetoothDevice device) { + if (device == null) return null; + final ParcelUuid[] ids = device.getUuids(); + if (ids == null) return null; + final String[] tokens = new String[ids.length]; + for (int i = 0; i < tokens.length; i++) { + tokens[i] = uuidToString(ids[i]); + } + return TextUtils.join(",", tokens); + } + + public static int uuidToProfile(ParcelUuid uuid) { + if (BluetoothUuid.AudioSink.equals(uuid)) return BluetoothProfile.A2DP; + if (BluetoothUuid.AdvAudioDist.equals(uuid)) return BluetoothProfile.A2DP; + + if (BluetoothUuid.HSP.equals(uuid)) return BluetoothProfile.HEADSET; + if (BluetoothUuid.Handsfree.equals(uuid)) return BluetoothProfile.HEADSET; + + if (BluetoothUuid.MAP.equals(uuid)) return BluetoothProfile.MAP; + if (BluetoothUuid.MNS.equals(uuid)) return BluetoothProfile.MAP; + if (BluetoothUuid.MAS.equals(uuid)) return BluetoothProfile.MAP; + + if (BluetoothUuid.AvrcpController.equals(uuid)) return BluetoothProfile.AVRCP_CONTROLLER; + + return 0; + } + + public static Profile getProfile(BluetoothProfile p) { + if (p instanceof BluetoothA2dp) return newProfile((BluetoothA2dp) p); + if (p instanceof BluetoothHeadset) return newProfile((BluetoothHeadset) p); + if (p instanceof BluetoothA2dpSink) return newProfile((BluetoothA2dpSink) p); + if (p instanceof BluetoothHeadsetClient) return newProfile((BluetoothHeadsetClient) p); + if (p instanceof BluetoothInputDevice) return newProfile((BluetoothInputDevice) p); + if (p instanceof BluetoothMap) return newProfile((BluetoothMap) p); + if (p instanceof BluetoothPan) return newProfile((BluetoothPan) p); + return null; + } + + private static Profile newProfile(final BluetoothA2dp a2dp) { + return new Profile() { + @Override + public boolean connect(BluetoothDevice device) { + return a2dp.connect(device); + } + + @Override + public boolean disconnect(BluetoothDevice device) { + return a2dp.disconnect(device); + } + }; + } + + private static Profile newProfile(final BluetoothHeadset headset) { + return new Profile() { + @Override + public boolean connect(BluetoothDevice device) { + return headset.connect(device); + } + + @Override + public boolean disconnect(BluetoothDevice device) { + return headset.disconnect(device); + } + }; + } + + private static Profile newProfile(final BluetoothA2dpSink sink) { + return new Profile() { + @Override + public boolean connect(BluetoothDevice device) { + return sink.connect(device); + } + + @Override + public boolean disconnect(BluetoothDevice device) { + return sink.disconnect(device); + } + }; + } + + private static Profile newProfile(final BluetoothHeadsetClient client) { + return new Profile() { + @Override + public boolean connect(BluetoothDevice device) { + return client.connect(device); + } + + @Override + public boolean disconnect(BluetoothDevice device) { + return client.disconnect(device); + } + }; + } + + private static Profile newProfile(final BluetoothInputDevice input) { + return new Profile() { + @Override + public boolean connect(BluetoothDevice device) { + return input.connect(device); + } + + @Override + public boolean disconnect(BluetoothDevice device) { + return input.disconnect(device); + } + }; + } + + private static Profile newProfile(final BluetoothMap map) { + return new Profile() { + @Override + public boolean connect(BluetoothDevice device) { + return map.connect(device); + } + + @Override + public boolean disconnect(BluetoothDevice device) { + return map.disconnect(device); + } + }; + } + + private static Profile newProfile(final BluetoothPan pan) { + return new Profile() { + @Override + public boolean connect(BluetoothDevice device) { + return pan.connect(device); + } + + @Override + public boolean disconnect(BluetoothDevice device) { + return pan.disconnect(device); + } + }; + } + + // common abstraction for supported profiles + public interface Profile { + boolean connect(BluetoothDevice device); + boolean disconnect(BluetoothDevice device); + } +} |