I'm having a problem in my app when I create an intent to launch the camera app to take a picture, my app crashes and I receive the following error:
2021-06-11 18:07:46.914 7506-7506/com.package.app E/JavaBinder: !!! FAILED BINDER TRANSACTION !!! (parcel size = 14763232)
...
2021-06-11 18:07:49.567 7506-7506/com.package.app E/AndroidRuntime: FATAL EXCEPTION: main
Process: com.package.app, PID: 7506
java.lang.RuntimeException: android.os.TransactionTooLargeException: data parcel size 14763232 bytes
at android.app.servertransaction.PendingTransactionActions$StopInfo.run(PendingTransactionActions.java:161)
at android.os.Handler.handleCallback(Handler.java:883)
at android.os.Handler.dispatchMessage(Handler.java:100)
at android.os.Looper.loop(Looper.java:214)
at android.app.ActivityThread.main(ActivityThread.java:7356)
at java.lang.reflect.Method.invoke(Native Method)
at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:492)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:930)
Caused by: android.os.TransactionTooLargeException: data parcel size 14763232 bytes
at android.os.BinderProxy.transactNative(Native Method)
at android.os.BinderProxy.transact(BinderProxy.java:510)
at android.app.IActivityTaskManager$Stub$Proxy.activityStopped(IActivityTaskManager.java:4524)
at android.app.servertransaction.PendingTransactionActions$StopInfo.run(PendingTransactionActions.java:145)
at android.os.Handler.handleCallback(Handler.java:883)
at android.os.Handler.dispatchMessage(Handler.java:100)
at android.os.Looper.loop(Looper.java:214)
at android.app.ActivityThread.main(ActivityThread.java:7356)
at java.lang.reflect.Method.invoke(Native Method)
at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:492)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:930)
The photos, after they are taken in the camera app and returned to my app, are saved in a Room database. Interestingly, the problem only happens if there is a photo already saved in the row of the database that I'm trying to add/replace a photo. When creating a new row or taking a photo in a row which has no picture, I'm able to take the photo and save it into my database without any problem.
My Room database has a TypeConverter which converts bitmaps into base64 string for storage in the database and back to a bitmap when required to view it. After playing with the code for a while, I tried removing the Converter from the database and implementing it's functions into my viewmodel and fragment. The app works now regardless of whether there is a picture being replaced or not.
I now suspect something to be wrong with how I implemented the Converter but I'm not sure what it could be. Please look at my code below.
Fragment
lateinit var currentPhotoPath: String
@AndroidEntryPoint
class Fragment : Fragment(R.layout.fragment) {
private val viewModel: ViewModel by viewModels()
private var _binding: FragmentBinding? = null
private val binding get() = _binding!!
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
_binding = FragmentBinding.inflate(inflater, container, false)
return binding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
binding.apply {
ivPicture.setImageBitmap(viewModel.entryPictures)
fab.setOnClickListener {
viewModel.onSaveClick()
}
}
viewLifecycleOwner.lifecycleScope.launchWhenStarted {
viewModel.event.collect { event ->
when (event) {
ViewModel.Event.NavigateToPhotoActivity -> {
dispatchTakePictureIntent()
}
}
}
}
setHasOptionsMenu(true)
}
private val REQUEST_IMAGE_CAPTURE = 23
private fun dispatchTakePictureIntent() {
Intent(MediaStore.ACTION_IMAGE_CAPTURE).also { takePictureIntent ->
val packageManager = requireContext().packageManager
takePictureIntent.resolveActivity(packageManager)?.also {
val photoFile: File? = try {
createImageFile()
} catch (ex: IOException) {
Toast.makeText(activity, "Error Creating File", Toast.LENGTH_LONG).show()
null
}
photoFile?.also {
val photoURI: Uri = FileProvider.getUriForFile(
requireContext(),
"com.package.app.fileprovider",
it
)
takePictureIntent.putExtra(MediaStore.EXTRA_OUTPUT, photoURI)
requireActivity().startActivityFromFragment(this, takePictureIntent, REQUEST_IMAGE_CAPTURE)
}
}
}
}
@Throws(IOException::class)
private fun createImageFile(): File {
val timeStamp: String = SimpleDateFormat("yyyyMMdd_HHmmss").format(Date())
val storageDir: File? = context?.getExternalFilesDir(Environment.DIRECTORY_PICTURES)
return File.createTempFile(
"JPEG_${timeStamp}_", /* prefix */
".jpg", /* suffix */
storageDir /* directory */
).apply {
// Save a file: path for use with ACTION_VIEW intents
currentPhotoPath = absolutePath
}
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
if (requestCode == REQUEST_IMAGE_CAPTURE && resultCode == Activity.RESULT_OK) {
lifecycleScope.launch {
val takenImage = BitmapFactory.decodeFile(currentPhotoPath)
viewModel.onPhotoRetrieved(takenImage)
binding.ivPicture.apply {
visibility = View.VISIBLE
setImageBitmap(takenImage)
}
}
} else {
Toast.makeText(activity, "Error Retrieving Image", Toast.LENGTH_LONG).show()
}
}
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
inflater.inflate(R.menu.menu_fragment, menu)
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
return when (item.itemId) {
R.id.icon_photo -> {
viewModel.onTakePhotoSelected()
true
}
else -> super.onOptionsItemSelected(item)
}
}
override fun onDestroyView() {
super.onDestroyView()
_binding = null
}
}
ViewModel:
@HiltViewModel
class ViewModel @Inject constructor(
private val dao: EntryDao,
private val state: SavedStateHandle
) : ViewModel() {
val entry = state.get<Entry>("entry")
var entryPictures = entry?.pictures
private val eventChannel = Channel<Event>()
val event = eventChannel.receiveAsFlow()
fun onSaveClick() {
if (entry != null) {
val updatedEntry = entry.copy(
pictures = entryPictures
)
updatedEntry(updatedEntry)
} else {
val newEntry = Entry(
pictures = entryPictures
)
createEntry(newEntry)
}
}
private fun createEntry(entry: Entry) = viewModelScope.launch {
dao.insert(entry)
}
private fun updatedEntry(entry: Entry) = viewModelScope.launch {
dao.update(entry)
}
fun onTakePhotoSelected() = viewModelScope.launch {
eventChannel.send(Event.NavigateToPhotoActivity)
}
fun onPhotoRetrieved(bitmap: Bitmap) = viewModelScope.launch {
entryPictures = bitmap
}
sealed class Event {
object NavigateToPhotoActivity : Event()
}
}
Database:
@Database(entities = [Entry::class], version = 1)
@TypeConverters(Converters::class)
abstract class Database : RoomDatabase() {
abstract fun entryDao(): EntryDao
class Callback @Inject constructor(
private val database: Provider<com.mayuram.ascend.data.Database>,
@ApplicationScope private val applicationScope: CoroutineScope
) : RoomDatabase.Callback() {
override fun onCreate(db: SupportSQLiteDatabase) {
super.onCreate(db)
val dao = database.get().entryDao()
applicationScope.launch {
dao.insert(Entry(null))
dao.insert(Entry(null))
dao.insert(Entry(null))
dao.insert(Entry(null))
}
}
}
}
Converter
class Converters {
@Suppress("DEPRECATION")
@TypeConverter
fun bitmapToString(bitmap: Bitmap?): String {
val outputStream = ByteArrayOutputStream()
if (android.os.Build.VERSION.SDK_INT >= 30) {
bitmap?.compress(Bitmap.CompressFormat.WEBP_LOSSY, 50, outputStream)
} else {
bitmap?.compress(Bitmap.CompressFormat.WEBP, 50, outputStream)
}
val imageBytes: ByteArray = outputStream.toByteArray()
return Base64.encodeToString(imageBytes, Base64.DEFAULT)
}
@TypeConverter
fun stringToBitmap(string: String): Bitmap? {
val imageBytes: ByteArray = Base64.decode(string, Base64.DEFAULT)
return BitmapFactory.decodeByteArray(imageBytes, 0, imageBytes.size)
}
}
DataClass:
@Entity(tableName = "entry_table")
@Parcelize
data class Entry(
val pictures: Bitmap?,
@PrimaryKey(autoGenerate = true) val id: Int = 0
) : Parcelable
Changes I made to get it to work:
In ViewModel, modified onPhotoRetrieved function to convert image to string
fun onPhotoRetrieved(bitmap: Bitmap) = viewModelScope.launch {
val outputStream = ByteArrayOutputStream()
if (android.os.Build.VERSION.SDK_INT >= 30) {
bitmap.compress(Bitmap.CompressFormat.WEBP_LOSSY, 50, outputStream)
} else {
bitmap.compress(Bitmap.CompressFormat.WEBP, 50, outputStream)
}
val imageBytes: ByteArray = outputStream.toByteArray()
val result = Base64.encodeToString(imageBytes, Base64.DEFAULT)
entryPictures = result
}
In fragment, added the convert string to bitmap function in onViewCreated
val imageBytes: ByteArray = Base64.decode(viewModel.entryPictures.toString(), Base64.DEFAULT)
val result = BitmapFactory.decodeByteArray(imageBytes, 0, imageBytes.size)
ivPicture.setImageBitmap(result)
Also changed type of val pictures to String? instead of Bitmap? in my data class and commented out @TypeConverters in my Database.