• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2019 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.settings.bluetooth;
18 
19 import android.bluetooth.BluetoothAdapter;
20 import android.bluetooth.BluetoothDevice;
21 import android.content.Context;
22 import android.graphics.Bitmap;
23 import android.graphics.PorterDuff;
24 import android.graphics.PorterDuffColorFilter;
25 import android.graphics.drawable.Drawable;
26 import android.net.Uri;
27 import android.os.Handler;
28 import android.os.Looper;
29 import android.provider.DeviceConfig;
30 import android.provider.MediaStore;
31 import android.util.Log;
32 import android.view.View;
33 import android.widget.ImageView;
34 import android.widget.LinearLayout;
35 import android.widget.TextView;
36 
37 import androidx.annotation.VisibleForTesting;
38 import androidx.preference.PreferenceScreen;
39 
40 import com.android.settings.R;
41 import com.android.settings.core.BasePreferenceController;
42 import com.android.settings.core.SettingsUIDeviceConfig;
43 import com.android.settings.fuelgauge.BatteryMeterView;
44 import com.android.settingslib.bluetooth.BluetoothUtils;
45 import com.android.settingslib.bluetooth.CachedBluetoothDevice;
46 import com.android.settingslib.core.lifecycle.LifecycleObserver;
47 import com.android.settingslib.core.lifecycle.events.OnDestroy;
48 import com.android.settingslib.core.lifecycle.events.OnStart;
49 import com.android.settingslib.core.lifecycle.events.OnStop;
50 import com.android.settingslib.utils.ThreadUtils;
51 import com.android.settingslib.widget.LayoutPreference;
52 
53 import java.io.IOException;
54 import java.util.HashMap;
55 import java.util.Map;
56 
57 /**
58  * This class adds a header with device name and status (connected/disconnected, etc.).
59  */
60 public class AdvancedBluetoothDetailsHeaderController extends BasePreferenceController implements
61         LifecycleObserver, OnStart, OnStop, OnDestroy, CachedBluetoothDevice.Callback {
62     private static final String TAG = "AdvancedBtHeaderCtrl";
63 
64     @VisibleForTesting
65     LayoutPreference mLayoutPreference;
66     @VisibleForTesting
67     final Map<String, Bitmap> mIconCache;
68     private CachedBluetoothDevice mCachedDevice;
69     @VisibleForTesting
70     BluetoothAdapter mBluetoothAdapter;
71     @VisibleForTesting
72     Handler mHandler = new Handler(Looper.getMainLooper());
73     @VisibleForTesting
74     final BluetoothAdapter.OnMetadataChangedListener mMetadataListener =
75             new BluetoothAdapter.OnMetadataChangedListener() {
76                 @Override
77                 public void onMetadataChanged(BluetoothDevice device, int key, byte[] value) {
78                     Log.i(TAG, String.format("Metadata updated in Device %s: %d = %s.", device, key,
79                             value == null ? null : new String(value)));
80                     refresh();
81                 }
82             };
83 
AdvancedBluetoothDetailsHeaderController(Context context, String prefKey)84     public AdvancedBluetoothDetailsHeaderController(Context context, String prefKey) {
85         super(context, prefKey);
86         mBluetoothAdapter = BluetoothAdapter.getDefaultAdapter();
87         mIconCache = new HashMap<>();
88     }
89 
90     @Override
getAvailabilityStatus()91     public int getAvailabilityStatus() {
92         final boolean advancedEnabled = DeviceConfig.getBoolean(DeviceConfig.NAMESPACE_SETTINGS_UI,
93                 SettingsUIDeviceConfig.BT_ADVANCED_HEADER_ENABLED, true);
94         final boolean untetheredHeadset = BluetoothUtils.getBooleanMetaData(
95                 mCachedDevice.getDevice(), BluetoothDevice.METADATA_IS_UNTETHERED_HEADSET);
96         return advancedEnabled && untetheredHeadset ? AVAILABLE : CONDITIONALLY_UNAVAILABLE;
97     }
98 
99     @Override
displayPreference(PreferenceScreen screen)100     public void displayPreference(PreferenceScreen screen) {
101         super.displayPreference(screen);
102         mLayoutPreference = screen.findPreference(getPreferenceKey());
103         mLayoutPreference.setVisible(isAvailable());
104 
105         refresh();
106     }
107 
108     @Override
onStart()109     public void onStart() {
110         if (!isAvailable()) {
111             return;
112         }
113         mCachedDevice.registerCallback(this::onDeviceAttributesChanged);
114         mBluetoothAdapter.addOnMetadataChangedListener(mCachedDevice.getDevice(),
115                 mContext.getMainExecutor(), mMetadataListener);
116     }
117 
118     @Override
onStop()119     public void onStop() {
120         if (!isAvailable()) {
121             return;
122         }
123         mCachedDevice.unregisterCallback(this::onDeviceAttributesChanged);
124         mBluetoothAdapter.removeOnMetadataChangedListener(mCachedDevice.getDevice(),
125                 mMetadataListener);
126     }
127 
128     @Override
onDestroy()129     public void onDestroy() {
130         if (!isAvailable()) {
131             return;
132         }
133         // Destroy icon bitmap associated with this header
134         for (Bitmap bitmap : mIconCache.values()) {
135             if (bitmap != null) {
136                 bitmap.recycle();
137             }
138         }
139         mIconCache.clear();
140     }
141 
init(CachedBluetoothDevice cachedBluetoothDevice)142     public void init(CachedBluetoothDevice cachedBluetoothDevice) {
143         mCachedDevice = cachedBluetoothDevice;
144     }
145 
146     @VisibleForTesting
refresh()147     void refresh() {
148         if (mLayoutPreference != null && mCachedDevice != null) {
149             final TextView title = mLayoutPreference.findViewById(R.id.entity_header_title);
150             title.setText(mCachedDevice.getName());
151             final TextView summary = mLayoutPreference.findViewById(R.id.entity_header_summary);
152             summary.setText(mCachedDevice.getConnectionSummary(true /* shortSummary */));
153 
154             if (!mCachedDevice.isConnected()) {
155                 updateDisconnectLayout();
156                 return;
157             }
158 
159             updateSubLayout(mLayoutPreference.findViewById(R.id.layout_left),
160                     BluetoothDevice.METADATA_UNTETHERED_LEFT_ICON,
161                     BluetoothDevice.METADATA_UNTETHERED_LEFT_BATTERY,
162                     BluetoothDevice.METADATA_UNTETHERED_LEFT_CHARGING,
163                     R.string.bluetooth_left_name);
164 
165             updateSubLayout(mLayoutPreference.findViewById(R.id.layout_middle),
166                     BluetoothDevice.METADATA_UNTETHERED_CASE_ICON,
167                     BluetoothDevice.METADATA_UNTETHERED_CASE_BATTERY,
168                     BluetoothDevice.METADATA_UNTETHERED_CASE_CHARGING,
169                     R.string.bluetooth_middle_name);
170 
171             updateSubLayout(mLayoutPreference.findViewById(R.id.layout_right),
172                     BluetoothDevice.METADATA_UNTETHERED_RIGHT_ICON,
173                     BluetoothDevice.METADATA_UNTETHERED_RIGHT_BATTERY,
174                     BluetoothDevice.METADATA_UNTETHERED_RIGHT_CHARGING,
175                     R.string.bluetooth_right_name);
176         }
177     }
178 
179     @VisibleForTesting
createBtBatteryIcon(Context context, int level, boolean charging)180     Drawable createBtBatteryIcon(Context context, int level, boolean charging) {
181         final BatteryMeterView.BatteryMeterDrawable drawable =
182                 new BatteryMeterView.BatteryMeterDrawable(context,
183                         context.getColor(R.color.meter_background_color));
184         drawable.setBatteryLevel(level);
185         drawable.setColorFilter(new PorterDuffColorFilter(
186                 com.android.settings.Utils.getColorAttrDefaultColor(context,
187                         android.R.attr.colorControlNormal),
188                 PorterDuff.Mode.SRC_IN));
189         drawable.setCharging(charging);
190 
191         return drawable;
192     }
193 
updateSubLayout(LinearLayout linearLayout, int iconMetaKey, int batteryMetaKey, int chargeMetaKey, int titleResId)194     private void updateSubLayout(LinearLayout linearLayout, int iconMetaKey, int batteryMetaKey,
195             int chargeMetaKey, int titleResId) {
196         if (linearLayout == null) {
197             return;
198         }
199         final BluetoothDevice bluetoothDevice = mCachedDevice.getDevice();
200         final String iconUri = BluetoothUtils.getStringMetaData(bluetoothDevice, iconMetaKey);
201         if (iconUri != null) {
202             final ImageView imageView = linearLayout.findViewById(R.id.header_icon);
203             updateIcon(imageView, iconUri);
204         }
205 
206         final int batteryLevel = BluetoothUtils.getIntMetaData(bluetoothDevice, batteryMetaKey);
207         final boolean charging = BluetoothUtils.getBooleanMetaData(bluetoothDevice, chargeMetaKey);
208         if (batteryLevel != BluetoothUtils.META_INT_ERROR) {
209             linearLayout.setVisibility(View.VISIBLE);
210             final ImageView imageView = linearLayout.findViewById(R.id.bt_battery_icon);
211             imageView.setImageDrawable(createBtBatteryIcon(mContext, batteryLevel, charging));
212             imageView.setVisibility(View.VISIBLE);
213             final TextView textView = linearLayout.findViewById(R.id.bt_battery_summary);
214             textView.setText(com.android.settings.Utils.formatPercentage(batteryLevel));
215             textView.setVisibility(View.VISIBLE);
216         } else {
217             // Hide it if it doesn't have battery information
218             linearLayout.setVisibility(View.GONE);
219         }
220 
221         final TextView textView = linearLayout.findViewById(R.id.header_title);
222         textView.setText(titleResId);
223         textView.setVisibility(View.VISIBLE);
224     }
225 
updateDisconnectLayout()226     private void updateDisconnectLayout() {
227         mLayoutPreference.findViewById(R.id.layout_left).setVisibility(View.GONE);
228         mLayoutPreference.findViewById(R.id.layout_right).setVisibility(View.GONE);
229 
230         // Hide title, battery icon and battery summary
231         final LinearLayout linearLayout = mLayoutPreference.findViewById(R.id.layout_middle);
232         linearLayout.setVisibility(View.VISIBLE);
233         linearLayout.findViewById(R.id.header_title).setVisibility(View.GONE);
234         linearLayout.findViewById(R.id.bt_battery_summary).setVisibility(View.GONE);
235         linearLayout.findViewById(R.id.bt_battery_icon).setVisibility(View.GONE);
236 
237         // Only show bluetooth icon
238         final BluetoothDevice bluetoothDevice = mCachedDevice.getDevice();
239         final String iconUri = BluetoothUtils.getStringMetaData(bluetoothDevice,
240                 BluetoothDevice.METADATA_MAIN_ICON);
241         if (iconUri != null) {
242             final ImageView imageView = linearLayout.findViewById(R.id.header_icon);
243             updateIcon(imageView, iconUri);
244         }
245     }
246 
247     /**
248      * Update icon by {@code iconUri}. If icon exists in cache, use it; otherwise extract it
249      * from uri in background thread and update it in main thread.
250      */
251     @VisibleForTesting
updateIcon(ImageView imageView, String iconUri)252     void updateIcon(ImageView imageView, String iconUri) {
253         if (mIconCache.containsKey(iconUri)) {
254             imageView.setImageBitmap(mIconCache.get(iconUri));
255             return;
256         }
257 
258         ThreadUtils.postOnBackgroundThread(() -> {
259             try {
260                 final Bitmap bitmap = MediaStore.Images.Media.getBitmap(
261                         mContext.getContentResolver(), Uri.parse(iconUri));
262                 ThreadUtils.postOnMainThread(() -> {
263                     mIconCache.put(iconUri, bitmap);
264                     imageView.setImageBitmap(bitmap);
265                 });
266             } catch (IOException e) {
267                 Log.e(TAG, "Failed to get bitmap for: " + iconUri);
268             }
269         });
270     }
271 
272     @Override
onDeviceAttributesChanged()273     public void onDeviceAttributesChanged() {
274         if (mCachedDevice != null) {
275             refresh();
276         }
277     }
278 }
279