从远程下载文件并将其保存到Android设备

时间:2017-11-24 09:24:51

标签: android kotlin rx-java2

我尝试做的是将文件的下载方式与我将其保存到存储区域的方式分开。

运行正常的代码:

以下代码从远程下载文件,最后将其保存到存储中。

private fun downloadFile(url: URL): Observable<Int> {

    return Observable.create(fun(emitter)
    {
        var input: InputStream? = null
        var output: OutputStream? = null
        var connection: HttpURLConnection? = null
        val path = Environment.getExternalStorageDirectory().path

        try
        {
            connection = url.openConnection() as HttpURLConnection

            if (connection != null)
            {
                connection.connect()

                if (connection.responseCode != HttpURLConnection.HTTP_OK)
                {
                    emitter.onError(IllegalStateException("<HTTP Error> ${connection.responseMessage}. Status code: ${connection.responseCode}."))
                    return
                }

                if (connection.contentType == null)
                {
                    emitter.onError(IllegalStateException("<HTTP Error> Unsupported content-type."))
                    return
                }

                val fileLength = connection.contentLength
                var fileext = MimeTypeMap.getSingleton().getExtensionFromMimeType(connection.contentType)

                input = connection.inputStream
                output = FileOutputStream("/$path/SmartTVMediaTest.$fileext")
                val data = ByteArray(4096)
                var totalBytesReceived: Int = 0

                while (!emitter.isDisposed)
                {
                    val receivedBytes: Int = input.read(data)

                    if (receivedBytes < 0)
                    {
                        break
                    }

                    totalBytesReceived += receivedBytes

                    if (fileLength > 0)
                    {
                        val portion = totalBytesReceived / fileLength.toFloat()
                        val percentage = portion * 100
                        emitter.onNext(percentage.toInt())

                        Log.d(TAG, "<downloadFile> $fileLength, $totalBytesReceived,  $percentage")
                    }

                    output.write(data, 0, receivedBytes)
                }

                emitter.onComplete()
            }
            else {
                emitter.onError(IllegalStateException("<HTTP Connection Error> Unsupported connection type."))
            }
        }
        catch(ex: InterruptedException) {
            Log.d(TAG, "<Thread> Download cancelled.")
        }
        catch (ex: IOException)
        {
            emitter.onError(ex)
        }
        finally
        {
            input?.close()
            output?.close()
            connection?.disconnect()
        }
    })
}

无效的代码:

所以我开始创建一个表示从远程读取的数据块的类,如下所示:

public data class DownloadChunk(val data: ByteArray, val length: Int, val totalLength: Int)

然后我创建了下载文件的observable,如下所示:

public class DownloadServiceObservable
{
    public fun download(url: URL): Observable<DownloadChunk>
    {
        return Observable.create(fun(emitter)
        {
            var input: InputStream? = null
            var connection: HttpURLConnection? = null

            try
            {
                connection = url.openConnection() as HttpURLConnection

                if (connection != null)
                {
                    connection.connect()

                    if (connection.responseCode != HttpURLConnection.HTTP_OK)
                    {
                        emitter.onError(IllegalStateException("<HTTP Error> ${connection.responseMessage}. Status code: ${connection.responseCode}."))
                        return
                    }

                    if (connection.contentType == null)
                    {
                        emitter.onError(IllegalStateException("<HTTP Error> Unsupported content-type."))
                        return
                    }

                    val totalLength = connection.contentLength

                    input = connection.inputStream

                    val data = ByteArray(4096)

                    while (!emitter.isDisposed)
                    {
                        val length: Int = input.read(data)

                        if (length < 0)
                        {
                            break
                        }

                        emitter.onNext(DownloadChunk(data, length, totalLength))
                    }

                    emitter.onComplete()
                }
                else
                {
                    emitter.onError(IllegalStateException("<HTTP Connection Error> Unsupported connection type."))
                }
            }
            catch(ex: InterruptedException)
            {
                emitter.onError(ex)
            }
            finally
            {
                input?.close()
                connection?.disconnect()
            }
        })
    }
}

最后,我创建了一个Observer,将文件保存到设备中,如下所示:

public class DownloadFileObserver: Observer<DownloadChunk>
{
    private val _file: OutputStream

    public override fun onSubscribe(d: Disposable)
    {
    }

    public constructor(path: String)
    {
        _file = FileOutputStream(path)
    }

    public override fun onNext(chunk: DownloadChunk)
    {
        _file.write(chunk.data, 0, chunk.length)
    }

    public override fun onError(e: Throwable)
    {
        _file.close()
    }

    public override fun onComplete()
    {
        _file.close()
    }
}

问题:

DownloadFileObserver创建的文件已损坏,因此我的第一个假设是在推送和接收时间项之间无法保证onNext的顺序,但据我所知,我是通过在观察者中添加一个计数器来测试它并在观察者中打印计数器。

我认为我错过了一些东西。

2 个答案:

答案 0 :(得分:1)

你需要测试你的代码,因为你知道,试图检查代码以查看错误只是一种挫败感。

首先,测试你的观察者(用Java编写的代码,JUnit 4,道歉):

@Test
public void testObserver() {
  DownloadFileObserver uut = new DownloadFileObserver("testpath.txt");
  bytes[] testData = "Test String".getBytes();
  uut.onNext( new DownloadChunk( testData, testData.length, testData.length );
  uut.onComplete();

  // inspect the result
  bytes[] resultData = Files.readAllBytes( Paths.get( "testpath.txt" );
  assertTrue( Arrays.equals( testData, resultData );
}

然后开始添加用于处理多个块的测试,然后是错误。

最后,为DownloadServiceObservable编写单元测试。这些将更加困难,因为您需要模拟实际的网络呼叫。

答案 1 :(得分:0)

我设法使用测试重现了这个问题并且这是一个线程问题,我订阅并观察了两个不同的线程,因此我读取和写入数据的顺序不同步所以我最终重构{{1现在它需要一个DownloadServiceObservable的实例来处理从给定源获取流以及负责将数据写入给定目标的StreamFactory实例,所以现在所有这项工作是在后台完成的。

只是澄清StreamWriterStreamFactory是我创建的两个界面。

现在,StreamWriter看起来像这样:

DownloadServiceObservable

最后,这是我写的测试:

public class DownloadServiceObservable
{
    private val _source: StreamFactory
    private val _destination: StreamWriter

    public constructor(source: StreamFactory, destination: StreamWriter)
    {
        _source = source
        _destination = destination
    }

    public fun download(): Observable<DownloadProgress>
    {
        return Observable.create(fun(emitter)
        {
            var input: InputStream? = null

            try {
                input = _source.create()

                if (input != null) {
                    val data = ByteArray(4096)
                    var totalBytesReceived = 0

                    while (!emitter.isDisposed) {
                        val receivedBytes: Int = input.read(data)

                        if (receivedBytes < 0) {
                            break
                        }

                        totalBytesReceived += receivedBytes

                        emitter.onNext(DownloadProgress(totalBytesReceived, _source.length))

                        _destination.write(data, receivedBytes)
                    }

                    emitter.onComplete()
                }
                else {
                    emitter.onError(IllegalStateException("<${DownloadServiceObservable::class.java}> Source returned with a null value."))
                }
            }
            catch(ex: InterruptedException) {
                emitter.onError(ex)
            }
            finally {
                input?.close()
            }
        })
    }

    public data class DownloadProgress(val receivedLength: Int, val totalLength: Int)
}