Licensed to the Apache Software Foundation (ASF) under one or more contributor license + * agreements. See the NOTICE file distributed with this work for additional information regarding + * copyright ownership. The ASF licenses this file to You under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance with the License. You may obtain a + * copy of the License at + * + *
http://www.apache.org/licenses/LICENSE-2.0 + * + *
Unless required by applicable law or agreed to in writing, software distributed under the + * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.obd.graphs.integrations.gcp.authorization + +import android.content.Context +import android.util.Log +import com.google.android.gms.auth.api.identity.AuthorizationRequest +import com.google.android.gms.auth.api.identity.Identity +import com.google.android.gms.common.api.Scope +import com.google.api.services.drive.DriveScopes +import kotlinx.coroutines.tasks.await + +private const val TAG = "SilentAuth" + +internal object SilentAuthorization { + + suspend fun getAccessTokenSilently(context: Context): String? { + return try { + val authorizationClient = Identity.getAuthorizationClient(context) + val request = AuthorizationRequest.Builder() + .setRequestedScopes(listOf(Scope(DriveScopes.DRIVE_FILE), Scope(DriveScopes.DRIVE_APPDATA))) + .build() + + val result = authorizationClient.authorize(request).await() + + if (result.hasResolution()) { + Log.w(TAG, "Silent auth failed: UI resolution required.") + null // We cannot show UI in the background + } else { + Log.i(TAG, "Silent auth successful.") + result.accessToken + } + } catch (e: Exception) { + Log.e(TAG, "Silent auth threw an exception", e) + null + } + } +} diff --git a/integrations/src/main/java/org/obd/graphs.integrations/gcp/gdrive/AbstractDriveManager.kt b/integrations/src/main/java/org/obd/graphs.integrations/gcp/gdrive/AbstractDriveManager.kt index 791239cd..94ae5b29 100644 --- a/integrations/src/main/java/org/obd/graphs.integrations/gcp/gdrive/AbstractDriveManager.kt +++ b/integrations/src/main/java/org/obd/graphs.integrations/gcp/gdrive/AbstractDriveManager.kt @@ -23,7 +23,6 @@ import com.google.android.gms.auth.GoogleAuthUtil import com.google.android.gms.common.api.Scope import com.google.api.client.googleapis.json.GoogleJsonResponseException import com.google.api.client.http.AbstractInputStreamContent -import com.google.api.client.http.FileContent import com.google.api.client.http.HttpRequestInitializer import com.google.api.client.http.javanet.NetHttpTransport import com.google.api.client.json.gson.GsonFactory @@ -32,10 +31,8 @@ import com.google.api.services.drive.DriveScopes import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext import org.obd.graphs.integrations.gcp.authorization.AuthorizationManager -import java.io.File import java.io.FileNotFoundException import java.io.InputStream -import com.google.api.services.drive.model.File as DriveFile private const val APP_NAME = "MyGiuliaBackup" private const val TAG = "AbstractDriveManager" @@ -108,98 +105,6 @@ internal abstract class AbstractDriveManager( } } - fun Drive.uploadFile( - content: InputStreamContent, - parentFolderId: String - ): DriveFile { - Log.i(TAG, "Uploading file ${content.fileName}") - val metadata = - DriveFile().apply { - name = content.fileName - parents = listOf(parentFolderId) - } - - val uploaded = - this - .files() - .create(metadata, content) - .setFields("id") - .execute() - - Log.i(TAG, "Uploaded ${content.fileName}, ID: ${uploaded.id}") - return uploaded - } - - fun Drive.uploadFile( - localFile: File, - fileName: String, - parentFolderId: String, - mimeType: String = "text/plain" - ): DriveFile { - Log.i(TAG, "Uploading file ${localFile.absolutePath} to $fileName") - val metadata = - DriveFile().apply { - name = fileName - parents = listOf(parentFolderId) - } - val content = FileContent(mimeType, localFile) - - val uploaded = - this - .files() - .create(metadata, content) - .setFields("id") - .execute() - - Log.i(TAG, "Uploaded ${localFile.name}, ID: ${uploaded.id} as $fileName") - return uploaded - } - - protected fun Drive.findFolderIdRecursive(path: String): String { - val folderNames = path.split("/").filter { it.isNotEmpty() } - var currentParentId = "root" - for (folderName in folderNames) { - currentParentId = findOrCreateSingleFolder(this, folderName, currentParentId) - } - return currentParentId - } - - private fun findOrCreateSingleFolder( - drive: Drive, - folderName: String, - parentId: String - ): String { - val query = - "mimeType = 'application/vnd.google-apps.folder' and name = '$folderName' and '$parentId' in parents and trashed = false" - - val files = - drive - .files() - .list() - .setQ(query) - .setSpaces("drive") - .setFields("files(id, name)") - .execute() - .files - - return if (files.isNotEmpty()) { - files.first().id - } else { - val metadata = - DriveFile().apply { - name = folderName - mimeType = "application/vnd.google-apps.folder" - parents = listOf(parentId) - } - drive - .files() - .create(metadata) - .setFields("id") - .execute() - .id - } - } - private fun credentials(accessToken: String): HttpRequestInitializer = HttpRequestInitializer { request -> request.headers.authorization = "Bearer $accessToken" diff --git a/integrations/src/main/java/org/obd/graphs.integrations/gcp/gdrive/DefaultDriveBackupManager.kt b/integrations/src/main/java/org/obd/graphs.integrations/gcp/gdrive/DefaultDriveBackupManager.kt index a18a0990..18e8151b 100644 --- a/integrations/src/main/java/org/obd/graphs.integrations/gcp/gdrive/DefaultDriveBackupManager.kt +++ b/integrations/src/main/java/org/obd/graphs.integrations/gcp/gdrive/DefaultDriveBackupManager.kt @@ -24,6 +24,8 @@ import org.obd.graphs.BACKUP_RESTORE_NO_FILES import org.obd.graphs.BACKUP_RESTORE_SUCCESSFUL import org.obd.graphs.BACKUP_SUCCESSFUL import org.obd.graphs.SCREEN_UNLOCK_PROGRESS_EVENT +import org.obd.graphs.integrations.gcp.gdrive.DriveHelper.findFolderIdRecursive +import org.obd.graphs.integrations.gcp.gdrive.DriveHelper.uploadFile import org.obd.graphs.sendBroadcastEvent import java.io.File import java.io.FileOutputStream diff --git a/integrations/src/main/java/org/obd/graphs.integrations/gcp/gdrive/DefaultTripLogDriveManager.kt b/integrations/src/main/java/org/obd/graphs.integrations/gcp/gdrive/DefaultTripLogDriveManager.kt deleted file mode 100644 index 4a3e2330..00000000 --- a/integrations/src/main/java/org/obd/graphs.integrations/gcp/gdrive/DefaultTripLogDriveManager.kt +++ /dev/null @@ -1,102 +0,0 @@ -/* - * Copyright 2019-2026, Tomasz Żebrowski - * - *
Licensed to the Apache Software Foundation (ASF) under one or more contributor license - * agreements. See the NOTICE file distributed with this work for additional information regarding - * copyright ownership. The ASF licenses this file to You under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance with the License. You may obtain a - * copy of the License at - * - *
http://www.apache.org/licenses/LICENSE-2.0 - * - *
Unless required by applicable law or agreed to in writing, software distributed under the
- * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
- * express or implied. See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package org.obd.graphs.integrations.gcp.gdrive
-
-import android.app.Activity
-import androidx.fragment.app.Fragment
-import org.obd.graphs.SCREEN_UNLOCK_PROGRESS_EVENT
-import org.obd.graphs.TRIPS_UPLOAD_FAILED
-import org.obd.graphs.TRIPS_UPLOAD_NO_FILES_SELECTED
-import org.obd.graphs.TRIPS_UPLOAD_SUCCESSFUL
-import org.obd.graphs.bl.datalogger.DataLoggerRepository
-import org.obd.graphs.bl.datalogger.scaleToRange
-import org.obd.graphs.bl.trip.TripDescParser
-import org.obd.graphs.integrations.log.OutputType
-import org.obd.graphs.integrations.log.TripLog
-import org.obd.graphs.sendBroadcastEvent
-import java.io.File
-import java.util.zip.GZIPOutputStream
-
-internal open class DefaultTripLogDriveManager(
- webClientId: String,
- activity: Activity,
- fragment: Fragment?
-) : AbstractDriveManager(webClientId, activity, fragment),
- TripLogDriveManager {
- override suspend fun exportTrips(files: List Licensed to the Apache Software Foundation (ASF) under one or more contributor license
+ * agreements. See the NOTICE file distributed with this work for additional information regarding
+ * copyright ownership. The ASF licenses this file to You under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance with the License. You may obtain a
+ * copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the
+ * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
+ * express or implied. See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.obd.graphs.integrations.gcp.gdrive
+
+import android.util.Log
+import com.google.api.client.http.FileContent
+import com.google.api.services.drive.Drive
+import java.io.File
+import com.google.api.services.drive.model.File as DriveFile
+
+private const val TAG = "DriveHelper"
+
+internal object DriveHelper {
+
+ fun Drive.findFolderIdRecursive(path: String): String {
+ val folderNames = path.split("/").filter { it.isNotEmpty() }
+ var currentParentId = "root"
+ for (folderName in folderNames) {
+ currentParentId = findOrCreateSingleFolder(folderName, currentParentId)
+ }
+ return currentParentId
+ }
+
+ fun Drive.findOrCreateSingleFolder(folderName: String, parentId: String): String {
+ val query = "mimeType = 'application/vnd.google-apps.folder' and name = '$folderName' and '$parentId' in parents and trashed = false"
+
+ val files = this.files().list()
+ .setQ(query)
+ .setSpaces("drive")
+ .setFields("files(id, name)")
+ .execute()
+ .files
+
+ return if (files.isNotEmpty()) {
+ files.first().id
+ } else {
+ val metadata = DriveFile().apply {
+ name = folderName
+ mimeType = "application/vnd.google-apps.folder"
+ parents = listOf(parentId)
+ }
+ this.files().create(metadata).setFields("id").execute().id
+ }
+ }
+
+ fun Drive.uploadFile(
+ localFile: File,
+ fileName: String,
+ parentFolderId: String,
+ mimeType: String = "text/plain"
+ ): DriveFile {
+ Log.i(TAG, "Uploading file ${localFile.absolutePath} to $fileName")
+ val metadata = DriveFile().apply {
+ name = fileName
+ parents = listOf(parentFolderId)
+ }
+ val content = FileContent(mimeType, localFile)
+
+ val uploaded = this.files().create(metadata, content).setFields("id").execute()
+
+ Log.i(TAG, "Uploaded ${localFile.name}, ID: ${uploaded.id} as $fileName")
+ return uploaded
+ }
+}
diff --git a/integrations/src/main/java/org/obd/graphs.integrations/gcp/gdrive/ManualTripLogUpload.kt b/integrations/src/main/java/org/obd/graphs.integrations/gcp/gdrive/ManualTripLogUpload.kt
new file mode 100644
index 00000000..61ebaad1
--- /dev/null
+++ b/integrations/src/main/java/org/obd/graphs.integrations/gcp/gdrive/ManualTripLogUpload.kt
@@ -0,0 +1,73 @@
+/*
+ * Copyright 2019-2026, Tomasz Żebrowski
+ *
+ * Licensed to the Apache Software Foundation (ASF) under one or more contributor license
+ * agreements. See the NOTICE file distributed with this work for additional information regarding
+ * copyright ownership. The ASF licenses this file to You under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance with the License. You may obtain a
+ * copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the
+ * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
+ * express or implied. See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.obd.graphs.integrations.gcp.gdrive
+
+import android.app.Activity
+import androidx.fragment.app.Fragment
+import org.obd.graphs.SCREEN_UNLOCK_PROGRESS_EVENT
+import org.obd.graphs.TRIPS_UPLOAD_FAILED
+import org.obd.graphs.TRIPS_UPLOAD_NO_FILES_SELECTED
+import org.obd.graphs.TRIPS_UPLOAD_SUCCESSFUL
+import org.obd.graphs.bl.trip.TripDescParser
+import org.obd.graphs.integrations.gcp.gdrive.DriveHelper.findFolderIdRecursive
+import org.obd.graphs.sendBroadcastEvent
+import java.io.File
+
+internal open class ManualTripLogUpload(
+ webClientId: String,
+ activity: Activity,
+ fragment: Fragment?
+) : AbstractDriveManager(webClientId, activity, fragment),
+ TripLogDriveManager {
+ override suspend fun uploadTrips(files: List Licensed to the Apache Software Foundation (ASF) under one or more contributor license
+ * agreements. See the NOTICE file distributed with this work for additional information regarding
+ * copyright ownership. The ASF licenses this file to You under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance with the License. You may obtain a
+ * copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the
+ * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
+ * express or implied. See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.obd.graphs.integrations.gcp.gdrive
+
+import android.content.Context
+import android.util.Log
+import androidx.work.Constraints
+import androidx.work.CoroutineWorker
+import androidx.work.ExistingWorkPolicy
+import androidx.work.NetworkType
+import androidx.work.OneTimeWorkRequestBuilder
+import androidx.work.WorkManager
+import androidx.work.WorkerParameters
+import com.google.android.gms.auth.GoogleAuthUtil
+import com.google.api.client.googleapis.json.GoogleJsonResponseException
+import com.google.api.client.http.HttpRequestInitializer
+import com.google.api.client.http.javanet.NetHttpTransport
+import com.google.api.client.json.gson.GsonFactory
+import com.google.api.services.drive.Drive
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.withContext
+import org.obd.graphs.bl.datalogger.DataLoggerRepository
+import org.obd.graphs.bl.datalogger.scaleToRange
+import org.obd.graphs.bl.trip.TripDescParser
+import org.obd.graphs.bl.trip.tripManager
+import org.obd.graphs.integrations.gcp.authorization.SilentAuthorization
+import org.obd.graphs.integrations.gcp.gdrive.DriveHelper.findFolderIdRecursive
+import org.obd.graphs.integrations.log.OutputType
+import org.obd.graphs.integrations.log.TripLog
+import org.obd.graphs.preferences.Prefs
+import org.obd.graphs.preferences.isEnabled
+import java.io.File
+
+private const val LOG_TAG = "TripCloudSyncWorker"
+private const val SYNC_WORK_NAME = "TripCloudSync"
+
+object DriveSync {
+
+ fun start(context: Context) {
+ try {
+ val enabled = Prefs.isEnabled("pref.trips.drive.auto_sync")
+
+ Log.i(LOG_TAG, "Received trips drive auto-sync request, sync enabled=$enabled")
+
+ if (enabled) {
+ val constraints = Constraints.Builder()
+ .setRequiredNetworkType(NetworkType.UNMETERED)
+ .setRequiresBatteryNotLow(true)
+ .build()
+
+ val syncRequest = OneTimeWorkRequestBuilder Licensed to the Apache Software Foundation (ASF) under one or more contributor license
+ * agreements. See the NOTICE file distributed with this work for additional information regarding
+ * copyright ownership. The ASF licenses this file to You under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance with the License. You may obtain a
+ * copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the
+ * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
+ * express or implied. See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.obd.graphs.integrations.gcp.gdrive
+
+import com.google.api.services.drive.Drive
+import org.obd.graphs.bl.datalogger.DataLoggerRepository
+import org.obd.graphs.bl.datalogger.scaleToRange
+import org.obd.graphs.bl.trip.TripDescParser
+import org.obd.graphs.integrations.gcp.gdrive.DriveHelper.uploadFile
+import org.obd.graphs.integrations.log.OutputType
+import org.obd.graphs.integrations.log.TripLog
+import org.obd.graphs.integrations.log.TripLogTransformer
+import java.io.File
+import java.util.zip.GZIPOutputStream
+
+internal object TripUpload {
+
+ /**
+ * Builds the unified transformer that scales PID values.
+ * This is heavy, so it should only be called once per upload batch.
+ */
+ fun buildTransformer(): TripLogTransformer {
+ val definitions = DataLoggerRepository.getPidDefinitionRegistry().findAll()
+ val signalsMapper = definitions.associate { it.id.toInt() to it.description.replace("\n", " ") }
+ val pidMap = definitions.associateBy { it.id.toInt() }
+
+ return TripLog.transformer(OutputType.JSON, signalsMapper) { s, v ->
+ if (v is Number) {
+ (pidMap[s]?.scaleToRange(v.toFloat())) ?: v
+ } else {
+ v
+ }
+ }
+ }
+
+ /**
+ * Transforms, compresses, and uploads a single trip file to Google Drive.
+ */
+ fun Drive.transformAndUploadTrip(
+ inFile: File,
+ cacheDir: File,
+ folderId: String,
+ deviceId: String,
+ transformer: TripLogTransformer,
+ tripDescParser: TripDescParser
+ ) {
+ val metadata = mutableMapOf