1 /*
2  * Copyright (C) 2008 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.content.BroadcastReceiver;
21 import android.content.Context;
22 import android.content.Intent;
23 import android.content.IntentFilter;
24 import android.content.SharedPreferences;
25 import android.os.Handler;
26 import android.os.SystemProperties;
27 import android.support.v7.preference.Preference;
28 import android.util.Log;
29 
30 import com.android.settings.R;
31 import com.android.settingslib.bluetooth.BluetoothDiscoverableTimeoutReceiver;
32 import com.android.settingslib.bluetooth.LocalBluetoothAdapter;
33 
34 /**
35  * BluetoothDiscoverableEnabler is a helper to manage the "Discoverable"
36  * checkbox. It sets/unsets discoverability and keeps track of how much time
37  * until the the discoverability is automatically turned off.
38  */
39 final class BluetoothDiscoverableEnabler implements Preference.OnPreferenceClickListener {
40 
41     private static final String TAG = "BluetoothDiscoverableEnabler";
42 
43     private static final String SYSTEM_PROPERTY_DISCOVERABLE_TIMEOUT =
44             "debug.bt.discoverable_time";
45 
46     private static final int DISCOVERABLE_TIMEOUT_TWO_MINUTES = 120;
47     private static final int DISCOVERABLE_TIMEOUT_FIVE_MINUTES = 300;
48     private static final int DISCOVERABLE_TIMEOUT_ONE_HOUR = 3600;
49     static final int DISCOVERABLE_TIMEOUT_NEVER = 0;
50 
51     // Bluetooth advanced settings screen was replaced with action bar items.
52     // Use the same preference key for discoverable timeout as the old ListPreference.
53     private static final String KEY_DISCOVERABLE_TIMEOUT = "bt_discoverable_timeout";
54 
55     private static final String VALUE_DISCOVERABLE_TIMEOUT_TWO_MINUTES = "twomin";
56     private static final String VALUE_DISCOVERABLE_TIMEOUT_FIVE_MINUTES = "fivemin";
57     private static final String VALUE_DISCOVERABLE_TIMEOUT_ONE_HOUR = "onehour";
58     private static final String VALUE_DISCOVERABLE_TIMEOUT_NEVER = "never";
59 
60     static final int DEFAULT_DISCOVERABLE_TIMEOUT = DISCOVERABLE_TIMEOUT_TWO_MINUTES;
61 
62     private Context mContext;
63     private final Handler mUiHandler;
64     private final Preference mDiscoveryPreference;
65 
66     private final LocalBluetoothAdapter mLocalAdapter;
67 
68     private final SharedPreferences mSharedPreferences;
69 
70     private boolean mDiscoverable;
71     private int mNumberOfPairedDevices;
72 
73     private int mTimeoutSecs = -1;
74 
75     private final BroadcastReceiver mReceiver = new BroadcastReceiver() {
76         @Override
77         public void onReceive(Context context, Intent intent) {
78             if (BluetoothAdapter.ACTION_SCAN_MODE_CHANGED.equals(intent.getAction())) {
79                 int mode = intent.getIntExtra(BluetoothAdapter.EXTRA_SCAN_MODE,
80                         BluetoothAdapter.ERROR);
81                 if (mode != BluetoothAdapter.ERROR) {
82                     handleModeChanged(mode);
83                 }
84             }
85         }
86     };
87 
88     private final Runnable mUpdateCountdownSummaryRunnable = new Runnable() {
89         public void run() {
90             updateCountdownSummary();
91         }
92     };
93 
BluetoothDiscoverableEnabler(LocalBluetoothAdapter adapter, Preference discoveryPreference)94     BluetoothDiscoverableEnabler(LocalBluetoothAdapter adapter,
95             Preference discoveryPreference) {
96         mUiHandler = new Handler();
97         mLocalAdapter = adapter;
98         mDiscoveryPreference = discoveryPreference;
99         mSharedPreferences = discoveryPreference.getSharedPreferences();
100         discoveryPreference.setPersistent(false);
101     }
102 
resume(Context context)103     public void resume(Context context) {
104         if (mLocalAdapter == null) {
105             return;
106         }
107 
108         if (mContext != context) {
109             mContext = context;
110         }
111 
112         IntentFilter filter = new IntentFilter(BluetoothAdapter.ACTION_SCAN_MODE_CHANGED);
113         mContext.registerReceiver(mReceiver, filter);
114         mDiscoveryPreference.setOnPreferenceClickListener(this);
115         handleModeChanged(mLocalAdapter.getScanMode());
116     }
117 
pause()118     public void pause() {
119         if (mLocalAdapter == null) {
120             return;
121         }
122 
123         mUiHandler.removeCallbacks(mUpdateCountdownSummaryRunnable);
124         mContext.unregisterReceiver(mReceiver);
125         mDiscoveryPreference.setOnPreferenceClickListener(null);
126     }
127 
onPreferenceClick(Preference preference)128     public boolean onPreferenceClick(Preference preference) {
129         // toggle discoverability
130         mDiscoverable = !mDiscoverable;
131         setEnabled(mDiscoverable);
132         return true;
133     }
134 
setEnabled(boolean enable)135     private void setEnabled(boolean enable) {
136         if (enable) {
137             int timeout = getDiscoverableTimeout();
138             long endTimestamp = System.currentTimeMillis() + timeout * 1000L;
139             LocalBluetoothPreferences.persistDiscoverableEndTimestamp(mContext, endTimestamp);
140 
141             mLocalAdapter.setScanMode(BluetoothAdapter.SCAN_MODE_CONNECTABLE_DISCOVERABLE, timeout);
142             updateCountdownSummary();
143 
144             Log.d(TAG, "setEnabled(): enabled = " + enable + "timeout = " + timeout);
145 
146             if (timeout > 0) {
147                 BluetoothDiscoverableTimeoutReceiver.setDiscoverableAlarm(mContext, endTimestamp);
148             } else {
149                 BluetoothDiscoverableTimeoutReceiver.cancelDiscoverableAlarm(mContext);
150             }
151 
152         } else {
153             mLocalAdapter.setScanMode(BluetoothAdapter.SCAN_MODE_CONNECTABLE);
154             BluetoothDiscoverableTimeoutReceiver.cancelDiscoverableAlarm(mContext);
155         }
156     }
157 
updateTimerDisplay(int timeout)158     private void updateTimerDisplay(int timeout) {
159         if (getDiscoverableTimeout() == DISCOVERABLE_TIMEOUT_NEVER) {
160             mDiscoveryPreference.setSummary(R.string.bluetooth_is_discoverable_always);
161         } else {
162             String textTimeout = formatTimeRemaining(timeout);
163             mDiscoveryPreference.setSummary(mContext.getString(R.string.bluetooth_is_discoverable,
164                     textTimeout));
165         }
166     }
167 
formatTimeRemaining(int timeout)168     private static String formatTimeRemaining(int timeout) {
169         StringBuilder sb = new StringBuilder(6);    // "mmm:ss"
170         int min = timeout / 60;
171         sb.append(min).append(':');
172         int sec = timeout - (min * 60);
173         if (sec < 10) {
174             sb.append('0');
175         }
176         sb.append(sec);
177         return sb.toString();
178     }
179 
setDiscoverableTimeout(int index)180     void setDiscoverableTimeout(int index) {
181         String timeoutValue;
182         switch (index) {
183             case 0:
184             default:
185                 mTimeoutSecs = DISCOVERABLE_TIMEOUT_TWO_MINUTES;
186                 timeoutValue = VALUE_DISCOVERABLE_TIMEOUT_TWO_MINUTES;
187                 break;
188 
189             case 1:
190                 mTimeoutSecs = DISCOVERABLE_TIMEOUT_FIVE_MINUTES;
191                 timeoutValue = VALUE_DISCOVERABLE_TIMEOUT_FIVE_MINUTES;
192                 break;
193 
194             case 2:
195                 mTimeoutSecs = DISCOVERABLE_TIMEOUT_ONE_HOUR;
196                 timeoutValue = VALUE_DISCOVERABLE_TIMEOUT_ONE_HOUR;
197                 break;
198 
199             case 3:
200                 mTimeoutSecs = DISCOVERABLE_TIMEOUT_NEVER;
201                 timeoutValue = VALUE_DISCOVERABLE_TIMEOUT_NEVER;
202                 break;
203         }
204         mSharedPreferences.edit().putString(KEY_DISCOVERABLE_TIMEOUT, timeoutValue).apply();
205         setEnabled(true);   // enable discovery and reset timer
206     }
207 
getDiscoverableTimeout()208     private int getDiscoverableTimeout() {
209         if (mTimeoutSecs != -1) {
210             return mTimeoutSecs;
211         }
212 
213         int timeout = SystemProperties.getInt(SYSTEM_PROPERTY_DISCOVERABLE_TIMEOUT, -1);
214         if (timeout < 0) {
215             String timeoutValue = mSharedPreferences.getString(KEY_DISCOVERABLE_TIMEOUT,
216                     VALUE_DISCOVERABLE_TIMEOUT_TWO_MINUTES);
217 
218             if (timeoutValue.equals(VALUE_DISCOVERABLE_TIMEOUT_NEVER)) {
219                 timeout = DISCOVERABLE_TIMEOUT_NEVER;
220             } else if (timeoutValue.equals(VALUE_DISCOVERABLE_TIMEOUT_ONE_HOUR)) {
221                 timeout = DISCOVERABLE_TIMEOUT_ONE_HOUR;
222             } else if (timeoutValue.equals(VALUE_DISCOVERABLE_TIMEOUT_FIVE_MINUTES)) {
223                 timeout = DISCOVERABLE_TIMEOUT_FIVE_MINUTES;
224             } else {
225                 timeout = DISCOVERABLE_TIMEOUT_TWO_MINUTES;
226             }
227         }
228         mTimeoutSecs = timeout;
229         return timeout;
230     }
231 
getDiscoverableTimeoutIndex()232     int getDiscoverableTimeoutIndex() {
233         int timeout = getDiscoverableTimeout();
234         switch (timeout) {
235             case DISCOVERABLE_TIMEOUT_TWO_MINUTES:
236             default:
237                 return 0;
238 
239             case DISCOVERABLE_TIMEOUT_FIVE_MINUTES:
240                 return 1;
241 
242             case DISCOVERABLE_TIMEOUT_ONE_HOUR:
243                 return 2;
244 
245             case DISCOVERABLE_TIMEOUT_NEVER:
246                 return 3;
247         }
248     }
249 
setNumberOfPairedDevices(int pairedDevices)250     void setNumberOfPairedDevices(int pairedDevices) {
251         mNumberOfPairedDevices = pairedDevices;
252         handleModeChanged(mLocalAdapter.getScanMode());
253     }
254 
handleModeChanged(int mode)255     void handleModeChanged(int mode) {
256         Log.d(TAG, "handleModeChanged(): mode = " + mode);
257         if (mode == BluetoothAdapter.SCAN_MODE_CONNECTABLE_DISCOVERABLE) {
258             mDiscoverable = true;
259             updateCountdownSummary();
260         } else {
261             mDiscoverable = false;
262             setSummaryNotDiscoverable();
263         }
264     }
265 
setSummaryNotDiscoverable()266     private void setSummaryNotDiscoverable() {
267         if (mNumberOfPairedDevices != 0) {
268             mDiscoveryPreference.setSummary(R.string.bluetooth_only_visible_to_paired_devices);
269         } else {
270             mDiscoveryPreference.setSummary(R.string.bluetooth_not_visible_to_other_devices);
271         }
272     }
273 
updateCountdownSummary()274     private void updateCountdownSummary() {
275         int mode = mLocalAdapter.getScanMode();
276         if (mode != BluetoothAdapter.SCAN_MODE_CONNECTABLE_DISCOVERABLE) {
277             return;
278         }
279 
280         long currentTimestamp = System.currentTimeMillis();
281         long endTimestamp = LocalBluetoothPreferences.getDiscoverableEndTimestamp(mContext);
282 
283         if (currentTimestamp > endTimestamp) {
284             // We're still in discoverable mode, but maybe there isn't a timeout.
285             updateTimerDisplay(0);
286             return;
287         }
288 
289         int timeLeft = (int) ((endTimestamp - currentTimestamp) / 1000L);
290         updateTimerDisplay(timeLeft);
291 
292         synchronized (this) {
293             mUiHandler.removeCallbacks(mUpdateCountdownSummaryRunnable);
294             mUiHandler.postDelayed(mUpdateCountdownSummaryRunnable, 1000);
295         }
296     }
297 }
298