Android Read/Write large file when activity is destoyed - android

When the activity or fragment is destroyed I want to write a large 1-10MB file. Since I want to store the file when the user closes the application I have to do that inside the onPause() method. I use DataOutputStream because it has methods for writing different types: Integer, Float, ByteArray and many more.
Lets say I need to write a large file and it take 2-3 seconds. The code in method onPause() is executed on the main thread. Lets say I freeze the thread for 6s to simulate the thread doing work, using Thread.sleep(6000). If the user try to return to the application the application is not responding for those 6 seconds, because all the work is done in the main thread.
If I start a AsyncTask that run the task on separate thread and do the writing of the file there, the problem with the freezing UI is solved. But as far as I know if the task takes 6s, and the application is destroyed before the thread has finished working, there is a big problem with memory leaks!
So my question is how to write a big file, on separate thread when the user is about to close the application and prevent memory leaks???
Below is example code of writing everything on the main thread:
override fun onStop() {
// code is run in the main thread
saveFile(context!!, "test", "testFile.txt")
}
fun saveFile(context: Context, fileDirectory: String, fileName: String) {
// path to /data/data/yourAppName/app_data/imageDir
val directory: File = context.getDir(fileDirectory, Context.MODE_PRIVATE)
val file = File(directory, fileName)
val fileOutputStream: FileOutputStream
try {
fileOutputStream = FileOutputStream(file)
val dataOutputStream = DataOutputStream(fileOutputStream)
dataOutputStream.writeInt(11) // write integer
dataOutputStream.writeFloat(1.8f) // write float
dataOutputStream.write(byteArrayOf(1, 33, 124, 41)) // write byte array
dataOutputStream.flush()
dataOutputStream.close()
fileOutputStream.flush()
fileOutputStream.close()
} catch (e: java.lang.Exception) {
e.printStackTrace()
}
}
Since I need the context to generate the File and if the activity is destroyed before the thread has finished working that would lead to memory leaks. If I wrap the context in WeakReference does that protect it from memory leaks?? And will the thread finish its work even if the activity/fragment is destroyed??
Below is example of using AsyncTask, and wrap the context in WeakReference!
class SaveFile(context: Context, var fileDirectory: String, var fileName: String) : AsyncTask<Void, Void, Int>() {
private var contextWrapper: WeakReference<ContextWrapper>
init {
val contextWrapper = ContextWrapper(context)
this.contextWrapper = WeakReference(contextWrapper)
}
override fun doInBackground(vararg params: Void): Int {
val directory: File = contextWrapper.get()!!.getDir(fileDirectory, Context.MODE_PRIVATE)
val file = File(directory, fileName)
val fileOutputStream: FileOutputStream
try {
fileOutputStream = FileOutputStream(file)
val dataOutputStream = DataOutputStream(fileOutputStream)
dataOutputStream.writeInt(11) // write integer
dataOutputStream.writeFloat(1.8f) // write float
dataOutputStream.write(byteArrayOf(1, 33, 124, 41)) // write byte array
dataOutputStream.flush()
dataOutputStream.close()
fileOutputStream.flush()
fileOutputStream.close()
} catch (e: java.lang.Exception) {
e.printStackTrace()
}
return 0
}
}

You can use an applicationContext (which you can get from any Context) if you want to make sure you're not holding onto an Activity or whatever. But holding onto an Activity for a few extra seconds isn't really a leak - there's no guarantee you'd even get a garbage collection event in that timespan anyway! It's long-lived references you need to be careful of.
Your WeakReference idea does prevent memory leaks by releasing the Context when nothing else is holding it - the problem is you're treating it as though it always returns the context, by saying
contextWrapper.get()!!
In the event your weak reference does its job and releases the context, this call returns null, and your non-null assertion (!!) fails and crashes your app. So you're swapping your potential memory usage issue for a much bigger one!
If you're ever doing this kind of thing, you need to handle the situation where the object with the weak reference has been garbage collected - that's what you're doing with the weak reference, making it so you can't guarantee you'll have access to it.
So you want something like this instead
val directory: File? = contextWrapper.get()?.getDir(fileDirectory, Context.MODE_PRIVATE)
if (directory == null) return
...
it's up to you how you handle the "no context" path, here I'm just giving up on the file stuff. If that's not an option, and you need that Context so you can do something important, then you can't just hold a weak reference to one.
I know you've already worked something out with services, but this is an important thing to know!

Related

Jetpack Compose - Resizing Image after Image PIcker (ContentResolver Exception?)

I'm just trying to resize an image after the user launches the Image Picker from my app and chooses an image file on the local device (handling a remote image from Dropbox or something will be another battle) and while this has worked for me previously, now I'm getting this exception:
java.lang.RuntimeException: Failure delivering result ResultInfo{who=null, request=1105296364, result=-1, data=Intent { dat=content://com.android.externalstorage.documents/document/primary:Download/20170307_223207_cropped.jpg flg=0x1 }} to activity {my.app/MainActivity}: java.io.FileNotFoundException: No content provider: /document/primary:Download/20170307_223207_cropped.jpg
This occurs after the image is chosen in the Picker, because I'm running my "processing" code to locate the image, resize it, and copy it to a subfolder in the app's folder.
Like I said, this worked, but I'm not sure what's wrong now. I've tried this on the emulator as well as on my Galaxy S10 via USB debugging and it's the same result. The image is in the local storage "Download" folder on the emulator as well as my own device.
The URI looks weird (I mean the picture is just in the local storage "Download" folder) but I'm no URI expert so I assume it's fine, because that's what the Image Picker returns.
Here's the immediate code that's throwing the exception (specifically, the ImageDecoder.decodeBitmap call):
private fun copyFileToAppDataFolder(
context: Context,
imageTempPath: String
): String {
// ensure we are sent at least a non-empty path
if (imageTempPath.isEmpty()) {
return ""
}
val appDataFolder = "${context.dataDir.absolutePath}/images/firearms"
var filename = imageTempPath.substringAfterLast("/", "")
if (filename.isNullOrBlank()) {
filename = imageTempPath.substringAfterLast("%2F", "")
}
// couldn't parse filename from Uri; exit
if (filename.isNullOrBlank()) {
return ""
}
// get a bitmap of the selected image so it can be saved in an outputstream
var selectedImage: Bitmap? = null
selectedImage = if (Build.VERSION.SDK_INT <= 28) {
MediaStore.Images.Media.getBitmap(context.contentResolver, Uri.parse(imageTempPath))
} else {
ImageDecoder.decodeBitmap(ImageDecoder.createSource(context.contentResolver, Uri.parse(imageTempPath)))
}
if (selectedImage == null) {
return ""
}
val destinationImagePath: String = "$appDataFolder/$filename"
val destinationStream = FileOutputStream(destinationImagePath)
selectedImage.compress(Bitmap.CompressFormat.JPEG, 100, destinationStream)
destinationStream.close()
return destinationImagePath
}
That above function is called from my ViewModel (that processFirearmImage function is just calling the one above), where I send the result URI from the image Picker as well as the Application Context:
// this event is fired when the Image Picker returns
is AddEditFirearmEvent.AssignedPicture -> {
val resizedImagePath = ShotTrackerUtility.processFirearmImage(
event.applicationContext, // this is from LocalContext.current in Composable
event.value // result uri from image picker
)
_firearmImageUrl.value = resizedImagePath
}
I don't know, lol. I can't believe this is such a difficult thing but information for this sure seems sparse (for Compose especially, but even so) but I don't really consider launching an Image Picker and resizing the resulting image to be that weird. Any help would be great from you smart people.
Taking a step away from programming problems and coming back seems about the best bet sometimes, lol.
I came back tonight and within a couple minutes noticed that I was sending an improper Uri to the ImageDecoder.createSource method that was causing the exception. Basically this was happening:
val imageTempPath = theUriReturnedFromImagePicker.path ?: ""
ImageDecoder.decodeBitmap(ImageDecoder.createSource(context.contentResolver, Uri.parse(imageTempPath)))
And it should've been:
val imageUrl = theUriReturnedFromImagePicker
ImageDecoder.decodeBitmap(ImageDecoder.createSource(context.contentResolver, imageUri))
As I mentioned in the OP, this originally worked but I must've changed code around a bit (arguments I'm sending to various methods/classes, mostly). I'm also using that Uri.path part to get the filename of the image chosen so I overlooked and/or got confused to what I was sending to ImageDecoder.createSource.
Doh. Maybe someone else will do something dumb like me and this can help.

Using LruCache: Is cache attatched to a LruCache instance?

I might just be confused about how LruCache is supposed to work, but are does it not allow accessing objects from one instance that were saved on another instance? Surely this is not the case otherwise it kind of defeats the purpose of having cache.
Example:
class CacheInterface {
private val lruCache: LruCache<String, Bitmap>
init {
val maxMemory = (Runtime.getRuntime().maxMemory() / 1024).toInt()
// Use 1/8th of the available memory for this memory cache.
val cacheSize = maxMemory / 8
lruCache = object : LruCache<String, Bitmap>(cacheSize) {
override fun sizeOf(key: String, value: Bitmap): Int {
return value.byteCount / 1024
}
}
}
fun getBitmap(key: String): Bitmap? {
return lruCache.get(key)
}
fun storeBitmap(key: String, bitmap: Bitmap) {
lruCache.put(key, bitmap)
Utils.log(lruCache.get(key))
}
}
val bitmap = getBitmal()
val instance1 = CacheInterface()
instance1.storeBitmap("key1", bitmap)
log(instance1.getBitmap("key1")) //android.graphics.Bitmap#6854e91
log(CacheInterface().getBitmap("key1")) //null
As far as I understand, cache is stored until it's deleted by the user (manually or uninstalling the app), or cleared by the system when it exceeds the allowed space. What am I missing?
An LruCache object just stores references to objects in memory. As soon as you lose the reference to the LruCache, the LruCache object and all of the objects within that cache are garbage collected. There's nothing stored to disk.
Yes it is. I'll just share here what I was confused about in case anyone also is.
Initially because of this guide (Caching Bitmaps) that reccomends using LruCache, I was left under the impression that LruCache was an interface to access app's cache, but like #CommonsWare mentioned it has no I/O in it - it's just a utility class to hold memory using the LRU policy. To access your app's cache you need to use Context.getCacheDir(), good explanation here. In my case I ended up using a singleton of LruCache, since I already have a service running most of the time the app will not be killed every time it's closed.
log(CacheInterface().getBitmap("key1")) //null
equals
val instance2 = CacheInterface()
log(instance2 .getBitmap("key1"))
instance1 != instance2
change to Singleton
object CacheInterface{
...
}
use
CacheInterface.storeBitmap("key1",bitmap)
CacheInterface.getBitmap("key1")

Flutters resource load faster than native Android

I'm trying to convert image taken from resources to ByteArray which
will later be send through Socket. I've been measuring time of each of this conversion.
I've done it on both Flutter and native Android (Kotlin). All of the test were done on the same image which was about 1-2MB.
Flutter code :
sendMessage() async {
if (socket != null) {
Stopwatch start = Stopwatch()..start();
final imageBytes = await rootBundle.load('assets/images/stars.jpg');
final image = base64Encode(imageBytes.buffer.asUint8List(imageBytes.offsetInBytes, imageBytes.lengthInBytes));
print('Converting took ${start.elapsedMilliseconds}');
socket.emit("message", [image]);
}
}
Kotlin code:
private fun sendMessage() {
var message = ""
val thread = Thread(Runnable {
val start = SystemClock.elapsedRealtime()
val bitmap = BitmapFactory.decodeResource(resources, R.drawable.stars)
message = Base64.encodeToString(getBytesFromBitmap(bitmap), Base64.DEFAULT)
Log.d("Tag", "Converting time was : ${SystemClock.elapsedRealtime() - start}")
})
thread.start()
thread.join()
socket.emit("message", message)
}
private fun getBytesFromBitmap(bitmap: Bitmap): ByteArray? {
val stream = ByteArrayOutputStream()
bitmap.compress(Bitmap.CompressFormat.JPEG, 100, stream)
return stream.toByteArray()
}
I've been actually expecting native code to be much much faster than Flutter's but thats not the case.. Conversion for Flutter takes about 50ms and its around 2000-3000ms for native.
I thought that Threading may be the case, so I've tried to run this conversion on background thread for native code but it didn't help.
Can you please tell me why is there such a different in time, and how I can implement it better in native code? Is there a way to omit casting to Bitmap etc.? Maybe this makes it so long.
EDIT. Added getBytesFromBitmap function
the difference you see is that in flutter code you just read your data without any image decoding, while in kotlin you are first decoding to Bitmap and then you are compress()ing it back - if you want to speed it up simply get an InputStream by calling Resources#openRawResource and read your image resource without any decoding
It have something to do with the way you convert it to bytes... Can you please post your
getBytesFromBitmap func? Plus, the conversion in native code really should be done in background thread, please upload the your results in this case.

How do I save/load an object? I've done a lot of searching and can't figure out what's wrong

I've been searching for an answer for a very long time and I know this has been answered, but none of the "correct" answers have worked for me.
In my MainActivity I do:
SaveData save = SaveData.load(getApplicationContext());
and then I pull values out of it.
In SaveData I have:
public void save(Context context)
{
try {
FileOutputStream fos = context.openFileOutput("savedata", Context.MODE_PRIVATE);
ObjectOutputStream os = new ObjectOutputStream(fos);
os.writeObject(this);
os.close();
fos.close();
}
catch (IOException e)
{
e.printStackTrace();
}
}
public static SaveData load(Context context)
{
try {
FileInputStream fis = context.openFileInput("savedata");
ObjectInputStream is = new ObjectInputStream(fis);
SaveData save = (SaveData) is.readObject();
is.close();
fis.close();
return save;
}
catch (Exception e)
{
e.printStackTrace();
return null;
}
}
In AndroidManifest.xml I have:
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"></uses-permission>
Every time I launch the app it crashes immediately (null pointer - obviously the file isn't being loaded properly). I have no clue what's wrong with my code and I've tried at least half a dozen different "correct" answers. Does anyone know what I might be doing wrong?
EDIT: I think I've serialized everything properly. SaveData implements Serializable and has these variable types: Class1, int, int, boolean, boolean, boolean, int. Class1 implements Serializable and has these variable types: ArrayList[Class2], ArrayList[Class2]. Class2 implements Serializable and has these variable types: String, double, double, double, String, int, int, int, int, int, int, int, int, int, int, int, int, int, int, ArrayList[Class3]. Class3 implement Serializable and has these variables: String, double, double, double, String, String, Uri, File.
I don't know much about serialization, so it would make sense that I messed up that part. Maybe Uri and File aren't serializable?
I don't know much about saving/loading in an Android application aside from what I've read online over the last few days. In my head it was as simple as "I have this Object, I am going to throw it into a file in the phone, when I start the app again later I'll grab that file and pull the Object out, simple." Clearly it's not that simple, but I'm not sure what part I've got wrong.
1-Did your class implemented as serializable ?
2-You don't need external storage permission since you are writing objects into the internal.
I've worked out a solution:
My save/load system seemed to be working previously, then I added some additional features and it stopped working. After Abdullah Tellioglu brought up the idea that the problem might be with my serialization I did a quick Google search and realized that Uri is not serializable, so instead of saving the Uri with my Object I saved the File and used Uri.fromFile(fileVariable) wherever I need the Uri.
After fixing that problem I simply took this:
SaveData save = SaveData.load(getApplicationContext());
//Pull out variables
and changed it to this:
SaveData save = SaveData.load(getApplicationContext());
if (save != null)
//Pull out variables
so there's no problems if it's the first run and a save file doesn't exist yet.
Now everything works. It looks like the code for saving and loading was correct, but I had errors in other places.
Thank you for your help.

Android HashMap Serialization / Deserialization

Hello I have a Hasmap of bitmaps which I need to store on the Android device to be used when next the application starts.
My hashmap looks like this, and contains up to 1000 Bitmaps:
private static HashMap <String, Bitmap> cache = new HashMap<String, Bitmap>();
You might want to consider create extension of Map (by using AbstractMap) and override the related functions. In general the structure of the extension should have:
An in memory hard cache using regular Map. This should be a size bound cache object. You could leverage LinkedHashMap and override removeEldesEntry() to check if the size is exceeded
this.objectMap = Collections.synchronizedMap(new LinkedHashMap() {
#Override
protected boolean removeEldestEntry(LinkedHashMap.Entry eldest) {
if (size() > HARD_CACHE_CAPACITY) {
// remove from cache, pass to secondary SoftReference cache or directly to the disk
}
}
});
If the cache is exceeded, then put it to disk
Override the get function to do the following : On initial get, load the bitmap from disk based on certain naming convention (related to the key) and store it in memory. Roughly something like (please forgive any syntax error)
#Override
public Bitmap get(Object key) {
if(key != null) {
// first level, hard cache
if(objectMap.containsKey(key)) {
return objectMap.get(key);
}
// soft reference cache
if(secondaryCache.containsKey(key)) {
return secondaryCache.get(key);
}
// get from disk if it is not in hard or soft cache
String fileName = "Disk-" + key + ".txt";
File f = new File(cacheDir, fileName);
if(f.exists()) {
// put this back to the hard cache
Bitmap object = readFromReader(f);
if(object != null) {
objectMap.put((String)key, object);
return object;
}
}
}
return null; // unable to get from any data source
}
Similarly your put has to be override to put to the disk for later use, so when you reinitialize your app you could just create an instance of the map extension. If you want, you could also preload the hashmap by most recently used items in the app. Basically, by extending the AbstractMap, you get the flexibilities without killing your memory with that 1000 bitmaps. Hope this helps

Categories

Resources