Convert NFC tags interface to Compose and update design (#2441)

* Convert NFC tags interface to Compose

 - Rebuild the NFC tags interface with Compose instead of multiple fragments, move more functionality to the NfcViewModel
 - Design updates to match other settings screens (white top app bar, additional icons, don't use text inputs for showing data)
 - Update strings for consistency
 - Update Navigation version to latest stable, and remove non-Compose dependency because it is no longer required

* Update NFC trigger example

 - Add slightly more text to explain how to use it as a trigger in the visual automation editor, which most users will use
 - Add an easier example for scanning the tag, and rename the existing example to clarify that it is only for this device

* Add link to tag documentation to top app bar

* Handle NFC turned off

 - Check to see if NFC is actually turned on when the user opens settings, and prompt them to turn it on if required. Also do the same for the tag writing screen because that might be opened directly by the frontend.
 - Update strings to always use 'device' instead of a mix of 'device' and 'phone'

* Add option to change ID written to tag

 - Allow the user to change the ID that is written to the NFC tag when using the 'write tag' button from the app settings, to match frontend and iOS which also allow the user to change the tag ID

* Don't allow setting state outside NfcViewModel

* Simplify device trigger example even further

* Don't duplicate tag data extraction code

* Also convert tag reader activity to Compose

 - Shouldn't be seen in most cases by users
This commit is contained in:
Joris Pelgröm 2022-04-13 00:29:57 +02:00 committed by GitHub
parent 6c64b31f7e
commit 8ce64f5ed6
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
30 changed files with 799 additions and 735 deletions

View file

@ -151,8 +151,6 @@ dependencies {
implementation("androidx.constraintlayout:constraintlayout:2.1.3")
implementation("androidx.recyclerview:recyclerview:1.2.1")
implementation("androidx.preference:preference-ktx:1.1.1")
implementation("androidx.navigation:navigation-fragment-ktx:2.3.5")
implementation("androidx.navigation:navigation-ui-ktx:2.3.5")
implementation("com.google.android.material:material:1.5.0")
implementation("com.squareup.retrofit2:retrofit:2.9.0")
@ -191,7 +189,7 @@ dependencies {
implementation("androidx.compose.ui:ui:1.1.1")
implementation("androidx.compose.ui:ui-tooling:1.1.1")
implementation("androidx.activity:activity-compose:1.4.0")
implementation("androidx.navigation:navigation-compose:2.4.0-rc01")
implementation("androidx.navigation:navigation-compose:2.4.2")
implementation("com.google.android.material:compose-theme-adapter:1.1.3")
implementation("com.google.accompanist:accompanist-appcompat-theme:0.23.1")

View file

@ -353,7 +353,8 @@
<activity
android:name=".nfc.NfcSetupActivity"
android:exported="false"
android:label="@string/nfc_title_nfc_setup" />
android:label="@string/nfc_title_nfc_setup"
android:theme="@style/Theme.HomeAssistant.Config" />
<activity android:name=".share.ShareActivity"
android:exported="true">

View file

@ -4,6 +4,7 @@ import android.app.Activity
import android.app.PendingIntent
import android.content.Intent
import android.content.IntentFilter
import android.net.Uri
import android.nfc.NdefMessage
import android.nfc.NdefRecord
import android.nfc.NfcAdapter
@ -14,6 +15,16 @@ import io.homeassistant.companion.android.BuildConfig
import java.io.IOException
object NFCUtil {
fun extractUrlFromNFCIntent(intent: Intent): Uri? {
if (intent.action != NfcAdapter.ACTION_NDEF_DISCOVERED && intent.action != NfcAdapter.ACTION_TECH_DISCOVERED) {
return null
}
val rawMessages = intent.getParcelableArrayExtra(NfcAdapter.EXTRA_NDEF_MESSAGES)
val ndefMessage = rawMessages?.get(0) as NdefMessage?
return ndefMessage?.records?.get(0)?.toUri()
}
@Throws(Exception::class)
fun createNFCMessage(url: String, intent: Intent?): Boolean {
val nfcRecord = NdefRecord.createUri(url)

View file

@ -1,107 +0,0 @@
package io.homeassistant.companion.android.nfc
import android.annotation.SuppressLint
import android.content.Intent
import android.os.Bundle
import android.provider.Settings
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.fragment.app.activityViewModels
import androidx.lifecycle.Observer
import androidx.navigation.fragment.findNavController
import dagger.hilt.android.AndroidEntryPoint
import io.homeassistant.companion.android.R
import io.homeassistant.companion.android.common.data.integration.IntegrationRepository
import io.homeassistant.companion.android.databinding.FragmentNfcEditBinding
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.cancel
import kotlinx.coroutines.launch
import javax.inject.Inject
import io.homeassistant.companion.android.common.R as commonR
/**
* A simple [Fragment] subclass as the second destination in the navigation.
*/
@AndroidEntryPoint
class NfcEditFragment : Fragment() {
val TAG = NfcEditFragment::class.simpleName
private val mainScope: CoroutineScope = CoroutineScope(Dispatchers.Main + Job())
private var _binding: FragmentNfcEditBinding? = null
private val binding get() = _binding!!
private val viewModel: NfcViewModel by activityViewModels()
@Inject
lateinit var integrationUseCase: IntegrationRepository
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
// Inflate the layout for this fragment
_binding = FragmentNfcEditBinding.inflate(inflater, container, false)
return binding.root
}
@SuppressLint("SetTextI18n", "HardwareIds")
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
val nfcReadObserver = Observer<String> { uuid ->
binding.etTagIdentifierContent.setText(uuid)
val deviceId = Settings.Secure.getString(requireActivity().contentResolver, Settings.Secure.ANDROID_ID)
binding.etTagExampleTriggerContent.setText("- platform: event\n event_type: tag_scanned\n event_data:\n device_id: $deviceId\n tag_id: $uuid")
}
viewModel.nfcReadEvent.observe(viewLifecycleOwner, nfcReadObserver)
binding.btnTagDuplicate.setOnClickListener {
viewModel.nfcWriteTagEvent.postValue(binding.etTagIdentifierContent.text.toString())
findNavController().navigate(R.id.action_NFC_WRITE)
}
binding.btnTagFireEvent.setOnClickListener {
mainScope.launch {
val uuid: String = viewModel.nfcReadEvent.value.toString()
try {
integrationUseCase.scanTag(
hashMapOf("tag_id" to uuid)
)
Toast.makeText(activity, commonR.string.nfc_event_fired_success, Toast.LENGTH_SHORT)
.show()
} catch (e: Exception) {
Toast.makeText(activity, commonR.string.nfc_event_fired_fail, Toast.LENGTH_LONG)
.show()
Log.e(TAG, "Unable to send tag to Home Assistant.", e)
}
}
}
binding.btnTagShareExampleTrigger.setOnClickListener {
val sendIntent: Intent = Intent().apply {
action = Intent.ACTION_SEND
putExtra(Intent.EXTRA_TEXT, binding.etTagExampleTriggerContent.text)
type = "text/plain"
}
val shareIntent = Intent.createChooser(sendIntent, null)
startActivity(shareIntent)
}
}
override fun onDestroyView() {
super.onDestroyView()
_binding = null
}
override fun onDestroy() {
mainScope.cancel()
super.onDestroy()
}
}

View file

@ -1,24 +0,0 @@
package io.homeassistant.companion.android.nfc
import android.os.Bundle
import android.view.View
import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels
import androidx.lifecycle.Observer
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(R.layout.fragment_nfc_read) {
private val viewModel: NfcViewModel by activityViewModels()
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
val nfcReadObserver = Observer<String> {
findNavController().navigate(R.id.action_NFC_EDIT)
}
viewModel.nfcReadEvent.observe(viewLifecycleOwner, nfcReadObserver)
}
}

View file

@ -1,18 +1,22 @@
package io.homeassistant.companion.android.nfc
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.nfc.NdefMessage
import android.content.IntentFilter
import android.nfc.NfcAdapter
import android.os.Bundle
import android.util.Log
import android.view.MenuItem
import android.widget.Toast
import androidx.activity.compose.setContent
import androidx.activity.viewModels
import androidx.lifecycle.lifecycleScope
import com.google.android.material.composethemeadapter.MdcTheme
import dagger.hilt.android.AndroidEntryPoint
import io.homeassistant.companion.android.BaseActivity
import io.homeassistant.companion.android.R
import io.homeassistant.companion.android.nfc.views.LoadNfcView
import io.homeassistant.companion.android.util.UrlHandler
import kotlinx.coroutines.launch
import io.homeassistant.companion.android.common.R as commonR
@AndroidEntryPoint
@ -23,11 +27,22 @@ class NfcSetupActivity : BaseActivity() {
private var simpleWrite = false
private var messageId: Int = -1
private val nfcStateChangedReceiver = object : BroadcastReceiver() {
override fun onReceive(context: Context?, intent: Intent?) {
if (intent?.action == NfcAdapter.ACTION_ADAPTER_STATE_CHANGED) viewModel.checkNfcEnabled()
}
}
companion object {
val TAG = NfcSetupActivity::class.simpleName
const val EXTRA_TAG_VALUE = "tag_value"
const val EXTRA_MESSAGE_ID = "message_id"
const val NAV_WELCOME = "nfc_welcome"
const val NAV_READ = "nfc_read"
const val NAV_WRITE = "nfc_write"
const val NAV_EDIT = "nfc_edit"
fun newInstance(context: Context, tagId: String? = null, messageId: Int = -1): Intent {
return Intent(context, NfcSetupActivity::class.java).apply {
putExtra(EXTRA_MESSAGE_ID, messageId)
@ -39,26 +54,37 @@ class NfcSetupActivity : BaseActivity() {
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)
intent.getStringExtra(EXTRA_TAG_VALUE)?.let {
simpleWrite = true
viewModel.nfcWriteTagEvent.postValue(it)
viewModel.writeNewTagSimple(it)
}
setContent {
MdcTheme {
LoadNfcView(
viewModel = viewModel,
startDestination = if (simpleWrite) NAV_WRITE else NAV_WELCOME,
pressedUpAtRoot = { finish() }
)
}
}
mNfcAdapter = NfcAdapter.getDefaultAdapter(this)
messageId = intent.getIntExtra(EXTRA_MESSAGE_ID, -1)
}
override fun onResume() {
super.onResume()
viewModel.checkNfcEnabled()
mNfcAdapter?.let {
NFCUtil.enableNFCInForeground(it, this, javaClass)
}
registerReceiver(
nfcStateChangedReceiver,
IntentFilter(NfcAdapter.ACTION_ADAPTER_STATE_CHANGED)
)
}
override fun onPause() {
@ -66,57 +92,47 @@ class NfcSetupActivity : BaseActivity() {
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)
unregisterReceiver(nfcStateChangedReceiver)
}
override fun onNewIntent(intent: Intent) {
super.onNewIntent(intent)
if (NfcAdapter.ACTION_TECH_DISCOVERED == intent.action) {
val nfcTagToWriteUUID = viewModel.nfcWriteTagEvent.value
if (intent.action == NfcAdapter.ACTION_TECH_DISCOVERED) {
lifecycleScope.launch {
val nfcTagToWriteUUID = viewModel.nfcTagIdentifier
// Create new nfc tag
if (nfcTagToWriteUUID == null) {
val rawMessages = intent.getParcelableArrayExtra(NfcAdapter.EXTRA_NDEF_MESSAGES)
val ndefMessage = rawMessages?.firstOrNull() as NdefMessage?
val url = ndefMessage?.records?.get(0)?.toUri().toString()
val nfcTagId = UrlHandler.splitNfcTagId(url)
if (nfcTagId == null) {
Log.w(TAG, "Unable to read tag!")
Toast.makeText(this, commonR.string.nfc_invalid_tag, Toast.LENGTH_LONG).show()
} 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 = commonR.string.nfc_write_tag_success
Toast.makeText(applicationContext, message, Toast.LENGTH_LONG).show()
viewModel.nfcReadEvent.value = nfcTagToWriteUUID
viewModel.nfcWriteTagDoneEvent.value = nfcTagToWriteUUID
// If we are a simple write it means the fontend asked us to write. This means
// we should return the user as fast as possible back to the UI to continue what
// they were doing!
if (simpleWrite) {
setResult(messageId)
finish()
// Create new nfc tag
if (!viewModel.nfcEventShouldWrite) {
val url = NFCUtil.extractUrlFromNFCIntent(intent)
val nfcTagId = UrlHandler.splitNfcTagId(url)
if (nfcTagId == null) {
viewModel.onNfcReadEmpty()
} else {
viewModel.onNfcReadSuccess(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")
// If we are a simple write it means the frontend asked us to write. This means
// we should return the user as fast as possible back to the UI to continue what
// they were doing!
if (simpleWrite) {
val message = commonR.string.nfc_write_tag_success
Toast.makeText(applicationContext, message, Toast.LENGTH_LONG).show()
setResult(messageId)
finish()
} else {
viewModel.onNfcWriteSuccess(nfcTagToWriteUUID!!)
}
} catch (e: Exception) {
viewModel.onNfcWriteFailure()
Log.e(TAG, "Unable to write tag.", e)
}
} catch (e: Exception) {
val message = commonR.string.nfc_write_tag_error
Toast.makeText(applicationContext, message, Toast.LENGTH_LONG).show()
Log.e(TAG, "Unable to write tag.", e)
}
}
}

View file

@ -1,27 +1,116 @@
package io.homeassistant.companion.android.nfc
import android.app.Application
import android.nfc.NfcAdapter
import android.util.Log
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.viewModelScope
import dagger.hilt.android.lifecycle.HiltViewModel
import io.homeassistant.companion.android.common.data.integration.IntegrationRepository
import io.homeassistant.companion.android.util.Navigator
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.asSharedFlow
import kotlinx.coroutines.launch
import java.util.UUID
import javax.inject.Inject
import io.homeassistant.companion.android.common.R as commonR
class NfcViewModel : ViewModel() {
@HiltViewModel
class NfcViewModel @Inject constructor(
private val integrationUseCase: IntegrationRepository,
application: Application
) : AndroidViewModel(application) {
// 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!")
companion object {
const val TAG = "NfcViewModel"
}
override fun onCleared() {
super.onCleared()
Log.i("NfcViewModel", "NfcViewModel destroyed!")
var isNfcEnabled by mutableStateOf(false)
private set
var nfcTagIdentifier by mutableStateOf<String?>(null)
private set
var nfcIdentifierIsEditable by mutableStateOf(true)
private set
var nfcEventShouldWrite = false
private set
val navigator = Navigator()
private val _nfcResultSnackbar = MutableSharedFlow<Int>()
var nfcResultSnackbar = _nfcResultSnackbar.asSharedFlow()
fun setDestination(destination: String?) {
nfcEventShouldWrite = nfcTagIdentifier != null && destination == NfcSetupActivity.NAV_WRITE
}
fun postNewUUID() {
nfcWriteTagEvent.postValue(UUID.randomUUID().toString())
fun checkNfcEnabled() {
isNfcEnabled = NfcAdapter.getDefaultAdapter(getApplication()).isEnabled
}
fun setTagIdentifier(value: String) {
if (nfcIdentifierIsEditable && value.trim().isNotEmpty()) nfcTagIdentifier = value
}
fun writeNewTagSimple(value: String) {
nfcTagIdentifier = value
nfcIdentifierIsEditable = false
// We don't need to perform navigation here because it will be set as the startDestination
}
fun writeNewTag() {
nfcTagIdentifier = UUID.randomUUID().toString()
nfcIdentifierIsEditable = true
navigator.navigateTo(NfcSetupActivity.NAV_WRITE)
}
fun onNfcReadSuccess(identifier: String) {
nfcTagIdentifier = identifier
navigator.navigateTo(
Navigator.NavigatorItem(
id = NfcSetupActivity.NAV_EDIT,
popBackstackTo = NfcSetupActivity.NAV_WELCOME
)
)
}
suspend fun onNfcReadEmpty() = _nfcResultSnackbar.emit(commonR.string.nfc_invalid_tag)
suspend fun onNfcWriteSuccess(identifier: String) {
_nfcResultSnackbar.emit(commonR.string.nfc_write_tag_success)
nfcTagIdentifier = identifier
navigator.navigateTo(
Navigator.NavigatorItem(
id = NfcSetupActivity.NAV_EDIT,
popBackstackTo = NfcSetupActivity.NAV_WELCOME
)
)
}
suspend fun onNfcWriteFailure() = _nfcResultSnackbar.emit(commonR.string.nfc_write_tag_error)
fun duplicateNfcTag() {
nfcIdentifierIsEditable = false
navigator.navigateTo(NfcSetupActivity.NAV_WRITE)
}
fun fireNfcTagEvent() {
viewModelScope.launch {
nfcTagIdentifier?.let {
try {
integrationUseCase.scanTag(
hashMapOf("tag_id" to it)
)
_nfcResultSnackbar.emit(commonR.string.nfc_event_fired_success)
} catch (e: Exception) {
Log.e(TAG, "Unable to send tag to Home Assistant.", e)
_nfcResultSnackbar.emit(commonR.string.nfc_event_fired_fail)
}
} ?: _nfcResultSnackbar.emit(commonR.string.nfc_event_fired_fail)
}
}
}

View file

@ -1,57 +0,0 @@
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.fragment.app.activityViewModels
import androidx.lifecycle.Observer
import androidx.navigation.fragment.findNavController
import io.homeassistant.companion.android.R
import io.homeassistant.companion.android.databinding.FragmentNfcWelcomeBinding
/**
* A simple [Fragment] subclass as the default destination in the navigation.
*/
class NfcWelcomeFragment : Fragment() {
private var _binding: FragmentNfcWelcomeBinding? = null
private val binding get() = _binding!!
private val viewModel: NfcViewModel by activityViewModels()
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
_binding = FragmentNfcWelcomeBinding.inflate(inflater, container, false)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
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)
binding.btnNfcRead.setOnClickListener {
findNavController().navigate(R.id.action_NFC_READ)
}
binding.btnNfcWrite.setOnClickListener {
viewModel.postNewUUID()
}
}
override fun onDestroyView() {
super.onDestroyView()
_binding = null
}
}

View file

@ -1,50 +0,0 @@
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.fragment.app.activityViewModels
import androidx.lifecycle.Observer
import androidx.navigation.fragment.findNavController
import io.homeassistant.companion.android.R
import io.homeassistant.companion.android.databinding.FragmentNfcWriteBinding
import io.homeassistant.companion.android.common.R as commonR
/**
* A simple [Fragment] subclass as the second destination in the navigation.
*/
class NfcWriteFragment : Fragment() {
private var _binding: FragmentNfcWriteBinding? = null
private val binding get() = _binding!!
private val viewModel: NfcViewModel by activityViewModels()
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
_binding = FragmentNfcWriteBinding.inflate(inflater, container, false)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
val nfcWriteTagObserver = Observer<String> {
binding.tvInstructionsWriteNfc.text = getString(commonR.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)
}
override fun onDestroyView() {
super.onDestroyView()
_binding = null
}
}

View file

@ -1,71 +0,0 @@
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) { 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

@ -1,20 +1,19 @@
package io.homeassistant.companion.android.nfc
import android.content.Intent
import android.nfc.NdefMessage
import android.net.Uri
import android.nfc.NfcAdapter
import android.os.Bundle
import android.util.Log
import android.widget.Toast
import androidx.activity.compose.setContent
import androidx.lifecycle.lifecycleScope
import com.google.android.material.composethemeadapter.MdcTheme
import dagger.hilt.android.AndroidEntryPoint
import io.homeassistant.companion.android.BaseActivity
import io.homeassistant.companion.android.R
import io.homeassistant.companion.android.common.data.integration.IntegrationRepository
import io.homeassistant.companion.android.nfc.views.TagReaderView
import io.homeassistant.companion.android.util.UrlHandler
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.cancel
import kotlinx.coroutines.launch
import javax.inject.Inject
import io.homeassistant.companion.android.common.R as commonR
@ -22,62 +21,58 @@ import io.homeassistant.companion.android.common.R as commonR
@AndroidEntryPoint
class TagReaderActivity : BaseActivity() {
val TAG = TagReaderActivity::class.simpleName
private val mainScope: CoroutineScope = CoroutineScope(Dispatchers.Main + Job())
companion object {
const val TAG = "TagReaderActivity"
}
@Inject
lateinit var integrationUseCase: IntegrationRepository
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_tag_reader)
setSupportActionBar(findViewById(R.id.toolbar))
setContent {
MdcTheme {
TagReaderView()
}
}
mainScope.launch {
if (NfcAdapter.ACTION_NDEF_DISCOVERED == intent.action) {
val rawMessages = intent.getParcelableArrayExtra(NfcAdapter.EXTRA_NDEF_MESSAGES)
val ndefMessage = rawMessages?.get(0) as NdefMessage?
val url = ndefMessage?.records?.get(0)?.toUri().toString()
lifecycleScope.launch {
if (intent.action == NfcAdapter.ACTION_NDEF_DISCOVERED || intent.action == Intent.ACTION_VIEW) {
val isNfcTag = intent.action == NfcAdapter.ACTION_NDEF_DISCOVERED
val url =
if (isNfcTag) NFCUtil.extractUrlFromNFCIntent(intent)
else intent.data
try {
handleTag(url)
handleTag(url, isNfcTag)
} catch (e: Exception) {
val message = commonR.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 = commonR.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()
showProcessingError(isNfcTag)
Log.e(TAG, "Unable to handle url (${if (isNfcTag) "nfc" else "qr"}}): $url", e)
}
}
finish()
}
}
override fun onDestroy() {
mainScope.cancel()
super.onDestroy()
}
private suspend fun handleTag(url: String) {
private suspend fun handleTag(url: Uri?, isNfcTag: Boolean) {
// https://www.home-assistant.io/tag/5f0ba733-172f-430d-a7f8-e4ad940c88d7
val nfcTagId = UrlHandler.splitNfcTagId(url)
Log.d(TAG, "nfcTagId: $nfcTagId")
Log.d(TAG, "Tag ID: $nfcTagId")
if (nfcTagId != null) {
integrationUseCase.scanTag(hashMapOf("tag_id" to nfcTagId))
Log.d(TAG, "Tag scanned to HA successfully")
} else {
Toast.makeText(this, commonR.string.nfc_processing_tag_error, Toast.LENGTH_LONG).show()
showProcessingError(isNfcTag)
}
finish()
}
private fun showProcessingError(isNfcTag: Boolean) {
Toast.makeText(
this,
if (isNfcTag) commonR.string.nfc_processing_tag_error else commonR.string.qrcode_processing_tag_error,
Toast.LENGTH_LONG
).show()
}
}

View file

@ -0,0 +1,147 @@
package io.homeassistant.companion.android.nfc.views
import android.annotation.SuppressLint
import android.content.Intent
import android.provider.Settings
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.text.selection.SelectionContainer
import androidx.compose.material.Button
import androidx.compose.material.Icon
import androidx.compose.material.IconButton
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Surface
import androidx.compose.material.Text
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Share
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.colorResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.unit.dp
import io.homeassistant.companion.android.common.R as commonR
@SuppressLint("HardwareIds")
@Composable
fun NfcEditView(
identifier: String?,
onDuplicateClicked: () -> Unit,
onFireEventClicked: () -> Unit
) {
val context = LocalContext.current
LazyColumn(contentPadding = PaddingValues(all = 16.dp)) {
item {
Text(
text = stringResource(commonR.string.nfc_tag_identifier),
modifier = Modifier.padding(bottom = 8.dp)
)
}
item {
NfcCodeContainer(text = identifier ?: "")
}
item {
Row(modifier = Modifier.padding(top = 8.dp)) {
Button(
modifier = Modifier
.padding(end = 8.dp)
.weight(1f),
onClick = onDuplicateClicked
) {
Text(stringResource(commonR.string.nfc_btn_create_duplicate))
}
Button(
modifier = Modifier
.padding(start = 8.dp)
.weight(1f),
onClick = onFireEventClicked
) {
Text(stringResource(commonR.string.nfc_btn_fire_event))
}
}
}
val deviceId = Settings.Secure.getString(context.contentResolver, Settings.Secure.ANDROID_ID)
val tagTriggerExample = "- platform: tag\n tag_id: $identifier"
val deviceTriggerExample = "- platform: tag\n tag_id: $identifier\n device_id: $deviceId"
item {
Text(
text = stringResource(commonR.string.nfc_trigger_summary),
modifier = Modifier.padding(top = 48.dp, bottom = 8.dp)
)
}
item {
NfcTriggerExample(
modifier = Modifier.padding(bottom = 8.dp),
description = stringResource(commonR.string.nfc_trigger_any),
example = tagTriggerExample
)
}
item {
NfcTriggerExample(
description = stringResource(commonR.string.nfc_trigger_device),
example = deviceTriggerExample
)
}
}
}
@Composable
fun NfcCodeContainer(
text: String
) {
Surface(
shape = MaterialTheme.shapes.medium,
color = colorResource(commonR.color.colorCodeBackground),
modifier = Modifier.fillMaxWidth()
) {
SelectionContainer {
Text(
text = text,
fontFamily = FontFamily.Monospace,
modifier = Modifier.padding(horizontal = 4.dp, vertical = 8.dp)
)
}
}
}
@Composable
fun NfcTriggerExample(
modifier: Modifier = Modifier,
description: String,
example: String
) {
val context = LocalContext.current
Column(modifier = modifier) {
Row(verticalAlignment = Alignment.CenterVertically) {
Text(
text = description,
modifier = Modifier.weight(1f)
)
IconButton(
modifier = Modifier.padding(all = 8.dp),
onClick = {
val sendIntent: Intent = Intent().apply {
action = Intent.ACTION_SEND
putExtra(Intent.EXTRA_TEXT, example)
type = "text/plain"
}
val shareIntent = Intent.createChooser(sendIntent, null)
context.startActivity(shareIntent)
}
) {
Icon(
imageVector = Icons.Default.Share,
contentDescription = stringResource(commonR.string.nfc_btn_share)
)
}
}
NfcCodeContainer(text = example)
}
}

View file

@ -0,0 +1,130 @@
package io.homeassistant.companion.android.nfc.views
import android.content.Intent
import android.net.Uri
import androidx.compose.material.Icon
import androidx.compose.material.IconButton
import androidx.compose.material.Scaffold
import androidx.compose.material.Text
import androidx.compose.material.TopAppBar
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.outlined.ArrowBack
import androidx.compose.material.icons.outlined.HelpOutline
import androidx.compose.material.rememberScaffoldState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.colorResource
import androidx.compose.ui.res.stringResource
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.compose.rememberNavController
import io.homeassistant.companion.android.nfc.NfcSetupActivity
import io.homeassistant.companion.android.nfc.NfcViewModel
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import io.homeassistant.companion.android.common.R as commonR
@Composable
fun LoadNfcView(
viewModel: NfcViewModel,
startDestination: String,
pressedUpAtRoot: () -> Unit
) {
val context = LocalContext.current
val navController = rememberNavController()
val canNavigateUp = remember { mutableStateOf(false) }
navController.addOnDestinationChangedListener { controller, destination, _ ->
canNavigateUp.value = controller.previousBackStackEntry != null
viewModel.setDestination(destination.route)
}
LaunchedEffect("navigation") {
viewModel.navigator.flow.onEach {
navController.navigate(it.id) {
if (it.popBackstackTo != null) {
popUpTo(it.popBackstackTo) { inclusive = it.popBackstackInclusive }
}
}
}.launchIn(this)
}
val scaffoldState = rememberScaffoldState()
LaunchedEffect("snackbar") {
viewModel.nfcResultSnackbar.onEach {
if (it != 0) {
scaffoldState.snackbarHostState.showSnackbar(context.getString(it))
}
}.launchIn(this)
}
Scaffold(
scaffoldState = scaffoldState,
topBar = {
TopAppBar(
title = { Text(stringResource(commonR.string.nfc_title_settings)) },
navigationIcon = {
IconButton(onClick = {
if (canNavigateUp.value) navController.navigateUp()
else pressedUpAtRoot()
}) {
Icon(
imageVector = Icons.Outlined.ArrowBack,
contentDescription = stringResource(commonR.string.navigate_up)
)
}
},
actions = {
IconButton(onClick = {
val intent = Intent(Intent.ACTION_VIEW, Uri.parse("https://companion.home-assistant.io/docs/integrations/universal-links"))
context.startActivity(intent)
}) {
Icon(
imageVector = Icons.Outlined.HelpOutline,
contentDescription = stringResource(commonR.string.get_help),
tint = colorResource(commonR.color.colorOnBackground)
)
}
},
backgroundColor = colorResource(commonR.color.colorBackground),
contentColor = colorResource(commonR.color.colorOnBackground)
)
}
) {
NavHost(
navController = navController,
startDestination = startDestination,
modifier = Modifier
) {
composable(NfcSetupActivity.NAV_WELCOME) {
NfcWelcomeView(
isNfcEnabled = viewModel.isNfcEnabled,
onReadClicked = { viewModel.navigator.navigateTo(NfcSetupActivity.NAV_READ) },
onWriteClicked = { viewModel.writeNewTag() }
)
}
composable(NfcSetupActivity.NAV_READ) {
NfcReadView()
}
composable(NfcSetupActivity.NAV_WRITE) {
NfcWriteView(
isNfcEnabled = viewModel.isNfcEnabled,
identifier = viewModel.nfcTagIdentifier,
onSetIdentifier = if (viewModel.nfcIdentifierIsEditable) {
{ viewModel.setTagIdentifier(it) }
} else null
)
}
composable(NfcSetupActivity.NAV_EDIT) {
NfcEditView(
identifier = viewModel.nfcTagIdentifier,
onDuplicateClicked = { viewModel.duplicateNfcTag() },
onFireEventClicked = { viewModel.fireNfcTagEvent() }
)
}
}
}
}

View file

@ -0,0 +1,41 @@
package io.homeassistant.companion.android.nfc.views
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.material.Icon
import androidx.compose.material.Text
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Nfc
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import io.homeassistant.companion.android.common.R as commonR
@Composable
fun NfcReadView() {
Column(
modifier = Modifier
.fillMaxSize()
.padding(all = 16.dp),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
) {
Icon(
imageVector = Icons.Filled.Nfc,
contentDescription = null
)
Text(
text = stringResource(commonR.string.nfc_read_tag_instructions),
textAlign = TextAlign.Center,
modifier = Modifier
.fillMaxWidth(0.75f)
.padding(top = 16.dp)
)
}
}

View file

@ -0,0 +1,72 @@
package io.homeassistant.companion.android.nfc.views
import android.content.Intent
import android.provider.Settings
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.material.Button
import androidx.compose.material.Text
import androidx.compose.material.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import io.homeassistant.companion.android.common.R as commonR
@Composable
fun NfcWelcomeView(
isNfcEnabled: Boolean,
onReadClicked: () -> Unit,
onWriteClicked: () -> Unit,
) {
LazyColumn(contentPadding = PaddingValues(all = 16.dp)) {
item {
Text(stringResource(commonR.string.nfc_welcome_message))
}
item {
Row(modifier = Modifier.padding(top = 16.dp)) {
Button(
modifier = Modifier
.padding(end = 8.dp)
.weight(1f),
enabled = isNfcEnabled,
onClick = onReadClicked
) {
Text(stringResource(commonR.string.nfc_btn_read_tag))
}
Button(
modifier = Modifier
.padding(start = 8.dp)
.weight(1f),
enabled = isNfcEnabled,
onClick = onWriteClicked
) {
Text(stringResource(commonR.string.nfc_btn_write_tag))
}
}
}
if (!isNfcEnabled) {
item {
Text(
text = stringResource(commonR.string.nfc_welcome_turnon),
modifier = Modifier.padding(top = 48.dp)
)
}
item {
val context = LocalContext.current
TextButton(
contentPadding = PaddingValues(horizontal = 0.dp),
onClick = {
context.startActivity(Intent(Settings.ACTION_NFC_SETTINGS))
}
) {
Text(stringResource(commonR.string.settings))
}
}
}
}
}

View file

@ -0,0 +1,108 @@
package io.homeassistant.companion.android.nfc.views
import android.content.Intent
import android.provider.Settings
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material.MaterialTheme
import androidx.compose.material.OutlinedTextField
import androidx.compose.material.Text
import androidx.compose.material.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.ColorFilter
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import com.mikepenz.iconics.compose.Image
import com.mikepenz.iconics.typeface.library.community.material.CommunityMaterial
import io.homeassistant.companion.android.util.compose.MdcAlertDialog
import io.homeassistant.companion.android.common.R as commonR
@Composable
fun NfcWriteView(
isNfcEnabled: Boolean,
identifier: String?,
onSetIdentifier: ((String) -> Unit)? = null
) {
var identifierDialog by remember { mutableStateOf(false) }
if (identifierDialog && onSetIdentifier != null) {
NfcWriteIdentifierDialog(
onCancel = { identifierDialog = false },
onSubmit = {
onSetIdentifier(it)
identifierDialog = false
}
)
}
Column(
modifier = Modifier
.fillMaxSize()
.padding(all = 16.dp),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
) {
Image(
asset = CommunityMaterial.Icon3.cmd_nfc_tap,
colorFilter = ColorFilter.tint(MaterialTheme.colors.onSurface),
)
Text(
text =
if (isNfcEnabled) stringResource(commonR.string.nfc_write_tag_instructions, identifier ?: "")
else stringResource(commonR.string.nfc_write_tag_turnon),
textAlign = TextAlign.Center,
modifier = Modifier
.fillMaxWidth(0.75f)
.padding(top = 16.dp)
)
if (!isNfcEnabled) {
val context = LocalContext.current
TextButton(onClick = {
context.startActivity(Intent(Settings.ACTION_NFC_SETTINGS))
}) {
Text(stringResource(commonR.string.settings))
}
} else if (onSetIdentifier != null) {
TextButton(onClick = { identifierDialog = true }) {
Text(stringResource(commonR.string.nfc_write_tag_change))
}
}
}
}
@Composable
fun NfcWriteIdentifierDialog(
onCancel: () -> Unit,
onSubmit: (String) -> Unit
) {
val inputValue = remember { mutableStateOf("") }
MdcAlertDialog(
onDismissRequest = onCancel,
title = { Text(stringResource(commonR.string.nfc_write_tag_enter_identifier)) },
content = {
OutlinedTextField(
value = inputValue.value,
keyboardOptions = KeyboardOptions(
imeAction = ImeAction.Done
),
onValueChange = { input -> inputValue.value = input }
)
},
onCancel = onCancel,
onSave = { onSubmit(inputValue.value) }
)
}

View file

@ -0,0 +1,36 @@
package io.homeassistant.companion.android.nfc.views
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.material.CircularProgressIndicator
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import io.homeassistant.companion.android.common.R
@Composable
fun TagReaderView() {
Column(
modifier = Modifier
.fillMaxSize()
.padding(all = 16.dp),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
) {
CircularProgressIndicator()
Text(
text = stringResource(R.string.tag_reader_title),
textAlign = TextAlign.Center,
modifier = Modifier
.fillMaxWidth(0.75f)
.padding(top = 16.dp)
)
}
}

View file

@ -0,0 +1,23 @@
package io.homeassistant.companion.android.util
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.asSharedFlow
class Navigator {
private val _sharedFlow = MutableSharedFlow<NavigatorItem>(extraBufferCapacity = 1)
val flow = _sharedFlow.asSharedFlow()
fun navigateTo(navTarget: String) {
_sharedFlow.tryEmit(NavigatorItem(navTarget))
}
fun navigateTo(navItem: NavigatorItem) {
_sharedFlow.tryEmit(navItem)
}
data class NavigatorItem(
val id: String,
val popBackstackTo: String? = null,
val popBackstackInclusive: Boolean = false
)
}

View file

@ -1,5 +1,6 @@
package io.homeassistant.companion.android.util
import android.net.Uri
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
import java.net.URL
@ -22,7 +23,7 @@ object UrlHandler {
return Regex("^https?://").containsMatchIn(it.toString())
}
fun splitNfcTagId(it: String?): String? {
fun splitNfcTagId(it: Uri?): String? {
val matches =
Regex("^https?://www\\.home-assistant\\.io/tag/(.*)").find(
it.toString()

View file

@ -1,23 +0,0 @@
<?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"
android:id="@+id/nfc_view"
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:theme="@style/ThemeOverlay.HomeAssistant.ActionBar" />
</com.google.android.material.appbar.AppBarLayout>
<include layout="@layout/content_nfc_setup" />
</androidx.coordinatorlayout.widget.CoordinatorLayout>

View file

@ -1,36 +0,0 @@
<?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

@ -1,17 +0,0 @@
<?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

@ -1,83 +0,0 @@
<?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

@ -1,19 +0,0 @@
<?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

@ -1,46 +0,0 @@
<?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

@ -1,20 +0,0 @@
<?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

@ -1,55 +0,0 @@
<?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

@ -21,4 +21,5 @@
<color name="colorWidgetButtonLabel">#E6E6E6</color>
<color name="colorActionBarPopupBackground">#2B2B2B</color>
<color name="colorLaunchScreenBackground">#111111</color>
<color name="colorCodeBackground">#282828</color>
</resources>

View file

@ -22,4 +22,5 @@
<color name="colorWidgetButtonLabel">#3A3A3A</color>
<color name="colorActionBarPopupBackground">@android:color/white</color>
<color name="colorLaunchScreenBackground">#fafafa</color>
<color name="colorCodeBackground">#f5f5f5</color>
</resources>

View file

@ -309,30 +309,32 @@
<string name="areas">Areas</string>
<string name="more_entities">More entities</string>
<string name="need_help">Need Help?</string>
<string name="navigate_up">Navigate up</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_share">Share</string>
<string name="nfc_btn_write_tag">Write NFC Tag</string>
<string name="nfc_edit_fragment_label">NFC Edit Fragment</string>
<string name="nfc_event_fired_fail">Firing event failed. Please try again.</string>
<string name="nfc_event_fired_fail">Firing event failed, please try again</string>
<string name="nfc_event_fired_success">Event fired</string>
<string name="nfc_example_trigger">Example Trigger</string>
<string name="nfc_trigger_summary">Use this tag as a trigger for an automation. Select the \'Tag\' trigger type and choose the identifier in the visual editor, or use the following YAML code:</string>
<string name="nfc_trigger_any">Scanned by any device:</string>
<string name="nfc_trigger_device">Scanned by this device:</string>
<string name="nfc_invalid_tag">This tag does not contain Home Assistant data</string>
<string name="nfc_processing_tag_error">Error while processing nfc tag</string>
<string name="nfc_read_fragment_label">NFC Read Fragment</string>
<string name="nfc_read_tag_instructions">Place your phone over the nfc tag to read it.</string>
<string name="nfc_processing_tag_error">Error while processing NFC tag</string>
<string name="nfc_read_tag_instructions">Hold your device near an NFC tag</string>
<string name="nfc_summary">Setup NFC Tags</string>
<string name="nfc_tag_identifier">Tag Identifier</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_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="nfc_write_fragment_label">NFC Write Fragment</string>
<string name="nfc_write_tag_error">Error while writing nfc tag</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_success">Successfully wrote nfc tag</string>
<string name="nfc_write_tag_too_early">Please fill out the form first</string>
<string name="nfc_welcome_turnon">NFC is turned off. To get started, please turn on NFC in your device\'s settings.</string>
<string name="nfc_write_tag_error">Error while writing NFC tag</string>
<string name="nfc_write_tag_instructions">Hold your device near an NFC tag to write the following identifier to it:\n\n%1$s</string>
<string name="nfc_write_tag_change">Change identifier</string>
<string name="nfc_write_tag_enter_identifier">Tag identifier</string>
<string name="nfc_write_tag_turnon">Please turn on NFC to enable writing to an NFC tag</string>
<string name="nfc_write_tag_success">Successfully wrote NFC tag</string>
<string name="no_notifications_summary">You have not received any notifications yet</string>
<string name="no_notifications">No Notifications</string>
<string name="no_supported_entities">No Supported Entities</string>
@ -374,7 +376,7 @@
<string name="privacy_policy">Privacy Policy</string>
<string name="privacy_url">https://www.home-assistant.io/privacy</string>
<string name="profile">Profile</string>
<string name="qrcode_processing_tag_error">Error while processing qrcode tag</string>
<string name="qrcode_processing_tag_error">Error while processing QR code tag</string>
<string name="quick_settings">Quick Settings</string>
<string name="rate_limit_notification.body">You have now sent more than %s notifications today. You will not receive new notifications until midnight UTC.</string>
<string name="rate_limit_notification.title">Notifications Rate Limited</string>