При модификации не удается загрузить двоичный файл в приложении для Android - конечный результат - поврежденный файл, размер которого превышает ожидаемый - PullRequest
0 голосов
/ 14 июня 2019

Я пытаюсь загрузить PDF-файлы с помощью Retrofit 2 в приложении для Android, написанном на Kotlin.Фрагмент ниже - это в основном весь мой код.Судя по моим выводам в журнале, файл успешно загружается и сохраняется в нужном месте.

Однако загруженный файл больше ожидаемого и поврежден.Я могу открыть его в PDF-ридере, но PDF-файл пуст.В приведенном ниже примере я попытался загрузить https://www.w3.org/WAI/ER/tests/xhtml/testfiles/resources/pdf/dummy.pdf. Если я скачаю этот файл через браузер, в результате получится PDF размером 13 264 байта.Однако, загруженный с этим кодом, он составляет 22 503 байта, что примерно на 70% больше, чем ожидалось.Я получаю аналогичный результат для других двоичных файлов, таких как JPEG.Тем не менее, загрузка TXT на самом деле работает нормально, даже большой.Похоже, проблема связана с двоичными файлами.

package com.ebelinski.RetrofitTestApp

import android.app.Application
import android.content.Context
import android.os.Build
import android.support.v7.app.AppCompatActivity
import android.os.Bundle
import android.util.Log
import com.google.gson.FieldNamingPolicy
import com.google.gson.GsonBuilder
import okhttp3.OkHttpClient
import okhttp3.ResponseBody
import org.jetbrains.anko.doAsync
import retrofit2.Call
import retrofit2.http.*
import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory
import java.io.*
import java.util.concurrent.Executors
import java.util.concurrent.TimeUnit

interface FileService {

    @Streaming
    @GET
    @Headers("Content-Type: application/pdf", "Accept: application/pdf")
    fun fileFromUrl(@Url url: String,
                    @Header("Authorization") tokenTypeWithAuthorization: String): Call<ResponseBody>

}

class MainActivity : AppCompatActivity() {

    val TAG = "MainActivity"

    val RETROFIT_CONNECT_TIMEOUT_SECONDS = 60
    private val RETROFIT_READ_TIMEOUT_SECONDS = 60
    private val RETROFIT_WRITE_TIMEOUT_SECONDS = 60

    private val retrofit: Retrofit
        get() {
            val gson = GsonBuilder()
                .setDateFormat("yyyyMMdd")
                .setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES)
                .create()

            val converterFactory = GsonConverterFactory.create(gson)

            val okHttpClient = OkHttpClient.Builder()
                .connectTimeout(RETROFIT_CONNECT_TIMEOUT_SECONDS.toLong(), TimeUnit.SECONDS)
                .readTimeout(RETROFIT_READ_TIMEOUT_SECONDS.toLong(), TimeUnit.SECONDS)
                .writeTimeout(RETROFIT_WRITE_TIMEOUT_SECONDS.toLong(), TimeUnit.SECONDS)
                .addInterceptor { chain ->
                    val userAgentValue = "doesn't matter"
                    val originalRequest = chain.request().newBuilder().addHeader("User-Agent", userAgentValue).build()

                    var response = chain.proceed(originalRequest)
                    if (BuildConfig.DEBUG) {
                        val bodyString = response.body()!!.string()
                        Log.d(TAG, String.format("Sending request %s with headers %s ", originalRequest.url(), originalRequest.headers()))
                        Log.d(TAG, String.format("Got response HTTP %s %s \n\n with body %s \n\n with headers %s ", response.code(), response.message(), bodyString, response.headers()))
                        response = response.newBuilder().body(ResponseBody.create(response.body()!!.contentType(), bodyString)).build()
                    }

                    response
                }
                .build()

            return Retrofit.Builder()
                .callbackExecutor(Executors.newCachedThreadPool())
                .baseUrl("https://example.com")
                .addConverterFactory(converterFactory)
                .client(okHttpClient)
                .build()
        }

    private val fileService: FileService = retrofit.create(FileService::class.java)

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        doAsync { downloadFile() }
    }

    fun downloadFile() {
        val uri = "https://www.w3.org/WAI/ER/tests/xhtml/testfiles/resources/pdf/dummy.pdf"
        val auth = "doesn't matter"

        val response = fileService.fileFromUrl(
            uri,
            auth
        ).execute()

        if (!response.isSuccessful) {
            Log.e(TAG, "response was not successful: " +
                    response.code() + " -- " + response.message())
            throw Throwable(response.message())
        }

        Log.d(TAG, "Server has file for ${uri}")
        saveFileFromResponseBody(response.body()!!)
    }



    // Returns the name of what the file should be, whether or not it exists locally
    private fun getFileName(): String? {
        return "dummy.pdf"
    }

    fun saveFileFromResponseBody(body: ResponseBody): Boolean {
        val fileName = getFileName()
        val localFullFilePath = File(getFileFullDirectoryPath(), fileName)
        var inputStream: InputStream? = null
        var outputStream: OutputStream? = null
        Log.d(TAG, "Attempting to download $fileName")

        try {
            val fileReader = ByteArray(4096)
            val fileSize = body.contentLength()
            var fileSizeDownloaded: Long = 0

            inputStream = body.byteStream()
            outputStream = FileOutputStream(localFullFilePath)

            while (true) {
                val read = inputStream.read(fileReader)
                if (read == -1) break

                outputStream.write(fileReader, 0, read)
                fileSizeDownloaded += read.toLong()

                Log.d(TAG, "$fileName download progress: $fileSizeDownloaded of $fileSize")
            }

            outputStream.flush()
            Log.d(TAG, "$fileName downloaded successfully")
            return true
        } catch (e: IOException) {
            Log.d(TAG, "$fileName downloaded attempt failed")
            return false
        } finally {
            inputStream?.close()
            outputStream?.close()
        }
    }

    fun getFileFullDirectoryPath(): String {
        val directory = getDir("test_dir", Context.MODE_PRIVATE)
        return directory.absolutePath
    }
}

Если это поможет, вот мой build.gradle файл:

apply plugin: 'com.android.application'

apply plugin: 'kotlin-android'

apply plugin: 'kotlin-android-extensions'

android {
    compileSdkVersion 28
    defaultConfig {
        applicationId "com.ebelinski.RetrofitTestApp"
        minSdkVersion 21
        targetSdkVersion 28
        versionCode 1
        versionName "1.0"
        testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
    }
    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
        }
    }
}

dependencies {
    implementation fileTree(dir: 'libs', include: ['*.jar'])
    implementation"org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
    implementation 'com.android.support:appcompat-v7:28.0.0'
    implementation 'com.android.support.constraint:constraint-layout:1.1.3'
    implementation 'com.squareup.retrofit2:converter-gson:2.5.0'
    implementation 'com.squareup.retrofit2:retrofit:2.5.0'
    implementation 'com.squareup.okhttp3:okhttp:3.12.0'
    implementation "org.jetbrains.anko:anko-commons:0.10.1"
    testImplementation 'junit:junit:4.12'
    androidTestImplementation 'com.android.support.test:runner:1.0.2'
    androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2'
}

1 Ответ

1 голос
/ 15 июня 2019

Предположим, проблема не в Retrofit, а в вашем перехватчике OkHTTP3, в частности, здесь:

val bodyString = response.body()!!.string()

Вот содержимое строки ():

  /**
   * Returns the response as a string.
   *
   * If the response starts with a
   * [Byte Order Mark (BOM)](https://en.wikipedia.org/wiki/Byte_order_mark), it is consumed and
   * used to determine the charset of the response bytes.
   *
   * Otherwise if the response has a `Content-Type` header that specifies a charset, that is used
   * to determine the charset of the response bytes.
   *
   * Otherwise the response bytes are decoded as UTF-8.
   *
   * This method loads entire response body into memory. If the response body is very large this
   * may trigger an [OutOfMemoryError]. Prefer to stream the response body if this is a
   * possibility for your response.
   */
  @Throws(IOException::class)
  fun string(): String = source().use { source ->
    source.readString(charset = source.readBomAsCharset(charset()))
  }

Вы можете проверить ResponseBody исходный код для более подробной информации.

Предположим, что вместо этого поможет body.source (). (или просто избегайте перехвата двоичных файлов)

Хороший пример реализации перехватчика здесь: HTTPLoggingInterceptor .

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...