diff --git a/BluetoothLeChat/app/build.gradle b/BluetoothLeChat/app/build.gradle index fb7fcbee..b1066143 100644 --- a/BluetoothLeChat/app/build.gradle +++ b/BluetoothLeChat/app/build.gradle @@ -1,11 +1,11 @@ /* - * Copyright 2020 Google LLC + * Copyright (C) 2021 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * - * https://www.apache.org/licenses/LICENSE-2.0 + * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, @@ -54,13 +54,14 @@ android { dependencies { implementation fileTree(dir: "libs", include: ["*.jar"]) implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version" - implementation 'androidx.core:core-ktx:1.3.1' - implementation 'androidx.fragment:fragment-ktx:1.3.0-alpha08' - implementation 'androidx.activity:activity-ktx:1.2.0-alpha08' + implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.5.1' + implementation 'androidx.core:core-ktx:1.3.2' + implementation 'androidx.fragment:fragment-ktx:1.3.4' + implementation 'androidx.activity:activity-ktx:1.3.0-alpha08' implementation 'androidx.appcompat:appcompat:1.2.0' - implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.2.0' - implementation 'androidx.constraintlayout:constraintlayout:2.0.1' - implementation 'androidx.recyclerview:recyclerview:1.1.0' - implementation "androidx.navigation:navigation-fragment-ktx:2.3.0" - implementation "androidx.navigation:navigation-ui-ktx:2.3.0" + implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.3.1' + implementation 'androidx.constraintlayout:constraintlayout:2.0.4' + implementation 'androidx.recyclerview:recyclerview:1.2.0' + implementation "androidx.navigation:navigation-fragment-ktx:2.3.5" + implementation "androidx.navigation:navigation-ui-ktx:2.3.5" } \ No newline at end of file diff --git a/BluetoothLeChat/app/src/main/AndroidManifest.xml b/BluetoothLeChat/app/src/main/AndroidManifest.xml index 99596546..8a00a879 100644 --- a/BluetoothLeChat/app/src/main/AndroidManifest.xml +++ b/BluetoothLeChat/app/src/main/AndroidManifest.xml @@ -8,7 +8,7 @@ tag in the AndroidManifest.xml. // If the app is installed on an emulator without bluetooth then the app will crash @@ -258,7 +265,15 @@ object ChatServer { offset: Int, value: ByteArray? ) { - super.onCharacteristicWriteRequest(device, requestId, characteristic, preparedWrite, responseNeeded, offset, value) + super.onCharacteristicWriteRequest( + device, + requestId, + characteristic, + preparedWrite, + responseNeeded, + offset, + value + ) if (characteristic.uuid == MESSAGE_UUID) { gattServer?.sendResponse(device, requestId, BluetoothGatt.GATT_SUCCESS, 0, null) val message = value?.toString(Charsets.UTF_8) @@ -275,7 +290,10 @@ object ChatServer { super.onConnectionStateChange(gatt, status, newState) val isSuccess = status == BluetoothGatt.GATT_SUCCESS val isConnected = newState == BluetoothProfile.STATE_CONNECTED - Log.d(TAG, "onConnectionStateChange: Client $gatt success: $isSuccess connected: $isConnected") + Log.d( + TAG, + "onConnectionStateChange: Client $gatt success: $isSuccess connected: $isConnected" + ) // try to send a message to the other device as a test if (isSuccess && isConnected) { // discover services @@ -303,7 +321,7 @@ object ChatServer { super.onStartFailure(errorCode) // Send error state to display val errorMessage = "Advertise failed with error: $errorCode" - Log.d(TAG, "Advertising failed") + Log.d(TAG, errorMessage) //_viewState.value = DeviceScanViewState.Error(errorMessage) } diff --git a/BluetoothLeChat/app/src/main/java/com/example/bluetoothlechat/bluetooth/Constants.kt b/BluetoothLeChat/app/src/main/java/com/example/bluetoothlechat/bluetooth/Constants.kt index bf6f1a6e..95fba96f 100644 --- a/BluetoothLeChat/app/src/main/java/com/example/bluetoothlechat/bluetooth/Constants.kt +++ b/BluetoothLeChat/app/src/main/java/com/example/bluetoothlechat/bluetooth/Constants.kt @@ -15,6 +15,7 @@ */ package com.example.bluetoothlechat.bluetooth + import java.util.* /** @@ -25,7 +26,7 @@ import java.util.* * * Bluetooth requires a certain format for UUIDs associated with Services. * The official specification can be found here: - * [://www.bluetooth.org/en-us/specification/assigned-numbers/service-discovery][https] + * [https://www.bluetooth.org/en-us/specification/assigned-numbers/service-discovery] */ val SERVICE_UUID: UUID = UUID.fromString("0000b81d-0000-1000-8000-00805f9b34fb") @@ -38,5 +39,3 @@ val MESSAGE_UUID: UUID = UUID.fromString("7db3e235-3608-41f3-a03c-955fcbd2ea4b") * UUID to confirm device connection */ val CONFIRM_UUID: UUID = UUID.fromString("36d4dc5c-814b-4097-a5a6-b93b39085928") - -const val REQUEST_ENABLE_BT = 1 diff --git a/BluetoothLeChat/app/src/main/java/com/example/bluetoothlechat/bluetooth/EnableBluetoothFragment.kt b/BluetoothLeChat/app/src/main/java/com/example/bluetoothlechat/bluetooth/EnableBluetoothFragment.kt index e19582a8..2546cf27 100644 --- a/BluetoothLeChat/app/src/main/java/com/example/bluetoothlechat/bluetooth/EnableBluetoothFragment.kt +++ b/BluetoothLeChat/app/src/main/java/com/example/bluetoothlechat/bluetooth/EnableBluetoothFragment.kt @@ -22,6 +22,7 @@ import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup +import androidx.activity.result.contract.ActivityResultContracts import androidx.fragment.app.Fragment import androidx.lifecycle.Observer import androidx.navigation.fragment.findNavController @@ -31,6 +32,7 @@ import com.example.bluetoothlechat.databinding.FragmentEnableBluetoothBinding class EnableBluetoothFragment : Fragment() { private var _binding: FragmentEnableBluetoothBinding? = null + // This property is only valid between onCreateView and onDestroyView. private val binding get() = _binding!! @@ -42,6 +44,13 @@ class EnableBluetoothFragment : Fragment() { } } + private val startForResultEnableBluetooth = + registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { + if (it.resultCode == Activity.RESULT_OK) { + ChatServer.startServer(requireActivity().application) + } + } + override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) ChatServer.requestEnableBluetooth.observe(this, bluetoothEnableObserver) @@ -51,32 +60,16 @@ class EnableBluetoothFragment : Fragment() { inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? - ): View? { + ): View { _binding = FragmentEnableBluetoothBinding.inflate(inflater, container, false) binding.errorAction.setOnClickListener { - // Prompt user to turn on Bluetooth (logic continues in onActivityResult()). + // Prompt user to turn on Bluetooth (logic continues in registerForActivityResult()). val enableBtIntent = Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE) - startActivityForResult(enableBtIntent, REQUEST_ENABLE_BT) + startForResultEnableBluetooth.launch(enableBtIntent) } return binding.root } - override fun onActivityResult( - requestCode: Int, - resultCode: Int, - data: Intent? - ) { - super.onActivityResult(requestCode, resultCode, data) - when (requestCode) { - REQUEST_ENABLE_BT -> { - if (resultCode == Activity.RESULT_OK) { - ChatServer.startServer(requireActivity().application) - } - super.onActivityResult(requestCode, resultCode, data) - } - else -> super.onActivityResult(requestCode, resultCode, data) - } - } } \ No newline at end of file diff --git a/BluetoothLeChat/app/src/main/java/com/example/bluetoothlechat/bluetooth/LocationRequiredFragment.kt b/BluetoothLeChat/app/src/main/java/com/example/bluetoothlechat/bluetooth/LocationRequiredFragment.kt index ad48c46b..ec9e2735 100644 --- a/BluetoothLeChat/app/src/main/java/com/example/bluetoothlechat/bluetooth/LocationRequiredFragment.kt +++ b/BluetoothLeChat/app/src/main/java/com/example/bluetoothlechat/bluetooth/LocationRequiredFragment.kt @@ -18,19 +18,16 @@ package com.example.bluetoothlechat.bluetooth import android.Manifest import android.content.pm.PackageManager import android.os.Bundle -import android.util.Log import android.view.LayoutInflater import android.view.View import android.view.ViewGroup +import androidx.activity.result.contract.ActivityResultContracts import androidx.core.content.ContextCompat import androidx.fragment.app.Fragment import androidx.navigation.fragment.findNavController import com.example.bluetoothlechat.R import com.example.bluetoothlechat.databinding.FragmentLocationRequiredBinding -private const val TAG = "LocationRequiredFrag" -private const val LOCATION_REQUEST_CODE = 0 - // Fragment responsible for checking if the app has the ACCESS_FINE_LOCATION permission. // This permission is required when using the BLE APIs so the user must grant permission // to the app before viewing the BluetoothChatFragment or DeviceListFragment @@ -44,7 +41,7 @@ class LocationRequiredFragment : Fragment() { inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? - ): View? { + ): View { _binding = FragmentLocationRequiredBinding.inflate(inflater, container, false) // hide the error messages while checking the permissions @@ -64,25 +61,14 @@ class LocationRequiredFragment : Fragment() { checkLocationPermission() } - override fun onRequestPermissionsResult( - requestCode: Int, - permissions: Array, - grantResults: IntArray - ) { - super.onRequestPermissionsResult(requestCode, permissions, grantResults) - Log.d(TAG, "onRequestPermissionsResult: ") - when(requestCode) { - LOCATION_REQUEST_CODE -> { - if (grantResults.isNotEmpty() && - grantResults[0] == PackageManager.PERMISSION_GRANTED) { - // Navigate to the chat fragment - findNavController().navigate(R.id.action_start_chat) - } else { - showError() - } + private val startForResultRequestPermission = + registerForActivityResult(ActivityResultContracts.RequestPermission()) { + if (it) { + findNavController().navigate(R.id.action_start_chat) + } else { + showError() } } - } private fun showError() { binding.locationErrorMessage.visibility = View.VISIBLE @@ -99,10 +85,7 @@ class LocationRequiredFragment : Fragment() { // Navigate to the chat fragment findNavController().navigate(R.id.action_start_chat) } else { - requestPermissions( - arrayOf(Manifest.permission.ACCESS_FINE_LOCATION), - LOCATION_REQUEST_CODE - ) + startForResultRequestPermission.launch(Manifest.permission.ACCESS_FINE_LOCATION) } } } \ No newline at end of file diff --git a/BluetoothLeChat/app/src/main/java/com/example/bluetoothlechat/chat/BluetoothChatFragment.kt b/BluetoothLeChat/app/src/main/java/com/example/bluetoothlechat/chat/BluetoothChatFragment.kt index bb5fe4c4..290c058a 100644 --- a/BluetoothLeChat/app/src/main/java/com/example/bluetoothlechat/chat/BluetoothChatFragment.kt +++ b/BluetoothLeChat/app/src/main/java/com/example/bluetoothlechat/chat/BluetoothChatFragment.kt @@ -27,9 +27,9 @@ import androidx.fragment.app.Fragment import androidx.lifecycle.Observer import androidx.navigation.fragment.findNavController import androidx.recyclerview.widget.LinearLayoutManager -import com.example.bluetoothlechat.bluetooth.Message import com.example.bluetoothlechat.R import com.example.bluetoothlechat.bluetooth.ChatServer +import com.example.bluetoothlechat.bluetooth.Message import com.example.bluetoothlechat.databinding.FragmentBluetoothChatBinding import com.example.bluetoothlechat.gone import com.example.bluetoothlechat.visible @@ -39,12 +39,13 @@ private const val TAG = "BluetoothChatFragment" class BluetoothChatFragment : Fragment() { private var _binding: FragmentBluetoothChatBinding? = null + // this property is valid between onCreateView and onDestroyView. private val binding: FragmentBluetoothChatBinding get() = _binding!! private val deviceConnectionObserver = Observer { state -> - when(state) { + when (state) { is DeviceConnectionState.Connected -> { val device = state.device Log.d(TAG, "Gatt connection observer: have device $device") @@ -54,7 +55,6 @@ class BluetoothChatFragment : Fragment() { showDisconnected() } } - } private val connectionRequestObserver = Observer { device -> @@ -77,7 +77,7 @@ class BluetoothChatFragment : Fragment() { inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? - ): View? { + ): View { _binding = FragmentBluetoothChatBinding.inflate(inflater, container, false) Log.d(TAG, "chatWith: set adapter $adapter") diff --git a/BluetoothLeChat/app/src/main/java/com/example/bluetoothlechat/chat/LocalMessageViewHolder.kt b/BluetoothLeChat/app/src/main/java/com/example/bluetoothlechat/chat/LocalMessageViewHolder.kt index 3283a556..e454adef 100644 --- a/BluetoothLeChat/app/src/main/java/com/example/bluetoothlechat/chat/LocalMessageViewHolder.kt +++ b/BluetoothLeChat/app/src/main/java/com/example/bluetoothlechat/chat/LocalMessageViewHolder.kt @@ -18,8 +18,8 @@ package com.example.bluetoothlechat.chat import android.view.View import android.widget.TextView import androidx.recyclerview.widget.RecyclerView -import com.example.bluetoothlechat.bluetooth.Message import com.example.bluetoothlechat.R +import com.example.bluetoothlechat.bluetooth.Message class LocalMessageViewHolder(view: View) : RecyclerView.ViewHolder(view) { diff --git a/BluetoothLeChat/app/src/main/java/com/example/bluetoothlechat/chat/MessageAdapter.kt b/BluetoothLeChat/app/src/main/java/com/example/bluetoothlechat/chat/MessageAdapter.kt index 5b38f161..9cb1bf62 100644 --- a/BluetoothLeChat/app/src/main/java/com/example/bluetoothlechat/chat/MessageAdapter.kt +++ b/BluetoothLeChat/app/src/main/java/com/example/bluetoothlechat/chat/MessageAdapter.kt @@ -19,9 +19,8 @@ import android.util.Log import android.view.LayoutInflater import android.view.ViewGroup import androidx.recyclerview.widget.RecyclerView -import com.example.bluetoothlechat.bluetooth.Message import com.example.bluetoothlechat.R -import java.lang.IllegalArgumentException +import com.example.bluetoothlechat.bluetooth.Message private const val TAG = "MessageAdapter" private const val REMOTE_MESSAGE = 0 @@ -33,7 +32,7 @@ class MessageAdapter : RecyclerView.Adapter() { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { Log.d(TAG, "onCreateViewHolder: ") val inflater = LayoutInflater.from(parent.context) - return when(viewType) { + return when (viewType) { REMOTE_MESSAGE -> { val view = inflater.inflate(R.layout.item_remote_message, parent, false) RemoteMessageViewHolder(view) @@ -50,8 +49,7 @@ class MessageAdapter : RecyclerView.Adapter() { override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) { Log.d(TAG, "onBindViewHolder: ") - val message = messages[position] - when(message) { + when (val message = messages[position]) { is Message.RemoteMessage -> { (holder as RemoteMessageViewHolder).bind(message) } @@ -68,7 +66,7 @@ class MessageAdapter : RecyclerView.Adapter() { override fun getItemViewType(position: Int): Int { Log.d(TAG, "getItemViewType: ") - return when(messages[position]) { + return when (messages[position]) { is Message.RemoteMessage -> REMOTE_MESSAGE is Message.LocalMessage -> LOCAL_MESSAGE } diff --git a/BluetoothLeChat/app/src/main/java/com/example/bluetoothlechat/chat/RemoteMessageViewHolder.kt b/BluetoothLeChat/app/src/main/java/com/example/bluetoothlechat/chat/RemoteMessageViewHolder.kt index dc6fa825..94b1d544 100644 --- a/BluetoothLeChat/app/src/main/java/com/example/bluetoothlechat/chat/RemoteMessageViewHolder.kt +++ b/BluetoothLeChat/app/src/main/java/com/example/bluetoothlechat/chat/RemoteMessageViewHolder.kt @@ -18,8 +18,8 @@ package com.example.bluetoothlechat.chat import android.view.View import android.widget.TextView import androidx.recyclerview.widget.RecyclerView -import com.example.bluetoothlechat.bluetooth.Message import com.example.bluetoothlechat.R +import com.example.bluetoothlechat.bluetooth.Message class RemoteMessageViewHolder(view: View) : RecyclerView.ViewHolder(view) { diff --git a/BluetoothLeChat/app/src/main/java/com/example/bluetoothlechat/scan/DeviceScanFragment.kt b/BluetoothLeChat/app/src/main/java/com/example/bluetoothlechat/scan/DeviceScanFragment.kt index a380ba1d..ef9c1b1f 100644 --- a/BluetoothLeChat/app/src/main/java/com/example/bluetoothlechat/scan/DeviceScanFragment.kt +++ b/BluetoothLeChat/app/src/main/java/com/example/bluetoothlechat/scan/DeviceScanFragment.kt @@ -35,7 +35,6 @@ import com.example.bluetoothlechat.scan.DeviceScanViewState.* import com.example.bluetoothlechat.visible private const val TAG = "DeviceScanFragment" -const val GATT_KEY = "gatt_bundle_key" class DeviceScanFragment : Fragment() { @@ -70,7 +69,7 @@ class DeviceScanFragment : Fragment() { inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? - ): View? { + ): View { _binding = FragmentDeviceScanBinding.inflate(inflater, container, false) val devAddr = getString(R.string.your_device_address) + ChatServer.getYourDeviceAddress() binding.yourDeviceAddr.text = devAddr diff --git a/BluetoothLeChat/app/src/main/java/com/example/bluetoothlechat/scan/DeviceScanViewModel.kt b/BluetoothLeChat/app/src/main/java/com/example/bluetoothlechat/scan/DeviceScanViewModel.kt index c5a8f0fa..fb9be9cd 100644 --- a/BluetoothLeChat/app/src/main/java/com/example/bluetoothlechat/scan/DeviceScanViewModel.kt +++ b/BluetoothLeChat/app/src/main/java/com/example/bluetoothlechat/scan/DeviceScanViewModel.kt @@ -16,9 +16,9 @@ package com.example.bluetoothlechat.scan import android.app.Application -import android.bluetooth.* +import android.bluetooth.BluetoothAdapter +import android.bluetooth.BluetoothDevice import android.bluetooth.le.* -import android.os.Handler import android.os.ParcelUuid import android.util.Log import androidx.lifecycle.AndroidViewModel @@ -26,9 +26,13 @@ import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import com.example.bluetoothlechat.bluetooth.SERVICE_UUID import com.example.bluetoothlechat.scan.DeviceScanViewState.* - +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch private const val TAG = "DeviceScanViewModel" + // 30 second scan period private const val SCAN_PERIOD = 30000L @@ -66,7 +70,7 @@ class DeviceScanViewModel(app: Application) : AndroidViewModel(app) { stopScanning() } - fun startScan() { + private fun startScan() { // If advertisement is not supported on this device then other devices will not be able to // discover and connect to it. if (!adapter.isMultipleAdvertisementSupported) { @@ -81,8 +85,10 @@ class DeviceScanViewModel(app: Application) : AndroidViewModel(app) { _viewState.value = ActiveScan // Stop scanning after the scan period - Handler().postDelayed({ stopScanning() }, SCAN_PERIOD) - + CoroutineScope(Dispatchers.Main).launch { + delay(SCAN_PERIOD) + stopScanning() + } // Kick off a new scan scanCallback = DeviceScanCallback() scanner?.startScan(scanFilters, scanSettings, scanCallback) @@ -152,5 +158,4 @@ class DeviceScanViewModel(app: Application) : AndroidViewModel(app) { } } - } \ No newline at end of file diff --git a/BluetoothLeChat/app/src/main/java/com/example/bluetoothlechat/scan/DeviceScanViewState.kt b/BluetoothLeChat/app/src/main/java/com/example/bluetoothlechat/scan/DeviceScanViewState.kt index f048a7c7..f684b4ec 100644 --- a/BluetoothLeChat/app/src/main/java/com/example/bluetoothlechat/scan/DeviceScanViewState.kt +++ b/BluetoothLeChat/app/src/main/java/com/example/bluetoothlechat/scan/DeviceScanViewState.kt @@ -18,8 +18,8 @@ package com.example.bluetoothlechat.scan import android.bluetooth.BluetoothDevice sealed class DeviceScanViewState { - object ActiveScan: DeviceScanViewState() - class ScanResults(val scanResults: Map): DeviceScanViewState() - class Error(val message: String): DeviceScanViewState() - object AdvertisementNotSupported: DeviceScanViewState() + object ActiveScan : DeviceScanViewState() + class ScanResults(val scanResults: Map) : DeviceScanViewState() + class Error(val message: String) : DeviceScanViewState() + object AdvertisementNotSupported : DeviceScanViewState() } \ No newline at end of file diff --git a/BluetoothLeChat/app/src/main/res/layout/activity_main.xml b/BluetoothLeChat/app/src/main/res/layout/activity_main.xml index f279db81..709dabdd 100644 --- a/BluetoothLeChat/app/src/main/res/layout/activity_main.xml +++ b/BluetoothLeChat/app/src/main/res/layout/activity_main.xml @@ -14,8 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. --> - @@ -27,4 +26,4 @@ android:layout_height="match_parent" app:defaultNavHost="true" app:navGraph="@navigation/nav_graph" /> - \ No newline at end of file + \ No newline at end of file diff --git a/BluetoothLeChat/app/src/main/res/layout/fragment_bluetooth_chat.xml b/BluetoothLeChat/app/src/main/res/layout/fragment_bluetooth_chat.xml index 685a163a..070a52d3 100644 --- a/BluetoothLeChat/app/src/main/res/layout/fragment_bluetooth_chat.xml +++ b/BluetoothLeChat/app/src/main/res/layout/fragment_bluetooth_chat.xml @@ -14,8 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. --> - @@ -23,21 +22,21 @@ android:id="@+id/not_connected_container" android:layout_width="wrap_content" android:layout_height="wrap_content" - android:orientation="vertical" - android:layout_gravity="center"> + android:layout_gravity="center" + android:orientation="vertical"> + android:text="@string/no_connected_device_message" />