Android NsdManager not able to discover services - android

I'm running into a problem with Androids NsdManager when following their tutorial Using Network Service Discovery.
I have a few zeroconf/bonjour hardware devices on my network. From my mac I can discover all of them as expected from my terminal with the following
dns-sd -Z _my-mesh._tcp.
From my Android app's first run I can flawlessly discover these services using NsdManager. However if I restart the application and try again none of the services are found. onDiscoveryStarted gets called successfully but then nothing else after. While waiting I can confirm from my mac that the services are still successfully there.
I can then turn on my Zeroconf app (on Android) and it will show the services like my mac. When I return to my app I see it immediately receive all the callbacks I expected previously. So I believe something is wrong with my approach, however I'm not sure what. Below is the code I use to discover and resolve services. The view is a giant textview (in a scroll view) I keep writing text to for debugging easier.
import android.annotation.SuppressLint
import android.content.Context
import android.net.nsd.NsdManager
import android.net.nsd.NsdServiceInfo
import android.support.v7.app.AppCompatActivity
import android.os.Bundle
import android.widget.TextView
class MainActivity : AppCompatActivity(),
NsdManager.DiscoveryListener {
private var nsdManager: NsdManager? = null
private var text: TextView? = null
private var isResolving = false
private val services = ArrayList<ServiceWrapper>()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
this.text = findViewById(R.id.text)
this.nsdManager = application.getSystemService(Context.NSD_SERVICE) as NsdManager
}
override fun onResume() {
super.onResume()
this.nsdManager?.discoverServices("_my-mesh._tcp.", NsdManager.PROTOCOL_DNS_SD, this)
write("Resume Discovering Services")
}
override fun onPause() {
super.onPause()
this.nsdManager?.stopServiceDiscovery(this)
write("Pause Discovering Services")
}
override fun onServiceFound(serviceInfo: NsdServiceInfo?) {
write("onServiceFound(serviceInfo = $serviceInfo))")
if (serviceInfo == null) {
return
}
add(serviceInfo)
}
override fun onStopDiscoveryFailed(serviceType: String?, errorCode: Int) {
write("onStopDiscoveryFailed(serviceType = $serviceType, errorCode = $errorCode)")
}
override fun onStartDiscoveryFailed(serviceType: String?, errorCode: Int) {
write("onStartDiscoveryFailed(serviceType = $serviceType, errorCode = $errorCode)")
}
override fun onDiscoveryStarted(serviceType: String?) {
write("onDiscoveryStarted(serviceType = $serviceType)")
}
override fun onDiscoveryStopped(serviceType: String?) {
write("onDiscoveryStopped(serviceType = $serviceType)")
}
override fun onServiceLost(serviceInfo: NsdServiceInfo?) {
write("onServiceLost(serviceInfo = $serviceInfo)")
}
private fun createResolveListener(): NsdManager.ResolveListener {
return object : NsdManager.ResolveListener {
override fun onResolveFailed(serviceInfo: NsdServiceInfo?, errorCode: Int) {
write("onResolveFailed(serviceInfo = $serviceInfo, errorCode = $errorCode)")
isResolving = false
resolveNext()
}
override fun onServiceResolved(serviceInfo: NsdServiceInfo?) {
write("onServiceResolved(serviceInfo = $serviceInfo)")
if (serviceInfo == null) {
return
}
for (servicewrapper in services) {
if (servicewrapper.serviceInfo.serviceName == serviceInfo.serviceName) {
servicewrapper.resolve(serviceInfo)
}
}
isResolving = false
resolveNext()
}
}
}
#SuppressLint("SetTextI18n")
private fun write(text: String?) {
this.text?.let {
it.post({
it.text = it.text.toString() + "\n" + text + "\n"
})
}
}
fun add(serviceInfo: NsdServiceInfo) {
for (servicewrapper in services) {
if (servicewrapper.serviceInfo.serviceName == serviceInfo.serviceName) {
return
}
}
services.add(ServiceWrapper(serviceInfo))
resolveNext()
}
#Synchronized
fun resolveNext() {
if (isResolving) {
return
}
isResolving = true
for (servicewrapper in services) {
if (servicewrapper.isResolved) {
continue
}
write("resolving")
this.nsdManager?.resolveService(servicewrapper.serviceInfo, createResolveListener())
return
}
isResolving = false
}
inner class ServiceWrapper(var serviceInfo: NsdServiceInfo) {
var isResolved = false
fun resolve(serviceInfo: NsdServiceInfo) {
isResolved = true
this.serviceInfo = serviceInfo
}
}
}

Better late than never. Did not realize other people were having this issue too until now.
What we discovered was some routers were blocking or not correctly forwarding the packets back and forth. Our solution to this was using wire shark to detect what other popular apps were doing to get around the issue. Androids NsdManager has limited customizability so it required manually transmitting the packet over a MulticastSocket.
interface NsdDiscovery {
suspend fun startDiscovery()
suspend fun stopDiscovery()
fun setListener(listener: Listener?)
fun isDiscovering(): Boolean
interface Listener {
fun onServiceFound(ip:String, local:String)
fun onServiceLost(event: ServiceEvent)
}
}
#Singleton
class ManualNsdDiscovery #Inject constructor()
: NsdDiscovery {
//region Fields
private val isDiscovering = AtomicBoolean(false)
private var socketManager: SocketManager? = null
private var listener: WeakReference<NsdDiscovery.Listener> = WeakReference<NsdDiscovery.Listener>(null)
//endregion
//region NsdDiscovery
override suspend fun startDiscovery() = withContext(Dispatchers.IO) {
if (isDiscovering()) return#withContext
this#ManualNsdDiscovery.isDiscovering.set(true)
val socketManager = SocketManager()
socketManager.start()
this#ManualNsdDiscovery.socketManager = socketManager
}
override suspend fun stopDiscovery() = withContext(Dispatchers.IO) {
if (!isDiscovering()) return#withContext
this#ManualNsdDiscovery.socketManager?.stop()
this#ManualNsdDiscovery.socketManager = null
this#ManualNsdDiscovery.isDiscovering.set(false)
}
override fun setListener(listener: NsdDiscovery.Listener?) {
this.listener = WeakReference<NsdDiscovery.Listener>(listener)
}
#Synchronized
override fun isDiscovering(): Boolean {
return this.isDiscovering.get()
}
//endregion
private inner class SocketManager {
//region Fields
private val group = InetAddress.getByName("224.0.0.251")
?: throw IllegalStateException("Can't setup group")
private val incomingNsd = IncomingNsd()
private val outgoingNsd = OutgoingNsd()
//endregion
//region Constructors
//endregion
//region Methods
suspend fun start() {
this.incomingNsd.startListening()
this.outgoingNsd.send()
}
fun stop() {
this.incomingNsd.stopListening()
}
//endregion
private inner class OutgoingNsd {
//region Fields
private val socketMutex = Mutex()
private var socket = MulticastSocket(5353)
suspend fun setUpSocket() {
this.socketMutex.withLock {
try {
this.socket = MulticastSocket(5353)
this.socket.reuseAddress = true
this.socket.joinGroup(group)
} catch (e: SocketException) {
return
}
}
}
suspend fun tearDownSocket() {
this.socketMutex.withLock {
this#OutgoingNsd.socket.close()
}
}
//ugly code but here is the packet
private val bytes = byteArrayOf(171.toByte(), 205.toByte(), 1.toByte(), 32.toByte(),
0.toByte(), 1.toByte(), 0.toByte(), 0.toByte(),
0.toByte(), 0.toByte(), 0.toByte(), 0.toByte(),
9.toByte(), 95.toByte(), 101.toByte(), 118.toByte(),
97.toByte(), 45.toByte(), 109.toByte(), 101.toByte(),
115.toByte(), 104.toByte(), 4.toByte(), 95.toByte(),
116.toByte(), 99.toByte(), 112.toByte(), 5.toByte(),
108.toByte(), 111.toByte(), 99.toByte(), 97.toByte(),
108.toByte(), 0.toByte(), 0.toByte(), 12.toByte(),
0.toByte(), 1.toByte())
private val outPacket = DatagramPacket(bytes,
bytes.size,
this#SocketManager.group,
5353)
//endregion
//region Methods
#Synchronized
suspend fun send() {
withContext(Dispatchers.Default) {
setUpSocket()
try {
this#OutgoingNsd.socket.send(this#OutgoingNsd.outPacket)
delay(1500L)
tearDownSocket()
} catch (e: Exception) {
}
}
}
//endregion
}
private inner class IncomingNsd {
//region Fields
private val isRunning = AtomicBoolean(false)
private var socket = MulticastSocket(5353)
//endregion
//region Any
fun setUpSocket() {
try {
this.socket = MulticastSocket(5353)
this.socket.reuseAddress = true
this.socket.joinGroup(group)
} catch (e: SocketException) {
} catch (e: BindException) {
}
}
fun run() {
GlobalScope.launch(Dispatchers.Default) {
setUpSocket()
try {
while (this#IncomingNsd.isRunning.get()) {
val bytes = ByteArray(4096)
val inPacket = DatagramPacket(bytes, bytes.size)
this#IncomingNsd.socket.receive(inPacket)
val incoming = DNSIncoming(inPacket)
for (answer in incoming.allAnswers) {
if (answer.key.contains("_my_mesh._tcp")) {
this#ManualNsdDiscovery.listener.get()?.onServiceFound(answer.recordSource.hostAddress, answer.name)
return#launch
}
}
}
this#IncomingNsd.socket.close()
} catch (e: Exception) {
}
}
}
//endregion
//region Methods
#Synchronized
fun startListening() {
if (this.isRunning.get()) {
return
}
this.isRunning.set(true)
run()
}
#Synchronized
fun stopListening() {
if (!this.isRunning.get()) {
return
}
this.isRunning.set(false)
}
//endregion
}
}
}

Related

How to make a check for entering the correct city when entering

I have a way where when I enter the name of the city, I substitute the name of the city in the api, and everything works fine, but if I enter a non-real city, then nonsense is put in the api accordingly and the application crashes. I would like to make it so that if the city does not exist, that is, I output an error, then Toast "enter the correct name of the city" appears. Thank you in advance
Код:
const val USE_DEVICE_LOCATION = "USE_DEVICE_LOCATION"
const val CUSTOM_LOCATION = "CUSTOM_LOCATION"
class LocationProviderImpl(
private val fusedLocationProviderClient: FusedLocationProviderClient,
context: Context) : PreferenceProvider(context), LocationProvider {
private val appContext = context.applicationContext
override suspend fun hasLocationChanged(lastWeatherLocation: WeatherLocation): Boolean {
val deviceLocationChanged = try {
hasDeviceLocationChanged(lastWeatherLocation)
} catch (e: LocationPermissionNotGrantedException) {
false
}
return deviceLocationChanged || hasCustomLocationChanged(lastWeatherLocation)
}
override suspend fun getPreferredLocationString(): String {
if(isUsingDeviceLocation()) {
try {
val deviceLocation = getLastDeviceLocation()
?: return "${getCustomLocationName()}"
return "${deviceLocation.latitude},${deviceLocation.longitude}"
} catch (e: LocationPermissionNotGrantedException) {
return "${getCustomLocationName()}"
}
}
else {
return "${getCustomLocationName()}"
}
}
private suspend fun hasDeviceLocationChanged(lastWeatherLocation: WeatherLocation): Boolean {
if (!isUsingDeviceLocation())
return false
val deviceLocation = getLastDeviceLocation()
?: return false
// Comparing doubles cannot be done with "=="
val comparisonThreshold = 0.03
return Math.abs(deviceLocation.latitude - lastWeatherLocation.lat) > comparisonThreshold &&
Math.abs(deviceLocation.longitude - lastWeatherLocation.lon) > comparisonThreshold
}
private fun hasCustomLocationChanged(lastWeatherLocation: WeatherLocation): Boolean {
if (!isUsingDeviceLocation()) {
val customLocationName = getCustomLocationName()
return customLocationName != lastWeatherLocation.name
}
return false
}
private fun getCustomLocationName(): String? {
try {
return preferences.getString(CUSTOM_LOCATION, null)
}
catch (e: Exception) {
isUsingDeviceLocation()
}
throw LocationPermissionNotGrantedException()
}
private fun isUsingDeviceLocation(): Boolean {
return preferences.getBoolean(USE_DEVICE_LOCATION, true)
}
#SuppressLint("MissingPermission")
private suspend fun getLastDeviceLocation(): Location? {
return (if (hasLocationPermission())
fusedLocationProviderClient.lastLocation.asDeferred()
else {
throw LocationPermissionNotGrantedException()
}).await()
}
private fun hasLocationPermission(): Boolean {
return ContextCompat.checkSelfPermission(appContext,
android.Manifest.permission.ACCESS_COARSE_LOCATION) == PackageManager.PERMISSION_GRANTED
}
}
CurrentWeatherFragment
class CurrentWeatherFragment : ScopedFragment(), KodeinAware, SharedPreferences.OnSharedPreferenceChangeListener {
override val kodein by closestKodein()
private val viewModelFactory: CurrentWeatherViewModelFactory by instance()
private lateinit var viewModel: CurrentWeatherViewModel
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
return inflater.inflate(R.layout.current_weather_fragment, container, false)
}
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
loadBannerAd()
viewModel = ViewModelProvider(this, viewModelFactory)
.get(CurrentWeatherViewModel::class.java)
bindUI()
applyDarkModeSetting()
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
}
private fun loadBannerAd() {
MobileAds.initialize(context) {}
mAdViewCurrent = adView
val adRequest = AdRequest.Builder().build()
mAdViewCurrent.loadAd(adRequest)
mAdViewCurrent.adListener = object: AdListener() {
override fun onAdLoaded() {
}
override fun onAdFailedToLoad(adError : LoadAdError) {
// Code to be executed when an ad request fails.
}
override fun onAdOpened() {
// Code to be executed when an ad opens an overlay that
// covers the screen.
}
override fun onAdClicked() {
// Code to be executed when the user clicks on an ad.
}
override fun onAdClosed() {
}
}
}
override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences?, key: String?) {
if (key == "GENDER") {
val prefs = sharedPreferences?.getString(key, "1")
when(prefs?.toInt()) {
1->{
bindUI()
}
2->{
bindUIWomen()
}
}
}
applyDarkModeSetting()
}
override fun onDestroy() {
super.onDestroy()
context?.let {
PreferenceManager.getDefaultSharedPreferences(it)
.registerOnSharedPreferenceChangeListener(this)
}
}
private fun applyDarkModeSetting() {
val sharedPreferences = context?.let { PreferenceManager.getDefaultSharedPreferences(it) }
val settingValue = sharedPreferences?.getString("GENDER", null)?.toIntOrNull() ?: 1
val mode = when (settingValue) {
1 -> {bindUI()}
2 -> {bindUIWomen()}
else -> Toast.makeText(context, "Nothing", Toast.LENGTH_LONG).show()
}
}
private fun bindUI() = launch(Dispatchers.Main) {
val currentWeather = viewModel.weather.await()
val weatherLocation = viewModel.weatherLocation.await()
val CurrentWeatherEntries = viewModel.weather.await()
CurrentWeatherEntries.observe(viewLifecycleOwner, Observer { weatherAll ->
if (weatherAll == null){
return#Observer
}
recyclerViewClothes.apply {
layoutManager = LinearLayoutManager(activity)
adapter = ClothesAdapter(weatherAll)
}
})
weatherLocation.observe(viewLifecycleOwner, Observer { location ->
if (location == null) return#Observer
updateLocation(location.name)
updateCityName(location.name)
})
currentWeather.observe(viewLifecycleOwner, Observer {
if(it == null) return#Observer
group_loading.visibility = View.GONE
weatherAll.visibility = View.VISIBLE
updateDateToday()
//updateIsDay(it.isDay)
updateHumidity(it.humidity)
updateTemperature(it.temperature, it.feelsLikeTemperature)
updateCondition(it.conditionText)
updatePressure(it.pressure)
updateWind(it.windSpeed)
updateCloudy(it.cloud)
GlideApp.with(this#CurrentWeatherFragment)
.load("https:${it.conditionIconUrl}")
.into(imageView_condition_icon)
})
refreshApp()
}
private fun List<UnitSpecificCurrentWeatherEntry>.toCurrentWeatherItems() : List<ClothesAdapter> {
return this.map {
ClothesAdapter(it) //ClothesAdapter
}
}
// private fun List<UnitSpecificCurrentWeatherEntry>.toCurrentWeatherItems() : List<ClothesAdapterWomen> {
// return this.map {
// ClothesAdapterWomen(it) //ClothesAdapter
// }
// }
private fun chooseLocalizationUnitAbbreviation(metric: String, imperial: String): String {
return if(viewModel.isMetricUnit) metric else imperial
}
private fun updateLocation(location: String) {
(activity as? AppCompatActivity)?.supportActionBar?.subtitle = getString(R.string.todayTitle)
}
private fun updateDateToday() {
(activity as? AppCompatActivity)?.supportActionBar?.subtitle = getString(R.string.todayTitle)
}
private fun updateCityName(cityName: String) {
textView_cityName.text = cityName
}
}

Android Espresso Testing: java.lang.NullPointerException: Can't toast on a thread that has not called Looper.prepare()

I am almost new to android testing and following the official docs and Udacity course for learning purposes.
Coming to the issue I want to check when the task is completed or incompleted to be displayed properly or not, for this I wrote a few tests. Here I got the exception that toast can not be displayed on a thread that has not called Looper.prepare.
When I comment out the toast msg live data updating line of code then all tests work fine and pass successfully. I am new to android testing and searched out a lot but did not get any info to solve this issue. Any help would be much appreciated. A little bit of explanation will be much more helpful if provided.
Below is my test class source code along with ViewModel, FakeRepository, and fragment source code.
Test Class.
#ExperimentalCoroutinesApi
#MediumTest
#RunWith(AndroidJUnit4::class)
class TaskDetailFragmentTest {
#get:Rule
var mainCoroutineRule = MainCoroutineRule()
#get:Rule
val rule = InstantTaskExecutorRule()
private lateinit var tasksRepository: FakeTasksRepository
#Before
fun setUp() {
tasksRepository = FakeTasksRepository()
ServiceLocator.taskRepositories = tasksRepository
}
#Test
fun addNewTask_addNewTaskToDatabase() = mainCoroutineRule.runBlockingTest {
val newTask = Task(id = "1", userId = 0, title = "Hello AndroidX World",false)
tasksRepository.addTasks(newTask)
val task = tasksRepository.getTask(newTask.id)
assertEquals(newTask.id,(task as Result.Success).data.id)
}
#Test
fun activeTaskDetails_DisplayedInUi() = mainCoroutineRule.runBlockingTest {
val newTask = Task(id = "2", userId = 0, title = "Hello AndroidX World",false)
tasksRepository.addTasks(newTask)
val bundle = TaskDetailFragmentArgs(newTask.id).toBundle()
launchFragmentInContainer<TaskDetailFragment>(bundle, R.style.Theme_ToDoWithTDD)
onView(withId(R.id.title_text)).check(matches(isDisplayed()))
onView(withId(R.id.title_text)).check(matches(withText("Hello AndroidX World")))
onView(withId(R.id.complete_checkbox)).check(matches(isDisplayed()))
onView(withId(R.id.complete_checkbox)).check(matches(isNotChecked()))
}
#Test
fun completedTaskDetails_DisplayedInUI() = mainCoroutineRule.runBlockingTest {
val newTask = Task(id = "2", userId = 0, title = "Hello AndroidX World",true)
tasksRepository.addTasks(newTask)
val bundle = TaskDetailFragmentArgs(newTask.id).toBundle()
launchFragmentInContainer <TaskDetailFragment>(bundle,R.style.Theme_ToDoWithTDD)
onView(withId(R.id.title_text)).check(matches(isDisplayed()))
onView(withId(R.id.title_text)).check(matches(withText("Hello AndroidX World")))
onView(withId(R.id.complete_checkbox)).check(matches(isDisplayed()))
onView(withId(R.id.complete_checkbox)).check(matches(isChecked()))
}
#After
fun tearDown() = mainCoroutineRule.runBlockingTest {
ServiceLocator.resetRepository()
}
}
FakeRepository class.
class FakeTasksRepository: TasksRepository {
var tasksServiceData: LinkedHashMap<String,Task> = LinkedHashMap()
private val observableTasks: MutableLiveData<Result<List<Task>>> = MutableLiveData()
private var shouldReturnError: Boolean = false
fun setReturnError(value: Boolean) {
shouldReturnError = value
}
override fun observeTasks(): LiveData<Result<List<Task>>> {
return observableTasks
}
override fun observeTask(taskId: String): LiveData<Result<Task>> {
runBlocking { fetchAllToDoTasks() }
return observableTasks.map { tasks ->
when(tasks) {
is Result.Loading -> Result.Loading
is Result.Error -> Result.Error(tasks.exception)
is Result.Success -> {
val task = tasks.data.firstOrNull() { it.id == taskId }
?: return#map Result.Error(Exception("Not found"))
Result.Success(task)
}
}
}
}
override suspend fun completeTask(id: String) {
tasksServiceData[id]?.completed = true
}
override suspend fun completeTask(task: Task) {
val compTask = task.copy(completed = true)
tasksServiceData[task.id] = compTask
fetchAllToDoTasks()
}
override suspend fun activateTask(id: String) {
tasksServiceData[id]?.completed = false
}
override suspend fun activateTask(task: Task) {
val activeTask = task.copy(completed = false)
tasksServiceData[task.id] = activeTask
fetchAllToDoTasks()
}
override suspend fun getTask(taskId: String): Result<Task> {
if (shouldReturnError) return Result.Error(Exception("Test Exception"))
tasksServiceData[taskId]?.let {
return Result.Success(it)
}
return Result.Error(Exception("Could not find task"))
}
override suspend fun getTasks(): Result<List<Task>> {
return Result.Success(tasksServiceData.values.toList())
}
override suspend fun saveTask(task: Task) {
tasksServiceData[task.id] = task
}
override suspend fun clearAllCompletedTasks() {
tasksServiceData = tasksServiceData.filterValues {
!it.completed
} as LinkedHashMap<String, Task>
}
override suspend fun deleteAllTasks() {
tasksServiceData.clear()
fetchAllToDoTasks()
}
override suspend fun deleteTask(taskId: String) {
tasksServiceData.remove(taskId)
fetchAllToDoTasks()
}
override suspend fun fetchAllToDoTasks(): Result<List<Task>> {
if(shouldReturnError) {
return Result.Error(Exception("Could not find task"))
}
val tasks = Result.Success(tasksServiceData.values.toList())
observableTasks.value = tasks
return tasks
}
override suspend fun updateLocalDataStore(list: List<Task>) {
TODO("Not yet implemented")
}
fun addTasks(vararg tasks: Task) {
tasks.forEach {
tasksServiceData[it.id] = it
}
runBlocking {
fetchAllToDoTasks()
}
}
}
Fragment class.
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
viewModel.loadTaskById(args.taskId)
setUpToast(this,viewModel.toastText)
viewModel.editTaskEvent.observe(viewLifecycleOwner, {
it?.let {
val action = TaskDetailFragmentDirections
.actionTaskDetailFragmentToAddEditFragment(
args.taskId,
resources.getString(R.string.edit_task)
)
findNavController().navigate(action)
}
})
binding.editTaskFab.setOnClickListener {
viewModel.editTask()
}
}
ViewModel class.
class TaskDetailViewModel(
private val tasksRepository: TasksRepository
) : ViewModel() {
private val TAG = "TaskDetailViewModel"
private val _taskId: MutableLiveData<String> = MutableLiveData()
private val _task = _taskId.switchMap {
tasksRepository.observeTask(it).map { res ->
Log.d("Test","res with value ${res.toString()}")
isolateTask(res)
}
}
val task: LiveData<Task?> = _task
private val _toastText = MutableLiveData<Int?>()
val toastText: LiveData<Int?> = _toastText
private val _dataLoading = MutableLiveData<Boolean>()
val dataLoading: LiveData<Boolean> = _dataLoading
private val _editTaskEvent = MutableLiveData<Unit?>(null)
val editTaskEvent: LiveData<Unit?> = _editTaskEvent
fun loadTaskById(taskId: String) {
if(dataLoading.value == true || _taskId.value == taskId) return
_taskId.value = taskId
Log.d("Test","loading task with id $taskId")
}
fun editTask(){
_editTaskEvent.value = Unit
}
fun setCompleted(completed: Boolean) = viewModelScope.launch {
val task = _task.value ?: return#launch
if(completed) {
tasksRepository.completeTask(task.id)
_toastText.value = R.string.task_marked_complete
}
else {
tasksRepository.activateTask(task.id)
_toastText.value = R.string.task_marked_active
}
}
private fun isolateTask(result: Result<Task?>): Task? {
return if(result is Result.Success) {
result.data
} else {
_toastText.value = R.string.loading_tasks_error
null
}
}
#Suppress("UNCHECKED_CAST")
class TasksDetailViewModelFactory(
private val tasksRepository: TasksRepository
): ViewModelProvider.NewInstanceFactory() {
override fun <T : ViewModel> create(modelClass: Class<T>): T {
return (TaskDetailViewModel(
tasksRepository
) as T)
}
}
}
In this method in ViewModel when I comment out the below line of code all tests passed.
_toastText.value = R.string.loading_tasks_error
private fun isolateTask(result: Result<Task?>): Task? {
return if(result is Result.Success) {
result.data
} else {
_toastText.value = R.string.loading_tasks_error // Comment out this line then all test passed.
null
}
}

How to show ProgressDialog when fetching data from ViewModel

I want to show ProgressDialog while fetching data from ViewModel and it works fine when I fetch data for the first time, but when I want to refresh the data from API the ProgressDialog starts and does not stops
I create MutableLiveData<Boolean>() and try to manage the visibility but it's not working
This is how i refresh my data from my Activity
private fun loadNorthTram() {
val model =
ViewModelProviders.of(this#MainActivity).get(MyViewModelNorth::class.java)
model.isNorthUpdating.observe(
this#MainActivity,
Observer { b ->
if (b!!)
AppUtil.showProgressSpinner(this#MainActivity)
else
AppUtil.dismissProgressDialog()
})
model.getNorthTrams().observe(this#MainActivity, Observer
{
if (it != null) {
setData(it)
}
})
}
Below is my ViewModel class
class MyViewModelNorth : ViewModel() {
private lateinit var mtoken: String
private val apiService: ApiInterface = ApiClient.client.create(ApiInterface::class.java)
private lateinit var trams: MutableLiveData<TramModel>
val isNorthUpdating = MutableLiveData<Boolean>().default(false)
fun getNorthTrams(): MutableLiveData<TramModel> {
isNorthUpdating.value = true
if (!::trams.isInitialized) {
trams = MutableLiveData()
callTokenAPI()
}
return trams
}
private fun callTokenAPI() {
val tokenObservable: Observable<TokenModel> = apiService.fetchToken()
tokenObservable
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.doOnNext { self ->
mtoken = self.responseObject[0].DeviceToken
callTramAPI()
}
.subscribe(getTokenObserver())
}
private fun callTramAPI() {
val apiService: ApiInterface = ApiClient.client.create(ApiInterface::class.java)
val observable: Observable<TramModel> = apiService.fetchTrams(AppUtil.NORTH_TRAMS, mtoken)
observable
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(getTramObserver())
}
private fun getTokenObserver(): Observer<TokenModel> {
return object : Observer<TokenModel> {
override fun onComplete() {}
override fun onSubscribe(d: Disposable) {}
override fun onNext(tokenModel: TokenModel) {}
override fun onError(e: Throwable) {
if (e is HttpException) {
val errorBody = e.response().errorBody()
HttpErrorUtil(e.code()).handelError()
}
}
}
}
private fun getTramObserver(): Observer<TramModel> {
return object : Observer<TramModel> {
override fun onComplete() {
isNorthUpdating.value = false
}
override fun onSubscribe(d: Disposable) {}
override fun onNext(t: TramModel) {
if (!t.hasError && t.hasResponse)
trams.value = t
else if (t.errorMessage.isBlank())
applicationContext().showToast(t.errorMessage)
else
applicationContext().showToast(applicationContext().getString(R.string.something_wrong))
}
override fun onError(e: Throwable) {
if (e is HttpException) {
val errorBody = e.response().errorBody()
HttpErrorUtil(e.code()).handelError()
}
}
}
}
public fun getIsNothUpdating(): LiveData<Boolean> {
return isNorthUpdating
}
fun <T : Any?> MutableLiveData<T>.default(initialValue: T) = apply { setValue(initialValue) }
}
I have not tested your code but I think your problem is in function getNorthTrams() in viewmodel.
First time when you fetch data, trams is not initialized, your api call is happening and at only onCompleted, you are setting isNorthUpdating.value = false. This code works.
But when you refresh data, trams is already initialized. So, there is no case for isNorthUpdating.value = false, which is causing progress dialog to not dismiss.
So I think you should handle else case in your viewmodel.
fun getNorthTrams(): MutableLiveData<TramModel> {
isNorthUpdating.value = true
if (!::trams.isInitialized) {
trams = MutableLiveData()
callTokenAPI()
}else{
//do your thing for refresh
isNorthUpdating.value = false
}
return trams
}
Also, in api call, if error occur, you should set isNorthUpdating to false and show some error message. Otherwise, progress dialog will always be showing even if some error occur in api call.

Kotlin coroutine scope & job cancellation in non-lifecycle classes

How to use new Kotlin v1.3 coroutines in classes which do not have lifecycles, like repositories?
I have a class where I check if the cache is expired and then decide whether I fetch the data from the remote API or local database. I need to start the launch and async from there. But then how do I cancel the job?
Example code:
class NotesRepositoryImpl #Inject constructor(
private val cache: CacheDataSource,
private val remote: RemoteDataSource
) : NotesRepository, CoroutineScope {
private val expirationInterval = 60 * 10 * 1000L /* 10 mins */
private val job = Job()
override val coroutineContext: CoroutineContext
get() = Dispatchers.IO + job
override fun getNotes(): LiveData<List<Note>> {
if (isOnline() && isCacheExpired()) {
remote.getNotes(object : GetNotesCallback {
override fun onGetNotes(data: List<Note>?) {
data?.let {
launch {
cache.saveAllNotes(it)
cache.setLastCacheTime(System.currentTimeMillis())
}
}
}
})
}
return cache.getNotes()
}
override fun addNote(note: Note) {
if (isOnline()) {
remote.createNote(note, object : CreateNoteCallback {
override fun onCreateNote(note: Note?) {
note?.let { launch { cache.addNote(it) } }
}
})
} else {
launch { cache.addNote(note) }
}
}
override fun getSingleNote(id: Int): LiveData<Note> {
if (isOnline()) {
val liveData: MutableLiveData<Note> = MutableLiveData()
remote.getNote(id, object : GetSingleNoteCallback {
override fun onGetSingleNote(note: Note?) {
note?.let {
liveData.value = it
}
}
})
return liveData
}
return cache.getSingleNote(id)
}
override fun editNote(note: Note) {
if (isOnline()) {
remote.updateNote(note, object : UpdateNoteCallback {
override fun onUpdateNote(note: Note?) {
note?.let { launch { cache.editNote(note) } }
}
})
} else {
cache.editNote(note)
}
}
override fun delete(note: Note) {
if (isOnline()) {
remote.deleteNote(note.id!!, object : DeleteNoteCallback {
override fun onDeleteNote(noteId: Int?) {
noteId?.let { launch { cache.delete(note) } }
}
})
} else {
cache.delete(note)
}
}
private fun isCacheExpired(): Boolean {
var delta = 0L
runBlocking(Dispatchers.IO) {
val currentTime = System.currentTimeMillis()
val lastCacheTime = async { cache.getLastCacheTime() }
delta = currentTime - lastCacheTime.await()
}
Timber.d("delta: $delta")
return delta > expirationInterval
}
private fun isOnline(): Boolean {
val runtime = Runtime.getRuntime()
try {
val ipProcess = runtime.exec("/system/bin/ping -c 1 8.8.8.8")
val exitValue = ipProcess.waitFor()
return exitValue == 0
} catch (e: IOException) {
e.printStackTrace()
} catch (e: InterruptedException) {
e.printStackTrace()
}
return false
}
}
You can create some cancelation method in the repository and call it from class which has lifecycle (Activity, Presenter or ViewModel), for example:
class NotesRepositoryImpl #Inject constructor(
private val cache: CacheDataSource,
private val remote: RemoteDataSource
) : NotesRepository, CoroutineScope {
private val job = Job()
override val coroutineContext: CoroutineContext
get() = Dispatchers.IO + job
fun cancel() {
job.cancel()
}
//...
}
class SomePresenter(val repo: NotesRepository) {
fun detachView() {
repo.cancel()
}
}
Or move launching coroutine to some class with lifecycle.

Android: How to detect Bluetooth connection status

I want to detect the connection status of a paired Bluetooth headset
to the phone.
In Android 3.0 (API level 11) "BluetoothHeadset" class has
"isAudioConnected()" method.
I don't know how to create (initialize) a "BluetoothHeadset" object.
It seems that I need to use
"getProfileProxy ()" but I need a sample code to find out how I need
to create and pass the parameters.
Thanks,
Hos
You need to implement BluetoothProfile.ServiceListener :
BluetoothProfile.ServiceListener b = new BlueToothListener();
boolean profileProxy = BluetoothAdapter.getDefaultAdapter()
.getProfileProxy(Handler.bot, b, BluetoothProfile.HEADSET);
public class BlueToothListener implements ServiceListener {
public static BluetoothHeadset headset;
public static BluetoothDevice bluetoothDevice;
#Override
public void onServiceDisconnected(int profile) {// dont care
headset = null;
}
#Override
public void onServiceConnected(int profile,
BluetoothProfile proxy) {// dont care
try {
Debugger.test("BluetoothProfile onServiceConnected "+proxy);
if (proxy instanceof BluetoothHeadset)
headset = ((BluetoothHeadset) proxy);
else// getProfileProxy(Handler.bot, b, BluetoothProfile.HEADSET);
return;// ^^ => NEVER
List<BluetoothDevice> connectedDevices = proxy
.getConnectedDevices();
for (BluetoothDevice device : connectedDevices) {
Debugger.log("BluetoothDevice found :" + device);
bluetoothDevice = device;
int connectionState = headset.getConnectionState(bluetoothDevice);
Debugger.log("BluetoothHeadset connectionState "+connectionState);//2 == OK
boolean startVoiceRecognition = headset
.startVoiceRecognition(device);
if (startVoiceRecognition) {
Debugger
.log("BluetoothHeadset init Listener OK");
return;
}
else
Notify.popup("Bluetooth headset can't start speech recognition");
}
} catch (Exception e) {
// }
}
}
}
`
Monitoring of the BT status can be done indeed by polling. Here's how I did it (full sample here) . Note that it's just a sample and you should manage the polling better:
manifest
<uses-permission android:name="android.permission.BLUETOOTH" />
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
gradle
implementation 'androidx.core:core-ktx:1.9.0'
implementation 'com.google.android.material:material:1.7.0'
implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
implementation "androidx.work:work-runtime-ktx:2.7.1"
MainActivityViewModel.kt
#UiThread
class MainActivityViewModel(application: Application) : BaseViewModel(application) {
private val bluetoothAdapter: BluetoothAdapter =
context.getSystemService<BluetoothManager>()!!.adapter
private var bluetoothHeadsetProfile: BluetoothProfile? = null
val connectedDevicesLiveData =
DistinctLiveDataWrapper(MutableLiveData<ConnectedDevicesState>(ConnectedDevicesState.Idle))
val bluetoothTurnedOnLiveData = DistinctLiveDataWrapper(MutableLiveData<Boolean?>(null))
val isConnectedToBtHeadsetLiveData = DistinctLiveDataWrapper(MutableLiveData<Boolean?>(null))
private val pollingBtStateRunnable: Runnable
init {
updateBtStates()
pollingBtStateRunnable = object : Runnable {
override fun run() {
updateBtStates()
handler.postDelayed(this, POLLING_TIME_IN_MS)
}
}
// Establish connection to the proxy.
val serviceListener = object : BluetoothProfile.ServiceListener {
override fun onServiceConnected(profile: Int, bluetoothProfile: BluetoothProfile) {
this#MainActivityViewModel.bluetoothHeadsetProfile = bluetoothProfile
handler.removeCallbacks(pollingBtStateRunnable)
pollingBtStateRunnable.run()
}
override fun onServiceDisconnected(profile: Int) {
handler.removeCallbacks(pollingBtStateRunnable)
updateBtStates()
}
}
bluetoothAdapter.getProfileProxy(context, serviceListener, BluetoothProfile.HEADSET)
onClearedListeners.add {
this.bluetoothHeadsetProfile?.let { bluetoothProfile ->
bluetoothAdapter.closeProfileProxy(BluetoothProfile.HEADSET, bluetoothProfile)
}
handler.removeCallbacks(pollingBtStateRunnable)
}
}
fun initWithLifecycle(lifecycle: Lifecycle) {
lifecycle.addObserver(object : DefaultLifecycleObserver {
override fun onResume(owner: LifecycleOwner) {
super.onResume(owner)
pollingBtStateRunnable.run()
}
override fun onPause(owner: LifecycleOwner) {
super.onPause(owner)
handler.removeCallbacks(pollingBtStateRunnable)
}
})
}
#UiThread
private fun updateBtStates() {
// Log.d("AppLog", "updateBtStates")
val isBlueToothTurnedOn = bluetoothAdapter.state == BluetoothAdapter.STATE_ON
bluetoothTurnedOnLiveData.value = isBlueToothTurnedOn
if (!isBlueToothTurnedOn) {
connectedDevicesLiveData.value = ConnectedDevicesState.BluetoothIsTurnedOff
isConnectedToBtHeadsetLiveData.value = false
return
}
val isConnectedToBtHeadset = try {
bluetoothAdapter.getProfileConnectionState(BluetoothProfile.HEADSET) == BluetoothAdapter.STATE_CONNECTED
} catch (e: SecurityException) {
null
}
isConnectedToBtHeadsetLiveData.value = isConnectedToBtHeadset
val bluetoothProfile = bluetoothHeadsetProfile
if (bluetoothProfile != null) {
if (ActivityCompat.checkSelfPermission(context, Manifest.permission.BLUETOOTH_CONNECT) == PackageManager.PERMISSION_GRANTED) {
val connectedDevicesSet = bluetoothProfile.connectedDevices.toHashSet()
val previousConnectedDevices =
(connectedDevicesLiveData.value as? ConnectedDevicesState.GotResult)?.connectedDevices
if (previousConnectedDevices == null || previousConnectedDevices != connectedDevicesSet)
connectedDevicesLiveData.value =
ConnectedDevicesState.GotResult(connectedDevicesSet)
} else {
connectedDevicesLiveData.value =
ConnectedDevicesState.NeedBlueToothConnectPermission
}
} else {
connectedDevicesLiveData.value = ConnectedDevicesState.Idle
}
}
companion object {
private const val POLLING_TIME_IN_MS = 500L
}
}
MainActivity.kt
class MainActivity : AppCompatActivity() {
private lateinit var viewModel: MainActivityViewModel
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
viewModel = ViewModelProvider(this)[MainActivityViewModel::class.java]
viewModel.initWithLifecycle(lifecycle)
viewModel.bluetoothTurnedOnLiveData.observe(this) {
Log.d("AppLog", "MainActivity bluetoothTurnedOnLiveData BT turned on? $it")
}
viewModel.isConnectedToBtHeadsetLiveData.observe(this) {
Log.d("AppLog", "MainActivity isConnectedToBtHeadsetLiveData BT headset connected? $it")
}
viewModel.connectedDevicesLiveData.observe(this) {
Log.d("AppLog", "MainActivity connectedDevicesLiveData devices: $it")
}
findViewById<View>(R.id.grantBtPermission).setOnClickListener {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
requestPermissions(arrayOf(Manifest.permission.BLUETOOTH_CONNECT), 1)
}
}
}
}
ConnectedDevicesState.kt
sealed class ConnectedDevicesState {
object Idle : ConnectedDevicesState() {
override fun toString(): String {
if (BuildConfig.DEBUG)
return "Idle"
return super.toString()
}
}
class GotResult(#Suppress("MemberVisibilityCanBePrivate") val connectedDevices: Set<BluetoothDevice>) : ConnectedDevicesState() {
override fun toString(): String {
if (BuildConfig.DEBUG) {
return "GotResult: connectedDevices:${
connectedDevices.map {
try {
it.name
} catch (e: SecurityException) {
it.address
}
}
}"
}
return super.toString()
}
}
object BluetoothIsTurnedOff : ConnectedDevicesState() {
override fun toString(): String {
if (BuildConfig.DEBUG)
return "BluetoothIsTurnedOff"
return super.toString()
}
}
object NeedBlueToothConnectPermission : ConnectedDevicesState() {
override fun toString(): String {
if (BuildConfig.DEBUG)
return "NeedBlueToothConnectPermission"
return super.toString()
}
}
}
DistinctLiveDataWrapper.kt
class DistinctLiveDataWrapper<T>(#Suppress("MemberVisibilityCanBePrivate") val mutableLiveData: MutableLiveData<T>) {
#Suppress("MemberVisibilityCanBePrivate")
val distinctLiveData = Transformations.distinctUntilChanged(mutableLiveData)
var value: T?
#UiThread
set(value) {
mutableLiveData.value = value
}
get() {
return mutableLiveData.value
}
#AnyThread
fun postValue(value: T) {
mutableLiveData.postValue(value)
}
fun observe(lifecycleOwner: LifecycleOwner, observer: Observer<in T>) {
distinctLiveData.observe(lifecycleOwner, observer)
}
}
BaseViewModel.kt
/**usage: class MyViewModel(application: Application) : BaseViewModel(application)
* getting instance: private lateinit var viewModel: MyViewModel
* viewModel=ViewModelProvider(this).get(MyViewModel::class.java)*/
abstract class BaseViewModel(application: Application) : AndroidViewModel(application) {
#Suppress("MemberVisibilityCanBePrivate")
var isCleared = false
#Suppress("MemberVisibilityCanBePrivate")
val onClearedListeners = ArrayList<Runnable>()
#Suppress("unused")
#SuppressLint("StaticFieldLeak")
val context: Context = application.applicationContext
#Suppress("unused")
val handler = Handler(Looper.getMainLooper())
override fun onCleared() {
super.onCleared()
isCleared = true
onClearedListeners.forEach { it.run() }
}
}

Categories

Resources