I've got an Android app written in Kotlin targeting framework 30+, so I'm working within the new Android 11 file access restrictions. The app needs to be able to open an arbitrary .zip file in the shared storage (chosen interactively by the user) then do stuff with the contents of that .zip file.
I'm getting a URI for the .zip file in what I'm led to understand is the canonical way:
val activity = this
val getContent = registerForActivityResult(ActivityResultContracts.GetContent()) {
CoroutineScope(Dispatchers.Main).launch {
if(it != null) doStuffWithZip(activity, it)
...
}
}
getContent.launch("application/zip")
My problem is that the Java.util.zip.ZipFile class I'm using only knows how to open a .zip file specified by a String or a File, and I don't have any easy way to get to either of those from a Uri. (I'm guessing that the ZipFile object needs the actual file rather than some kind of stream because it needs to be able to seek...)
The workaround I'm using at present is to turn the Uri into an InputStream, copy the contents to a temp file in private storage, and make a ZipFile instance from that:
private suspend fun <T> withZipFromUri(
context: Context,
uri: Uri, block: suspend (ZipFile) -> T
) : T {
val file = File(context.filesDir, "tempzip.zip")
try {
return withContext(Dispatchers.IO) {
kotlin.runCatching {
context.contentResolver.openInputStream(uri).use { input ->
if (input == null) throw FileNotFoundException("openInputStream failed")
file.outputStream().use { input.copyTo(it) }
}
ZipFile(file, ZipFile.OPEN_READ).use { block.invoke(it) }
}.getOrThrow()
}
} finally {
file.delete()
}
}
Then, I can use it like this:
suspend fun doStuffWithZip(context: Context, uri: Uri) {
withZipFromUri(context, uri) { // it: ZipFile
for (entry in it.entries()) {
dbg("entry: ${entry.name}") // or whatever
}
}
}
This works, and (in my particular case, where the .zip file in question is never more than a couple MB) is reasonably performant.
But, I tend to regard programming by temporary file as the last refuge of the terminally incompetent, thus I can't escape the feeling that I'm missing a trick here. (Admittedly, I am terminally incompetent in the context of Android + Kotlin, but I'd like to learn to not be...)
Any better ideas? Is there a cleaner way to implement this that doesn't involve making an extra copy of the file?
Copying from external source (and risking downvoting to oblivion) and this isn't quite an answer, but too long for a comment
public class ZipFileUnZipExample {
public static void main(String[] args) {
Path source = Paths.get("/home/mkyong/zip/test.zip");
Path target = Paths.get("/home/mkyong/zip/");
try {
unzipFolder(source, target);
System.out.println("Done");
} catch (IOException e) {
e.printStackTrace();
}
}
public static void unzipFolder(Path source, Path target) throws IOException {
// Put the InputStream obtained from Uri here instead of the FileInputStream perhaps?
try (ZipInputStream zis = new ZipInputStream(new FileInputStream(source.toFile()))) {
// list files in zip
ZipEntry zipEntry = zis.getNextEntry();
while (zipEntry != null) {
boolean isDirectory = false;
// example 1.1
// some zip stored files and folders separately
// e.g data/
// data/folder/
// data/folder/file.txt
if (zipEntry.getName().endsWith(File.separator)) {
isDirectory = true;
}
Path newPath = zipSlipProtect(zipEntry, target);
if (isDirectory) {
Files.createDirectories(newPath);
} else {
// example 1.2
// some zip stored file path only, need create parent directories
// e.g data/folder/file.txt
if (newPath.getParent() != null) {
if (Files.notExists(newPath.getParent())) {
Files.createDirectories(newPath.getParent());
}
}
// copy files, nio
Files.copy(zis, newPath, StandardCopyOption.REPLACE_EXISTING);
// copy files, classic
/*try (FileOutputStream fos = new FileOutputStream(newPath.toFile())) {
byte[] buffer = new byte[1024];
int len;
while ((len = zis.read(buffer)) > 0) {
fos.write(buffer, 0, len);
}
}*/
}
zipEntry = zis.getNextEntry();
}
zis.closeEntry();
}
}
// protect zip slip attack
public static Path zipSlipProtect(ZipEntry zipEntry, Path targetDir)
throws IOException {
// test zip slip vulnerability
// Path targetDirResolved = targetDir.resolve("../../" + zipEntry.getName());
Path targetDirResolved = targetDir.resolve(zipEntry.getName());
// make sure normalized file still has targetDir as its prefix
// else throws exception
Path normalizePath = targetDirResolved.normalize();
if (!normalizePath.startsWith(targetDir)) {
throw new IOException("Bad zip entry: " + zipEntry.getName());
}
return normalizePath;
}
}
This apparently works with pre-existing files; however since you already have an InputStream read from the Uri - you can adapt this and give it a shot.
EDIT:
It seems like it's extracting to Files as well - you could store the individual ByteArrays somewhere then decide what to do with them later on. But I hope you get the general idea - you can do all of this in-memory, without having to use the disk (temp files or files) in between.
Your requirement is a bit vague and unclear however, so I don't know what you're trying to do, merely suggesting a venue/approach to try out
What about a simple ZipInputStream ? –
Shark
Good idea #Shark.
InputSteam is = getContentResolver().openInputStream(uri);
ZipInputStream zis = new ZipInputStream(is);
#Shark has it with ZipInputStream. I'm not sure how I missed that to begin with, but I sure did.
My withZipFromUri() method is much simpler and nicer now:
suspend fun <T> withZipFromUri(
context: Context,
uri: Uri, block: suspend (ZipInputStream) -> T
) : T =
withContext(Dispatchers.IO) {
kotlin.runCatching {
context.contentResolver.openInputStream(uri).use { input ->
if (input == null) throw FileNotFoundException("openInputStream failed")
ZipInputStream(input).use {
block.invoke(it)
}
}
}.getOrThrow()
}
This isn't call-compatible with the old one (since the block function now takes a ZipInputStream as a parameter rather than a ZipFile). In my particular case -- and really, in any case where the consumer doesn't mind dealing with entries in the order they appear -- that's OK.
Okio (3-Alpha) has a ZipFileSystem https://github.com/square/okio/blob/master/okio/src/jvmMain/kotlin/okio/ZipFileSystem.kt
You could probably combine it with a custom FileSystem that reads the content of that file. It will require a fair bit of code but will be efficient.
This is an example of a custom filesystem https://github.com/square/okio/blob/88fa50645946bc42725d2f33e143628e7892be1b/okio/src/jvmMain/kotlin/okio/internal/ResourceFileSystem.kt
But I suspect it's simpler to convert the URI to a file and avoid any copying or additional code.
It's easy to check the .zip and .rar files in the Android-Kotlin FileAdapter(work with file manager), add the bellow function to your code:
private fun isZip(name: String): Boolean {
return name.contains(".zip") || name.contains(".rar")
}
Related
I am developing an Android Library that contains some files with data. I want to package this library into an AAR file and distribute it. I'm having trouble opening the resource files within this library.
Here is a scenario that is analogous to mine:
In the Library there is a file labels.txt which is stored in res/raw.
I have a class LabelReader that sits inside the library. It has a function read() that will return the string contents of labels.txt.
package com.library.mine
class LabelReader {
fun read():string {
val resourceID = Resources.getSystem().getIdentifier("labels.txt", "raw", "com.library.mine") //returns 0
//Exception here
val br = BufferedReader(InputStreamReader(Resources.getSystem().openRawResource(identifier)))
...
}
}
I try to use the function in the app and that triggers the exception:
package com.app.mine
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val labelReader = LabelReader()
val labels = LabelReader.read() //problem
}
}
From what I have read it's not possible to include the file in the assets directory of an android library.
Most questions I have googled seem to make use of the Activity's Context, but that's not available to me here.
What is the correct way to handle this?
In my AAR libraries, I use to include and use raw resources. To use them, in the library or in the client application you need to do the same things:
Use the openRawResource method of Resources object (from Context).
Use raw resource name (letter/number and _) without the file extension.
A simple method to read raw resource content as text:
public static String readRawTextFile(Context context, int rawResourceId) throws IOException {
StringBuilder buffer = new StringBuilder();
InputStream inputStream = context.getResources().openRawResource(rawResourceId);
BufferedReader in = new BufferedReader(new InputStreamReader(inputStream));
String read = in.readLine();
while (read != null) {
buffer.append(read + "\n");
read = in.readLine();
}
inputStream.close();
buffer.deleteCharAt(buffer.length() - 1);
return buffer.toString();
}
I hope this helps.
I am working with Skobbler SDK 2.3 and I am wondering how to update the map styles in SKMaps.zip.
The problem:
I modified the contained "daystyle", but I noticed that this only takes effect after deleting the app data. This is really not what I want. It looks like SKPrepareMapTextureThread(android.content.Context context, java.lang.String mapTexturesPath,java.lang.String zipName, SKPrepareMapTextureListener listener).start()
only copies the files if SKMaps folder doesn't exists. Does anybody know if there is such a check inside the start() method, or (better) how to deliver modified SKMap styles to the users?
SKPrepareMapTextureThread only copies the files if SKMaps folder doesn't exists and this is how it is intended to be, as the unziping of the map resources takes a rather long time and is intended to be executed only once.
To update a style a workaround will be required:
1) delete the file .installed.txt from map resources path and call SKPrepareMapTextureThread so that the resources will be restored from assets. Although this is the easiest way, it is also the most time consuming:
final File txtPreparedFile = new File(mapResourcesDirPath + "/.installed.txt");
if (!txtPreparedFile.exists()) {
txtPreparedFile.delete();
}
new SKPrepareMapTextureThread(context, mapResourcesDirPath, "SKMaps.zip", skPrepareMapTextureListener).start();
2) a more optimal approach would be to write a routine that replace the old style with the new one
copyFile(new File("path/daystyle.json"), new File("mapResourcesDirPath/SKMaps/daystyle/daystyle.json"));
...
public static void copyFile(File sourceFile, File destFile) throws IOException {
if(!destFile.exists()) {
destFile.createNewFile();
}
FileChannel source = null;
FileChannel destination = null;
try {
source = new FileInputStream(sourceFile).getChannel();
destination = new FileOutputStream(destFile).getChannel();
// previous code: destination.transferFrom(source, 0, source.size());
// to avoid infinite loops, should be:
long count = 0;
long size = source.size();
while((count += destination.transferFrom(source, count, size-count))<size);
}
finally {
if(source != null) {
source.close();
}
if(destination != null) {
destination.close();
}
}
}
Is there a Java equivalent for System.IO.Path.Combine() in C#/.NET? Or any code to accomplish this?
This static method combines one or more strings into a path.
Rather than keeping everything string-based, you should use a class which is designed to represent a file system path.
If you're using Java 7 or Java 8, you should strongly consider using java.nio.file.Path; Path.resolve can be used to combine one path with another, or with a string. The Paths helper class is useful too. For example:
Path path = Paths.get("foo", "bar", "baz.txt");
If you need to cater for pre-Java-7 environments, you can use java.io.File, like this:
File baseDirectory = new File("foo");
File subDirectory = new File(baseDirectory, "bar");
File fileInDirectory = new File(subDirectory, "baz.txt");
If you want it back as a string later, you can call getPath(). Indeed, if you really wanted to mimic Path.Combine, you could just write something like:
public static String combine(String path1, String path2)
{
File file1 = new File(path1);
File file2 = new File(file1, path2);
return file2.getPath();
}
In Java 7, you should use resolve:
Path newPath = path.resolve(childPath);
While the NIO2 Path class may seem a bit redundant to File with an unnecessarily different API, it is in fact subtly more elegant and robust.
Note that Paths.get() (as suggested by someone else) doesn't have an overload taking a Path, and doing Paths.get(path.toString(), childPath) is NOT the same thing as resolve(). From the Paths.get() docs:
Note that while this method is very convenient, using it will imply an assumed reference to the default FileSystem and limit the utility of the calling code. Hence it should not be used in library code intended for flexible reuse. A more flexible alternative is to use an existing Path instance as an anchor, such as:
Path dir = ...
Path path = dir.resolve("file");
The sister function to resolve is the excellent relativize:
Path childPath = path.relativize(newPath);
The main answer is to use File objects. However Commons IO does have a class FilenameUtils that can do this kind of thing, such as the concat() method.
platform independent approach (uses File.separator, ie will works depends on operation system where code is running:
java.nio.file.Paths.get(".", "path", "to", "file.txt")
// relative unix path: ./path/to/file.txt
// relative windows path: .\path\to\filee.txt
java.nio.file.Paths.get("/", "path", "to", "file.txt")
// absolute unix path: /path/to/filee.txt
// windows network drive path: \\path\to\file.txt
java.nio.file.Paths.get("C:", "path", "to", "file.txt")
// absolute windows path: C:\path\to\file.txt
I know its a long time since Jon's original answer, but I had a similar requirement to the OP.
By way of extending Jon's solution I came up with the following, which will take one or more path segments takes as many path segments that you can throw at it.
Usage
Path.combine("/Users/beardtwizzle/");
Path.combine("/", "Users", "beardtwizzle");
Path.combine(new String[] { "/", "Users", "beardtwizzle", "arrayUsage" });
Code here for others with a similar problem
public class Path {
public static String combine(String... paths)
{
File file = new File(paths[0]);
for (int i = 1; i < paths.length ; i++) {
file = new File(file, paths[i]);
}
return file.getPath();
}
}
To enhance JodaStephen's answer, Apache Commons IO has FilenameUtils which does this. Example (on Linux):
assert org.apache.commons.io.FilenameUtils.concat("/home/bob", "work\\stuff.log") == "/home/bob/work/stuff.log"
It's platform independent and will produce whatever separators your system needs.
Late to the party perhaps, but I wanted to share my take on this. I prefer not to pull in entire libraries for something like this. Instead, I'm using a Builder pattern and allow conveniently chained append(more) calls. It even allows mixing File and String, and can easily be extended to support Path as well. Furthermore, it automatically handles the different path separators correctly on both Linux, Macintosh, etc.
public class Files {
public static class PathBuilder {
private File file;
private PathBuilder ( File root ) {
file = root;
}
private PathBuilder ( String root ) {
file = new File(root);
}
public PathBuilder append ( File more ) {
file = new File(file, more.getPath()) );
return this;
}
public PathBuilder append ( String more ) {
file = new File(file, more);
return this;
}
public File buildFile () {
return file;
}
}
public static PathBuilder buildPath ( File root ) {
return new PathBuilder(root);
}
public static PathBuilder buildPath ( String root ) {
return new PathBuilder(root);
}
}
Example of usage:
File root = File.listRoots()[0];
String hello = "hello";
String world = "world";
String filename = "warez.lha";
File file = Files.buildPath(root).append(hello).append(world)
.append(filename).buildFile();
String absolute = file.getAbsolutePath();
The resulting absolute will contain something like:
/hello/world/warez.lha
or maybe even:
A:\hello\world\warez.lha
If you do not need more than strings, you can use com.google.common.io.Files
Files.simplifyPath("some/prefix/with//extra///slashes" + "file//name")
to get
"some/prefix/with/extra/slashes/file/name"
Here's a solution which handles multiple path parts and edge conditions:
public static String combinePaths(String ... paths)
{
if ( paths.length == 0)
{
return "";
}
File combined = new File(paths[0]);
int i = 1;
while ( i < paths.length)
{
combined = new File(combined, paths[i]);
++i;
}
return combined.getPath();
}
This also works in Java 8 :
Path file = Paths.get("Some path");
file = Paths.get(file + "Some other path");
This solution offers an interface for joining path fragments from a String[] array. It uses java.io.File.File(String parent, String child):
public static joinPaths(String[] fragments) {
String emptyPath = "";
return buildPath(emptyPath, fragments);
}
private static buildPath(String path, String[] fragments) {
if (path == null || path.isEmpty()) {
path = "";
}
if (fragments == null || fragments.length == 0) {
return "";
}
int pathCurrentSize = path.split("/").length;
int fragmentsLen = fragments.length;
if (pathCurrentSize <= fragmentsLen) {
String newPath = new File(path, fragments[pathCurrentSize - 1]).toString();
path = buildPath(newPath, fragments);
}
return path;
}
Then you can just do:
String[] fragments = {"dir", "anotherDir/", "/filename.txt"};
String path = joinPaths(fragments);
Returns:
"/dir/anotherDir/filename.txt"
Assuming all given paths are absolute paths. you can follow below snippets to merge these paths.
String baseURL = "\\\\host\\testdir\\";
String absoluteFilePath = "\\\\host\\testdir\\Test.txt";;
String mergedPath = Paths.get(baseURL, absoluteFilePath.replaceAll(Matcher.quoteReplacement(baseURL), "")).toString();
output path is \\host\testdir\Test.txt.
I'm writing a word game in Android. It's my first app so my knowledge is almost non-existent.
What I would like to do is use JWI to access the WordNet dictionary. This requires specifying the WordNet dictionary's file path.
From what I've read, Android "assets" are not available via a simple file path, but what JWI requires to initialize the WordNet dictionary API is a URL to the disk location of the dictionary files.
So, what is the best course of action? Should I copy the assets at startup-time into a known folder on the android device? I can't think of a better way but that seems entirely stupid to me.
Any help gratefully received.
I have the same problem (for a jetty webapp however and not android) and tried those two approaches, however unsuccessfully:
JWNL.initialize(this.getClass().getClassLoader().getResourceAsStream("wordnet_properties.xml");
dict = Dictionary.getInstance();
Here it successfully loads wordnet_properties.xml but it cannot access the dictionary which is pointed to by the properties file.
Using the dictionary folder directly:
String dictPath = "models/en/wordnet/dict/";
URL url = this.getClass().getClassLoader().getResource(dictPath);
System.out.println("loading wordnet from "+url);
dict = new RAMDictionary(url, ILoadPolicy.NO_LOAD);
Here I get the dictionary URL to be jar:file:/home/myusername/.m2/repository/package/1.0-SNAPSHOT/commons-1.0-SNAPSHOT.jar!/models/en/wordnet/dict/. WordNet however doesn't accept the jar protocol and gives me the error:
java.lang.IllegalArgumentException: URL source must use 'file' protocol
at edu.mit.jwi.data.FileProvider.toFile(FileProvider.java:693)
at edu.mit.jwi.data.FileProvider.open(FileProvider.java:304)
at edu.mit.jwi.DataSourceDictionary.open(DataSourceDictionary.java:92)
at edu.mit.jwi.RAMDictionary.open(RAMDictionary.java:216)
My next investigation will be to create a subclass to RAMDictionary or something similar, please tell me if you have found a solution in the meantime.
P.S.: I just wrote the developer a mail asking for help after I tried to rewrite the FileProvider to use resources instead but after one or two hours I gave up because the code calls so much other code that also only works with files. I will keep you up to date!
P.P.S.: I received an answer from the developer saying that it is principially not possible with streams because they don't offer random access which is necessary. However, he offered to implement a solution to load it all in RAM, if really necessary, but that would use up about 500 MB and I guess that is too much for android apps so I guess it is still best to unpack it somewhere.
P.S.: Here is my unpacking solution (you can replace the System.out.println statements with logger statements if you use logging or remove them if you don't like them):
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.URISyntaxException;
import java.util.Enumeration;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;
/** Allows WordNet to be run from within a jar file by unpacking it to a temporary directory.**/
public class WordNetUnpacker
{
static final String ID = "178558556719"; // minimize the chance of interfering with an existing directory
static final String jarDir = "models/en/wordnet/dict";
/**If running from within a jar, unpack wordnet from the jar to a temp directory (if not already done) and return that.
* If not running from a jar, just return the existing wordnet directory.
* #see getUnpackedWordNetDir(Class)*/
static File getUnpackedWordNetDir() throws IOException
{return getUnpackedWordNetDir(WordNetUnpacker.class);}
/**If running from within a jar, unpack wordnet from the jar to a temp directory (if not already done) and return that.
* If not running from a jar, just return the existing wordnet directory.
* #param clazz the class in whose classloader the wordnet resources are found.
* #see getUnpackedWordNetDir()**/
static File getUnpackedWordNetDir(Class clazz) throws IOException
{
String codeSource = clazz.getProtectionDomain().getCodeSource().getLocation().getPath();
System.out.println("getUnpackedWordNetDir: using code source "+codeSource);
if(!codeSource.endsWith(".jar"))
{
System.out.println("not running from jar, no unpacking necessary");
try{return new File(WordNetUnpacker.class.getClassLoader().getResource(jarDir).toURI());}
catch (URISyntaxException e) {throw new IOException(e);}
}
try(JarFile jarFile = new JarFile(codeSource))
{
String tempDirString = System.getProperty("java.io.tmpdir");
if(tempDirString==null) {throw new IOException("java.io.tmpdir not set");}
File tempDir = new File(tempDirString);
if(!tempDir.exists()) {throw new IOException("temporary directory does not exist");}
if(!tempDir.isDirectory()) {throw new IOException("temporary directory is a file, not a directory ");}
File wordNetDir = new File(tempDirString+'/'+"wordnet"+ID);
wordNetDir.mkdir();
System.out.println("unpacking jarfile "+jarFile.getName());
copyResourcesToDirectory(jarFile, jarDir, wordNetDir.getAbsolutePath());
return wordNetDir;
}
}
/** Copies a directory from a jar file to an external directory. Copied from Stack Overflow. */
public static void copyResourcesToDirectory(JarFile fromJar, String jarDir, String destDir) throws IOException
{
int copyCount = 0;
for (Enumeration<JarEntry> entries = fromJar.entries(); entries.hasMoreElements();)
{
JarEntry entry = entries.nextElement();
if(!entry.getName().contains("models")) continue;
if (entry.getName().startsWith(jarDir) && !entry.isDirectory()) {
copyCount++;
File dest = new File(destDir + "/" + entry.getName().substring(jarDir.length() + 1));
File parent = dest.getParentFile();
if (parent != null) {
parent.mkdirs();
}
FileOutputStream out = new FileOutputStream(dest);
InputStream in = fromJar.getInputStream(entry);
try {
byte[] buffer = new byte[8 * 1024];
int s = 0;
while ((s = in.read(buffer)) > 0) {
out.write(buffer, 0, s);
}
} catch (IOException e) {
throw new IOException("Could not copy asset from jar file", e);
} finally {
try {
in.close();
} catch (IOException ignored) {}
try {
out.close();
} catch (IOException ignored) {}
}
}
}
if(copyCount==0) System.out.println("Warning: No files copied!");
}
}
You can just copy all dict files from "assets" to the internal directory of your app. Just do it once, on the first app launch.
Since then you can use JWI in a causual way like this:
String path = getFilesDir() + "/dict";
URL url = new URL("file", null, path);
IDictionary dict = new Dictionary(url);
I am writing a app which can programatically clear application cache of all the third party apps installed on the device. Following is the code snippet for Android 2.2
public static void trimCache(Context myAppctx) {
Context context = myAppctx.createPackageContext("com.thirdparty.game",
Context.CONTEXT_INCLUDE_CO|Context.CONTEXT_IGNORE_SECURITY);
File cachDir = context.getCacheDir();
Log.v("Trim", "dir " + cachDir.getPath());
if (cachDir!= null && cachDir.isDirectory()) {
Log.v("Trim", "can read " + cachDir.canRead());
String[] fileNames = cachDir.list();
//Iterate for the fileName and delete
}
}
My manifest has following permissions:
android.permission.CLEAR_APP_CACHE
android.permission.DELETE_CACHE_FILES
Now the problem is that the name of the cache directory is printed but the list of files cachDir.list() always returns null. I am not able to delete the cache directory since the file list is always null.
Is there any other way to clear the application cache?
"android.permission.CLEAR_APP_CACHE" android.permission.DELETE_CACHE_FILES"
Ordinary SDK applications cannot hold the DELETE_CACHE_FILES permission. While you can hold CLEAR_APP_CACHE, there is nothing in the Android SDK that allows you to clear an app's cache.
Is there any other way to clear the application cache?
You are welcome to clear your own cache by deleting the files in that cache.
Check out android.content.pm.PackageManager.clearApplicationUserData: http://grepcode.com/file/repository.grepcode.com/java/ext/com.google.android/android/2.3.3_r1/android/content/pm/PackageManager.java/
The other hidden methods in that class might be useful, too.
In case you've never used hidden methods before, you can access hidden methods using Java reflection.
poate iti merge asta
static int clearCacheFolder(final File dir, final int numDays) {
int deletedFiles = 0;
if (dir!= null && dir.isDirectory()) {
try {
for (File child:dir.listFiles()) {
//first delete subdirectories recursively
if (child.isDirectory()) {
deletedFiles += clearCacheFolder(child, numDays);
}
//then delete the files and subdirectories in this dir
//only empty directories can be deleted, so subdirs have been done first
if (child.lastModified() < new Date().getTime() - numDays * DateUtils.DAY_IN_MILLIS) {
if (child.delete()) {
deletedFiles++;
}
}
}
}
catch(Exception e) {
Log.e("ATTENTION!", String.format("Failed to clean the cache, error %s", e.getMessage()));
}
}
return deletedFiles;
}
public static void clearCache(final Context context, final int numDays) {
Log.i("ADVL", String.format("Starting cache prune, deleting files older than %d days", numDays));
int numDeletedFiles = clearCacheFolder(context.getCacheDir(), numDays);
Log.i("ADVL", String.format("Cache pruning completed, %d files deleted", numDeletedFiles));
}
I'm not sure how appropriate this is in terms of convention, but this works so far for me in my Global Application class:
File[] files = cacheDir.listFiles();
for (File file : files){
file.delete();
}
Of course, this doesn't address nested directories, which might be done with a recursive function like this (not tested extensively with subdirectories):
deleteFiles(cacheDir);
private void deleteFiles(File dir){
if (dir != null){
if (dir.listFiles() != null && dir.listFiles().length > 0){
// RECURSIVELY DELETE FILES IN DIRECTORY
for (File file : dir.listFiles()){
deleteFiles(file);
}
} else {
// JUST DELETE FILE
dir.delete();
}
}
}
I didn't use File.isDirectory because it was unreliable in my testing.