First I gave the exerciseList a null value then inside the lifecycleScope I feched data from a preloaded database but why am I still getting null pointer exception on calling the exerciseList variable?
private var exerciseList: ArrayList<Exercise>? = null
lifecycleScope.launch {
exerciseList = getExerciseListBySetName(exListName!!)
setUpUiAndExerciseStatusRV()
}
private suspend fun getExerciseListBySetName(exListName: String): ArrayList<Exercise> {
val allExerciseList = Constants.getAllExerciseList()
var ans = ArrayList<Exercise>()
when (exListName) {
Constants.MISCELLANEOUS_LIST -> {
val miscellaneousExercisesDao = (application as HelloHealthyApp).miscellaneousExercisesDb.miscellaneousExercisesDao()
miscellaneousExercisesDao.fetchAllMiscellaneousExercises().collect {
val list = ArrayList(it)
for (i in list) {
ans.add(allExerciseList[i.toInt()])
}
}
}
else -> {
ans = ArrayList()
}
}
for(exercise in ans) {
exercise.setIsSelected(false)
exercise.setIsCompleted(false)
}
return ans
}
private fun setUpUiAndExerciseStatusRV() {
var i = 0
while (i < exerciseList?.size!!) {
exercisesArrStr = if (i == exerciseList?.size!! - 1) {
"$exercisesArrStr${exerciseList?.get(i)?.getId().toString()}"
} else {
"$exercisesArrStr${exerciseList?.get(i)?.getId().toString()},"
}
i++
}
exerciseRVAdapter = ExerciseStatusAdapter(exerciseList!!)
binding?.rVExerciseStatus?.layoutManager =
LinearLayoutManager(this#ExerciseActivity, LinearLayoutManager.HORIZONTAL, false)
binding?.rVExerciseStatus?.adapter = exerciseRVAdapter
exerciseRVAdapter?.notifyDataSetChanged()
binding?.exerciseImgView?.setImageResource(
exerciseList?.get(currentExercisePosition)?.getImage()!!
)
binding?.tVTitle?.text = exerciseList?.get(currentExercisePosition)?.getName()
binding?.upcomingExerciseTextView?.visibility = View.GONE
}
Related
I need to fetch data inside WhatsApp folders on External Storage.
As i am targeting API Level 30 i am no longer able to access WhatsApp folders on External Storage. I have implemented Storage Access Framework and got Android/media folder Uri and Document File. And using listFiles() i am able to list files but with filter() and sortedByDescending() functions it becomes very slow.
What i have tried?
Used Cursor loader with Projection and Selection Arguments but it only
worked for non hidden folders like WhatsApp Images and WhatsApp Videos
It returns empty cursor for hidden folder .Statuses
Tried replacing MediaStore.Video.Media.EXTERNAL_CONTENT_URI with MediaStore.Video.Media.getContentUri(MediaStore.VOLUME_EXTERNAL)
What is required?
List images and videos from .Statuses folder same as i am listing WhatsApp Images using Media Store in HomeActivity.java
Below is my code
In this activity i get permision to Android/media and set all WhatsApp folders URIs for status fetching and other use, but fetched WhatsApp Images with projection and selection from WhatsApp Images folder
class HomeActivity : AppCompatActivity(), InternetListener, PurchasesUpdatedListener,
CoroutineScope {
private val exceptionHandler = CoroutineExceptionHandler { context, exception ->
Toast.makeText(this, exception.message, Toast.LENGTH_LONG).show()
}
private val dataRepository: DataRepository by inject()
val tinyDB: TinyDB by inject()
val REQUEST_CODE = 12123
init {
newNativeAdSetUp = null
}
val sharedViewModel by viewModel<SharedViewModel>()
val viewModel by viewModel<HomeViewModel>()
val handler = CoroutineExceptionHandler { _, exception ->
Log.d("CoroutineException", "$exception handled !")
}
override val coroutineContext: CoroutineContext
get() = Dispatchers.Main + job + handler
private lateinit var job: Job
val sdk30PermissionListener = object : PermissionListener {
override fun onPermissionGranted() {
openDocumentTree()
}
override fun onPermissionDenied(deniedPermissions: MutableList<String>?) {
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_home)
handlePermissionsByVersion()
}
private fun handlePermissionsByVersion() {
if (SDK_INT >= Build.VERSION_CODES.R) {
if ((ContextCompat.checkSelfPermission(
this,
Manifest.permission.WRITE_EXTERNAL_STORAGE
)
== PackageManager.PERMISSION_GRANTED) && (ContextCompat.checkSelfPermission(
this,
Manifest.permission.READ_EXTERNAL_STORAGE
)
== PackageManager.PERMISSION_GRANTED)
) {
//if granted load whatsapp images and some uris setup to viewmodel
loadWhatsAppImages()
if (arePermissionsGranted()) {
if (dataRepository.mrWhatsAppImages == null || dataRepository.mrWhatsAppBusinessImages == null) {
setUpWAURIs()
}
}
} else {
TedPermission.with(this)
.setPermissionListener(sdk30PermissionListener)
.setDeniedMessage("If you reject permission,you can not use this service\n\nPlease turn on permissions at [Setting] > [Permission]")
.setPermissions(
Manifest.permission.WRITE_EXTERNAL_STORAGE,
Manifest.permission.READ_EXTERNAL_STORAGE
)
.check()
}
}
}
override fun onActivityResult(requestCode: Int, resultCode: Int, #Nullable data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
if (resultCode == RESULT_OK && requestCode == REQUEST_CODE) {
if (data != null) {
//this is the uri user has provided us
val treeUri: Uri? = data.data
if (treeUri != null) {
sharedViewModel.treeUri = treeUri
val decoded = Uri.decode(treeUri.toString())
Log.i(LOGTAG, "got uri: ${treeUri.toString()}")
// here we should do some checks on the uri, we do not want root uri
// because it will not work on Android 11, or perhaps we have some specific
// folder name that we want, etc
if (Uri.decode(treeUri.toString()).endsWith(":")) {
showWrongFolderSelection()
return
}
if (!decoded.equals(Constants.WHATSAPP_MEDIA_URI_DECODED)) {
showWrongFolderSelection()
return
}
// here we ask the content resolver to persist the permission for us
val takeFlags: Int = Intent.FLAG_GRANT_READ_URI_PERMISSION or
Intent.FLAG_GRANT_WRITE_URI_PERMISSION
contentResolver.takePersistableUriPermission(
treeUri,
takeFlags
)
val treeUriAsString = treeUri.toString()
tinyDB.putString("FOLDER_URI", treeUriAsString)
if (SDK_INT >= Build.VERSION_CODES.R) {
setupPaths()
}
}
}
}
}
private fun setupPaths() {
setUpOverlay()
fetchWhatsAppRootURIs(
this,
sharedViewModel,
dataRepository,
tinyDB
) {
fetchWhatsAppBusinessRootURIs(
this,
sharedViewModel,
dataRepository,
tinyDB
) {
tinyDB.putBoolean("WARootPathsDone", true)
removeOverlay()
}
}
}
override fun onDestroy() {
dialogHandler.removeCallbacksAndMessages(null)
super.onDestroy()
}
val loadmanagerImages = object : LoaderManager.LoaderCallbacks<Cursor> {
val whatsAppImagesArrayList = arrayListOf<File>()
override fun onCreateLoader(id: Int, args: Bundle?): Loader<Cursor> {
var location: File = File(
Environment.getExternalStorageDirectory()
.toString() + Constants.whatsapp_images_path
)
if (!location.exists()) {
location = File(
Environment.getExternalStorageDirectory()
.toString() + Constants.whatsapp_images_path11
)
}
if (location != null && location.exists()) {
whatsAppImagesArrayList.clear()
Timber.e("checkLoaded-onCreateLoader $id")
if (id == 0) {
var folder = location.absolutePath
val projection = arrayOf(
MediaStore.MediaColumns.DATA,
MediaStore.MediaColumns.DATE_MODIFIED
)
val selection = MediaStore.Images.Media.DATA + " like ? "
val selectionArgs: String = "%$folder%"
return CursorLoader(
this#HomeActivity,
MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
projection,
selection,
arrayOf(selectionArgs),
"${MediaStore.Images.Media.DATE_MODIFIED} DESC"
)
}
}
return null!!
}
override fun onLoadFinished(loader: Loader<Cursor>, cursor: Cursor?) {
Timber.e("checkLoaded-onLoadFinished")
var absolutePathOfImage: String
if (loader.id == 0) {
cursor?.let {
val columnIndexData = cursor.getColumnIndexOrThrow(MediaStore.MediaColumns.DATA)
GlobalScope.launch(Dispatchers.Main + exceptionHandler) {
async(Dispatchers.IO + exceptionHandler) {
while (!cursor.isClosed && cursor.moveToNext() == true) {
absolutePathOfImage = cursor.getString(columnIndexData!!)
whatsAppImagesArrayList.add(File(absolutePathOfImage))
}
}.await()
LoaderManager.getInstance(this#HomeActivity).destroyLoader(0)
Timber.e("checkLoaded-Completion")
galleryViewModel.whatsAppImagesList.postValue(whatsAppImagesArrayList)
}
}
}
}
override fun onLoaderReset(loader: Loader<Cursor>) {
}
}
fun loadWhatsAppImages() {
try {
tinyDB.putBoolean("whatsAppMediaLoadCalled", true)
LoaderManager.getInstance(this).initLoader(
0,
null,
loadmanagerImages
)
} catch (e: RuntimeException) {
Log.e("exVideos ", "ex : ${e.localizedMessage}")
}
}
companion object {
const val ANDROID_DOCID = "primary:Android/media/"
const val EXTERNAL_STORAGE_PROVIDER_AUTHORITY = "com.android.externalstorage.documents"
private val androidUri = DocumentsContract.buildDocumentUri(
EXTERNAL_STORAGE_PROVIDER_AUTHORITY, ANDROID_DOCID
)
val androidTreeUri = DocumentsContract.buildTreeDocumentUri(
EXTERNAL_STORAGE_PROVIDER_AUTHORITY, ANDROID_DOCID
)
}
private fun openDocumentTree() {
val uriString = tinyDB.getString("FOLDER_URI", "")
when {
uriString == "" -> {
Log.w(LOGTAG, "uri not stored")
askPermission()
}
arePermissionsGranted() -> {
}
else -> {
Log.w(LOGTAG, "uri permission not stored")
askPermission()
}
}
}
// this will present the user with folder browser to select a folder for our data
private fun askPermission() {
val intent = Intent(Intent.ACTION_OPEN_DOCUMENT_TREE)
intent.putExtra(DocumentsContract.EXTRA_INITIAL_URI, androidUri)
startActivityForResult(intent, REQUEST_CODE)
}
private fun arePermissionsGranted(): Boolean {
var uriString = tinyDB.getString("FOLDER_URI", "")
val list = contentResolver.persistedUriPermissions
for (i in list.indices) {
val persistedUriString = list[i].uri.toString()
if (persistedUriString == uriString && list[i].isWritePermission && list[i].isReadPermission) {
return true
}
}
return false
}
private fun showWrongFolderSelection() {
val layoutInflaterAndroid = LayoutInflater.from(this)
val mView = layoutInflaterAndroid.inflate(R.layout.layout_dialog_wrong_folder, null)
val builder = AlertDialog.Builder(this, R.style.ThemePageSearchDialog)
builder.setView(mView)
val alertDialog = builder.show()
alertDialog.setCancelable(false)
val btnOk = mView.findViewById(R.id.tvExit) as TextView
val tvCancel = mView.findViewById(R.id.tvCancel) as TextView
btnOk.setOnClickListener {
alertDialog.dismiss()
openDocumentTree()
}
tvCancel.setOnClickListener {
alertDialog.dismiss()
}
}
private fun setUpWAURIs() {
dataRepository.mrWhatsAppImages =
getDocumentFileFromStringURI(
this,
tinyDB.getString("mrWhatsAppImages")
)
dataRepository.mrWhatsAppVN =
getDocumentFileFromStringURI(
this,
tinyDB.getString("mrWhatsAppVN")
)
dataRepository.mrWhatsAppDocs =
getDocumentFileFromStringURI(
this,
tinyDB.getString("mrWhatsAppDocs")
)
dataRepository.mrWhatsAppVideo =
getDocumentFileFromStringURI(
this,
tinyDB.getString("mrWhatsAppVideo")
)
dataRepository.mrWhatsAppAudio =
getDocumentFileFromStringURI(
this,
tinyDB.getString("mrWhatsAppAudio")
)
dataRepository.WhatsAppStatuses =
getDocumentFileFromStringURIStatuses(
this,
tinyDB.getString("WhatsAppStatuses")
)
dataRepository.mrWhatsAppBusinessImages =
getDocumentFileFromStringURI(
this,
tinyDB.getString("mrWhatsAppBusinessImages")
)
dataRepository.mrWhatsAppBusinessVN =
getDocumentFileFromStringURI(
this,
tinyDB.getString("mrWhatsAppBusinessVN")
)
dataRepository.mrWhatsAppBusinessDocs =
getDocumentFileFromStringURI(
this,
tinyDB.getString("mrWhatsAppBusinessDocs")
)
dataRepository.mrWhatsAppBusinessVideo =
getDocumentFileFromStringURI(
this,
tinyDB.getString("mrWhatsAppBusinessVideo")
)
dataRepository.mrWhatsAppBusinessAudio =
getDocumentFileFromStringURI(
this,
tinyDB.getString("mrWhatsAppBusinessAudio")
)
dataRepository.WhatsAppBusinessStatuses =
getDocumentFileFromStringURIStatuses(
this,
tinyDB.getString("WhatsAppBusinessStatuses")
)
}
fun setUpOverlay() {
val dialogfragment = FullScreenLoadingDialog()
dialogfragment.isCancelable = false
dialogfragment.setisAdmobAd(true)
val ft: FragmentTransaction =
supportFragmentManager.beginTransaction()
ft.add(dialogfragment, "DialogFragment_FLAG")
ft.commitAllowingStateLoss()
}
fun removeOverlay() {
val fragment: Fragment? = supportFragmentManager.findFragmentByTag("DialogFragment_FLAG")
if (fragment != null && fragment is DialogFragment) {
fragment.dismissAllowingStateLoss()
}
}
fun fetchWhatsAppRootURIs(
context: Context,
sharedViewModel: SharedViewModel,
dataRepository: DataRepository,
tinyDB: TinyDB, completed: () -> Unit
) {
val selectedPackageName = Constants.WHATSAPP_PKG_NAME
val selectedRootName = Constants.WHATSAPP_ROOT_NAME
var waImages: DocumentFile? = null
var waVN: DocumentFile? = null
var waDocs: DocumentFile? = null
var waVideos: DocumentFile? = null
var waAudio: DocumentFile? = null
var waStatus: DocumentFile? = null
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R && sharedViewModel.treeUri != null) {
CoroutineScope(Dispatchers.Main).launch {
async(Dispatchers.IO) {
val dir = DocumentFile.fromTreeUri(
context,
sharedViewModel.treeUri!!
)
dir?.listFiles()?.forEach {
if (it.name.equals(selectedPackageName)) {
it.listFiles().forEach {
if (it.name.equals(selectedRootName)) {
it.listFiles().forEach {
if (it.name.equals(Constants.WHATSAPP_MEDIA_FOLDER_NAME)) {
it.listFiles().forEach {
if (it.name.equals(Constants.FOLDER_NAME_WHATSAPP_IMAGES)) {
waImages = it
} else if (it.name.equals(Constants.FOLDER_NAME_WHATSAPP_VN)) {
waVN = it
} else if (it.name.equals(Constants.FOLDER_NAME_WHATSAPP_DOCUMENTS)) {
waDocs = it
} else if (it.name.equals(Constants.FOLDER_NAME_WHATSAPP_VIDEO)) {
waVideos = it
} else if (it.name.equals(Constants.FOLDER_NAME_WHATSAPP_AUDIO)) {
waAudio = it
} else if (it.name.equals(Constants.FOLDER_NAME_STATUSES)) {
waStatus = it
}
}
}
}
}
}
}
}
}.await()
Timber.e("processStatusFetch:Done")
tinyDB.putString("mrWhatsAppImages", waImages?.uri.toString())
tinyDB.putString("mrWhatsAppVN", waImages?.uri.toString())
tinyDB.putString("mrWhatsAppDocs", waImages?.uri.toString())
tinyDB.putString("mrWhatsAppVideo", waImages?.uri.toString())
tinyDB.putString("mrWhatsAppAudio", waImages?.uri.toString())
tinyDB.putString("WhatsAppStatuses", waStatus?.uri.toString())
dataRepository.mrWhatsAppImages = waImages
dataRepository.mrWhatsAppVN = waVN
dataRepository.mrWhatsAppDocs = waDocs
dataRepository.mrWhatsAppVideo = waVideos
dataRepository.mrWhatsAppAudio = waAudio
dataRepository.WhatsAppStatuses = waStatus
completed()
}
}
}
Here i am using .Statuses folder URI to list DocumentFiles and display but this way it is slow
class StatusImageFragment : Fragment(), StatusListener, CoroutineScope {
companion object {
fun newInstance() = StatusImageFragment()
}
val handler = CoroutineExceptionHandler { _, exception ->
Log.d("CoroutineException", "$exception handled !")
}
override val coroutineContext: CoroutineContext
get() = Dispatchers.Main + job + handler
private lateinit var job: Job
private var adapterSDK30 = StatusImageAdapterSDK30()
private var no_image: ImageView? = null
private var no_image_txt: TextView? = null
val tinyDB: TinyDB by inject()
val sharedViewModel by viewModel<SharedViewModel>()
private val dataRepository: DataRepository by inject()
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
job = Job()
return inflater.inflate(R.layout.status_image_fragment, container, false)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
swipeRefresh(false, false)
}
public fun swipeRefresh(isReloadRequired: Boolean, isFromModeChanged: Boolean) {
try {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
if (isFromModeChanged) {
status_image_recycler.visibility = View.GONE
progressbar.visibility = View.VISIBLE
no_image?.let {
it.visibility = View.GONE
}
no_image_txt?.let {
it.visibility = View.GONE
}
go_to_app?.let {
it.visibility = View.GONE
}
} else {
if (adapterSDK30.listImages == null || adapterSDK30.listImages.size == 0) {
no_image?.let {
it.visibility = View.GONE
}
no_image_txt?.let {
it.visibility = View.GONE
}
go_to_app?.let {
it.visibility = View.GONE
}
progressbar.visibility = View.VISIBLE
}
}
if (isReloadRequired) {
processStatusFetchFromChild({
sharedViewModel.statusImages.observe(viewLifecycleOwner, Observer {
val arrayList = it
adapterSDK30.listImages = arrayList
postFetchingExecutionSDK30()
})
})
} else {
sharedViewModel.statusImages.observe(viewLifecycleOwner, Observer {
val arrayList = it
adapterSDK30.listImages = arrayList
adapterSDK30.listImages = it
postFetchingExecutionSDK30()
})
}
}
} catch (ex: Exception) {
ex.printStackTrace()
}
}
private fun postFetchingExecutionSDK30() {
progressbar.visibility = View.GONE
status_image_recycler.visibility = View.VISIBLE
if (adapterSDK30!!.listImages != null && adapterSDK30!!.listImages.size > 0) {
no_image?.let {
it.visibility = View.GONE
}
no_image_txt?.let {
it.visibility = View.GONE
}
go_to_app?.let {
it.visibility = View.GONE
}
} else {
no_image?.let {
it.visibility = View.VISIBLE
}
no_image_txt?.let {
it.visibility = View.VISIBLE
}
go_to_app?.let {
it.visibility = View.VISIBLE
}
}
adapterSDK30!!.notifyDataSetChanged()
status_img_swipe.isRefreshing = false
}
override fun onDestroyView() {
job.cancel()
super.onDestroyView()
}
fun processStatusFetchFromChild(completed: () -> Unit) {
val statusSelection = tinyDB.getInt(Constants.status_accounts)
if (statusSelection == 0 || statusSelection == 1) {
if (dataRepository.WhatsAppStatuses == null) {
(activity as StatusActivity).setUpWAURIs()
}
var documentFileStatuses: DocumentFile? = dataRepository.WhatsAppStatuses
if (statusSelection == 1) {
documentFileStatuses = dataRepository.WhatsAppBusinessStatuses
}
if (documentFileStatuses != null) {
launch(Dispatchers.Main) {
val statusImages1 = arrayListOf<DocumentFile>()
async(Dispatchers.IO) {
//this takes time ; want to fetch this same as WhatsApp Gallery
statusImages1.addAll(documentFileStatuses!!.listFiles().filter {
it.mimeType.equals(Constants.MIME_TYPE_IMG_PNG) || it.mimeType.equals(
Constants.MIME_TYPE_IMG_JPG
) || it.mimeType.equals(Constants.MIME_TYPE_IMG_JPEG)
}.sortedByDescending { it.lastModified() })
}.await()
Timber.e("processStatusFetch:Done")
sharedViewModel.statusImages.postValue(statusImages1)
completed()
}
} else {
Timber.e("processStatusFetch:Done")
sharedViewModel.statusImages.postValue(arrayListOf<DocumentFile>())
completed()
}
} else {
Timber.e("processStatusFetch:Done")
sharedViewModel.statusImages.postValue(arrayListOf<DocumentFile>())
completed()
}
}
}
Please note WhatsApp folder path which i used is
val whatsapp_images_path11 = "/Android/media/“ +"com.whatsapp" +"/WhatsApp/Media/WhatsAppImages/"
How i can use MediaStore in this case so that i don't need to use sort and filter functions of list? Its not important to get java.io File only i can work with URIs as well.
What I have finally implemented, in android 10+ you need to ask the user for your specific directory access. Then you can use this functions to fetch statuses:
#RequiresApi(Build.VERSION_CODES.LOLLIPOP)
private fun readSDKFrom30(): ArrayList<String> {
val treeUri = DocumentsContract.buildTreeDocumentUri(
EXTERNAL_STORAGE_PROVIDER_AUTHORITY,
"primary:Android/media/com.whatsapp/WhatsApp/Media/.Statuses"
)
val tree = DocumentFile.fromTreeUri(context, treeUri)!!
val pathList = ArrayList<String>()
listFolderContent(tree).forEach { uri ->
val file = createFileFromContentUri(uri)
pathList.add(file.toString())
}
return pathList
}
private fun listFolderContent(folder: DocumentFile): List<Uri> {
return if (folder.isDirectory) {
val files = folder.listFiles().toMutableList()
files.sortByDescending { it.lastModified() }
files.mapNotNull { file ->
if (file.name != null) file.uri else null
}
} else {
emptyList()
}
}
#RequiresApi(Build.VERSION_CODES.O)
private fun createFileFromContentUri(fileUri: Uri): File {
var fileName = ""
fileUri.let { returnUri ->
context.contentResolver.query(returnUri, null, null, null)
}?.use { cursor ->
val nameIndex = cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME)
cursor.moveToFirst()
fileName = cursor.getString(nameIndex)
}
val iStream: InputStream =
context.contentResolver.openInputStream(fileUri)!!
val outputDir: File = context.cacheDir!!
val outputFile = File(outputDir, fileName)
copyStreamToFile(iStream, outputFile)
iStream.close()
return outputFile
}
private fun copyStreamToFile(inputStream: InputStream, outputFile: File) {
inputStream.use { input ->
val outputStream = FileOutputStream(outputFile)
outputStream.use { output ->
val buffer = ByteArray(4 * 1024) // buffer size
while (true) {
val byteCount = input.read(buffer)
if (byteCount < 0) break
output.write(buffer, 0, byteCount)
}
output.flush()
}
}
}
Using DocumentFile to handle SAF uries is slow indeed.
Better use DocumentsContract to do so.
Its about twenty times as fast as DocumentFile and about as fast as classic File class stuff.
Using MediaStore for hidden folders should be possible. You cannot create hidden folders with the mediastore. But if you managed to make them not using mediastore you should be able to list files in them using mediastore. Well if they are scanned. And if they belong to your app.
I'm stuck on a problem with my app.
I'm trying to upload some photos to a cloud and then, I'll show to the user a list of url to copy, in order to use them in a second time.
I'm using RxWorker for this.
When I enqueue my workers list, everything seems fine (one OneTimeWorkRequestBuilder for each image), but then, when I retrieve the result, seems that my observe is triggered too many times.
In order to make this step suspendable, I've two lists (image and worker ones).
When a work is finished (both failure or success) I cancel both image and worker from their own list.
Here's my code. Thanks to everyone!
My Fragment:
class ImagesUploadFragment : Fragment() {
companion object {
private const val TAG = "ImagesUploadFragment"
private const val PHOTOS_LIST = "PHOTOS_LIST"
private const val WORK_NAME = "PHOTO_UPLOAD"
fun newInstance(optionalData: String?): ImagesUploadFragment {
val imagesUploadFragment = ImagesUploadFragment()
val bundle = Bundle()
bundle.putString(PHOTOS_LIST, optionalData)
imagesUploadFragment.arguments = bundle
return imagesUploadFragment
}
}
private lateinit var imagesUploadBinding: FragmentImagesUploadBinding
private var stepListener: StepListener? = null
private val mDownloadUrlsList = mutableListOf<String>()
private val mWorkManger: WorkManager by lazy { WorkManager.getInstance(requireContext()) }
private val mWorkRequestList = mutableListOf<OneTimeWorkRequest>()
private var isUploadPaused = false
private val mImagesList = mutableListOf<String>()
private val gson: Gson by inject()
override fun onAttach(context: Context) {
super.onAttach(context)
stepListener = context as? StepListener
}
private val imagesUploadViewModel: ImagesUploadViewModel by viewModel()
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
imagesUploadBinding = FragmentImagesUploadBinding.inflate(inflater, container, false)
return imagesUploadBinding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
val imagesList = arguments?.getString(PHOTOS_LIST)
imagesList?.let {
startImagesUpload(it)
}
imagesUploadBinding.successfullyUpdated.text = String.format(getString(R.string.succeeded_updates), 0)
imagesUploadBinding.workActionIv.setImageDrawable(ContextCompat.getDrawable(requireContext(), R.drawable.ic_pause))
imagesUploadBinding.buttonContinue.setOnClickListener {
stepListener?.onStepChange(4, gson.toJson(mDownloadUrlsList))
}
mWorkManger.cancelAllWorkByTag(TAG)
mWorkManger.cancelAllWork()
mWorkManger.pruneWork()
imagesUploadBinding.workActionIv.setOnClickListener {
if (!isUploadPaused) {
isUploadPaused = true
pauseUpload()
imagesUploadBinding.workActionIv.setImageDrawable(ContextCompat.getDrawable(requireContext(), R.drawable.ic_play))
} else {
isUploadPaused = false
resumeUpload()
imagesUploadBinding.workActionIv.setImageDrawable(ContextCompat.getDrawable(requireContext(), R.drawable.ic_pause))
}
}
setupWorkManagerObserve()
with(imagesUploadViewModel) {
observe(imagesListResource,
success = { imagesList ->
imagesUploadBinding.uploadProgress.max = imagesList.first.size
mImagesList.addAll(imagesList.first.map { it.jsonValue })
setupWorkerList()
},
failure = {
Timber.e(it)
}
)
observe(uploadedFileResource,
success = {
if(it.isNotEmpty()) mDownloadUrlsList.add(it)
imagesUploadBinding.buttonContinue.isVisible = mImagesList.size == 0
imagesUploadBinding.workActionIv.isVisible = mImagesList.size > 0
imagesUploadBinding.horizontalSpace.isVisible = mImagesList.size > 0
},
failure = {
Timber.e(it)
}
)
}
}
private fun pauseUpload() {
mWorkManger.cancelAllWork()
}
private fun resumeUpload() {
setupWorkerList()
}
private fun startImagesUpload(imagesListJson: String) {
imagesUploadViewModel.getImagesFromUri(imagesListJson)
}
private fun setupWorkManagerObserve() {
mWorkManger
.getWorkInfosByTagLiveData(TAG)
.observe(viewLifecycleOwner) {
var successNumber = 0
var failureNumber = 0
it.forEach { work ->
when {
work.state == WorkInfo.State.SUCCEEDED && work.state.isFinished -> {
successNumber++
val image = work.outputData.getString(PHOTO)
val uploadedFileJson = work.outputData.getString(UPLOAD_RESPONSE)
mWorkRequestList.removeAll { it.id == work.id }
mImagesList.removeAll { it == image }
uploadedFileJson?.let {
imagesUploadViewModel.getDownloadUrlFromResponse(it)
}
imagesUploadBinding.uploadProgress.progress = successNumber + failureNumber
imagesUploadBinding.successfullyUpdated.text =
String.format(getString(R.string.succeeded_updates), successNumber)
}
work.state == WorkInfo.State.RUNNING -> {
}
work.state == WorkInfo.State.FAILED -> {
failureNumber++
val image = work.outputData.getString(PHOTO)
mWorkRequestList.removeAll { it.id == work.id }
mImagesList.removeAll { it == image }
imagesUploadBinding.uploadProgress.progress = successNumber + failureNumber
imagesUploadBinding.failuredUpdated.text =
String.format(getString(R.string.failed_updates), failureNumber)
imagesUploadViewModel.addFailure()
}
else -> {
}
}
}
}
}
private fun setupWorkerList() {
for (i in 0 until mImagesList.size) {
val work = OneTimeWorkRequestBuilder<ImagesUploadWorkManager>()
.addTag(TAG)
.setInputData(Data.Builder().putString(PHOTO, mImagesList[i])
.build())
.build()
mWorkRequestList.add(work)
}
mWorkManger.enqueue(mWorkRequestList)
}
}
My worker:
class ImagesUploadWorkManager(val context: Context, parameters: WorkerParameters) : RxWorker(context, parameters) {
companion object {
const val PHOTO = "PHOTO"
const val UPLOAD_RESPONSE = "UPLOAD_RESPONSE"
}
private val uploadFilesUseCase: UploadFilesUseCase by inject(UploadFilesUseCase::class.java)
private val gson: Gson by inject(Gson::class.java)
override fun createWork(): Single<Result> {
val imageStringUri = inputData.getString(PHOTO)
val imageUri = Uri.parse(imageStringUri)
return Single.fromObservable(
uploadFilesUseCase.buildObservable(UploadFilesUseCase.Params(imageUri))
.doOnError {
Timber.e(it)
}
.map {
Result.success(workDataOf(UPLOAD_RESPONSE to gson.toJson(it), PHOTO to imageStringUri))
}
.onErrorReturn {
Timber.e(it)
Result.failure(workDataOf(PHOTO to imageStringUri))
}
)
}
}
My UseCase:
class UploadFilesUseCase(
private val context: Context,
private val getServerUseCase: GetServerUseCase,
) : UseCase<UploadFilesUseCase.Params, GoFileDataEntity>() {
private val goFileApi: GoFileApi by inject(GoFileApi::class.java)
override fun buildObservable(param: Params): Observable<GoFileDataEntity> {
return Observable.just(param.imageUri)
.flatMap {
val file = createTempFile(it) ?: throw Exception()
Observable.just(file)
}.flatMap { imageFile ->
getServerUseCase.buildObservable(GetServerUseCase.Params())
.flatMap { server ->
goFileApi.uploadFile(
String.format(BuildConfig.api_gofile_upload, server.server),
MultipartBody.Part.createFormData("file", imageFile.name, createUploadRequestBody(imageFile, "image/jpeg"))
).toObservable()
}.map { it.goFileDataDTO.toEntity() }
}
}
private fun createUploadRequestBody(file: File, mimeType: String) =
file.asRequestBody(mimeType.toMediaType())
private fun createTempFile(uri: Uri): File? {
val tempFile = File.createTempFile(System.currentTimeMillis().toString().take(4), ".jpg", context.externalCacheDir)
val inputStream = context.contentResolver.openInputStream(uri) ?: return null
FileOutputStream(tempFile, false).use { outputStream ->
var read: Int
val bytes = ByteArray(DEFAULT_BUFFER_SIZE)
while (inputStream.read(bytes).also { read = it } != -1) {
outputStream.write(bytes, 0, read)
}
outputStream.close()
}
inputStream.close()
return tempFile
}
data class Params(val imageUri: Uri)
}
I had a similar problem, and it looks like the queue was full from previous Workmanager enqueuings.
I did following during application start and now it works fine:
WorkManager.getInstance(mContext).cancelAllWork();
I want to load just one element with one scroll gesture. Now it is like one scroll gesture loads 1 or few new elements (depends on time of scroll gesture). As a solution I could do this gesture in shorter time than 500ms or make this postDelayed's delay longer but I guess there are better solutions for that. Do you have any ideas how to do that?
This app is written in MVP pattern. Here is my code:
CurrencyFragmentList.kt
private fun addScrollerListener() {
rvItem.addOnScrollListener(object : RecyclerView.OnScrollListener() {
override fun onScrollStateChanged(_rvItem: RecyclerView, newState: Int) {
super.onScrollStateChanged(_rvItem, newState)
Log.e("scroll", isLoading.toString())
if (!isLoading) {
if (!_rvItem.canScrollVertically(1)) {
loadMore()
isLoading = true
}
}
}
})
}
private fun loadMore() {
showProgressDialog()
var numberOfDays = mainPresenter.getNumberOfMinusDays()
numberOfDays++
mainPresenter.saveNumberOfMinusDaysIntoSp(numberOfDays)
var dateMinusXDays = mainPresenter.currentDateMinusXDaysToStr(numberOfDays)
val nextLimit = listSize + 1
for (i in listSize until nextLimit) {
if (mainPresenter.checkIfSuchDateExistsinSp(dateMinusXDays)) {
Log.d("such date already exists in shared prefs", dateMinusXDays)
handler.postDelayed({
mainPresenter.processDateWithoutMakingACall(dateMinusXDays)
}, 500)
} else {
mainPresenter.makeACall(dateMinusXDays)
Log.d("retrofit call made", dateMinusXDays)
}
}
itemAdapter.notifyDataSetChanged()
}
override fun hideProgressDialog() {
if (apiResponseList.size > 1) {
apiResponseList.removeAt(apiResponseList.size - 1)
listSize = apiResponseList.size
itemAdapter.notifyItemRemoved(listSize)
} else progress_bar.visibility = View.GONE
isLoading = false
}
override fun assignResponseToRecyclerview(apiResponse: ApiResponse?) {
rvItem.apply {
layoutManager = _layoutManager
apiResponseList.add(apiResponse!!)
itemAdapter = activity?.let { ItemAdapter(apiResponseList, it) }!!
adapter = itemAdapter
}
Log.e("assign", isLoading.toString())
}
MainPresenter.kt
override fun makeACall(date: String?) {
//view.showProgressDialog()
date?.let { restModel.fetchApiResponse(this, it) }
}
fun processDateWithoutMakingACall(date: String) {
val apiResponse = processRawJson(sp.getString(date, "").toString())
passResponseToView(apiResponse)
}
override fun processRawJson(rawJson: String): ApiResponse {
val parser = JsonParser()
val rootObj = parser.parse(rawJson).asJsonObject
var ratesObj = JsonObject()
var ratesKeys: Set<String> = HashSet()
val ratesArrayList: ArrayList<Currency> = ArrayList()
val rootKeys = rootObj.keySet();
var baseValue = ""
var dateValue = ""
for (key in rootKeys) {
if (key == "base")
baseValue = rootObj.get(key).asString
if (key == "date")
dateValue = rootObj.get(key).asString
if (key == "rates") {
ratesObj = rootObj.get(key).asJsonObject
ratesKeys = ratesObj.keySet()
}
}
for (key2 in ratesKeys) {
Log.e("ratesKey", key2)
Log.e("ratesValue", ratesObj.get(key2).asFloat.toString())
ratesArrayList.add(Currency(key2, ratesObj.get(key2).asFloat))
}
saveRawJsonIntoSp(rawJson, dateValue)
return ApiResponse(baseValue, dateValue, ratesArrayList, false)
}
override fun passResponseToView(apiResponse: ApiResponse?) {
view.hideProgressDialog()
view.assignResponseToRecyclerview(apiResponse)
}
RestModel.kt
override fun fetchApiResponse(presenter: MainPresenter, date: String) {
job = CoroutineScope(Dispatchers.IO).launch {
val response = userService.getCurrenciesForDate(date)
withContext(Dispatchers.Main) {
if (response.isSuccessful) {
val rawJson = response.body()
val apiResponse = presenter.processRawJson(rawJson)
presenter.passResponseToView(apiResponse)
}
}
}
}
Any help will be really appreciated. Thank you in advance!
Try out the SnapHelper, it might slow layout manager to make more callbacks and stop overloading
I have a list of user record sound
The user can click on the list and play the audio file
How can I handle the MediaPlayer and the RecyclerView item in the Seekbar as well in the MVVM correctly?
That is, when the user clicks, the visitor changes the item and updates itself, and when it is clicked on an item again, it updates itself.
I did it now, but unfortunately, to the dirtiest possible form
activity code :
class SoundListActivity : BaseActivity(), Observer<List<VoiceEntity>>, VoiceAdapter.OnClickItemListener,
OnMultiSelectVoiceListener {
private lateinit var viewModel: VoiceViewModel
private val adapter = VoiceAdapter()
private val player = MediaPlayer()
private var positionPlayItem = -1
companion object {
fun start(context: Context) {
context.startActivity(Intent(context, SoundListActivity::class.java))
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_sound_list)
viewModel = ViewModelProviders.of(this).get(VoiceViewModel::class.java)
viewModel.mutableList!!.observe(this, this)
adapter.onItemClickListener = this
adapter.listenerMultiSelect = this
recycler.layoutManager = LinearLayoutManager(this)
recycler.adapter = adapter
fabAdd.setOnClickListener {
stopPlay()
RecordSoundActivity.start(this)
}
toolbar.setIconLeftListener(View.OnClickListener {
stopPlay()
finish()
})
}
override fun onChanged(list: List<VoiceEntity>?) {
this.progressBar.visibility = View.GONE
this.layoutEmptyState.visibility = View.INVISIBLE
this.adapter.removeAll()
if (list == null || list.isEmpty()) {
layoutEmptyState.visibility = View.VISIBLE
return
}
adapter.addItems(ArrayList(list))
}
override fun onClickItem(item: VoiceEntity, position: Int) {
if (player.isPlaying) {
player.reset()
}
if (item.isPlaying) {
item.isPlaying = false
player.reset()
adapter.notifyDataSetChanged()
return
}
this.positionPlayItem = position
adapter.items!!.forEach {
if (it != item) {
it.isPlaying = false
}
}
player.setDataSource(item.path)
player.prepare()
player.start()
item.isPlaying = true
adapter.notifyDataSetChanged()
player.setOnCompletionListener {
player.reset()
adapter.notifyItemChanged(position)
item.isPlaying = false
}
}
private fun stopPlay() {
if (positionPlayItem == -1) {
return
}
player.reset()
adapter.items!![positionPlayItem].isPlaying = false
adapter.notifyItemChanged(positionPlayItem)
}
override fun onMultiSelectVoice(items: ArrayList<VoiceEntity>) {
stopPlay()
if (items.size == 0) {
layoutSelectItem.visibility = View.GONE
return
}
txtCounterSelect.text = String.format(getString(R.string.selected_number), items.size.toString())
setStatusBarColor(R.color.black)
if (layoutSelectItem.visibility == View.GONE) {
layoutSelectItem.visibility = View.VISIBLE
}
if (items.size > 1) {
imgShare.visibility = View.GONE
imgEdit.visibility = View.GONE
} else {
imgShare.visibility = View.VISIBLE
imgEdit.visibility = View.VISIBLE
}
imgCancelSelect.setOnClickListener {
resetData()
}
imgEdit.setOnClickListener {
edit(items.first())
}
imgShare.setOnClickListener {
if (items.isEmpty()) {
return#setOnClickListener
}
shareVoice(this, items[0].path)
}
imgDelete.setOnClickListener {
val alertDialog = AlertDialog.Builder(
supportFragmentManager,
getString(R.string.note), getString(R.string.do_you_sure_delete)
)
alertDialog.setBtnNegative(getString(R.string.no), View.OnClickListener {
alertDialog.dialog!!.dismiss()
})
alertDialog.setBtnPositive(getString(R.string.yes), View.OnClickListener {
val ex = Executors.newSingleThreadExecutor()
items.forEach { item ->
viewModel.remove(item)
ex.execute { File(item.path).deleteOnExit() }
}
items.clear()
layoutSelectItem.visibility = View.GONE
setStatusBarColor(R.color.colorPrimaryDark)
alertDialog.dialog!!.dismissAllowingStateLoss()
})
alertDialog.build().show()
}
}
private fun resetData() {
adapter.itemsSelected.clear()
adapter.items?.forEach { item ->
item.statusSelect = false
}
adapter.notifyDataSetChanged()
layoutSelectItem.visibility = View.GONE
setStatusBarColor(R.color.colorPrimaryDark)
}
private fun edit(item: VoiceEntity) {
val bottomSheet = NameBottomSheet(supportFragmentManager)
bottomSheet.listener = object : NameBottomSheet.OnTitleListener {
override fun onTitle(title: String) {
item.title = title
viewModel.update(item)
resetData()
}
}
bottomSheet.item = item
bottomSheet.show()
}
override fun onBackPressed() {
if (layoutSelectItem.visibility == View.VISIBLE) {
resetData()
return
}
stopPlay()
super.onBackPressed()
}
}
adapter class code :
class VoiceAdapter : AdapterRecyclerView<VoiceEntity>() {
var onItemClickListener: OnClickItemListener? = null
var itemsSelected: ArrayList<VoiceEntity> = ArrayList()
var listenerMultiSelect: OnMultiSelectVoiceListener? = null
override fun getItemLayout(viewType: Int): Int {
return R.layout.item_voice
}
override fun onBindView(
viewDataBinding: ViewDataBinding,
viewHolder: ItemViewHolder,
position: Int,
viewType: Int,
element: VoiceEntity
) {
val binding = viewDataBinding as ItemVoiceBinding
binding.txtTitle.text = element.title
binding.txtDate.text = element.date.toAgoTime(context!!)
binding.icPlay.setImageResource(if (element.isPlaying) R.drawable.ic_pause else R.drawable.ic_play)
binding.seekBar.max = element.duration / 60
val colorSelectItem =
ContextCompat.getColor(binding.rootLayout.context, R.color.color_background_select_item_recycler_view)
val color = if (element.statusSelect) colorSelectItem else Color.TRANSPARENT
binding.rootLayout.setBackgroundColor(color)
if (element.statusSelect) {
changeColorLight(binding)
} else {
changeColorDarker(binding)
}
if (element.isPlaying) {
binding.layoutPlaying.visibility = View.VISIBLE
binding.lottieLayer.playAnimation()
//TODO : change handled voice progressBar show
val t = object : Thread() {
override fun run() {
super.run()
for (i in 0..element.duration) {
Thread.sleep(60)
binding.seekBar.progress = i
if (!element.isPlaying) break
}
}
}
t.start()
} else {
binding.layoutPlaying.visibility = View.GONE
binding.lottieLayer.cancelAnimation()
}
binding.rootLayout.setOnClickListener {
if (itemsSelected.size > 0) {
val item = items!![viewHolder.adapterPosition]
if (itemsSelected.contains(item)) {
item.statusSelect = false
itemsSelected.remove(item)
binding.rootLayout.animatedColorBackgroundSelected(false)
listenerMultiSelect?.onMultiSelectVoice(itemsSelected)
changeColorDarker(binding)
return#setOnClickListener
}
item.statusSelect = true
itemsSelected.add(item)
binding.rootLayout.animatedColorBackgroundSelected()
listenerMultiSelect?.onMultiSelectVoice(itemsSelected)
changeColorLight(binding)
return#setOnClickListener
}
onItemClickListener?.onClickItem(element, position)!!
}
binding.rootLayout.setOnLongClickListener {
val item = items!![viewHolder.adapterPosition]
if (itemsSelected.contains(item)) {
item.statusSelect = false
itemsSelected.remove(item)
binding.rootLayout.animatedColorBackgroundSelected(false)
changeColorDarker(binding)
listenerMultiSelect?.onMultiSelectVoice(itemsSelected)
}
item.statusSelect = true
itemsSelected.add(item)
binding.rootLayout.animatedColorBackgroundSelected()
changeColorLight(binding)
listenerMultiSelect?.onMultiSelectVoice(itemsSelected)
true
}
}
private fun changeColorLight(binding: ItemVoiceBinding) {
binding.txtDate.setTextColor(ContextCompat.getColor(binding.root.context, R.color.color_subtitle_light))
binding.txtTitle.setTextColor(ContextCompat.getColor(binding.root.context, R.color.color_title_light))
}
private fun changeColorDarker(binding: ItemVoiceBinding) {
binding.txtDate.setTextColor(ContextCompat.getColor(binding.root.context, R.color.color_subtitle))
binding.txtTitle.setTextColor(ContextCompat.getColor(binding.root.context, R.color.color_title))
}
interface OnClickItemListener {
fun onClickItem(item: VoiceEntity, position: Int)
}
}
github repository (open source project)
I have a recycler view. For the first time, it loads all the data and the view is shown perfect. But when the data is added dynamically, pull to refresh, I add it to the list of data and call notifyDataSetChanged. It increases the count of the list and the list is updated with the latest data but the view is not updating with the latest data added to the list. onBindViewHolder has the updated listView size and the listView has all the valid data within it.
override fun onBindViewHolder(holder: HistoryListAdapter.ViewHolder?, position: Int) {
info("onBindViewHolder =>"+listAssets.size)
info("onBindViewHolder itemCount =>"+itemCount)
info("onBindViewHolder position =>"+position)
val notesButton = holder?.notesButton
val notesView = holder?.notesTextView
val dateTime = listAssets[position].date
val location = listAssets[position].location
val sessionId = listAssets[position].id
holder?.sessionID = sessionId
holder?.portraitImageView?.setImageDrawable(listAssets[position].image)
holder?.titleTextView?.text = DateTimeFormatter.getFormattedDate(context, dateTime)
val timeString = DateTimeFormatter.getFormattedTime(context, dateTime)
if (location.length != 0) {
holder?.subtitleTextView?.text = "$timeString # $location"
} else {
holder?.subtitleTextView?.text = "$timeString"
}
val data = listAssets[position].data
for (actionData in data) {
val inflater = context.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater
val parent = inflater.inflate(R.layout.history_card_action, null)
val icon = parent?.findViewById(R.id.historyActionIcon) as ImageView
val title = parent?.findViewById(R.id.historyActionTitle) as TextView
val subtitle = parent?.findViewById(R.id.historyActionSubtitle) as TextView
var iconDrawable: Drawable? = null
when(actionData.type) {
ActionType.HEART -> {
iconDrawable = ContextCompat.getDrawable(context, R.drawable.heart)
}
ActionType.LUNGS -> {
iconDrawable = ContextCompat.getDrawable(context, R.drawable.lungs)
}
ActionType.TEMPERATURE -> {
iconDrawable = ContextCompat.getDrawable(context, R.drawable.temperature)
}
}
icon.setImageDrawable(iconDrawable)
val titleString = actionData.title
titleString?.let {
title.text = titleString
}
val subtitleString = actionData.subtitle
subtitleString?.let {
subtitle.text = subtitleString
}
holder?.actionContainer?.addView(parent)
}
val notes = listAssets[position].notes
notesView?.text = notes
if (notes.length == 0) {
notesButton?.layoutParams?.width = 0
} else {
notesButton?.layoutParams?.width = toggleButtonWidth
}
if (expandedNotes.contains(sessionId)) {
notesView?.expandWithoutAnimation()
} else {
notesView?.collapseWithoutAnimation()
}
notesButton?.onClick {
notesView?.toggleExpansion()
}
}
data class ListAssets(val id: String,
val date: Date,
val location: String,
val notes: String,
val image: Drawable,
val data: ArrayList<ListData>)
data class ListData(val type: ActionType,
val title: String?,
val subtitle: String?)
override fun onViewRecycled(holder: HistoryListAdapter.ViewHolder?) {
super.onViewRecycled(holder)
if (holder != null) {
holder.actionContainer.removeAllViews()
val notesTextView = holder.notesTextView
if (notesTextView != null) {
if (notesTextView.expandedState) {
val sessionID = holder.sessionID
sessionID?.let {
val sessionSearch = expandedNotes.firstOrNull {
it.contentEquals(sessionID)
}
if (sessionSearch == null) {
expandedNotes.add(sessionID)
}
}
} else {
val sessionID = holder.sessionID
sessionID?.let {
val sessionSearch = expandedNotes.firstOrNull {
it.contentEquals(sessionID)
}
if (sessionSearch != null) {
expandedNotes.remove(sessionSearch)
}
}
}
}
}
}
ActionView is Enum.
Kindly please let me know if I am doing anything wrong.