【Android/Kotlin】拍照/相簿照片上传到Server

前言:

本篇文章内容注重在把照相照片/相簿照片转成上传至Server的 Multipart.Part格式,
之後有空会再补上去 Retrofit的分享文/images/emoticon/emoticon12.gif
大致上要做的事情如以下

相簿:

▶从相簿那边拿回来的照片Uri会是content://Uri
▶把content://Uri改成真实路径
▶再把它改成要上传的格式 MultipartBody.Part格式

照相:

▶先创造一个临时file
▶建立fileProvider,并把拍完照的照片储存至指定位置
▶透过Uri拿到真实路径
▶改成要上传的MultipartBody.Part格式

相簿

一、新增权限

在Manifest新增以下权限

		//相机
		<uses-permission android:name="android.permission.CAMERA"/>
    
		//写入外部资料权限
		<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
    
		//读外部资料权限
		<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>

在Android6之後,需要向使用者询问权限

fun permissionPhoto() {
ActivityCompat.requestPermissions(
requireActivity(),arrayOf(
android.Manifest.permission.CAMERA,
            android.Manifest.permission.WRITE_EXTERNAL_STORAGE,
            android.Manifest.permission.READ_EXTERNAL_STORAGE
    ), 0
	)
}

二、接下来设定开启相簿

binding.btnPickImage.setOnClickListener {
  val gallery =   
  Intent(Intent.ACTION_PICK,MediaStore.Images.Media.INTERNAL_CONTENT_URI)
  startActivityForResult(gallery, PICTUREFROMGALLERY)
        }

Intent里面第一个参数填写为ACTION_PICK
▶ACTION_PICK:获取相簿照片,回传值为 Content://Uri
▶ACTION_GET_CONTENT:获取所有本地端图片,Android 4.4以下跟ACTION_PICK回传一样,4.4则返回多种格式

并且设定requestCode

companion object {
        const val PICTUREFROMGALLERY = 1001
        const val PICTUREFROMCAMERA = 1002
    }

在startActivityForResult第二个参数填写RequestCode,才可以跟onActivityResult对起来

三、拿返回值

因为我们是用startActivityForResult,所以我们的回传资料从onActivityResult取得

override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {

        super.onActivityResult(requestCode, resultCode, data)
        if (resultCode == Activity.RESULT_OK && requestCode == PICTUREFROMGALLERY) {
            if (data != null && data?.data != null) {
                imageUri = data?.data
                imageView.setImageURI(imageUri)
                imagePath = imageUri?.let { getPathFromUri(it) }
				val file: File = File(imagePath)         
                val requestBody: RequestBody = file.asRequestBody("multipart/form-data".toMediaTypeOrNull())               
                val multipart: MultipartBody.Part = MultipartBody.Part.createFormData("image",file.name,requestBody)              
                viewModel.uploadImage(multipart)
            }
}

关於onActivityResult 我们可以透过

if (resultCode == Activity.RESULT_OK && requestCode == PICTUREFROMGALLERY)

▶reqeustCode来判别是从哪个来源的
▶resultCode来判别回传结果

相簿我们可以透过 data.data去拿到 Content://Uri(该uri不能直接上传)
所以我们需要把它转成可以上传的格式
再过来我们要创一个getPathFromUri的funtion

private fun getPathFromUri(uri: Uri): String{
        val projection = arrayOf(MediaStore.MediaColumns.DATA)
        val cursor = requireActivity().contentResolver.query(uri,projection,null,null,null)
        val column_index: Int? = cursor?.getColumnIndexOrThrow(MediaStore.MediaColumns.DATA) ?: null
        cursor?.moveToFirst()
        val result: String = column_index?.let { cursor?.getString(it) } ?: ""
        cursor?.close()
        return result
    }

▶val projection = arrayOf(MediaStore.MediaColumns.DATA): 需要撷取的资料栏位,以下显示只有DATA的栏位,所以不能印出其他资讯(已被弃用,之後再回来改)
▶cursor:逐一看每一列的欲查询查询栏位内容(参考如下)

https://ithelp.ithome.com.tw/upload/images/20210616/20138017z1yGnyTbn5.png
(每一栏位代表项目,每一列代表实体,图片取自Android官方文件)

▶column_index:取得indext
▶cursor?.moveToFirst():因为cursor的初始位置是 -1,所以要透过此行让他移动到0否则会报以下的错

android.database.CursorIndexOutOfBoundsException: Index -1 requested, with a size of 1

▶result:拿到刚刚搜寻到的结果 path

继续回到onActivitiyResult

			imagePath = imageUri?.let { getPathFromUri(it) }
			val file: File = File(imagePath)         
            val requestBody: RequestBody = file.asRequestBody("multipart/form-data".toMediaTypeOrNull())               
            val multipart: MultipartBody.Part = MultipartBody.Part.createFormData("image",file.name,requestBody)              
                viewModel.uploadImage(multipart)

▶imagePath:拿到刚刚getPathFromUri的返回值
▶file:透过path创建file
▶requestBody:建立requestBody,因为这次上传照片的 content-Type是multipart/form-data,并用toMediaTypeOrNull()转变成asRequestBody所需要的MediaType
▶multipart:透过MultipartBody.Part.创建FormData,第一个参数是要写Server要的Body的Key名称,不然会上传不上去

大功告成啦!!
/images/emoticon/emoticon01.gif

照相

一、创立Intent

一样,首先我们先做出Intent
照相的话有分成两种

在Intent没有给予储存Uri的话,那就会返回缩图,可以透过以下的code拿到bitmap显示

binding.btnTakePicture.setOnClickListener{
          val camera = Intent(MediaStore.ACTION_IMAGE_CAPTURE)
          startActivityForResult(camera, PICTURE_FROM_CAMERA)
}

并在onActivityResult拿到bitmap,并显示

                 val extras = data?.extras
                 val imageBitmap = extras?.get("data") as Bitmap
                 imageView.setImageBitmap(imageBitmap)

但是我们要上传,所以我们就要在Intent的地方先建立一个临时的tmpFile,并用他拿到realPath,那就需要有个fileProvider

我也是很好奇为什麽相簿不用碰到fileProvider,而拍照就要呢?

/後来爬文後发现,onActivityResult方法中可以获取授予临时许可权的Content URI,所以就可以直接用它去转换并上传
但是照相功能则不是返回contentUri,所以我们要给予它一个临时的file,并再获取Content URI/

二、创立fileProvider

打开Manifest

<provider
    android:name="androidx.core.content.FileProvider"
    android:authorities="com.example.bookreports.provider"
    android:exported="false"
    android:grantUriPermissions="true">
<meta-data
            android:name="android.support.FILE_PROVIDER_PATHS"
            android:resource="@xml/provider_paths">

</meta-data>


</provider>

▶authorities: 一个手机里面只能有一个唯一识别
▶exported: 是否给其他应用使用,这边要false 拒绝外部直接访问
▶grantUriPermissions:授予临时Uri权限

再过来去建立 xml档案

<?xml version="1.0" encoding="utf-8"?>
<paths xmlns:android="http://schemas.android.com/apk/res/android">

<!--    外部储存空间的根目录,等同 Environment.getExternalStorageDirectory()获取的路径-->
        <external-path
        name="external_files"
        path="."/>

</paths>

再把它放到Manifest的 fileProvider内的 android:resource

三、回到Intent

既然我们已经创建好fileProvider後,我们就回来看Intent

binding.btnTakePicture.setOnClickListener{

     val camera = Intent(MediaStore.ACTION_IMAGE_CAPTURE)
     //先新增一张照片
     val tmpFile = File(context?.getExternalFilesDir(null),"image.jpg")
     
	//建立uri,这边拿到的格式就会是 content://了
     val outputFileUri = FileProvider.getUriForFile(requireActivity(),"com.example.bookreports.provider",tmpFile)

			imageUri = outputFileUri
            val path: String = tmpFile.absolutePath
			imagePath = path
  
			//指定为输出档案的位置
            camera.putExtra(MediaStore.EXTRA_OUTPUT,outputFileUri)
startActivityForResult(camera, PICTURE_FROM_CAMERA)
}

▶tmpFile:这里根据parent抽象路径名和child路径名字串建立一个新File
▶outputFileUri:getUriFromFile,第二个参数是在Manifest里面填的provider的authority,必须一致
▶path:透过刚刚建立的file,来拿到path

好的,既然我们都拿到path了,我们就来到 onActivityResult

四、转换成MultipartBody.Part

if(imageUri != null){
imageView.setImageURI(imageUri)
val file: File = File(imagePath)
val requestBody:RequestBody = file.asRequestBody("multipart/form-data".toMediaTypeOrNull())
val multipart: MultipartBody.Part = MultipartBody.Part.createFormData("image",file.name,requestBody)
viewModel.uploadImage(multipart)
}

後续就跟之前大同小异
▶imagePath:拿到刚刚从Intent的值
▶file:透过path创建file
▶requestBody:建立requestBody,因为这次上传照片的 content-Type是multipart/form-data,并用toMediaTypeOrNull()转变成asRequestBody所需要的MediaType
▶multipart:透过MultipartBody.Part.创建FormData,第一个参数是要写Server要的Body的Key名称

大功告成!!大功告成!!

/images/emoticon/emoticon74.gif


<<:  替代网站(Alternative Sites)- 冷站点的最大好处

>>:  CI/CD:使用Jenkins(Docker image)自动部署+bitbucket

Day 17 - 卷积神经网络 CNN (2)- 战国时代之版图扩张

再看一次... 注:成功大学 连震杰教授 百家争鸣 我们了解在1998 LeNet / 2012 A...

CSS display:Grid

grid-template-areas 使用 grid-template-areas 定义每个区块,...

从零开始的8-bit迷宫探险【Level 14】让主角奔跑吧!Running Sam

适应了黑森林的孤寂,山姆开始这趟旅程的目的:找寻水晶。 森林虽然漆黑,但是路还算好走,山姆的脚步也...

Day8 - 2D渲染环境基础篇 IV[像素操作概论] - 成为Canvas Ninja ~ 理解2D渲染的精髓

『像素操作(Pixel Manipulation)』 顾名思义就是要去以单一像素为最小单位来进行操作...

[Day2] 开放银行

什麽是开放银行 开放银行(Open Banking)核心目标为为透过Open API的方式将存在於银...