Add nfc support (#689)

This commit is contained in:
David Luhmer 2020-08-11 16:37:43 +02:00 committed by GitHub
parent 4354e817c6
commit ec9ff0ae6b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
30 changed files with 1034 additions and 0 deletions

View file

@ -130,6 +130,8 @@ dependencies {
implementation(Config.Dependency.AndroidX.constraintlayout)
implementation(Config.Dependency.AndroidX.recyclerview)
implementation(Config.Dependency.AndroidX.preference)
implementation(Config.Dependency.AndroidX.navigationFragment)
implementation(Config.Dependency.AndroidX.navigationUi)
implementation(Config.Dependency.Google.material)
implementation(Config.Dependency.AndroidX.roomRuntime)

View file

@ -12,6 +12,7 @@
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.RECORD_AUDIO" />
<uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS" />
<uses-permission android:name="android.permission.NFC" />
<uses-feature android:name="android.hardware.touchscreen" android:required="false"/>
<uses-feature android:name="android.hardware.telephony" android:required="false"/>
@ -120,6 +121,34 @@
android:name=".settings.SettingsActivity"
android:parentActivityName=".webview.WebViewActivity" />
<activity
android:name=".nfc.TagReaderActivity"
android:label="@string/tag_reader_title">
<tools:validation testUrl="https://www.home-assistant.io/tag/123e4567-e89b-12d3-a456-426614174000" />
<intent-filter>
<action android:name="android.nfc.action.NDEF_DISCOVERED" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data
android:scheme="https"
android:host="www.home-assistant.io"
android:pathPrefix="/tag/" />
</intent-filter>
<intent-filter android:autoVerify="true">
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data
android:scheme="https"
android:host="www.home-assistant.io"
android:pathPrefix="/tag/" />
</intent-filter>
</activity>
<activity
android:name=".nfc.NfcSetupActivity"
android:label="@string/nfc_title_nfc_setup" />
</application>
</manifest>

View file

@ -0,0 +1,79 @@
package io.homeassistant.companion.android.nfc
import android.app.Activity
import android.app.PendingIntent
import android.content.Intent
import android.content.IntentFilter
import android.nfc.NdefMessage
import android.nfc.NdefRecord
import android.nfc.NfcAdapter
import android.nfc.Tag
import android.nfc.tech.Ndef
import android.nfc.tech.NdefFormatable
import java.io.IOException
object NFCUtil {
@Throws(Exception::class)
fun createNFCMessage(url: String, intent: Intent?): Boolean {
val nfcRecord = NdefRecord.createUri(url)
val nfcMessage = NdefMessage(arrayOf(nfcRecord))
intent?.let {
val tag = it.getParcelableExtra<Tag>(NfcAdapter.EXTRA_TAG)
return writeMessageToTag(nfcMessage, tag)
}
return false
}
fun disableNFCInForeground(nfcAdapter: NfcAdapter, activity: Activity) {
nfcAdapter.disableForegroundDispatch(activity)
}
fun <T> enableNFCInForeground(nfcAdapter: NfcAdapter, activity: Activity, classType: Class<T>) {
val pendingIntent = PendingIntent.getActivity(
activity, 0,
Intent(activity, classType).addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP), 0
)
val nfcIntentFilter = IntentFilter(NfcAdapter.ACTION_NDEF_DISCOVERED)
val filters = arrayOf(nfcIntentFilter)
val techLists =
arrayOf(arrayOf(Ndef::class.java.name), arrayOf(NdefFormatable::class.java.name))
nfcAdapter.enableForegroundDispatch(activity, pendingIntent, filters, techLists)
}
@Throws(Exception::class)
private fun writeMessageToTag(nfcMessage: NdefMessage, tag: Tag?): Boolean {
val nDefTag = Ndef.get(tag)
nDefTag?.let {
it.connect()
if (it.maxSize < nfcMessage.toByteArray().size) {
// Message to large to write to NFC tag
throw Exception("Message is too large")
}
return if (it.isWritable) {
it.writeNdefMessage(nfcMessage)
it.close()
// Message is written to tag
true
} else {
throw Exception("NFC tag is read-only")
}
}
val nDefFormatableTag = NdefFormatable.get(tag)
nDefFormatableTag?.let {
try {
it.connect()
it.format(nfcMessage)
it.close()
// The data is written to the tag
} catch (e: IOException) {
// Failed to format tag
throw Exception("Failed to format tag", e)
}
}
return true
}
}

View file

@ -0,0 +1,108 @@
package io.homeassistant.companion.android.nfc
import android.annotation.SuppressLint
import android.content.Intent
import android.os.Bundle
import android.util.Log
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.Toast
import androidx.fragment.app.Fragment
import androidx.lifecycle.Observer
import androidx.lifecycle.ViewModelProvider
import androidx.navigation.fragment.findNavController
import io.homeassistant.companion.android.R
import io.homeassistant.companion.android.common.dagger.GraphComponentAccessor
import io.homeassistant.companion.android.domain.integration.IntegrationUseCase
import javax.inject.Inject
import kotlinx.android.synthetic.main.fragment_nfc_edit.*
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.cancel
import kotlinx.coroutines.launch
/**
* A simple [Fragment] subclass as the second destination in the navigation.
*/
class NfcEditFragment : Fragment() {
val TAG = NfcEditFragment::class.simpleName
private val mainScope: CoroutineScope = CoroutineScope(Dispatchers.Main + Job())
private lateinit var viewModel: NfcViewModel
@Inject
lateinit var integrationUseCase: IntegrationUseCase
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
viewModel = ViewModelProvider(requireActivity()).get(NfcViewModel::class.java)
// Inject components
DaggerProviderComponent
.builder()
.appComponent((activity?.application as GraphComponentAccessor).appComponent)
.build()
.inject(this)
// Inflate the layout for this fragment
return inflater.inflate(R.layout.fragment_nfc_edit, container, false)
}
@SuppressLint("SetTextI18n")
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
val nfcReadObserver = Observer<String> { uuid ->
mainScope.launch {
et_tag_identifier_content.setText(uuid)
val deviceName = integrationUseCase.getRegistration().deviceName!!
et_tag_example_trigger_content.setText("- platform: event\n event_type: tag_scanned\n event_data:\n device_id: $deviceName\n tag_id: $uuid")
}
}
viewModel.nfcReadEvent.observe(viewLifecycleOwner, nfcReadObserver)
btn_tag_duplicate.setOnClickListener {
viewModel.nfcWriteTagEvent.postValue(et_tag_identifier_content.text.toString())
findNavController().navigate(R.id.action_NFC_WRITE)
}
btn_tag_fire_event.setOnClickListener {
mainScope.launch {
val uuid: String = viewModel.nfcReadEvent.value.toString()
try {
integrationUseCase.scanTag(
hashMapOf("tag_id" to uuid)
)
Toast.makeText(activity, R.string.nfc_event_fired_success, Toast.LENGTH_SHORT)
.show()
} catch (e: Exception) {
Toast.makeText(activity, R.string.nfc_event_fired_fail, Toast.LENGTH_LONG)
.show()
Log.e(TAG, "Unable to send tag to Home Assistant.", e)
}
}
}
btn_tag_share_example_trigger.setOnClickListener {
val sendIntent: Intent = Intent().apply {
action = Intent.ACTION_SEND
putExtra(Intent.EXTRA_TEXT, et_tag_example_trigger_content.text)
type = "text/plain"
}
val shareIntent = Intent.createChooser(sendIntent, null)
startActivity(shareIntent)
}
}
override fun onDestroy() {
mainScope.cancel()
super.onDestroy()
}
}

View file

@ -0,0 +1,38 @@
package io.homeassistant.companion.android.nfc
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 androidx.lifecycle.ViewModelProvider
import androidx.navigation.fragment.findNavController
import io.homeassistant.companion.android.R
/**
* A simple [Fragment] subclass as the second destination in the navigation.
*/
class NfcReadFragment : Fragment() {
private lateinit var viewModel: NfcViewModel
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
viewModel = ViewModelProvider(requireActivity()).get(NfcViewModel::class.java)
return inflater.inflate(R.layout.fragment_nfc_read, container, false)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
val nfcReadObserver = Observer<String> {
findNavController().navigate(R.id.action_NFC_EDIT)
}
viewModel.nfcReadEvent.observe(viewLifecycleOwner, nfcReadObserver)
}
}

View file

@ -0,0 +1,100 @@
package io.homeassistant.companion.android.nfc
import android.content.Context
import android.content.Intent
import android.nfc.NdefMessage
import android.nfc.NfcAdapter
import android.os.Bundle
import android.util.Log
import android.view.MenuItem
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.ViewModelProvider
import io.homeassistant.companion.android.R
import io.homeassistant.companion.android.util.UrlHandler
class NfcSetupActivity : AppCompatActivity() {
val TAG = NfcSetupActivity::class.simpleName
// private val viewModel: NfcViewModel by viewModels()
private lateinit var viewModel: NfcViewModel
private var mNfcAdapter: NfcAdapter? = null
companion object {
fun newInstance(context: Context): Intent {
return Intent(context, NfcSetupActivity::class.java)
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_nfc_setup)
setSupportActionBar(findViewById(R.id.toolbar))
supportActionBar?.setDisplayHomeAsUpEnabled(true)
mNfcAdapter = NfcAdapter.getDefaultAdapter(this)
viewModel = ViewModelProvider(this).get(NfcViewModel::class.java)
}
override fun onResume() {
super.onResume()
mNfcAdapter?.let {
NFCUtil.enableNFCInForeground(it, this, javaClass)
}
}
override fun onPause() {
super.onPause()
mNfcAdapter?.let {
NFCUtil.disableNFCInForeground(it, this)
}
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
when (item.itemId) {
android.R.id.home -> {
finish()
return true
}
}
return super.onOptionsItemSelected(item)
}
override fun onNewIntent(intent: Intent) {
super.onNewIntent(intent)
if (NfcAdapter.ACTION_TECH_DISCOVERED == intent.action) {
val nfcTagToWriteUUID = viewModel.nfcWriteTagEvent.value
// Create new nfc tag
if (nfcTagToWriteUUID == null) {
val rawMessages = intent.getParcelableArrayExtra(NfcAdapter.EXTRA_NDEF_MESSAGES)
val ndefMessage = rawMessages.first() as NdefMessage
val url = ndefMessage?.records?.get(0)?.toUri().toString()
val nfcTagId = UrlHandler.splitNfcTagId(url)
if (nfcTagId == null) {
viewModel.postNewUUID()
} else {
viewModel.nfcReadEvent.postValue(nfcTagId)
}
} else {
try {
val nfcTagUrl = "https://www.home-assistant.io/tag/$nfcTagToWriteUUID"
NFCUtil.createNFCMessage(nfcTagUrl, intent)
Log.d(TAG, "Wrote nfc tag with url: $nfcTagUrl")
val message = R.string.nfc_write_tag_success
Toast.makeText(applicationContext, message, Toast.LENGTH_LONG).show()
viewModel.nfcReadEvent.value = nfcTagToWriteUUID
viewModel.nfcWriteTagDoneEvent.value = nfcTagToWriteUUID
} catch (e: Exception) {
val message = R.string.nfc_write_tag_error
Toast.makeText(applicationContext, message, Toast.LENGTH_LONG).show()
Log.e(TAG, "Unable to write tag.", e)
}
}
}
}
}

View file

@ -0,0 +1,27 @@
package io.homeassistant.companion.android.nfc
import android.util.Log
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import java.util.UUID
class NfcViewModel : ViewModel() {
// Create a LiveData with a String
val nfcReadEvent: MutableLiveData<String> = MutableLiveData()
val nfcWriteTagEvent: MutableLiveData<String> = MutableLiveData()
val nfcWriteTagDoneEvent: SingleLiveEvent<String> = SingleLiveEvent()
init {
Log.i("NfcViewModel", "NfcViewModel created!")
}
override fun onCleared() {
super.onCleared()
Log.i("NfcViewModel", "NfcViewModel destroyed!")
}
fun postNewUUID() {
nfcWriteTagEvent.postValue(UUID.randomUUID().toString())
}
}

View file

@ -0,0 +1,52 @@
package io.homeassistant.companion.android.nfc
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 androidx.lifecycle.ViewModelProvider
import androidx.navigation.fragment.findNavController
import io.homeassistant.companion.android.R
import kotlinx.android.synthetic.main.fragment_nfc_welcome.*
/**
* A simple [Fragment] subclass as the default destination in the navigation.
*/
class NfcWelcomeFragment : Fragment() {
private lateinit var viewModel: NfcViewModel
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
viewModel = ViewModelProvider(requireActivity()).get(NfcViewModel::class.java)
return inflater.inflate(R.layout.fragment_nfc_welcome, container, false)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
val nfcReadObserver = Observer<String> {
findNavController().navigate(R.id.action_NFC_READ)
}
viewModel.nfcReadEvent.observe(viewLifecycleOwner, nfcReadObserver)
val nfcWriteTagObserver = Observer<String> {
findNavController().navigate(R.id.action_NFC_WRITE)
}
viewModel.nfcWriteTagEvent.observe(viewLifecycleOwner, nfcWriteTagObserver)
btn_nfc_read.setOnClickListener {
findNavController().navigate(R.id.action_NFC_READ)
}
btn_nfc_write.setOnClickListener {
findNavController().navigate(R.id.action_NFC_WRITE)
}
}
}

View file

@ -0,0 +1,46 @@
package io.homeassistant.companion.android.nfc
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 androidx.lifecycle.ViewModelProvider
import androidx.navigation.fragment.findNavController
import io.homeassistant.companion.android.R
import kotlinx.android.synthetic.main.fragment_nfc_write.*
/**
* A simple [Fragment] subclass as the second destination in the navigation.
*/
class NfcWriteFragment : Fragment() {
private lateinit var viewModel: NfcViewModel
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
viewModel = ViewModelProvider(requireActivity()).get(NfcViewModel::class.java)
return inflater.inflate(R.layout.fragment_nfc_write, container, false)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
val nfcWriteTagObserver = Observer<String> {
tv_instructions_write_nfc.text = getString(R.string.nfc_write_tag_instructions, it)
}
viewModel.nfcWriteTagEvent.observe(viewLifecycleOwner, nfcWriteTagObserver)
val nfcWriteTagDoneObserver = Observer<String> {
findNavController().navigate(R.id.action_NFC_EDIT)
}
viewModel.nfcWriteTagDoneEvent.observe(viewLifecycleOwner, nfcWriteTagDoneObserver)
viewModel.postNewUUID()
}
}

View file

@ -0,0 +1,11 @@
package io.homeassistant.companion.android.nfc
import dagger.Component
import io.homeassistant.companion.android.common.dagger.AppComponent
@Component(dependencies = [AppComponent::class])
interface ProviderComponent {
fun inject(activity: TagReaderActivity)
fun inject(nfcEditFragment: NfcEditFragment)
}

View file

@ -0,0 +1,71 @@
package io.homeassistant.companion.android.nfc
/*
* Copyright 2017 Google Inc.
*
* 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
*
* 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,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import android.util.Log
import androidx.annotation.MainThread
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.Observer
import java.util.concurrent.atomic.AtomicBoolean
/**
* A lifecycle-aware observable that sends only new updates after subscription, used for events like
* navigation and Snackbar messages.
*
*
* This avoids a common problem with events: on configuration change (like rotation) an update
* can be emitted if the observer is active. This LiveData only calls the observable if there's an
* explicit call to setValue() or call().
*
*
* Note that only one observer is going to be notified of changes.
*/
class SingleLiveEvent<T> : MutableLiveData<T>() {
private val pending = AtomicBoolean(false)
@MainThread
override fun observe(owner: LifecycleOwner, observer: Observer<in T>) {
if (hasActiveObservers()) {
Log.w(TAG, "Multiple observers registered but only one will be notified of changes.")
}
// Observe the internal MutableLiveData
super.observe(owner, Observer { t ->
if (pending.compareAndSet(true, false)) {
observer.onChanged(t)
}
})
}
@MainThread
override fun setValue(t: T?) {
pending.set(true)
super.setValue(t)
}
/**
* Used for cases where T is Void, to make calls cleaner.
*/
@MainThread
fun call() {
value = null
}
companion object {
private val TAG = "SingleLiveEvent"
}
}

View file

@ -0,0 +1,86 @@
package io.homeassistant.companion.android.nfc
import android.content.Intent
import android.nfc.NdefMessage
import android.nfc.NfcAdapter
import android.os.Bundle
import android.util.Log
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity
import io.homeassistant.companion.android.R
import io.homeassistant.companion.android.common.dagger.GraphComponentAccessor
import io.homeassistant.companion.android.domain.integration.IntegrationUseCase
import io.homeassistant.companion.android.util.UrlHandler
import javax.inject.Inject
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.cancel
import kotlinx.coroutines.launch
class TagReaderActivity : AppCompatActivity() {
val TAG = TagReaderActivity::class.simpleName
private val mainScope: CoroutineScope = CoroutineScope(Dispatchers.Main + Job())
@Inject
lateinit var integrationUseCase: IntegrationUseCase
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_tag_reader)
setSupportActionBar(findViewById(R.id.toolbar))
// Inject components
DaggerProviderComponent
.builder()
.appComponent((application as GraphComponentAccessor).appComponent)
.build()
.inject(this)
mainScope.launch {
if (NfcAdapter.ACTION_NDEF_DISCOVERED == intent.action) {
val rawMessages = intent.getParcelableArrayExtra(NfcAdapter.EXTRA_NDEF_MESSAGES)
val ndefMessage = rawMessages[0] as NdefMessage?
val url = ndefMessage?.records?.get(0)?.toUri().toString()
try {
handleTag(url)
} catch (e: Exception) {
val message = R.string.nfc_processing_tag_error
Toast.makeText(this@TagReaderActivity, message, Toast.LENGTH_LONG).show()
Log.e(TAG, "Unable to handle url (nfc): $url", e)
finish()
}
} else if (Intent.ACTION_VIEW == intent.action) {
val url: String = intent?.data.toString()
try {
handleTag(url)
} catch (e: Exception) {
val message = R.string.qrcode_processing_tag_error
Toast.makeText(this@TagReaderActivity, message, Toast.LENGTH_LONG).show()
Log.e(TAG, "Unable to handle url (qrcode): $url", e)
finish()
}
}
}
}
override fun onDestroy() {
mainScope.cancel()
super.onDestroy()
}
private suspend fun handleTag(url: String) {
// https://www.home-assistant.io/tag/5f0ba733-172f-430d-a7f8-e4ad940c88d7
val nfcTagId = UrlHandler.splitNfcTagId(url)
if (nfcTagId != null) {
integrationUseCase.scanTag(hashMapOf("tag_id" to nfcTagId))
} else {
Toast.makeText(this, R.string.nfc_processing_tag_error, Toast.LENGTH_LONG).show()
}
finish()
}
}

View file

@ -14,6 +14,7 @@ import io.homeassistant.companion.android.PresenterModule
import io.homeassistant.companion.android.R
import io.homeassistant.companion.android.authenticator.Authenticator
import io.homeassistant.companion.android.common.dagger.GraphComponentAccessor
import io.homeassistant.companion.android.nfc.NfcSetupActivity
import io.homeassistant.companion.android.settings.shortcuts.ShortcutsFragment
import io.homeassistant.companion.android.settings.ssid.SsidDialogFragment
import io.homeassistant.companion.android.settings.ssid.SsidPreference
@ -91,6 +92,11 @@ class SettingsFragment : PreferenceFragmentCompat(), SettingsView {
true
}
findPreference<Preference>("nfc_tags")?.onPreferenceClickListener = Preference.OnPreferenceClickListener {
startActivity(NfcSetupActivity.newInstance(requireActivity()))
true
}
findPreference<EditTextPreference>("connection_internal")?.onPreferenceChangeListener =
onChangeUrlValidator

View file

@ -19,4 +19,12 @@ object UrlHandler {
fun isAbsoluteUrl(it: String?): Boolean {
return Regex("^https?://").containsMatchIn(it.toString())
}
fun splitNfcTagId(it: String?): String? {
val matches =
Regex("^https?://www\\.home-assistant\\.io/tag/([0-9a-fA-F]{8}\\-[0-9a-fA-F]{4}\\-[0-9a-fA-F]{4}\\-[0-9a-fA-F]{4}\\-[0-9a-fA-F]{12})").find(
it.toString()
)
return matches?.groups?.get(1)?.value
}
}

View file

@ -0,0 +1,23 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context="io.homeassistant.companion.android.nfc.NfcSetupActivity">
<com.google.android.material.appbar.AppBarLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<androidx.appcompat.widget.Toolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:background="?attr/colorPrimary"
android:theme="@style/ThemeOverlay.HomeAssistant.ActionBar" />
</com.google.android.material.appbar.AppBarLayout>
<include layout="@layout/content_nfc_setup" />
</androidx.coordinatorlayout.widget.CoordinatorLayout>

View file

@ -0,0 +1,36 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout 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=".nfc.TagReaderActivity">
<com.google.android.material.appbar.AppBarLayout
android:id="@+id/appBarLayout"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<androidx.appcompat.widget.Toolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:background="?attr/colorPrimary"
android:theme="@style/ThemeOverlay.HomeAssistant.ActionBar" />
</com.google.android.material.appbar.AppBarLayout>
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<ProgressBar
android:id="@+id/nfc_loading"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.coordinatorlayout.widget.CoordinatorLayout>

View file

@ -0,0 +1,17 @@
<?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"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:padding="16dp"
app:layout_behavior="@string/appbar_scrolling_view_behavior">
<androidx.fragment.app.FragmentContainerView
android:id="@+id/nav_host_fragment"
android:name="androidx.navigation.fragment.NavHostFragment"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:defaultNavHost="true"
app:navGraph="@navigation/nfc_nav_graph" />
</androidx.constraintlayout.widget.ConstraintLayout>

View file

@ -0,0 +1,83 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout android:id="@+id/container_nfc_tag_preview"
android:layout_width="match_parent"
android:layout_height="wrap_content"
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<TextView
android:id="@+id/tv_tag_identifier_headline"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/nfc_tag_identifier"
app:layout_constraintTop_toTopOf="parent" />
<EditText
android:id="@+id/et_tag_identifier_content"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:ems="10"
android:gravity="start|top"
android:inputType="textMultiLine"
android:editable="false"
app:layout_constraintTop_toBottomOf="@+id/tv_tag_identifier_headline" />
<com.google.android.material.button.MaterialButton
android:id="@+id/btn_tag_duplicate"
style="@style/Widget.HomeAssistant.Button.Colored"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:text="@string/nfc_btn_create_duplicate"
android:layout_marginEnd="8dp"
app:layout_constraintEnd_toStartOf="@+id/guideline"
app:layout_constraintHorizontal_chainStyle="spread"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/et_tag_identifier_content" />
<com.google.android.material.button.MaterialButton
android:id="@+id/btn_tag_fire_event"
style="@style/Widget.HomeAssistant.Button.Colored"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:text="@string/nfc_btn_fire_event"
android:layout_marginStart="8dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="@+id/guideline"
app:layout_constraintTop_toBottomOf="@+id/et_tag_identifier_content" />
<TextView
android:id="@+id/tv_tag_example_trigger_headline"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/nfc_example_trigger"
app:layout_constraintTop_toBottomOf="@+id/btn_tag_duplicate"
android:layout_marginTop="48dp" />
<EditText
android:id="@+id/et_tag_example_trigger_content"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:ems="10"
android:gravity="start|top"
android:inputType="textMultiLine"
android:editable="false"
app:layout_constraintTop_toBottomOf="@+id/tv_tag_example_trigger_headline" />
<androidx.appcompat.widget.AppCompatButton
android:id="@+id/btn_tag_share_example_trigger"
style="@style/Widget.HomeAssistant.Button.Colored"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/nfc_btn_share"
app:layout_constraintTop_toBottomOf="@+id/et_tag_example_trigger_content" />
<androidx.constraintlayout.widget.Guideline
android:id="@+id/guideline"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical"
app:layout_constraintGuide_percent="0.5" />
</androidx.constraintlayout.widget.ConstraintLayout>

View file

@ -0,0 +1,19 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout android:id="@+id/container_nfc_tag_preview"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:padding="16dp"
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<TextView
android:id="@+id/tv_instructions_read_nfc"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/nfc_read_tag_instructions"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintTop_toTopOf="parent"
android:gravity="center_horizontal" />
</androidx.constraintlayout.widget.ConstraintLayout>

View file

@ -0,0 +1,46 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout android:id="@+id/container_mode_switch"
android:layout_width="match_parent"
android:layout_height="match_parent"
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
xmlns:app="http://schemas.android.com/apk/res-auto">
<TextView
android:id="@+id/tv_welcome_message"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/nfc_welcome_message"
app:layout_constraintTop_toTopOf="parent"
android:paddingBottom="16dp"/>
<com.google.android.material.button.MaterialButton
android:id="@+id/btn_nfc_read"
style="@style/Widget.HomeAssistant.Button.Colored"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:text="@string/nfc_btn_read_tag"
android:layout_marginEnd="8dp"
app:layout_constraintEnd_toStartOf="@+id/guideline"
app:layout_constraintHorizontal_chainStyle="spread"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/tv_welcome_message" />
<com.google.android.material.button.MaterialButton
android:id="@+id/btn_nfc_write"
style="@style/Widget.HomeAssistant.Button.Colored"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:text="@string/nfc_btn_write_tag"
android:layout_marginStart="8dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="@+id/guideline"
app:layout_constraintTop_toBottomOf="@+id/tv_welcome_message" />
<androidx.constraintlayout.widget.Guideline
android:id="@+id/guideline"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical"
app:layout_constraintGuide_percent="0.5" />
</androidx.constraintlayout.widget.ConstraintLayout>

View file

@ -0,0 +1,20 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout android:id="@+id/container_nfc_read"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:padding="16dp"
android:visibility="visible"
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<TextView
android:id="@+id/tv_instructions_write_nfc"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/nfc_write_tag_instructions"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintTop_toTopOf="parent"
android:gravity="center_horizontal" />
</androidx.constraintlayout.widget.ConstraintLayout>

View file

@ -0,0 +1,55 @@
<?xml version="1.0" encoding="utf-8"?>
<navigation 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:id="@+id/nav_graph"
app:startDestination="@id/WelcomeFragment">
<fragment
android:id="@+id/WelcomeFragment"
android:name="io.homeassistant.companion.android.nfc.NfcWelcomeFragment"
android:label="@string/nfc_welcome_fragment_label"
tools:layout="@layout/fragment_nfc_welcome">
<action
android:id="@+id/action_NFC_READ"
app:destination="@id/ReadFragment" />
<action
android:id="@+id/action_NFC_WRITE"
app:destination="@id/WriteFragment" />
</fragment>
<fragment
android:id="@+id/WriteFragment"
android:name="io.homeassistant.companion.android.nfc.NfcWriteFragment"
android:label="@string/nfc_write_fragment_label"
tools:layout="@layout/fragment_nfc_write">
<action
android:id="@+id/action_NFC_EDIT"
app:destination="@id/EditFragment" />
</fragment>
<fragment
android:id="@+id/ReadFragment"
android:name="io.homeassistant.companion.android.nfc.NfcReadFragment"
android:label="@string/nfc_read_fragment_label"
tools:layout="@layout/fragment_nfc_read">
<action
android:id="@+id/action_NFC_EDIT"
app:destination="@id/EditFragment" />
</fragment>
<fragment
android:id="@+id/EditFragment"
android:name="io.homeassistant.companion.android.nfc.NfcEditFragment"
android:label="@string/nfc_edit_fragment_label"
tools:layout="@layout/fragment_nfc_edit">
<action
android:id="@+id/action_NFC_WRITE"
app:destination="@id/WriteFragment" />
</fragment>
</navigation>

View file

@ -1,5 +1,30 @@
<?xml version="1.0" encoding="UTF-8"?>
<resources>
<string name="nfc_tag_identifier">Tag Identifier</string>
<string name="nfc_btn_share">Share</string>
<string name="nfc_btn_create_duplicate">Create duplicate</string>
<string name="nfc_btn_fire_event">Fire event</string>
<string name="nfc_btn_read_tag">Read NFC Tag</string>
<string name="nfc_btn_write_tag">Write NFC Tag</string>
<string name="nfc_event_fired_success">Event fired</string>
<string name="nfc_event_fired_fail">Firing event failed. Please try again.</string>
<string name="nfc_title_nfc_setup">NFC Tags</string>
<string name="nfc_title_settings">NFC Tags</string>
<string name="nfc_welcome_fragment_label">NFC Welcome Fragment</string>
<string name="nfc_read_fragment_label">NFC Read Fragment</string>
<string name="nfc_write_fragment_label">NFC Write Fragment</string>
<string name="nfc_edit_fragment_label">NFC Edit Fragment</string>
<string name="nfc_example_trigger">Example Trigger</string>
<string name="nfc_read_tag_instructions">Place your phone over the nfc tag to read it.</string>
<string name="nfc_write_tag_instructions">Place phone over nfc tag to write the following identifier to it %1$s</string>
<string name="nfc_write_tag_too_early">Please fill out the form first</string>
<string name="nfc_write_tag_success">Successfully wrote nfc tag</string>
<string name="nfc_write_tag_error">Error while writing nfc tag</string>
<string name="nfc_processing_tag_error">Error while processing nfc tag</string>
<string name="nfc_summary">Setup NFC Tags</string>
<string name="nfc_welcome_message">NFC tags written by the app will fire an event once you bring your device near them.\n\nTags will work on any device with Home Assistant installed which has hardware support to read them.</string>
<string name="tag_reader_title">Processing Tag</string>
<string name="qrcode_processing_tag_error">Error while processing qrcode tag</string>
<string name="add_service_data_field">Add Field</string>
<string name="add_widget">Add widget</string>
<string name="app_name">Home Assistant</string>

View file

@ -62,6 +62,12 @@
android:icon="@drawable/ic_plus"
android:title="@string/shortcuts"
android:summary="@string/shortcuts_summary" />
<Preference
android:key="nfc_tags"
android:icon="@drawable/ic_plus"
android:title="@string/nfc_title_settings"
android:summary="@string/nfc_summary" />
</PreferenceCategory>
<PreferenceCategory
android:title="@string/app_version_info">

View file

@ -51,6 +51,9 @@ object Config {
const val constraintlayout = "androidx.constraintlayout:constraintlayout:1.1.3"
const val preference = "androidx.preference:preference-ktx:1.1.1"
const val navigationFragment = "androidx.navigation:navigation-fragment-ktx:2.3.0"
const val navigationUi = "androidx.navigation:navigation-ui-ktx:2.3.0"
const val workManager = "androidx.work:work-runtime-ktx:2.3.4"
const val biometric = "androidx.biometric:biometric:1.0.1"

View file

@ -174,6 +174,30 @@ class IntegrationRepositoryImpl @Inject constructor(
throw IntegrationException()
}
override suspend fun scanTag(data: HashMap<String, Any>) {
var wasSuccess = false
for (it in urlRepository.getApiUrls()) {
try {
wasSuccess =
integrationService.scanTag(
it.toHttpUrlOrNull()!!,
IntegrationRequest(
"scan_tag",
data
)
).isSuccessful
} catch (e: Exception) {
// Ignore failure until we are out of URLS to try!
}
// if we had a successful call we can return
if (wasSuccess)
return
}
throw IntegrationException()
}
override suspend fun fireEvent(eventType: String, eventData: Map<String, Any>) {
var wasSuccess = false

View file

@ -59,6 +59,12 @@ interface IntegrationService {
@Body request: IntegrationRequest
): Response<ResponseBody>
@POST
suspend fun scanTag(
@Url url: HttpUrl,
@Body request: IntegrationRequest
): Response<ResponseBody>
@POST
suspend fun fireEvent(
@Url url: HttpUrl,

View file

@ -37,6 +37,8 @@ interface IntegrationRepository {
suspend fun callService(domain: String, service: String, serviceData: HashMap<String, Any>)
suspend fun scanTag(data: HashMap<String, Any>)
suspend fun fireEvent(eventType: String, eventData: Map<String, Any>)
suspend fun registerSensor(sensorRegistration: SensorRegistration<Any>)

View file

@ -23,6 +23,8 @@ interface IntegrationUseCase {
suspend fun fireEvent(eventType: String, eventData: Map<String, Any>)
suspend fun scanTag(data: HashMap<String, Any>)
suspend fun getZones(): Array<Entity<ZoneAttributes>>
suspend fun setZoneTrackingEnabled(enabled: Boolean)

View file

@ -48,6 +48,10 @@ class IntegrationUseCaseImpl @Inject constructor(
return integrationRepository.fireEvent(eventType, eventData)
}
override suspend fun scanTag(data: HashMap<String, Any>) {
return integrationRepository.scanTag(data)
}
override suspend fun getZones(): Array<Entity<ZoneAttributes>> {
return integrationRepository.getZones()
}