diff --git a/README.org b/README.org
index 4b07aa5cf..bdb750319 100644
--- a/README.org
+++ b/README.org
@@ -8,7 +8,7 @@
Orgzly is an outliner for taking notes and managing to-do lists.
You can keep notebooks stored in plain-text and have them synchronized
-with a directory on your mobile device, SD card, WebDAV server or Dropbox.
+with a directory on your mobile device, SD card, WebDAV server, Dropbox or Google Drive.
Notebooks are saved in /Org mode/'s file format. “Org mode is for
keeping notes, maintaining TODO lists, planning projects, and
diff --git a/app/build.gradle b/app/build.gradle
index abedd005b..96ad7e9f1 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -62,6 +62,8 @@ android {
premium {
buildConfigField "boolean", "IS_DROPBOX_ENABLED", "true"
+ buildConfigField "boolean", "IS_GOOGLE_DRIVE_ENABLED", "true"
+
buildConfigField "String", "VERSION_NAME_SUFFIX", '""'
dimension "store"
@@ -69,11 +71,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"
@@ -88,7 +92,6 @@ android {
kotlinOptions {
jvmTarget = 11
}
-
packagingOptions {
resources {
excludes += ['META-INF/DEPENDENCIES', 'plugin.properties']
@@ -99,6 +102,10 @@ android {
checkDependencies true
disable 'MissingTranslation', 'MissingQuantity', 'ImpliedQuantity', 'InvalidPackage'
}
+ lint {
+ disable 'MissingTranslation', 'MissingQuantity', 'ImpliedQuantity', 'InvalidPackage'
+ }
+
}
dependencies {
@@ -164,6 +171,18 @@ dependencies {
implementation "com.dropbox.core:dropbox-core-sdk:$versions.dropbox_core_sdk"
+ // Google Drive
+ 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'
+ }
+ // just to avoid duplicate class build error
+ implementation 'com.google.guava:listenablefuture:9999.0-empty-to-avoid-conflict-with-guava'
+
implementation "com.googlecode.juniversalchardet:juniversalchardet:$versions.juniversalchardet"
implementation "joda-time:joda-time:$versions.joda_time"
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index c9a8755f3..2624f2793 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -99,6 +99,11 @@
android:windowSoftInputMode="stateAlwaysHidden">
+
+
+
1
url.startsWith("dropbox") -> 2
- url.startsWith("file") -> 3
- url.startsWith("content") -> 4
- url.matches("^(webdav|dav|http)s?.*".toRegex()) -> 5
+ url.startsWith("google") -> 3
+ url.startsWith("file") -> 4
+ url.startsWith("content") -> 5
+ url.matches("^(webdav|dav|http)s?.*".toRegex()) -> 6
else -> throw IllegalArgumentException("Unknown repo $url")
}
diff --git a/app/src/main/java/com/orgzly/android/di/AppComponent.kt b/app/src/main/java/com/orgzly/android/di/AppComponent.kt
index d04a70f58..d75b2e619 100644
--- a/app/src/main/java/com/orgzly/android/di/AppComponent.kt
+++ b/app/src/main/java/com/orgzly/android/di/AppComponent.kt
@@ -24,6 +24,7 @@ import com.orgzly.android.ui.refile.RefileFragment
import com.orgzly.android.ui.repo.BrowserActivity
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.android.ui.repos.ReposActivity
@@ -51,6 +52,7 @@ interface AppComponent {
fun inject(arg: MainActivity)
fun inject(arg: ReposActivity)
fun inject(arg: DropboxRepoActivity)
+ fun inject(arg: GoogleDriveRepoActivity)
fun inject(arg: DirectoryRepoActivity)
fun inject(arg: WebdavRepoActivity)
fun inject(arg: GitRepoActivity)
@@ -84,4 +86,4 @@ interface AppComponent {
fun inject(arg: TimeChangeBroadcastReceiver)
fun inject(arg: RemindersBroadcastReceiver)
fun inject(arg: SharingShortcutsManager)
-}
\ No newline at end of file
+}
diff --git a/app/src/main/java/com/orgzly/android/repos/GoogleDriveClient.java b/app/src/main/java/com/orgzly/android/repos/GoogleDriveClient.java
new file mode 100644
index 000000000..2d54588b2
--- /dev/null
+++ b/app/src/main/java/com/orgzly/android/repos/GoogleDriveClient.java
@@ -0,0 +1,379 @@
+package com.orgzly.android.repos;
+
+import android.content.Context;
+import android.net.Uri;
+import android.os.Build;
+
+import androidx.annotation.RequiresApi;
+
+import com.google.android.gms.auth.api.signin.GoogleSignIn;
+
+import com.google.android.gms.auth.api.signin.GoogleSignInAccount;
+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.gson.GsonFactory;
+import com.google.api.services.drive.Drive;
+import com.google.api.services.drive.DriveScopes;
+import com.google.api.services.drive.model.File;
+import com.google.api.services.drive.model.FileList;
+
+import com.orgzly.android.BookName;
+
+import java.io.BufferedOutputStream;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+public class GoogleDriveClient {
+ private static final String TAG = GoogleDriveClient.class.getName();
+
+ private static final long UPLOAD_FILE_SIZE_LIMIT = 150; // MB
+
+ // TODO: Throw GoogleDriveNotLinked etc. instead and let the client get message from resources
+ private static final String NOT_LINKED = "Not linked to Google Drive";
+ private static final String LARGE_FILE = "File larger then " + UPLOAD_FILE_SIZE_LIMIT + " MB";
+
+ /* Using the empty string ("") to represent the root folder. */
+ private static final String ROOT_PATH = "";
+
+ private final Context mContext;
+ private final long repoId;
+ private static Drive mDriveService;
+
+ private static Map pathIds;
+ static {
+ pathIds = new HashMap<>();
+ pathIds.put("My Drive", "root");
+ pathIds.put("", "root");
+ }
+
+ public GoogleDriveClient(Context context, long id) {
+ mContext = context;
+
+ repoId = id;
+
+ if (isLinked()) setDriveService();
+ }
+
+ public boolean isLinked() {
+ // Check for existing Google Sign In account, if the user is already signed in
+ // the GoogleSignInAccount will be non-null.
+ return getGoogleAccount() != null;
+ }
+
+ private GoogleSignInAccount getGoogleAccount() {
+ return GoogleSignIn.getLastSignedInAccount(mContext);
+ }
+
+ public Drive getDriveService(GoogleSignInAccount googleAccount) {
+ GoogleAccountCredential credential = GoogleAccountCredential.usingOAuth2(
+ mContext, Collections.singleton(DriveScopes.DRIVE));
+ credential.setSelectedAccount(googleAccount.getAccount());
+ return new Drive.Builder(
+ AndroidHttp.newCompatibleTransport(),
+ new GsonFactory(),
+ credential)
+ .setApplicationName("Orgzly")
+ .build();
+ }
+
+ public Drive getDriveService() {
+ return getDriveService(getGoogleAccount());
+ }
+
+ public void setDriveService() {
+ if (mDriveService == null) mDriveService = getDriveService();
+ }
+
+ public void setDriveService(GoogleSignInAccount googleAccount) {
+ mDriveService = getDriveService(googleAccount);
+ }
+
+ private void linkedOrThrow() throws IOException {
+ if (! isLinked()) {
+ throw new IOException(NOT_LINKED);
+ } else {
+ setDriveService();
+ }
+ }
+
+ @RequiresApi(api = Build.VERSION_CODES.N)
+ private String findId(String path) throws IOException {
+ if (pathIds.containsKey(path)) {
+ return pathIds.get(path);
+ }
+
+ String[] parts = path.split("/");
+ parts = Arrays.stream(parts).filter(s -> !s.isEmpty()).toArray(String[]::new);
+ String[] ids = new String[parts.length+1];
+
+ ids[0] = "root";
+
+ for (int i=0; i < parts.length; ++i) {
+ FileList result = mDriveService.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();
+ }
+ }
+
+ if (ids[ids.length-1] != null) {
+ pathIds.put(path, ids[ids.length-1]);
+ }
+
+ return ids[ids.length-1]; // Returns null if no file is found
+ }
+
+ @RequiresApi(api = Build.VERSION_CODES.N)
+ 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 pathId = findId(path);
+
+ if (pathId != null) {
+
+ File folder = mDriveService.files().get(pathId)
+ .setFields("id, name, mimeType, version, modifiedTime")
+ .execute();
+
+ if (folder.getMimeType().equals("application/vnd.google-apps.folder")) {
+
+ String pageToken = null;
+ do {
+ FileList result = mDriveService.files().list()
+ .setQ(String.format("mimeType != 'application/vnd.google-apps.folder' " +
+ "and '%s' in parents and trashed = false", pathId))
+ .setSpaces("drive")
+ .setFields("nextPageToken, files(id, name, mimeType, version, modifiedTime)")
+ .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("Path not found: " + 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.
+ */
+ @RequiresApi(api = Build.VERSION_CODES.N)
+ 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 = mDriveService.files().get(fileId)
+ .setFields("id, mimeType, version, modifiedTime")
+ .execute();
+
+ if (!file.getMimeType().equals("application/vnd.google-apps.folder")) {
+
+ String rev = Long.toString(file.getVersion());
+ long mtime = file.getModifiedTime().getValue();
+
+ mDriveService.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);
+ }
+ } finally {
+ out.close();
+ }
+ }
+
+
+ /** Upload file to Google Drive. */
+ @RequiresApi(api = Build.VERSION_CODES.N)
+ 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 = mDriveService.files().create(fileMetadata, mediaContent)
+ .setFields("id, parents")
+ .execute();
+ fileId = fileMetadata.getId();
+
+ pathIds.put(filePath, fileId);
+ } else {
+ fileMetadata = mDriveService.files().update(fileId, fileMetadata, mediaContent)
+ .setFields("id, version, modifiedTime")
+ .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);
+ }
+ }
+
+ String rev = Long.toString(fileMetadata.getVersion());
+ long mtime = fileMetadata.getModifiedTime().getValue();
+
+ return new VersionedRook(repoId, RepoType.GOOGLE_DRIVE, repoUri, bookUri, rev, mtime);
+ }
+
+ @RequiresApi(api = Build.VERSION_CODES.N)
+ public void delete(String path) throws IOException {
+ linkedOrThrow();
+
+ try {
+ String fileId = findId(path);
+
+ if (fileId != null) {
+ File file = mDriveService.files().get(fileId).setFields("id, mimeType").execute();
+ if (!file.getMimeType().equals("application/vnd.google-apps.folder")) {
+ File fileMetadata = new File();
+ fileMetadata.setTrashed(true);
+ mDriveService.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);
+ }
+ }
+ }
+
+ @RequiresApi(api = Build.VERSION_CODES.N)
+ 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 = mDriveService.files().update(fileId, fileMetadata)
+ .setFields("id, mimeType, version, modifiedTime")
+ .execute();
+
+ if (fileMetadata.getMimeType().equals("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.GOOGLE_DRIVE, 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, e);
+ }
+ }
+ }
+}
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..65ac60b0a
--- /dev/null
+++ b/app/src/main/java/com/orgzly/android/ui/repo/googledrive/GoogleDriveRepoActivity.kt
@@ -0,0 +1,319 @@
+package com.orgzly.android.ui.repo.googledrive
+
+import android.app.Activity
+import android.content.DialogInterface
+import android.content.Intent
+import android.net.Uri
+import android.os.Bundle
+import android.text.TextUtils
+import androidx.lifecycle.Observer
+import androidx.lifecycle.ViewModelProvider
+import com.google.android.material.dialog.MaterialAlertDialogBuilder
+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.showSnackbar
+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 com.google.android.gms.auth.api.signin.GoogleSignInOptions
+import com.google.android.gms.common.api.Scope
+import com.google.android.gms.tasks.Task
+import com.google.android.gms.tasks.OnCompleteListener
+
+import com.google.api.services.drive.DriveScopes
+import androidx.annotation.NonNull
+import android.util.Log
+
+class GoogleDriveRepoActivity : CommonActivity() {
+ 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 = ActivityRepoGoogleDriveBinding.inflate(layoutInflater)
+
+ setContentView(binding.root)
+
+ /* Google Drive link / unlink button. */
+ binding.activityRepoGoogleDriveLinkButton.setOnClickListener {
+ if (isGoogleDriveLinked()) {
+ toggleLinkAfterConfirmation()
+ } else {
+ toggleLink()
+ }
+ }
+
+ // binding.activityRepoGoogleDriveLinkButton.setOnLongClickListener {
+ // editAccessToken()
+ // true
+ // }
+
+ // 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.openSoftKeyboard(this, binding.activityRepoGoogleDriveDirectory)
+
+ client = GoogleDriveClient(applicationContext, repoId)
+
+ binding.bottomAppBar.run {
+ setNavigationOnClickListener {
+ finish()
+ }
+ }
+
+ binding.fab.setOnClickListener {
+ saveAndFinish()
+ }
+
+ createSignInClient()
+ }
+
+ fun createSignInClient() {
+ Log.d(TAG, "Creating sign-in client")
+ val signInOptions = GoogleSignInOptions.Builder(GoogleSignInOptions.DEFAULT_SIGN_IN)
+ .requestEmail()
+ .requestScopes(Scope(DriveScopes.DRIVE))
+ .build()
+ gsiClient = GoogleSignIn.getClient(this, signInOptions)
+ }
+
+ 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)
+ }
+
+ private fun handleSignInResult(requestCode:Int, result:Intent?) {
+ if (requestCode == REQUEST_CODE_SIGN_IN)
+ {
+ GoogleSignIn.getSignedInAccountFromIntent(result)
+ .addOnSuccessListener { googleAccount ->
+ Log.d(TAG, "Signed in as " + googleAccount.email)
+ // Use the authenticated account to sign in to the Drive service.
+ client.setDriveService(googleAccount)
+ showSnackbar(R.string.message_google_drive_linked)
+ }
+ .addOnFailureListener { exception -> Log.d(TAG, "Unable to sign in.$exception") }
+ }
+ }
+
+ public override fun onResume() {
+ super.onResume()
+
+ 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 = MaterialAlertDialogBuilder(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()) {
+ unlinkGoogleDrive()
+ showSnackbar(R.string.message_google_drive_unlinked)
+ true
+
+ } else {
+ linkGoogleDrive()
+ false
+ }
+ }
+
+ 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_google_drive_linked, 0))
+ } else {
+ Pair(
+ getString(R.string.repo_google_drive_button_not_linked),
+ typedArray.getResourceId(R.styleable.Icons_oic_google_drive_not_linked, 0))
+ }
+ }
+
+ binding.activityRepoGoogleDriveLinkButton.text = resources.first
+
+ if (resources.second != 0) {
+ binding.activityRepoGoogleDriveIcon.setImageResource(resources.second)
+ }
+ }
+
+ private fun isGoogleDriveLinked(): Boolean {
+ // Check for existing Google Sign In account, if the user is already signed in
+ // the GoogleSignInAccount will be non-null.
+ return GoogleSignIn.getLastSignedInAccount(this) != null
+ }
+
+ private fun linkGoogleDrive() {
+ startActivityForResult(gsiClient.getSignInIntent(), REQUEST_CODE_SIGN_IN)
+ }
+
+ private fun unlinkGoogleDrive() {
+ gsiClient.revokeAccess()
+ .addOnCompleteListener(this, OnCompleteListener {
+ fun onComplete(@NonNull task:Task) {
+ Log.d(TAG, "Signed out")
+ }
+ })
+ }
+
+ 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 1b99d98c6..a97788a57 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
@@ -23,6 +23,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.android.ui.showSnackbar
@@ -105,6 +106,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)
}
@@ -122,6 +127,10 @@ class ReposActivity : CommonActivity(), AdapterView.OnItemClickListener, Activit
startRepoActivity(menuItem.itemId)
}
+ R.id.repos_options_menu_item_new_google_drive -> {
+ startRepoActivity(menuItem.itemId)
+ }
+
R.id.repos_options_menu_item_new_git -> {
startRepoActivity(menuItem.itemId)
}
@@ -156,6 +165,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 {
@@ -218,6 +237,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 (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R || ContextCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED) {
GitRepoActivity.start(this)
@@ -262,6 +286,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/drawable-hdpi/cic_google_drive_black.png b/app/src/main/res/drawable-hdpi/cic_google_drive_black.png
new file mode 100644
index 000000000..6b0c3f04c
Binary files /dev/null and b/app/src/main/res/drawable-hdpi/cic_google_drive_black.png differ
diff --git a/app/src/main/res/drawable-hdpi/cic_google_drive_color.png b/app/src/main/res/drawable-hdpi/cic_google_drive_color.png
new file mode 100644
index 000000000..e6c65e7dc
Binary files /dev/null and b/app/src/main/res/drawable-hdpi/cic_google_drive_color.png differ
diff --git a/app/src/main/res/drawable-hdpi/cic_google_drive_white.png b/app/src/main/res/drawable-hdpi/cic_google_drive_white.png
new file mode 100644
index 000000000..decfdb1f4
Binary files /dev/null and b/app/src/main/res/drawable-hdpi/cic_google_drive_white.png differ
diff --git a/app/src/main/res/drawable-mdpi/cic_google_drive_black.png b/app/src/main/res/drawable-mdpi/cic_google_drive_black.png
new file mode 100644
index 000000000..d199d1c29
Binary files /dev/null and b/app/src/main/res/drawable-mdpi/cic_google_drive_black.png differ
diff --git a/app/src/main/res/drawable-mdpi/cic_google_drive_color.png b/app/src/main/res/drawable-mdpi/cic_google_drive_color.png
new file mode 100644
index 000000000..631151494
Binary files /dev/null and b/app/src/main/res/drawable-mdpi/cic_google_drive_color.png differ
diff --git a/app/src/main/res/drawable-mdpi/cic_google_drive_white.png b/app/src/main/res/drawable-mdpi/cic_google_drive_white.png
new file mode 100644
index 000000000..b576e39ec
Binary files /dev/null and b/app/src/main/res/drawable-mdpi/cic_google_drive_white.png differ
diff --git a/app/src/main/res/drawable-xhdpi/cic_google_drive_black.png b/app/src/main/res/drawable-xhdpi/cic_google_drive_black.png
new file mode 100644
index 000000000..67c56f1b5
Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/cic_google_drive_black.png differ
diff --git a/app/src/main/res/drawable-xhdpi/cic_google_drive_color.png b/app/src/main/res/drawable-xhdpi/cic_google_drive_color.png
new file mode 100644
index 000000000..c98e79930
Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/cic_google_drive_color.png differ
diff --git a/app/src/main/res/drawable-xhdpi/cic_google_drive_white.png b/app/src/main/res/drawable-xhdpi/cic_google_drive_white.png
new file mode 100644
index 000000000..d6dcfece8
Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/cic_google_drive_white.png differ
diff --git a/app/src/main/res/drawable-xxhdpi/cic_google_drive_black.png b/app/src/main/res/drawable-xxhdpi/cic_google_drive_black.png
new file mode 100644
index 000000000..7ff51f1aa
Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/cic_google_drive_black.png differ
diff --git a/app/src/main/res/drawable-xxhdpi/cic_google_drive_color.png b/app/src/main/res/drawable-xxhdpi/cic_google_drive_color.png
new file mode 100644
index 000000000..f01295d64
Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/cic_google_drive_color.png differ
diff --git a/app/src/main/res/drawable-xxhdpi/cic_google_drive_white.png b/app/src/main/res/drawable-xxhdpi/cic_google_drive_white.png
new file mode 100644
index 000000000..37e084377
Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/cic_google_drive_white.png differ
diff --git a/app/src/main/res/drawable-xxxhdpi/cic_google_drive_black.png b/app/src/main/res/drawable-xxxhdpi/cic_google_drive_black.png
new file mode 100644
index 000000000..c483dc92f
Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/cic_google_drive_black.png differ
diff --git a/app/src/main/res/drawable-xxxhdpi/cic_google_drive_color.png b/app/src/main/res/drawable-xxxhdpi/cic_google_drive_color.png
new file mode 100644
index 000000000..ed4a57250
Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/cic_google_drive_color.png differ
diff --git a/app/src/main/res/drawable-xxxhdpi/cic_google_drive_white.png b/app/src/main/res/drawable-xxxhdpi/cic_google_drive_white.png
new file mode 100644
index 000000000..283285112
Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/cic_google_drive_white.png differ
diff --git a/app/src/main/res/layout/activity_repo_dropbox.xml b/app/src/main/res/layout/activity_repo_dropbox.xml
index 890698a33..14411fb42 100644
--- a/app/src/main/res/layout/activity_repo_dropbox.xml
+++ b/app/src/main/res/layout/activity_repo_dropbox.xml
@@ -87,4 +87,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..36f227684
--- /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 f32867846..bfbd3f877 100644
--- a/app/src/main/res/layout/activity_repos.xml
+++ b/app/src/main/res/layout/activity_repos.xml
@@ -45,6 +45,12 @@
app:icon="?attr/oic_dropbox_not_linked"
android:text="@string/dropbox" />
+
+
-
\ No newline at end of file
+
diff --git a/app/src/main/res/menu/repos_actions.xml b/app/src/main/res/menu/repos_actions.xml
index e98283271..db1945fc0 100644
--- a/app/src/main/res/menu/repos_actions.xml
+++ b/app/src/main/res/menu/repos_actions.xml
@@ -23,6 +23,10 @@
android:id="@+id/repos_options_menu_item_new_dropbox"
app:showAsAction="ifRoom"
android:title="@string/dropbox"/>
+
+
+
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 735b29910..ee58f389d 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -4,7 +4,7 @@
Settings
OK
Yes
- Unlink
+ Unlink
Clear
Cancel
Create
@@ -18,6 +18,7 @@
Remove link
Browse
Dropbox
+ Google Drive
View
Edit
Edit note
@@ -82,6 +83,9 @@
Unlinked from Dropbox
Linked to Dropbox successfully
+ Unlinked from Google Drive
+ Linked to Google Drive successfully
+
Repositories
Location to synchronize your notebooks with
@@ -151,6 +155,8 @@
Search
Link to Dropbox
Unlink from Dropbox
+ Link to Google Drive
+ Unlink from Google Drive
Settings
Preference affects notes
@@ -191,6 +197,7 @@
Directory
Directory inside Dropbox (e.g. “/Documents/Orgzly”)
+ Directory inside Google Drive (e.g. “/Documents/Orgzly”)
Directory URI
Cloning repository
@@ -246,6 +253,9 @@
Unlink from Dropbox?
You will not be able to sync to Dropbox until you link to it again.
+ Unlink from Google Drive?
+ You will not be able to sync to Google Drive until you link to it again.
+
Notebook not set
Title cannot be empty
Cannot be empty
@@ -524,7 +534,7 @@
Auto-sync
Used for local repositories only
- Not supported for Dropbox
+ Not supported for Dropbox or Google Drive
Depending on which options you choose, sync could be triggered often. If notebook is modified, its entire content is transferred to or from the repository. Certain actions might be delayed while syncing is in progress.
Note created
diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml
index 2b77209fb..1c09313e5 100644
--- a/app/src/main/res/values/styles.xml
+++ b/app/src/main/res/values/styles.xml
@@ -140,8 +140,10 @@
- @drawable/ic_widgets_black_24dp
- @drawable/cic_dropbox_blue_48px
+ - @drawable/cic_google_drive_color
- @drawable/cic_git_black_48dp
- @drawable/cic_dropbox_black_48px
+ - @drawable/cic_google_drive_black
- @drawable/cic_new_above_black_24dp
- @drawable/cic_new_below_black_24dp
- @drawable/cic_new_under_black_24dp
@@ -270,8 +272,10 @@
- @drawable/ic_widgets_white_24dp
- @drawable/cic_dropbox_blue_48px
+ - @drawable/cic_google_drive_color
- @drawable/cic_git_white_48dp
- @drawable/cic_dropbox_white_48px
+ - @drawable/cic_google_drive_white
- @drawable/cic_new_above_white_24dp
- @drawable/cic_new_below_white_24dp
- @drawable/cic_new_under_white_24dp