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