I have made a simple app to populate recycler view with video or audio files. The recycler view get populated when installing app on any android device through android studio but I have a mobile device which is rooted. On this device, no audio or video file is detected nor recycler view get populated. Any solution for this problem please?
#Klox
private var videosPaths:ArrayList<File> = ArrayList()
private lateinit var pathsViewModel: PathsViewModel
private val videoFormat = arrayOf(".mp4", ".MP4", ".mkv", ".MKV")
// here I'm inserting video/audio files in database created
// room database files are not shown here
val pathsDao = PathsDatabase.getPathsInstance(applicationContext).pathsDao()
val pathsRepository = PathsRepository(pathsDao)
val pathsMainDatabase = PathsMainDatabase()
pathsViewModel = ViewModelProvider(this, PathsViewModelFactory(pathsRepository))[PathsViewModel::class.java]
// I have created database with name "video_paths"
if(!applicationContext.getDatabasePath("videos_paths").exists()) {
videosPaths = getVideoFolders()
// here videoPaths returns 0 video folders on rooted device and as app
// opens, it gives toast message: "shell granted superuser access"
videosPaths.forEach{ file ->
pathsViewModel.insertPath(Paths(0, file.absolutePath))
}
}
// my custom code for extracting video/audio files in device w/o using cursor library
//getting video folders
fun getVideoFolders() : ArrayList<File> {
searchVideoFiles(Environment.getExternalStorageDirectory())
return videoFolders
}
// a recursive function to scan video files in device all folders
private fun searchVideoFiles(file: File) {
val dir = file.listFiles() ?: return
for (f in dir) {
if (f.isDirectory) {
searchVideoFiles(f)
if (isVideoFolder(f)) videoFolders.add(f)
}
}
}
// return true if at least one video file is found
private fun isVideoFolder(file: File?): Boolean {
assert(file != null)
assert(file!!.isDirectory)
val dir = file.listFiles() ?: return false
for (f in dir) {
val path = f.absolutePath
for(v in videoFormat) {
if(path.endsWith(v)) return true
}
}
return false
}
Related
I am building a photo vault where users can hide their photos.
I write the following code which traverses through all the directories (except the hidden one) and creates a report mentioning the number of images with directory name and file(images) path.
It works fine and does its job but the problem here is the amount of time it takes to execute.
I run it on my OnePlus 7T with 128 GB Storage and 52% used it takes almost 30-40 seconds to get all the images. That is an insane amount of time for a user to wait every time they want to add an image to the vault.
I want to know what optimization could be made here so that its speed gets optimized. I have tested other similar applications and they are doing it in a snap.
Please let me know if you can help.
#OptIn(ExperimentalTime::class)
private fun getFiles(): List<MyFileModel> {
val list = mutableListOf<MyFileModel>()
val time = measureTime {
Environment.getExternalStorageDirectory().listFiles()?.forEach {file->
if (file.isDirectory) {
openDirectory(file)
} else if (file.isImage) {
addImage(file)
Log.i(
TAG,
"getFiles: image: ${file.name}\nParent File: ${file.parentFile}\nParent: ${file.parent}"
)
}
}
}
Log.i(
TAG,
"getFiles: took ${time.inWholeHours}h : ${time.inWholeMinutes}m : ${time.inWholeSeconds}s"
)
map.keys.forEach {
Log.i(TAG, "getFiles: There are ${map[it]?.size} images in $it directory")
}
return listOf()
}
private fun addImage(file: File) {
val parentPath = file.parent ?: throw Exception("Could not add file as image. File: $file")
var folderName: String? = null
if (parentPath == FileUtils.ROOT_ADDRESS.path) {
folderName = "STORAGE"
//File is in the home directory
} else {
folderName = parentPath.substring(parentPath.lastIndexOf("/") + 1)
}
val files: MutableList<File>? = map[folderName]
if (files.isNullOrEmpty()) {
map[folderName] = mutableListOf(file)
} else {
files.addIfNotAlreadyAdded(file)
}
// Log.i(TAG, "addImage: map: $map")
}
//
private fun openDirectory(file: File) {
Log.i(TAG, "getFiles: FILE: ${file.absolutePath}")
if (file.isHidden) return
if (file.isImage) {
addImage(file)
return
}
if (file.isDirectory) {
file.listFiles()?.forEach {
Log.i(TAG, "openDirectory: file.listFiles().forEach : file: $it")
if (it.isImage) {
addImage(it)
}
if (it.isDirectory) {
openDirectory(it)
}
}
}
}
Here is the extensions function that checks if the file is an image or not.
val File.isImage: Boolean
get() {
val fileName = this.name
val lasIndexOfDot = fileName.lastIndexOf(".")
if (lasIndexOfDot == -1) {
//This means that the file got no extension
return false
}
val extension = fileName.substring(fileName.lastIndexOf(".") + 1).lowercase()
return extension.equals("png") ||
extension.equals("jpeg") ||
extension.equals("jpg") ||
extension.equals("gif")
}
Thank you :)
Finally, I was able to do that by implementing the Content Provider.
Going through all files and folders in the storage and then checking each file if it is an image or not and that too by looking at the file extension. It was a set of terrible ideas.
But in the end, this is how we learn. :)
My app has an extract button that takes the data from text view and paste it in a text file "Ledger.txt". It creates a folder in Mobile internal Storage Root Directory "WaterLedger" and place a Ledger.txt file in it and if the text file is already present it appends the text in the file. The code is only working till android 9 not above.
In Android 10 on pressing Extract button it asks "grant permission to write file in storage" but even if you press Yes it still wont create folder (WaterLerger) and paste/update data in Text File (Ledger.txt)
Here is the code
class WriteFile: AsyncTask<String, Int, String>() {
private val mFolder = "/WaterLedger"
lateinit var folder: File
internal var writeThis = "string to cacheApp.txt"
internal var cacheApptxt = "Ledger.txt"
override fun doInBackground(vararg writethis: String): String? {
val received = writethis[0]
if(received.isNotEmpty()){
writeThis = received
}
folder = File(Environment.getExternalStorageDirectory(),"$mFolder/")
if(!folder.exists() || folder.exists()){
folder.mkdir()
val readME = File(folder, cacheApptxt)
val file = File(readME.path)
val out: BufferedWriter
try {
out = BufferedWriter(FileWriter(file, true), 1024)
out.write("****Ledger Updated On Dated:$currentDate*****\n")
out.newLine()
out.write(writeThis)
out.newLine()
out.close()
Log.d("Output_Success", folder.path)
} catch (e: Exception) {
Log.d("Output_Exception", "$e")
}
}
return folder.path
}
// Driver Code
extract.setOnClickListener {
var pathToFileCreated = ""
val anRW = ReadandWrite().readAndwriteStorage(this, this)
if (anRW) {
pathToFileCreated = WriteFile().execute("$StringBuilder").get()
Log.d("pathToFileCreated", pathToFileCreated)
Toast.makeText(this,"File Saved",android.widget.Toast.LENGTH_LONG).show()
}
This will give you External Pictures directory URI (You can create your desirable directory into it):
private fun createTextURI(): Uri? {
val imageCollection = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q)
MediaStore.Video.Media.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY)
else
MediaStore.Video.Media.EXTERNAL_CONTENT_URI
val fileName = System.currentTimeMillis()
val contentValues = ContentValues().apply {
put(MediaStore.MediaColumns.DISPLAY_NAME, "$fileName.txt")
put(MediaStore.MediaColumns.MIME_TYPE, "text/plain")
}
val finalURI = contentResolver.insert(imageCollection, contentValues)
resultUri = finalURI
return finalURI
}
Use this URI for file creation like this:
new File(finalURI.getPath());
And write into this file which you created above. ✌
Make sure you have WRITE_EXTERNAL_STORAGE permission.
Ref. Demo App
I am using below code to save image data into local directory from Uris obtained through pick intent.
private fun sendImages(uriString: String, messageBody: String, uriList: ArrayList<Uri>) {
val pathList = mutableListOf<String>()
val fileNameList = mutableListOf<String>()
var fieSize = 0
var multiSize = 0
if(uriList.isEmpty() && uriString.isNotEmpty())
uriList.add(Uri.parse(uriString))
val localId: String = "localId"
for(i in 0 until uriList.size) {
val uri = uriList[i]
val path = FileUtils.getPath(application, uri)!!
val fullName = path.substring(path.lastIndexOf("/") + 1)
val name = "some function call that returns unique name for file"
val file = File(requireActivity().applicationContext.filesDir, localId + name)
file.createNewFile()
var bitmapdata: ByteArray? = null
var inputStream: InputStream? = null
try {
val ei = ExifInterface(path)
val orientation = ei.getAttributeInt(ExifInterface.TAG_ORIENTATION, ExifInterface.ORIENTATION_UNDEFINED)
var bitmap: Bitmap? = null
bitmap = if (Build.VERSION.SDK_INT > Build.VERSION_CODES.P) {
ImageDecoder.decodeBitmap(ImageDecoder.createSource(requireActivity().applicationContext.contentResolver, uri))
}
else MediaStore.Images.Media.getBitmap(requireActivity().applicationContext.contentResolver, uri)
val newBitmap = FunctionUtil.rotateImage(bitmap, orientation)
bitmapdata = FileUtils.getCompressedBitmapForUpload(newBitmap!!)
FunctionUtil.recycleBitmap(newBitmap)
} catch (e: Exception) {
return
}
}
requireActivity().applicationContext.openFileOutput(file.name, Context.MODE_PRIVATE).use {
it.write(bitmapdata)
}
val localPath = FunctionUtil.getMediaStorePath(requireActivity().application)
val completePath = requireActivity().application.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS)!!
var outFile =File(localPath+"/"+file.name)
org.apache.commons.io.FileUtils.copyFile(file,outFile)
Logger.log("PATH ${ outFile.absolutePath} ${ outFile.path}")
pathList.add(outFile.absolutePath)
fileNameList.add(file.name)
//... Do something with this data
}
}
In the same code, if single file is copied to local directory, it is getting saved & i am able to send it . But whenever i try to save multiple files, the files are becoming empty when i try to send them.
I am not able to find what issue is there. Please help
Some Android phones will encounter such a problem :
the file does exist, but the 'fd' returned by the 'c++ open()' method is -1, and the 'strerror' showing 'No such file or directory".
The phones I have come across are as follows:
vivo
V2055A V2073A V2241A
huawei
GIA-AN00 JLH-AN00 CMA-AN00 HPB-AN00 NTN-AN20 CMA-AN40
oppo
LE2110
In the source code of the Android System, a similar phenomenon can also be found :
The above is a comment added in 2017, and only the common 'bmp' and 'ico' formats are written.
In face, this problem also occurs with pictures in 'heif/heic' format.
Finally,
Using 'FileInputStream' will be ok.
I am using Exoplayer to create my own music player. I am also adding the option to download the track but I have a problem when I am trying to download the track that I am playing. I add a notification to the download to check the progress of the download and it appears but it even doesn't start. What I think is that it might have some kind of problem with the buffering cache and the download since they are stored in the same folder.
To download the tracks I do the following:
override fun addDownloadTrack(track: Track) {
getIfTrackIsCached.run({ isCached ->
if (!isCached) {
val data = Util.toByteArray(track.title.byteInputStream())
val downloadRequest =
DownloadRequest(track.id, DownloadRequest.TYPE_PROGRESSIVE, Uri.parse(track.href), Collections.emptyList(), track.id, data)
DownloadService.sendAddDownload(context, ExoPlayerDownloadService::class.java, downloadRequest, false)
}
}, ::onError, GetIfTrackIsCached.Params(track.id))
}
This is the DownloadService:
class ExoPlayerDownloadService : DownloadService(
FOREGROUND_NOTIFICATION_ID,
DEFAULT_FOREGROUND_NOTIFICATION_UPDATE_INTERVAL,
Constants.CHANNEL_DOWNLOAD_ID,
R.string.exo_download_notification_channel_name) {
private val manager: DownloadManager by inject()
private val channelIsCached: ChannelPublisher<CachedMedia> by inject(PUBLISHER_IS_CACHED)
private val notificationHelper: DownloadNotificationHelper by inject()
private var nextNotificationId: Int = FOREGROUND_NOTIFICATION_ID
override fun onCreate() {
super.onCreate()
if (!manager.isInitialized) {
manager.maxParallelDownloads = MAX_PARALLEL_DOWNLOADS
}
}
override fun getDownloadManager(): DownloadManager = manager
override fun getForegroundNotification(downloads: MutableList<Download>?): Notification {
var text = ""
var index = 1
downloads?.forEach { text += "${if (downloads.size > 1) "${index++} - " else ""}${Util.fromUtf8Bytes(it.request.data)}\n" }
return notificationHelper.buildProgressNotification(R.drawable.ic_stat_downloading, null, text, downloads)
}
override fun getScheduler(): Scheduler? = null
override fun onDownloadChanged(download: Download?) {
val notification = when (download?.state) {
Download.STATE_COMPLETED -> {
channelIsCached.publish(CachedMedia(download.request.id, true))
notificationHelper.buildDownloadCompletedNotification(R.drawable.ic_stat_download_complete, null, Util.fromUtf8Bytes(download.request.data))
}
Download.STATE_FAILED ->
notificationHelper.buildDownloadFailedNotification(R.drawable.ic_stat_download_failed, null, Util.fromUtf8Bytes(download.request.data))
else -> null
}
notification?.let { NotificationUtil.setNotification(this#ExoPlayerDownloadService, ++nextNotificationId, it) }
}
companion object {
private const val MAX_PARALLEL_DOWNLOADS = 3
private const val FOREGROUND_NOTIFICATION_ID = 2000
}
}
And to create the cache I use this:
SimpleCache(File(androidContext().cacheDir, CACHE_MEDIA_FOLDER), NoOpCacheEvictor(), get<DatabaseProvider>())
How can I avoid conflicts between buffering cache and downloaded files?
I had this issue also, and found the solution!
The downloading documentation states
The CacheDataSource.Factory should be configured as read-only to avoid downloading that content as well during playback.
To do this you must call setCacheWriteDataSinkFactory(null) on your CacheDataSource.Factory object.
This will prevent the stream from writing to the cache, allowing the downloader to write as expected.
Background
Android supports various audio files encoding and decoding.
I record audio into an audio file using android.media.MediaRecorder class, but I also wish to show information about the files I've recorded (not standard data, but still just text, maybe even configurable by user), and I think it's best to store this information within the files.
examples of possible data to store: when it was recorded, where it was recorded, notes by the user...
The problem
The MediaRecorder class doesn't have any function that I can find, to add or even read metadata of the recorded audio file.
I also can't find a similar class that does it.
What I've tried
I tried searching how to do it for specific files types, and also tried to find a library that does it.
I haven't find even a clue about this information.
The only thing I've found for MediaRecorder class, is a function called "setLocation" , which is used to indicate where the recording has started (geographically), and looking at its code, I can see it sets parameters:
public void setLocation(float latitude, float longitude) {
int latitudex10000 = (int) (latitude * 10000 + 0.5);
int longitudex10000 = (int) (longitude * 10000 + 0.5);
if (latitudex10000 > 900000 || latitudex10000 < -900000) {
String msg = "Latitude: " + latitude + " out of range.";
throw new IllegalArgumentException(msg);
}
if (longitudex10000 > 1800000 || longitudex10000 < -1800000) {
String msg = "Longitude: " + longitude + " out of range";
throw new IllegalArgumentException(msg);
}
setParameter("param-geotag-latitude=" + latitudex10000);
setParameter("param-geotag-longitude=" + longitudex10000);
}
But setParameter is private, and I'm not sure if it's ok to put anything I want into it, even if I had a way to access it (reflection, for example) :
private native void setParameter(String nameValuePair);
I also don't get, given an audio/video file, how to get/modify this kind of information. It's not available for SimpleExoPlayer, for example.
The questions
How can I read,write, and modify metadata inside supported audio files of Android?
Are there any limitations/restrictions for those actions?
Which file formats are available for this?
Is it possible to add the metadata while I record the audio?
Is it possible perhaps via MediaStore ? But then how do I do those operations? And which files are supported? And does the metadata stay within the file?
EDIT: ok I've looked at the solution offered to me (here, repo here, based on here) , and it seems to work well. However, it doesn't work on latest version of the library that it uses (org.mp4parser.isoparser:1.9.37 dependency of mp4parser) , so I leave this question to be answered : Why doesn't it work on latest version of this library?
Code:
object MediaMetaDataUtil {
interface PrepareBoxListener {
fun prepareBox(existingBox: Box?): Box
}
#WorkerThread
fun <T : Box> readMetadata(mediaFilePath: String, boxType: String): T? {
return try {
val isoFile = IsoFile(FileDataSourceImpl(FileInputStream(mediaFilePath).channel))
val nam = Path.getPath<T>(isoFile, "/moov[0]/udta[0]/meta[0]/ilst/$boxType")
isoFile.close()
nam
} catch (e: Exception) {
null
}
}
/**
* #param boxType the type of the box. Example is "©nam" (AppleNameBox.TYPE). More available here: https://kdenlive.org/en/project/adding-meta-data-to-mp4-video/
* #param listener used to prepare the existing or new box
* */
#WorkerThread
#Throws(IOException::class)
fun writeMetadata(mediaFilePath: String, boxType: String, listener: PrepareBoxListener) {
val videoFile = File(mediaFilePath)
if (!videoFile.exists()) {
throw FileNotFoundException("File $mediaFilePath not exists")
}
if (!videoFile.canWrite()) {
throw IllegalStateException("No write permissions to file $mediaFilePath")
}
val isoFile = IsoFile(mediaFilePath)
val moov = isoFile.getBoxes<MovieBox>(MovieBox::class.java)[0]
var freeBox = findFreeBox(moov)
val correctOffset = needsOffsetCorrection(isoFile)
val sizeBefore = moov.size
var offset: Long = 0
for (box in isoFile.boxes) {
if ("moov" == box.type) {
break
}
offset += box.size
}
// Create structure or just navigate to Apple List Box.
var userDataBox: UserDataBox? = Path.getPath(moov, "udta")
if (userDataBox == null) {
userDataBox = UserDataBox()
moov.addBox(userDataBox)
}
var metaBox: MetaBox? = Path.getPath(userDataBox, "meta")
if (metaBox == null) {
metaBox = MetaBox()
val hdlr = HandlerBox()
hdlr.handlerType = "mdir"
metaBox.addBox(hdlr)
userDataBox.addBox(metaBox)
}
var ilst: AppleItemListBox? = Path.getPath(metaBox, "ilst")
if (ilst == null) {
ilst = AppleItemListBox()
metaBox.addBox(ilst)
}
if (freeBox == null) {
freeBox = FreeBox(128 * 1024)
metaBox.addBox(freeBox)
}
// Got Apple List Box
var nam: Box? = Path.getPath(ilst, boxType)
nam = listener.prepareBox(nam)
ilst.addBox(nam)
var sizeAfter = moov.size
var diff = sizeAfter - sizeBefore
// This is the difference of before/after
// can we compensate by resizing a Free Box we have found?
if (freeBox.data.limit() > diff) {
// either shrink or grow!
freeBox.data = ByteBuffer.allocate((freeBox.data.limit() - diff).toInt())
sizeAfter = moov.size
diff = sizeAfter - sizeBefore
}
if (correctOffset && diff != 0L) {
correctChunkOffsets(moov, diff)
}
val baos = BetterByteArrayOutputStream()
moov.getBox(Channels.newChannel(baos))
isoFile.close()
val fc: FileChannel = if (diff != 0L) {
// this is not good: We have to insert bytes in the middle of the file
// and this costs time as it requires re-writing most of the file's data
splitFileAndInsert(videoFile, offset, sizeAfter - sizeBefore)
} else {
// simple overwrite of something with the file
RandomAccessFile(videoFile, "rw").channel
}
fc.position(offset)
fc.write(ByteBuffer.wrap(baos.buffer, 0, baos.size()))
fc.close()
}
#WorkerThread
#Throws(IOException::class)
fun splitFileAndInsert(f: File, pos: Long, length: Long): FileChannel {
val read = RandomAccessFile(f, "r").channel
val tmp = File.createTempFile("ChangeMetaData", "splitFileAndInsert")
val tmpWrite = RandomAccessFile(tmp, "rw").channel
read.position(pos)
tmpWrite.transferFrom(read, 0, read.size() - pos)
read.close()
val write = RandomAccessFile(f, "rw").channel
write.position(pos + length)
tmpWrite.position(0)
var transferred: Long = 0
while (true) {
transferred += tmpWrite.transferTo(0, tmpWrite.size() - transferred, write)
if (transferred == tmpWrite.size())
break
//System.out.println(transferred);
}
//System.out.println(transferred);
tmpWrite.close()
tmp.delete()
return write
}
#WorkerThread
private fun needsOffsetCorrection(isoFile: IsoFile): Boolean {
if (Path.getPath<Box>(isoFile, "moov[0]/mvex[0]") != null) {
// Fragmented files don't need a correction
return false
} else {
// no correction needed if mdat is before moov as insert into moov want change the offsets of mdat
for (box in isoFile.boxes) {
if ("moov" == box.type) {
return true
}
if ("mdat" == box.type) {
return false
}
}
throw RuntimeException("I need moov or mdat. Otherwise all this doesn't make sense")
}
}
#WorkerThread
private fun findFreeBox(c: Container): FreeBox? {
for (box in c.boxes) {
// System.err.println(box.type)
if (box is FreeBox)
return box
if (box is Container) {
val freeBox = findFreeBox(box as Container)
if (freeBox != null) {
return freeBox
}
}
}
return null
}
#WorkerThread
private fun correctChunkOffsets(movieBox: MovieBox, correction: Long) {
var chunkOffsetBoxes = Path.getPaths<ChunkOffsetBox>(movieBox as Box, "trak/mdia[0]/minf[0]/stbl[0]/stco[0]")
if (chunkOffsetBoxes.isEmpty())
chunkOffsetBoxes = Path.getPaths(movieBox as Box, "trak/mdia[0]/minf[0]/stbl[0]/st64[0]")
for (chunkOffsetBox in chunkOffsetBoxes) {
val cOffsets = chunkOffsetBox.chunkOffsets
for (i in cOffsets.indices)
cOffsets[i] += correction
}
}
private class BetterByteArrayOutputStream : ByteArrayOutputStream() {
val buffer: ByteArray
get() = buf
}
}
Sample usage for writing&reading title:
object MediaMetaData {
#JvmStatic
#Throws(IOException::class)
fun writeTitle(mediaFilePath: String, title: String) {
MediaMetaDataUtil.writeMetadata(mediaFilePath, AppleNameBox.TYPE, object : MediaMetaDataUtil.PrepareBoxListener {
override fun prepareBox(existingBox: Box?): Box {
var nam: AppleNameBox? = existingBox as AppleNameBox?
if (nam == null)
nam = AppleNameBox()
nam.dataCountry = 0
nam.dataLanguage = 0
nam.value = title
return nam
}
})
}
#JvmStatic
fun readTitle(mediaFilePath: String): String? {
return MediaMetaDataUtil.readMetadata<AppleNameBox>(mediaFilePath, AppleNameBox.TYPE)?.value
}
}
It seems there's no way to do it uniformly for all supported audio formats in Android. There are some limited options for particular formats though, so I suggest to stick with one format.
MP3 is the most popular one and there should be a lot of options like this one.
If you don't want to deal with encoding/decoding, there are some options for a WAV format.
There's also a way to add a metadata track to a MP4 container using MediaMuxer (you can have an audio-only MP4 file) or like this.
Regarding MediaStore: here's an example (at the end of page 318) on how to add metadata to it just after using MediaRecorder. Though as far as I know the data won't be recorded inside the file.
Update
I compiled an example app using this MP4 parser library and MediaRecorder example from SDK docs. It records an audio, puts it in MP4 container and adds String metadata like this:
MetaDataInsert cmd = new MetaDataInsert();
cmd.writeRandomMetadata(fileName, "lore ipsum tralalala");
Then on the next app launch this metadata is read and displayed:
MetaDataRead cmd = new MetaDataRead();
String text = cmd.read(fileName);
tv.setText(text);
Update #2
Regarding m4a file extension: m4a is just an alias for an mp4 file with AAC audio and has the same file format. So you can use my above example app and just change the file name from audiorecordtest.mp4 to audiorecordtest.m4a and change audio encoder from MediaRecorder.AudioEncoder.AMR_NB to MediaRecorder.AudioEncoder.AAC.