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
17package com.android.settings.bluetooth;
18
19import android.bluetooth.BluetoothAdapter;
20import android.content.BroadcastReceiver;
21import android.content.Context;
22import android.content.Intent;
23import android.content.IntentFilter;
24import android.content.SharedPreferences;
25import android.os.Handler;
26import android.os.SystemProperties;
27import android.support.v7.preference.Preference;
28import android.util.Log;
29
30import com.android.settings.R;
31import com.android.settingslib.bluetooth.BluetoothDiscoverableTimeoutReceiver;
32import 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 */
39final 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
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
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
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
128    public boolean onPreferenceClick(Preference preference) {
129        // toggle discoverability
130        mDiscoverable = !mDiscoverable;
131        setEnabled(mDiscoverable);
132        return true;
133    }
134
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
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
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
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
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
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
250    void setNumberOfPairedDevices(int pairedDevices) {
251        mNumberOfPairedDevices = pairedDevices;
252        handleModeChanged(mLocalAdapter.getScanMode());
253    }
254
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
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
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