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 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/layout/activity_repos.xml b/app/src/main/res/layout/activity_repos.xml
index 2ea7ab05a..9d2a01848 100644
--- a/app/src/main/res/layout/activity_repos.xml
+++ b/app/src/main/res/layout/activity_repos.xml
@@ -47,6 +47,15 @@
android:drawableTop="?attr/oic_dropbox_not_linked"
android:text="@string/dropbox" />
+
+