package org.thoughtcrime.securesms.exporter import android.content.Context import android.content.Intent import androidx.core.app.NotificationCompat import app.cash.exhaustive.Exhaustive import org.signal.core.util.PendingIntentFlags import org.signal.smsexporter.ExportableMessage import org.signal.smsexporter.SmsExportService import org.thoughtcrime.securesms.R import org.thoughtcrime.securesms.attachments.AttachmentId import org.thoughtcrime.securesms.database.SignalDatabase import org.thoughtcrime.securesms.database.model.MessageId import org.thoughtcrime.securesms.database.model.databaseprotos.MessageExportState import org.thoughtcrime.securesms.dependencies.ApplicationDependencies import org.thoughtcrime.securesms.exporter.flow.SmsExportActivity import org.thoughtcrime.securesms.jobs.ForegroundServiceUtil import org.thoughtcrime.securesms.notifications.NotificationChannels import org.thoughtcrime.securesms.notifications.NotificationIds import org.thoughtcrime.securesms.notifications.v2.NotificationPendingIntentHelper import org.thoughtcrime.securesms.util.JsonUtils import java.io.IOException import java.io.InputStream /** * Service which integrates the SMS exporter functionality. */ class SignalSmsExportService : SmsExportService() { companion object { /** * Launches the export service and immediately begins exporting messages. */ fun start(context: Context, clearPreviousExportState: Boolean) { val intent = Intent(context, SignalSmsExportService::class.java) .apply { putExtra(CLEAR_PREVIOUS_EXPORT_STATE_EXTRA, clearPreviousExportState) } ForegroundServiceUtil.startOrThrow(context, intent) } } private var reader: SignalSmsExportReader? = null override fun getNotification(progress: Int, total: Int): ExportNotification { val pendingIntent = NotificationPendingIntentHelper.getActivity( this, 0, SmsExportActivity.createIntent(this), PendingIntentFlags.mutable() ) return ExportNotification( NotificationIds.SMS_EXPORT_SERVICE, NotificationCompat.Builder(this, NotificationChannels.BACKUPS) .setSmallIcon(R.drawable.ic_signal_backup) .setContentTitle(getString(R.string.SignalSmsExportService__exporting_messages)) .setContentIntent(pendingIntent) .setProgress(total, progress, false) .build() ) } override fun getExportCompleteNotification(): ExportNotification? { if (ApplicationDependencies.getAppForegroundObserver().isForegrounded) { return null } val pendingIntent = NotificationPendingIntentHelper.getActivity( this, 0, SmsExportActivity.createIntent(this), PendingIntentFlags.mutable() ) return ExportNotification( NotificationIds.SMS_EXPORT_COMPLETE, NotificationCompat.Builder(this, NotificationChannels.APP_ALERTS) .setSmallIcon(R.drawable.ic_notification) .setContentTitle(getString(R.string.SignalSmsExportService__signal_sms_export_complete)) .setContentText(getString(R.string.SignalSmsExportService__tap_to_return_to_signal)) .setContentIntent(pendingIntent) .build() ) } override fun clearPreviousExportState() { SignalDatabase.sms.clearExportState() SignalDatabase.mms.clearExportState() } override fun prepareForExport() { SignalDatabase.sms.clearInsecureMessageExportedErrorStatus() SignalDatabase.mms.clearInsecureMessageExportedErrorStatus() } override fun getUnexportedMessageCount(): Int { ensureReader() return reader!!.getCount() } override fun getUnexportedMessages(): Iterable { ensureReader() return reader!! } override fun onMessageExportStarted(exportableMessage: ExportableMessage) { SignalDatabase.mmsSms.updateMessageExportState(exportableMessage.getMessageId()) { it.toBuilder().setProgress(MessageExportState.Progress.STARTED).build() } } override fun onMessageExportSucceeded(exportableMessage: ExportableMessage) { SignalDatabase.mmsSms.updateMessageExportState(exportableMessage.getMessageId()) { it.toBuilder().setProgress(MessageExportState.Progress.COMPLETED).build() } SignalDatabase.mmsSms.markMessageExported(exportableMessage.getMessageId()) } override fun onMessageExportFailed(exportableMessage: ExportableMessage) { SignalDatabase.mmsSms.updateMessageExportState(exportableMessage.getMessageId()) { it.toBuilder().setProgress(MessageExportState.Progress.INIT).build() } SignalDatabase.mmsSms.markMessageExportFailed(exportableMessage.getMessageId()) } override fun onMessageIdCreated(exportableMessage: ExportableMessage, messageId: Long) { SignalDatabase.mmsSms.updateMessageExportState(exportableMessage.getMessageId()) { it.toBuilder().setMessageId(messageId).build() } } override fun onAttachmentPartExportStarted(exportableMessage: ExportableMessage, part: ExportableMessage.Mms.Part) { SignalDatabase.mmsSms.updateMessageExportState(exportableMessage.getMessageId()) { it.toBuilder().addStartedAttachments(part.contentId).build() } } override fun onAttachmentPartExportSucceeded(exportableMessage: ExportableMessage, part: ExportableMessage.Mms.Part) { SignalDatabase.mmsSms.updateMessageExportState(exportableMessage.getMessageId()) { it.toBuilder().addCompletedAttachments(part.contentId).build() } } override fun onAttachmentPartExportFailed(exportableMessage: ExportableMessage, part: ExportableMessage.Mms.Part) { SignalDatabase.mmsSms.updateMessageExportState(exportableMessage.getMessageId()) { val startedAttachments = it.startedAttachmentsList - part.contentId it.toBuilder().clearStartedAttachments().addAllStartedAttachments(startedAttachments).build() } } override fun onRecipientExportStarted(exportableMessage: ExportableMessage, recipient: String) { SignalDatabase.mmsSms.updateMessageExportState(exportableMessage.getMessageId()) { it.toBuilder().addStartedRecipients(recipient).build() } } override fun onRecipientExportSucceeded(exportableMessage: ExportableMessage, recipient: String) { SignalDatabase.mmsSms.updateMessageExportState(exportableMessage.getMessageId()) { it.toBuilder().addCompletedRecipients(recipient).build() } } override fun onRecipientExportFailed(exportableMessage: ExportableMessage, recipient: String) { SignalDatabase.mmsSms.updateMessageExportState(exportableMessage.getMessageId()) { val startedAttachments = it.startedRecipientsList - recipient it.toBuilder().clearStartedRecipients().addAllStartedRecipients(startedAttachments).build() } } @Throws(IOException::class) override fun getInputStream(part: ExportableMessage.Mms.Part): InputStream { return SignalDatabase.attachments.getAttachmentStream(JsonUtils.fromJson(part.contentId, AttachmentId::class.java), 0) } override fun onExportPassCompleted() { reader?.close() } private fun ExportableMessage.getMessageId(): MessageId { @Exhaustive val messageId: Any = when (this) { is ExportableMessage.Mms<*> -> id is ExportableMessage.Sms<*> -> id } if (messageId is MessageId) { return messageId } else { throw AssertionError("Exportable message id must be type MessageId. Type: ${messageId.javaClass}") } } private fun ensureReader() { if (reader == null) { reader = SignalSmsExportReader() } } }