diff --git a/app/build.gradle b/app/build.gradle index c1451957d..dff650596 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -63,6 +63,8 @@ android { premium { buildConfigField "boolean", "IS_DROPBOX_ENABLED", "true" + buildConfigField "boolean", "IS_GOOGLE_DRIVE_ENABLED", "true" + buildConfigField "String", "VERSION_NAME_SUFFIX", '""' dimension "store" @@ -70,11 +72,13 @@ android { fdroid { /* - * Disable Dropbox. + * Disable Dropbox and Google Drive. * Properties file which contains the required API key is not included with the code. */ buildConfigField "boolean", "IS_DROPBOX_ENABLED", "false" + buildConfigField "boolean", "IS_GOOGLE_DRIVE_ENABLED", "false" + buildConfigField "String", "VERSION_NAME_SUFFIX", '" (fdroid)"' dimension "store" @@ -165,6 +169,22 @@ dependencies { implementation "com.dropbox.core:dropbox-core-sdk:$versions.dropbox_core_sdk" + // Google Drive + // implementation 'com.google.api-client:google-api-client:1.23.0' + // implementation 'com.google.oauth-client:google-oauth-client-jetty:1.23.0' + // implementation 'com.google.apis:google-api-services-drive:v3-rev110-1.23.0' + + // implementation 'com.google.android.gms:play-services:17.0.0' + implementation 'com.google.android.gms:play-services-drive:17.0.0' + implementation 'com.google.android.gms:play-services-auth:19.0.0' + 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: 'org.apache.httpcomponents' + } + implementation('com.google.apis:google-api-services-drive:v3-rev136-1.25.0') { + exclude group: 'org.apache.httpcomponents' + } + implementation "com.googlecode.juniversalchardet:juniversalchardet:$versions.juniversalchardet" implementation "com.evernote:android-job:$versions.evernote_android_job" diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 52f7fa18b..a96cf052b 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -95,6 +95,11 @@ android:windowSoftInputMode="stateAlwaysHidden"> + + + pathIds; + { + pathIds = new HashMap<>(); + pathIds.put("My Drive", "root"); + pathIds.put("", "root"); + } + + /** + * Requires passing in Activity because it is used to create the authClient. + */ + public GoogleDriveClient(Context context, long id) { + mContext = context; + + repoId = id; + + createClient(); + } + + private void createClient() { + // If the user is already signed in, the GoogleSignInAccount will be non-null. + final GoogleSignInAccount account = GoogleSignIn.getLastSignedInAccount(mContext); + + if (account != null) { + googleDriveClient = getNewGoogleDriveClient(account); + } + } + + private GoogleSignInClient createAuthClient(Activity activity) { + var signInOptions = new GoogleSignInOptions.Builder( + GoogleSignInOptions.DEFAULT_SIGN_IN) + .requestEmail() + .requestScopes(new Scope(DriveScopes.DRIVE_FILE)) + .build(); + return GoogleSignIn.getClient(activity, signInOptions); + } + + public boolean isLinked() { + return googleDriveClient != null; + } + + private void linkedOrThrow() throws IOException { + if (! isLinked()) { + throw new IOException(NOT_LINKED); + } + } + + /** + * Requires activity in order to create authClient if needed. + * Return type differs from Dropbox because authClient offers async. + */ + public Task unlink(Activity activity) { + if (authClient == null) { + authClient = createAuthClient(activity); + } + googleDriveClient = null; + final Task task = authClient.revokeAccess(); + authClient = null; + return task; + } + + /** + * Unlike Dropbox, returns an Intent. + */ + public Intent beginAuthentication(Activity activity) { + tryLinking = true; + var signInOptions = new GoogleSignInOptions.Builder(GoogleSignInOptions.DEFAULT_SIGN_IN) + .requestEmail() + .requestScopes(new Scope(DriveScopes.DRIVE_FILE)) + .build(); + authClient = GoogleSignIn.getClient(activity, signInOptions); + return authClient.getSignInIntent(); + } + + /** + * The logic is slightly different from Dropbox. Rather than FinishAuthentication being called + * on activity resume, it is called when the sign-in activity returns, + * when we actually have an account. + */ + public boolean finishAuthentication(GoogleSignInAccount account) { + if (googleDriveClient == null && tryLinking) { + googleDriveClient = getNewGoogleDriveClient(account); + return true; + } + + return false; + } + + public Drive getNewGoogleDriveClient(GoogleSignInAccount account) { + assert account != null; + GoogleAccountCredential credential = GoogleAccountCredential + .usingOAuth2(mContext, Collections.singleton(DriveScopes.DRIVE_FILE)); + credential.setSelectedAccount(account.getAccount()); + return new Drive.Builder(AndroidHttp.newCompatibleTransport(), + new GsonFactory(), + credential) + .setApplicationName("Orgzly") + .build(); + } + + // no need for setToken, saveToken, getToken, deleteToken + + /** + * This is unique to Google Drive. + * @param path A path in x/y/z form + * @return A Google Drive file ID. (Note that in Google Drive, a folder is a type of file.) + * @throws IOException On error + */ + private String findId(String path) throws IOException { + // TODO testing + FileList result1 = googleDriveClient.files().list() + .setSpaces("drive") + .setFields("files(id, name, mimeType)") + .execute(); + List files1 = result1.getFiles(); + Log.d(TAG, files1.toString()); + + + if (pathIds.containsKey(path)) { + return pathIds.get(path); + } + + String[] parts = path.split("/"); + String[] ids = new String[parts.length+1]; + + ids[0] = "root"; + + for (int i=0; i < parts.length; ++i) { + FileList result = googleDriveClient.files().list() + .setQ(String.format("name = '%s' and '%s' in parents", parts[i], ids[i])) + .setSpaces("drive") + .setFields("files(id, name, mimeType)") + .execute(); + List files = result.getFiles(); + if (!files.isEmpty()) { + File file = (File) files.get(0); + ids[i+1] = file.getId(); + } + } + + for (int i = 0; i < ids.length; ++i) { + if (ids[i] == null) { + break; + } + pathIds.put(path, ids[i]); + } + + return ids[ids.length-1]; // Returns null if no file is found + } + + public List getBooks(Uri repoUri) throws IOException { + linkedOrThrow(); + + List list = new ArrayList<>(); + + String path = repoUri.getPath(); + + /* Fix root path. */ + if (path == null || path.equals("/")) { + path = ROOT_PATH; + } + + /* Strip trailing slashes. */ + path = path.replaceAll("/+$", ""); + + try { + + String folderId = findId(path); + + + if (folderId != null) { + + File folder = googleDriveClient.files().get(folderId) + .setFields("id, mimeType") + .execute(); + + if (folder.getMimeType() == "application/vnd.google-apps.folder") { + + String pageToken = null; + do { + FileList result = googleDriveClient.files().list() + .setQ(String.format("mimeType != 'application/vnd.google-apps.folder' " + + "and '%s' in parents and trashed = false", folderId)) + .setSpaces("drive") + .setFields("nextPageToken, files(id, name, mimeType)") + .setPageToken(pageToken) + .execute(); + for (File file : result.getFiles()) { + if(BookName.isSupportedFormatFileName(file.getName())) { + Uri uri = repoUri.buildUpon().appendPath(file.getName()).build(); + VersionedRook book = new VersionedRook( + repoId, + RepoType.GOOGLE_DRIVE, + repoUri, + uri, + Long.toString(file.getVersion()), + file.getModifiedTime().getValue()); + + list.add(book); + } + } + pageToken = result.getNextPageToken(); + } while (pageToken != null); + + } else { + throw new IOException("Not a directory: " + repoUri); + } + } else { + throw new IOException("Not a directory: " + repoUri); + } + + } catch (Exception e) { + e.printStackTrace(); + + throw new IOException("Failed getting the list of files in " + repoUri + + " listing " + path + ": " + + (e.getMessage() != null ? e.getMessage() : e.toString())); + } + + return list; + } + + /** + * Download file from Google Drive and store it to a local file. + */ + public VersionedRook download(Uri repoUri, String fileName, java.io.File localFile) throws IOException { + linkedOrThrow(); + + Uri uri = repoUri.buildUpon().appendPath(fileName).build(); + + OutputStream out = new BufferedOutputStream(new FileOutputStream(localFile)); + + try { + + String fileId = findId(uri.getPath()); + + if (fileId != null) { + File file = googleDriveClient.files().get(fileId) + .setFields("id, mimeType, version, modifiedDate") + .execute(); + + if (file.getMimeType() != "application/vnd.google-apps.folder") { + + String rev = Long.toString(file.getVersion()); + long mtime = file.getModifiedTime().getValue(); + + googleDriveClient.files().get(fileId).executeMediaAndDownloadTo(out); + + return new VersionedRook(repoId, RepoType.GOOGLE_DRIVE, repoUri, uri, rev, mtime); + + } else { + throw new IOException("Failed downloading Google Drive file " + uri + ": Not a file"); + } + } else { + throw new IOException("Failed downloading Google Drive file " + uri + ": File not found"); + } + } catch (Exception e) { + if (e.getMessage() != null) { + throw new IOException("Failed downloading Google Drive file " + uri + ": " + e.getMessage()); + } else { + throw new IOException("Failed downloading Google Drive file " + uri + ": " + e.toString()); + } + } finally { + out.close(); + } + } + + + /** Upload file to Google Drive. */ + public VersionedRook upload(java.io.File file, Uri repoUri, String fileName) throws IOException { + linkedOrThrow(); + + Uri bookUri = repoUri.buildUpon().appendPath(fileName).build(); + + if (file.length() > UPLOAD_FILE_SIZE_LIMIT * 1024 * 1024) { + throw new IOException(LARGE_FILE); + } + + // FileMetadata metadata; + // InputStream in = new FileInputStream(file); + + File fileMetadata = new File(); + String filePath = bookUri.getPath(); + + try { + fileMetadata.setName(fileName); + fileMetadata.setTrashed(false); + FileContent mediaContent = new FileContent("text/plain", file); + + String fileId = findId(filePath); + + if (fileId == null) { + filePath = "/" + filePath; // Avoids errors when file is in root folder + String folderPath = filePath.substring(0, filePath.lastIndexOf('/')); + String folderId = findId(folderPath); + + fileMetadata.setParents(Collections.singletonList(folderId)); + fileMetadata = googleDriveClient.files().create(fileMetadata, mediaContent) + .setFields("id, parents") + .execute(); + fileId = fileMetadata.getId(); + + pathIds.put(filePath, fileId); + } else { + fileMetadata = googleDriveClient.files().update(fileId, fileMetadata, mediaContent) + .setFields("id") + .execute(); + } + + } catch (Exception e) { + if (e.getMessage() != null) { + throw new IOException("Failed overwriting " + filePath + " on Google Drive: " + e.getMessage()); + } else { + throw new IOException("Failed overwriting " + filePath + " on Google Drive: " + e.toString()); + } + } + + String rev = Long.toString(fileMetadata.getVersion()); + long mtime = fileMetadata.getModifiedTime().getValue(); + + return new VersionedRook(repoId, RepoType.GOOGLE_DRIVE, repoUri, bookUri, rev, mtime); + + } + + public void delete(String path) throws IOException { + linkedOrThrow(); + + try { + String fileId = findId(path); + + if (fileId != null) { + File file = googleDriveClient.files().get(fileId).setFields("id, mimeType").execute(); + if (file.getMimeType() != "application/vnd.google-apps.folder") { + File fileMetadata = new File(); + fileMetadata.setTrashed(true); + googleDriveClient.files().update(fileId, fileMetadata).execute(); + } else { + throw new IOException("Not a file: " + path); + } + } + + } catch (Exception e) { + e.printStackTrace(); + + if (e.getMessage() != null) { + throw new IOException("Failed deleting " + path + " on Google Drive: " + e.getMessage()); + } else { + throw new IOException("Failed deleting " + path + " on Google Drive: " + e.toString()); + } + } + } + + public VersionedRook move(Uri repoUri, Uri from, Uri to) throws IOException { + linkedOrThrow(); + + try { + String fileId = findId(from.getPath()); + + File fileMetadata = new File(); + fileMetadata.setName(to.getPath()); + + if (fileId != null) { + fileMetadata = googleDriveClient.files().update(fileId, fileMetadata) + .setFields("id, mimeType, version, modifiedDate") + .execute(); + + if (fileMetadata.getMimeType() == "application/vnd.google-apps.folder") { + throw new IOException("Relocated object not a file?"); + } + + } + + String rev = Long.toString(fileMetadata.getVersion()); + long mtime = fileMetadata.getModifiedTime().getValue(); + + return new VersionedRook(repoId, RepoType.DROPBOX, repoUri, to, rev, mtime); + + } catch (Exception e) { + e.printStackTrace(); + + if (e.getMessage() != null) { // TODO: Move this throwing to utils + throw new IOException("Failed moving " + from + " to " + to + ": " + e.getMessage(), e); + } else { + throw new IOException("Failed moving " + from + " to " + to + ": " + e.toString(), e); + } + } + } + + /** + * This is unique to Google Drive. A special utility to create an empty directory. + * @return The ID of the directory. TODO: maybe I will use this in future code + */ + public String createDirectory() throws IOException { + linkedOrThrow(); + // https://github.com/googleworkspace/android-samples/blob/master/drive/deprecation/app/src/main/java/com/google/android/gms/drive/sample/driveapimigration/DriveServiceHelper.java + File metadata = new File(); + metadata.setName("Orgzly Files"); + metadata.setMimeType("application/vnd.google-apps.folder"); + File file = googleDriveClient + .files() + .create(metadata) + .setFields("id") + .execute(); + if (file == null) { + throw new IOException("Null result when requesting file creation"); + } + return file.getId(); + } + + /** + * Unique to Google Drive. A special utility to run an async task. + */ + public Task runAsTask(Callable callable) { + return Tasks.call(mExecutor, callable); + } +} diff --git a/app/src/main/java/com/orgzly/android/repos/GoogleDriveRepo.java b/app/src/main/java/com/orgzly/android/repos/GoogleDriveRepo.java new file mode 100644 index 000000000..ef3d69cfe --- /dev/null +++ b/app/src/main/java/com/orgzly/android/repos/GoogleDriveRepo.java @@ -0,0 +1,69 @@ +package com.orgzly.android.repos; + +import android.content.Context; +import android.net.Uri; + +import com.orgzly.android.util.UriUtils; + +import java.io.File; +import java.io.IOException; + +import java.util.List; + +public class GoogleDriveRepo implements SyncRepo { + public static final String SCHEME = "googledrive"; + + private final Uri repoUri; + private final GoogleDriveClient client; + + public GoogleDriveRepo(RepoWithProps repoWithProps, Context context) { + this.repoUri = Uri.parse(repoWithProps.getRepo().getUrl()); + this.client = new GoogleDriveClient(context, repoWithProps.getRepo().getId()); + } + + @Override + public boolean isConnectionRequired() { + return true; + } + + @Override + public boolean isAutoSyncSupported() { + return false; + } + + @Override + public Uri getUri() { + return repoUri; + } + + @Override + public List getBooks() throws IOException { + return client.getBooks(repoUri); + } + + @Override + public VersionedRook retrieveBook(String fileName, File file) throws IOException { + return client.download(repoUri, fileName, file); + } + + @Override + public VersionedRook storeBook(File file, String fileName) throws IOException { + return client.upload(file, repoUri, fileName); + } + + @Override + public VersionedRook renameBook(Uri fromUri, String name) throws IOException { + Uri toUri = UriUtils.getUriForNewName(fromUri, name); + return client.move(repoUri, fromUri, toUri); + } + + @Override + public void delete(Uri uri) throws IOException { + client.delete(uri.getPath()); + } + + @Override + public String toString() { + return repoUri.toString(); + } +} diff --git a/app/src/main/java/com/orgzly/android/repos/RepoFactory.kt b/app/src/main/java/com/orgzly/android/repos/RepoFactory.kt index 25ad16cc7..0c23a2102 100644 --- a/app/src/main/java/com/orgzly/android/repos/RepoFactory.kt +++ b/app/src/main/java/com/orgzly/android/repos/RepoFactory.kt @@ -22,6 +22,9 @@ class RepoFactory @Inject constructor( type == RepoType.DROPBOX.id && BuildConfig.IS_DROPBOX_ENABLED -> DropboxRepo(repoWithProps, context) + type == RepoType.GOOGLE_DRIVE.id && BuildConfig.IS_GOOGLE_DRIVE_ENABLED -> + GoogleDriveRepo(repoWithProps, context) + type == RepoType.DIRECTORY.id -> DirectoryRepo(repoWithProps, false) diff --git a/app/src/main/java/com/orgzly/android/repos/RepoType.kt b/app/src/main/java/com/orgzly/android/repos/RepoType.kt index e72cd0d49..a631808f7 100644 --- a/app/src/main/java/com/orgzly/android/repos/RepoType.kt +++ b/app/src/main/java/com/orgzly/android/repos/RepoType.kt @@ -5,10 +5,11 @@ import java.lang.IllegalArgumentException enum class RepoType(val id: Int) { MOCK(1), DROPBOX(2), - DIRECTORY(3), - DOCUMENT(4), - WEBDAV(5), - GIT(6); + GOOGLE_DRIVE(3), + DIRECTORY(4), + DOCUMENT(5), + WEBDAV(6), + GIT(7); companion object { @JvmStatic @@ -16,13 +17,14 @@ enum class RepoType(val id: Int) { return when (type) { 1 -> MOCK 2 -> DROPBOX - 3 -> DIRECTORY - 4 -> DOCUMENT - 5 -> WEBDAV - 6 -> GIT + 3 -> GOOGLE_DRIVE + 4 -> DIRECTORY + 5 -> DOCUMENT + 6 -> WEBDAV + 7 -> GIT else -> throw IllegalArgumentException("Unknown repo type id $type") } } } -} \ No newline at end of file +} diff --git a/app/src/main/java/com/orgzly/android/ui/repo/googledrive/GoogleDriveRepoActivity.kt b/app/src/main/java/com/orgzly/android/ui/repo/googledrive/GoogleDriveRepoActivity.kt new file mode 100644 index 000000000..09a01887f --- /dev/null +++ b/app/src/main/java/com/orgzly/android/ui/repo/googledrive/GoogleDriveRepoActivity.kt @@ -0,0 +1,307 @@ +package com.orgzly.android.ui.repo.googledrive + +import android.app.Activity +import android.app.AlertDialog +import android.content.DialogInterface +import android.content.Intent +import android.net.Uri +import android.os.Bundle +import android.text.TextUtils +import android.view.Menu +import android.view.MenuItem +import androidx.databinding.DataBindingUtil +import androidx.lifecycle.Observer +import com.orgzly.BuildConfig +import com.orgzly.R +import com.orgzly.android.App +import com.orgzly.android.repos.GoogleDriveClient +import com.orgzly.android.repos.GoogleDriveRepo +import com.orgzly.android.repos.RepoFactory +import com.orgzly.android.repos.RepoType +import com.orgzly.android.ui.CommonActivity +import com.orgzly.android.ui.repo.RepoViewModel +import com.orgzly.android.ui.repo.RepoViewModelFactory +import com.orgzly.android.ui.util.ActivityUtils +import com.orgzly.android.ui.util.styledAttributes +import com.orgzly.android.util.LogUtils +import com.orgzly.android.util.MiscUtils +import com.orgzly.android.util.UriUtils +import com.orgzly.databinding.ActivityRepoGoogleDriveBinding +import javax.inject.Inject + +import com.google.android.gms.auth.api.signin.GoogleSignIn; +import com.google.android.gms.auth.api.signin.GoogleSignInClient; + +import android.util.Log; +import androidx.activity.result.ActivityResultLauncher +import androidx.activity.result.contract.ActivityResultContracts +import androidx.lifecycle.ViewModelProvider + +class GoogleDriveRepoActivity : CommonActivity() { + private lateinit var signInLauncher: ActivityResultLauncher + private lateinit var binding: ActivityRepoGoogleDriveBinding + + @Inject + lateinit var repoFactory: RepoFactory + + private lateinit var client: GoogleDriveClient + + private val REQUEST_CODE_SIGN_IN = 1 + + private lateinit var gsiClient: GoogleSignInClient + + private lateinit var viewModel: RepoViewModel + + override fun onCreate(savedInstanceState: Bundle?) { + App.appComponent.inject(this) + + super.onCreate(savedInstanceState) + + binding = DataBindingUtil.setContentView(this, R.layout.activity_repo_google_drive) + + setupActionBar(R.string.google_drive) + + /* Google Drive link / unlink button. */ + binding.activityRepoGoogleDriveLinkButton.setOnClickListener { + if (isGoogleDriveLinked()) { + toggleLinkAfterConfirmation() + } else { + toggleLink() + } + } + + /* TODO: Google Drive create folder button. */ + binding.activityRepoGoogleDriveCreateDirectoryButton.setOnClickListener { + // TODO need a check to see if client is logged in; or hide the button when they're not + val task = client.runAsTask(({ client.createDirectory() })) + task.addOnSuccessListener { showSnackbar("Folder created") } + } + + // No need for editAccessToken() logic + + // Not working when done in XML + binding.activityRepoGoogleDriveDirectory.apply { + setHorizontallyScrolling(false) + + maxLines = 3 + + setOnEditorActionListener { _, _, _ -> + saveAndFinish() + finish() + true + } + } + + val repoId = intent.getLongExtra(ARG_REPO_ID, 0) + + val factory = RepoViewModelFactory.getInstance(dataRepository, repoId) + + viewModel = ViewModelProvider(this, factory).get(RepoViewModel::class.java) + + if (viewModel.repoId != 0L) { // Editing existing + viewModel.loadRepoProperties()?.let { repoWithProps -> + val path = Uri.parse(repoWithProps.repo.url).path + + binding.activityRepoGoogleDriveDirectory.setText(path) + } + } + + viewModel.finishEvent.observeSingle(this, Observer { + finish() + }) + + viewModel.alreadyExistsEvent.observeSingle(this, Observer { + showSnackbar(R.string.repository_url_already_exists) + }) + + viewModel.errorEvent.observeSingle(this, Observer { error -> + if (error != null) { + showSnackbar((error.cause ?: error).localizedMessage) + } + }) + + MiscUtils.clearErrorOnTextChange( + binding.activityRepoGoogleDriveDirectory, + binding.activityRepoGoogleDriveDirectoryInputLayout) + + ActivityUtils.openSoftKeyboardWithDelay(this, binding.activityRepoGoogleDriveDirectory) + + client = GoogleDriveClient(this, repoId) + + signInLauncher = registerForActivityResult( + ActivityResultContracts.StartActivityForResult()) { result -> + GoogleSignIn.getSignedInAccountFromIntent(result.data) + .addOnSuccessListener { googleAccount -> + Log.d(TAG, "Signed in as " + googleAccount.getEmail()) + client.finishAuthentication(googleAccount) + showSnackbar(R.string.message_google_drive_linked) + } + .addOnFailureListener { + exception -> Log.d(TAG, "Unable to sign in." + exception) } + } + } + + // TODO remove + override fun onActivityResult(requestCode:Int, resultCode:Int, resultData:Intent?) { + super.onActivityResult(requestCode, resultCode, resultData) + // Result returned from launching the Intent from GoogleSignInClient.getSignInIntent(...); + handleSignInResult(requestCode, resultData) + } + + // TODO remove + fun handleSignInResult(requestCode:Int, result:Intent?) { + if (requestCode == REQUEST_CODE_SIGN_IN) + { + GoogleSignIn.getSignedInAccountFromIntent(result) + .addOnSuccessListener({ googleAccount-> + Log.d(TAG, "Signed in as " + googleAccount.getEmail()) + // The DriveServiceHelper encapsulates all REST API and SAF functionality. + // Its instantiation is required before handling any onClick actions. + showSnackbar(R.string.message_google_drive_linked) + }) + .addOnFailureListener({ exception-> Log.d(TAG, "Unable to sign in." + exception) }) + } + } + + public override fun onResume() { + super.onResume() + + // There is no need for googleDriveCompleteAuthentication() + + updateGoogleDriveLinkUnlinkButton() + } + + override fun onCreateOptionsMenu(menu: Menu): Boolean { + super.onCreateOptionsMenu(menu) + + menuInflater.inflate(R.menu.done, menu) + + return true + } + + override fun onOptionsItemSelected(item: MenuItem): Boolean { + return when (item.itemId) { + R.id.done -> { + saveAndFinish() + true + } + + android.R.id.home -> { + finish() + true + } + + else -> + super.onOptionsItemSelected(item) + } + } + + private fun saveAndFinish() { + val directory = binding.activityRepoGoogleDriveDirectory.text.toString().trim { it <= ' ' } + + if (TextUtils.isEmpty(directory)) { + binding.activityRepoGoogleDriveDirectoryInputLayout.error = getString(R.string.can_not_be_empty) + return + } else { + binding.activityRepoGoogleDriveDirectoryInputLayout.error = null + } + + val url = UriUtils.uriFromPath(GoogleDriveRepo.SCHEME, directory).toString() + + val repo = try { + viewModel.validate(RepoType.GOOGLE_DRIVE, url) + } catch (e: Exception) { + e.printStackTrace() + binding.activityRepoGoogleDriveDirectoryInputLayout.error = + getString(R.string.repository_not_valid_with_reason, e.message) + return + } + + viewModel.saveRepo(RepoType.GOOGLE_DRIVE, repo.uri.toString()) + } + + private fun toggleLinkAfterConfirmation() { + val dialogClickListener = DialogInterface.OnClickListener { _, which -> + if (which == DialogInterface.BUTTON_POSITIVE) { + toggleLink() + } + } + + alertDialog = AlertDialog.Builder(this) + .setTitle(R.string.confirm_unlinking_from_google_drive_title) + .setMessage(R.string.confirm_unlinking_from_google_drive_message) + .setPositiveButton(R.string.unlink, dialogClickListener) + .setNegativeButton(R.string.cancel, dialogClickListener) + .show() + } + + private fun toggleLink() { + if (onGoogleDriveLinkToggleRequest()) { // Unlinked + updateGoogleDriveLinkUnlinkButton() + } // Else - Linking process started - button should stay the same. + } + + /** + * Toggle Google Drive link. Link to Google Drive or unlink from it, depending on current state. + * + * @return true if there was a change (Google Drive has been unlinked). + */ + private fun onGoogleDriveLinkToggleRequest(): Boolean { + return if (isGoogleDriveLinked()) { + // note that this is async + client.unlink(this).addOnCompleteListener { + showSnackbar(R.string.message_google_drive_unlinked) + } + true + } else { + intent = client.beginAuthentication(this) + // Note that startActivityForResult() is deprecated. + signInLauncher.launch(intent) + false + } + } + + // no need for googleDriveCompleteAuthentication() + + private fun updateGoogleDriveLinkUnlinkButton() { + if (BuildConfig.LOG_DEBUG) LogUtils.d(TAG) + + val resources = styledAttributes(R.styleable.Icons) { typedArray -> + if (isGoogleDriveLinked()) { + Pair( + getString(R.string.repo_google_drive_button_linked), + typedArray.getResourceId(R.styleable.Icons_oic_dropbox_linked, 0)) + } else { + Pair( + getString(R.string.repo_google_drive_button_not_linked), + typedArray.getResourceId(R.styleable.Icons_oic_dropbox_not_linked, 0)) + } + } + + binding.activityRepoGoogleDriveLinkButton.text = resources.first + + if (resources.second != 0) { + binding.activityRepoGoogleDriveIcon.setImageResource(resources.second) + } + } + + private fun isGoogleDriveLinked(): Boolean { + return client.isLinked + } + + companion object { + private val TAG: String = GoogleDriveRepoActivity::class.java.name + + private const val ARG_REPO_ID = "repo_id" + + @JvmStatic + @JvmOverloads + fun start(activity: Activity, repoId: Long = 0) { + val intent = Intent(Intent.ACTION_VIEW) + .setClass(activity, GoogleDriveRepoActivity::class.java) + .putExtra(ARG_REPO_ID, repoId) + + activity.startActivity(intent) + } + } +} diff --git a/app/src/main/java/com/orgzly/android/ui/repos/ReposActivity.kt b/app/src/main/java/com/orgzly/android/ui/repos/ReposActivity.kt index c3f24c2b0..b5ac3e438 100644 --- a/app/src/main/java/com/orgzly/android/ui/repos/ReposActivity.kt +++ b/app/src/main/java/com/orgzly/android/ui/repos/ReposActivity.kt @@ -24,6 +24,7 @@ import com.orgzly.android.repos.RepoType import com.orgzly.android.ui.CommonActivity import com.orgzly.android.ui.repo.directory.DirectoryRepoActivity import com.orgzly.android.ui.repo.dropbox.DropboxRepoActivity +import com.orgzly.android.ui.repo.googledrive.GoogleDriveRepoActivity import com.orgzly.android.ui.repo.git.GitRepoActivity import com.orgzly.android.ui.repo.webdav.WebdavRepoActivity import com.orgzly.databinding.ActivityReposBinding @@ -107,6 +108,16 @@ class ReposActivity : CommonActivity(), AdapterView.OnItemClickListener, Activit } } + binding.activityReposGoogleDrive.let { button -> + if (BuildConfig.IS_GOOGLE_DRIVE_ENABLED) { + button.setOnClickListener { + startRepoActivity(R.id.repos_options_menu_item_new_google_drive) + } + } else { + button.visibility = View.GONE + } + } + binding.activityReposGit.let { button -> if (AppPreferences.gitIsEnabled(this)) { button.setOnClickListener { @@ -158,6 +169,10 @@ class ReposActivity : CommonActivity(), AdapterView.OnItemClickListener, Activit newRepos.removeItem(R.id.repos_options_menu_item_new_dropbox) } + if (!BuildConfig.IS_GOOGLE_DRIVE_ENABLED) { + newRepos.removeItem(R.id.repos_options_menu_item_new_google_drive) + } + if (!AppPreferences.gitIsEnabled(App.getAppContext())) { newRepos.removeItem(R.id.repos_options_menu_item_new_git) } @@ -173,6 +188,11 @@ class ReposActivity : CommonActivity(), AdapterView.OnItemClickListener, Activit return true } + R.id.repos_options_menu_item_new_google_drive -> { + startRepoActivity(item.itemId) + return true + } + R.id.repos_options_menu_item_new_git -> { startRepoActivity(item.itemId) return true @@ -219,6 +239,11 @@ class ReposActivity : CommonActivity(), AdapterView.OnItemClickListener, Activit return } + R.id.repos_options_menu_item_new_google_drive -> { + GoogleDriveRepoActivity.start(this) + return + } + R.id.repos_options_menu_item_new_git -> { if (ContextCompat.checkSelfPermission(this, READ_WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED) { GitRepoActivity.start(this) @@ -260,6 +285,9 @@ class ReposActivity : CommonActivity(), AdapterView.OnItemClickListener, Activit RepoType.DROPBOX -> DropboxRepoActivity.start(this, repoEntity.id) + RepoType.GOOGLE_DRIVE -> + GoogleDriveRepoActivity.start(this, repoEntity.id) + RepoType.DIRECTORY -> DirectoryRepoActivity.start(this, repoEntity.id) diff --git a/app/src/main/res/layout/activity_repo_dropbox.xml b/app/src/main/res/layout/activity_repo_dropbox.xml index 89d7baa7c..51b989d3c 100644 --- a/app/src/main/res/layout/activity_repo_dropbox.xml +++ b/app/src/main/res/layout/activity_repo_dropbox.xml @@ -72,4 +72,4 @@ - \ No newline at end of file + diff --git a/app/src/main/res/layout/activity_repo_google_drive.xml b/app/src/main/res/layout/activity_repo_google_drive.xml new file mode 100644 index 000000000..c4862ea40 --- /dev/null +++ b/app/src/main/res/layout/activity_repo_google_drive.xml @@ -0,0 +1,90 @@ + + + + + + + + + + + + + + + +