AWS Amplifyを使ってAndroidアプリ（AWS Cognito認証、S3へファイルアップロード）を開発してみた

# はじめに
AWS Amplifyを使えば高速にアプリ開発ができるみたいだが、実際に使ったことがないので、AWS Amplifyを使った簡単なAndroidアプリの開発をやってみました。
Androidアプリ開発用の統合開発環境（IDE）は、Android Studioを用います。
AmplifyやAndroid Studioを使ったことないけど、興味がある方はぜひ参考にしてみてください！
### 前提条件
- Java、Node.js、npmがインストールされていること
- AWSアカウントを持っていること
### 開発環境
OS：Windows 11 Home
Java：OpenJDK 11.06
Node.js：16.17.1
npm：8.19.2
Android Studio：2022.1
Amplify CLI：10.7.2
# Android Studioについて
### 概要
Android Studioは、Googleが提供するAndroidアプリケーション開発のためのIDEです。JavaやKotlinなどのプログラミング言語を使用して、Androidアプリケーションを開発するためのツールを提供します。Android Studioには、コードエディタ、デバッガ、Gradle ベースのビルドシステム、UIデザイナー、エミュレータ、およびその他の開発ツールが含まれています。

https://developer.android.com/studio/intro?hl=ja

### インストール手順
こちらを参考にAndroid Studioのインストールを行ってください。尚、Android Studioを利用するには、Javaのインストールが必要です。Javaがインストールされていない場合は、先にJavaのインストールを行ってください。 

https://developer.android.com/studio/install?hl=ja#windows

具体的には、以下の手順になります。
1. 公式サイトからAndroid Studioをダウンロードします。
1. ダウンロードしたファイルを開き、インストールウィザードに従ってインストールを行います。
1. インストールが完了したら、Android Studioを起動します。
1. 初回起動時には、必要なSDKやツールのダウンロードが行われます。ダウンロードには時間がかかる場合があります。
1. ダウンロードが完了したら、新規プロジェクトを作成することができます。
# AWS Amplifyについて
### 概要
AWS Amplifyは、モバイルアプリやWebアプリの開発を簡単にするためのフレームワークです。機能として、バックエンドの構築、認証、ストレージ、API、分析などが提供されており、開発者がこれらの機能を簡単に実装できます。Amplifyは、React、React Native、Angular、VueなどのフレームワークやAWSのクラウドサービスと統合されており、開発者は自分の好きなフレームワークやAWSのサービスを使用してアプリを開発することができます。

https://aws.amazon.com/jp/amplify/?nc=sn&loc=0

### インストール手順
こちらを参考にAWS Amplifyの導入を行います。

https://docs.amplify.aws/cli/start/install/

具体的には、以下の手順になります。
1. 次のコマンドを実行して、AWS Amplify CLIをインストールします。尚、AWS Amplify CLI は Node.js に依存します。AWS Amplify CLI を使用することで、コードをローカルで開発し、AWS Amplifyにデプロイできます。また、AWS Amplifyでサポートされている多数のサービスを簡単に追加できます。
```
npm install -g @aws-amplify/cli
```
2\. 次のコマンドを実行して、AWSユーザに接続できるようにAmplifyを構成します。
```
amplify configure
```
# アプリ開発
### 概要
今回開発するアプリの機能目標は以下の通りです。
- Amazon Cognitoのユーザを作成することができる。
- Amazon Cognitoのユーザにログインすることができる。
- Androidアプリケーションの内部ストレージに保存されている動画ファイルをAWS S3へアップロードすることができる。
### アプリの実装手順
##### 1\. Androidプロジェクトの作成
1\. Android Studioを開きます。[New Project]から[Empty Activity]を選択し、[Next]を押します。
![image2023-3-15_17-58-17.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3287572/c178a035-3d0d-d5f7-1d4b-5202f239a448.png)
2\. プロジェクトに次の情報を入力し、[FInish]を押します。
 <table>
    <tr>
      <td><b>Name</b></td>
      <td>AmplifyDemo</td>
    </tr>
    <tr>
      <td><b>Language</b></td>
      <td>Kotlin</td>
    </tr>
    <tr>
      <td><b>Mininum SDK</b></td>
      <td>API 24: Android 7.0 (Nougat)</td>
    </tr>
 </table>

![image2023-3-15_18-1-44.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3287572/3d514234-05f2-9951-f38c-d1299df5aab5.png)
3\. `build.gradle(:app)`を開きます。dependenciesを見ると、androidx.core:core-ktxのバージョンが古いという警告が出ているので、1.9.0に変更して、[Sync Now]を押します。[Sync Now]を押すことで、ファイルが同期されます。
![image2023-3-17_10-4-54.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3287572/29a5e503-4ace-8f73-66ba-051ef2502ec3.png)
##### 2\. Amplifyの導入
以下のガイドを参考に、AndroidプロジェクトにAmplifyの導入を行います。

https://docs.amplify.aws/lib/project-setup/create-application/q/platform/android/#3-provision-the-backend-with-amplify-cli

a\. まず、ターミナルを開き、プロジェクトのルートディレクトリへ移動します。
```
cd ~/AndroidStudioProjects/AmplifyDemo
```
b\. 以下のコマンドで、Amplifyの初期化を行います。
```
amplify init
```
c\. 上記のコマンドを実行すると、使用エディタ、AWSユーザ情報などを聞かれるので入力します。
```
? Enter a name for the project
AmplifyDemo
? Initialize the project with the above configuration?
n
? Enter a name for the environment
dev
? Choose your default editor:
Android Studio
? Choose the type of app that you're building
android
? Where is your Res directory:
app/src/main/res
? Select the authentication method you want to use:
AWS profile
? Please choose the profile you want to use
default
? Help improve Amplify CLI by sharing non sensitive configurations on failures
n
```
d\. `amplify init`が正常に終了すると、`./app/src/main/res/raw/amplifyconfiguration.json`が作成されます。
e\. 次に、Androidプロジェクト側の実装を行います。まず、`build.gradle(:app)`に以下を追加し、[Sync Now]を押します。
```build.gradle
android {
    compileOptions {
        // Support for Java 8 features
        coreLibraryDesugaringEnabled true
        sourceCompatibility JavaVersion.VERSION_1_8
        targetCompatibility JavaVersion.VERSION_1_8
    }
}
dependencies {
    implementation 'com.amplifyframework:core:2.2.2'
    coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:2.0.2'
}
```
f\. 次にAmplify の初期化を行うクラスを追加します。`com.example.amplifydemo`を右クリックして、[New] - [Kotlin Class/File]をクリックします。Nameは「MyAmplifyApp」とし、コードを以下に書き換えます。
```MyAmplifyApp.kt
package com.example.amplifydemo
 
import android.app.Application
import android.util.Log
import com.amplifyframework.AmplifyException
import com.amplifyframework.core.Amplify
 
class MyAmplifyApp: Application() {
    override fun onCreate() {
        super.onCreate()
 
        try {
            Amplify.configure(applicationContext)
            Log.i("MyAmplifyApp", "Initialized Amplify")
        } catch (error: AmplifyException) {
            Log.e("MyAmplifyApp", "Could not initialize Amplify", error)
        }
    }
}
```
g\. 次に、作成したクラスを使用するように`manifests/AndroidManifest.xml`に`android:name=".MyAmplifyApp"`を追記します。`AndroidManifest.xml`とは、Androidアプリケーションの設定ファイルであり、アプリケーションの構成情報を含むものです。
```AndroidManifest.xml
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools">
 
    <!-- Add the android:name attribute to the application node -->
    <application
        android:name=".MyAmplifyApp"
        ...>
    </application>
</manifest>
```
h\. 次に、アプリケーションをビルドして実行するのですが、実行にはAndroidデバイスが必要です。そのために、Android Virtual Device（AVD）を作成します。[Tools] - [DeviceManager]を開き、[Create device]をクリックします。
i\. Pixel XLを選択し、[Next]をクリックします。
![image2023-3-17_12-49-29.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3287572/ad150f96-16e3-5721-92fd-86a864352b51.png)
j\. Recommendedタブには推奨されるシステムイメージの一覧が表示されます。今回は、API Levelが31ものを選んで[Next]をクリックします。
![image2023-3-17_12-53-37.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3287572/babed313-55d5-7b80-1ba1-20532944d866.png)
k\. 名前の変更などが行えますが、今回は変えずに[Finish]ボタンをクリックします。
![image2023-3-17_12-55-28.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3287572/d25bb24f-c2b5-65b2-a111-e7b4799b3fa3.png)
l\. AVDの作成が完了したら、アプリケーションを実行できるようになります。▶をクリックして、アプリケーションをビルドして実行します。
![image2023-3-17_12-57-24.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3287572/1dc7e291-9689-49f3-693a-3465b553a8b8.png)
m\. [Logcat]からAmplifyの初期化が成功したログが確認できます。
![image2023-3-17_13-0-59.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3287572/e5c84413-a176-7ae8-4ea4-66a8d4b141e5.png)
##### 3\. Amplify Authの追加
AWSはS3へファイルアップロードを行う場合、ユーザ認証が必要です。Amplifyでユーザ認証を行う方法としては、以下の方法があります。今回は、ユーザ名とパスワードを用いる認証方法を用います。
- Amazon Cognito User Poolsを使用した認証：ユーザ名とパスワードを使用して認証を行う。
- Amazon Cognito Identity Poolsを使用した認証：FacebookやGoogleなどのソーシャルプロバイダを使用して認証を行う。
- カスタム認証プロバイダを使用した認証：独自の認証システムを使用して認証を行う。
a\. AmplifyにAuthを追加します。以下のコマンドを実行することで、バックエンドにcognitoやiamロールが作成されます。
```
amplify add auth
```
b\. 上記コマンドを実行すると、認証、セキュリティ構成やサインイン方法について聞かれるので入力します。
```
? Do you want to use the default authentication and security configuration?
Default configuration
?  How do you want users to be able to sign in?
Username
? Do you want to configure advanced settings?
No, I am done.
```
c\. 変更をクラウド側にpushします。この処理には数分かかります。
```
amplify push
```
d\. これで、Amplifyのバックエンドにauthが追加されました。次は、Androidプロジェクト側です。まず、`build.gradle(:app)`のdependenciesに次の依存関係を追加し、[Sync Now]を押します。
```build.gradle
dependencies {
    implementation 'com.amplifyframework:aws-auth-cognito:2.2.2'
}
```
次に`MyAmplifyApp.kt`に`Amplify.addPlugin(AWSCognitoAuthPlugin())`を追加し、以下のように書き換えます。
```MyAmplifyApp.kt
package com.example.amplifydemo
 
import android.app.Application
import android.util.Log
import com.amplifyframework.AmplifyException
import com.amplifyframework.auth.cognito.AWSCognitoAuthPlugin
import com.amplifyframework.core.Amplify
 
class MyAmplifyApp: Application() {
    override fun onCreate() {
        super.onCreate()
 
        try {
            // Add this line, to include the Auth plugin.
            Amplify.addPlugin(AWSCognitoAuthPlugin())
            Amplify.configure(applicationContext)
            Log.i("MyAmplifyApp", "Initialized Amplify")
        } catch (error: AmplifyException) {
            Log.e("MyAmplifyApp", "Could not initialize Amplify", error)
        }
    }
}
```
##### 4\. ログイン画面の作成
Cognitoユーザへのログイン画面を作成します。今回はこちらの記事を参考にしました。

https://qiita.com/bob_yama/items/27123c3d455752800abf

a\. まず、`build.gradle(:app)`に以下を追加し、[Sync Now]を押します。
```build.gradle
android {
    dataBinding {
        enabled = true
    }
}
dependencies {
    implementation "com.amazonaws:aws-android-sdk-mobile-client:2.63.0"
    implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.4'
    implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.4'
}
```
b\. ログイン画面を作るために、`MainActivity.kt`と`activity_main.xml`を以下のように修正します。
```MainActivity.kt
package com.example.amplifydemo
 
import androidx.appcompat.app.AppCompatActivity
import androidx.databinding.DataBindingUtil.setContentView
import android.os.Bundle
import android.util.Log
import androidx.appcompat.app.AlertDialog
import com.amazonaws.mobile.client.AWSMobileClient
import com.amazonaws.mobile.client.Callback
import com.amazonaws.mobile.client.UserStateDetails
import com.amazonaws.mobile.client.results.SignInResult
import com.amazonaws.mobile.client.results.SignInState
import com.example.amplifydemo.databinding.ActivityMainBinding
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.MainScope
import kotlinx.coroutines.*
import java.util.concurrent.CountDownLatch
import java.util.concurrent.TimeUnit
 
class MainActivity : AppCompatActivity(), CoroutineScope by MainScope() {
 
    companion object {
        const val TAG = "MainActivity"
    }
 
    private lateinit var binding: ActivityMainBinding
    private lateinit var mobileClient: AWSMobileClient
 
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding = setContentView(this, R.layout.activity_main)
        mobileClient = AWSMobileClient.getInstance()
 
        val mobileClientLatch = CountDownLatch(1)
        mobileClient.initialize(applicationContext, object : Callback<UserStateDetails> {
            override fun onResult(result: UserStateDetails?) {
                mobileClientLatch.countDown()
            }
 
            override fun onError(e: java.lang.Exception?) {
                Log.e(TAG, "Initialization error.", e)
            }
        })
 
 
        try {
            if (!mobileClientLatch.await(
                    2000L,
                    TimeUnit.MILLISECONDS
                )
            ) throw Exception("Failed to initialize mobile client.")
        } catch (exception: Exception) {
            Log.d(TAG, "${exception.message}")
        }
 
        binding.loginButton.setOnClickListener {
            val username = binding.userIdEditText.text.toString()
            val password = binding.passwordEditText.text.toString()
 
            login(username, password)
        }
 
        binding.createAccountButton.setOnClickListener {
//            val intent = Intent(this, SignUpActivity::class.java)
//            startActivity(intent)
        }
    }
 
    override fun onDestroy() {
        super.onDestroy()
        cancel()
    }
 
    private fun login(username: String, password: String) {
        mobileClient.signIn(username, password, null, object : Callback<SignInResult> {
            override fun onResult(result: SignInResult?) {
                Log.d(TAG, "initialize onResult: ${result?.signInState}")
                when (result?.signInState) {
                    SignInState.DONE -> {
//                        val intent = Intent(this@MainActivity, UploadImageActivity::class.java)
//                        startActivity(intent)
                    }
                    else -> {
                        Log.e(TAG, "initialize onResult: ${result?.signInState}")
                        createDialog("${result?.signInState}")
                    }
                }
            }
 
            override fun onError(e: Exception?) {
                e?.printStackTrace()
                Log.e(TAG, "signIn onError: ${e?.message}")
                launch(Dispatchers.Main) {
                    createDialog("${e?.message}")
                }
            }
        })
    }
 
 
    private fun createDialog(message: String?) {
        AlertDialog.Builder(this)
            .setMessage(message)
            .setNeutralButton("OK", null)
            .show()
    }
 
}
```
```activity_main.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">
 
    <androidx.constraintlayout.widget.ConstraintLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        tools:context=".MainActivity">
        <TextView
            android:id="@+id/textView5"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="userId"
            app:layout_constraintBottom_toTopOf="@+id/textView6"
            app:layout_constraintEnd_toStartOf="@+id/userIdEditText"
            app:layout_constraintHorizontal_bias="0.5"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toTopOf="parent"
            app:layout_constraintVertical_bias="0.3"
            app:layout_constraintVertical_chainStyle="packed" />
 
        <TextView
            android:id="@+id/textView6"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginTop="64dp"
            android:text="password"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintEnd_toStartOf="@+id/passwordEditText"
            app:layout_constraintHorizontal_bias="0.5"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toBottomOf="@+id/textView5"
            app:layout_constraintVertical_chainStyle="packed" />
 
        <EditText
            android:id="@+id/userIdEditText"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:ems="10"
            android:inputType="textPersonName"
            app:layout_constraintBaseline_toBaselineOf="@+id/textView5"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintHorizontal_bias="0.5"
            app:layout_constraintStart_toEndOf="@+id/textView5" />
 
        <EditText
            android:id="@+id/passwordEditText"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:ems="10"
            android:inputType="textPassword"
            app:layout_constraintBaseline_toBaselineOf="@+id/textView6"
            app:layout_constraintStart_toStartOf="@+id/userIdEditText" />
 
        <Button
            android:id="@+id/loginButton"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_marginStart="64dp"
            android:layout_marginTop="64dp"
            android:layout_marginEnd="64dp"
            android:background="@android:color/holo_blue_bright"
            android:text="login"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toBottomOf="@+id/passwordEditText" />
 
        <Button
            android:id="@+id/createAccountButton"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_marginStart="64dp"
            android:layout_marginTop="32dp"
            android:layout_marginEnd="64dp"
            android:background="@android:color/holo_blue_bright"
            android:text="create account"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toBottomOf="@+id/loginButton" />
 
    </androidx.constraintlayout.widget.ConstraintLayout>
</layout>
```
##### 5\. Cognitoユーザ作成画面の作成
Cognitoユーザ作成画面を作成します。今回はこちらの記事を参考にしました。

https://qiita.com/bob_yama/items/27123c3d455752800abf

a\. `com.example.amplifydemo`を右クリックして、[New] - [Kotlin Class/File]をクリックします。Nameは「SignUpActivity」とし、コードを以下に書き換えます。
```SignUpActivity.kt
package com.example.amplifydemo
 
import android.content.Intent
import android.os.Bundle
import android.util.Log
import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatActivity
import androidx.databinding.DataBindingUtil.setContentView
import com.amazonaws.mobile.client.AWSMobileClient
import com.amazonaws.mobile.client.Callback
import com.amazonaws.mobile.client.results.SignUpResult
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.MainScope
import com.example.amplifydemo.databinding.ActivitySignUpBinding
import kotlinx.coroutines.*
 
class SignUpActivity : AppCompatActivity(), CoroutineScope by MainScope() {
 
    companion object {
        const val TAG = "SignUpActivity"
    }
 
    private lateinit var binding: ActivitySignUpBinding
    private lateinit var mobileClient: AWSMobileClient
 
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding = setContentView(this, R.layout.activity_sign_up)
        mobileClient = AWSMobileClient.getInstance()
 
 
        binding.signUpButton.setOnClickListener {
            val username = binding.userIdEditText.text.toString()
            val password = binding.passwordEditText.text.toString()
            val email = binding.emailEditText.text.toString()
 
            Log.d(TAG, "userId: $username, password: $password, email: $email")
 
            signUp(username, password, email)
        }
 
    }
 
    override fun onDestroy() {
        super.onDestroy()
        cancel()
    }
 
    private fun signUp(username: String, password: String, email: String) {
        val userAttributes = mapOf("email" to email)
 
        mobileClient.signUp(
            username,
            password,
            userAttributes,
            null,
            object : Callback<SignUpResult> {
                override fun onResult(result: SignUpResult?) {
                    Log.d(TAG, "signUp onResult: ${result?.confirmationState}")
                    result?.confirmationState?.let { confirmed ->
                        if (confirmed) {
                            val intent = Intent(this@SignUpActivity, MainActivity::class.java)
                            startActivity(intent)
                        }
                    }
 
                    result?.userCodeDeliveryDetails?.attributeName.let { attribute ->
                        when (attribute) {
                            "email" -> {
//                                val intent = Intent(this@SignUpActivity, VerificationActivity::class.java)
//                                    .putExtra("username", username)
//                                startActivity(intent)
                            }
                            else -> {
                                Log.e(TAG, "signUp onResult: $attribute")
                                launch(Dispatchers.Main) {
                                    createDialog("unknown attribute: $attribute")
                                }
                            }
                        }
                    }
                }
 
                override fun onError(e: Exception?) {
                    Log.e(TAG, "signUp onError: ${e?.message}")
                    launch(Dispatchers.Main) {
                        createDialog("${e?.message}")
                    }
                }
            })
    }
 
    private fun createDialog(message: String?) {
        AlertDialog.Builder(this)
            .setMessage(message)
            .setNeutralButton("OK", null)
            .show()
    }
 
}
```
b\. 次に、`layout`を右クリックして[New] - [Layout Resource File]をクリックし、`activity_sign_up.xml`を作成します。コードは以下に書き換えます。
```activity_sign_up.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">
 
    <androidx.constraintlayout.widget.ConstraintLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        tools:context=".SignUpActivity">
 
        <TextView
            android:id="@+id/textView"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="userId"
            app:layout_constraintBottom_toTopOf="@+id/textView4"
            app:layout_constraintEnd_toStartOf="@+id/userIdEditText"
            app:layout_constraintHorizontal_bias="0.5"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toTopOf="parent"
            app:layout_constraintVertical_bias="0.3"
            app:layout_constraintVertical_chainStyle="packed" />
 
        <TextView
            android:id="@+id/textView2"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginTop="64dp"
            android:text="password"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintEnd_toStartOf="@+id/passwordEditText"
            app:layout_constraintHorizontal_bias="0.5"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toBottomOf="@+id/textView4"
            app:layout_constraintVertical_chainStyle="packed" />
 
        <EditText
            android:id="@+id/userIdEditText"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:ems="10"
            android:inputType="textPersonName"
            app:layout_constraintBaseline_toBaselineOf="@+id/textView"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintHorizontal_bias="0.5"
            app:layout_constraintStart_toEndOf="@+id/textView" />
 
        <EditText
            android:id="@+id/passwordEditText"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:ems="10"
            android:inputType="textPassword"
            app:layout_constraintBaseline_toBaselineOf="@+id/textView2"
            app:layout_constraintStart_toEndOf="@+id/textView2"
            app:layout_constraintStart_toStartOf="@+id/userIdEditText" />
 
        <Button
            android:id="@+id/signUpButton"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_marginStart="64dp"
            android:layout_marginTop="64dp"
            android:layout_marginEnd="64dp"
            android:background="@android:color/holo_blue_bright"
            android:text="sigin up"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toBottomOf="@+id/passwordEditText" />
 
        <TextView
            android:id="@+id/textView4"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginTop="64dp"
            android:text="email"
            app:layout_constraintBottom_toTopOf="@+id/textView2"
            app:layout_constraintEnd_toStartOf="@+id/emailEditText"
            app:layout_constraintHorizontal_bias="0.5"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toBottomOf="@+id/textView" />
 
        <EditText
            android:id="@+id/emailEditText"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:ems="10"
            android:inputType="textEmailAddress"
            app:layout_constraintBaseline_toBaselineOf="@+id/textView4"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintHorizontal_bias="0.5"
            app:layout_constraintStart_toEndOf="@+id/textView4" />
    </androidx.constraintlayout.widget.ConstraintLayout>
 
</layout>
```
c\. また、`MainActivity.kt`の以下のコメントアウトを解除します。
```MainActivity.kt

- //            val intent = Intent(this, SignUpActivity::class.java)
- //            startActivity(intent)
 
+            val intent = Intent(this, SignUpActivity::class.java)
+            startActivity(intent)
```
d\. 次に、登録したメールアドレスに送信される6ケタの認証コードを入力する画面を作成します。`com.example.amplifydemo`を右クリックして、[New] - [Kotlin Class/File]をクリックします。Nameは「VerificationActivity」とし、コードを以下に書き換えます。
```VerificationActivity.kt
package com.example.amplifydemo
 
 
import android.content.Intent
import androidx.databinding.DataBindingUtil.setContentView
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.util.Log
import androidx.appcompat.app.AlertDialog
import com.amazonaws.mobile.client.AWSMobileClient
import com.amazonaws.mobile.client.Callback
import com.amazonaws.mobile.client.results.SignUpResult
import com.example.amplifydemo.databinding.ActivityVerificationBinding
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.MainScope
import kotlinx.coroutines.launch
 
class VerificationActivity : AppCompatActivity(), CoroutineScope by MainScope() {
 
    companion object {
        const val TAG = "VerificationActivity"
    }
 
 
    private lateinit var binding: ActivityVerificationBinding
    private lateinit var mobileClient: AWSMobileClient
    var username: String? = null
 
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding = setContentView(this, R.layout.activity_verification)
        mobileClient = AWSMobileClient.getInstance()
 
        username = intent.getStringExtra("username")
 
        binding.submitButton.setOnClickListener {
            val code = binding.confirmationCode.text.toString()
            confirm(code)
        }
 
    }
 
    private fun confirm(code: String) {
        mobileClient.confirmSignUp(username, code, object : Callback<SignUpResult> {
            override fun onResult(result: SignUpResult?) {
                Log.d(TAG, "signUp onResult: ${result?.confirmationState}")
                result?.confirmationState?.let { confirmed ->
                    if (confirmed) {
                        val intent = Intent(this@VerificationActivity, MainActivity::class.java)
                        startActivity(intent)
                    }
                }
            }
 
            override fun onError(e: Exception?) {
                Log.e(TAG, "signUp onError: ${e?.message}")
                launch(Dispatchers.Main) {
                    createDialog("${e?.message}")
                }
            }
        })
 
    }
 
    private fun createDialog(message: String?) {
        AlertDialog.Builder(this)
            .setMessage(message)
            .setNeutralButton("OK", null)
            .show()
    }
 
}
```
e\. 次に、`layout`を右クリックして[New] - [Layout Resource File]をクリックし、`activity_verification.xml`を作成します。コードは以下に書き換えます。
```activity_verification.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">
 
    <androidx.constraintlayout.widget.ConstraintLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        tools:context=".VerificationActivity">
 
        <TextView
            android:id="@+id/textView3"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginTop="128dp"
            android:text="confirmation code"
            app:layout_constraintEnd_toStartOf="@+id/confirmationCode"
            app:layout_constraintHorizontal_bias="0.5"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toTopOf="parent" />
 
        <EditText
            android:id="@+id/confirmationCode"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:ems="10"
            android:inputType="number"
            app:layout_constraintBaseline_toBaselineOf="@+id/textView3"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintHorizontal_bias="0.5"
            app:layout_constraintStart_toEndOf="@+id/textView3" />
 
        <Button
            android:id="@+id/submitButton"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginTop="32dp"
            android:text="submit"
            app:layout_constraintEnd_toEndOf="@+id/confirmationCode"
            app:layout_constraintTop_toBottomOf="@+id/confirmationCode" />
    </androidx.constraintlayout.widget.ConstraintLayout>
</layout>
```
f\. `SignUpActivigy.kt`の以下のコメントアウトを解除します。
```SignUpActivigy.kt
- //                                val intent = Intent(this@SignUpActivity, VerificationActivity::class.java)
- //                                    .putExtra("username", username)
- //                                startActivity(intent)
 
+                                 val intent = Intent(this@SignUpActivity, VerificationActivity::class.java)
+                                     .putExtra("username", username)
+                                 startActivity(intent)
```
g\. 最後に、`AndroidManifest.xml`に以下のパーミッションと遷移先の画面定義を追加します。
```AndroidManifest.xml
<application>
    <activity
        android:name=".SignUpActivity">
    </activity>
    <activity
        android:name=".VerificationActivity">
    </activity>
</application>
```
h\. アプリをビルド＆実行してユーザを作成すると、Amazon Cognitoのコンソール画面にユーザが追加されていることが確認できます。
![image2023-3-22_15-40-42.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3287572/8e2e23a0-9666-f53d-66b0-e74dc4f715c5.png)
##### 6\. Amplify Storageの追加
a\. 以下のコマンドを実行することで、S3を作成します。
```
amplify add storage
```
b\. 上記コマンドを実行すると、S3の名前や権限について聞かれるので入力します。
```
? Please select from one of the below mentioned services:
    `Content (Images, audio, video, etc.)`
? Please provide a friendly name for your resource that will be used to label this category in the project:
    `S3friendlyName`
? Please provide bucket name:
    `storagebucketname`
? Who should have access:
    `Auth users only`
? What kind of access do you want for Authenticated users?
    `create/update, read, delete`
? Do you want to add a Lambda Trigger for your S3 Bucket?
    `No`
```
c\. 変更をクラウド側にpushします。この処理には数分かかります。
```
amplify push
```
d\. これで、Amplifyのバックエンドにstorageが追加されました。次に、Androidプロジェクトの`build.gradle(:app)`のdependenciesに次の依存関係を追加し、[Sync Now]を押します。
```build.gradle
dependencies {
    implementation 'com.amplifyframework:aws-storage-s3:2.2.2'
}
```
e\. 次に`MyAmplifyApp.kt`に`Amplify.addPlugin(AWSS3StoragePlugin())`を追加し、以下のように書き換えます。
```MyAmplifyApp.kt
package com.example.amplifydemo
 
import android.app.Application
import android.util.Log
import com.amplifyframework.AmplifyException
import com.amplifyframework.auth.cognito.AWSCognitoAuthPlugin
import com.amplifyframework.core.Amplify
import com.amplifyframework.storage.s3.AWSS3StoragePlugin
 
class MyAmplifyApp: Application() {
    override fun onCreate() {
        super.onCreate()
 
        try {
            Amplify.addPlugin(AWSCognitoAuthPlugin())
            Amplify.addPlugin(AWSS3StoragePlugin())
            Amplify.configure(applicationContext)
            Log.i("MyAmplifyApp", "Initialized Amplify")
        } catch (error: AmplifyException) {
            Log.e("MyAmplifyApp", "Could not initialize Amplify", error)
        }
    }
}
```
##### 7\. アップロード画面の作成
a\. `com.example.amplifydemo`を右クリックして、[New] - [Kotlin Class/File]をクリックします。Nameは「UploadVideoActivity」とし、コードを以下に書き換えます。
```UploadVideoActivity.kt
package com.example.amplifydemo
 
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.util.Log
import androidx.databinding.DataBindingUtil.setContentView
import com.amazonaws.mobile.client.AWSMobileClient
import com.amplifyframework.core.Amplify
import com.example.amplifydemo.databinding.ActivityUploadVideoBinding
import kotlinx.coroutines.*
import java.io.File
 
class UploadVideoActivity : AppCompatActivity(), CoroutineScope by MainScope() {
 
    companion object {
        const val TAG = "UploadVideoActivity"
    }
 
    private lateinit var binding: ActivityUploadVideoBinding
    private lateinit var mobileClient: AWSMobileClient
 
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding = setContentView(this, R.layout.activity_upload_video)
        mobileClient = AWSMobileClient.getInstance()
 
        binding.uploadButton.setOnClickListener {
            upload()
        }
    }
 
    private fun upload() {
        val file = File(applicationContext.filesDir, "sample.mp4")
        Amplify.Storage.uploadFile(
            "sample.mp4",
            file,
            { Log.i(TAG, "Successfully uploaded: ${it.key}") },
            { Log.e(TAG, "Upload failed", it) }
        )
    }
 
}
```
b\. このクラスは、`applicationContext.filesDir`に格納された`sample.mp4`をS3にアップロードするクラスです。そのため、`applicationContext.filesDir`に`sample.mp4`を格納する必要があります。
`applicationContext.filesDir`を出力させると、`/data/user/0/com.example.amplifydemo/files`でした。したがって、ここに動画ファイルを格納すれば参照することができます。仮想デバイス内のファイルは、「Device File Explorer」から確認することができ、ドラッグ＆ドロップでファイルの格納を行うことができます。
`/data/user/0/com.example.amplifydemo/files`に`sample.mp4`を格納すると、以下のように表示されます。

:::note 
applicationContext.filesDirとは、Androidアプリケーションの内部ストレージにあるファイルのディレクトリへのパスを表します。内部ストレージは、アプリケーションが使用するプライベートな領域であり、他のアプリケーションやユーザからアクセスできません。このディレクトリには、アプリケーションが生成したファイルやデータを保存することができます。例えば、アプリケーションが使用する設定ファイルや、ユーザが作成したファイルなどが保存されます。このディレクトリに保存されたファイルは、アプリケーションがアンインストールされると同時に削除されます。また、このディレクトリに保存されたファイルは、外部ストレージに保存されたファイルよりも高速にアクセスできるため、アプリケーションのパフォーマンスを向上させることができます。
:::
![image2023-3-22_16-4-48.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3287572/b3167562-fec6-eaa3-fe16-361295591274.png)
c\. 次に、`layout`を右クリックして[New] - [Layout Resource File]をクリックし、`activity_upload_video.xml`を作成します。コードは以下に書き換えます。
```activity_upload_video.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">
 
    <androidx.constraintlayout.widget.ConstraintLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        tools:context=".UploadVideoActivity">
 
        <Button
            android:id="@+id/uploadButton"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginBottom="340dp"
            android:text="upload to s3"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintHorizontal_bias="0.5"
            app:layout_constraintStart_toStartOf="parent" />
 
    </androidx.constraintlayout.widget.ConstraintLayout>
</layout>
```
d\. また、`MainActivity.kt`の以下のコメントアウトを解除します。
```MainActivity.kt

- //                        val intent = Intent(this@MainActivity, UploadVideoActivity::class.java)
- //                        startActivity(intent)
 
+                         val intent = Intent(this@MainActivity, UploadVideoActivity::class.java)
+                         startActivity(intent)
```
e\. 最後に、`AndroidManifest.xml`に以下のパーミッションと遷移先の遷移先の画面定義を追加します。
```AndroidManifest.xml
<application>
    <activity
        android:name=".UploadVideoActivity">
    </activity>
</application>
```
f\. アプリをビルド＆実行し、Cognitoユーザにログインして「upload to s3」ボタンを押すと、S3のpublicに`sample.mp4`がアップロードされます。
### 実行結果
##### エミュレータの画面遷移
<img width=40% alt="result_app.gif" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3287572/555068a7-d600-87a8-05e7-7a7e9045adb1.gif">

##### Android Studio上でアップロードが成功したことを確認するLog
![logcat.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3287572/e9425dc3-782f-874f-9c5c-a188cf64e617.png)
##### S3の画面
![image2023-3-22_16-15-58.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3287572/de202b2e-7113-b750-028a-8a143d8b8982.png)
# まとめ
本記事では、Cognitoユーザの作成、ログイン、S3へのファイルアップロードを行うAndroidアプリの開発手順を紹介しました。Androidアプリ開発にAWS Amplifyを用いることで、セキュアでスケーラブルなアプリケーションを簡単に構築することができるということがわかりました。
amplify addでバックエンドをどんどん追加できるのはとても便利だと思いました。
一方、AWSの知識が必要な点やドキュメントの少なさなどから学習コストは高い印象を持ちました。
今回は最低限の機能の実装だけを目指したので、画面レイアウトやソースコードはまだまだ改善の余地があると思います。
今後の展望として、Amplifyのfunction（AWS lambda）を使って今回アップロードした動画ファイルから骨格情報を取得する関数を作成し、取得した骨格情報をエミュレータで表示する機能の追加を試していきたいです。
# 参考

https://developer.android.com/studio/intro?hl=ja

https://docs.amplify.aws/lib/q/platform/android/

https://qiita.com/bob_yama/items/27123c3d455752800abf