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.
Related
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")
}
So I'm trying to read data from a .json file in my Android project:
val file = context.assets.open("myfile.json").bufferedReader().readText()
This works fine and successfully prints out my .json file as one String.
However I want to know what exactly is bufferedReader() and why .readText() can't be called directly on the opened .json file.
PS: readText() returns a String. However:
val json2: JSONObject = JSONObject("mystring") returns:
Caused by: org.json.JSONException: Value mystring of type java.lang.String cannot be converted to JSONObject
How does this make sense?
The readText function is defined as an extension on Reader:
public fun Reader.readText(): String {
val buffer = StringWriter()
copyTo(buffer)
return buffer.toString()
}
An InputStream isn't a Reader, so you have to transform it into some Reader:
public inline fun InputStream.reader(charset: Charset = Charsets.UTF_8): InputStreamReader =
InputStreamReader(this, charset)
You can use the reader as a buffered reader with the alternative bufferedReader function:
public inline fun InputStream.bufferedReader(charset: Charset = Charsets.UTF_8): BufferedReader =
reader(charset).buffered()
Reader and also BufferedReader are part of the Java standard library and the buffered version is described like this:
Reads text from a character-input stream, buffering characters so as to
provide for the efficient reading of characters, arrays, and lines.
In general, each read request made of a Reader causes a corresponding read request to be made of the underlying character or byte stream. It is
therefore advisable to wrap a BufferedReader around any Reader whose read()
operations may be costly, such as FileReaders and InputStreamReaders...
It basically wraps a Reader and adds support for reading single lines etc.
val file = context.assets.open("myfile.json").bufferedReader().readText()
Here is the code that do the same thing as above line.
val inputStream = context.assets.open("myfile.json")
val reader = inputStream.bufferedReader()
val file = reader.readText()
Assume here is the content of myfile.json
{
"os": "Android",
"version": "KitKat",
"codeName": 4.4
}
Let's go step by step
Step 1: The first line
val inputStream = context.assets.open("myfile.json")
This will be return an InputStream object, which reads one byte or number of bytes from the json file. If you print the json file content in byte format on the screen, it will really hard for us (as programmers) to read.
Step 2: The second line
val reader = inputStream.bufferedReader()
This will create a BufferedReader object, which read a character or a number of characters from the json file, but they have another useful method named readLine(), this method reads a line of text. A line is considered to be terminated by any one of a line feed ('\n'), a carriage return ('\r'), or a carriage return followed immediately by a linefeed.
Let's modify the current code.
val inputStream = context.assets.open("myfile.json")
val reader = inputStream.bufferedReader()
// Read line by line from reader until reach the end.
var line = reader.readLine()
while(line != null) {
Log.i("TAG", line)
line = reader.readLine()
}
Output:
I/TAG: {
I/TAG: "os": "Android",
I/TAG: "version": "KitKat",
I/TAG: "codeName": 4.4
I/TAG: }
As we can see, they print 5 lines from the json file. But in some cases, we want to print all the json file as a String, that why we move to next step.
Step 3: The third line
val file = reader.readText()
This will reads the buffer reader completely as a String. You can write your own to do the same like.
val inputStream = context.assets.open("myfile.json")
val reader = inputStream.bufferedReader()
val sb = StringBuffer()
var line = reader.readLine()
while(line != null) {
Log.i("TAG", line)
sb.append(line).append("\n")
line = reader.readLine()
}
val file = sb.toString()
Log.i("TAG", file)
Output:
I/TAG: {
"os": "Android",
"version": "KitKat",
"codeName": 4.4
}
This output is the same as reader.readText().
Conclusion: BufferReader wraps an InputStream (or sub-classes of InputStream) inside them, then provide methods to read character-by-character instead of byte-by-byte in InputStream. In addition, they provide readLine() method, buffer data.
InputStream (byte-by-byte) -> Reader (character-by-character)
InputStream (byte-by-byte) -> BufferReader (character-by-character,
read line, buffer data).
Is it possible to load strings.xml from sd card instead of application res/values/... Search on the web but didn't find any tutorials. My thought is download the xml to sd card then save the strings element to an array.
public void stringsxml(){
File file = new File(Environment.getExternalStorageDirectory()
+ ".strings.xml");
StringBuilder contents = new StringBuilder();
try {
//use buffering, reading one line at a time
//FileReader always assumes default encoding is OK!
BufferedReader input = new BufferedReader(new FileReader(file));
try {
String line = null; //not declared within while loop
/*
* readLine is a bit quirky :
* it returns the content of a line MINUS the newline.
* it returns null only for the END of the stream.
* it returns an empty String if two newlines appear in a row.
*/
while (( line = input.readLine()) != null){
contents.append(line);
contents.append(System.getProperty("line.separator"));
}
}
finally {
input.close();
}
}
catch (IOException ex){
ex.printStackTrace();
}
String data= contents.toString();
}
Well, actually it is semi-possible, but you have to create a derivate LayoutInflater which will replace string codes with thus read strings.
I have documented my attempts and failings together with initial implementation here.
Summary: simple strings work, string arrays do not
No, this is not possible. Check Android decoumentation about resources:
The Android SDK tools compile your application's resources into the application binary at build time. To use a resource, you must install it correctly in the source tree (inside your project's res/ directory) and build your application.
Resources are built-in into the application binary and you can't read them from a file.
I currently have a compiled jar file that I would like to use on an android device. The code outputs to the command line using System.out.println().
How would I create a wrapper to grab the stdout and put it in a text view on an android device? Would I need to make any changes to the jar (I do have all the source code) to allow the wrapper?
Thanks in advance.
I think you'll need to make some changes. You can set standart output by calling
System.setOut(PrintStream out)
// Reassigns the "standard" output stream.
Where out is your own class that will print data to text view. See swing solution. Just set appending to text view and you can use this code.
Or just create one method
void log(String message);
where you appending text to your view. Then change all println() calls to this.
First you should consider that Android has a specific Java VM called Dalvik and not any jar can be ran under it.
If there's one point in your jar where output occurs, the best option would be to create a usual application with a TextView, include your jar to it's build path and replace a call to println() with output to it:
public void print(String msg) {
mTextView.setText(msg);
}
If there're many sources of output you could run you jar using java.lang.Process and use it's getInputStream() method to read printed messages:
public static final String XBOOT_CLASS_PATH = "-Xbootclasspath:/system/framework/core.jar"
public static final String CLASS_PATH = "-classpath /path/to/your/file.jar com.your.package.name"
...
Process p = new ProcessBuilder("dalvikvm", XBOOT_CLASS_PATH, CLASS_PATH).start();
BufferedReader reader = new BufferedReader (new InputStreamReader(p.getInputStream()));
String msg = reader.readLine();
if (msg != null) {
mTextView.setText(msg);
}
// Cleanup omitted for simplicity
If it's an executable jar file here is a working example
Add this simple executable HelloWorld jar file to your Android Project's build path
If the jar file doesn't have a package, then you will have to use Reflection to invoke methods in it.Other wise you can just import the class files and invoke the main method directly.(This example jar has a package "psae")
eg:
TextView tv = (TextView)findViewById(R.id.textv);
ByteArrayOutputStream baos = new ByteArrayOutputStream();
PrintStream ps = new PrintStream(baos);
System.setOut(ps);
String[] params = {"Aneesh","Joseph"};
psae.HelloWorld.main(params);
String output = baos.toString();
tv.setText(output)
If the jar file just has a default package, then you won't be able to import class files from that jar, and hence you will have to use Reflection to invoke the method.
TextView tv = (TextView)findViewById(R.id.textv);
ByteArrayOutputStream baos = new ByteArrayOutputStream();
PrintStream ps = new PrintStream(baos);
System.setOut(ps);
try {
//pick the entry class from the jar Manifest
//Main-Class: psae.HelloWorld
Class myClass = Class.forName("psae.HelloWorld");
//since this has a package, there is no need of reflection.This is just an example
//If the jar file had just a default package, the it would have been something like the below line (and this is where it would be useful)
//Class myClass = Class.forName("Main");
Method myMethod = myClass.getMethod("main", String[].class);
//parameters to the main method
String[] params = {"Aneesh","Joseph"};
myMethod.invoke(null, (Object) params);
String output = baos.toString();
tv.setText(output);
}
catch(Exception d)
{
tv.setText(d.toString());
}
I am developing software that loads information from XML files using Android's implementation of java.xml.parsers.DocumentBuilder and DocumentBuilderFactory. I am writing unit tests of my objects and I need to be able to provide a variety of xml files that will exercise the code under test. I am using Eclipse and have a separate Android Test Project. I cannot find a way to put the test xml into the test project such that the code under test can open the files.
If I put the files in /assets of the test project, the code under test cannot see it.
If I put the files in the /assets of the code under test, it can of course see the files, but now I'm cluttering up my actual system with test only data files.
If I hand copy the files to the /sdcard/data directory, I can open them from the code under test, but that interferes with automating my tests.
Any suggestions of how to have different xml test files reside in the test package but be visible to the code under test would be greatly appreciated.
Here is how I tried to structure the unit test:
public class AppDescLoaderTest extends AndroidTestCase
{
private static final String SAMPLE_XML = "sample.xml";
private AppDescLoader m_appDescLoader;
private Application m_app;
protected void setUp() throws Exception
{
super.setUp();
m_app = new Application();
//call to system under test to load m_app using
//a sample xml file
m_appDescLoader = new AppDescLoader(m_app, SAMPLE_XML, getContext());
}
public void testLoad_ShouldPopulateDocument() throws Exception
{
m_appDescLoader.load();
}
}
This did not work as the SAMPLE_XML file is in the context of the test, but AndroidTestCase is providing a context for the system under test, which cannot see an asset from the test package.
This is the modified code that worked per answer given:
public class AppDescLoaderTest extends InstrumentationTestCase
{
...
protected void setUp() throws Exception
{
super.setUp();
m_app = new Application();
//call to system under test to load m_app using
//a sample xml file
m_appDescLoader = new AppDescLoader(m_app, SAMPLE_XML, getInstrumentation().getContext());
}
Option 1: Use InstrumentationTestCase
Suppose you got assets folder in both android project and test project, and you put the XML file in the assets folder. in your test code under test project, this will load xml from the android project assets folder:
getInstrumentation().getTargetContext().getResources().getAssets().open(testFile);
This will load xml from the test project assets folder:
getInstrumentation().getContext().getResources().getAssets().open(testFile);
Option 2: Use ClassLoader
In your test project, if the assets folder is added to project build path (which was automatically done by ADT plugin before version r14), you can load file from res or assets directory (i.e. directories under project build path) without Context:
String file = "assets/sample.xml";
InputStream in = this.getClass().getClassLoader().getResourceAsStream(file);
For Android and JVM unit tests I use following:
public final class DataStub {
private static final String BASE_PATH = resolveBasePath(); // e.g. "./mymodule/src/test/resources/";
private static String resolveBasePath() {
final String path = "./mymodule/src/test/resources/";
if (Arrays.asList(new File("./").list()).contains("mymodule")) {
return path; // version for call unit tests from Android Studio
}
return "../" + path; // version for call unit tests from terminal './gradlew test'
}
private DataStub() {
//no instances
}
/**
* Reads file content and returns string.
* #throws IOException
*/
public static String readFile(#Nonnull final String path) throws IOException {
final StringBuilder sb = new StringBuilder();
String strLine;
try (final BufferedReader reader = new BufferedReader(new InputStreamReader(new FileInputStream(path), "UTF-8"))) {
while ((strLine = reader.readLine()) != null) {
sb.append(strLine);
}
} catch (final IOException ignore) {
//ignore
}
return sb.toString();
}
}
All raw files I put into next path: ".../project_root/mymodule/src/test/resources/"
Try this for Kotlin:
val json = File("src\\main\\assets\\alphabets\\alphabets.json").bufferedReader().use { it.readText() }