1 /* 2 * Copyright (C) 2014 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.example.android.bluetoothchat; 18 19 import android.app.ActionBar; 20 import android.app.Activity; 21 import android.bluetooth.BluetoothAdapter; 22 import android.bluetooth.BluetoothDevice; 23 import android.content.Intent; 24 import android.os.Bundle; 25 import android.os.Handler; 26 import android.os.Message; 27 import android.support.annotation.Nullable; 28 import android.support.v4.app.Fragment; 29 import android.support.v4.app.FragmentActivity; 30 import android.view.KeyEvent; 31 import android.view.LayoutInflater; 32 import android.view.Menu; 33 import android.view.MenuInflater; 34 import android.view.MenuItem; 35 import android.view.View; 36 import android.view.ViewGroup; 37 import android.view.inputmethod.EditorInfo; 38 import android.widget.ArrayAdapter; 39 import android.widget.Button; 40 import android.widget.EditText; 41 import android.widget.ListView; 42 import android.widget.TextView; 43 import android.widget.Toast; 44 45 import com.example.android.common.logger.Log; 46 47 /** 48 * This fragment controls Bluetooth to communicate with other devices. 49 */ 50 public class BluetoothChatFragment extends Fragment { 51 52 private static final String TAG = "BluetoothChatFragment"; 53 54 // Intent request codes 55 private static final int REQUEST_CONNECT_DEVICE_SECURE = 1; 56 private static final int REQUEST_CONNECT_DEVICE_INSECURE = 2; 57 private static final int REQUEST_ENABLE_BT = 3; 58 59 // Layout Views 60 private ListView mConversationView; 61 private EditText mOutEditText; 62 private Button mSendButton; 63 64 /** 65 * Name of the connected device 66 */ 67 private String mConnectedDeviceName = null; 68 69 /** 70 * Array adapter for the conversation thread 71 */ 72 private ArrayAdapter<String> mConversationArrayAdapter; 73 74 /** 75 * String buffer for outgoing messages 76 */ 77 private StringBuffer mOutStringBuffer; 78 79 /** 80 * Local Bluetooth adapter 81 */ 82 private BluetoothAdapter mBluetoothAdapter = null; 83 84 /** 85 * Member object for the chat services 86 */ 87 private BluetoothChatService mChatService = null; 88 89 @Override onCreate(Bundle savedInstanceState)90 public void onCreate(Bundle savedInstanceState) { 91 super.onCreate(savedInstanceState); 92 setHasOptionsMenu(true); 93 // Get local Bluetooth adapter 94 mBluetoothAdapter = BluetoothAdapter.getDefaultAdapter(); 95 96 // If the adapter is null, then Bluetooth is not supported 97 if (mBluetoothAdapter == null) { 98 FragmentActivity activity = getActivity(); 99 Toast.makeText(activity, "Bluetooth is not available", Toast.LENGTH_LONG).show(); 100 activity.finish(); 101 } 102 } 103 104 105 @Override onStart()106 public void onStart() { 107 super.onStart(); 108 // If BT is not on, request that it be enabled. 109 // setupChat() will then be called during onActivityResult 110 if (!mBluetoothAdapter.isEnabled()) { 111 Intent enableIntent = new Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE); 112 startActivityForResult(enableIntent, REQUEST_ENABLE_BT); 113 // Otherwise, setup the chat session 114 } else if (mChatService == null) { 115 setupChat(); 116 } 117 } 118 119 @Override onDestroy()120 public void onDestroy() { 121 super.onDestroy(); 122 if (mChatService != null) { 123 mChatService.stop(); 124 } 125 } 126 127 @Override onResume()128 public void onResume() { 129 super.onResume(); 130 131 // Performing this check in onResume() covers the case in which BT was 132 // not enabled during onStart(), so we were paused to enable it... 133 // onResume() will be called when ACTION_REQUEST_ENABLE activity returns. 134 if (mChatService != null) { 135 // Only if the state is STATE_NONE, do we know that we haven't started already 136 if (mChatService.getState() == BluetoothChatService.STATE_NONE) { 137 // Start the Bluetooth chat services 138 mChatService.start(); 139 } 140 } 141 } 142 143 @Override onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState)144 public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, 145 @Nullable Bundle savedInstanceState) { 146 return inflater.inflate(R.layout.fragment_bluetooth_chat, container, false); 147 } 148 149 @Override onViewCreated(View view, @Nullable Bundle savedInstanceState)150 public void onViewCreated(View view, @Nullable Bundle savedInstanceState) { 151 mConversationView = (ListView) view.findViewById(R.id.in); 152 mOutEditText = (EditText) view.findViewById(R.id.edit_text_out); 153 mSendButton = (Button) view.findViewById(R.id.button_send); 154 } 155 156 /** 157 * Set up the UI and background operations for chat. 158 */ setupChat()159 private void setupChat() { 160 Log.d(TAG, "setupChat()"); 161 162 // Initialize the array adapter for the conversation thread 163 mConversationArrayAdapter = new ArrayAdapter<String>(getActivity(), R.layout.message); 164 165 mConversationView.setAdapter(mConversationArrayAdapter); 166 167 // Initialize the compose field with a listener for the return key 168 mOutEditText.setOnEditorActionListener(mWriteListener); 169 170 // Initialize the send button with a listener that for click events 171 mSendButton.setOnClickListener(new View.OnClickListener() { 172 public void onClick(View v) { 173 // Send a message using content of the edit text widget 174 View view = getView(); 175 if (null != view) { 176 TextView textView = (TextView) view.findViewById(R.id.edit_text_out); 177 String message = textView.getText().toString(); 178 sendMessage(message); 179 } 180 } 181 }); 182 183 // Initialize the BluetoothChatService to perform bluetooth connections 184 mChatService = new BluetoothChatService(getActivity(), mHandler); 185 186 // Initialize the buffer for outgoing messages 187 mOutStringBuffer = new StringBuffer(""); 188 } 189 190 /** 191 * Makes this device discoverable for 300 seconds (5 minutes). 192 */ ensureDiscoverable()193 private void ensureDiscoverable() { 194 if (mBluetoothAdapter.getScanMode() != 195 BluetoothAdapter.SCAN_MODE_CONNECTABLE_DISCOVERABLE) { 196 Intent discoverableIntent = new Intent(BluetoothAdapter.ACTION_REQUEST_DISCOVERABLE); 197 discoverableIntent.putExtra(BluetoothAdapter.EXTRA_DISCOVERABLE_DURATION, 300); 198 startActivity(discoverableIntent); 199 } 200 } 201 202 /** 203 * Sends a message. 204 * 205 * @param message A string of text to send. 206 */ sendMessage(String message)207 private void sendMessage(String message) { 208 // Check that we're actually connected before trying anything 209 if (mChatService.getState() != BluetoothChatService.STATE_CONNECTED) { 210 Toast.makeText(getActivity(), R.string.not_connected, Toast.LENGTH_SHORT).show(); 211 return; 212 } 213 214 // Check that there's actually something to send 215 if (message.length() > 0) { 216 // Get the message bytes and tell the BluetoothChatService to write 217 byte[] send = message.getBytes(); 218 mChatService.write(send); 219 220 // Reset out string buffer to zero and clear the edit text field 221 mOutStringBuffer.setLength(0); 222 mOutEditText.setText(mOutStringBuffer); 223 } 224 } 225 226 /** 227 * The action listener for the EditText widget, to listen for the return key 228 */ 229 private TextView.OnEditorActionListener mWriteListener 230 = new TextView.OnEditorActionListener() { 231 public boolean onEditorAction(TextView view, int actionId, KeyEvent event) { 232 // If the action is a key-up event on the return key, send the message 233 if (actionId == EditorInfo.IME_NULL && event.getAction() == KeyEvent.ACTION_UP) { 234 String message = view.getText().toString(); 235 sendMessage(message); 236 } 237 return true; 238 } 239 }; 240 241 /** 242 * Updates the status on the action bar. 243 * 244 * @param resId a string resource ID 245 */ setStatus(int resId)246 private void setStatus(int resId) { 247 FragmentActivity activity = getActivity(); 248 if (null == activity) { 249 return; 250 } 251 final ActionBar actionBar = activity.getActionBar(); 252 if (null == actionBar) { 253 return; 254 } 255 actionBar.setSubtitle(resId); 256 } 257 258 /** 259 * Updates the status on the action bar. 260 * 261 * @param subTitle status 262 */ setStatus(CharSequence subTitle)263 private void setStatus(CharSequence subTitle) { 264 FragmentActivity activity = getActivity(); 265 if (null == activity) { 266 return; 267 } 268 final ActionBar actionBar = activity.getActionBar(); 269 if (null == actionBar) { 270 return; 271 } 272 actionBar.setSubtitle(subTitle); 273 } 274 275 /** 276 * The Handler that gets information back from the BluetoothChatService 277 */ 278 private final Handler mHandler = new Handler() { 279 @Override 280 public void handleMessage(Message msg) { 281 FragmentActivity activity = getActivity(); 282 switch (msg.what) { 283 case Constants.MESSAGE_STATE_CHANGE: 284 switch (msg.arg1) { 285 case BluetoothChatService.STATE_CONNECTED: 286 setStatus(getString(R.string.title_connected_to, mConnectedDeviceName)); 287 mConversationArrayAdapter.clear(); 288 break; 289 case BluetoothChatService.STATE_CONNECTING: 290 setStatus(R.string.title_connecting); 291 break; 292 case BluetoothChatService.STATE_LISTEN: 293 case BluetoothChatService.STATE_NONE: 294 setStatus(R.string.title_not_connected); 295 break; 296 } 297 break; 298 case Constants.MESSAGE_WRITE: 299 byte[] writeBuf = (byte[]) msg.obj; 300 // construct a string from the buffer 301 String writeMessage = new String(writeBuf); 302 mConversationArrayAdapter.add("Me: " + writeMessage); 303 break; 304 case Constants.MESSAGE_READ: 305 byte[] readBuf = (byte[]) msg.obj; 306 // construct a string from the valid bytes in the buffer 307 String readMessage = new String(readBuf, 0, msg.arg1); 308 mConversationArrayAdapter.add(mConnectedDeviceName + ": " + readMessage); 309 break; 310 case Constants.MESSAGE_DEVICE_NAME: 311 // save the connected device's name 312 mConnectedDeviceName = msg.getData().getString(Constants.DEVICE_NAME); 313 if (null != activity) { 314 Toast.makeText(activity, "Connected to " 315 + mConnectedDeviceName, Toast.LENGTH_SHORT).show(); 316 } 317 break; 318 case Constants.MESSAGE_TOAST: 319 if (null != activity) { 320 Toast.makeText(activity, msg.getData().getString(Constants.TOAST), 321 Toast.LENGTH_SHORT).show(); 322 } 323 break; 324 } 325 } 326 }; 327 onActivityResult(int requestCode, int resultCode, Intent data)328 public void onActivityResult(int requestCode, int resultCode, Intent data) { 329 switch (requestCode) { 330 case REQUEST_CONNECT_DEVICE_SECURE: 331 // When DeviceListActivity returns with a device to connect 332 if (resultCode == Activity.RESULT_OK) { 333 connectDevice(data, true); 334 } 335 break; 336 case REQUEST_CONNECT_DEVICE_INSECURE: 337 // When DeviceListActivity returns with a device to connect 338 if (resultCode == Activity.RESULT_OK) { 339 connectDevice(data, false); 340 } 341 break; 342 case REQUEST_ENABLE_BT: 343 // When the request to enable Bluetooth returns 344 if (resultCode == Activity.RESULT_OK) { 345 // Bluetooth is now enabled, so set up a chat session 346 setupChat(); 347 } else { 348 // User did not enable Bluetooth or an error occurred 349 Log.d(TAG, "BT not enabled"); 350 Toast.makeText(getActivity(), R.string.bt_not_enabled_leaving, 351 Toast.LENGTH_SHORT).show(); 352 getActivity().finish(); 353 } 354 } 355 } 356 357 /** 358 * Establish connection with other device 359 * 360 * @param data An {@link Intent} with {@link DeviceListActivity#EXTRA_DEVICE_ADDRESS} extra. 361 * @param secure Socket Security type - Secure (true) , Insecure (false) 362 */ connectDevice(Intent data, boolean secure)363 private void connectDevice(Intent data, boolean secure) { 364 // Get the device MAC address 365 String address = data.getExtras() 366 .getString(DeviceListActivity.EXTRA_DEVICE_ADDRESS); 367 // Get the BluetoothDevice object 368 BluetoothDevice device = mBluetoothAdapter.getRemoteDevice(address); 369 // Attempt to connect to the device 370 mChatService.connect(device, secure); 371 } 372 373 @Override onCreateOptionsMenu(Menu menu, MenuInflater inflater)374 public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { 375 inflater.inflate(R.menu.bluetooth_chat, menu); 376 } 377 378 @Override onOptionsItemSelected(MenuItem item)379 public boolean onOptionsItemSelected(MenuItem item) { 380 switch (item.getItemId()) { 381 case R.id.secure_connect_scan: { 382 // Launch the DeviceListActivity to see devices and do scan 383 Intent serverIntent = new Intent(getActivity(), DeviceListActivity.class); 384 startActivityForResult(serverIntent, REQUEST_CONNECT_DEVICE_SECURE); 385 return true; 386 } 387 case R.id.insecure_connect_scan: { 388 // Launch the DeviceListActivity to see devices and do scan 389 Intent serverIntent = new Intent(getActivity(), DeviceListActivity.class); 390 startActivityForResult(serverIntent, REQUEST_CONNECT_DEVICE_INSECURE); 391 return true; 392 } 393 case R.id.discoverable: { 394 // Ensure this device is discoverable by others 395 ensureDiscoverable(); 396 return true; 397 } 398 } 399 return false; 400 } 401 402 } 403