implementing google drive sync
This commit is contained in:
parent
371fd70b24
commit
077184deae
|
@ -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'
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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<FileMetadata>(context) {
|
||||
|
|
|
@ -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<T>(val obj: T) : Closeable {
|
||||
override fun close() {
|
||||
if (obj is Closeable) {
|
||||
obj.close()
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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)
|
|
@ -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<GoogleDriveDraftsSyncAction.DriveFileInfo>(context) {
|
||||
) : FileBasedDraftsSyncAction<DriveFileInfo>(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<DriveFileInfo>): 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<DriveFileInfo> {
|
||||
val result = ArrayList<DriveFileInfo>()
|
||||
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
|
||||
}
|
||||
|
||||
}
|
|
@ -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<CloseableAny<File>, GoogleDriveUploadSession<FiltersData>>(context) {
|
||||
|
||||
private val fileName = "filters.xml"
|
||||
|
||||
private lateinit var commonFolderId: String
|
||||
private val files = drive.files()
|
||||
|
||||
override fun newLoadFromRemoteSession(): CloseableAny<File> {
|
||||
val file = files.getOrNull(fileName, xmlMimeType, commonFolderId) ?: throw FileNotFoundException()
|
||||
return CloseableAny(file)
|
||||
}
|
||||
|
||||
override fun CloseableAny<File>.getRemoteLastModified(): Long {
|
||||
return (obj.modifiedTime ?: obj.createdTime)?.value ?: 0
|
||||
}
|
||||
|
||||
override fun CloseableAny<File>.loadFromRemote(): FiltersData {
|
||||
val data = FiltersData()
|
||||
data.parse(files.get(obj.id).executeAsInputStream().newPullParser(charset = Charsets.UTF_8))
|
||||
data.initFields()
|
||||
return data
|
||||
}
|
||||
|
||||
override fun GoogleDriveUploadSession<FiltersData>.setRemoteLastModified(lastModified: Long) {
|
||||
this.localModifiedTime = lastModified
|
||||
}
|
||||
|
||||
override fun GoogleDriveUploadSession<FiltersData>.saveToRemote(data: FiltersData): Boolean {
|
||||
return this.uploadData(data)
|
||||
}
|
||||
|
||||
override fun newSaveToRemoteSession(): GoogleDriveUploadSession<FiltersData> {
|
||||
return object : GoogleDriveUploadSession<FiltersData>(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
|
||||
}
|
||||
|
||||
}
|
|
@ -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<CloseableAny<File>,
|
||||
GoogleDriveUploadSession<Map<String, String>>>(context, preferences, processor) {
|
||||
|
||||
private lateinit var commonFolderId: String
|
||||
|
||||
private val files = drive.files()
|
||||
|
||||
override fun newLoadFromRemoteSession(): CloseableAny<File> {
|
||||
val file = files.getOrNull(fileName, xmlMimeType, commonFolderId) ?: throw FileNotFoundException()
|
||||
return CloseableAny(file)
|
||||
}
|
||||
|
||||
override fun CloseableAny<File>.getRemoteLastModified(): Long {
|
||||
return (obj.modifiedTime ?: obj.createdTime)?.value ?: 0
|
||||
}
|
||||
|
||||
override fun CloseableAny<File>.loadFromRemote(): MutableMap<String, String> {
|
||||
val data = HashMap<String, String>()
|
||||
data.parse(files.get(obj.id).executeAsInputStream().newPullParser())
|
||||
return data
|
||||
}
|
||||
|
||||
override fun newSaveToRemoteSession(): GoogleDriveUploadSession<Map<String, String>> {
|
||||
return object : GoogleDriveUploadSession<Map<String, String>>(fileName, commonFolderId, xmlMimeType, files) {
|
||||
override fun Map<String, String>.toInputStream(): InputStream {
|
||||
val os = DirectByteArrayOutputStream()
|
||||
this.serialize(os.newSerializer(charset = Charsets.UTF_8, indent = true))
|
||||
return os.inputStream(true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun GoogleDriveUploadSession<Map<String, String>>.saveToRemote(data: MutableMap<String, String>): Boolean {
|
||||
return this.uploadData(data)
|
||||
}
|
||||
|
||||
|
||||
override fun GoogleDriveUploadSession<Map<String, String>>.setRemoteLastModified(lastModified: Long) {
|
||||
this.localModifiedTime = lastModified
|
||||
}
|
||||
|
||||
override fun setup(): Boolean {
|
||||
commonFolderId = files.getOrCreate("Common", folderMimeType).id
|
||||
return true
|
||||
}
|
||||
}
|
|
@ -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()
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
|
|
|
@ -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<in Data>(
|
||||
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
|
||||
}
|
||||
|
||||
}
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
|
@ -30,6 +30,11 @@ abstract class FileBasedDraftsSyncAction<RemoteFileInfo>(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<RemoteFileInfo>(val context: Context) :
|
|||
|
||||
abstract val RemoteFileInfo.draftFileName: String
|
||||
abstract val RemoteFileInfo.draftTimestamp: Long
|
||||
|
||||
@Throws(IOException::class)
|
||||
open fun setup(): Boolean = true
|
||||
}
|
||||
|
|
|
@ -16,6 +16,11 @@ abstract class SingleFileBasedDataSyncAction<Data, SnapshotStore, DownloadSessio
|
|||
if (BuildConfig.DEBUG) {
|
||||
Log.d(LOGTAG_SYNC, "Begin syncing $whatData")
|
||||
}
|
||||
|
||||
if (!setup()) {
|
||||
return false
|
||||
}
|
||||
|
||||
val snapshotStore = newSnapshotStore()
|
||||
|
||||
var remoteData: Data? = null
|
||||
|
@ -36,6 +41,9 @@ abstract class SingleFileBasedDataSyncAction<Data, SnapshotStore, DownloadSessio
|
|||
}
|
||||
}
|
||||
} catch (e: FileNotFoundException) {
|
||||
if (BuildConfig.DEBUG) {
|
||||
Log.d(LOGTAG_SYNC, "Remote $whatData doesn't exists, will upload new one")
|
||||
}
|
||||
shouldCreateRemote = true
|
||||
}
|
||||
|
||||
|
@ -148,6 +156,8 @@ abstract class SingleFileBasedDataSyncAction<Data, SnapshotStore, DownloadSessio
|
|||
|
||||
protected abstract fun Data.dataContentEquals(localData: Data): Boolean
|
||||
|
||||
protected open fun setup(): Boolean = true
|
||||
|
||||
protected open val whatData: String = "data"
|
||||
|
||||
}
|
||||
|
|
|
@ -6,6 +6,7 @@ import nl.komponents.kovenant.task
|
|||
import org.mariotaku.twidere.util.TaskServiceRunner
|
||||
import org.mariotaku.twidere.util.UserColorNameManager
|
||||
import org.mariotaku.twidere.util.dagger.GeneralComponentHelper
|
||||
import java.util.*
|
||||
import javax.inject.Inject
|
||||
|
||||
/**
|
||||
|
@ -54,9 +55,16 @@ abstract class SyncTaskRunner(val context: Context) {
|
|||
}
|
||||
|
||||
fun performSync() {
|
||||
TaskServiceRunner.ACTIONS_SYNC.forEach { action ->
|
||||
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 {
|
||||
|
|
Loading…
Reference in New Issue