diff --git a/twidere/build.gradle b/twidere/build.gradle index d090c536f..044d39e43 100644 --- a/twidere/build.gradle +++ b/twidere/build.gradle @@ -99,11 +99,15 @@ dependencies { googleCompile "com.google.android.gms:play-services-maps:$play_services_version" googleCompile "com.google.android.gms:play-services-auth:$play_services_version" googleCompile 'com.google.maps.android:android-maps-utils:0.4.4' - googleCompile("com.crashlytics.sdk.android:crashlytics:$crashlyrics_version@aar") { transitive = true } + googleCompile("com.crashlytics.sdk.android:crashlytics:$crashlyrics_version@aar") { + transitive = true + } googleCompile 'com.anjlab.android.iab.v3:library:1.0.38' googleCompile "com.dropbox.core:dropbox-core-sdk:$dropbox_core_sdk_version" googleCompile ':YouTubeAndroidPlayerApi:1.2.2@jar' - googleCompile "com.google.apis:google-api-services-drive:$google_api_drive_version" + googleCompile("com.google.apis:google-api-services-drive:$google_api_drive_version") { + exclude group: 'org.apache.httpcomponents' + } // END Non-FOSS component fdroidCompile 'org.osmdroid:osmdroid-android:5.5:release@aar' diff --git a/twidere/src/google/kotlin/org/mariotaku/twidere/activity/sync/GoogleDriveAuthActivity.kt b/twidere/src/google/kotlin/org/mariotaku/twidere/activity/sync/GoogleDriveAuthActivity.kt index 7dc2adf6a..ba94b4c68 100644 --- a/twidere/src/google/kotlin/org/mariotaku/twidere/activity/sync/GoogleDriveAuthActivity.kt +++ b/twidere/src/google/kotlin/org/mariotaku/twidere/activity/sync/GoogleDriveAuthActivity.kt @@ -1,13 +1,16 @@ package org.mariotaku.twidere.activity.sync -import android.accounts.Account -import android.accounts.AccountManager import android.app.Activity import android.content.Intent import android.os.Bundle -import com.google.android.gms.auth.GoogleAuthUtil -import com.google.android.gms.auth.UserRecoverableAuthException -import com.google.android.gms.common.AccountPicker +import com.google.android.gms.auth.api.Auth +import com.google.android.gms.auth.api.signin.GoogleSignInOptions +import com.google.android.gms.common.ConnectionResult +import com.google.android.gms.common.api.GoogleApiClient +import com.google.android.gms.common.api.Scope +import com.google.api.client.googleapis.auth.oauth2.GoogleAuthorizationCodeTokenRequest +import com.google.api.client.http.javanet.NetHttpTransport +import com.google.api.client.json.jackson2.JacksonFactory import com.google.api.services.drive.DriveScopes import nl.komponents.kovenant.task import nl.komponents.kovenant.ui.successUi @@ -17,51 +20,86 @@ import org.mariotaku.twidere.constant.dataSyncProviderInfoKey import org.mariotaku.twidere.model.sync.GoogleDriveSyncProviderInfo -class GoogleDriveAuthActivity : BaseActivity() { +class GoogleDriveAuthActivity : BaseActivity(), GoogleApiClient.ConnectionCallbacks, + GoogleApiClient.OnConnectionFailedListener { + + + private lateinit var googleApiClient: GoogleApiClient public override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - val intent = AccountPicker.newChooseAccountIntent(null, null, - arrayOf(GoogleAuthUtil.GOOGLE_ACCOUNT_TYPE), false, null, null, null, null) - startActivityForResult(intent, REQUEST_CODE_CHOOSE_ACCOUNT) + + val gso = GoogleSignInOptions.Builder(GoogleSignInOptions.DEFAULT_SIGN_IN) + .requestScopes(Scope(DriveScopes.DRIVE), Scope(DriveScopes.DRIVE_METADATA)) + .requestServerAuthCode(GoogleDriveSyncProviderInfo.WEB_CLIENT_ID, true) + .build() + + googleApiClient = GoogleApiClient.Builder(this) + .addConnectionCallbacks(this) + .addOnConnectionFailedListener(this) + .addApi(Auth.GOOGLE_SIGN_IN_API, gso) + .build(); + + googleApiClient.connect(); + } + + override fun onDestroy() { + googleApiClient.disconnect() + super.onDestroy() } override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { when (requestCode) { - REQUEST_CODE_CHOOSE_ACCOUNT -> { - if (resultCode == Activity.RESULT_OK && data != null) { - val name = data.getStringExtra(AccountManager.KEY_ACCOUNT_NAME) - val type = data.getStringExtra(AccountManager.KEY_ACCOUNT_TYPE) - val account = Account(name, type) + REQUEST_RESOLVE_ERROR -> { + if (!googleApiClient.isConnected && !googleApiClient.isConnecting) { + googleApiClient.connect() + } + } + REQUEST_GOOGLE_SIGN_IN -> { + if (resultCode == Activity.RESULT_OK) { + val result = Auth.GoogleSignInApi.getSignInResultFromIntent(data) + val authCode = result.signInAccount?.serverAuthCode ?: return + val httpTransport = NetHttpTransport() + val jsonFactory = JacksonFactory.getDefaultInstance() + val tokenRequest = GoogleAuthorizationCodeTokenRequest(httpTransport, jsonFactory, + "https://www.googleapis.com/oauth2/v4/token", GoogleDriveSyncProviderInfo.WEB_CLIENT_ID, + GoogleDriveSyncProviderInfo.WEB_CLIENT_SECRET, authCode, "") task { - return@task GoogleAuthUtil.getToken(this, account, "oauth2:${DriveScopes.DRIVE_APPDATA}") - }.successUi { accessToken -> - preferences[dataSyncProviderInfoKey] = GoogleDriveSyncProviderInfo(accessToken) + tokenRequest.execute() + }.successUi { response -> + preferences[dataSyncProviderInfoKey] = GoogleDriveSyncProviderInfo(response.refreshToken) + setResult(Activity.RESULT_OK) finish() }.fail { ex -> - if (ex is UserRecoverableAuthException) { - startActivityForResult(ex.intent, REQUEST_CODE_AUTH_ERROR_RECOVER) - } else { - finish() - } + ex.printStackTrace() } } } - REQUEST_CODE_AUTH_ERROR_RECOVER -> { - if (resultCode == Activity.RESULT_OK && data != null) { - val name = data.getStringExtra(AccountManager.KEY_ACCOUNT_NAME) - val type = data.getStringExtra(AccountManager.KEY_ACCOUNT_TYPE) - val token = data.getStringExtra(AccountManager.KEY_AUTHTOKEN) - preferences[dataSyncProviderInfoKey] = GoogleDriveSyncProviderInfo(token) - } - finish() - } + } + } + + override fun onConnected(connectionHint: Bundle?) { + Auth.GoogleSignInApi.signOut(googleApiClient).setResultCallback { + // Start sign in + val signInIntent = Auth.GoogleSignInApi.getSignInIntent(googleApiClient) + startActivityForResult(signInIntent, REQUEST_GOOGLE_SIGN_IN) + } + } + + override fun onConnectionSuspended(cause: Int) { + } + + override fun onConnectionFailed(connectionResult: ConnectionResult) { + if (connectionResult.hasResolution()) { + connectionResult.startResolutionForResult(this, REQUEST_RESOLVE_ERROR) + } else { + } } companion object { - private const val REQUEST_CODE_CHOOSE_ACCOUNT: Int = 101 - private const val REQUEST_CODE_AUTH_ERROR_RECOVER: Int = 102 + private const val REQUEST_RESOLVE_ERROR: Int = 101 + private const val REQUEST_GOOGLE_SIGN_IN: Int = 102 } } \ No newline at end of file diff --git a/twidere/src/google/kotlin/org/mariotaku/twidere/model/sync/GoogleDriveSyncProviderInfo.kt b/twidere/src/google/kotlin/org/mariotaku/twidere/model/sync/GoogleDriveSyncProviderInfo.kt index 5db1f1972..341ecec60 100644 --- a/twidere/src/google/kotlin/org/mariotaku/twidere/model/sync/GoogleDriveSyncProviderInfo.kt +++ b/twidere/src/google/kotlin/org/mariotaku/twidere/model/sync/GoogleDriveSyncProviderInfo.kt @@ -5,21 +5,24 @@ import android.content.SharedPreferences import org.mariotaku.twidere.util.sync.SyncTaskRunner import org.mariotaku.twidere.util.sync.google.GoogleDriveSyncTaskRunner -class GoogleDriveSyncProviderInfo(val accessToken: String) : SyncProviderInfo(GoogleDriveSyncProviderInfo.TYPE) { +class GoogleDriveSyncProviderInfo(val refreshToken: String) : SyncProviderInfo(GoogleDriveSyncProviderInfo.TYPE) { override fun writeToPreferences(editor: SharedPreferences.Editor) { - editor.putString(KEY_GOOGLE_DRIVE_AUTH_TOKEN, accessToken) + editor.putString(KEY_GOOGLE_DRIVE_REFRESH_TOKEN, refreshToken) } override fun newSyncTaskRunner(context: Context): SyncTaskRunner { - return GoogleDriveSyncTaskRunner(context, accessToken) + return GoogleDriveSyncTaskRunner(context, refreshToken) } companion object { const val TYPE = "google_drive" - private const val KEY_GOOGLE_DRIVE_AUTH_TOKEN = "google_drive_auth_token" + private const val KEY_GOOGLE_DRIVE_REFRESH_TOKEN = "google_drive_refresh_token" + + const val WEB_CLIENT_ID = "223623398518-0sc2i5fsqliidcdoogn53iqltpktfnff.apps.googleusercontent.com" + const val WEB_CLIENT_SECRET = "BsZ0a06UgJf5hJOTI3fcxI2u" fun newInstance(preferences: SharedPreferences): GoogleDriveSyncProviderInfo? { - val accessToken = preferences.getString(KEY_GOOGLE_DRIVE_AUTH_TOKEN, null) ?: return null + val accessToken = preferences.getString(KEY_GOOGLE_DRIVE_REFRESH_TOKEN, null) ?: return null return GoogleDriveSyncProviderInfo(accessToken) } } diff --git a/twidere/src/google/kotlin/org/mariotaku/twidere/util/sync/dropbox/DropboxDraftsSyncAction.kt b/twidere/src/google/kotlin/org/mariotaku/twidere/util/sync/dropbox/DropboxDraftsSyncAction.kt index de3debf47..bc1ddcc55 100644 --- a/twidere/src/google/kotlin/org/mariotaku/twidere/util/sync/dropbox/DropboxDraftsSyncAction.kt +++ b/twidere/src/google/kotlin/org/mariotaku/twidere/util/sync/dropbox/DropboxDraftsSyncAction.kt @@ -15,7 +15,7 @@ import org.mariotaku.twidere.util.sync.FileBasedDraftsSyncAction import java.io.IOException import java.util.* -class DropboxDraftsSyncAction( +internal class DropboxDraftsSyncAction( context: Context, val client: DbxClientV2 ) : FileBasedDraftsSyncAction(context) { diff --git a/twidere/src/google/kotlin/org/mariotaku/twidere/util/sync/google/CloseableAny.kt b/twidere/src/google/kotlin/org/mariotaku/twidere/util/sync/google/CloseableAny.kt new file mode 100644 index 000000000..9e13f526d --- /dev/null +++ b/twidere/src/google/kotlin/org/mariotaku/twidere/util/sync/google/CloseableAny.kt @@ -0,0 +1,15 @@ +package org.mariotaku.twidere.util.sync.google + +import java.io.Closeable + +/** + * Created by mariotaku on 1/21/17. + */ + +internal class CloseableAny(val obj: T) : Closeable { + override fun close() { + if (obj is Closeable) { + obj.close() + } + } +} diff --git a/twidere/src/google/kotlin/org/mariotaku/twidere/util/sync/google/DriveFileInfo.kt b/twidere/src/google/kotlin/org/mariotaku/twidere/util/sync/google/DriveFileInfo.kt new file mode 100644 index 000000000..2a2067418 --- /dev/null +++ b/twidere/src/google/kotlin/org/mariotaku/twidere/util/sync/google/DriveFileInfo.kt @@ -0,0 +1,5 @@ +package org.mariotaku.twidere.util.sync.google + +import java.util.* + +internal data class DriveFileInfo(val fileId: String, val name: String, val modifiedDate: Date) \ No newline at end of file diff --git a/twidere/src/google/kotlin/org/mariotaku/twidere/util/sync/google/GoogleDriveDraftsSyncAction.kt b/twidere/src/google/kotlin/org/mariotaku/twidere/util/sync/google/GoogleDriveDraftsSyncAction.kt index cec277a1d..545130707 100644 --- a/twidere/src/google/kotlin/org/mariotaku/twidere/util/sync/google/GoogleDriveDraftsSyncAction.kt +++ b/twidere/src/google/kotlin/org/mariotaku/twidere/util/sync/google/GoogleDriveDraftsSyncAction.kt @@ -3,68 +3,62 @@ package org.mariotaku.twidere.util.sync.google import android.content.Context import com.google.api.client.util.DateTime import com.google.api.services.drive.Drive -import com.google.api.services.drive.model.File import org.mariotaku.twidere.extension.model.filename import org.mariotaku.twidere.extension.model.readMimeMessageFrom +import org.mariotaku.twidere.extension.model.writeMimeMessageTo import org.mariotaku.twidere.model.Draft +import org.mariotaku.twidere.util.io.DirectByteArrayOutputStream import org.mariotaku.twidere.util.sync.FileBasedDraftsSyncAction import java.io.IOException import java.util.* -class GoogleDriveDraftsSyncAction( + +internal class GoogleDriveDraftsSyncAction( context: Context, val drive: Drive -) : FileBasedDraftsSyncAction(context) { +) : FileBasedDraftsSyncAction(context) { + + val draftsDirName = "Drafts" + val draftMimeType = "message/rfc822" + + private lateinit var folderId: String + private val files = drive.files() + @Throws(IOException::class) override fun Draft.saveToRemote(): DriveFileInfo { - try { - val filename = "/Drafts/$filename" - val modifiedTime = DateTime(timestamp) - val create = drive.files().create(File().setOriginalFilename(filename).setModifiedTime(modifiedTime)) - val file = create.execute() - return DriveFileInfo(file.id, file.originalFilename, Date(timestamp)) - } catch (e: Exception) { - throw IOException(e) - } + val os = DirectByteArrayOutputStream() + this.writeMimeMessageTo(context, os) + val file = files.updateOrCreate(filename, draftMimeType, folderId, stream = os.inputStream(true), fileConfig = { + it.modifiedTime = DateTime(timestamp) + }) + return DriveFileInfo(file.id, file.name, Date(timestamp)) } @Throws(IOException::class) override fun Draft.loadFromRemote(info: DriveFileInfo): Boolean { - try { - val get = drive.files().get(info.fileId) - get.executeAsInputStream().use { - val parsed = this.readMimeMessageFrom(context, it) - if (parsed) { - this.timestamp = info.draftTimestamp - this.unique_id = info.draftFileName.substringBeforeLast(".eml") - } - return parsed + val get = files.get(info.fileId) + get.executeAsInputStream().use { + val parsed = this.readMimeMessageFrom(context, it) + if (parsed) { + this.timestamp = info.draftTimestamp + this.unique_id = info.draftFileName.substringBeforeLast(".eml") } - } catch (e: Exception) { - throw IOException(e) + return parsed } } @Throws(IOException::class) override fun removeDrafts(list: List): Boolean { - try { - list.forEach { info -> - drive.files().delete(info.fileId).execute() - } - return true - } catch (e: Exception) { - throw IOException(e) + list.forEach { info -> + files.delete(info.fileId).execute() } + return true } @Throws(IOException::class) override fun removeDraft(info: DriveFileInfo): Boolean { - try { - drive.files().delete(info.fileId).execute() - return true - } catch (e: Exception) { - throw IOException(e) - } + files.delete(info.fileId).execute() + return true } override val DriveFileInfo.draftTimestamp: Long get() = this.modifiedDate.time @@ -74,17 +68,25 @@ class GoogleDriveDraftsSyncAction( @Throws(IOException::class) override fun listRemoteDrafts(): List { val result = ArrayList() - try { - val list = drive.files().list() - list.execute().files.mapTo(result) { file -> - DriveFileInfo(file.id, file.originalFilename, Date(file.modifiedTime.value)) + var pageToken: String? + do { + val executeResult = files.list().apply { + q = "'$folderId' in parents and mimeType = '$draftMimeType' and trashed = false" + }.execute() + executeResult.files.filter { file -> + file.mimeType == draftMimeType + }.mapTo(result) { file -> + val lastModified = file.modifiedTime ?: file.createdTime + DriveFileInfo(file.id, file.name, Date(lastModified?.value ?: 0)) } - } catch (e: Exception) { - throw IOException(e) - } + pageToken = executeResult.nextPageToken + } while (pageToken != null) return result } - data class DriveFileInfo(val fileId: String, val name: String, val modifiedDate: Date) + override fun setup(): Boolean { + folderId = files.getOrCreate(draftsDirName, folderMimeType).id + return true + } } \ No newline at end of file diff --git a/twidere/src/google/kotlin/org/mariotaku/twidere/util/sync/google/GoogleDriveFiltersDataSyncAction.kt b/twidere/src/google/kotlin/org/mariotaku/twidere/util/sync/google/GoogleDriveFiltersDataSyncAction.kt new file mode 100644 index 000000000..1f6a36d77 --- /dev/null +++ b/twidere/src/google/kotlin/org/mariotaku/twidere/util/sync/google/GoogleDriveFiltersDataSyncAction.kt @@ -0,0 +1,67 @@ +package org.mariotaku.twidere.util.sync.google + +import android.content.Context +import com.google.api.services.drive.Drive +import com.google.api.services.drive.model.File +import org.mariotaku.twidere.extension.model.initFields +import org.mariotaku.twidere.extension.model.parse +import org.mariotaku.twidere.extension.model.serialize +import org.mariotaku.twidere.extension.newPullParser +import org.mariotaku.twidere.extension.newSerializer +import org.mariotaku.twidere.model.FiltersData +import org.mariotaku.twidere.util.io.DirectByteArrayOutputStream +import org.mariotaku.twidere.util.sync.FileBasedFiltersDataSyncAction +import java.io.FileNotFoundException +import java.io.InputStream + +internal class GoogleDriveFiltersDataSyncAction( + context: Context, + val drive: Drive +) : FileBasedFiltersDataSyncAction, GoogleDriveUploadSession>(context) { + + private val fileName = "filters.xml" + + private lateinit var commonFolderId: String + private val files = drive.files() + + override fun newLoadFromRemoteSession(): CloseableAny { + val file = files.getOrNull(fileName, xmlMimeType, commonFolderId) ?: throw FileNotFoundException() + return CloseableAny(file) + } + + override fun CloseableAny.getRemoteLastModified(): Long { + return (obj.modifiedTime ?: obj.createdTime)?.value ?: 0 + } + + override fun CloseableAny.loadFromRemote(): FiltersData { + val data = FiltersData() + data.parse(files.get(obj.id).executeAsInputStream().newPullParser(charset = Charsets.UTF_8)) + data.initFields() + return data + } + + override fun GoogleDriveUploadSession.setRemoteLastModified(lastModified: Long) { + this.localModifiedTime = lastModified + } + + override fun GoogleDriveUploadSession.saveToRemote(data: FiltersData): Boolean { + return this.uploadData(data) + } + + override fun newSaveToRemoteSession(): GoogleDriveUploadSession { + return object : GoogleDriveUploadSession(fileName, commonFolderId, xmlMimeType, files) { + override fun FiltersData.toInputStream(): InputStream { + val os = DirectByteArrayOutputStream() + this.serialize(os.newSerializer(charset = Charsets.UTF_8, indent = true)) + return os.inputStream(true) + } + } + } + + + override fun setup(): Boolean { + commonFolderId = files.getOrCreate("Common", folderMimeType).id + return true + } + +} \ No newline at end of file diff --git a/twidere/src/google/kotlin/org/mariotaku/twidere/util/sync/google/GoogleDrivePreferencesValuesSyncAction.kt b/twidere/src/google/kotlin/org/mariotaku/twidere/util/sync/google/GoogleDrivePreferencesValuesSyncAction.kt new file mode 100644 index 000000000..9e8cf363e --- /dev/null +++ b/twidere/src/google/kotlin/org/mariotaku/twidere/util/sync/google/GoogleDrivePreferencesValuesSyncAction.kt @@ -0,0 +1,72 @@ +package org.mariotaku.twidere.util.sync.google + +import android.content.Context +import android.content.SharedPreferences +import com.dropbox.core.DbxDownloader +import com.dropbox.core.v2.DbxClientV2 +import com.dropbox.core.v2.files.FileMetadata +import com.dropbox.core.v2.files.UploadUploader +import com.google.api.services.drive.Drive +import com.google.api.services.drive.model.File +import org.mariotaku.twidere.extension.model.serialize +import org.mariotaku.twidere.extension.newPullParser +import org.mariotaku.twidere.extension.newSerializer +import org.mariotaku.twidere.model.FiltersData +import org.mariotaku.twidere.util.io.DirectByteArrayOutputStream +import org.mariotaku.twidere.util.sync.FileBasedPreferencesValuesSyncAction +import java.io.FileNotFoundException +import java.io.InputStream +import java.util.* + +internal class GoogleDrivePreferencesValuesSyncAction( + context: Context, + val drive: Drive, + preferences: SharedPreferences, + processor: Processor, + val fileName: String +) : FileBasedPreferencesValuesSyncAction, + GoogleDriveUploadSession>>(context, preferences, processor) { + + private lateinit var commonFolderId: String + + private val files = drive.files() + + override fun newLoadFromRemoteSession(): CloseableAny { + val file = files.getOrNull(fileName, xmlMimeType, commonFolderId) ?: throw FileNotFoundException() + return CloseableAny(file) + } + + override fun CloseableAny.getRemoteLastModified(): Long { + return (obj.modifiedTime ?: obj.createdTime)?.value ?: 0 + } + + override fun CloseableAny.loadFromRemote(): MutableMap { + val data = HashMap() + data.parse(files.get(obj.id).executeAsInputStream().newPullParser()) + return data + } + + override fun newSaveToRemoteSession(): GoogleDriveUploadSession> { + return object : GoogleDriveUploadSession>(fileName, commonFolderId, xmlMimeType, files) { + override fun Map.toInputStream(): InputStream { + val os = DirectByteArrayOutputStream() + this.serialize(os.newSerializer(charset = Charsets.UTF_8, indent = true)) + return os.inputStream(true) + } + } + } + + override fun GoogleDriveUploadSession>.saveToRemote(data: MutableMap): Boolean { + return this.uploadData(data) + } + + + override fun GoogleDriveUploadSession>.setRemoteLastModified(lastModified: Long) { + this.localModifiedTime = lastModified + } + + override fun setup(): Boolean { + commonFolderId = files.getOrCreate("Common", folderMimeType).id + return true + } +} \ No newline at end of file diff --git a/twidere/src/google/kotlin/org/mariotaku/twidere/util/sync/google/GoogleDriveSyncCommons.kt b/twidere/src/google/kotlin/org/mariotaku/twidere/util/sync/google/GoogleDriveSyncCommons.kt new file mode 100644 index 000000000..e8b11556f --- /dev/null +++ b/twidere/src/google/kotlin/org/mariotaku/twidere/util/sync/google/GoogleDriveSyncCommons.kt @@ -0,0 +1,82 @@ +package org.mariotaku.twidere.util.sync.google + +import com.google.api.client.googleapis.json.GoogleJsonResponseException +import com.google.api.client.http.InputStreamContent +import com.google.api.services.drive.Drive +import com.google.api.services.drive.model.File +import java.io.InputStream + +/** + * Created by mariotaku on 1/21/17. + */ + + +internal const val folderMimeType = "application/vnd.google-apps.folder" +internal const val xmlMimeType = "application/xml" + + +internal fun Drive.Files.getOrNull(name: String, mimeType: String?, parent: String? = "root", + trashed: Boolean = false): File? { + val find = list() + var query = "name = '$name'" + if (parent != null) { + query += " and '$parent' in parents" + } + if (mimeType != null) { + query += " and mimeType = '$mimeType'" + } + query += " and trashed = $trashed" + find.q = query + try { + return find.execute().files.firstOrNull() + } catch (e: GoogleJsonResponseException) { + if (e.statusCode == 404) { + return null + } else { + throw e + } + } +} + +internal fun Drive.Files.getOrCreate(name: String, mimeType: String, parent: String = "root", + trashed: Boolean = false): File { + return getOrNull(name, mimeType, parent, trashed) ?: run { + val fileMetadata = File() + fileMetadata.name = name + fileMetadata.mimeType = mimeType + fileMetadata.parents = listOf(parent) + return@run create(fileMetadata).execute() + } +} + +internal fun Drive.Files.updateOrCreate( + name: String, + mimeType: String, + parent: String = "root", + trashed: Boolean = false, + stream: InputStream, + fileConfig: ((file: File) -> Unit)? = null +): File { + return run { + val find = list() + find.q = "name = '$name' and '$parent' in parents and mimeType = '$mimeType' and trashed = $trashed" + try { + val file = find.execute().files.firstOrNull() ?: return@run null + fileConfig?.invoke(file) + return@run update(file.id, file, InputStreamContent(mimeType, stream)).execute() + } catch (e: GoogleJsonResponseException) { + if (e.statusCode == 404) { + return@run null + } else { + throw e + } + } + } ?: run { + val file = File() + file.name = name + file.mimeType = mimeType + file.parents = listOf(parent) + fileConfig?.invoke(file) + return@run create(file, InputStreamContent(mimeType, stream)).execute() + } +} \ No newline at end of file diff --git a/twidere/src/google/kotlin/org/mariotaku/twidere/util/sync/google/GoogleDriveSyncTaskRunner.kt b/twidere/src/google/kotlin/org/mariotaku/twidere/util/sync/google/GoogleDriveSyncTaskRunner.kt index f7b8fd2dc..a5bd3ac38 100644 --- a/twidere/src/google/kotlin/org/mariotaku/twidere/util/sync/google/GoogleDriveSyncTaskRunner.kt +++ b/twidere/src/google/kotlin/org/mariotaku/twidere/util/sync/google/GoogleDriveSyncTaskRunner.kt @@ -8,27 +8,42 @@ import com.google.api.services.drive.Drive import nl.komponents.kovenant.task import nl.komponents.kovenant.ui.failUi import nl.komponents.kovenant.ui.successUi +import org.mariotaku.twidere.model.sync.GoogleDriveSyncProviderInfo import org.mariotaku.twidere.util.TaskServiceRunner import org.mariotaku.twidere.util.sync.ISyncAction import org.mariotaku.twidere.util.sync.SyncTaskRunner +import org.mariotaku.twidere.util.sync.UserColorsSyncProcessor +import org.mariotaku.twidere.util.sync.UserNicknamesSyncProcessor +import org.mariotaku.twidere.util.sync.dropbox.DropboxPreferencesValuesSyncAction /** * Created by mariotaku on 2017/1/6. */ -class GoogleDriveSyncTaskRunner(context: Context, val accessToken: String) : SyncTaskRunner(context) { +class GoogleDriveSyncTaskRunner(context: Context, val refreshToken: String) : SyncTaskRunner(context) { override fun onRunningTask(action: String, callback: (Boolean) -> Unit): Boolean { - val transport = NetHttpTransport.Builder().build() - val credential = GoogleCredential().setAccessToken(accessToken) - val drive = Drive.Builder(transport, JacksonFactory.getDefaultInstance(), credential).build() + val httpTransport = NetHttpTransport.Builder().build() + val jsonFactory = JacksonFactory.getDefaultInstance() + val credential = GoogleCredential.Builder() + .setTransport(httpTransport) + .setJsonFactory(jsonFactory) + .setClientSecrets(GoogleDriveSyncProviderInfo.WEB_CLIENT_ID, GoogleDriveSyncProviderInfo.WEB_CLIENT_SECRET) + .build() + credential.refreshToken = refreshToken + val drive = Drive.Builder(httpTransport, JacksonFactory.getDefaultInstance(), credential).build() val syncAction: ISyncAction = when (action) { TaskServiceRunner.ACTION_SYNC_DRAFTS -> GoogleDriveDraftsSyncAction(context, drive) + TaskServiceRunner.ACTION_SYNC_FILTERS -> GoogleDriveFiltersDataSyncAction(context, drive) + TaskServiceRunner.ACTION_SYNC_USER_COLORS -> GoogleDrivePreferencesValuesSyncAction(context, + drive, userColorNameManager.colorPreferences, UserColorsSyncProcessor, + "user_colors.xml") + TaskServiceRunner.ACTION_SYNC_USER_NICKNAMES -> GoogleDrivePreferencesValuesSyncAction(context, + drive, userColorNameManager.nicknamePreferences, UserNicknamesSyncProcessor, + "user_nicknames.xml") else -> null } ?: return false task { - val about = drive.about().get().execute() - println(about) syncAction.execute() }.successUi { callback(true) diff --git a/twidere/src/google/kotlin/org/mariotaku/twidere/util/sync/google/GoogleDriveUploadSession.kt b/twidere/src/google/kotlin/org/mariotaku/twidere/util/sync/google/GoogleDriveUploadSession.kt new file mode 100644 index 000000000..56bd5e091 --- /dev/null +++ b/twidere/src/google/kotlin/org/mariotaku/twidere/util/sync/google/GoogleDriveUploadSession.kt @@ -0,0 +1,34 @@ +package org.mariotaku.twidere.util.sync.google + +import com.dropbox.core.v2.files.UploadUploader +import com.google.api.client.util.DateTime +import com.google.api.services.drive.Drive +import java.io.Closeable +import java.io.IOException +import java.io.InputStream + +abstract internal class GoogleDriveUploadSession( + val name: String, + val parentId: String, + val mimeType: String, + val files: Drive.Files +) : Closeable { + private var uploader: UploadUploader? = null + + var localModifiedTime: Long = 0 + + override fun close() { + uploader?.close() + } + + @Throws(IOException::class) + abstract fun Data.toInputStream(): InputStream + + fun uploadData(data: Data): Boolean { + files.updateOrCreate(name, mimeType, parentId, stream = data.toInputStream(), fileConfig = { + it.modifiedTime = DateTime(localModifiedTime) + }) + return true + } + +} \ No newline at end of file diff --git a/twidere/src/main/kotlin/org/mariotaku/twidere/task/twitter/UpdateStatusTask.kt b/twidere/src/main/kotlin/org/mariotaku/twidere/task/twitter/UpdateStatusTask.kt index 8e93ab0cf..8089683d2 100644 --- a/twidere/src/main/kotlin/org/mariotaku/twidere/task/twitter/UpdateStatusTask.kt +++ b/twidere/src/main/kotlin/org/mariotaku/twidere/task/twitter/UpdateStatusTask.kt @@ -42,6 +42,7 @@ import org.mariotaku.twidere.provider.TwidereDataStore.Drafts import org.mariotaku.twidere.util.* import org.mariotaku.twidere.util.dagger.GeneralComponentHelper import org.mariotaku.twidere.util.io.ContentLengthInputStream +import org.mariotaku.twidere.util.io.DirectByteArrayOutputStream import java.io.* import java.util.* import java.util.concurrent.TimeUnit @@ -744,27 +745,7 @@ class UpdateStatusTask( return Pair(FileBody(cis, "attachment", cis.length(), contentType), size) } - internal class DirectByteArrayOutputStream : ByteArrayOutputStream { - constructor() : super() - constructor(size: Int) : super(size) - - fun inputStream(close: Boolean): InputStream { - return DirectInputStream(this, close) - } - - internal class DirectInputStream( - val os: DirectByteArrayOutputStream, - val close: Boolean - ) : ByteArrayInputStream(os.buf, 0, os.count) { - override fun close() { - if (close) { - os.close() - } - super.close() - } - } - } - } + } diff --git a/twidere/src/main/kotlin/org/mariotaku/twidere/util/io/DirectByteArrayOutputStream.kt b/twidere/src/main/kotlin/org/mariotaku/twidere/util/io/DirectByteArrayOutputStream.kt new file mode 100644 index 000000000..d41fd9b14 --- /dev/null +++ b/twidere/src/main/kotlin/org/mariotaku/twidere/util/io/DirectByteArrayOutputStream.kt @@ -0,0 +1,24 @@ +package org.mariotaku.twidere.util.io + +import java.io.ByteArrayInputStream +import java.io.ByteArrayOutputStream +import java.io.InputStream + +class DirectByteArrayOutputStream : ByteArrayOutputStream() { + + fun inputStream(close: Boolean): InputStream { + return DirectInputStream(this, close) + } + + internal class DirectInputStream( + val os: DirectByteArrayOutputStream, + val close: Boolean + ) : ByteArrayInputStream(os.buf, 0, os.count) { + override fun close() { + if (close) { + os.close() + } + super.close() + } + } +} \ No newline at end of file diff --git a/twidere/src/main/kotlin/org/mariotaku/twidere/util/sync/FileBasedDraftsSyncAction.kt b/twidere/src/main/kotlin/org/mariotaku/twidere/util/sync/FileBasedDraftsSyncAction.kt index d1765987c..298afef65 100644 --- a/twidere/src/main/kotlin/org/mariotaku/twidere/util/sync/FileBasedDraftsSyncAction.kt +++ b/twidere/src/main/kotlin/org/mariotaku/twidere/util/sync/FileBasedDraftsSyncAction.kt @@ -30,6 +30,11 @@ abstract class FileBasedDraftsSyncAction(val context: Context) : if (BuildConfig.DEBUG) { Log.d(LOGTAG_SYNC, "Begin syncing drafts") } + + if (!setup()) { + return false + } + val syncDataDir: File = context.syncDataDir.mkdirIfNotExists() ?: return false val snapshotsListFile = File(syncDataDir, "draft_ids.list") @@ -211,4 +216,7 @@ abstract class FileBasedDraftsSyncAction(val context: Context) : abstract val RemoteFileInfo.draftFileName: String abstract val RemoteFileInfo.draftTimestamp: Long + + @Throws(IOException::class) + open fun setup(): Boolean = true } diff --git a/twidere/src/main/kotlin/org/mariotaku/twidere/util/sync/SingleFileBasedDataSyncAction.kt b/twidere/src/main/kotlin/org/mariotaku/twidere/util/sync/SingleFileBasedDataSyncAction.kt index 72ea560c5..054174c71 100644 --- a/twidere/src/main/kotlin/org/mariotaku/twidere/util/sync/SingleFileBasedDataSyncAction.kt +++ b/twidere/src/main/kotlin/org/mariotaku/twidere/util/sync/SingleFileBasedDataSyncAction.kt @@ -16,6 +16,11 @@ abstract class SingleFileBasedDataSyncAction - this.runTask(action) + val actions = TaskServiceRunner.ACTIONS_SYNC.toCollection(LinkedList()) + val runnable = object : Runnable { + override fun run() { + val action = actions.poll() ?: return + runTask(action) { + this.run() + } + } } + runnable.run() } companion object {