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.app.AlertDialog; 20 import android.app.Dialog; 21 22 import android.bluetooth.BluetoothAdapter; 23 import android.content.BroadcastReceiver; 24 import android.content.Context; 25 import android.content.Intent; 26 import android.content.IntentFilter; 27 import android.preference.EditTextPreference; 28 import android.text.Editable; 29 import android.text.InputFilter; 30 import android.text.Spanned; 31 import android.text.TextWatcher; 32 import android.util.AttributeSet; 33 import android.widget.Button; 34 import android.widget.EditText; 35 36 /** 37 * BluetoothNamePreference is the preference type for editing the device's 38 * Bluetooth name. It asks the user for a name, and persists it via the 39 * Bluetooth API. 40 */ 41 public class BluetoothNamePreference extends EditTextPreference implements TextWatcher { 42 private static final String TAG = "BluetoothNamePreference"; 43 // max. length reduced from 248 to 246 bytes to work around Bluez bug 44 private static final int BLUETOOTH_NAME_MAX_LENGTH_BYTES = 246; 45 46 private LocalBluetoothManager mLocalManager; 47 48 private BroadcastReceiver mReceiver = new BroadcastReceiver() { 49 @Override 50 public void onReceive(Context context, Intent intent) { 51 String action = intent.getAction(); 52 if (action.equals(BluetoothAdapter.ACTION_LOCAL_NAME_CHANGED)) { 53 setSummaryToName(); 54 } else if (action.equals(BluetoothAdapter.ACTION_STATE_CHANGED) && 55 (intent.getIntExtra(BluetoothAdapter.EXTRA_STATE, BluetoothAdapter.ERROR) == 56 BluetoothAdapter.STATE_ON)) { 57 setSummaryToName(); 58 } 59 } 60 }; 61 62 public BluetoothNamePreference(Context context, AttributeSet attrs) { 63 super(context, attrs); 64 65 mLocalManager = LocalBluetoothManager.getInstance(context); 66 67 setSummaryToName(); 68 } 69 70 public void resume() { 71 IntentFilter filter = new IntentFilter(); 72 filter.addAction(BluetoothAdapter.ACTION_STATE_CHANGED); 73 filter.addAction(BluetoothAdapter.ACTION_LOCAL_NAME_CHANGED); 74 getContext().registerReceiver(mReceiver, filter); 75 76 // Make sure the OK button is disabled (if necessary) after rotation 77 EditText et = getEditText(); 78 if (et != null) { 79 et.setFilters(new InputFilter[] { 80 new Utf8ByteLengthFilter(BLUETOOTH_NAME_MAX_LENGTH_BYTES) 81 }); 82 83 et.addTextChangedListener(this); 84 Dialog d = getDialog(); 85 if (d instanceof AlertDialog) { 86 Button b = ((AlertDialog) d).getButton(AlertDialog.BUTTON_POSITIVE); 87 b.setEnabled(et.getText().length() > 0); 88 } 89 } 90 } 91 92 public void pause() { 93 EditText et = getEditText(); 94 if (et != null) { 95 et.removeTextChangedListener(this); 96 } 97 getContext().unregisterReceiver(mReceiver); 98 } 99 100 private void setSummaryToName() { 101 BluetoothAdapter adapter = mLocalManager.getBluetoothAdapter(); 102 if (adapter.isEnabled()) { 103 setSummary(adapter.getName()); 104 } 105 } 106 107 @Override 108 protected boolean persistString(String value) { 109 BluetoothAdapter adapter = mLocalManager.getBluetoothAdapter(); 110 adapter.setName(value); 111 return true; 112 } 113 114 @Override 115 protected void onClick() { 116 super.onClick(); 117 118 // The dialog should be created by now 119 EditText et = getEditText(); 120 if (et != null) { 121 et.setText(mLocalManager.getBluetoothAdapter().getName()); 122 } 123 } 124 125 // TextWatcher interface 126 public void afterTextChanged(Editable s) { 127 Dialog d = getDialog(); 128 if (d instanceof AlertDialog) { 129 ((AlertDialog) d).getButton(AlertDialog.BUTTON_POSITIVE).setEnabled(s.length() > 0); 130 } 131 } 132 133 // TextWatcher interface 134 public void beforeTextChanged(CharSequence s, int start, int count, int after) { 135 // not used 136 } 137 138 // TextWatcher interface 139 public void onTextChanged(CharSequence s, int start, int before, int count) { 140 // not used 141 } 142 143 /** 144 * This filter will constrain edits so that the text length is not 145 * greater than the specified number of bytes using UTF-8 encoding. 146 * <p>The JNI method used by {@link android.server.BluetoothService} 147 * to convert UTF-16 to UTF-8 doesn't support surrogate pairs, 148 * therefore code points outside of the basic multilingual plane 149 * (0000-FFFF) will be encoded as a pair of 3-byte UTF-8 characters, 150 * rather than a single 4-byte UTF-8 encoding. Dalvik implements this 151 * conversion in {@code convertUtf16ToUtf8()} in 152 * {@code dalvik/vm/UtfString.c}. 153 * <p>This JNI method is unlikely to change in the future due to 154 * backwards compatibility requirements. It's also unclear whether 155 * the installed base of Bluetooth devices would correctly handle the 156 * encoding of surrogate pairs in UTF-8 as 4 bytes rather than 6. 157 * However, this filter will still work in scenarios where surrogate 158 * pairs are encoded as 4 bytes, with the caveat that the maximum 159 * length will be constrained more conservatively than necessary. 160 */ 161 public static class Utf8ByteLengthFilter implements InputFilter { 162 private int mMaxBytes; 163 164 public Utf8ByteLengthFilter(int maxBytes) { 165 mMaxBytes = maxBytes; 166 } 167 168 public CharSequence filter(CharSequence source, int start, int end, 169 Spanned dest, int dstart, int dend) { 170 int srcByteCount = 0; 171 // count UTF-8 bytes in source substring 172 for (int i = start; i < end; i++) { 173 char c = source.charAt(i); 174 srcByteCount += (c < 0x0080) ? 1 : (c < 0x0800 ? 2 : 3); 175 } 176 int destLen = dest.length(); 177 int destByteCount = 0; 178 // count UTF-8 bytes in destination excluding replaced section 179 for (int i = 0; i < destLen; i++) { 180 if (i < dstart || i >= dend) { 181 char c = dest.charAt(i); 182 destByteCount += (c < 0x0080) ? 1 : (c < 0x0800 ? 2 : 3); 183 } 184 } 185 int keepBytes = mMaxBytes - destByteCount; 186 if (keepBytes <= 0) { 187 return ""; 188 } else if (keepBytes >= srcByteCount) { 189 return null; // use original dest string 190 } else { 191 // find end position of largest sequence that fits in keepBytes 192 for (int i = start; i < end; i++) { 193 char c = source.charAt(i); 194 keepBytes -= (c < 0x0080) ? 1 : (c < 0x0800 ? 2 : 3); 195 if (keepBytes < 0) { 196 return source.subSequence(start, i); 197 } 198 } 199 // If the entire substring fits, we should have returned null 200 // above, so this line should not be reached. If for some 201 // reason it is, return null to use the original dest string. 202 return null; 203 } 204 } 205 } 206 } 207