Kotlin app using camera does not return picture if camera is never launched before - kotlin

I've stuck for a while with annoying issue with emulator's camera in my app. (The same is repro on a real device)
The camera is used from a dialog fragment to take a picture that later will be uploaded to remote host.
private val cameraPermissionLauncher = registerForActivityResult(
ActivityResultContracts.RequestPermission()
) { isGranted ->
if (isGranted) {
makeImageWithCamera()
} else {
dismiss()
}
}
private val cameraLauncher = registerForActivityResult(
ActivityResultContracts.TakePicture()
) { isSuccess ->
if (isSuccess) {
path?.let { path ->
viewModel.setPhoto(path = path)
}
}
dismiss()
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
subscribeFlow(viewModel.action) { dismiss() }
with(binding) {
buttonCamera.setOnClickListener { useCamera() }
buttonCancel.setOnClickListener { dismiss() }
}
}
private fun useCamera() {
val isGranted = ContextCompat.checkSelfPermission(requireContext(), PERM_CAMERA) ==
PackageManager.PERMISSION_GRANTED
if (!isGranted) {
cameraPermissionLauncher.launch(PERM_CAMERA)
} else {
makeImageWithCamera()
}
}
private fun makeImageWithCamera() {
try {
val photoFileName = "photo${id}"
val photosDir = requireContext()
.getExternalFilesDir(Environment.DIRECTORY_PICTURES)
val photoFile = File.createTempFile(photoFileName, ".jpg", photosDir)
val photoUri: Uri = FileProvider.getUriForFile(
requireContext(),
"app.provider",
photoFile
)
path = photoFile.absolutePath
cameraLauncher.launch(photoUri)
} catch (err : IOException) {
Log.e(TAG, err.stackTraceToString())
}
}
The issue happens only in situation when camera itself is never launched on a device:
Inside my app I press camera button
Receive a request for camera usage permission from my app and accept it.
Camera app is starting and requests its own permission to access geolocation (it wants to add location-based tags to photos) - at this point it is irrelevant wether to accept or deny it.
Camera enters the scene. Now if I press shot button it makes a photo and stays in the scene allowing to make infinite amount of photos, as if I've launched Camera app in regular way.
If I use back button to return to my app - nothing have change at all, dialog is just dismisses.
If I launch the same second time, or just launch Camera app itself and accept/deny permissions request from it, all is fixed. Camera takes exactly one shot and allows me to accept it, after that it comes back to my app and processes received photo.
So while for the user experience this behaviour can be just annoying, it with a high probability makes the Firebase robotests to fail on this point.
What could be the possible root of a problem?

Related

Store full-size images using a FileProvider

I want to save an image in its full size. I use a file provider for this. I can take the picture, but I don't get any data in the onActivityResult function. The intent is null.
What am I doing wrong?
provider_path.xml:
<?xml version="1.0" encoding="utf-8"?>
<paths xmlns:android="http://schemas.android.com/apk/res/android">
<root-path name="root" path="." />
</paths>
provider in AndroidManifest.xml:
<provider
android:name="androidx.core.content.FileProvider"
android:authorities="com.bkoubik.longlesstime.fileprovider"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="#xml/provider_path"/>
</provider>
My Fragment Class:
fun capturePhoto(){
val intent = Intent(MediaStore.ACTION_IMAGE_CAPTURE)
val fileUri:File = createImageFile();
val photoURI = FileProvider.getUriForFile(
requireActivity(),
"com.bkoubik.longlesstime.fileprovider",
fileUri
)
intent.putExtra( MediaStore.EXTRA_OUTPUT, photoURI )
startActivityForResult(intent,IMAGE_REQUEST)
}
private fun createImageFile(): File {
val wrapper = ContextWrapper(requireContext())
var photoFile = wrapper.getDir(IMAGE_DIRECTORY,Context.MODE_PRIVATE)
photoFile = File(photoFile,"${UUID.randomUUID()}.jpg")
return photoFile
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
if(requestCode == IMAGE_REQUEST && resultCode == Activity.RESULT_OK && data != null)
{
//data = null
val imgData = data.data!!
...
}
}
companion object {
private val IMAGE_DIRECTORY = "long_less"
}
Error:
java.lang.IllegalArgumentException: Failed to find configured root that contains /data/data/com.bkoubik.longlesstime/app_long_less/1219a0bd-c17e-4709-b880-1e4f549362f6.jpg
First of all, I'll strongly recommend you to use new contracts API which is also recommended by Google:
While the underlying startActivityForResult() and onActivityResult() APIs are available on the Activity class on all API levels, it is strongly recommended to use the Activity Result APIs introduced in AndroidX Activity and Fragment. (see Android docs)
Than, when photo was taken from GUI, I'm getting photo data from provider like this (it's my separate camera request Activity). Also, I'm using external cache directory because on my Android 10.0 device data folder is not accessible (see Android docs)
P.S. Take a note about grantUriPermission method call on photo URI
// URI for photo file
private lateinit var mPhotoUri : Uri
/// Photo file itself
private lateinit var mPhotoFile : File
// Camera permission request
private val mRequestCamera = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
// Camera permission request parse
when (result.resultCode) {
// Success
RESULT_OK -> {
// Photo request
mRequestPhoto.launch(mPhotoUri)
}
// No permisison
else -> {
// Close activity
finish()
}
}
}
// Photo request
private val mRequestPhoto = registerForActivityResult(ActivityResultContracts.TakePicture()) { result ->
// Check request result
when (result) {
// Success
true -> {
// Photo from media stream
val photoData = BitmapFactory.decodeStream(
contentResolver.openInputStream(mPhotoUri)
)
// Stream for photo save
val dataStream = ByteArrayOutputStream()
// Compress photo as JPEG of 90% quality
photoData.compress(
Bitmap.CompressFormat.JPEG,
90,
dataStream
)
// Coroutine to write photo
CoroutineScope(Dispatchers.Main).launch {
// Inside IO context
withContext(Dispatchers.IO) {
// File output stream
FileOutputStream(mPhotoFile).also { fileStream ->
// Write JPEG image data to file
fileStream.write(dataStream.toByteArray())
// Close JPEG image data stream
dataStream.close()
// Close file stream
fileStream.close()
}
}
}
// Done here - notify someone photo is ready
sendBroadcast(
Intent(
<Where/Whom to send>
).apply {
// Content provider URI
putExtra(
<Photo URI extra name>,
mPhotoUri
)
}
)
// Close activity
finish()
}
// Error
else -> {
// Close activity
finish()
}
}
}
// Activity onCreate method
#Override
override fun onCreate(savedInstanceState: Bundle?) {
// Parent method call
super.onCreate(savedInstanceState)
// Try
try {
// Create temporary file
mPhotoFile = File.createTempFile(
"<file prefix>",
"<file suffix>",
externalCacheDir
).apply {
// Make it writable
setWritable(true)
}
// Get file URI (Android M+ ?)
mPhotoUri = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
// Trying to get provider (I send it to this Activity via Intent with extra "EXTRA_AUTHORITY")
intent?.extras?.getString("EXTRA_AUTHORITY")
?.let { authority ->
// For Android 7.0+ getting FileProvider
FileProvider.getUriForFile(
applicationContext,
authority,
mPhotoFile
)
// Else for Android 7.0 or lower
} ?: Uri.fromFile(mPhotoFile)
} else {
// Else for Android 7.0 or lower
Uri.fromFile(mPhotoFile)
}
// Permissions for URI itself
grantUriPermission(
packageName,
mPhotoUri,
Intent.FLAG_GRANT_WRITE_URI_PERMISSION or Intent.FLAG_GRANT_READ_URI_PERMISSION
)
// Catch
} catch (e: Exception) {
// Stacktrace to logcat
e.printStackTrace()
// Close camera activity
finish()
}
// No camera permissions ?
if (!ActivityPermissionsCamera.hasPermissions(applicationContext)) {
// Intent for camera permissions
val permissionsIntent = Intent(
applicationContext,
<Permissions for Camera Activity>::class.java
).apply {
// New task
flags = Intent.FLAG_ACTIVITY_NEW_TASK
}
// Request permissions
mRequestCamera.launch(permissionsIntent)
} else {
// Camera launch
mRequestPhoto.launch(mPhotoUri)
}
}
And this is how I reques photo from camera:
// Photo pending intent
private fun createPendingIntentPhoto() : PendingIntent? {
// Runtime check - any camera ?!
if (!applicationContext.packageManager.hasSystemFeature(PackageManager.FEATURE_CAMERA_ANY)) {
// No camera - no photo !!!
return null
}
// Intent flags
val intentFlags = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
PendingIntent.FLAG_IMMUTABLE or
PendingIntent.FLAG_UPDATE_CURRENT
} else {
PendingIntent.FLAG_UPDATE_CURRENT
}
// Pending intent
return PendingIntent.getActivity(
applicationContext,
1,
Intent(
applicationContext,
<Activity for Camera>::class.java
).apply {
// Pass provider as Intent extra
putExtra("EXTRA_AUTHORITY", "${BuildConfig.APPLICATION_ID}.provider")
},
intentFlags
)
}
P.P.S. Quick note on why do I pass provider URI over Intent's extra - my camera routines are in separate library which is used in GUI-project So they can have different BuildConfig.APPLICATION_ID. This allows me to use same library in many projects.

How to set Double back press Exit in Jetpack Compose?

There are some tutorials on YouTube about how to set double back press exit in XML android, but most of them are in JAVA and None of them are in Jetpack Compose.
So how can we set that Double back press in Jetpack Compose?
I mean that thing that ask us in a Toast to press back again if we are sure to Exit. Thanks for help
This sample shows Toast on first touch and waits for 2 seconds to touch again to exit app otherwise goes back to Idle state.
sealed class BackPress {
object Idle : BackPress()
object InitialTouch : BackPress()
}
#Composable
private fun BackPressSample() {
var showToast by remember { mutableStateOf(false) }
var backPressState by remember { mutableStateOf<BackPress>(BackPress.Idle) }
val context = LocalContext.current
if(showToast){
Toast.makeText(context, "Press again to exit", Toast.LENGTH_SHORT).show()
showToast= false
}
LaunchedEffect(key1 = backPressState) {
if (backPressState == BackPress.InitialTouch) {
delay(2000)
backPressState = BackPress.Idle
}
}
BackHandler(backPressState == BackPress.Idle) {
backPressState = BackPress.InitialTouch
showToast = true
}
}

Document references must have an even number of segments

Error: Document references must have an even number of segments, but Users has 1
I have been looking through different posts on here and on different forums but all have the problem when first loading but my problem is after I logout or reset the password. When I load the contents from firebase I get the information but when I click on the sign out then go to login again it crash's and I get this error. I have logged the users.uid and Document references and does not change after logging out.
My collection path is done with Constants so I don't have a mis type.
I have found that the error is in the Fragment side of my app in the FirestoreClass().loadUserData_fragment(this)
As commenting this line out after the log out will allow the app to run but in the activity the data can still be loaded as the activity load data and the fragment is the same so I don't get why it wouldn't load into the fragment after the sign out but will load first time.
Fragment
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
FirestoreClass().loadUserData_fragment(this)
}
Activity
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityUpdateProfileBinding.inflate(layoutInflater)
val view : LinearLayout = binding.root
setContentView(view)
setupActionBar()
FirestoreClass().loadUserData(this)
}
GetCurrentUserID
fun getCurrentUserID():String{
// auto login
var currentUser = FirebaseAuth.getInstance().currentUser
var currentUserId = ""
if (currentUser != null){
currentUserId = currentUser.uid
Log.i("uis",currentUser.uid)
}
return currentUserId
}
Activity version
fun loadUserData(activity:Activity){
mFireStore.collection(Constants.USERS)
.document(getCurrentUserID())
.get()
.addOnSuccessListener { document ->
val loggedInUser = document.toObject(User::class.java)!!
Log.i("uis",getCurrentUserID() + Constants.USERS)
when(activity){
is UpdateProfileActivity ->{
activity.setUserDataInUI(loggedInUser)
}
is LoginActivity -> {
// Call a function of base activity for transferring the result to it.
activity.userLoggedInSuccess(loggedInUser)
}
}
}
}
Fragment version
fun loadUserData_fragment(fragment: Fragment){
mFireStore.collection(Constants.USERS)
.document(getCurrentUserID())
.get()
.addOnSuccessListener { document ->
val loggedInUser = document.toObject(User::class.java)!!
Log.i("uis",getCurrentUserID() + Constants.USERS)
when(fragment){
is HomeFragment ->{
fragment.setUserDataInUIFragment(loggedInUser)
}
}
}
}
It seems that your getCurrentUserID() returns no value, which you're not handling in your code. The best option is to only call loadUserData when there is an active user, but alternatively you can also check whether getCurrentUserID() returns a value:
fun loadUserData(activity:Activity){
if (getCurrentUserID() != "") { // 👈
mFireStore.collection(Constants.USERS)
.document(getCurrentUserID())
.get()
.addOnSuccessListener { document ->
...
}
}
}

Kotlin: Is it possible to make a function, which calls a retrofit service, to return a String value?

I have a Fragment and a View Model.
The layout of the Fragment contains a button.
When the button is clicked, we try to get an API response, which contains a url.
That url is used to start an intent to open a web page.
I am currently accomplishing this with event driven programming.
The button in the Fragment is clicked.
The function in the view model is called to get the API response, which contains the url.
The url in the view model is assigned as live data, which is observed in the fragment.
The fragment observes the url live data has changed. It attempts to launch the WebView with the new url.
Can the Fragment skip Observing for the url and directly get the ViewModel function to return a string?
Here is the code for the Fragment:
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
// Set the OnClickListener
myButton.setOnClickListener {
myViewModel.getUrlQueryResults()
}
// Observables to open WebView from Url
myViewModel.myUrl.observe(viewLifecycleOwner, Observer {
it?.let{
if (it.isEmpty()) {
// No Url found in this API response
}
else {
// Open the WebView
try {
val intent = Intent(Intent.ACTION_VIEW, Uri.parse(it))
startActivity(intent)
}
catch (e: Exception) {
// Log the catch statement
}
}
}
})
}
Here is the code for the ViewModel:
// Live data observed in fragment. When this changes, fragment will attempt to launch Website with the url
private val _myUrl = MutableLiveData<String>()
val myUrl: LiveData<String>
get() = _myUrl
// Possible to make this return a string?
fun getUrlQueryResults() {
InfoQueryApi.retrofitService.getInfo(apiKey).enqueue(object : Callback<String> {
override fun onResponse(call: Call<String>, response: Response<String>) {
try {
// Store the response here
apiResponse = parseInfoJsonResult(JSONObject(response.body()!!))
// Grab the url from the response
var urlFromResponse = apiResponse?.url
if (urlFromResponse.isNullOrEmpty()) {
urlFromResponse = ""
}
// Store the urlFromResponse in the live data so Fragment can Observe and act when the value changes
_myUrl.value = urlFromResponse
} catch (e: Exception) {
// Log catch statement
}
}
override fun onFailure(call: Call<String>, t: Throwable) {
// Log error
}
})
}

Avoid fragment recreation when opening from notification navigation component

I want when I click on a notification to open a fragment and not recreate it. I am using navigation component and using NavDeepLinkBuilder
val pendingIntent = NavDeepLinkBuilder(this)
.setComponentName(MainActivity::class.java)
.setGraph(R.navigation.workouts_graph)
.setDestination(R.id.workoutFragment)
.createPendingIntent()
My case is I have a fragment and when you exit the app, there is a notification which when you click on it, it should return you to that same fragment. Problem is every time i click on it it's creating this fragment again, I don't want to be recreated.
I had the same issue. Looks like there is not an option to use the NavDeepLinkBuilder without clearing the stack according to the documentation
I'm not sure the exact nature of your action, but I'll make two assumptions:
You pass the destination id to your MainActivity to navigate.
Your MainActivity is using ViewBinding and has a NavHostFragment
You will have to create the pending intent like:
val intent = Intent(this, MainActivity::class.java).apply {
flags = Intent.FLAG_ACTIVITY_SINGLE_TOP
putExtra("destination", R.id.workoutFragment)
}
val pendingIntent = PendingIntent.getActivity(this, 0, intent, 0);
And in your MainActivity, you can handle both cases (app was already open, app was not already open)
override fun onStart() {
super.onStart()
// called when application was not open
intent?.let { processIntent(it) }
}
override fun onNewIntent(intent: Intent?) {
super.onNewIntent(intent)
// called when application was open
intent?.let { processIntent(it) }
}
private fun processIntent(intent: Intent) {
intent.extras?.getInt("destination")?.let {
intent.removeExtra("destination")
binding.navHostFragment.findNavController().navigate(it)
}
}