[Huawei Developers] AbdurrahimCillioglu:
View attachment 5231049

Introduction

Hi everyone, In this article, we’ll explore how scheduling a task to upload files to the Huawei Drive with WorkManager. Also, we will develop a demo app using Kotlin in the Android Studio.

Huawei Drive Kit

Drive Kit (the short form of Huawei Drive Kit) allows developers to create apps that use Drive. Drive Kit gives us cloud storage capabilities for our apps, enabling users to store files that are created while using our apps, including photos, videos, and documents.

Some of the main function of the Drive kit:

Obtaining User Information

Managing and Searching for Files

Storing App Data

Performing Batch Operations

You can find more information in the official documentation link.

We’re not going to go into the details of adding Account Kit and Drive Kit to a project. You can follow the instructions to add Drive Kit to your project via official docs or codelab.

WorkManager

WorkManager is an API that makes it easy to schedule deferrable, asynchronous tasks that are expected to run even if the app exits or the device restarts. WorkManager gives us a guarantee that our action will be taken, regardless of if the app exits. But, our tasks can be deferrable to wait for some constraints to be met or to save battery life. We will go into details on WorkManager while developing the app.

Our Sample Project DriveWithWorkManager

In this project, we’re going to develop a sample app that uploading users’ files to their drive with WorkManager. Developers can use the users’ drive to save their photos, videos, documents, or app data. With the help of WorkManager, we ensure that our upload process continues even if our application is terminated.

View attachment 5231051

Setup the Project

Add the necessary dependencies to build.gradle (app level)

// HMS Account Kit
implementation 'com.huawei.hms:hwid:5.1.0.301'

// HMS Drive Kit
implementation 'com.huawei.hms:drive:5.0.0.301'

// WorkManager
implementation "androidx.work:work-runtime-ktx:2.5.0"

// Kotlin Coroutines for asynchronously programming
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.4.1'

Layout File

activity_main.xml is the only layout file in our project. There are two buttons here, one button for login with Huawei ID and one button for creating a work. I should note that apps that support Huawei ID sign-in must comply with the Huawei ID Sign-In Button Usage Rules. Also, we used the Drive icon here. But, the icon must comply with the HUAWEI Drive icon specifications. We can download and customize icons in compliance with the specifications. For more information about the specifications, click here.

<?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"
android:background="#D0D0D0"
tools:context=".MainActivity">

<com.huawei.hms.support.hwid.ui.HuaweiIdAuthButton
android:id="@+id/btnLogin"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="32dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.497"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/imageView" />

<Button
android:id="@+id/btnCreateWork"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="32dp"
android:backgroundTint="#1f70f2"
android:text="Create a Work"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.498"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/btnLogin" />

<TextView
android:id="@+id/textView"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginTop="32dp"
android:layout_marginEnd="16dp"
android:text="Upload File to Huawei Drive with WorkManager"
android:textAlignment="center"
android:textColor="#1f70f2"
android:textSize="24sp"
android:textStyle="bold"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />

<ImageView
android:id="@+id/imageView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="32dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/textView"
app:srcCompat="@drawable/ic_drive" />

</androidx.constraintlayout.widget.ConstraintLayout>

Permission for Storage

We need permission to access the phone’s storage. Let’s add the necessary permissions to our manifest.

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

We have two code file in our application: MainActivity.kt and UploadWorker.kt

MainActivity

MainActivity.kt -> Drive functions strongly depend on Huawei Id. To use Drive functions, users must sign in with their Huawei IDs. In this file, we perform our login process and create new works.

Let’s interpret the functions on this page one by one.

onCreate() - Starting in API 23, we need to request the user for specific permission on runtime. So, we added a simple permission request. Then, we added our button-click listeners.

signIn() - We create a scope list and added the necessary drive permissions. Then, we started the login process. If you want to use other drive functions, ensure to add permission here.

refreshAt() - We can obtain a new accessToken through the HMS Core SDK.

checkDriveCode() - First, we checked whether the unionId and access token are null or an empty string. Then, we construct a DriveCredential.Builder object and returned the code.

onActivityResult() - We get the authorization result and obtain the authorization code from AuthAccount.

createWorkRequest() - I would like to explain this function after a quick explanation of the Work Request.

Creating Work Request

This is an important part of our application. With the creatingWorkRequest function, we create a work request. There are two types of work requests; OneTimeWorkRequest and PeriodicWorkRequest. OneTimeWorkRequest is run only once. We used it for simplicity in our example. PeriodicWorkRequest is used to run tasks that need to be called periodically until canceled.

createWorkRequest() - We created an OneTimeWorkRequest and added input data as accessToken and unionId to the work. We would also like to make sure that our works only run in certain situations such as we have a network and not a low battery. So, we used constraints to achieve this. Finally, we enqueued our uploadWorkRequest to run.

class MainActivity : AppCompatActivity() {

private val TAG = "MainActivity"

private val REQUEST_SIGN_IN_CODE = 1001
private val REQUEST_STORAGE_PERMISSION_CODE = 1002

private lateinit var btnLogin: HuaweiIdAuthButton
private lateinit var btnCreateWork: Button

private var accessToken: String = ""
private var unionId: String = ""

private lateinit var driveCredential: DriveCredential

private val PERMISSIONS_STORAGE = arrayOf(
Manifest.permission.READ_EXTERNAL_STORAGE,
Manifest.permission.WRITE_EXTERNAL_STORAGE,
)

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

btnLogin = findViewById(R.id.btnLogin)
btnCreateWork = findViewById(R.id.btnCreateWork)

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
requestPermissions(PERMISSIONS_STORAGE, REQUEST_STORAGE_PERMISSION_CODE)
}

btnLogin.setOnClickListener {
signIn()
}

btnCreateWork.setOnClickListener {
if (accessToken.isEmpty() || unionId.isEmpty()) {
showToastMessage("AccessToken or UnionId is empty")
} else {
createWorkRequest(accessToken, unionId)
}
}

}

private fun signIn() {
val scopeList: MutableList<Scope> = ArrayList()

scopeList.apply {
add(Scope(DriveScopes.SCOPE_DRIVE))
add(Scope(DriveScopes.SCOPE_DRIVE_FILE))
add(Scope(DriveScopes.SCOPE_DRIVE_APPDATA))
add(HuaweiIdAuthAPIManager.HUAWEIID_BASE_SCOPE)
}

val authParams = HuaweiIdAuthParamsHelper(
HuaweiIdAuthParams.DEFAULT_AUTH_REQUEST_PARAM
)
.setAccessToken()
.setIdToken()
.setScopeList(scopeList)
.createParams()

val client = HuaweiIdAuthManager.getService(this, authParams)
startActivityForResult(client.signInIntent, REQUEST_SIGN_IN_CODE)
}

private val refreshAT = DriveCredential.AccessMethod {
/**
* Simplified code snippet for demonstration purposes. For the complete code snippet,
* please go to Client Development > Obtaining Authentication Information > Save authentication information
* in the HUAWEI Drive Kit Development Guide.
**/

return@AccessMethod accessToken
}

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

if (requestCode == REQUEST_SIGN_IN_CODE) {
val authAccountTask = AccountAuthManager.parseAuthResultFromIntent(data)
if (authAccountTask.isSuccessful) {

val authAccount = authAccountTask.result

accessToken = authAccount.accessToken
unionId = authAccount.unionId

val driveCode = checkDriveCode(unionId, accessToken, refreshAT)

when (driveCode) {
DriveCode.SUCCESS -> {
showToastMessage("You are Signed In successfully")
}
DriveCode.SERVICE_URL_NOT_ENABLED -> {
showToastMessage("Drive is not Enabled")
}
else -> {
Log.d(TAG, "onActivityResult: Drive SignIn Failed")
}
}

} else {
Log.e(
TAG,
"Sign in Failed : " + (authAccountTask.exception as ApiException).statusCode
)
}
}
}

private fun checkDriveCode(
unionId: String?,
accessToken: String?,
refreshAccessToken: DriveCredential.AccessMethod?
): Int {
if (StringUtils.isNullOrEmpty(unionId) || StringUtils.isNullOrEmpty(accessToken)) {
return DriveCode.ERROR
}
val builder = DriveCredential.Builder(unionId, refreshAccessToken)
driveCredential = builder.build().setAccessToken(accessToken)
return DriveCode.SUCCESS
}

private fun createWorkRequest(accessToken: String, unionId: String) {
val uploadWorkRequest: WorkRequest = OneTimeWorkRequestBuilder<UploadWorker>()
.setInputData(
workDataOf(
"access_token" to accessToken,
"union_Id" to unionId
)
)
.setConstraints(
Constraints.Builder()
.setRequiredNetworkType(NetworkType.CONNECTED)
.setRequiresBatteryNotLow(true)
.build()
)
.setInitialDelay(1, TimeUnit.MINUTES)
.build()

WorkManager.getInstance(applicationContext).enqueue(uploadWorkRequest)
showToastMessage("Work Request is created")
}

private fun showToastMessage(message: String) {
Toast.makeText(this, message, Toast.LENGTH_SHORT).show()
}

}

UploadWorker

In time, WorkManager will run a worker. To define a worker, we should create a class that extends the Worker class. For Kotlin users, WorkManager provides first-class support for coroutines. So, we can extend our UploadWorker class from CoroutinesWorker. And, it takes two parameters; context and worker parameters.

Then, we need to override the doWork function. doWork is a suspending function, it means that we can run asynchronous tasks and perform network operations. Also, it handles stoppages and cancellations automatically.

Dispatchers.IO is optimized to perform network I/O outside of the main thread. So, we call the withContext(Dispatchers.IO) to create a block that runs on the IO thread pool.

To gets the accessToken and unionId as input data, we used inputData.getString. Then, we checked the drive status. If our access code is still valid, we can use it in the Drive Kit. Otherwise, we have to get renew our code to use Drive Kit.

createAndUploadFile() - First, we created a folder in the Drive named DriveWithWorkManager. We have already put a photo on Android/data/com.huawei.drivewithwork/files as you see in the below image. Note: Don’t forget to replace the package name with yours and put a sample image.

Then, we got the file path of the image on Android.

View attachment 5231053

There are two ways to upload files: direct upload and resumable upload. We used the direct upload in our sample app. Direct upload allows a file of max. 20 MB and, resumable upload doesn’t have a limit. Direct upload is recommended for files smaller than 5 MB and resumable upload for files larger than 5 MB. You can also see the codes of resumable upload as the comment.

class UploadWorker(val appContext: Context, val params: WorkerParameters) :
CoroutineWorker(appContext, params) {

private val TAG = "UploadWorker"

private var accessToken: String = ""
private var unionId: String = ""

private lateinit var driveCredential: DriveCredential

companion object {

private val MIME_TYPE_MAP: MutableMap<String, String> = HashMap()

init {
MIME_TYPE_MAP.apply {
MIME_TYPE_MAP[".doc"] = "application/msword"
MIME_TYPE_MAP[".jpg"] = "image/jpeg"
MIME_TYPE_MAP[".mp3"] = "audio/x-mpeg"
MIME_TYPE_MAP[".mp4"] = "video/mp4"
MIME_TYPE_MAP[".pdf"] = "application/pdf"
MIME_TYPE_MAP[".png"] = "image/png"
MIME_TYPE_MAP[".txt"] = "text/plain"
}
}
}

override suspend fun doWork(): Result {

return withContext(Dispatchers.IO) {
try {

accessToken = inputData.getString("access_token") ?: ""
unionId = inputData.getString("union_Id") ?: ""

if (accessToken.isEmpty() || unionId.isEmpty()) {
Result.failure()
} else {

val driveCode = checkDriveCode(unionId, accessToken, refreshAT)

if (driveCode == DriveCode.SUCCESS) {
GlobalScope.launch { createAndUploadFile() }
} else {
Log.d(TAG, "onActivityResult: DriveSignIn Failed")
}

Result.success()
}

} catch (error: Throwable) {
Result.failure()
}
}
}

private fun checkDriveCode(
unionId: String?,
accessToken: String?,
refreshAccessToken: DriveCredential.AccessMethod?
): Int {
if (StringUtils.isNullOrEmpty(unionId) || StringUtils.isNullOrEmpty(accessToken)) {
return DriveCode.ERROR
}
val builder = DriveCredential.Builder(unionId, refreshAccessToken)
driveCredential = builder.build().setAccessToken(accessToken)
return DriveCode.SUCCESS
}

private val refreshAT = DriveCredential.AccessMethod {
/**
* Simplified code snippet for demonstration purposes. For the complete code snippet,
* please go to Client Development > Obtaining Authentication Information > Save authentication information
* in the HUAWEI Drive Kit Development Guide.
**/

return@AccessMethod accessToken
}

private fun buildDrive(): Drive? {
return Drive.Builder(driveCredential, appContext).build()
}

private fun createAndUploadFile() {
try {

val appProperties: MutableMap<String, String> =
HashMap()
appProperties["appProperties"] = "property"

val file = com.huawei.cloud.services.drive.model.File()
.setFileName("DriveWithWorkManager")
.setMimeType("application/vnd.huawei-apps.folder")
.setAppSettings(appProperties)

val directoryCreated = buildDrive()?.files()?.create(file)?.execute()

val path = appContext.getExternalFilesDir(null)

val fileObject = java.io.File(path.toString() + "/NatureAndMan.jpg")
appContext.getExternalFilesDir(null)?.absolutePath

val mimeType = mimeType(fileObject)

val content = com.huawei.cloud.services.drive.model.File()
.setFileName(fileObject.name)
.setMimeType(mimeType)
.setParentFolder(listOf(directoryCreated?.id))
buildDrive()?.files()
?.create(content, FileContent(mimeType, fileObject))
?.setFields("*")
?.execute()

// Resumable upload for files larger than 5 MB.
/*
val fileInputStream = FileInputStream(fileObject)
val inputStreamLength = fileInputStream.available()

val streamContent = InputStreamContent(mimeType, fileInputStream)
streamContent.length = inputStreamLength.toLong()
val content = com.huawei.cloud.services.drive.model.File()
.setFileName(fileObject.name)
.setParentFolder(listOf(directoryCreated?.id))
val drive = buildDrive()
drive!!.files().create(content, streamContent).execute()
*/

} catch (exception: Exception) {
Log.d(TAG, "Error when creating file : $exception")
}
}

private fun mimeType(file: File?): String? {
if (file != null && file.exists() && file.name.contains(".")) {
val fileName = file.name
val suffix = fileName.substring(fileName.lastIndexOf("."))
if (MIME_TYPE_MAP.keys.contains(suffix)) {
return MIME_TYPE_MAP[suffix]
}
}
return "*/*"
}

}

Now, everything is ready. We can upload our file to the Drive. Let’s run our app and see what happens.

Launch the app and login with your Huawei Id. Then click the Create A Work Button. After waiting at least a minute, WorkManager will run our work if the conditions are met. And our photo will be uploaded to the drive.

View attachment 5231055

Tips & Tricks

Your app can save app data, such as configuration files and archives in the app folder inside Drive. This folder stores any files with which the user does not have direct interactions. Also, this folder can be accessed only by your app, and the content in the folder is hidden to the user and other apps using Drive.

If the size of the file you download or upload is big, you can use NetworkType.UNMETERED constraint to reduce the cost to the user.

Conclusion

In this article, we have learned how to use Drive Kit with WorkManager. And, we’ve developed a sample app that uploads images to users’ drives. In addition to uploading a file, Drive Kit offers many functions such as reading, writing, and syncing files in Huawei Drive. Please do not hesitate to ask your questions as a comment.

Thank you for your time and dedication. I hope it was helpful. See you in other articles.

References

Huawei Drive Kit Official Documentation

Huawei Drive Kit Official Codelab

WorkManager Official Documentation