diff --git a/Android/.DS_Store b/Android/.DS_Store new file mode 100644 index 0000000..6dd33ec Binary files /dev/null and b/Android/.DS_Store differ diff --git a/Android/BluetoothChat/.gitignore b/Android/BluetoothChat/.gitignore new file mode 100644 index 0000000..aa724b7 --- /dev/null +++ b/Android/BluetoothChat/.gitignore @@ -0,0 +1,15 @@ +*.iml +.gradle +/local.properties +/.idea/caches +/.idea/libraries +/.idea/modules.xml +/.idea/workspace.xml +/.idea/navEditor.xml +/.idea/assetWizardSettings.xml +.DS_Store +/build +/captures +.externalNativeBuild +.cxx +local.properties diff --git a/Android/BluetoothChat/.idea/.gitignore b/Android/BluetoothChat/.idea/.gitignore new file mode 100644 index 0000000..26d3352 --- /dev/null +++ b/Android/BluetoothChat/.idea/.gitignore @@ -0,0 +1,3 @@ +# Default ignored files +/shelf/ +/workspace.xml diff --git a/Android/BluetoothChat/.idea/compiler.xml b/Android/BluetoothChat/.idea/compiler.xml new file mode 100644 index 0000000..61a9130 --- /dev/null +++ b/Android/BluetoothChat/.idea/compiler.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/Android/BluetoothChat/.idea/gradle.xml b/Android/BluetoothChat/.idea/gradle.xml new file mode 100644 index 0000000..23a89bb --- /dev/null +++ b/Android/BluetoothChat/.idea/gradle.xml @@ -0,0 +1,22 @@ + + + + + + + \ No newline at end of file diff --git a/Android/BluetoothChat/.idea/jarRepositories.xml b/Android/BluetoothChat/.idea/jarRepositories.xml new file mode 100644 index 0000000..a5f05cd --- /dev/null +++ b/Android/BluetoothChat/.idea/jarRepositories.xml @@ -0,0 +1,25 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/Android/BluetoothChat/.idea/misc.xml b/Android/BluetoothChat/.idea/misc.xml new file mode 100644 index 0000000..d5d35ec --- /dev/null +++ b/Android/BluetoothChat/.idea/misc.xml @@ -0,0 +1,9 @@ + + + + + + + + \ No newline at end of file diff --git a/Android/BluetoothChat/.idea/vcs.xml b/Android/BluetoothChat/.idea/vcs.xml new file mode 100644 index 0000000..b2bdec2 --- /dev/null +++ b/Android/BluetoothChat/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/Android/BluetoothChat/app/.gitignore b/Android/BluetoothChat/app/.gitignore new file mode 100644 index 0000000..42afabf --- /dev/null +++ b/Android/BluetoothChat/app/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/Android/BluetoothChat/app/build.gradle b/Android/BluetoothChat/app/build.gradle new file mode 100644 index 0000000..fe1876f --- /dev/null +++ b/Android/BluetoothChat/app/build.gradle @@ -0,0 +1,52 @@ +plugins { + id 'com.android.application' + id 'kotlin-android' +} + +android { + compileSdkVersion 30 + buildToolsVersion "30.0.3" + + defaultConfig { + applicationId "ditto.live.bluetoothchat" + minSdkVersion 25 + targetSdkVersion 30 + versionCode 1 + versionName "1.0" + + testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" + } + + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' + } + } + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + kotlinOptions { + jvmTarget = '1.8' + } +} + +dependencies { + + implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version" + implementation 'androidx.core:core-ktx:1.3.2' + implementation 'androidx.appcompat:appcompat:1.2.0' + implementation 'com.google.android.material:material:1.3.0' + implementation 'androidx.constraintlayout:constraintlayout:2.0.4' + implementation 'androidx.legacy:legacy-support-v4:1.0.0' + testImplementation 'junit:junit:4.+' + androidTestImplementation 'androidx.test.ext:junit:1.1.2' + androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0' + + implementation 'androidx.fragment:fragment-ktx:1.2.5' + implementation 'com.github.bassaer:chatmessageview:2.0.1' + implementation 'de.hdodenhof:circleimageview:3.0.0' + implementation "androidx.swiperefreshlayout:swiperefreshlayout:1.0.0" + implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.2.0' +} \ No newline at end of file diff --git a/Android/BluetoothChat/app/proguard-rules.pro b/Android/BluetoothChat/app/proguard-rules.pro new file mode 100644 index 0000000..481bb43 --- /dev/null +++ b/Android/BluetoothChat/app/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/Android/BluetoothChat/app/src/androidTest/java/ditto/live/bluetoothchat/ExampleInstrumentedTest.kt b/Android/BluetoothChat/app/src/androidTest/java/ditto/live/bluetoothchat/ExampleInstrumentedTest.kt new file mode 100644 index 0000000..54a57d7 --- /dev/null +++ b/Android/BluetoothChat/app/src/androidTest/java/ditto/live/bluetoothchat/ExampleInstrumentedTest.kt @@ -0,0 +1,24 @@ +package ditto.live.bluetoothchat + +import androidx.test.platform.app.InstrumentationRegistry +import androidx.test.ext.junit.runners.AndroidJUnit4 + +import org.junit.Test +import org.junit.runner.RunWith + +import org.junit.Assert.* + +/** + * Instrumented test, which will execute on an Android device. + * + * See [testing documentation](http://d.android.com/tools/testing). + */ +@RunWith(AndroidJUnit4::class) +class ExampleInstrumentedTest { + @Test + fun useAppContext() { + // Context of the app under test. + val appContext = InstrumentationRegistry.getInstrumentation().targetContext + assertEquals("ditto.live.bluetoothchat", appContext.packageName) + } +} \ No newline at end of file diff --git a/Android/BluetoothChat/app/src/main/AndroidManifest.xml b/Android/BluetoothChat/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..ff9e66b --- /dev/null +++ b/Android/BluetoothChat/app/src/main/AndroidManifest.xml @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Android/BluetoothChat/app/src/main/java/ditto/live/bluetoothchat/BLEMessage.kt b/Android/BluetoothChat/app/src/main/java/ditto/live/bluetoothchat/BLEMessage.kt new file mode 100644 index 0000000..ed8c550 --- /dev/null +++ b/Android/BluetoothChat/app/src/main/java/ditto/live/bluetoothchat/BLEMessage.kt @@ -0,0 +1,5 @@ +package ditto.live.bluetoothchat + +abstract class BLEMessage(val text: String) +class RemoteMessage(text: String) : BLEMessage(text) +class LocalMessage(text: String) : BLEMessage(text) \ No newline at end of file diff --git a/Android/BluetoothChat/app/src/main/java/ditto/live/bluetoothchat/BluetoothConstants.kt b/Android/BluetoothChat/app/src/main/java/ditto/live/bluetoothchat/BluetoothConstants.kt new file mode 100644 index 0000000..2a5b882 --- /dev/null +++ b/Android/BluetoothChat/app/src/main/java/ditto/live/bluetoothchat/BluetoothConstants.kt @@ -0,0 +1,11 @@ +package ditto.live.bluetoothchat +import java.util.* + +//App service UUID +val chatDiscoveryServiceID = UUID.fromString("42332fe8-9915-11ea-bb37-0242ac130002") + +//Gatt service message characteric ID used +val chatMessageCharacteristicID = UUID.fromString("43eb0d29-4188-4c84-b1e8-73231e02af95") + +//UUID to confirm device connection +val confirmCharacteristicID = UUID.fromString("f0ab5a15-b003-4653-a248-73fd504c128f") diff --git a/Android/BluetoothChat/app/src/main/java/ditto/live/bluetoothchat/BluetoothScanViewModel.kt b/Android/BluetoothChat/app/src/main/java/ditto/live/bluetoothchat/BluetoothScanViewModel.kt new file mode 100644 index 0000000..89be239 --- /dev/null +++ b/Android/BluetoothChat/app/src/main/java/ditto/live/bluetoothchat/BluetoothScanViewModel.kt @@ -0,0 +1,85 @@ +package ditto.live.bluetoothchat + +import android.app.Application +import android.bluetooth.* +import android.bluetooth.le.* +import android.os.ParcelUuid +import androidx.lifecycle.AndroidViewModel +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData + +class BluetoothScanViewModel(app: Application) : AndroidViewModel(app) { + //our variables that will be set and used for the callback of nearby discovered devices + private val _scanResults = MutableLiveData>() + val scanResults = _scanResults as LiveData> + + //get an instance of the local bluetooth adapter + private val adapter: BluetoothAdapter = BluetoothAdapter.getDefaultAdapter() + //scanner we will use to discover nearby devices using our UUID + private var scanner: BluetoothLeScanner? = null + + private var scanCallback: DeviceScanCallback? = null + private val scanFilters: List + private val scanSettings: ScanSettings + + init { + scanFilters = createFilters() + scanSettings = ScanSettings.Builder() + .setScanMode(ScanSettings.SCAN_MODE_LOW_POWER) + .build() + startScan() + } + + override fun onCleared() { + super.onCleared() + scanner?.stopScan(scanCallback) + scanCallback = null + } + + fun startScan() { + if (scanCallback == null) { + scanner = adapter.bluetoothLeScanner + scanCallback = DeviceScanCallback() + scanner?.startScan(scanFilters, scanSettings, scanCallback) + } + } + + /** + * Use our unique UUID for filtering results for Bluetooth LE scans + */ + private fun createFilters(): List { + val builder = ScanFilter.Builder() + builder.setServiceUuid(ParcelUuid(chatDiscoveryServiceID)) + val filter = builder.build() + return listOf(filter) + } + + /** + * A callback of device scanning + * Set our scan results for discovered devices + */ + private inner class DeviceScanCallback : ScanCallback() { + override fun onBatchScanResults(results: List) { + super.onBatchScanResults(results) + val res = mutableMapOf() + for (item in results) { + item.device?.let { device -> + res[device.address] = device + } + } + _scanResults.value = res + } + + override fun onScanResult( + callbackType: Int, + result: ScanResult + ) { + super.onScanResult(callbackType, result) + val res = mutableMapOf() + result.device?.let { device -> + res[device.address] = device + } + _scanResults.value = res + } + } +} \ No newline at end of file diff --git a/Android/BluetoothChat/app/src/main/java/ditto/live/bluetoothchat/ChatFragment.kt b/Android/BluetoothChat/app/src/main/java/ditto/live/bluetoothchat/ChatFragment.kt new file mode 100644 index 0000000..b872a10 --- /dev/null +++ b/Android/BluetoothChat/app/src/main/java/ditto/live/bluetoothchat/ChatFragment.kt @@ -0,0 +1,73 @@ +package ditto.live.bluetoothchat + +import android.bluetooth.BluetoothAdapter +import android.graphics.Bitmap +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.fragment.app.Fragment +import androidx.lifecycle.Observer +import com.github.bassaer.chatmessageview.model.ChatUser +import com.github.bassaer.chatmessageview.model.Message +import com.github.bassaer.chatmessageview.view.ChatView +import ditto.live.bluetoothchat.services.BluetoothChatService + +class ChatFragment: Fragment() { + private lateinit var chatView: ChatView + private lateinit var name: String + + private val messageObserver = Observer { message -> + updateMessages(message) + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + name = BluetoothAdapter.getDefaultAdapter().name + } + + override fun onCreateView( + inflater: LayoutInflater, container: ViewGroup?, + savedInstanceState: Bundle? + ): View { + val view = inflater.inflate(R.layout.fragment_chat, container, false) + setupChatView(view) + return view + } + + override fun onStart() { + super.onStart() + BluetoothChatService.messages.observe(viewLifecycleOwner, messageObserver) + } + + private fun setupChatView(view: View) { + chatView = view.findViewById(R.id.chat_view) + chatView.setOnClickSendButtonListener({ onClickSend() }) + chatView.setAutoScroll(true) + chatView.setSendTimeTextColor(0) + chatView.setMessageFontSize(50F) + } + + private fun onClickSend() { + val text = chatView.inputText + if (chatView.inputText.isEmpty()) return + BluetoothChatService.sendMessage(text) + chatView.inputText = "" + } + + private fun updateMessages(message: BLEMessage) { + val emptyIcon = Bitmap.createBitmap(1, 1, Bitmap.Config.ARGB_8888) + val myUser = message is LocalMessage + val username = if (myUser) BluetoothChatService.currentDevice!!.name else BluetoothAdapter.getDefaultAdapter().name + val user = ChatUser(0, username, emptyIcon) + val messageBuilder = Message.Builder() + .setUser(user) + .setRight(myUser) + .setText(message.text) + .hideIcon(true) + + requireActivity().runOnUiThread { + chatView.receive(messageBuilder.build()) + } + } +} \ No newline at end of file diff --git a/Android/BluetoothChat/app/src/main/java/ditto/live/bluetoothchat/JoinFragment.kt b/Android/BluetoothChat/app/src/main/java/ditto/live/bluetoothchat/JoinFragment.kt new file mode 100644 index 0000000..5d4dbba --- /dev/null +++ b/Android/BluetoothChat/app/src/main/java/ditto/live/bluetoothchat/JoinFragment.kt @@ -0,0 +1,53 @@ +package ditto.live.bluetoothchat + +import android.bluetooth.BluetoothDevice +import android.os.Bundle +import androidx.fragment.app.Fragment +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.fragment.app.viewModels +import androidx.lifecycle.Observer +import androidx.recyclerview.widget.LinearLayoutManager +import androidx.recyclerview.widget.RecyclerView +import ditto.live.bluetoothchat.services.BluetoothChatService + +class JoinFragment : Fragment() { + + private val viewModel: BluetoothScanViewModel by viewModels() + + private val adapter by lazy { + JoinRecyclerViewAdapter(onDeviceSelected) + } + + private val onDeviceSelected: (BluetoothDevice) -> Unit = { device -> + BluetoothChatService.setCurrentDevice(requireActivity().application, device) + val fragment = ChatFragment() + requireActivity().supportFragmentManager.beginTransaction() + .add(R.id.container, fragment) + .commit() + } + + override fun onCreateView( + inflater: LayoutInflater, container: ViewGroup?, + savedInstanceState: Bundle? + ): View { + val view = inflater.inflate(R.layout.fragment_join, container, false) as RecyclerView + view.layoutManager = LinearLayoutManager(context) + view.adapter = adapter + return view + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + viewModel.scanResults.observe(viewLifecycleOwner, scanResultObserver) + } + + private val scanResultObserver = Observer> { results -> + showResults(results) + } + + private fun showResults(devices: Map) { + adapter.updateDevices(devices.values.toList()) + } +} \ No newline at end of file diff --git a/Android/BluetoothChat/app/src/main/java/ditto/live/bluetoothchat/JoinRecyclerViewAdapter.kt b/Android/BluetoothChat/app/src/main/java/ditto/live/bluetoothchat/JoinRecyclerViewAdapter.kt new file mode 100644 index 0000000..22ec363 --- /dev/null +++ b/Android/BluetoothChat/app/src/main/java/ditto/live/bluetoothchat/JoinRecyclerViewAdapter.kt @@ -0,0 +1,92 @@ +package ditto.live.bluetoothchat + +import android.bluetooth.BluetoothAdapter +import android.bluetooth.BluetoothDevice +import android.util.Log +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.TextView +import androidx.appcompat.widget.AppCompatTextView +import androidx.recyclerview.widget.RecyclerView + +class JoinRecyclerViewAdapter(private val onDeviceSelected: (BluetoothDevice) -> Unit +): RecyclerView.Adapter() { + private var devices = emptyList() + private val bluetoothName: String + + companion object { + private const val LOGO_TYPE = 0 + private const val HEADER_TYPE = 1 + private const val MY_DEVICE_TYPE = 2 + private const val DEVICE_TYPE = 3 + private const val STATIC_LIST_SIZE = 4 + } + + init { + bluetoothName = BluetoothAdapter.getDefaultAdapter().name + } + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { + val view: View + if (viewType == LOGO_TYPE) { + view = LayoutInflater.from(parent.context).inflate(R.layout.logo_item, parent, false) + return LogoViewHolder(view) + } else if (viewType == HEADER_TYPE) { + view = LayoutInflater.from(parent.context).inflate(R.layout.join_header_item, parent, false) + return HeaderViewHolder(view) + } else { + view = LayoutInflater.from(parent.context).inflate(R.layout.device_item, parent, false) + return DeviceViewHolder(view) + } + } + + override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) { + if (getItemViewType(position) == HEADER_TYPE) { + if (position == 1) { + (holder as HeaderViewHolder).headerLabelView.text = "My Device Name" + } else { + (holder as HeaderViewHolder).headerLabelView.text = "DEVICE NAME" + } + } else if (getItemViewType(position) == MY_DEVICE_TYPE) { + (holder as DeviceViewHolder).deviceNameLabelView.text = bluetoothName + } else if (getItemViewType(position) == DEVICE_TYPE) { + val device = devices[position - STATIC_LIST_SIZE] + (holder as DeviceViewHolder).deviceNameLabelView.text = device.name + holder.itemView.setOnClickListener { + onDeviceSelected(device) + } + } + } + + override fun getItemCount(): Int = devices.size + STATIC_LIST_SIZE + + inner class DeviceViewHolder(view: View) : RecyclerView.ViewHolder(view) { + val deviceNameLabelView: AppCompatTextView = view.findViewById(R.id.device) + } + + inner class LogoViewHolder(view: View) : RecyclerView.ViewHolder(view) { + + } + + inner class HeaderViewHolder(view: View) : RecyclerView.ViewHolder(view) { + val headerLabelView: AppCompatTextView = view.findViewById(R.id.header) + } + + fun updateDevices(devices: List) { + this.devices = devices + notifyDataSetChanged() + } + + override fun getItemViewType(position: Int): Int { + if (position == 0) { + return LOGO_TYPE + } else if (position == 1 || position == 3) { + return HEADER_TYPE + } else if (position == 2) { + return MY_DEVICE_TYPE + } + + return DEVICE_TYPE + } +} \ No newline at end of file diff --git a/Android/BluetoothChat/app/src/main/java/ditto/live/bluetoothchat/MainActivity.kt b/Android/BluetoothChat/app/src/main/java/ditto/live/bluetoothchat/MainActivity.kt new file mode 100644 index 0000000..6277558 --- /dev/null +++ b/Android/BluetoothChat/app/src/main/java/ditto/live/bluetoothchat/MainActivity.kt @@ -0,0 +1,69 @@ +package ditto.live.bluetoothchat + +import android.Manifest +import android.content.pm.PackageManager +import androidx.appcompat.app.AppCompatActivity +import android.os.Bundle +import androidx.core.content.ContextCompat +import ditto.live.bluetoothchat.services.BluetoothDeviceDiscovery + +private const val LOCATION_REQUEST_CODE = 1001 + +class MainActivity : AppCompatActivity() { + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(R.layout.activity_main) + + val fragment = JoinFragment() + supportFragmentManager.beginTransaction() + .add(R.id.container, fragment) + .commit() + } + + override fun onStart() { + super.onStart() + checkLocationPermission() + } + + override fun onStop() { + super.onStop() + BluetoothDeviceDiscovery.stopDiscovery() + } + + override fun onRequestPermissionsResult( + requestCode: Int, + permissions: Array, + grantResults: IntArray + ) { + super.onRequestPermissionsResult(requestCode, permissions, grantResults) + when(requestCode) { + LOCATION_REQUEST_CODE -> { + if (grantResults.isNotEmpty() && + grantResults[0] == PackageManager.PERMISSION_GRANTED) { + BluetoothDeviceDiscovery.startDiscovery(application) + } + } + } + } + + /** + * BLE SCAN + * ACCESS_FINE_LOCATION is required for Android 10 and above + */ + private fun checkLocationPermission() { + val hasLocationPermission = ContextCompat.checkSelfPermission( + this, + Manifest.permission.ACCESS_FINE_LOCATION + ) == PackageManager.PERMISSION_GRANTED + + if (hasLocationPermission) { + BluetoothDeviceDiscovery.startDiscovery(application) + } else { + requestPermissions( + arrayOf(Manifest.permission.ACCESS_FINE_LOCATION), + LOCATION_REQUEST_CODE + ) + } + } +} \ No newline at end of file diff --git a/Android/BluetoothChat/app/src/main/java/ditto/live/bluetoothchat/services/BluetoothChatService.kt b/Android/BluetoothChat/app/src/main/java/ditto/live/bluetoothchat/services/BluetoothChatService.kt new file mode 100644 index 0000000..ff85e7a --- /dev/null +++ b/Android/BluetoothChat/app/src/main/java/ditto/live/bluetoothchat/services/BluetoothChatService.kt @@ -0,0 +1,148 @@ +package ditto.live.bluetoothchat.services + + +import android.app.Application +import android.bluetooth.* +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import ditto.live.bluetoothchat.* + + +object BluetoothChatService { + + private val _messages = MutableLiveData() + val messages = _messages as LiveData + + private var gattServer: BluetoothGattServer? = null + private var gattServerCallback: BluetoothGattServerCallback? = null + private var gattClient: BluetoothGatt? = null + private var gattClientCallback: BluetoothGattCallback? = null + private var gatt: BluetoothGatt? = null + private var gattMessageCharacteristic: BluetoothGattCharacteristic? = null + + var currentDevice: BluetoothDevice? = null + + /** + * A device has been selected in our JoinFragment + * Set our current device and connect our devive with the current device + */ + fun setCurrentDevice(context: Application, device: BluetoothDevice) { + currentDevice = device + connectDevices(context, device) + } + + /** + * Create The GattClientCallback to handle Bluetooth GATT callbacks + * Set our GATT client to the remote device + */ + private fun connectDevices(context: Application, device: BluetoothDevice) { + gattClientCallback = GattClientCallback() + gattClient = device.connectGatt(context, false, gattClientCallback) + } + + /** + * Function to write messages to the remote device + */ + fun sendMessage(message: String): Boolean { + gattMessageCharacteristic?.let { characteristic -> + characteristic.writeType = BluetoothGattCharacteristic.WRITE_TYPE_DEFAULT + val messageBytes = message.toByteArray(Charsets.UTF_8) + characteristic.value = messageBytes + gatt?.let { + val success = it.writeCharacteristic(gattMessageCharacteristic) + if (success) { + _messages.value = LocalMessage(message) + } + } + } + return false + } + + /** + * Function to open a GATT server + */ + fun createServer(bluetoothManager: BluetoothManager, app: Application) { + gattServerCallback = GattServerCallback() + gattServer = bluetoothManager.openGattServer( + app, + gattServerCallback + ).apply { + addService(setupGattService()) + } + } + + /** + * Function to create the GATT service + * Set our custom service and characteristics + */ + private fun setupGattService(): BluetoothGattService { + val gattService = BluetoothGattService(chatDiscoveryServiceID, BluetoothGattService.SERVICE_TYPE_PRIMARY) + val gattCharacteristic = BluetoothGattCharacteristic( + chatMessageCharacteristicID, + BluetoothGattCharacteristic.PROPERTY_WRITE, + BluetoothGattCharacteristic.PERMISSION_WRITE + ) + val confirmCharacteristic = BluetoothGattCharacteristic( + confirmCharacteristicID, + BluetoothGattCharacteristic.PROPERTY_WRITE, + BluetoothGattCharacteristic.PERMISSION_WRITE + ) + gattService.addCharacteristic(gattCharacteristic) + gattService.addCharacteristic(confirmCharacteristic) + + return gattService + } + + /** + * Callback for the GATT Server this device implements + * Get message from the request value + * save as a remote message + */ + private class GattServerCallback : BluetoothGattServerCallback() { + override fun onCharacteristicWriteRequest( + device: BluetoothDevice, + requestId: Int, + characteristic: BluetoothGattCharacteristic, + preparedWrite: Boolean, + responseNeeded: Boolean, + offset: Int, + value: ByteArray? + ) { + super.onCharacteristicWriteRequest(device, requestId, characteristic, preparedWrite, responseNeeded, offset, value) + //check that the characteristic matches our constant value + if (characteristic.uuid == chatMessageCharacteristicID) { + //send response back to sender that message was successfully received + gattServer?.sendResponse(device, requestId, BluetoothGatt.GATT_SUCCESS, 0, null) + val message = value?.toString(Charsets.UTF_8) + message?.let { + _messages.postValue(RemoteMessage(it)) + } + } + } + } + + private class GattClientCallback : BluetoothGattCallback() { + override fun onConnectionStateChange(gatt: BluetoothGatt, status: Int, newState: Int) { + super.onConnectionStateChange(gatt, status, newState) + //Check if our GATT operation has successfully completed + //Check if our profile is in the state of connected + if (status == BluetoothGatt.GATT_SUCCESS && newState == BluetoothProfile.STATE_CONNECTED) { + gatt.discoverServices() + } + } + + /** + * new services have been discovered + * set our GATT to the new discovery + * use our UUID constants to set our message characteristic + */ + override fun onServicesDiscovered(discoveredGatt: BluetoothGatt, status: Int) { + super.onServicesDiscovered(discoveredGatt, status) + if (status == BluetoothGatt.GATT_SUCCESS) { + gatt = discoveredGatt + val service = discoveredGatt.getService(chatDiscoveryServiceID) + gattMessageCharacteristic = service.getCharacteristic(chatMessageCharacteristicID) + } + } + } +} \ No newline at end of file diff --git a/Android/BluetoothChat/app/src/main/java/ditto/live/bluetoothchat/services/BluetoothDeviceDiscovery.kt b/Android/BluetoothChat/app/src/main/java/ditto/live/bluetoothchat/services/BluetoothDeviceDiscovery.kt new file mode 100644 index 0000000..4c7b568 --- /dev/null +++ b/Android/BluetoothChat/app/src/main/java/ditto/live/bluetoothchat/services/BluetoothDeviceDiscovery.kt @@ -0,0 +1,63 @@ +package ditto.live.bluetoothchat.services + +import android.app.Application +import android.bluetooth.* +import android.bluetooth.le.AdvertiseCallback +import android.bluetooth.le.AdvertiseData +import android.bluetooth.le.AdvertiseSettings +import android.bluetooth.le.BluetoothLeAdvertiser +import android.content.Context +import android.os.ParcelUuid +import ditto.live.bluetoothchat.* + +object BluetoothDeviceDiscovery { + private class BleAdvertiseCallback : AdvertiseCallback() + private lateinit var bluetoothManager: BluetoothManager + private val bluetoothAdapter = BluetoothAdapter.getDefaultAdapter() + + private var advertiser: BluetoothLeAdvertiser? = null + private var advertiseCallback: AdvertiseCallback? = null + + fun startDiscovery(app: Application) { + bluetoothManager = app.getSystemService(Context.BLUETOOTH_SERVICE) as BluetoothManager + // + BluetoothChatService.createServer(bluetoothManager, app) + //start advertising once our BluetoothGattService has been created + startAdvertisement() + } + + /** + * Stop advertising when our app is stopped + */ + fun stopDiscovery() { + stopAdvertising() + } + + /** + * Start Bluetooth LE advertising + */ + private fun startAdvertisement() { + //set our advertiser for future BLE advertising operations + advertiser = bluetoothAdapter.bluetoothLeAdvertiser + //create a new instance of AdvertiseCallback to be used in our advertisement + advertiseCallback = BleAdvertiseCallback() + //Set the advertisement settings to ADVERTISE_MODE_LOW_LATENCY + //as we'll only be scanning for short periods looking for specfic devices + val advertiseSettings = AdvertiseSettings.Builder() + .setAdvertiseMode(AdvertiseSettings.ADVERTISE_MODE_LOW_LATENCY) + .build() + //Use our unique service ID to advertise to other devices + val advertiseData = AdvertiseData.Builder() + .addServiceUuid(ParcelUuid(chatDiscoveryServiceID)) + .setIncludeDeviceName(true).build() + advertiser?.startAdvertising(advertiseSettings, advertiseData, advertiseCallback) + } + + /** + * stop and deinit our advertisement + */ + private fun stopAdvertising() { + advertiser?.stopAdvertising(advertiseCallback) + advertiseCallback = null + } +} \ No newline at end of file diff --git a/Android/BluetoothChat/app/src/main/res/drawable-v24/ic_launcher_foreground.xml b/Android/BluetoothChat/app/src/main/res/drawable-v24/ic_launcher_foreground.xml new file mode 100644 index 0000000..2b068d1 --- /dev/null +++ b/Android/BluetoothChat/app/src/main/res/drawable-v24/ic_launcher_foreground.xml @@ -0,0 +1,30 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/Android/BluetoothChat/app/src/main/res/drawable/ic_launcher_background.xml b/Android/BluetoothChat/app/src/main/res/drawable/ic_launcher_background.xml new file mode 100644 index 0000000..07d5da9 --- /dev/null +++ b/Android/BluetoothChat/app/src/main/res/drawable/ic_launcher_background.xml @@ -0,0 +1,170 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Android/BluetoothChat/app/src/main/res/drawable/logo.png b/Android/BluetoothChat/app/src/main/res/drawable/logo.png new file mode 100644 index 0000000..323525f Binary files /dev/null and b/Android/BluetoothChat/app/src/main/res/drawable/logo.png differ diff --git a/Android/BluetoothChat/app/src/main/res/layout/activity_main.xml b/Android/BluetoothChat/app/src/main/res/layout/activity_main.xml new file mode 100644 index 0000000..72d708d --- /dev/null +++ b/Android/BluetoothChat/app/src/main/res/layout/activity_main.xml @@ -0,0 +1,9 @@ + + + + \ No newline at end of file diff --git a/Android/BluetoothChat/app/src/main/res/layout/device_item.xml b/Android/BluetoothChat/app/src/main/res/layout/device_item.xml new file mode 100644 index 0000000..5ebaaf8 --- /dev/null +++ b/Android/BluetoothChat/app/src/main/res/layout/device_item.xml @@ -0,0 +1,21 @@ + + + + \ No newline at end of file diff --git a/Android/BluetoothChat/app/src/main/res/layout/fragment_chat.xml b/Android/BluetoothChat/app/src/main/res/layout/fragment_chat.xml new file mode 100644 index 0000000..f2323e2 --- /dev/null +++ b/Android/BluetoothChat/app/src/main/res/layout/fragment_chat.xml @@ -0,0 +1,15 @@ + + + + + + \ No newline at end of file diff --git a/Android/BluetoothChat/app/src/main/res/layout/fragment_join.xml b/Android/BluetoothChat/app/src/main/res/layout/fragment_join.xml new file mode 100644 index 0000000..7d24bba --- /dev/null +++ b/Android/BluetoothChat/app/src/main/res/layout/fragment_join.xml @@ -0,0 +1,9 @@ + + \ No newline at end of file diff --git a/Android/BluetoothChat/app/src/main/res/layout/join_header_item.xml b/Android/BluetoothChat/app/src/main/res/layout/join_header_item.xml new file mode 100644 index 0000000..690aaa8 --- /dev/null +++ b/Android/BluetoothChat/app/src/main/res/layout/join_header_item.xml @@ -0,0 +1,21 @@ + + + + + \ No newline at end of file diff --git a/Android/BluetoothChat/app/src/main/res/layout/logo_item.xml b/Android/BluetoothChat/app/src/main/res/layout/logo_item.xml new file mode 100644 index 0000000..b26acf3 --- /dev/null +++ b/Android/BluetoothChat/app/src/main/res/layout/logo_item.xml @@ -0,0 +1,33 @@ + + + + + + + + \ No newline at end of file diff --git a/Android/BluetoothChat/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/Android/BluetoothChat/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml new file mode 100644 index 0000000..eca70cf --- /dev/null +++ b/Android/BluetoothChat/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/Android/BluetoothChat/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/Android/BluetoothChat/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml new file mode 100644 index 0000000..eca70cf --- /dev/null +++ b/Android/BluetoothChat/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/Android/BluetoothChat/app/src/main/res/mipmap-hdpi/ic_launcher.png b/Android/BluetoothChat/app/src/main/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 0000000..a571e60 Binary files /dev/null and b/Android/BluetoothChat/app/src/main/res/mipmap-hdpi/ic_launcher.png differ diff --git a/Android/BluetoothChat/app/src/main/res/mipmap-hdpi/ic_launcher_round.png b/Android/BluetoothChat/app/src/main/res/mipmap-hdpi/ic_launcher_round.png new file mode 100644 index 0000000..61da551 Binary files /dev/null and b/Android/BluetoothChat/app/src/main/res/mipmap-hdpi/ic_launcher_round.png differ diff --git a/Android/BluetoothChat/app/src/main/res/mipmap-mdpi/ic_launcher.png b/Android/BluetoothChat/app/src/main/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 0000000..c41dd28 Binary files /dev/null and b/Android/BluetoothChat/app/src/main/res/mipmap-mdpi/ic_launcher.png differ diff --git a/Android/BluetoothChat/app/src/main/res/mipmap-mdpi/ic_launcher_round.png b/Android/BluetoothChat/app/src/main/res/mipmap-mdpi/ic_launcher_round.png new file mode 100644 index 0000000..db5080a Binary files /dev/null and b/Android/BluetoothChat/app/src/main/res/mipmap-mdpi/ic_launcher_round.png differ diff --git a/Android/BluetoothChat/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/Android/BluetoothChat/app/src/main/res/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 0000000..6dba46d Binary files /dev/null and b/Android/BluetoothChat/app/src/main/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/Android/BluetoothChat/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png b/Android/BluetoothChat/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png new file mode 100644 index 0000000..da31a87 Binary files /dev/null and b/Android/BluetoothChat/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png differ diff --git a/Android/BluetoothChat/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/Android/BluetoothChat/app/src/main/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 0000000..15ac681 Binary files /dev/null and b/Android/BluetoothChat/app/src/main/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/Android/BluetoothChat/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png b/Android/BluetoothChat/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png new file mode 100644 index 0000000..b216f2d Binary files /dev/null and b/Android/BluetoothChat/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png differ diff --git a/Android/BluetoothChat/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/Android/BluetoothChat/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 0000000..f25a419 Binary files /dev/null and b/Android/BluetoothChat/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/Android/BluetoothChat/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png b/Android/BluetoothChat/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png new file mode 100644 index 0000000..e96783c Binary files /dev/null and b/Android/BluetoothChat/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png differ diff --git a/Android/BluetoothChat/app/src/main/res/values-night/themes.xml b/Android/BluetoothChat/app/src/main/res/values-night/themes.xml new file mode 100644 index 0000000..a95ed7a --- /dev/null +++ b/Android/BluetoothChat/app/src/main/res/values-night/themes.xml @@ -0,0 +1,16 @@ + + + + \ No newline at end of file diff --git a/Android/BluetoothChat/app/src/main/res/values/colors.xml b/Android/BluetoothChat/app/src/main/res/values/colors.xml new file mode 100644 index 0000000..f8c6127 --- /dev/null +++ b/Android/BluetoothChat/app/src/main/res/values/colors.xml @@ -0,0 +1,10 @@ + + + #FFBB86FC + #FF6200EE + #FF3700B3 + #FF03DAC5 + #FF018786 + #FF000000 + #FFFFFFFF + \ No newline at end of file diff --git a/Android/BluetoothChat/app/src/main/res/values/strings.xml b/Android/BluetoothChat/app/src/main/res/values/strings.xml new file mode 100644 index 0000000..a6a18fe --- /dev/null +++ b/Android/BluetoothChat/app/src/main/res/values/strings.xml @@ -0,0 +1,5 @@ + + BluetoothChat + + Hello blank fragment + \ No newline at end of file diff --git a/Android/BluetoothChat/app/src/main/res/values/themes.xml b/Android/BluetoothChat/app/src/main/res/values/themes.xml new file mode 100644 index 0000000..a01fb0d --- /dev/null +++ b/Android/BluetoothChat/app/src/main/res/values/themes.xml @@ -0,0 +1,16 @@ + + + + \ No newline at end of file diff --git a/Android/BluetoothChat/app/src/test/java/ditto/live/bluetoothchat/ExampleUnitTest.kt b/Android/BluetoothChat/app/src/test/java/ditto/live/bluetoothchat/ExampleUnitTest.kt new file mode 100644 index 0000000..0df080c --- /dev/null +++ b/Android/BluetoothChat/app/src/test/java/ditto/live/bluetoothchat/ExampleUnitTest.kt @@ -0,0 +1,17 @@ +package ditto.live.bluetoothchat + +import org.junit.Test + +import org.junit.Assert.* + +/** + * Example local unit test, which will execute on the development machine (host). + * + * See [testing documentation](http://d.android.com/tools/testing). + */ +class ExampleUnitTest { + @Test + fun addition_isCorrect() { + assertEquals(4, 2 + 2) + } +} \ No newline at end of file diff --git a/Android/BluetoothChat/build.gradle b/Android/BluetoothChat/build.gradle new file mode 100644 index 0000000..4e6e31c --- /dev/null +++ b/Android/BluetoothChat/build.gradle @@ -0,0 +1,26 @@ +// Top-level build file where you can add configuration options common to all sub-projects/modules. +buildscript { + ext.kotlin_version = "1.4.20" + repositories { + google() + jcenter() + } + dependencies { + classpath "com.android.tools.build:gradle:4.1.1" + classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" + + // NOTE: Do not place your application dependencies here; they belong + // in the individual module build.gradle files + } +} + +allprojects { + repositories { + google() + jcenter() + } +} + +task clean(type: Delete) { + delete rootProject.buildDir +} \ No newline at end of file diff --git a/Android/BluetoothChat/gradle.properties b/Android/BluetoothChat/gradle.properties new file mode 100644 index 0000000..98bed16 --- /dev/null +++ b/Android/BluetoothChat/gradle.properties @@ -0,0 +1,21 @@ +# Project-wide Gradle settings. +# IDE (e.g. Android Studio) users: +# Gradle settings configured through the IDE *will override* +# any settings specified in this file. +# For more details on how to configure your build environment visit +# http://www.gradle.org/docs/current/userguide/build_environment.html +# Specifies the JVM arguments used for the daemon process. +# The setting is particularly useful for tweaking memory settings. +org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8 +# When configured, Gradle will run in incubating parallel mode. +# This option should only be used with decoupled projects. More details, visit +# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects +# org.gradle.parallel=true +# AndroidX package structure to make it clearer which packages are bundled with the +# Android operating system, and which are packaged with your app"s APK +# https://developer.android.com/topic/libraries/support-library/androidx-rn +android.useAndroidX=true +# Automatically convert third-party libraries to use AndroidX +android.enableJetifier=true +# Kotlin code style for this project: "official" or "obsolete": +kotlin.code.style=official \ No newline at end of file diff --git a/Android/BluetoothChat/gradle/wrapper/gradle-wrapper.jar b/Android/BluetoothChat/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000..f6b961f Binary files /dev/null and b/Android/BluetoothChat/gradle/wrapper/gradle-wrapper.jar differ diff --git a/Android/BluetoothChat/gradle/wrapper/gradle-wrapper.properties b/Android/BluetoothChat/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..ec1ac59 --- /dev/null +++ b/Android/BluetoothChat/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,6 @@ +#Wed Feb 24 09:56:02 EST 2021 +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-6.5-bin.zip diff --git a/Android/BluetoothChat/gradlew b/Android/BluetoothChat/gradlew new file mode 100755 index 0000000..cccdd3d --- /dev/null +++ b/Android/BluetoothChat/gradlew @@ -0,0 +1,172 @@ +#!/usr/bin/env sh + +############################################################################## +## +## Gradle start up script for UN*X +## +############################################################################## + +# Attempt to set APP_HOME +# Resolve links: $0 may be a link +PRG="$0" +# Need this for relative symlinks. +while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >/dev/null +APP_HOME="`pwd -P`" +cd "$SAVED" >/dev/null + +APP_NAME="Gradle" +APP_BASE_NAME=`basename "$0"` + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS="" + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD="maximum" + +warn () { + echo "$*" +} + +die () { + echo + echo "$*" + echo + exit 1 +} + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "`uname`" in + CYGWIN* ) + cygwin=true + ;; + Darwin* ) + darwin=true + ;; + MINGW* ) + msys=true + ;; + NONSTOP* ) + nonstop=true + ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD="java" + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then + MAX_FD_LIMIT=`ulimit -H -n` + if [ $? -eq 0 ] ; then + if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then + MAX_FD="$MAX_FD_LIMIT" + fi + ulimit -n $MAX_FD + if [ $? -ne 0 ] ; then + warn "Could not set maximum file descriptor limit: $MAX_FD" + fi + else + warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" + fi +fi + +# For Darwin, add options to specify how the application appears in the dock +if $darwin; then + GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" +fi + +# For Cygwin, switch paths to Windows format before running java +if $cygwin ; then + APP_HOME=`cygpath --path --mixed "$APP_HOME"` + CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + JAVACMD=`cygpath --unix "$JAVACMD"` + + # We build the pattern for arguments to be converted via cygpath + ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` + SEP="" + for dir in $ROOTDIRSRAW ; do + ROOTDIRS="$ROOTDIRS$SEP$dir" + SEP="|" + done + OURCYGPATTERN="(^($ROOTDIRS))" + # Add a user-defined pattern to the cygpath arguments + if [ "$GRADLE_CYGPATTERN" != "" ] ; then + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" + fi + # Now convert the arguments - kludge to limit ourselves to /bin/sh + i=0 + for arg in "$@" ; do + CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` + CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option + + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition + eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` + else + eval `echo args$i`="\"$arg\"" + fi + i=$((i+1)) + done + case $i in + (0) set -- ;; + (1) set -- "$args0" ;; + (2) set -- "$args0" "$args1" ;; + (3) set -- "$args0" "$args1" "$args2" ;; + (4) set -- "$args0" "$args1" "$args2" "$args3" ;; + (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + esac +fi + +# Escape application args +save () { + for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done + echo " " +} +APP_ARGS=$(save "$@") + +# Collect all arguments for the java command, following the shell quoting and substitution rules +eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" + +# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong +if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then + cd "$(dirname "$0")" +fi + +exec "$JAVACMD" "$@" diff --git a/Android/BluetoothChat/gradlew.bat b/Android/BluetoothChat/gradlew.bat new file mode 100644 index 0000000..e95643d --- /dev/null +++ b/Android/BluetoothChat/gradlew.bat @@ -0,0 +1,84 @@ +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS= + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto init + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto init + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:init +@rem Get command-line arguments, handling Windows variants + +if not "%OS%" == "Windows_NT" goto win9xME_args + +:win9xME_args +@rem Slurp the command line arguments. +set CMD_LINE_ARGS= +set _SKIP=2 + +:win9xME_args_slurp +if "x%~1" == "x" goto execute + +set CMD_LINE_ARGS=%* + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/Android/BluetoothChat/settings.gradle b/Android/BluetoothChat/settings.gradle new file mode 100644 index 0000000..5141a2a --- /dev/null +++ b/Android/BluetoothChat/settings.gradle @@ -0,0 +1,2 @@ +include ':app' +rootProject.name = "BluetoothChat" \ No newline at end of file diff --git a/README.md b/README.md index 22afdc8..fb17a8c 100644 --- a/README.md +++ b/README.md @@ -21,6 +21,15 @@ __Running the iOS BluetoothChat__: 5. Run the app __on 2 or more physical devices__, the simulator is not recommended. -## Getting Started with Android Bluetooth +## Getting Started with Android Bluetooth(https://www.ditto.live/blog/posts/getting-started-with-android-ble) -Coming Soon! \ No newline at end of file +### Running the sample app + +__Prerequisites__: +1. Android Studio 4.0 or higher +2. Android API 4.3 or higher + +__Running the iOS BluetoothChat__: +1. Clone this repo with `git clone https://github.com/getditto/bluetooth-blog-tutorial` +2. In Android Studio, open the project at `Android/BluetoothChat` +3. Run the app __on 2 or more physical devices__, the simulator is not supported.