2020-08-04

【Kotlin】以 HttpURLConnection 下載檔案

注意:本篇筆記只適用 Android9(Pie, API28) 以前版本

Environment.getExternalStorageDirectory() 這種寫法,因安全因素在 Android10(Q, API29) 被棄用了!

甚至 AsyncTask 也在 Android11(R, API30) 被棄用!

有用到上述二種寫法的朋友,要趕緊修改您的 APP,不然,會無法通過編譯,或是 APP 在執行時會 crash!!

這篇筆記完全是應急性質,讓老人家的 APPAndroid10 不會 crash  :~(



話說 Android 已經提供了 DownloadManager 這個好用的工具,但個人覺得很難控制,尤其是低階手機/平板,DownloadManager 的參考指南就有提到,當您將要下載的檔案排入佇列後,就只能痴痴的等 DownloadManager 決定何時輪到下載您的檔案;如果想知道下載進度,還要再另外寫一段監看/監聽程式,時時去詢問 DownloadManager 的現況。

所以還是以舊的 ApacheHttpClient 集,自己處理 "下載" 的工作算了。


app 層級build.gradle
  
 
...
...

android {
    compileSdkVersion 28    // 注意:一定要小於等於 28

    defaultConfig {
        applicationId "com.example.httpdownload"
        minSdkVersion 16
        targetSdkVersion 28    // 注意:一定要小於等於 28
        versionCode 1
        versionName "1.0"

        testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
    }

    buildTypes {
        ...
        }
    }

    // 一定要加這行, 因 AndroidStudio 已停用 httpclient
    useLibrary 'org.apache.http.legacy'
}
 



AndroidManifest.xml
 
<manifest
    xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.example.httpdownload">

    <uses-permission android:name="android.permission.INTERNET" />
    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
    <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>

    <application
        ...
        ...
 



activity_main.xml
 
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout 
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <!-- 這個檔沒特別的,就是 AndroidStudio 自動產生的版面,加上 onClick 事件 -->
    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Hello World!"
        android:textSize="30dp"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        android:onClick="DownloadFile"/>

</androidx.constraintlayout.widget.ConstraintLayout>
 



MainActivity.kt
 
class MainActivity : AppCompatActivity()
{
    private val TAG = "MainActivity"
    lateinit var filePath: String
    lateinit var myDir: String

    // 儲存檔案權限
    private val PERMISSION_REQUEST_WRITE_EXTERNAL_STORAGE = 0

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

        // 先建立目錄
        // 第一次執行, 因目錄尚不存在, 所以是建立目錄
        // 第二次以後執行, 就變成是開啟目錄了
        filePath = Environment.getExternalStorageDirectory().path + "/myDownload"
        File(filePath).mkdir()
        myDir = filePath
    }

    fun DownloadFile(vv: View)
    {
        if (Build.VERSION.SDK_INT>22)
            {	// 若是 API23(含) 以上版本, 需請求儲存檔案的權限
                if(checkSelfPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE)!=PackageManager.PERMISSION_GRANTED)
                    {
                        requestStoragePermission();
                    }
                else
                    {
                        Toast.makeText(this, "開始下載", Toast.LENGTH_LONG).show()
                        DownloadFileTask().execute()
                    }
            }
        else
            {
                Toast.makeText(this, "開始下載", Toast.LENGTH_LONG).show()
                DownloadFileTask().execute()
            }
    }

    private inner class DownloadFileTask : AsyncTask<Any, Any, Any>()
    {
        override fun onPreExecute()
        {
            Log.i(TAG, "onPreExecute")
        }

        override fun doInBackground(vararg args: Any)
        {
            try
            {
                val url = URL("https://網址/檔名")    // 沒記錯的話,因安全性要求,一定得是 https
                val urlConn: HttpURLConnection = url.openConnection() as HttpURLConnection

                val bis = BufferedInputStream(urlConn.inputStream, 8192)
                val outfile = File(myDir, "要儲存的檔名")

                val bos = BufferedOutputStream(FileOutputStream(outfile))
                var inByte: Int
                inByte = 8192
                while (bis.read().also({ inByte = it }) != -1)
                    bos.write(inByte)
                bis.close()
                bos.close()
            }
            catch (err:Exception)
            {
                Log.i(TAG, "error: "+err.message)
            }
        }
    }


    private fun requestStoragePermission()
    {
        if (Build.VERSION.SDK_INT > 22)
        {
            if (shouldShowRequestPermissionRationale(Manifest.permission.WRITE_EXTERNAL_STORAGE))
            {
                requestPermissions(arrayOf(Manifest.permission.WRITE_EXTERNAL_STORAGE),
                                            PERMISSION_REQUEST_WRITE_EXTERNAL_STORAGE
                                )
            }
            else
            {
                requestPermissions(arrayOf(Manifest.permission.WRITE_EXTERNAL_STORAGE),
                                    PERMISSION_REQUEST_WRITE_EXTERNAL_STORAGE
                                )
            }
        }
    }

    override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<out String>, grantResults: IntArray)
    {
        if (requestCode == PERMISSION_REQUEST_WRITE_EXTERNAL_STORAGE) {
            if (grantResults.size == 1 && grantResults[0] == PackageManager.PERMISSION_GRANTED)
                {   // 請求權限獲准
                    object : Thread()
                            {
                                override fun run()
                                {
                                    // ... 執行獲准權限後要做的事
                                }
                            }.start()
                }
            else
                {    // 請求權限被拒, 關閉程式
                    finish()
                }
        }
    }
}
 



沒有留言:

張貼留言