diff --git a/app/src/main/java/net/opendasharchive/openarchive/db/MediaAdapter.kt b/app/src/main/java/net/opendasharchive/openarchive/db/MediaAdapter.kt index 48e42a16..a2e56611 100644 --- a/app/src/main/java/net/opendasharchive/openarchive/db/MediaAdapter.kt +++ b/app/src/main/java/net/opendasharchive/openarchive/db/MediaAdapter.kt @@ -76,11 +76,14 @@ class MediaAdapter( it, it.getString(R.string.upload_unsuccessful_description), R.string.upload_unsuccessful, R.drawable.ic_error, listOf( AlertHelper.positiveButton(R.string.retry) { _, _ -> - media[pos].sStatus = Media.Status.Queued - media[pos].statusMessage = "" - media[pos].save() - updateItem(media[pos].id) + media[pos].apply { + sStatus = Media.Status.Queued + statusMessage = "" + save() + + BroadcastManager.postChange(it, collectionId, id) + } UploadService.startUploadService(it) }, @@ -136,14 +139,16 @@ class MediaAdapter( holder.handle?.toggle(isEditMode) } - fun updateItem(mediaId: Long): Boolean { + fun updateItem(mediaId: Long, progress: Long): Boolean { val idx = media.indexOfFirst { it.id == mediaId } if (idx < 0) return false - val item = Media.get(mediaId) ?: return false - - media[idx] = item - + if (progress >= 0) { + media[idx].progress = progress + } else { + val item = Media.get(mediaId) ?: return false + media[idx] = item + } notifyItemChanged(idx) return true diff --git a/app/src/main/java/net/opendasharchive/openarchive/db/MediaViewHolder.kt b/app/src/main/java/net/opendasharchive/openarchive/db/MediaViewHolder.kt index 6dfcbdd9..74110417 100644 --- a/app/src/main/java/net/opendasharchive/openarchive/db/MediaViewHolder.kt +++ b/app/src/main/java/net/opendasharchive/openarchive/db/MediaViewHolder.kt @@ -28,6 +28,7 @@ import net.opendasharchive.openarchive.fragments.VideoRequestHandler import net.opendasharchive.openarchive.util.extensions.hide import net.opendasharchive.openarchive.util.extensions.show import timber.log.Timber +import java.io.InputStream import kotlin.math.roundToInt abstract class MediaViewHolder(protected val binding: ViewBinding): RecyclerView.ViewHolder(binding.root) { @@ -303,18 +304,19 @@ abstract class MediaViewHolder(protected val binding: ViewBinding): RecyclerView fileInfo?.text = Formatter.formatShortFileSize(mContext, file.length()) } else { if (media.contentLength == -1L) { + var iStream: InputStream? = null try { - val iStream = mContext.contentResolver.openInputStream(media.fileUri) + iStream = mContext.contentResolver.openInputStream(media.fileUri) if (iStream != null) { media.contentLength = iStream.available().toLong() media.save() - - iStream.close() } } catch (e: Throwable) { Timber.e(e) + } finally { + iStream?.close() } } diff --git a/app/src/main/java/net/opendasharchive/openarchive/features/main/MainMediaFragment.kt b/app/src/main/java/net/opendasharchive/openarchive/features/main/MainMediaFragment.kt index 51560572..ec1898a4 100644 --- a/app/src/main/java/net/opendasharchive/openarchive/features/main/MainMediaFragment.kt +++ b/app/src/main/java/net/opendasharchive/openarchive/features/main/MainMediaFragment.kt @@ -4,6 +4,8 @@ import android.content.BroadcastReceiver import android.content.Context import android.content.Intent import android.os.Bundle +import android.os.Handler +import android.os.Looper import android.view.LayoutInflater import android.view.MenuItem import android.view.View @@ -42,25 +44,26 @@ class MainMediaFragment : Fragment() { private var mAdapters = HashMap() private var mSection = HashMap() private var mProjectId = -1L - private var mCollections = ArrayList() + private var mCollections = mutableMapOf() private lateinit var mBinding: FragmentMainMediaBinding private val mMessageReceiver: BroadcastReceiver = object : BroadcastReceiver() { - + private val handler = Handler(Looper.getMainLooper()) override fun onReceive(context: Context, intent: Intent) { - val action = BroadcastManager.getAction(intent) - val mediaId = action?.mediaId ?: return - - if (mediaId < 0) return + val action = BroadcastManager.getAction(intent) ?: return when (action) { BroadcastManager.Action.Change -> { - updateItem(mediaId) + handler.post { + updateItem(action.collectionId, action.mediaId, action.progress) + } } BroadcastManager.Action.Delete -> { - refresh() + handler.post { + refresh() + } } } } @@ -109,36 +112,43 @@ class MainMediaFragment : Fragment() { mBinding = FragmentMainMediaBinding.inflate(inflater, container, false) - refresh() - return mBinding.root } - fun updateItem(mediaId: Long) { - for (adapter in mAdapters.values) { - if (adapter.updateItem(mediaId)) break + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + + refresh() + } + + fun updateItem(collectionId: Long, mediaId: Long, progress: Long) { + mAdapters[collectionId]?.apply { + updateItem(mediaId, progress) + mCollections[collectionId]?.let { collection -> + mSection[collectionId]?.setHeader(collection, media) + } } } fun refresh() { - mCollections = ArrayList(Collection.getByProject(mProjectId)) + mCollections = Collection.getByProject(mProjectId).associateBy { it.id }.toMutableMap() // Remove all sections, which' collections don't exist anymore. val toDelete = mAdapters.keys.filter { id -> - mCollections.firstOrNull { it.id == id } == null + mCollections.containsKey(id).not() }.toMutableList() - mCollections.forEach { collection -> + mCollections.forEach { (id, collection) -> val media = collection.media // Also remove all empty collections. if (media.isEmpty()) { - toDelete.add(collection.id) + toDelete.add(id) return@forEach } - val adapter = mAdapters[collection.id] - val holder = mSection[collection.id] + val adapter = mAdapters[id] + val holder = mSection[id] if (adapter != null) { adapter.updateData(media) @@ -154,22 +164,20 @@ class MainMediaFragment : Fragment() { // while adding images. deleteCollections(toDelete, false) - if (::mBinding.isInitialized) { - mBinding.addMediaHint.toggle(mCollections.isEmpty()) - } + mBinding.addMediaHint.toggle(mCollections.isEmpty()) } fun deleteSelected() { val toDelete = ArrayList() - mCollections.forEach { collection -> - if (mAdapters[collection.id]?.deleteSelected() == true) { + mCollections.forEach { (id, collection) -> + if (mAdapters[id]?.deleteSelected() == true) { val media = collection.media if (media.isEmpty()) { toDelete.add(collection.id) } else { - mSection[collection.id]?.setHeader(collection, media) + mSection[id]?.setHeader(collection, media) } } } @@ -208,12 +216,11 @@ class MainMediaFragment : Fragment() { val holder = mSection.remove(collectionId) (holder?.root?.parent as? ViewGroup)?.removeView(holder.root) - val idx = mCollections.indexOfFirst { it.id == collectionId } - - if (idx > -1 && idx < mCollections.size) { - val collection = mCollections.removeAt(idx) - - if (cleanup) collection.delete() + mCollections[collectionId]?.let { + mCollections.remove(collectionId) + if (cleanup) { + it.delete() + } } } } diff --git a/app/src/main/java/net/opendasharchive/openarchive/features/main/SectionViewHolder.kt b/app/src/main/java/net/opendasharchive/openarchive/features/main/SectionViewHolder.kt index 47391c38..eaba7555 100644 --- a/app/src/main/java/net/opendasharchive/openarchive/features/main/SectionViewHolder.kt +++ b/app/src/main/java/net/opendasharchive/openarchive/features/main/SectionViewHolder.kt @@ -37,7 +37,7 @@ data class SectionViewHolder( collection: Collection, media: List ) { - if (media.firstOrNull { it.isUploading } != null) + if (media.any { it.isUploading }) { timestamp.setText(R.string.uploading) diff --git a/app/src/main/java/net/opendasharchive/openarchive/services/Conduit.kt b/app/src/main/java/net/opendasharchive/openarchive/services/Conduit.kt index 95909af3..83cdd285 100644 --- a/app/src/main/java/net/opendasharchive/openarchive/services/Conduit.kt +++ b/app/src/main/java/net/opendasharchive/openarchive/services/Conduit.kt @@ -90,15 +90,15 @@ abstract class Conduit( * result is a site specific unique id that we can use to fetch the data, * build an embed tag, etc. for some sites this might be a URL */ - fun jobSucceeded() { - mMedia.progress = 100 + fun jobSucceeded() { + mMedia.progress = mMedia.contentLength mMedia.sStatus = Media.Status.Uploaded mMedia.save() - BroadcastManager.postChange(mContext, mMedia.id) + BroadcastManager.postChange(mContext, mMedia.collectionId, mMedia.id) } - fun jobFailed(exception: Throwable) { + fun jobFailed(exception: Throwable) { // If an upload was cancelled, ignore the error. if (mCancelled) return @@ -109,25 +109,14 @@ abstract class Conduit( Timber.d(exception) - BroadcastManager.postChange(mContext, mMedia.id) + BroadcastManager.postChange(mContext, mMedia.collectionId, mMedia.id) } - // track when the last progress broadcast was sent, timestamp - // we use this to limit the rate of sending out these broadcasts - private var lastProgressBroadcast = 0L + fun jobProgress(uploadedBytes: Long) { + mMedia.progress = uploadedBytes - fun jobProgress(uploadedBytes: Long) { - // making sure we're not writing to the database more often than (1000/150=)~7 times a second. - // jobProgress is getting called up to several hundred times a second. - if (System.currentTimeMillis() > lastProgressBroadcast + 150) { - lastProgressBroadcast = System.currentTimeMillis() - - mMedia.progress = uploadedBytes - mMedia.save() - - BroadcastManager.postChange(mContext, mMedia.id) - } - } + BroadcastManager.postProgress(mContext, mMedia.collectionId, mMedia.id, uploadedBytes) + } /** * workaround to deal with some quirks in our data model? diff --git a/app/src/main/java/net/opendasharchive/openarchive/services/SaveClient.kt b/app/src/main/java/net/opendasharchive/openarchive/services/SaveClient.kt index 95802892..bb217a70 100644 --- a/app/src/main/java/net/opendasharchive/openarchive/services/SaveClient.kt +++ b/app/src/main/java/net/opendasharchive/openarchive/services/SaveClient.kt @@ -35,9 +35,9 @@ class SaveClient(context: Context) : StrongBuilderBase okBuilder = OkHttpClient.Builder() .addInterceptor(cacheInterceptor) - .connectTimeout(20L, TimeUnit.SECONDS) - .writeTimeout(20L, TimeUnit.SECONDS) - .readTimeout(20L, TimeUnit.SECONDS) + .connectTimeout(40L, TimeUnit.SECONDS) + .writeTimeout(40L, TimeUnit.SECONDS) + .readTimeout(40L, TimeUnit.SECONDS) .retryOnConnectionFailure(false) .protocols(arrayListOf(Protocol.HTTP_1_1)) } diff --git a/app/src/main/java/net/opendasharchive/openarchive/services/internetarchive/IaConduit.kt b/app/src/main/java/net/opendasharchive/openarchive/services/internetarchive/IaConduit.kt index 1b3322d6..355a1fda 100644 --- a/app/src/main/java/net/opendasharchive/openarchive/services/internetarchive/IaConduit.kt +++ b/app/src/main/java/net/opendasharchive/openarchive/services/internetarchive/IaConduit.kt @@ -2,14 +2,15 @@ package net.opendasharchive.openarchive.services.internetarchive import android.content.Context import android.net.Uri -import com.google.gson.Gson +import com.google.gson.GsonBuilder +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext import net.opendasharchive.openarchive.R import net.opendasharchive.openarchive.db.Media import net.opendasharchive.openarchive.services.Conduit import net.opendasharchive.openarchive.services.SaveClient import okhttp3.* import okhttp3.MediaType.Companion.toMediaTypeOrNull -import okio.BufferedSink import java.io.File import java.io.IOException @@ -26,36 +27,51 @@ class IaConduit(media: Media, context: Context) : Conduit(media, context) { private fun getSlug(title: String): String { return title.replace("[^A-Za-z\\d]".toRegex(), "-") } + + val textMediaType = "texts".toMediaTypeOrNull() + + private val gson = GsonBuilder().excludeFieldsWithoutExposeAnnotation().create() } override suspend fun upload(): Boolean { sanitize() try { - val mediaUri = mMedia.originalFilePath val mimeType = mMedia.mimeType + val client = SaveClient.get(mContext) + // TODO this should make sure we aren't accidentally using one of archive.org's metadata fields by accident val slug = getSlug(mMedia.title) + /// Upload metadata var basePath = "$slug-${Util.RandomString(4).nextString()}" - val url = "$ARCHIVE_API_ENDPOINT/$basePath/" + getUploadFileName(mMedia, true) - val requestBody = getRequestBody(mMedia, mediaUri, mimeType.toMediaTypeOrNull(), basePath) + val fileName = getUploadFileName(mMedia, true) + val metaJson = gson.toJsonTree(mMedia) + val proof = getProof() - put(url, requestBody, mainHeader()) + val url = "$ARCHIVE_API_ENDPOINT/$basePath/$fileName" - /// Upload metadata - basePath = "$slug-${Util.RandomString(4).nextString()}" + // upload content synchronously for progress + client.uploadContent(url, mimeType) - uploadMetaData(Gson().toJson(mMedia), basePath, getUploadFileName(mMedia, true)) + // upload metadata and proofs async, and report failures + basePath = "$slug-${Util.RandomString(4).nextString()}" + client.uploadMetaData(metaJson.toString(), basePath, fileName) /// Upload ProofMode metadata, if enabled and successfully created. - for (file in getProof()) { - uploadProofFiles(file, basePath) + for (file in proof) { + client.uploadProofFiles(file, basePath) + } + + val finalPath = ARCHIVE_DETAILS_ENDPOINT + basePath + mMedia.serverUrl = finalPath + + withContext(Dispatchers.IO) { + jobSucceeded() } return true - } - catch (e: Exception) { + } catch (e: Exception) { jobFailed(e) } @@ -66,91 +82,74 @@ class IaConduit(media: Media, context: Context) : Conduit(media, context) { // Ignored. Not used here. } - @Throws(IOException::class) - private suspend fun uploadMetaData(content: String, basePath: String, fileName: String) { - val requestBody = object : RequestBody() { - override fun contentType(): MediaType? { - return "texts".toMediaTypeOrNull() - } + private suspend fun OkHttpClient.uploadContent(url: String, mimeType: String) { - override fun writeTo(sink: BufferedSink) { - sink.writeString(content, Charsets.UTF_8) - } - } + val mediaUri = mMedia.originalFilePath + val requestBody = getRequestBody(mMedia, mediaUri, mimeType.toMediaTypeOrNull()) - put( - "$ARCHIVE_API_ENDPOINT/$basePath/$fileName.meta.json", - requestBody, - metadataHeader() - ) + val request = Request.Builder() + .url(url) + .put(requestBody) + .headers(mainHeader()) + .build() + + execute(request) } - /// upload proof mode @Throws(IOException::class) - private suspend fun uploadProofFiles(uploadFile: File, basePath: String) { - val requestBody = getRequestBodyMetaData( - uploadFile, - Uri.fromFile(uploadFile).toString(), - "texts".toMediaTypeOrNull() + private fun OkHttpClient.uploadMetaData(content: String, basePath: String, fileName: String) { + val requestBody = RequestBodyUtil.create( + textMediaType, + content.byteInputStream(), + content.length.toLong(), + null ) - put("$ARCHIVE_API_ENDPOINT/$basePath/${uploadFile.name}", - requestBody, - metadataHeader()) + val url = "$ARCHIVE_API_ENDPOINT/$basePath/$fileName.meta.json" + + val request = Request.Builder() + .url(url) + .put(requestBody) + .headers(metadataHeader()) + .build() + + enqueue(request) } - private fun getRequestBody(media: Media, mediaUri: String?, mediaType: MediaType?, basePath: String): RequestBody { - return RequestBodyUtil.create( + /// upload proof mode + @Throws(IOException::class) + private fun OkHttpClient.uploadProofFiles(uploadFile: File, basePath: String) { + val requestBody = RequestBodyUtil.create( mContext.contentResolver, - Uri.parse(mediaUri), - media.contentLength, - mediaType, - object : RequestListener { - var lastBytes: Long = 0 - override fun transferred(bytes: Long) { - if (bytes > lastBytes) { - jobProgress(bytes) - lastBytes = bytes - } - } + Uri.fromFile(uploadFile), + uploadFile.length(), + textMediaType, createListener(cancellable = { !mCancelled }) + ) - override fun continueUpload(): Boolean { - return !mCancelled - } + val url = "$ARCHIVE_API_ENDPOINT/$basePath/${uploadFile.name}" - override fun transferComplete() { - val finalPath = ARCHIVE_DETAILS_ENDPOINT + basePath - media.serverUrl = finalPath - jobSucceeded() - } - }) + val request = Request.Builder() + .url(url) + .put(requestBody) + .headers(metadataHeader()) + .build() + + enqueue(request) } - /// request body for meta data - private fun getRequestBodyMetaData(media: File, mediaUri: String, mediaType: MediaType?): RequestBody { + private fun getRequestBody( + media: Media, + mediaUri: String?, + mediaType: MediaType? + ): RequestBody { return RequestBodyUtil.create( mContext.contentResolver, Uri.parse(mediaUri), - media.length(), - mediaType, - object : RequestListener { - var lastBytes: Long = 0 - - override fun transferred(bytes: Long) { - if (bytes > lastBytes) { - jobProgress(bytes) - lastBytes = bytes - } - } - - override fun continueUpload(): Boolean { - return !mCancelled - } - - override fun transferComplete() { - jobSucceeded() - } + media.contentLength, + mediaType, createListener(cancellable = { !mCancelled }, onProgress = { + jobProgress(it) }) + ) } private fun mainHeader(): Headers { @@ -167,6 +166,10 @@ class IaConduit(media: Media, context: Context) : Conduit(media, context) { builder.add("x-archive-meta-author", author) } + if (mMedia.contentLength > 0) { + builder.add("x-archive-size-hint", mMedia.contentLength.toString()) + } + val collection = when { mMedia.mimeType.startsWith("video") -> "opensource_movies" mMedia.mimeType.startsWith("audio") -> "opensource_audio" @@ -219,7 +222,7 @@ class IaConduit(media: Media, context: Context) : Conduit(media, context) { private fun metadataHeader(): Headers { return Headers.Builder() .add("x-amz-auto-make-bucket", "1") - .add("x-archive-meta-language","eng") // FIXME set based on locale or selected + .add("x-archive-meta-language", "eng") // FIXME set based on locale or selected .add("Authorization", "LOW " + mMedia.space?.username + ":" + mMedia.space?.password) .add("x-archive-meta-mediatype", "texts") .add("x-archive-meta-collection", "opensource") @@ -227,33 +230,29 @@ class IaConduit(media: Media, context: Context) : Conduit(media, context) { } @Throws(Exception::class) - private suspend fun put(url: String, requestBody: RequestBody, headers: Headers) { - val request = Request.Builder() - .url(url) - .put(requestBody) - .headers(headers) - .build() + private suspend fun OkHttpClient.execute(request: Request) = withContext(Dispatchers.IO) { + val result = newCall(request) + .execute() - execute(request) + if (result.isSuccessful.not()) { + throw RuntimeException("${result.code}: ${result.message}") + } } @Throws(Exception::class) - private suspend fun execute(request: Request) { - SaveClient.get(mContext) - .newCall(request) + private fun OkHttpClient.enqueue(request: Request) { + newCall(request) .enqueue(object : Callback { override fun onFailure(call: Call, e: IOException) { jobFailed(e) } override fun onResponse(call: Call, response: Response) { - if (response.isSuccessful) { - jobSucceeded() - } - else { - jobFailed(Exception("${response.code} ${response.message}")) + if (!response.isSuccessful) { + jobFailed(Exception("${response.code}: ${response.message}")) } } + }) } } \ No newline at end of file diff --git a/app/src/main/java/net/opendasharchive/openarchive/services/internetarchive/RequestBodyUtil.kt b/app/src/main/java/net/opendasharchive/openarchive/services/internetarchive/RequestBodyUtil.kt index 352200d2..ea2b0804 100644 --- a/app/src/main/java/net/opendasharchive/openarchive/services/internetarchive/RequestBodyUtil.kt +++ b/app/src/main/java/net/opendasharchive/openarchive/services/internetarchive/RequestBodyUtil.kt @@ -12,19 +12,27 @@ import okio.Source import timber.log.Timber import java.io.* +fun createListener(cancellable: () -> Boolean, onProgress: (Long) -> Unit = {}, onComplete: () -> Unit = {}) = object : RequestListener { + override fun transferred(bytes: Long) = onProgress(bytes) + + override fun continueUpload() = cancellable() + + override fun transferComplete() = Unit +} + /** * Created by n8fr8 on 12/29/17. */ object RequestBodyUtil { - fun create(mediaType: MediaType?, inputStream: InputStream): RequestBody { + + fun create(mediaType: MediaType?, inputStream: InputStream, contentLength: Long? = null, + listener: RequestListener?): RequestBody { return object : RequestBody() { - override fun contentType(): MediaType? { - return mediaType - } + override fun contentType() = mediaType override fun contentLength(): Long { return try { - inputStream.available().toLong() + contentLength ?: inputStream.available().toLong() } catch (e: IOException) { Timber.i("BodyRequestUtil couldn't get contentLength, returning 0 instead", e) 0 @@ -36,7 +44,7 @@ object RequestBodyUtil { var source: Source? = null try { source = inputStream.source() - sink.writeAll(source) + sink.writeAll(source, listener) } finally { source!!.closeQuietly() } @@ -66,41 +74,42 @@ object RequestBodyUtil { } } - override fun contentType(): MediaType? { - return mediaType - } + override fun contentType() = mediaType - override fun contentLength(): Long { - return contentLength - } + override fun contentLength() = contentLength @Synchronized @Throws(IOException::class) override fun writeTo(sink: BufferedSink) { init() - val source = inputStream!!.source() - if (mListener == null) { - sink.writeAll(source) - } else { - try { - var total: Long = 0 - var read: Long - while (source.read(sink.buffer, SEGMENT_SIZE.toLong()).also { - read = it - } != -1L && mListener != null && mListener!!.continueUpload()) { - total += read - if (mListener != null) mListener!!.transferred(total) - sink.flush() - } - mListener!!.transferComplete() - } finally { - source.closeQuietly() - } + var source: Source? = null + try { + source = inputStream!!.source() + sink.writeAll(source, listener) + } finally { + source?.closeQuietly() } } } } + fun BufferedSink.writeAll(source: Source, listener: RequestListener?) { + if (listener == null) { + writeAll(source) + } else { + var total: Long = 0 + var read: Long + while (source.read(buffer, SEGMENT_SIZE.toLong()).also { + read = it + } != -1L && listener.continueUpload()) { + total += read + listener.transferred(total) + flush() + } + listener.transferComplete() + } + } + fun create(fileSource: File, mediaType: MediaType?, listener: RequestListener?): RequestBody { return object : RequestBody() { var inputStream: InputStream? = null @@ -112,13 +121,9 @@ object RequestBodyUtil { } } - override fun contentType(): MediaType? { - return mediaType - } + override fun contentType() = mediaType - override fun contentLength(): Long { - return fileSource.length() - } + override fun contentLength() = fileSource.length() @Synchronized @Throws(IOException::class) diff --git a/app/src/main/java/net/opendasharchive/openarchive/upload/BroadcastManager.kt b/app/src/main/java/net/opendasharchive/openarchive/upload/BroadcastManager.kt index 7d3ec687..d76d2e34 100644 --- a/app/src/main/java/net/opendasharchive/openarchive/upload/BroadcastManager.kt +++ b/app/src/main/java/net/opendasharchive/openarchive/upload/BroadcastManager.kt @@ -8,18 +8,30 @@ import androidx.localbroadcastmanager.content.LocalBroadcastManager object BroadcastManager { - enum class Action(val id: String, var mediaId: Long = -1) { + enum class Action(val id: String, var collectionId: Long = -1, var mediaId: Long = -1, var progress: Long = -1) { Change("media_change_intent"), Delete("media_delete_intent") } private const val MEDIA_ID = "media_id" + private const val COLLECTION_ID = "collection_id" + private const val MEDIA_PROGRESS = "media_progress" - fun postChange(context: Context, mediaId: Long) { + fun postChange(context: Context, collectionId: Long, mediaId: Long) { val i = Intent(Action.Change.id) i.putExtra(MEDIA_ID, mediaId) + i.putExtra(COLLECTION_ID, collectionId) - LocalBroadcastManager.getInstance(context).sendBroadcast(i) + LocalBroadcastManager.getInstance(context).sendBroadcastSync(i) + } + + fun postProgress(context: Context, collectionId: Long, mediaId: Long, progress: Long) { + val i = Intent(Action.Change.id) + i.putExtra(MEDIA_ID, mediaId) + i.putExtra(COLLECTION_ID, collectionId) + i.putExtra(MEDIA_PROGRESS, progress) + + LocalBroadcastManager.getInstance(context).sendBroadcastSync(i) } fun postDelete(context: Context, mediaId: Long) { @@ -32,6 +44,8 @@ object BroadcastManager { fun getAction(intent: Intent): Action? { val action = Action.values().firstOrNull { it.id == intent.action } action?.mediaId = intent.getLongExtra(MEDIA_ID, -1) + action?.collectionId = intent.getLongExtra(COLLECTION_ID, -1) + action?.progress = intent.getLongExtra(MEDIA_PROGRESS, -1) return action } diff --git a/app/src/main/java/net/opendasharchive/openarchive/upload/UploadManagerFragment.kt b/app/src/main/java/net/opendasharchive/openarchive/upload/UploadManagerFragment.kt index ac9e8037..2e6d3914 100644 --- a/app/src/main/java/net/opendasharchive/openarchive/upload/UploadManagerFragment.kt +++ b/app/src/main/java/net/opendasharchive/openarchive/upload/UploadManagerFragment.kt @@ -92,7 +92,7 @@ open class UploadManagerFragment : Fragment() { } open fun updateItem(mediaId: Long) { - mediaAdapter?.updateItem(mediaId) + mediaAdapter?.updateItem(mediaId, -1) } open fun removeItem(mediaId: Long) { diff --git a/app/src/main/java/net/opendasharchive/openarchive/upload/UploadService.kt b/app/src/main/java/net/opendasharchive/openarchive/upload/UploadService.kt index 9b0cca8b..196d8dea 100644 --- a/app/src/main/java/net/opendasharchive/openarchive/upload/UploadService.kt +++ b/app/src/main/java/net/opendasharchive/openarchive/upload/UploadService.kt @@ -17,6 +17,7 @@ import androidx.core.content.ContextCompat import androidx.work.Configuration import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.launch import net.opendasharchive.openarchive.CleanInsightsManager import net.opendasharchive.openarchive.R @@ -64,8 +65,10 @@ class UploadService : JobService() { Configuration.Builder().setJobSchedulerJobIdRange(0, Integer.MAX_VALUE).build() } + private val scope = CoroutineScope(Dispatchers.IO + SupervisorJob()) + override fun onStartJob(params: JobParameters): Boolean { - CoroutineScope(Dispatchers.IO).launch { + scope.launch { upload { jobFinished(params, false) } @@ -92,7 +95,9 @@ class UploadService : JobService() { } private suspend fun upload(completed: () -> Unit) { - if (mRunning) return completed() + if (mRunning) { + return completed() + } mRunning = true @@ -103,47 +108,47 @@ class UploadService : JobService() { } // Get all media items that are set into queued state. - var results = emptyList() + val results = Media.getByStatus( + listOf(Media.Status.Queued, Media.Status.Uploading), + Media.ORDER_PRIORITY + ).toMutableList() while (mKeepUploading && - Media.getByStatus( - listOf(Media.Status.Queued, Media.Status.Uploading), - Media.ORDER_PRIORITY - ) - .also { results = it } - .isNotEmpty() + results.isNotEmpty() ) { val datePublish = Date() - for (media in results) { - if (media.sStatus != Media.Status.Uploading) { - media.uploadDate = datePublish - media.progress = 0 // Should we reset this? - media.sStatus = Media.Status.Uploading - media.statusMessage = "" - } + val media = results.removeFirst() - media.licenseUrl = media.project?.licenseUrl + if (media.sStatus != Media.Status.Uploading) { + media.uploadDate = datePublish + media.progress = 0 // Should we reset this? + media.sStatus = Media.Status.Uploading + media.statusMessage = "" + } - val collection = media.collection + media.licenseUrl = media.project?.licenseUrl - if (collection?.uploadDate == null) { - collection?.uploadDate = datePublish - collection?.save() - } + val collection = media.collection - try { - upload(media) - } catch (ioe: IOException) { - Timber.d(ioe) + if (collection?.uploadDate == null) { + collection?.uploadDate = datePublish + collection?.save() + } - media.statusMessage = "error in uploading media: " + ioe.message - media.sStatus = Media.Status.Error - media.save() - } + try { + upload(media) + } catch (ioe: IOException) { + Timber.d(ioe) - if (!mKeepUploading) break // Time to end this. + media.statusMessage = "error in uploading media: " + ioe.message + media.sStatus = Media.Status.Error + media.save() + + BroadcastManager.postChange(applicationContext, media.collectionId, media.id) } + + if (!mKeepUploading) break // Time to end this. } mRunning = false @@ -152,18 +157,21 @@ class UploadService : JobService() { @Throws(IOException::class) private suspend fun upload(media: Media): Boolean { - media.sStatus = Media.Status.Uploading - media.save() - BroadcastManager.postChange(this, media.id) val conduit = Conduit.get(media, this) ?: return false + media.sStatus = Media.Status.Uploading + media.save() + BroadcastManager.postChange(this, media.collectionId, media.id) + CleanInsightsManager.measureEvent("upload", "try_upload", media.space?.tType?.friendlyName) mConduits.add(conduit) - conduit.upload() - mConduits.remove(conduit) + scope.launch { + conduit.upload() + mConduits.remove(conduit) + } return true } diff --git a/app/src/main/res/layout/rv_media_box.xml b/app/src/main/res/layout/rv_media_box.xml index 01db3ed8..cd7332e2 100644 --- a/app/src/main/res/layout/rv_media_box.xml +++ b/app/src/main/res/layout/rv_media_box.xml @@ -85,7 +85,7 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="100%" - android:textSize="14sp" + android:textSize="12sp" android:visibility="gone" android:textColor="@color/colorPrimary" app:layout_constraintBottom_toBottomOf="parent"