Skip to content

Instantly share code, notes, and snippets.

@Kaspic
Last active February 2, 2021 18:54
Show Gist options
  • Select an option

  • Save Kaspic/82dd2b9f26e4bdf80933fe577c67ce4d to your computer and use it in GitHub Desktop.

Select an option

Save Kaspic/82dd2b9f26e4bdf80933fe577c67ce4d to your computer and use it in GitHub Desktop.
Samsung Galaxy Watch Companion App Tutorial - Part 1
<!DOCTYPE resources [
<!ELEMENT resources (application)>
<!ELEMENT application (serviceProfile)+>
<!ATTLIST application name CDATA #REQUIRED>
<!ELEMENT serviceProfile (supportedTransports, serviceChannel+) >
<!ATTLIST application xmlns:android CDATA #IMPLIED>
<!ATTLIST serviceProfile xmlns:android CDATA #IMPLIED>
<!ATTLIST serviceProfile serviceImpl CDATA #REQUIRED>
<!ATTLIST serviceProfile role (PROVIDER | CONSUMER | provider | consumer) #REQUIRED>
<!ATTLIST serviceProfile name CDATA #REQUIRED>
<!ATTLIST serviceProfile id CDATA #REQUIRED>
<!ATTLIST serviceProfile version CDATA #REQUIRED>
<!ATTLIST serviceProfile serviceLimit (ANY | ONE_ACCESSORY | ONE_PEERAGENT | any | one_accessory | one_peeragent) #IMPLIED>
<!ATTLIST serviceProfile serviceTimeout CDATA #IMPLIED>
<!ELEMENT supportedTransports (transport)+>
<!ATTLIST supportedTransports xmlns:android CDATA #IMPLIED>
<!ELEMENT transport EMPTY>
<!ATTLIST transport xmlns:android CDATA #IMPLIED>
<!ATTLIST transport type (TRANSPORT_WIFI | TRANSPORT_BT | TRANSPORT_BLE | TRANSPORT_USB |
transport_wifi | transport_bt | transport_ble | transport_usb) #REQUIRED>
<!ELEMENT serviceChannel EMPTY>
<!ATTLIST serviceChannel xmlns:android CDATA #IMPLIED>
<!ATTLIST serviceChannel id CDATA #REQUIRED>
<!ATTLIST serviceChannel dataRate (LOW | HIGH | low | high) #REQUIRED>
<!ATTLIST serviceChannel priority (LOW | MEDIUM | HIGH | low | medium | high) #REQUIRED>
<!ATTLIST serviceChannel reliability (ENABLE | DISABLE | enable | disable ) #REQUIRED>
]>
<resources>
<application name="ExampleApp" >
<serviceProfile
id="/com/example"
name="GalaxyProvider"
role="provider"
serviceImpl="com.example.galaxyprovider.GalaxyProviderService"
version="1.0"
serviceLimit="ANY"
serviceTimeout="15">
<supportedTransports>
<transport type="TRANSPORT_BT" />
</supportedTransports>
<serviceChannel
id="333"
dataRate="low"
priority="low"
reliability= "enable"/>
</serviceProfile>
</application>
</resources>
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/textInputEditText"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_margin="16dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<MaterialButton
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/textInputEditText" />
</androidx.constraintlayout.widget.ConstraintLayout>
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="com.samsung.accessory.permission.ACCESSORY_FRAMEWORK" />
<application>
<service android:name="GalaxyProviderService" />
<receiver android:name="com.samsung.android.sdk.accessory.RegisterUponInstallReceiver">
<intent-filter>
<action android:name="com.samsung.accessory.action.REGISTER_AGENT" />
</intent-filter>
</receiver>
<receiver android:name="com.samsung.android.sdk.accessory.ServiceConnectionIndicationBroadcastReceiver">
<intent-filter>
<action android:name="com.samsung.accessory.action.SERVICE_CONNECTION_REQUESTED" />
</intent-filter>
</receiver>
</application
dependencies {
...
implementation fileTree(dir: 'libs', include: ['*.jar'])
}
import android.app.Notification
import android.app.NotificationChannel
import android.app.NotificationManager
import android.content.Intent
import android.os.Binder
import android.os.Build
import android.os.IBinder
import android.util.Log
import android.widget.Toast
import androidx.core.app.NotificationCompat
import com.samsung.android.sdk.SsdkUnsupportedException
import com.samsung.android.sdk.accessory.SA
import com.samsung.android.sdk.accessory.SAAgent
import com.samsung.android.sdk.accessory.SAPeerAgent
class GalaxyProviderService : SAAgent(TAG, SASOCKET_CLASS), GalaxyServiceConnection.GalaxyServiceConnectionListener {
companion object {
private const val TAG = "GalaxyProviderService"
private val SASOCKET_CLASS: Class<GalaxyServiceConnection> = GalaxyServiceConnection::class.java
private const val CHANNEL_ID = "ExampleCompanionApp"
private const val CHANNEL_NAME = "ExampleCompanionApp"
private const val NOTIFICATION_ID = 1234
private const val SAP_CONNECTION_CHANNEL_ID = 333
}
private val binder: IBinder = LocalBinder()
private var connectionHandler: GalaxyServiceConnection? = null
private var notificationManager: NotificationManager? = null
private val requiresForegroundService = Build.VERSION.SDK_INT >= Build.VERSION_CODES.O
override fun onCreate() {
super.onCreate()
if (requiresForegroundService) {
if (notificationManager == null) {
notificationManager = getSystemService(NOTIFICATION_SERVICE) as NotificationManager
val notificationChannel = NotificationChannel(
CHANNEL_ID,
CHANNEL_NAME,
NotificationManager.IMPORTANCE_LOW
)
notificationManager?.createNotificationChannel(notificationChannel)
}
val initialNotificationMessage = "Galaxy connection ready"
val notification = createNotification(initialNotificationMessage)
startForeground(NOTIFICATION_ID, notification)
}
try {
val accessory = SA()
accessory.initialize(this)
} catch (e: SsdkUnsupportedException) {
if (processUnsupportedException(e)) {
terminateService()
return
}
} catch (e1: Exception) {
e1.printStackTrace()
stopSelf()
}
}
private fun createNotification(notificationMessage: String): Notification {
val notificationTitle = "Galaxy Connection"
return NotificationCompat.Builder(applicationContext, CHANNEL_ID)
.setContentTitle(notificationTitle)
.setContentText(notificationMessage)
.setChannelId(CHANNEL_ID)
.build()
}
private fun processUnsupportedException(ssdkUnsupportedException: SsdkUnsupportedException): Boolean {
ssdkUnsupportedException.printStackTrace()
when (ssdkUnsupportedException.type) {
SsdkUnsupportedException.VENDOR_NOT_SUPPORTED, SsdkUnsupportedException.DEVICE_NOT_SUPPORTED -> {
stopSelf()
}
SsdkUnsupportedException.LIBRARY_NOT_INSTALLED -> {
Log.e(TAG, "You need to install Samsung Accessory SDK to use this application.")
}
SsdkUnsupportedException.LIBRARY_UPDATE_IS_REQUIRED -> {
Log.e(TAG, "You need to update Samsung Accessory SDK to use this application.")
}
}
return true
}
fun sendMessage(currentMessage: String) {
connectionHandler?.let { connectionHandler ->
try {
Log.d(TAG, "Message sent: $currentMessage")
connectionHandler.send(SAP_CONNECTION_CHANNEL_ID, currentMessage.toByteArray())
} catch (e: Exception) {
Log.e(TAG, "SendData error: $e")
}
}
}
private fun stopForegroundNotification() {
if (requiresForegroundService) {
notificationManager?.cancel(NOTIFICATION_ID)
stopForeground(true)
}
}
private fun terminateService() {
connectionHandler?.close()
stopForegroundNotification()
stopSelf()
}
override fun onServiceConnectionRequested(peerAgent: SAPeerAgent?) {
if (peerAgent != null) {
Log.d(TAG, "----> onServiceConnectionRequested(): $peerAgent")
Toast.makeText(baseContext, "Connection established", Toast.LENGTH_SHORT).show()
createNotification("Connected")
acceptServiceConnectionRequest(peerAgent)
} else {
terminateService()
}
}
override fun onServiceConnectionLost() {
connectionHandler?.callback = null
connectionHandler?.close()
connectionHandler = null
mainExecutor.execute {
Toast.makeText(baseContext, "Connection to galaxy watch lost", Toast.LENGTH_SHORT)
.show()
}
}
override fun onBind(intent: Intent): IBinder = binder
override fun onTaskRemoved(rootIntent: Intent?) = terminateService()
inner class LocalBinder : Binder() {
val service: GalaxyProviderService
get() = this@GalaxyProviderService
}
}
import android.util.Log
import com.samsung.android.sdk.accessory.SASocket
class GalaxyServiceConnection : SASocket(GalaxyProviderService::class.java.name) {
companion object {
private const val TAG = "GalaxyServiceConnection"
}
var callback: GalaxyServiceConnectionListener? = null
override fun onError(channelId: Int, errorMessage: String?, errorCode: Int) {
Log.e(TAG, "----> onError: errorCode: $errorCode -- $errorMessage")
}
override fun onReceive(channelId: Int, data: ByteArray?) {
Log.d(TAG, "----> ServiceConnection onReceive(): " + data?.let { String(it) })
}
override fun onServiceConnectionLost(reason: Int) {
Log.e(TAG, "----> onServiceConnectionLost: reason: $reason")
callback?.onServiceConnectionLost()
}
interface GalaxyServiceConnectionListener {
fun onServiceConnectionLost()
}
}
<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:tools="http://schemas.android.com/tools"
tools:keep="@xml/accessoryservices" />
import android.content.ComponentName
import android.content.Context
import android.content.Intent
import android.content.ServiceConnection
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.os.IBinder
import android.os.PersistableBundle
import com.google.android.material.button.MaterialButton
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.google.android.material.textfield.TextInputEditText
class MainActivity : AppCompatActivity(R.layout.activity_main) {
private var boundService: GalaxyProviderService? = null
private var serviceIntent = Intent(applicationContext, GalaxyProviderService::class.java)
private val boundServiceConnection: ServiceConnection = object : ServiceConnection {
override fun onServiceConnected(name: ComponentName, service: IBinder) {
val binderBridge: GalaxyProviderService.LocalBinder = service as GalaxyProviderService.LocalBinder
boundService = binderBridge.service
}
override fun onServiceDisconnected(name: ComponentName) {
boundService = null
}
}
override fun onCreate(savedInstanceState: Bundle?, persistentState: PersistableBundle?) {
super.onCreate(savedInstanceState, persistentState)
setClickListeners()
initializeGalaxyProviderService()
}
override fun onDestroy() {
super.onDestroy()
disconnect()
}
private fun setClickListeners() {
findViewById<MaterialButton>(R.id.send_button).setOnClickListener { sendCurrentInputMessage() }
}
private fun disconnect() {
boundService = null
applicationContext.unbindService(boundServiceConnection)
applicationContext.stopService(serviceIntent)
}
private fun sendCurrentInputMessage() {
val inputEditText = findViewById<TextInputEditText>(R.id.input)
val currentMessage = inputEditText.text.toString()
boundService?.sendMessage(currentMessage) ?: kotlin.run {
MaterialAlertDialogBuilder(this)
.setMessage("No active connection available")
.setPositiveButton(android.R.string.ok, null)
.show()
}
inputEditText.text?.clear()
}
private fun initializeGalaxyProviderService() {
if (boundService == null) {
val serviceAlreadyExisted = applicationContext.bindService(serviceIntent, boundServiceConnection, Context.BIND_AUTO_CREATE)
if (!serviceAlreadyExisted) {
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) {
applicationContext.startForegroundService(serviceIntent)
} else {
applicationContext.startService(serviceIntent)
}
applicationContext.bindService(serviceIntent, boundServiceConnection, Context.BIND_AUTO_CREATE)
}
}
}
}
-keepclassmembers class com.samsung.** { *; }
-keep class com.samsung.** { *; }
-dontwarn com.samsung.**
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment