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