I have a Fragment with Mapbox and I want to display the device location on it.
class SampleMapFragment : Fragment(), PermissionsListener {
private lateinit var binding: FragmentExploreBinding
#Inject
lateinit var permissionsManager: PermissionsManager
private lateinit var mapboxMap: MapboxMap
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
Mapbox.getInstance(requireContext().applicationContext, getString(R.string.mapbox_token))
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
if (!::binding.isInitialized) {
binding = DataBindingUtil.inflate(
inflater,
R.layout.fragment_explore,
container,
false
)
binding.lifecycleOwner = this
binding.mapView.onCreate(savedInstanceState)
setUpMap()
}
return binding.root
}
private fun setUpMap() {
binding.mapView.getMapAsync { mapboxMap ->
this.mapboxMap = mapboxMap
mapboxMap.setStyle(Style.MAPBOX_STREETS) { loadedMapStyle ->
starLocationTracking(loadedMapStyle)
}
}
}
private fun starLocationTracking(loadedMapStyle: Style) {
if (!PermissionsManager.areLocationPermissionsGranted(requireContext())) {
permissionsManager.requestLocationPermissions(requireActivity())
return
}
initLocationComponent(loadedMapStyle)
}
private fun initLocationComponent(loadedMapStyle: Style) {
val customLocationComponentOptions = LocationComponentOptions.builder(requireActivity())
.elevation(5f)
.accuracyAlpha(.6f)
.accuracyColor(Color.RED)
.build()
val locationComponentActivationOptions = LocationComponentActivationOptions.builder(
requireActivity(),
loadedMapStyle
)
.locationComponentOptions(customLocationComponentOptions)
.build()
mapboxMap.locationComponent.apply {
activateLocationComponent(locationComponentActivationOptions)
isLocationComponentEnabled = true
renderMode = RenderMode.COMPASS
cameraMode = CameraMode.TRACKING
}
}
override fun onExplanationNeeded(permissionsToExplain: MutableList<String>?) {
// TODO some explanation can be shown here
}
override fun onPermissionResult(granted: Boolean) {
if (granted) mapboxMap.getStyle { loadedStyle -> starLocationTracking(loadedStyle) }
//else TODO some explanation can be shown here
}
fun onMapBoxRequestPermissionsResult(
requestCode: Int,
permissions: Array<out String>,
grantResults: IntArray
) = permissionsManager.onRequestPermissionsResult(requestCode, permissions, grantResults)
override fun onStart() {
super.onStart()
binding.mapView.onStart()
}
override fun onResume() {
super.onResume()
binding.mapView.onResume()
}
override fun onPause() {
super.onPause()
binding.mapView.onPause()
}
override fun onStop() {
super.onStop()
binding.mapView.onStop()
}
override fun onDestroy() {
super.onDestroy()
releaseMapResources()
releasePermissionsManagerListener()
}
private fun releaseMapResources() {
binding.mapView.onDestroy()
}
private fun releasePermissionsManagerListener() {
permissionsManager.listener = null
}
override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
binding.mapView.onSaveInstanceState(outState)
}
override fun onLowMemory() {
super.onLowMemory()
binding.mapView.onLowMemory()
}
}
I tested the implementation above with 2 real devices and 1 emulator.
The solution works fine with the Android 10 device. User location is found, even after closing the location services and opening it again after the map, is visible.
But location is sometimes not found, sometimes it take very long, even the location services are ready before the map is visible while trying with Android 9 and Android 7 devices.
What could be the problem here? Any help would be appreciated.
"But location is sometimes not found, sometimes it take very long,
even the location services are ready before the map is visible while
trying with Android 9 and Android 7 devices."
This sounds as if the device tries to obtain the location from the GPS sensor, but takes long to retrieve it. With newer devices and Android 10 localization has been improved, so this could explain what you are seeing.
To understand what is going on, I would recommend to implement the Android FusedLocationProvider and compare the behaviour to the one wrapped by Mapbox SDKs (locationComponent).
Also, please check whether the "snappy" location you get with Android 10 is actually retrieved from the GPS sensor, or if it is "just" the last known location, that was cached by Android.
Related
I have a simple MainActivity and if the app is completely killed it looks like onCreate() is called once. If however I back out of the app so it still appears in the background, when I re-open it I get every log message twice. The weirdest part is if I generate a random number it is always the same in the 2 log messages.
I've tried adding android:LaunchMode="singleTop" (also singleInstance singleTask) in the activity and application tags of the Manifest.
class MainActivity : AppCompatActivity() {
private val binding: ActivityMainBinding by lazy {
ActivityMainBinding.inflate(layoutInflater)
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val view = binding.root
setContentView(view)
setupViews()
val data: Uri? = intent?.data
DataHolder.getInstance().setItem(data)
Timber.plant(Timber.DebugTree())
setupInjection()
Timber.d("review nanoTime = ${System.nanoTime()}")
Timber.d("review savedInstance = $savedInstanceState")
Timber.d("review random = ${Random.nextInt()}")
}
override fun onPause() {
Timber.d("review onPause()")
super.onPause()
}
override fun onStop() {
Timber.d("review onStop()")
super.onStop()
}
override fun onDestroy() {
Timber.d("review onDestroy()")
super.onDestroy()
finish()
}
override fun onStart() {
Timber.d("review onStart()")
super.onStart()
}
override fun onRestart() {
Timber.d("review onRestart()")
super.onRestart()
}
override fun onResume() {
Timber.d("review onResume()")
super.onResume()
}
private fun setupInjection() {
val appInjector = InjectorImpl(
firebaseAuth = FirebaseAuth.getInstance()
)
Injector.initialize(appInjector)
}
private fun setupViews() = binding.apply {
val navController = findNavController(R.id.nav_host_fragment)
navView.setupWithNavController(navController)
navView.setOnItemSelectedListener { item ->
when (item.itemId){
R.id.navigation_item_calculator -> {
navController.navigate(BuilderFragmentDirections.actionBuilderToCalculator())
}
R.id.navigation_item_builder -> {
navController.navigate(CalculatorFragmentDirections.actionCalculatorToBuilder())
}
}
true
}
navView.setOnItemReselectedListener { }
}
}
Here is a table of the log trace I get when I run the app on my phone from Android studio. Since the random numbers are the same I feel like this is actually a Logging bug in Android studio and the app isn't actually opened twice.
Realized my problem was with my logging library I used.
Timber was planting a new tree but wasn't uprooting old ones from being backed out so there were 2 instances of them. I fixed by putting a Timber.uprootAll() just before Timber.plant(Timber.DebugTree())
I am using live data from a shared ViewModel across multiple fragments. I have a sign-in fragment which takes user's phone number and password and then the user presses sign in button I am calling the API for that, now if the sign-in fails I am showing a toast "Sign In failed", now if the user goes to "ForgotPassword" screen which also uses the same view model as "SignInFragment" and presses back from the forgot password screen, it comes to sign-in fragment, but it again shows the toast "Sign In failed" but the API is not called, it gets data from the previously registered observer, so is there any way to fix this?
SignInFragment.kt
class SignInFragment : Fragment() {
private lateinit var binding: FragmentSignInBinding
//Shared view model across two fragments
private val onBoardViewModel by activityViewModels<OnBoardViewModel>()
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
binding = DataBindingUtil.inflate(
inflater,
R.layout.fragment_sign_in,
container,
false
)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
onBoardViewModel.signInResponse.observe(viewLifecycleOwner) { response ->
//This is calling again after coming back from new fragment it.
showToast("Sign In Failed")
}
}
override fun onClick(v: View?) {
when (v?.id!!) {
R.id.forgotPasswordTV -> {
findNavController().navigate(SignInFragmentDirections.actionSignInFragmentToForgotPasswordFragment())
}
R.id.signInTV -> {
val phoneNumber = binding.phoneNumberET.text
val password = binding.passwordET.text
val signInRequestModel = SignInRequestModel(
phoneNumber.toString(),
password.toString(),
""
)
//Calling API for the sign-in
onBoardViewModel.callSignInAPI(signInRequestModel)
}
}
}
}
ForgotPasswordFragment
class ForgotPasswordFragment : Fragment() {
private lateinit var binding: FragmentForgotPasswordBinding
//Shared view model across two fragments
private val onBoardViewModel by activityViewModels<OnBoardViewModel>()
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
binding = DataBindingUtil.inflate(
inflater,
R.layout.fragment_forgot_password,
container,
false
)
return binding.root
}
}
OnBoardViewModel
class OnBoardViewModel : ViewModel() {
private var repository: OnBoardRepository = OnBoardRepository.getInstance()
private val signInRequestLiveData = MutableLiveData<SignInRequestModel>()
//Observing this data in sign in fragment
val signInResponse: LiveData<APIResource<SignInResponse>> =
signInRequestLiveData.switchMap {
repository.callSignInAPI(it)
}
//Calling this function from sign in fragment
fun callSignInAPI(signInRequestModel: SignInRequestModel) {
signInRequestLiveData.value = signInRequestModel
}
override fun onCleared() {
super.onCleared()
repository.clearRepo()
}
}
I have tried to move this code inside onActivityCreated but it's still getting called after coming back from new fragment.
onBoardViewModel.signInResponse.observe(viewLifecycleOwner) { response ->
showToast("Sign In Failed")
}
Using SingleLiveEvent class instead of LiveData in OnBoardViewModel class will solve your problem:
val signInResponse: SingleLiveEvent <APIResource<SignInResponse>>.
class SingleLiveEvent<T> : MutableLiveData<T>() {
private val pending = AtomicBoolean(false)
override fun observe(owner: LifecycleOwner, observer: Observer<in T>) {
super.observe(owner, Observer<T> { t ->
if (pending.compareAndSet(true, false)) {
observer.onChanged(t)
}
})
}
override fun setValue(t: T?) {
pending.set(true)
super.setValue(t)
}
fun call() {
postValue(null)
}
}
This is a lifecycle-aware observable that sends only new updates after subscription. This LiveData only calls the observable if there's an explicit call to setValue() or call().
I would provide a way to reset your live data. Give it a nullable type. Your observers can ignore it when they get a null value. Call this function when you receive login data, so you also won't be repeating messages on a screen rotation.
class OnBoardViewModel : ViewModel() {
// ...
fun consumeSignInResponse() {
signInRequestLiveData.value = null
}
}
onBoardViewModel.signInResponse.observe(viewLifecycleOwner) { response ->
if (response != null) {
showToast("Sign In Failed")
onBoardViewModel.consumeSignInResponse()
}
}
For Kotlin users #Sergey answer can also be implemented using delegates like below
class SingleLiveEvent<T> : MutableLiveData<T>() {
var curUser: Boolean by Delegates.vetoable(false) { property, oldValue, newValue ->
newValue != oldValue
}
override fun observe(owner: LifecycleOwner, observer: Observer<in T>) {
super.observe(owner, Observer<T> { t ->
if (curUser) {
observer.onChanged(t)
curUser = false
}
})
}
override fun setValue(t: T?) {
curUser = true
super.setValue(t)
}
fun call() {
postValue(null)
}
}
I'm using Biometrics library to lock the app. Everything works fine and when I'm unlocking with fingerprint onAuthenticationSucceeded() get's called and device navigates from the lock screen. However if unlock with pattern the onAuthenticationSucceeded() get's called but navigation doesn't initialise and I'm left stuck on the lock screen fragment.
EDIT: This only affects API29 with ANY device credentials
EDIT2: I'm also getting
FragmentNavigator: Ignoring popBackStack() call: FragmentManager has
already saved its state
FragmentNavigator: Ignoring navigate() call: FragmentManager has already saved its state
private lateinit var biometricPrompt: BiometricPrompt
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
biometricPrompt = createBiometricPrompt()
return inflater.inflate(R.layout.lock_screen_fragment, container, false)
}
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
val isAppLockEnabled: Boolean = PreferenceManager.getDefaultSharedPreferences(context)
.getBoolean("lock_app_preference", false)
// If app locks is not set go to home fragment else display app lock screen
if (!isAppLockEnabled) {
findNavController().navigate(R.id.action_lock_screen_fragment_dest_to_home_fragment_dest)
} else {
// Prompt appears when user clicks "Unlock".
unlock_button.setOnClickListener {
val promptInfo = createPromptInfo()
biometricPrompt.authenticate(promptInfo)
}
}
}
private fun createBiometricPrompt(): BiometricPrompt {
val executor = ContextCompat.getMainExecutor(context)
val callback = object : BiometricPrompt.AuthenticationCallback() {
override fun onAuthenticationError(errorCode: Int, errString: CharSequence) {
super.onAuthenticationError(errorCode, errString)
Log.d("AuthenticationError()", "$errorCode :: $errString")
}
override fun onAuthenticationFailed() {
super.onAuthenticationFailed()
Log.d("AuthenticationFailed()", "Authentication failed for an unknown reason")
}
override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult) {
super.onAuthenticationSucceeded(result)
lock_icon.setImageResource(R.drawable.ic_unlock)
lock_screen_text_view.text = getString(R.string.app_unlocked)
//This doesn't work when using pattern unlock findNavController().navigate(R.id.action_lock_screen_fragment_dest_to_home_fragment_dest)
}
}
return BiometricPrompt(this, executor, callback)
}
private fun createPromptInfo(): BiometricPrompt.PromptInfo {
return BiometricPrompt.PromptInfo.Builder()
.setTitle("Unlock App")
.setConfirmationRequired(false)
.setDeviceCredentialAllowed(true)
.build()
}
}
Ok, so I solved this issue. Moved navigation from onAuthenticationSucceeded() to fragments onResume(). Device credentials window pauses my app and somehow navigation cannot be called after that.
Solution code:
private var isAppUnlocked : Boolean = false
override fun onAuthenticationSucceeded(result: BiometricPrompt.AuthenticationResult) {
super.onAuthenticationSucceeded(result)
isAppUnlocked = true
unlockApp()
Log.d("AuthenticationSuccess", "Authentication succeeded")
}
override fun onResume() {
super.onResume()
if(isAppUnlocked){
unlockApp()
}
}
private fun unlockApp(){
lock_icon.setImageResource(R.drawable.ic_unlock)
lock_screen_text_view.text = getString(R.string.app_unlocked)
findNavController().navigate(R.id.action_lock_screen_fragment_dest_to_home_fragment_dest)
}
I have an application that performs writing by NFC on a card, depending on the number that has passed will perform a number or other reads. I guess I do this with a simple for loop, the problem is that I do not know where to put that for loop. I give you an example of the class:
class HomeFragment : Fragment(), OnClickDetailsMonuments {
private lateinit var homeFragmentViewModel: HomeFragmentViewModel
private lateinit var homeMonumentsAdapter: MonumentsAdapter
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View =
inflater.inflate(R.layout.home_fragment, container, false)
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
initViews()
getObservers()
onClickGoToMaps(view)
homeFragmentViewModel.loadJsonFromRetrofit()
}
private fun onClickGoToMaps(view: View) {
fbGoToMaps.setOnClickListener {
view.findNavController().navigate(R.id.googleMapsMonumentsFragment)
}
}
private fun getObservers() {
viewModel.getNFCInfo().observe(viewLifecycleOwner, Observer {
when(it.status){
AsyncResult.Status.SUCCESS -> {
NFCProvider.initialize(context, it.data)
}
AsyncResult.Status.ERROR -> {
}
AsyncResult.Status.LOADING -> {
}
}
})
}
override fun onClickFavListener(monuments: MonumentsVO) {
homeFragmentViewModel.updateDatabaseFavoriteCheckFromViewModel(monuments.id, monuments.fav)
}
override fun onClickRowListenerExtras(monuments: MonumentsVO, position: Int, extras: FragmentNavigator.Extras) {
val bundle = bundleOf(BUNDLE_MONUMENT to monuments)
view?.findNavController()?.navigate(R.id.detailsMonumentsFragment, bundle, null, extras)
}
override fun onClickRowListener(monuments: MonumentsVO, position: Int) {}
private fun initViews() {
homeFragmentViewModel = ViewModelProviders.of(this).get(HomeFragmentViewModel::class.java)
rvHomeFragmentMonumentsRetrofit.setHasFixedSize(true)
val layoutManager = LinearLayoutManager(context)
rvHomeFragmentMonumentsRetrofit.layoutManager = layoutManager
}
override fun onResume() {
super.onResume()
(activity as AppCompatActivity).supportActionBar?.show()
}
private fun showDialog(title: String, process: String, titleButton: String) {
val dialog = CustomSuccessDialog(title, process, titleButton)
dialog.show()
}
override onNewIntentResult(intent) {
val message = NFCProvider.retrieveNfcMessage(intent)
if(message) {
showDialog("Correct reading", "1 de 4", "Continue")
} else {
showDialog("Error reading", "1 de 4", "Retry")
}
}
}
As you can see the problem is that when I start the NFC is when I pass the message. I get this message from an internet service and it returns the number of cards that I have to record, which is the one I have to go through, and since on the one hand I have the onNewIntent that I need for the NFC and on the other hand the observer, I don't know how to do it to put it all together and that every time I write an NFC card I get the correct dialogue and when I continue to the next one, the number of the dialogue increases: 1 of 4, 2 of 4, 3 of 4, etc. See if you can give me a hand. Thank you very much.
You can save the instance of the dialog and update your textView in your dialog like this:
if(message) {
if(mDialog?.isShowing == false) {
showDialog("Correct reading", "1 de 4", "Continue")
} else {
mDialog.updateText("$count de 4")
count ++
}
}
And your showDialog method will look something like this:
private fun showDialog(title: String, process: String, titleButton: String) {
mDialog = CustomSuccessDialog(title, process, titleButton)
mDialog.show()
}
I am trying to ask for permissions in an activity (using easyPermissions) when a user clicks on a text input field. My current code looks like this:
class PageFragment : Fragment(), EasyPermissions.PermissionCallbacks {
private lateinit var viewModel: PageViewModel
private lateinit var binding: PageBinding
private lateinit var interpreter: PageInterpreter
private val messagesCoordinator: MessageCoordinator by lazy {
MessageCoordinator(viewModel, interpreter)
}
companion object {
private const val RC_READ_PERMISSIONS = 102
#JvmStatic
fun newInstance() = PageFragment().apply {
arguments = Bundle().apply {
}
}
}
interface PageCallback {
fun goBack()
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
binding = DataBindingUtil.inflate(inflater, R.layout.fragment_page, container, false)
return binding.root
}
override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<out String>, grantResults: IntArray) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
EasyPermissions.onRequestPermissionsResult(requestCode, permissions, grantResults, this)
}
#AfterPermissionGranted(RC_READ_PERMISSIONS)
private fun getPermissions() {
Log.i("Permission asked","Get Permission function activated")
context?.let {
if (EasyPermissions.hasPermissions(it, android.Manifest.permission.READ_CONTACTS)) {
//TODO
} else {
EasyPermissions.requestPermissions(this, it.getString(R.string.fragment_page_explanation), RC_READ_PERMISSIONS,
android.Manifest.permission.READ_CONTACTS)
}
}
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
Log.i("View Created","View has been created")
editName.setOnClickListener {
Log.i("Name","on click listener activated")
this.getPermissions()
}
}
override fun onPermissionsGranted(requestCode: Int, perms: MutableList<String>) {
//TODO
}
override fun onPermissionsDenied(requestCode: Int, perms: MutableList<String>) {
//TODO
}
}
Basically, I have 6 input fields on the fragment layout and I want the permission to be asked if any of the text input fields are click on by the user. Here I am only trying with one.
Thanks,
Feras A.
As far as I understood, if you sure that all EditTexts will behave identically, you can create single instance of clickListener, and set it to EditTexts.
val clickListener = View.OnClickListener {
Log.i("Name","on click listener activated")
this#PageFragment.getPermissions()
}
editName.setOnClickListener(clickListener)
editSomethingElse.setOnClickListener(clickListener)
editSurname.setOnClickListener(clickListener)
Or even create list of similar EditTexts and set listener with following:
listOf(editName, editSomethingElse, editSurname).map {
it.setOnClickListener(clickListener)
}
EDIT
May be this is the case? One more related topic.