diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index cc7f881..2c41ccd 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -198,6 +198,10 @@ android:exported="true" android:permission="android.permission.BIND_JOB_SERVICE" /> + + - if (msg.encoding == Plaintext.Encoding.SIMPLE) { - conversationService.getSubject(listOf(msg))?.let { subject -> - msg.conversationId = UUID.nameUUIDFromBytes(subject.toByteArray()) - messageRepo.save(msg) - Thread.yield() + service.process(SimpleJob( + messageRepo.count(), + { messageRepo.findNextLegacyMessages(it) }, + { msg -> + if (msg.encoding == Plaintext.Encoding.SIMPLE) { + conversationService.getSubject(listOf(msg))?.let { subject -> + msg.conversationId = UUID.nameUUIDFromBytes(subject.toByteArray()) + messageRepo.save(msg) + Thread.yield() + } } - } - } - if (!messages.isEmpty()) { - previous = messages.last() - } - } while (!messages.isEmpty()) - - uiThread { - Toast.makeText( - ctx, - R.string.cleanup_notification_end, - Toast.LENGTH_LONG - ).show() - conversationInit.isEnabled = true + }, + R.drawable.ic_notification_batch, + R.string.emulate_conversations_batch + )) } } - return@OnPreferenceClickListener true + + override fun onServiceDisconnected(name: ComponentName) { + } } - private fun emulateConversationChangeListener(conversationInit: Preference?) = Preference.OnPreferenceChangeListener { preference, newValue -> + private fun conversationInitClickListener() = Preference.OnPreferenceClickListener { + val ctx = activity?.applicationContext + ?: throw IllegalStateException("Context not available") + ctx.bindService(Intent(ctx, BatchProcessorService::class.java), connection, Context.BIND_AUTO_CREATE) + true + } + + private fun emulateConversationChangeListener(conversationInit: Preference?) = Preference.OnPreferenceChangeListener { _, newValue -> conversationInit?.isEnabled = newValue as Boolean true } diff --git a/app/src/main/java/ch/dissem/apps/abit/notification/BatchNotification.kt b/app/src/main/java/ch/dissem/apps/abit/notification/BatchNotification.kt new file mode 100644 index 0000000..aadd9af --- /dev/null +++ b/app/src/main/java/ch/dissem/apps/abit/notification/BatchNotification.kt @@ -0,0 +1,54 @@ +/* + * Copyright 2016 Christian Basler + * + * Licensed 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 ch.dissem.apps.abit.notification + +import android.content.Context +import android.support.v4.app.NotificationCompat +import ch.dissem.apps.abit.R +import ch.dissem.apps.abit.service.Job + +/** + * Ongoing notification while proof of work is in progress. + */ +class BatchNotification(ctx: Context) : AbstractNotification(ctx) { + + private val builder = NotificationCompat.Builder(ctx, ONGOING_CHANNEL_ID) + .setVisibility(NotificationCompat.VISIBILITY_PUBLIC) + .setUsesChronometer(true) + + init { + initChannel(ONGOING_CHANNEL_ID, R.color.colorAccent) + notification = builder.build() + } + + override val notificationId = ONGOING_NOTIFICATION_ID + + fun update(job: Job): BatchNotification { + + builder.setContentTitle(ctx.getString(job.description)) + .setSmallIcon(job.icon) + .setProgress(job.numberOfItems, job.numberOfProcessedItems, job.numberOfItems <= 0) + + notification = builder.build() + show() + return this + } + + companion object { + const val ONGOING_NOTIFICATION_ID = 4 + } +} diff --git a/app/src/main/java/ch/dissem/apps/abit/repository/AndroidMessageRepository.kt b/app/src/main/java/ch/dissem/apps/abit/repository/AndroidMessageRepository.kt index 174f9d1..4fd1963 100644 --- a/app/src/main/java/ch/dissem/apps/abit/repository/AndroidMessageRepository.kt +++ b/app/src/main/java/ch/dissem/apps/abit/repository/AndroidMessageRepository.kt @@ -47,6 +47,13 @@ class AndroidMessageRepository(private val sql: SqlHelper) : AbstractMessageRepo super.findMessages(label, offset, limit) } + fun count() = DatabaseUtils.queryNumEntries( + sql.readableDatabase, + TABLE_NAME, + null, + null + ).toInt() + override fun countUnread(label: Label?) = when { label === LABEL_ARCHIVE -> 0 label == null -> DatabaseUtils.queryNumEntries( diff --git a/app/src/main/java/ch/dissem/apps/abit/service/BatchProcessorService.kt b/app/src/main/java/ch/dissem/apps/abit/service/BatchProcessorService.kt new file mode 100644 index 0000000..bf87598 --- /dev/null +++ b/app/src/main/java/ch/dissem/apps/abit/service/BatchProcessorService.kt @@ -0,0 +1,117 @@ +package ch.dissem.apps.abit.service + +import android.app.Service +import android.content.Intent +import android.os.Binder +import android.support.annotation.DrawableRes +import android.support.annotation.StringRes +import android.support.v4.content.ContextCompat +import ch.dissem.apps.abit.notification.BatchNotification +import ch.dissem.apps.abit.notification.BatchNotification.Companion.ONGOING_NOTIFICATION_ID +import org.jetbrains.anko.doAsync +import java.util.* + +class BatchProcessorService : Service() { + private lateinit var notification: BatchNotification + + override fun onCreate() { + notification = BatchNotification(this) + } + + override fun onBind(intent: Intent) = BatchBinder(this) + + class BatchBinder internal constructor(val service: BatchProcessorService) : Binder() { + private val notification = service.notification + + fun process(job: Job) = synchronized(queue) { + ContextCompat.startForegroundService( + service, + Intent(service, BatchProcessorService::class.java) + ) + service.startForeground( + ONGOING_NOTIFICATION_ID, + notification.notification + ) + if (!working) { + working = true + service.processQueue(job) + } else { + queue.add(job) + } + } + + } + + private fun processQueue(job: Job) { + doAsync { + var next: Job? = job + while (next != null) { + next.process(notification) + + synchronized(queue) { + next = queue.poll() + if (next == null) { + working = false + stopForeground(true) + stopSelf() + } + } + } + + } + } + + companion object { + private var working = false + private val queue = LinkedList<Job>() + } +} + +interface Job { + val icon: Int + @DrawableRes get + + val description: Int + @StringRes get + + val numberOfItems: Int + var numberOfProcessedItems: Int + + /** + * Runs the job. This shouldn't happen in a separate thread, as this is handled by the service. + */ + fun process(notification: BatchNotification) +} + +data class SimpleJob<T>( + override val numberOfItems: Int, + /** + * Provides the next batch of items, given the last item of the previous batch, + * or null for the first batch. + */ + private val provider: (T?) -> List<T>, + /** + * Processes an item. + */ + private val processor: (T) -> Unit, + override val icon: Int, + override val description: Int +) : Job { + override var numberOfProcessedItems: Int = 0 + + override fun process(notification: BatchNotification) { + notification.update(this) + var batch = provider.invoke(null) + while (batch.isNotEmpty()) { + Thread.yield() + batch.forEach { + processor.invoke(it) + Thread.yield() + } + numberOfProcessedItems += batch.size + notification.update(this) + batch = provider.invoke(batch.last()) + } + } + +} diff --git a/app/src/main/res/drawable/ic_notification_batch.xml b/app/src/main/res/drawable/ic_notification_batch.xml new file mode 100644 index 0000000..fc8b546 --- /dev/null +++ b/app/src/main/res/drawable/ic_notification_batch.xml @@ -0,0 +1,10 @@ +<?xml version="1.0" encoding="utf-8"?> +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="24dp" + android:height="24dp" + android:viewportHeight="24" + android:viewportWidth="24"> + <path + android:fillColor="#FFFFFFFF" + android:pathData="M15.9,18.45C17.25,18.45 18.35,17.35 18.35,16C18.35,14.65 17.25,13.55 15.9,13.55C14.54,13.55 13.45,14.65 13.45,16C13.45,17.35 14.54,18.45 15.9,18.45M21.1,16.68L22.58,17.84C22.71,17.95 22.75,18.13 22.66,18.29L21.26,20.71C21.17,20.86 21,20.92 20.83,20.86L19.09,20.16C18.73,20.44 18.33,20.67 17.91,20.85L17.64,22.7C17.62,22.87 17.47,23 17.3,23H14.5C14.32,23 14.18,22.87 14.15,22.7L13.89,20.85C13.46,20.67 13.07,20.44 12.71,20.16L10.96,20.86C10.81,20.92 10.62,20.86 10.54,20.71L9.14,18.29C9.05,18.13 9.09,17.95 9.22,17.84L10.7,16.68L10.65,16L10.7,15.31L9.22,14.16C9.09,14.05 9.05,13.86 9.14,13.71L10.54,11.29C10.62,11.13 10.81,11.07 10.96,11.13L12.71,11.84C13.07,11.56 13.46,11.32 13.89,11.15L14.15,9.29C14.18,9.13 14.32,9 14.5,9H17.3C17.47,9 17.62,9.13 17.64,9.29L17.91,11.15C18.33,11.32 18.73,11.56 19.09,11.84L20.83,11.13C21,11.07 21.17,11.13 21.26,11.29L22.66,13.71C22.75,13.86 22.71,14.05 22.58,14.16L21.1,15.31L21.15,16L21.1,16.68M6.69,8.07C7.56,8.07 8.26,7.37 8.26,6.5C8.26,5.63 7.56,4.92 6.69,4.92A1.58,1.58 0 0,0 5.11,6.5C5.11,7.37 5.82,8.07 6.69,8.07M10.03,6.94L11,7.68C11.07,7.75 11.09,7.87 11.03,7.97L10.13,9.53C10.08,9.63 9.96,9.67 9.86,9.63L8.74,9.18L8,9.62L7.81,10.81C7.79,10.92 7.7,11 7.59,11H5.79C5.67,11 5.58,10.92 5.56,10.81L5.4,9.62L4.64,9.18L3.5,9.63C3.41,9.67 3.3,9.63 3.24,9.53L2.34,7.97C2.28,7.87 2.31,7.75 2.39,7.68L3.34,6.94L3.31,6.5L3.34,6.06L2.39,5.32C2.31,5.25 2.28,5.13 2.34,5.03L3.24,3.47C3.3,3.37 3.41,3.33 3.5,3.37L4.63,3.82L5.4,3.38L5.56,2.19C5.58,2.08 5.67,2 5.79,2H7.59C7.7,2 7.79,2.08 7.81,2.19L8,3.38L8.74,3.82L9.86,3.37C9.96,3.33 10.08,3.37 10.13,3.47L11.03,5.03C11.09,5.13 11.07,5.25 11,5.32L10.03,6.06L10.06,6.5L10.03,6.94Z" /> +</vector> diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 630db20..d57db8f 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -140,4 +140,5 @@ As an alternative you could configure a trusted node in the settings, but as of <string name="emulate_conversations">Guess conversations</string> <string name="emulate_conversations_summary">Use subject to determine which messages belong together. The order will likely be wrong.</string> <string name="emulate_conversations_initialize">Group existing messages by subject</string> + <string name="emulate_conversations_batch">Grouping existing messages by subject</string> </resources>