[Huawei Developers] muraliameakula:
The new Huawei Network kit provides you with convenient and easy to use APIs to perform HTTP operations. You can use it as HTTP Client to quickly download many kind of files from internet. In this article we are going to use it to download files from Huawei Drive by using the Public APIs, by this way we will be able to browse and download files, even from non-huawei devices.

Note: This article is a continuation of my previous post called "Accessing to Huawei Drive by using AppAuth and REST APIs". Is highly recommended to read it first before continue with this one.

Integrating Drive Kit

We will use the new Huawei Drive kit to download the user's files stored in Huawei Drive, by doing so, the kit will handle the HTTP calls in asynchronous mode for us. Add the Network Kit SDK under dependencies in your app-level build.gradle. We will also use CardView to display the user's files in a list, so let us add both dependencies.

implementation 'com.huawei.hms:network-embedded:5.0.1.301'
implementation "androidx.cardview:cardview:1.0.0"

Network Kit must be initialized before calling it's APIs, we can ensure the initialization upon each startup by defining our own Application class.

MyApplication.kt

class MyApplication : Application(){
override fun onCreate() {
super.onCreate()
initNetworkKit()
}
fun initNetworkKit() {
// Initialize the object only once, upon the first call.
NetworkKit.init(this, object : NetworkKit.Callback() {
override fun onResult(result: Boolean) {
val TAG = "NetworkKit"
if (result) {
Log.i(TAG, "Networkkit init success")
} else {
Log.i(TAG, "Networkkit init failed")
}
}
})
}
}

Do not forget to register your Application class in your AndroidManifest.xml

AndroidManifest.xml (Modifying)

<application
android:name=".MyApplication"
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/Theme.AppAuthDriveKit">

Modifying the Contract

Let us add the file management related methods to our abstract class, this will be useful if we want to add more Drive providers in the future (like Microsoft's OneDrive).

DriveService.kt (Modifying)

abstract class DriveService (val service: AuthorizationService,
val authState: AuthState){
var driveListener:OnDriveEventListener?=null
abstract fun startService()
abstract fun listFiles()
abstract fun downloadFile(context: Context, cloudFile: CloudFile)
abstract fun uploadFile(file: File)

interface OnDriveEventListener{
fun onServiceReady(storageData: String)
fun onServiceFailure(info: String)
fun onFilesListed(files: List<CloudFile>)
fun onFileUploaded()
fun onDownloadStarted(cloudFile: CloudFile)
fun onDownloadProgress(cloudFile: CloudFile, progress: Int)
fun onFileDownloaded(cloudFile: CloudFile)
}
}

Listing the Files

We will call the Files.List API to get the details of all the stored user's files on his Root directory. First, we need a class to store all the information of a file in the cloud. A data class will be useful to achieve this.

Note: A file object in the cloud has a lot of information, for demonstration purposes we will just keep the most important properties.

data class CloudFile(
val filename: String,
val fileSuffix: String,
val createdTime: String,
val downloadLink: String,
val mimeType:String,
val fileSize:Int
){
val stringSizeInKb="${fileSize/1024} KB"
}

Previously, we have called he About API to get some general information about the user's Drive, into that information, it came the "domain", that's the name of the host which is able to dispatch all the Drive requests related to the current user. We will use the "domain" to query the file list. This information will be obtained by using a normal HTTP call.

Note: The next code can be improved by using the REST Client mode of Network Kit, but is not the focus of this article.

HuaweiDriveService.kt (modifying)

override fun listFiles() {
CoroutineScope(Dispatchers.IO).launch {
authState.performActionWithFreshTokens(service){accessToken,_,_->
val url="$domain/drive/v1/files?fields=*"
val conn=URL(url).openConnection() as HttpURLConnection
conn.addRequestProperty("Authorization","Bearer $accessToken")
conn.addRequestProperty("Cache-Control","no-cache")
val response=if(conn.responseCode<400) convertStreamToString(conn.inputStream)
else convertStreamToString(conn.errorStream)
if(conn.responseCode==200){
val json=JSONObject(response)
buildList(json)
}else driveListener?.onServiceFailure(response)
}
}
}

As you can see, the registered OnDriveEventListener will receive the list of available files in the Cloud. In this case, the DriveViewModel, which will store the files list into a MutableLiveData.

DriveViewModel.kt(Modifying)

var list=MutableLiveData<ArrayList<CloudFile>>()

override fun onFilesListed(files: List<CloudFile>) {
list.apply {
value?.run {
clear()
addAll(files)
}
postValue(value)
}
}

Let us define the view which will be used to display our file items.

file_item.xml

<?xml version="1.0" encoding="utf-8"?>
<layout
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"
xmlns:card_view="http://schemas.android.com/apk/res-auto">
<data class="FileItemBinding"
><variable
name="item"
type="com.hms.demo.appauthdrivekit.CloudFile" /></data>
<androidx.cardview.widget.CardView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="5dp"
card_view:cardElevation="20dp"
card_view:cardCornerRadius="15dp"
android:foreground="?android:attr/selectableItemBackground"
android:clickable="true"
android:focusable="true"
>
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:padding="5dp"
android:layout_height="wrap_content"
>

<ImageView
android:id="@+id/icon"
android:layout_width="60dp"
android:layout_height="60dp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:srcCompat="@drawable/ic_insert_drive_file_black_18dp"
android:src="@drawable/ic_insert_drive_file_black_18dp"
android:layout_marginEnd="5dp"/>

<TextView
android:id="@+id/fileName"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="5dp"
android:layout_marginTop="5dp"
android:text="@{item.filename}"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@+id/icon"
app:layout_constraintTop_toTopOf="parent" />

<TextView
android:id="@+id/fileType"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="5dp"
android:text="@{item.fileSuffix}"
app:layout_constraintBottom_toTopOf="@+id/fileSize"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.0"
app:layout_constraintStart_toEndOf="@+id/icon"
app:layout_constraintTop_toBottomOf="@+id/fileName" />

<TextView
android:id="@+id/fileSize"
android:layout_width="0dp"
android:layout_height="19dp"
android:layout_marginStart="5dp"
android:layout_marginBottom="5dp"
android:text="@{item.stringSizeInKb}"
android:textAlignment="textEnd"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@+id/icon" />

</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.cardview.widget.CardView>
</layout>

Is time to create the adapter, a download request will be triggered if the user performs a long click on any item.

FileAdapter.kt

class FileAdapter : RecyclerView.Adapter<FileAdapter.FileVH>() {

lateinit var items:ArrayList<CloudFile>
var cloudFileEventListener:CloudFileEventListener?=null

class FileVH(private val binding: FileItemBinding): RecyclerView.ViewHolder(binding.root),View.OnLongClickListener {
var fileEventListener:CloudFileEventListener?=null
fun bind(item:CloudFile){
binding.item=item
binding.root.setOnLongClickListener(this)
}

override fun onLongClick(v: View?): Boolean {
binding.item?.let {
fileEventListener?.onDownloadRequest(it)
}

return true
}

}

override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): FileVH {
val layoutInflater=LayoutInflater.from(parent.context)
val binding=FileItemBinding.inflate(layoutInflater,parent,false)
return FileVH(binding).apply { fileEventListener= cloudFileEventListener}
}

override fun onBindViewHolder(holder: FileVH, position: Int) {
holder.bind(items[position])
}

override fun getItemCount(): Int {
return items.size
}

interface CloudFileEventListener{
fun onDownloadRequest(item:CloudFile)
}
}

Downloading a file

Before performing any File operation, we must make sure to have the related permissions granted, by othe way we will fall in a SecurityException. the DriveViewModel will listen for the download requests from the Recycler Adapter, and this Viewmodel is connected to our activity by the DriveNavigator interface, by using that connections, we can ask to the activity to check if the storage permissions are granted upon any download request.

DriveVM.kt (Modifying)

override fun onDownloadRequest(item:CloudFile) {
navigator?.apply {
if(checkStoragePermissions()){
val context=navigator as Context
driveService?.downloadFile(context,item)
} else requestStoragePermissions()
}
}

From the DriveActivity, we must check if the permissions are granted and asking for them if not.

DriveActivity.kt (Modifying)

override fun checkStoragePermissions(): Boolean {
val read=ContextCompat.checkSelfPermission(this,Manifest.permission.READ_EXTERNAL_STORAGE)
val write=ContextCompat.checkSelfPermission(this,Manifest.permission.WRITE_EXTERNAL_STORAGE)
return read==PackageManager.PERMISSION_GRANTED&&write==PackageManager.PERMISSION_GRANTED
}

override fun requestStoragePermissions() {
val permissions= arrayOf(Manifest.permission.READ_EXTERNAL_STORAGE,Manifest.permission.WRITE_EXTERNAL_STORAGE)
requestPermissions(permissions, STORAGE_PERMISSIONS)
}

Since Android 10, the access to the user's storage must be performed by using MediaStore. As the file creation process is the same no matter the Drive provider (Huawei, Microsoft, Google, etc.), let's add to our DriveService class the capability of creating new Files, so any subclass of this will be able to create new files in the local storage.

DriveService.kt (Modifying)

open fun createFileUri(context: Context, item: CloudFile):Uri?{
val resolver=context.contentResolver
val contentValues = ContentValues().apply {
put(MediaStore.MediaColumns.DISPLAY_NAME, item.filename)
put(MediaStore.MediaColumns.MIME_TYPE, item.mimeType)
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.Q) {
put(MediaStore.MediaColumns.RELATIVE_PATH, "Documents")
}
}
return resolver.insert(MediaStore.Files.getContentUri("external"), contentValues)
}

Is time to download the file, at this point you may think we can easily download files by using the Network Kit file downloading capability. The problem here is the files.get web API of Drive Kit, doesn't provide you a file by definition, it provides only the file content and we must use that content as well as the file metadata to build the file by ourselves.

Let's suppose the APIs from other Drive providers have the same behavior (provide only the file content), in that case, we can add the download capability to the DriveService class. to achieve our goal, we must create an HTTP Client and a request.

DriveService.kt (Modifying)

private val httpClient:HttpClient by lazy { HttpClient.Builder()
.connectTimeout(10000)
.build() }

open fun buildGetRequest(url: String, headers: Map<String, String>):Request{
return httpClient.newRequest().apply {
url(url)
method("GET")
for(key in headers.keys){
addHeader(key,headers[key])
}
}.build()
}

Now we are ready to develop the file downloading capability.

DrveService.kt (Modifying)

open fun enqueueDownload(output:OutputStream,cloudFile: CloudFile, request: Request) {
httpClient.newSubmit(request).enqueue(object : Callback<ResponseBody?>() {
@Throws(IOException::class)
override fun onResponse(
submit: Submit<ResponseBody?>?,
response: Response<ResponseBody?>
) {
if(response.isOK){
driveListener?.onDownloadStarted(cloudFile)
response.body?.inputStream?.let {input->
val fileWriter= DataOutputStream(output)
val bytes=ByteArray(1024)
var downloaded=0
var readed=0
while (input.read(bytes,0,bytes.size).also { readed=it }>0){
downloaded+=readed
fileWriter.apply {
write(bytes,0,readed)
flush()
val progress=(downloaded*100)/cloudFile.fileSize
driveListener?.onDownloadProgress(cloudFile,progress)
}
}
fileWriter.close()
driveListener?.onFileDownloaded(cloudFile)
}
}
}

override fun onFailure(submit: Submit<ResponseBody?>?, throwable: Throwable) {
// Process the exception if the request fails.
Log.w("Download", "response onFailure = " + throwable.message)
}
})
}

As you can see, we are using the OnDriveEventListener interface to notify the download state and the download progress. Is time to override the downloadFile function in our HuaweiDriveService class.

HuaweiDriveService.kt (Modifying)

override fun downloadFile(context: Context, cloudFile: CloudFile) {
authState.performActionWithFreshTokens(service){ accessToken, _, _->
//Creating the file in the local storage
createFileUri(context,cloudFile)?.let {
val output=context.contentResolver.openOutputStream(it)
//Adding the authorization information for the request
val headers= mapOf("Content-Type" to "application/json","Authorization" to "Bearer $accessToken")

//Building the request
val request=buildGetRequest(cloudFile.downloadLink,headers)

//Starting the download
output?.let { stream -> enqueueDownload(stream,cloudFile,request) }
}

}

}

Starting and listening the download

Let's go back to the DriveVM class, from here, if a download request is received and the proper permissions are granted, it will call the driveService.downloadFile function to start the download task. The ViewModel will be notified about the download status changes thorugh the DriveService.OnDriveEventListener interface.

DriveVM.kt (Modifying)

override fun onDownloadStarted(cloudFile: CloudFile) {
Log.i("File","onDownloadStarted ${cloudFile.filename}")
}

override fun onDownloadProgress(cloudFile: CloudFile, progress: Int) {
navigator?.onDownloadProgress(cloudFile,progress)
}

override fun onFileDownloaded(cloudFile: CloudFile) {
navigator?.onFileDownloaded(cloudFile)
}

From this callbacks we will notify the activity (DriveNavigator) to update the user interface upon any download update.

DriveActivity.kt (Modifying)

private var progressDialog:AlertDialog?=null

override fun onDownloadProgress(cloudFile: CloudFile, progress: Int) {
runOnUiThread{
if(progressDialog==null) setupDialog(cloudFile.filename)
progressDialog?.apply {
setMessage("Progress: ${progress}% of ${cloudFile.fileSize/1000}KB")
show()
}
}
}

override fun onFileDownloaded(cloudFile: CloudFile) {
runOnUiThread {
progressDialog?.let {
if(it.isShowing) it.dismiss()
it.cancel()
}
progressDialog=null

AlertDialog.Builder(this)
.setTitle("Download Complete")
.setMessage("File ${cloudFile.filename} downloaded successfully!")
.setPositiveButton("Ok"){dialogInterface,_->
dialogInterface.dismiss()
}
.create().show()
}

}

private fun setupDialog(filename:String) {
progressDialog=AlertDialog.Builder(this).setTitle("Downloading: $filename").setCancelable(false).create()
}

Final Result

Conclusion

We have used Network Kit to download files from Huawei Drive without depending on the HMSCore APK, by this way, our app will be able to work even in non-Huawei Devices.

Network kit provides powerful APIs as it's File Upload/Download feature or it's REST Client mode, but is also useful to perform lower-level operations if is used in HttpClient mode, this versatility can help you to build any HTTP operation from your app with support for QUIC and Asynchronous mode.

Reference

Network Kit: URL Request

Drive Kit: Files.get

Original Source