diff --git a/app/src/main/java/com/ivy/wallet/ui/RootActivity.kt b/app/src/main/java/com/ivy/wallet/ui/RootActivity.kt index c0aec5c32..c83235b1b 100644 --- a/app/src/main/java/com/ivy/wallet/ui/RootActivity.kt +++ b/app/src/main/java/com/ivy/wallet/ui/RootActivity.kt @@ -72,6 +72,12 @@ class RootActivity : AppCompatActivity(), RootScreen { @Inject lateinit var timeProvider: TimeProvider + /** + * Uncomment below code to use gDrive feature + */ +// @Inject +// lateinit var googleDriveService: GoogleDriveService + private lateinit var googleSignInLauncher: ActivityResultLauncher private lateinit var onGoogleSignInIdTokenResult: (idToken: String?) -> Unit @@ -139,6 +145,12 @@ class RootActivity : AppCompatActivity(), RootScreen { } private fun setupActivityForResultLaunchers() { + + /** + * Uncomment below code to use gDrive feature + */ +// requestSignIn() + googleSignInLauncher() createFileLauncher() @@ -179,6 +191,23 @@ class RootActivity : AppCompatActivity(), RootScreen { // } } + /** + * Uncomment below code to use gDrive feature + */ +// private fun requestSignIn() { +// val signInOptions = googleDriveService.requestSignIn() +// val client = GoogleSignIn.getClient(this, signInOptions) +// +// Timber.d("Sign In Requested") +// // The result of the sign-in Intent is handled in onActivityResult. +// val launcher = registerForActivityResult( +// ActivityResultContracts.StartActivityForResult() +// ) { +// googleDriveService.handleSignInResult(this@RootActivity) +// } +// launcher.launch(client.signInIntent) +// } + private fun createFileLauncher() { createFileLauncher = activityForResultLauncher( createIntent = { _, fileName -> diff --git a/drive/google-drive/.gitignore b/drive/google-drive/.gitignore new file mode 100644 index 000000000..42afabfd2 --- /dev/null +++ b/drive/google-drive/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/drive/google-drive/build.gradle.kts b/drive/google-drive/build.gradle.kts new file mode 100644 index 000000000..1d60abdff --- /dev/null +++ b/drive/google-drive/build.gradle.kts @@ -0,0 +1,78 @@ +import com.ivy.buildsrc.Google +import com.ivy.buildsrc.Timber +import com.ivy.buildsrc.Hilt + +apply() + +plugins { + id("com.android.library") + id("kotlin-android") + id("kotlin-kapt") + id("org.jetbrains.kotlin.android") + id("dagger.hilt.android.plugin") +} + +android { + namespace = "com.ivy.drive.google_drive" + compileSdk = 32 + + defaultConfig { + minSdk = 24 + targetSdk = 32 + + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" +// consumerProguardFiles("consumer-rules.pro") + } + + buildTypes { + release { + isMinifyEnabled = false + proguardFiles( + getDefaultProguardFile("proguard-android-optimize.txt"), + "proguard-rules.pro" + ) + } + } + compileOptions { + sourceCompatibility = JavaVersion.VERSION_1_8 + targetCompatibility = JavaVersion.VERSION_1_8 + } + kotlinOptions { + jvmTarget = "1.8" + } + + + packagingOptions { + + resources.excludes.add("META-INF/DEPENDENCIES") + resources.excludes.add("listenablefuture") + } +} + +dependencies { + +// implementation("androidx.core:core-ktx:1.7.0") + implementation("androidx.appcompat:appcompat:1.5.1") + implementation("com.google.android.material:material:1.6.1") + testImplementation("junit:junit:4.13.2") + androidTestImplementation("androidx.test.ext:junit:1.1.3") + androidTestImplementation("androidx.test.espresso:espresso-core:3.4.0") + + + + implementation("com.google.apis:google-api-services-drive:v3-rev136-1.25.0") { + exclude(group = "com.google.guava", module = "listenablefuture") + } + implementation("com.google.http-client:google-http-client-gson:1.26.0") + implementation("com.google.api-client:google-api-client-android:1.26.0") { + exclude(group = "com.google.guava", module = "listenablefuture") + } + api("com.google.guava:guava:28.1-android") + + Hilt() + + Google() + + Timber(api = true) + +} \ No newline at end of file diff --git a/drive/google-drive/consumer-rules.pro b/drive/google-drive/consumer-rules.pro new file mode 100644 index 000000000..e69de29bb diff --git a/drive/google-drive/proguard-rules.pro b/drive/google-drive/proguard-rules.pro new file mode 100644 index 000000000..481bb4348 --- /dev/null +++ b/drive/google-drive/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/drive/google-drive/src/androidTest/java/com/ivy/drive/google_drive/ExampleInstrumentedTest.kt b/drive/google-drive/src/androidTest/java/com/ivy/drive/google_drive/ExampleInstrumentedTest.kt new file mode 100644 index 000000000..8a6f786f3 --- /dev/null +++ b/drive/google-drive/src/androidTest/java/com/ivy/drive/google_drive/ExampleInstrumentedTest.kt @@ -0,0 +1,24 @@ +package com.ivy.drive.google_drive + +import androidx.test.platform.app.InstrumentationRegistry +import androidx.test.ext.junit.runners.AndroidJUnit4 + +import org.junit.Test +import org.junit.runner.RunWith + +import org.junit.Assert.* + +/** + * Instrumented test, which will execute on an Android device. + * + * See [testing documentation](http://d.android.com/tools/testing). + */ +@RunWith(AndroidJUnit4::class) +class ExampleInstrumentedTest { + @Test + fun useAppContext() { + // Context of the app under test. + val appContext = InstrumentationRegistry.getInstrumentation().targetContext + assertEquals("com.ivy.drive.google_drive.test", appContext.packageName) + } +} \ No newline at end of file diff --git a/drive/google-drive/src/main/AndroidManifest.xml b/drive/google-drive/src/main/AndroidManifest.xml new file mode 100644 index 000000000..a5918e68a --- /dev/null +++ b/drive/google-drive/src/main/AndroidManifest.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/drive/google-drive/src/main/java/com/ivy/drive/google_drive/data/GoogleDriveFileType.kt b/drive/google-drive/src/main/java/com/ivy/drive/google_drive/data/GoogleDriveFileType.kt new file mode 100644 index 000000000..80d0ef9b7 --- /dev/null +++ b/drive/google-drive/src/main/java/com/ivy/drive/google_drive/data/GoogleDriveFileType.kt @@ -0,0 +1,20 @@ +package com.ivy.drive.google_drive.data + +sealed class GoogleDriveFileType { + class Backup(val type: GoogleDriveType = GoogleDriveType.BackupTypeCSV): GoogleDriveFileType(){ + companion object { + const val FOLDER_NAME = "Backup" + } + } + class Image(val type: GoogleDriveType = GoogleDriveType.ImageTypeJPEG): GoogleDriveFileType(){ + companion object { + const val FOLDER_NAME = "Image" + } + } + class Video(val type: GoogleDriveType = GoogleDriveType.VideoType): GoogleDriveFileType(){ + companion object { + const val FOLDER_NAME = "Video" + } + } + +} \ No newline at end of file diff --git a/drive/google-drive/src/main/java/com/ivy/drive/google_drive/data/GoogleDriveServiceHelper.kt b/drive/google-drive/src/main/java/com/ivy/drive/google_drive/data/GoogleDriveServiceHelper.kt new file mode 100644 index 000000000..94b3bb7ae --- /dev/null +++ b/drive/google-drive/src/main/java/com/ivy/drive/google_drive/data/GoogleDriveServiceHelper.kt @@ -0,0 +1,167 @@ +package com.ivy.drive.google_drive.data + +import com.google.api.client.http.FileContent +import com.google.api.services.drive.Drive +import com.google.api.services.drive.model.File +import com.google.api.services.drive.model.FileList +import com.ivy.drive.google_drive.data.GoogleDriveFileType.Image +import com.ivy.drive.google_drive.data.GoogleDriveFileType.Backup +import com.ivy.drive.google_drive.data.GoogleDriveFileType.Video +import com.ivy.drive.google_drive.util.GoogleDriveUtil +import com.ivy.drive.google_drive.util.GoogleDriveUtil.IVY_WALLET_ROOT_FOLDER +import com.ivy.drive.google_drive.util.GoogleDriveUtil.createGoogleDriveFile +import com.ivy.drive.google_drive.util.GoogleDriveUtil.getParents +import com.ivy.drive.google_drive.util.GoogleDriveUtil.ivyRootFolderParent +import com.ivy.drive.google_drive.util.GoogleDriveUtil.updateGoogleDriveFile +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Deferred +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.async +import timber.log.Timber +import java.io.ByteArrayOutputStream +import java.io.IOException +import java.io.OutputStream + +internal class GoogleDriveServiceHelper(private val drive: Drive) { + + fun uploadFileAsync( + file: File, + fileContent: FileContent? = null, + driveFiletype: GoogleDriveFileType + ): Deferred { + + return CoroutineScope(Dispatchers.IO).async { + val currentFile = getFileByName(file.name) + + if(currentFile != null) { + Timber.d("Current File not null") + val parentFolder = checkIfFolderExist(driveFiletype.toString()) + if (fileContent != null) { + updateFile( + fileId = currentFile.id, + driveType = driveFiletype, + fileName = currentFile.name, + parent = parentFolder, + fileContent = fileContent + ) + } else { + "Couldn't upload the file as contents were empty" + } + } else { + Timber.d("Current File null creating new file") + + createFile( + fileContent = fileContent, + fileName = file.name, + driveType = driveFiletype + ) + } + } + } + + fun downloadFile(fileName: String): OutputStream? { + val file = getFileByName(fileName) + val outputStream = ByteArrayOutputStream() + if(file != null) { + drive.files().get(file.id).executeAndDownloadTo(outputStream) + return outputStream + } + return null + } + + private fun updateFile( + fileId: String, + fileContent: FileContent, + fileName: String, + parent: String?, + driveType: GoogleDriveFileType + ): String { + val type = driveType.toString() + val fileMetadata = updateGoogleDriveFile(fileName,type) + val googleFile = drive.files().update(fileId,fileMetadata,fileContent).setAddParents(parent).execute() + ?: throw IOException("Error while updating and uploading google drive file") + return googleFile.id + } + + private fun createFile( + fileContent: FileContent?, + fileName: String, + driveType: GoogleDriveFileType + ): String { + val type = when(driveType) { + is Backup -> driveType.type + is Image -> driveType.type + is Video -> driveType.type + } + + val typeValue = when(type) { + is GoogleDriveType.BackupTypeCSV -> type.VALUE + is GoogleDriveType.ImageTypeJPEG -> type.VALUE + is GoogleDriveType.ImageTypePNG -> type.VALUE + is GoogleDriveType.ImageTypeWEBP -> type.VALUE + is GoogleDriveType.BackupTypePlainText -> type.VALUE + else -> { + GoogleDriveType.BackupTypePlainText.VALUE + } + } + val parentId = checkIfFolderExist(driveType.toString()) + val parents = if(parentId != null) { + getParents(parentId) + } else { + val rootId = checkIfFolderExist(IVY_WALLET_ROOT_FOLDER) + val rootParents = + if(rootId != null) { + getParents(rootId) + } else { + val id = createFolder() + getParents(id) + } + val id = createFolder(rootParents,driveType.toString()) + getParents(id) + } + val fileMetadata = createGoogleDriveFile(fileName,parents,typeValue) + + val googleFile = + if(fileContent == null) { + drive.files().create(fileMetadata).execute() + ?: throw IOException("Error while creating and uploading google drive file") + } else { + drive.files().create(fileMetadata, fileContent).execute() + ?: throw IOException("Error while creating and uploading google drive file") + } + return googleFile.id + } + + private fun getFileByName(fileName: String): File? { + val fileList = getAllFiles() + if(fileList.isEmpty().not()) { + return fileList.files.firstOrNull { it.name == fileName } + } + return null + } + + private fun getAllFiles(): FileList { + return drive.files().list().setSpaces(GoogleDriveUtil.DRIVE_SPACE).execute() + ?: throw Exception ("Error in querying the files from the drive") + } + + private fun checkIfFolderExist(fileName: String): String? { + val files = getAllFiles() + val file = files.files.firstOrNull { it.name == fileName } + return file?.id + } + + private fun createFolder( + parents: List = ivyRootFolderParent, + folderName: String = IVY_WALLET_ROOT_FOLDER + ): String { + val metadata = createGoogleDriveFile( + folderName, + parents, + GoogleDriveUtil.MIME_TYPE_FOLDER + ) + val googleFile = drive.files().create(metadata).execute() + ?: throw IOException("Error when requesting file creation.") + return googleFile.id + } +} \ No newline at end of file diff --git a/drive/google-drive/src/main/java/com/ivy/drive/google_drive/data/GoogleDriveServiceImpl.kt b/drive/google-drive/src/main/java/com/ivy/drive/google_drive/data/GoogleDriveServiceImpl.kt new file mode 100644 index 000000000..a9981a0b0 --- /dev/null +++ b/drive/google-drive/src/main/java/com/ivy/drive/google_drive/data/GoogleDriveServiceImpl.kt @@ -0,0 +1,119 @@ +package com.ivy.drive.google_drive.data + + +import android.content.Context +import com.google.android.gms.auth.api.signin.GoogleSignIn +import com.google.android.gms.auth.api.signin.GoogleSignInOptions +import com.google.android.gms.common.api.Scope +import com.google.api.client.extensions.android.http.AndroidHttp +import com.google.api.client.googleapis.extensions.android.gms.auth.GoogleAccountCredential +import com.google.api.client.http.FileContent +import com.google.api.client.json.jackson2.JacksonFactory +import com.google.api.services.drive.Drive +import com.google.api.services.drive.DriveScopes +import com.ivy.drive.google_drive.domain.GoogleDriveService +import com.google.api.services.drive.model.File +import com.ivy.drive.google_drive.util.GoogleDriveUtil.APP_NAME +import timber.log.Timber +import java.io.OutputStream + +internal class GoogleDriveServiceImpl : GoogleDriveService { + + private var driveHelper: GoogleDriveServiceHelper? = null + + override suspend fun upload( + file: File, + fileContent: FileContent, + driveFiletype: GoogleDriveFileType + ): String { + return try{ + driveHelper!!.uploadFileAsync( + file = file, + fileContent = fileContent, + driveFiletype = driveFiletype + ).await() + } catch (e: Exception) { + "Error while uploading a file : ${e.printStackTrace()}" + } + } + + override suspend fun download(fileName: String): OutputStream? { + return driveHelper!!.downloadFile(fileName) + } + + override fun handleSignInResult(context: Context) { + initDriveHelper(context) + } + + override fun requestSignIn(): GoogleSignInOptions { + return handleRequestSignIn() + } + + + private fun handleRequestSignIn(): GoogleSignInOptions { + Timber.d("Requesting sign-in") + return GoogleSignInOptions.Builder(GoogleSignInOptions.DEFAULT_SIGN_IN) + .requestIdToken("364763737033-t1d2qe7s0s8597k7anu3sb2nq79ot5tp.apps.googleusercontent.com") + .requestEmail() + .requestProfile() + .requestScopes(Scope(DriveScopes.DRIVE_FILE)) + .build() + } + + private fun initDriveHelper(context: Context) { + GoogleSignIn.getLastSignedInAccount(context).let { googleSignInAccount -> + + // Use the authenticated account to sign in to the Drive service. + val credential = GoogleAccountCredential.usingOAuth2( + context, listOf(DriveScopes.DRIVE_FILE) + ) + Timber.d("googleSignInAccount value $googleSignInAccount") + if (googleSignInAccount != null) { + credential.selectedAccount = googleSignInAccount.account + } + val googleDriveService = Drive.Builder( + AndroidHttp.newCompatibleTransport(), + JacksonFactory.getDefaultInstance(), + credential + ) + .setApplicationName(APP_NAME) + .build() + + // The DriveServiceHelper encapsulates all REST API and SAF functionality. + // Its instantiation is required before handling any onClick actions. + driveHelper = GoogleDriveServiceHelper(googleDriveService) + + } + } + + +// private fun mockFilesAndUpload() { +// CoroutineScope(Dispatchers.IO).launch { +// val file = createGoogleDriveFile( +// "IvyImageFile", +// getParents(GoogleDriveFileType.Image.FOLDER_NAME), +// GoogleDriveType.BackupTypePlainText.VALUE +// ) +// +// println(GoogleDriveFileType.Backup().toString()) +// val content = "Hey GDrive file Upload was a success Congrats!!" +// +// val textFile = java.io.File(content) +// val fileContents = FileContent("text/plain",textFile) +// +// Timber.d("Uploading the file") +// +// val result = upload( +// file = file, +// fileContent = fileContents, +// driveFiletype = GoogleDriveFileType.Image( +// GoogleDriveType.BackupTypePlainText +// ) +// ) +// +// Timber.d("File uploaded with id : $result") +// +// } +// } + +} \ No newline at end of file diff --git a/drive/google-drive/src/main/java/com/ivy/drive/google_drive/data/GoogleDriveType.kt b/drive/google-drive/src/main/java/com/ivy/drive/google_drive/data/GoogleDriveType.kt new file mode 100644 index 000000000..151da2287 --- /dev/null +++ b/drive/google-drive/src/main/java/com/ivy/drive/google_drive/data/GoogleDriveType.kt @@ -0,0 +1,22 @@ +package com.ivy.drive.google_drive.data + +sealed interface GoogleDriveType { + object BackupTypeCSV: GoogleDriveType { + const val VALUE = "text/csv" + } + object BackupTypePlainText: GoogleDriveType { + const val VALUE = "text/plain" + } + object ImageTypePNG: GoogleDriveType { + const val VALUE = "image/png" + } + object ImageTypeJPEG: GoogleDriveType { + const val VALUE = "image/jpeg" + } + object ImageTypeWEBP: GoogleDriveType { + const val VALUE = "image/webp" + } + object VideoType: GoogleDriveType { + const val VALUE = "video/*" + } +} \ No newline at end of file diff --git a/drive/google-drive/src/main/java/com/ivy/drive/google_drive/di/GoogleDriveModuleDI.kt b/drive/google-drive/src/main/java/com/ivy/drive/google_drive/di/GoogleDriveModuleDI.kt new file mode 100644 index 000000000..717fa3d9f --- /dev/null +++ b/drive/google-drive/src/main/java/com/ivy/drive/google_drive/di/GoogleDriveModuleDI.kt @@ -0,0 +1,22 @@ +package com.ivy.drive.google_drive.di + +import com.google.api.services.drive.Drive +import com.ivy.drive.google_drive.data.GoogleDriveServiceHelper +import com.ivy.drive.google_drive.data.GoogleDriveServiceImpl +import com.ivy.drive.google_drive.domain.GoogleDriveService +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +object GoogleDriveModuleDI { + + @Singleton + @Provides + fun provideGoogleDriveService(): GoogleDriveService = + GoogleDriveServiceImpl() + +} \ No newline at end of file diff --git a/drive/google-drive/src/main/java/com/ivy/drive/google_drive/domain/GoogleDriveService.kt b/drive/google-drive/src/main/java/com/ivy/drive/google_drive/domain/GoogleDriveService.kt new file mode 100644 index 000000000..7fdc7c0e2 --- /dev/null +++ b/drive/google-drive/src/main/java/com/ivy/drive/google_drive/domain/GoogleDriveService.kt @@ -0,0 +1,36 @@ +package com.ivy.drive.google_drive.domain + +import android.content.Context +import android.content.Intent +import com.google.android.gms.auth.api.signin.GoogleSignInOptions +import com.google.api.client.http.FileContent +import com.google.api.services.drive.model.File +import com.ivy.drive.google_drive.data.GoogleDriveFileType +import dagger.hilt.android.qualifiers.ApplicationContext +import java.io.OutputStream + +interface GoogleDriveService { + + companion object { + const val IVY_DEFAULT_BACKUP_FILE_NAME: String = "IvyBackupFile" + const val IVY_DEFAULT_IMAGE_FILE_NAME: String = "IvyImageFile" + } + + suspend fun upload( + file: File, + fileContent: FileContent, + driveFiletype: GoogleDriveFileType + ): String + + // TODO: Refactor how to download a file + suspend fun download( + fileName: String + ): OutputStream? + + fun handleSignInResult( + context: Context + ) + + fun requestSignIn(): + GoogleSignInOptions +} \ No newline at end of file diff --git a/drive/google-drive/src/main/java/com/ivy/drive/google_drive/util/GoogleDriveUtil.kt b/drive/google-drive/src/main/java/com/ivy/drive/google_drive/util/GoogleDriveUtil.kt new file mode 100644 index 000000000..72656ac12 --- /dev/null +++ b/drive/google-drive/src/main/java/com/ivy/drive/google_drive/util/GoogleDriveUtil.kt @@ -0,0 +1,38 @@ +package com.ivy.drive.google_drive.util + +import com.google.api.services.drive.model.File + +object GoogleDriveUtil { + const val DRIVE_SPACE = "drive" + const val IVY_WALLET_ROOT_FOLDER = "IvyWallet" + const val IVY_WALLET_BACKUP_FOLDER = "Backups" + const val IVY_WALLET_BACKUP_FILENAME = "IvyWalletBackup" + const val APP_NAME = "IvyAndroidApp" + const val MIME_TYPE_FOLDER = "application/vnd.google-apps.folder" + val ivyRootFolderParent = listOf("root") + + // TODO: Make this private + fun createGoogleDriveFile( + fileName: String, + parents: List, + type: String + ): File { + return File() + .setName(fileName) + .setParents(parents) + .setMimeType(type) + } + + fun updateGoogleDriveFile( + fileName: String, + type: String + ): File { + return File() + .setName(fileName) + .setMimeType(type) + } + + fun getParents(fileId: String): List { + return listOf(fileId) + } +} \ No newline at end of file diff --git a/drive/google-drive/src/test/java/com/ivy/drive/google_drive/ExampleUnitTest.kt b/drive/google-drive/src/test/java/com/ivy/drive/google_drive/ExampleUnitTest.kt new file mode 100644 index 000000000..5af57a462 --- /dev/null +++ b/drive/google-drive/src/test/java/com/ivy/drive/google_drive/ExampleUnitTest.kt @@ -0,0 +1,17 @@ +package com.ivy.drive.google_drive + +import org.junit.Test + +import org.junit.Assert.* + +/** + * Example local unit test, which will execute on the development machine (host). + * + * See [testing documentation](http://d.android.com/tools/testing). + */ +class ExampleUnitTest { + @Test + fun addition_isCorrect() { + assertEquals(4, 2 + 2) + } +} \ No newline at end of file