0

I am new to programming in Android Studio and I keep running into problems with asynchronous downloading (at least, that is where I think the problem is).

I am trying to make an app that will display every new comic by a given publisher for a given week (by comic, I mean the information about them, as in the name, description, publisher and covers). For this to work, I need to be able to access a websites html and store an image from a site as a ByteArray. I then put the gathered information into a JSONComic object, which stores itself into internal memory in a way that will be easy to parse through Gson. For now, this is the only thing I need the program to do for now, as later I will make it so that the download (which may take a while, as it will cover hundreds of comics, depending on the week) happens only once pre week and display the information in an appealing way. As far as I know, my code gives me three types of errors:

FileNotFoundException, when the comic is supposed to save into a file,

IllegalStateException: result must not be null, within the callback of the Ion download referring to the result (this seems to happen in all of my uses of Ion, but most frequently with the link to a cover)

and then there is some sort of handshake failure, of which I understand nothing, not even it's importance. I just wanted to mention it, in case it is important.

I have a few Ideas as to why I am getting these errors, but I may be totally wrong, because I am a beginner at Android.

  1. The way I structured the cycle with the Ion downloads might be problematic. Perhaps the cycle continues while the background thread is executing, causing the threads to be confused? Ideally, I would make the main thread wait for the Ion download to complete, but using .get() caused the program to freeze for eternity, and the await method from Ion-kotlin is, for whatever reason unavailable.

    It is possible that I just imported Ion incorrectly, but I spent days trying to get await to be available to me, to no avail, so I just decided to make the program continue from the callback, and do this each time I ask for Ion. I am sure there is a better way to use Ion, but I just don't know it (If you do, please, tell me, as I kind of hate my solution, but it's the only one that I figured out that semi-works).

  2. Perhaps the cycle executes faster then the file is able to be created? It doesn't make much sense, but I tried to put the main thread to sleep in multiple places, to slow it down, just in case this was the issue. Sleeping didn't help, so I again removed it.

  3. Most likely, I am using Ion completely wrong

How can I fix the code?

The Activity:

class MainActivity : AppCompatActivity() {

    val comics: ArrayList<JSONComic> = arrayListOf()



    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        getComics("https://freshcomics.us/issues/2019-08-14")
    }

    //Sends the html of a website with a given link as a string to the function parseHTML
    fun getComics(link: String){
        Ion.with(applicationContext).load(link).asString()
            .setCallback { e, result -> parseHTML(result) }
    }


    //Searches the html string for comic properties, which are in the JSONComic class. It also downloads the image from found link belonging to the comic
    fun parseHTML(file: String){

        var publisher: String = ""
        val html: List<String> = file.lines()
        Log.d("HTML SIZE", html.size.toString())
        var lineIsPublisher: Boolean = false
        for (line in html) {
            if (line.contains("entry-title")) {
                lineIsPublisher = true
            } else if (lineIsPublisher) {
                publisher = line
                lineIsPublisher = false

                //Log.d("Publisher", "Found")
            } else if (line.contains("<a href=") && line.contains("<img src=") && line.contains(
                    "alt="
                ) && !line.contains("(True Believers)") && !line.contains(" Printing)") && !line.contains(
                    "(Complete Collection)"
                ) && !line.contains(" Vol. ")
            ) {
                Log.d("Found Comic", "I found it")
                var temp_title =
                    line.substring(line.indexOf("alt=") + 5, line.indexOf("style=") - 2)
                var link: String =
                    line.substring(line.indexOf("src=") + 5, line.indexOf("alt=") - 2)
                Ion.with(this)
                    .load(link)
                    .progress(ProgressCallback { downloaded, total -> println("$downloaded / $total") })
                    .asByteArray()
                    .setCallback(FutureCallback<ByteArray> { e, image ->
                        if(image == null){
                            Log.d("Link was:    ", link)
                        }
                        finalizeParsingComic(line,image,publisher,temp_title)
                    })
            }
        }
        Log.d("Done", "Done")
    }

    //Creates the comic, after downloading its description as a string
    fun finalizeParsingComic(line: String, cover: ByteArray, publisher: String, temp_title: String){
        if (temp_title.contains("Cover") && comics.size > 0) {
            comics.elementAt(comics.lastIndex).addCover(cover)
        } else {
            val descriptionLink = "https://freshcomics.us/" + line.substring(
                line.indexOf("<a href=") + 9,
                line.indexOf("><img") - 1
            )
            Ion.with(this)
                .load(descriptionLink)
                .progress(ProgressCallback { downloaded, total -> println("$downloaded / $total") })
                .asString()
                .setCallback(FutureCallback<String> { e, description ->
                    val title = temp_title
                    val coverArray: ArrayList<ByteArray> = arrayListOf()
                    coverArray.add(cover)
                    // mainCover = line.substring(line.indexOf("src=") + 5,line.indexOf("alt=")-2)
                    val comic: JSONComic = JSONComic(title,this,publisher,coverArray, description)
                    comics.add(comic)
                    Log.d("Created Comic", publisher)
                })
        }
`

    //Formats the given string into a format that is appropriate for the description of a comic
    fun descriptionFormatter(text: String): String{
        val lines: List<String> = text.lines()
        var description: String = ""
        val sliceEnd: String  = "</p>"
        val sliceStart: String = "<p>"
        var found: Boolean = false
        lines.forEach {
            if(it.contains("entry-content") && it.contains("<div class=")){
                found = true
            }
            else if(found && it.contains(sliceStart)){
                description = it.substring(it.indexOf(sliceStart)+sliceStart.length,it.indexOf(sliceEnd)-1)
                description = description.replace("<br><br>","\n")
                description = description.replace("&#39;", "'")
                description = description.replace("<br />", "\n")
                description = description.replace("&quot;", "''")
                found = false
            }
        }
        return description
    }

}

JSONComic class:

//The comic is stored in a way that should allow for easy JSON parsing

class JSONComic(val title:String, val context: Context, val publish: String, val coversArr: ArrayList<ByteArray>, val descrip: String) {

//The properties of each comic (each comic may have multiple covers, but the first one will be the main one)
    private val comic: JsonObject = createComic()
    private val pub: String = publish
    private val cov: ArrayList<ByteArray> = coversArr
    private val desc: String = descrip
    private val name: String = title

//Stores the comic
    private fun createComic(): JsonObject{
        val temp_comic: JsonObject = JsonObject()
        var coversString: String = ""
        for (cover in coversArr){
            coversString += String(cover) + "[*]"
        }
        coversString = coversString.substring(0,coversString.length-3)

        temp_comic.addProperty("publisher",pub)
        temp_comic.addProperty("title",title)
        temp_comic.addProperty("description",descrip)
        temp_comic.addProperty("covers",coversString)
        var fileName: String = title
        fileName = fileName.replace(" ", "_")
        fileName = fileName.replace(")", "")
        fileName = fileName.replace("(", "")
        val file: File = File(context.filesDir.path, "$fileName.json")
        val gson = Gson()
        val jsonString:String = gson.toJson(temp_comic)
        file.writeText(jsonString)
        Log.d("File Path", file.path)

        return temp_comic
    }

//Adds aditional covers to the comic
    fun addCover(cover: ByteArray){
        val newCoversString = comic.get("covers").asString + "[*]" + String(cover)
        comic.remove("covers")
        comic.addProperty("covers", newCoversString)
    }


}

Android Manifest:

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
          xmlns:dist="http://schemas.android.com/apk/distribution" xmlns:tools="http://schemas.android.com/tools"
          package="com.example.comicschedule">

    <dist:module dist:instant="true"/>
    <uses-permission android:name="android.permission.INTERNET"/>

    <application
            tools:replace="android:icon"
            android:allowBackup="true"
            android:icon="@mipmap/ic_launcher"
            android:label="@string/app_name"
            android:roundIcon="@mipmap/ic_launcher_round"
            android:supportsRtl="true"
            android:theme="@style/AppTheme">
        <activity android:name=".MainActivity">
            <intent-filter>
                <action android:name="android.intent.action.MAIN"/>

                <category android:name="android.intent.category.LAUNCHER"/>
            </intent-filter>
        </activity>
    </application>

</manifest>

build.gradle:

apply plugin: 'com.android.application'

apply plugin: 'kotlin-android'

apply plugin: 'kotlin-android-extensions'

android {
    compileSdkVersion 29
    buildToolsVersion "29.0.1"
    defaultConfig {
        applicationId "com.example.stackoverflowtesting"
        minSdkVersion 24
        targetSdkVersion 29
        versionCode 1
        versionName "1.0"
        testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
    }
    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
        }
    }
}

dependencies {
    implementation 'com.google.code.gson:gson:2.8.5'
    implementation 'com.koushikdutta.ion:ion:2.2.1'
    implementation fileTree(dir: 'libs', include: ['*.jar'])
    implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
    implementation 'androidx.appcompat:appcompat:1.0.2'
    implementation 'androidx.core:core-ktx:1.0.2'
    implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
    testImplementation 'junit:junit:4.12'
    androidTestImplementation 'androidx.test:runner:1.2.0'
    androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0'
}

These are examples of the errors I get. I don't get each one every time, but I do always get at least one of them

E/AndroidRuntime: FATAL EXCEPTION: main
    Process: com.example.comicschedule, PID: 5137
    java.lang.RuntimeException: java.lang.reflect.InvocationTargetException
        at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:756)
     Caused by: java.lang.reflect.InvocationTargetException
        at java.lang.reflect.Method.invoke(Native Method)
        at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:866)
        at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:756) 
     Caused by: java.io.FileNotFoundException: /data/user/0/com.example.comicschedule/files/Friendly_Neighborhood_Spider-Man_#10_Sliney_/_BobG_Cover.json (No such file or directory)
        at java.io.FileOutputStream.open(Native Method)
        at java.io.FileOutputStream.<init>(FileOutputStream.java:221)
        at java.io.FileOutputStream.<init>(FileOutputStream.java:169)
        at kotlin.io.FilesKt__FileReadWriteKt.writeBytes(FileReadWrite.kt:84)
        at kotlin.io.FilesKt__FileReadWriteKt.writeText(FileReadWrite.kt:110)
        at kotlin.io.FilesKt__FileReadWriteKt.writeText$default(FileReadWrite.kt:110)
        at com.example.comicschedule.JSONComic.createComic(JSONComic.kt:48)
        at com.example.comicschedule.JSONComic.<init>(JSONComic.kt:20)
        at com.example.comicschedule.MainActivity$finalizeParsingComic$2.onCompleted(MainActivity.kt:105)
        at com.example.comicschedule.MainActivity$finalizeParsingComic$2.onCompleted(MainActivity.kt:27)
        at com.koushikdutta.async.future.SimpleFuture.handleCallbackUnlocked(SimpleFuture.java:107)
        at com.koushikdutta.async.future.SimpleFuture.setComplete(SimpleFuture.java:141)
        at com.koushikdutta.async.future.SimpleFuture.setComplete(SimpleFuture.java:128)
        at com.koushikdutta.ion.IonRequestBuilder$1.run(IonRequestBuilder.java:246)
        at com.koushikdutta.async.AsyncServer$RunnableWrapper.run(AsyncServer.java:60)
        at android.os.Handler.handleCallback(Handler.java:751)
        at android.os.Handler.dispatchMessage(Handler.java:95)
        at android.os.Looper.loop(Looper.java:154)
        at android.app.ActivityThread.main(ActivityThread.java:6077)
        at java.lang.reflect.Method.invoke(Native Method) 
        at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:866) 
        at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:756) 
E/AndroidRuntime: FATAL EXCEPTION: main
    Process: com.example.comicschedule, PID: 5374
    java.lang.IllegalStateException: result must not be null
        at com.example.comicschedule.MainActivity$getComics$1.onCompleted(MainActivity.kt:41)
        at com.example.comicschedule.MainActivity$getComics$1.onCompleted(MainActivity.kt:27)
        at com.koushikdutta.async.future.SimpleFuture.handleCallbackUnlocked(SimpleFuture.java:107)
        at com.koushikdutta.async.future.SimpleFuture.setComplete(SimpleFuture.java:141)
        at com.koushikdutta.async.future.SimpleFuture.setComplete(SimpleFuture.java:124)
        at com.koushikdutta.ion.IonRequestBuilder$1.run(IonRequestBuilder.java:244)
        at com.koushikdutta.async.AsyncServer$RunnableWrapper.run(AsyncServer.java:60)
        at android.os.Handler.handleCallback(Handler.java:751)
        at android.os.Handler.dispatchMessage(Handler.java:95)
        at android.os.Looper.loop(Looper.java:154)
        at android.app.ActivityThread.main(ActivityThread.java:6077)
        at java.lang.reflect.Method.invoke(Native Method)
        at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:866)
        at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:756)
E/AndroidRuntime: FATAL EXCEPTION: main
    Process: com.example.stackoverflowtesting, PID: 6194
    java.lang.IllegalStateException: result must not be null
        at com.example.stackoverflowtesting.MainActivity$getComics$1.onCompleted(MainActivity.kt:27)
        at com.example.stackoverflowtesting.MainActivity$getComics$1.onCompleted(MainActivity.kt:14)
        at com.koushikdutta.async.future.SimpleFuture.handleCallbackUnlocked(SimpleFuture.java:107)
        at com.koushikdutta.async.future.SimpleFuture.setComplete(SimpleFuture.java:141)
        at com.koushikdutta.async.future.SimpleFuture.setComplete(SimpleFuture.java:124)
        at com.koushikdutta.ion.IonRequestBuilder$1.run(IonRequestBuilder.java:244)
        at com.koushikdutta.async.AsyncServer$RunnableWrapper.run(AsyncServer.java:60)
        at android.os.Handler.handleCallback(Handler.java:751)
        at android.os.Handler.dispatchMessage(Handler.java:95)
        at android.os.Looper.loop(Looper.java:154)
        at android.app.ActivityThread.main(ActivityThread.java:6077)
        at java.lang.reflect.Method.invoke(Native Method)
        at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:866)
        at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:756)

And sometimes I get this one, though I am not sure of its importance:

W/System.err: javax.net.ssl.SSLHandshakeException: Handshake failed
        at com.android.org.conscrypt.OpenSSLEngineImpl.unwrap(OpenSSLEngineImpl.java:441)
        at javax.net.ssl.SSLEngine.unwrap(SSLEngine.java:1270)
        at com.koushikdutta.async.AsyncSSLSocketWrapper$5.onDataAvailable(AsyncSSLSocketWrapper.java:194)
        at com.koushikdutta.async.Util.emitAllData(Util.java:23)
        at com.koushikdutta.async.AsyncNetworkSocket.onReadable(AsyncNetworkSocket.java:152)
        at com.koushikdutta.async.AsyncServer.runLoop(AsyncServer.java:821)
        at com.koushikdutta.async.AsyncServer.run(AsyncServer.java:658)
        at com.koushikdutta.async.AsyncServer.access$800(AsyncServer.java:44)
        at com.koushikdutta.async.AsyncServer$14.run(AsyncServer.java:600)
    Caused by: javax.net.ssl.SSLHandshakeException: Connection closed by peer
        at com.android.org.conscrypt.NativeCrypto.SSL_do_handshake_bio(Native Method)
        at com.android.org.conscrypt.OpenSSLEngineImpl.unwrap(OpenSSLEngineImpl.java:426)
        ... 8 more
halfer
  • 19,824
  • 17
  • 99
  • 186
Levyaton
  • 29
  • 5

1 Answers1

0

In your first stack trace, your file has a slash in it, so that file could not be created as the directory does not exist.

/data/user/0/com.example.comicschedule/files/Friendly_Neighborhood_Spider-Man_#10_Sliney_/_BobG_Cover.json (No such file or directory)

Try using File.mkdirs on the parent file to ensure it exists, before writing.

koush
  • 2,972
  • 28
  • 31
  • Thanks for the tip, I will use it! Do you have any idea on what I can do to make the main thread wait for Ion to finish it's task :)? My current solution doesn't seem like the best one – Levyaton Aug 21 '19 at 09:00
  • I would not make the main thread "wait", because that would block the main thread. You can use get() to block for a result, but I would not do that. That will cause an app ANR. – koush Nov 06 '19 at 20:22