diff --git a/data/src/main/java/mapper/CursorToMessageImpl.kt b/data/src/main/java/mapper/CursorToMessageImpl.kt index 0368bbb2..0478f4b0 100644 --- a/data/src/main/java/mapper/CursorToMessageImpl.kt +++ b/data/src/main/java/mapper/CursorToMessageImpl.kt @@ -24,6 +24,7 @@ import android.net.Uri import android.provider.Telephony.* import com.google.android.mms.pdu_alt.PduHeaders import manager.KeyManager +import model.Conversation import model.Message import util.extensions.map import javax.inject.Inject @@ -77,12 +78,13 @@ class CursorToMessageImpl @Inject constructor( id = keys.newId() contentId = cursor.getLong(columnsMap.msgId) - threadId = cursor.getLong(columnsMap.threadId) date = cursor.getLong(columnsMap.date) dateSent = cursor.getLong(columnsMap.dateSent) read = cursor.getInt(columnsMap.read) != 0 locked = cursor.getInt(columnsMap.locked) != 0 + conversation = Conversation().apply { id = cursor.getLong(columnsMap.threadId) } + when (type) { "sms" -> { address = cursor.getString(columnsMap.smsAddress) ?: "" diff --git a/data/src/main/java/repository/MessageRepositoryImpl.kt b/data/src/main/java/repository/MessageRepositoryImpl.kt index bc935912..3bb29b7c 100644 --- a/data/src/main/java/repository/MessageRepositoryImpl.kt +++ b/data/src/main/java/repository/MessageRepositoryImpl.kt @@ -34,7 +34,6 @@ import io.reactivex.Flowable import io.reactivex.Maybe import io.reactivex.Observable import io.reactivex.android.schedulers.AndroidSchedulers -import io.reactivex.rxkotlin.Flowables import io.reactivex.schedulers.Schedulers import io.realm.Realm import io.realm.RealmResults @@ -64,54 +63,24 @@ class MessageRepositoryImpl @Inject constructor( private val cursorToRecipient: CursorToRecipient, private val prefs: Preferences) : MessageRepository { - override fun getConversations(archived: Boolean): Flowable>> { - val realm = Realm.getDefaultInstance() - - val conversationFlowable = realm.where(Conversation::class.java) - .equalTo("archived", archived) - .equalTo("blocked", false) - .findAllAsync() - .asFlowable() - .filter { it.isLoaded } - .map { realm.copyFromRealm(it) } - - val messageFlowable = realm.where(Message::class.java) - .sort("date", Sort.DESCENDING) + override fun getConversations(archived: Boolean): RealmResults { + return Realm.getDefaultInstance() + .where(Message::class.java) .distinctValues("threadId") + .equalTo("conversation.archived", archived) + .equalTo("conversation.blocked", false) + .sort("date", Sort.DESCENDING) .findAllAsync() - .asFlowable() - .filter { it.isLoaded } - .map { realm.copyFromRealm(it) } - - return Flowables.combineLatest(conversationFlowable, messageFlowable, { conversations, messages -> - val conversationMap = conversations.associateBy { conversation -> conversation.id } - - messages.mapNotNull { message -> - val conversation = conversationMap[message.threadId] - if (conversation == null) null else Pair(conversation, message) - } - }) } - override fun getConversationsForWidget(): List> { + override fun getConversationsForWidget(): List { val realm = Realm.getDefaultInstance() - - val conversations = realm.copyFromRealm(realm.where(Conversation::class.java) - .equalTo("archived", false) - .equalTo("blocked", false) - .findAll()) - - val messages = realm.copyFromRealm(realm.where(Message::class.java) - .sort("date", Sort.DESCENDING) + return realm.copyFromRealm(realm.where(Message::class.java) .distinctValues("threadId") - .findAll()) - - val conversationMap = conversations.associateBy { conversation -> conversation.id } - - return messages.mapNotNull { message -> - val conversation = conversationMap[message.threadId] - if (conversation == null) null else Pair(conversation, message) - } + .equalTo("conversation.archived", false) + .equalTo("conversation.blocked", false) + .sort("date", Sort.DESCENDING) + .findAllAsync()) } override fun getBlockedConversations(): Flowable> { @@ -383,7 +352,6 @@ class MessageRepositoryImpl @Inject constructor( // Insert the message to Realm val message = Message().apply { - this.threadId = threadId this.address = address this.body = body this.date = System.currentTimeMillis() @@ -393,10 +361,12 @@ class MessageRepositoryImpl @Inject constructor( type = "sms" read = true seen = true + + conversation = getOrCreateConversation(threadId) } val realm = Realm.getDefaultInstance() var managedMessage: Message? = null - realm.executeTransaction { managedMessage = realm.copyToRealm(message) } + realm.executeTransaction { managedMessage = realm.copyToRealmOrUpdate(message) } // Insert the message to the native content provider val values = ContentValues().apply { @@ -427,14 +397,14 @@ class MessageRepositoryImpl @Inject constructor( this.date = System.currentTimeMillis() id = messageIds.newId() - threadId = getOrCreateConversation(address).blockingGet().id boxId = Telephony.Sms.MESSAGE_TYPE_INBOX type = "sms" + conversation = getOrCreateConversation(address).blockingGet() } val realm = Realm.getDefaultInstance() var managedMessage: Message? = null - realm.executeTransaction { managedMessage = realm.copyToRealm(message) } + realm.executeTransaction { managedMessage = realm.copyToRealmOrUpdate(message) } // Insert the message to the native content provider val values = ContentValues().apply { diff --git a/data/src/main/java/repository/SyncRepositoryImpl.kt b/data/src/main/java/repository/SyncRepositoryImpl.kt index 8b951c5c..c4ab87cb 100644 --- a/data/src/main/java/repository/SyncRepositoryImpl.kt +++ b/data/src/main/java/repository/SyncRepositoryImpl.kt @@ -97,6 +97,17 @@ class SyncRepositoryImpl @Inject constructor( val lastSync = realm.where(Message::class.java)?.max("date")?.toLong() ?: 0 realm.insert(SyncLog()) + + // Sync messages + val messageCursor = cursorToMessage.getMessagesCursor() + val messageColumns = CursorToMessage.MessageColumns(messageCursor) + val messages = messageCursor.mapWhile( + { cursor -> cursorToMessage.map(Pair(cursor, messageColumns)) }, + { message -> message.date > lastSync }) + realm.insertOrUpdate(messages) + messageCursor.close() + + // Sync conversations val conversationCursor = cursorToConversation.getConversationsCursor() val conversations = conversationCursor @@ -112,16 +123,6 @@ class SyncRepositoryImpl @Inject constructor( conversationCursor.close() - // Sync messages - val messageCursor = cursorToMessage.getMessagesCursor() - val messageColumns = CursorToMessage.MessageColumns(messageCursor) - val messages = messageCursor.mapWhile( - { cursor -> cursorToMessage.map(Pair(cursor, messageColumns)) }, - { message -> message.date > lastSync }) - realm.insertOrUpdate(messages) - messageCursor.close() - - // Sync recipients val recipientCursor = cursorToRecipient.getRecipientCursor() val recipients = recipientCursor.map { cursor -> cursorToRecipient.map(cursor) } @@ -167,11 +168,11 @@ class SyncRepositoryImpl @Inject constructor( // Map the cursor to a message return cursor.asFlowable() .map { cursorToMessage.map(Pair(it, columnsMap)) } - .map { message -> - existingId?.let { message.id = it } - message + .doOnNext { message -> existingId?.let { message.id = it } } + .doOnNext { message -> + val threadId = message.conversation?.id + message.conversation = threadId?.let { messageRepo.getOrCreateConversation(threadId) } } - .doOnNext { message -> messageRepo.getOrCreateConversation(message.threadId) } .doOnNext { message -> message.insertOrUpdate() } } diff --git a/domain/src/main/java/interactor/ReceiveMms.kt b/domain/src/main/java/interactor/ReceiveMms.kt index 3aa7eaf0..603fafb4 100644 --- a/domain/src/main/java/interactor/ReceiveMms.kt +++ b/domain/src/main/java/interactor/ReceiveMms.kt @@ -36,7 +36,7 @@ class ReceiveMms @Inject constructor( override fun buildObservable(params: Uri): Flowable<*> { return Flowable.just(params) .flatMap { uri -> syncManager.syncMessage(uri) } // Sync the message - .mapNotNull { message -> messageRepo.getOrCreateConversation(message.threadId) } // Map message to conversation + .mapNotNull { message -> message.conversation } // Map message to conversation .filter { conversation -> !conversation.blocked } // Don't notify for blocked conversations .doOnNext { conversation -> if (conversation.archived) messageRepo.markUnarchived(conversation.id) } // Unarchive conversation if necessary .doOnNext { conversation -> notificationManager.update(conversation.id) } // Update the notification diff --git a/domain/src/main/java/interactor/ReceiveSms.kt b/domain/src/main/java/interactor/ReceiveSms.kt index c9beec5c..04960616 100644 --- a/domain/src/main/java/interactor/ReceiveSms.kt +++ b/domain/src/main/java/interactor/ReceiveSms.kt @@ -43,7 +43,7 @@ class ReceiveSms @Inject constructor( messageRepo.insertReceivedSms(address, body, time) // Add the message to the db } - .mapNotNull { message -> messageRepo.getOrCreateConversation(message.threadId) } // Map message to conversation + .mapNotNull { message -> message.conversation } // Map message to conversation .filter { conversation -> !conversation.blocked } // Don't notify for blocked conversations .doOnNext { conversation -> if (conversation.archived) messageRepo.markUnarchived(conversation.id) } // Unarchive conversation if necessary .doOnNext { conversation -> notificationManager.update(conversation.id) } // Update the notification diff --git a/domain/src/main/java/interactor/RetrySending.kt b/domain/src/main/java/interactor/RetrySending.kt index a349973c..ea3c86f1 100644 --- a/domain/src/main/java/interactor/RetrySending.kt +++ b/domain/src/main/java/interactor/RetrySending.kt @@ -19,6 +19,7 @@ package interactor import io.reactivex.Flowable +import model.Conversation import model.Message import repository.MessageRepository import javax.inject.Inject @@ -32,7 +33,7 @@ class RetrySending @Inject constructor(private val messageRepo: MessageRepositor val message = Message().apply { id = params.id type = params.type - threadId = params.threadId + conversation = params.conversation?.id?.let { threadId -> Conversation().apply { id = threadId } } address = params.address body = params.body } diff --git a/domain/src/main/java/model/Message.kt b/domain/src/main/java/model/Message.kt index 6048c0d6..3c63f304 100644 --- a/domain/src/main/java/model/Message.kt +++ b/domain/src/main/java/model/Message.kt @@ -32,12 +32,26 @@ open class Message : RealmObject() { @PrimaryKey var id: Long = 0 + var conversation: Conversation? = null + set(value) { + field = value + threadId = value?.id ?: 0 + } + + /** + * This is a hack so that we can index by the threadId, and return distinct threadIds when + * querying for all conversations for the MainActivity + */ + @Index + @Deprecated("This is only here for Realm to access - it should be ignored in our codebase") + var threadId: Long = 0 + get() = conversation?.id ?: 0 + // The MMS-SMS content provider returns messages where duplicate ids can exist. This is because // SMS and MMS are stored in separate tables. We can't use these ids as our realm message id // since it's our primary key for the single message object, so we'll store the original id in // case we need to access the original message item in the content provider var contentId: Long = 0 - @Index var threadId: Long = 0 var address: String = "" var boxId: Int = 0 var type: String = "" diff --git a/domain/src/main/java/repository/MessageRepository.kt b/domain/src/main/java/repository/MessageRepository.kt index 0b41cd58..00a24155 100644 --- a/domain/src/main/java/repository/MessageRepository.kt +++ b/domain/src/main/java/repository/MessageRepository.kt @@ -28,9 +28,9 @@ import model.MmsPart interface MessageRepository { - fun getConversations(archived: Boolean = false): Flowable>> + fun getConversations(archived: Boolean = false): RealmResults - fun getConversationsForWidget(): List> + fun getConversationsForWidget(): List fun getBlockedConversations(): Flowable> diff --git a/presentation/src/main/java/common/util/NotificationManagerImpl.kt b/presentation/src/main/java/common/util/NotificationManagerImpl.kt index 3e0140e9..c3a5af68 100644 --- a/presentation/src/main/java/common/util/NotificationManagerImpl.kt +++ b/presentation/src/main/java/common/util/NotificationManagerImpl.kt @@ -138,8 +138,8 @@ class NotificationManagerImpl @Inject constructor( return } - val threadId = message.threadId - val conversation = messageRepo.getConversation(threadId) ?: return + val conversation = message.conversation ?: return + val threadId = conversation.id val contentIntent = Intent(context, ComposeActivity::class.java).putExtra("threadId", threadId) val taskStackBuilder = TaskStackBuilder.create(context) diff --git a/presentation/src/main/java/common/util/filter/ConversationFilter.kt b/presentation/src/main/java/common/util/filter/ConversationFilter.kt index fcc0bdbf..4e3acc0d 100644 --- a/presentation/src/main/java/common/util/filter/ConversationFilter.kt +++ b/presentation/src/main/java/common/util/filter/ConversationFilter.kt @@ -18,13 +18,13 @@ */ package common.util.filter -import model.Conversation +import model.Message import javax.inject.Inject -class ConversationFilter @Inject constructor(private val recipientFilter: RecipientFilter) : Filter() { +class ConversationFilter @Inject constructor(private val recipientFilter: RecipientFilter) : Filter() { - override fun filter(item: Conversation, query: CharSequence): Boolean { - return item.recipients.any { recipient -> recipientFilter.filter(recipient, query) } + override fun filter(item: Message, query: CharSequence): Boolean { + return item.conversation?.recipients?.any { recipient -> recipientFilter.filter(recipient, query) } ?: false } } \ No newline at end of file diff --git a/presentation/src/main/java/feature/conversations/ConversationsAdapter.kt b/presentation/src/main/java/feature/conversations/ConversationsAdapter.kt index a1521620..c8cba8a1 100644 --- a/presentation/src/main/java/feature/conversations/ConversationsAdapter.kt +++ b/presentation/src/main/java/feature/conversations/ConversationsAdapter.kt @@ -25,20 +25,19 @@ import android.view.ViewGroup import com.jakewharton.rxbinding2.view.clicks import com.jakewharton.rxbinding2.view.longClicks import com.moez.QKSMS.R +import common.Navigator +import common.base.QkViewHolder import common.util.Colors import common.util.DateFormatter import io.reactivex.disposables.CompositeDisposable import io.reactivex.rxkotlin.plusAssign import io.reactivex.subjects.PublishSubject import io.reactivex.subjects.Subject +import io.realm.RealmRecyclerViewAdapter import kotlinx.android.synthetic.main.conversation_list_item.view.* import model.Contact -import model.Conversation import model.Message import model.PhoneNumber -import common.Navigator -import common.base.FlowableAdapter -import common.base.QkViewHolder import repository.MessageRepository import javax.inject.Inject @@ -48,7 +47,7 @@ class ConversationsAdapter @Inject constructor( val messageRepo: MessageRepository, val dateFormatter: DateFormatter, val colors: Colors -) : FlowableAdapter>() { +) : RealmRecyclerViewAdapter(null, true) { val clicks: Subject = PublishSubject.create() val longClicks: Subject = PublishSubject.create() @@ -78,8 +77,8 @@ class ConversationsAdapter @Inject constructor( } override fun onBindViewHolder(viewHolder: QkViewHolder, position: Int) { - val conversation = getItem(position).first - val message = getItem(position).second + val message = getItem(position)!! + val conversation = message.conversation!! val view = viewHolder.itemView view.clicks().subscribe { clicks.onNext(conversation.id) } @@ -94,16 +93,6 @@ class ConversationsAdapter @Inject constructor( } override fun getItemViewType(position: Int): Int { - return if (getItem(position).second.read) 0 else 1 - } - - override fun areItemsTheSame(old: Pair, new: Pair): Boolean { - return old.first.id == new.first.id - } - - override fun areContentsTheSame(old: Pair, new: Pair): Boolean { - return old.second.id == new.second.id && - old.second.read == new.second.read && - old.first.recipients == new.first.recipients + return if (getItem(position)!!.read) 0 else 1 } } \ No newline at end of file diff --git a/presentation/src/main/java/feature/gallery/GalleryViewModel.kt b/presentation/src/main/java/feature/gallery/GalleryViewModel.kt index 37a63ff7..61aa5546 100644 --- a/presentation/src/main/java/feature/gallery/GalleryViewModel.kt +++ b/presentation/src/main/java/feature/gallery/GalleryViewModel.kt @@ -23,13 +23,13 @@ import android.content.Intent import com.moez.QKSMS.R import com.uber.autodispose.android.lifecycle.scope import com.uber.autodispose.kotlin.autoDisposable -import injection.appComponent +import common.base.QkViewModel import common.util.extensions.makeToast +import injection.appComponent import interactor.SaveImage import io.reactivex.Flowable import io.reactivex.rxkotlin.plusAssign import io.reactivex.rxkotlin.withLatestFrom -import common.base.QkViewModel import repository.MessageRepository import util.extensions.mapNotNull import javax.inject.Inject @@ -54,7 +54,7 @@ class GalleryViewModel(intent: Intent) : QkViewModel( disposables += partIdFlowable .mapNotNull { partId -> messageRepo.getMessageForPart(partId) } - .mapNotNull { message -> messageRepo.getConversation(message.threadId) } + .mapNotNull { message -> message.conversation } .subscribe { conversation -> newState { it.copy(title = conversation.getTitle()) } } } diff --git a/presentation/src/main/java/feature/main/MainActivity.kt b/presentation/src/main/java/feature/main/MainActivity.kt index 870a2af1..b80f0f1a 100644 --- a/presentation/src/main/java/feature/main/MainActivity.kt +++ b/presentation/src/main/java/feature/main/MainActivity.kt @@ -199,7 +199,7 @@ class MainActivity : QkThemedActivity(), MainView { is Inbox -> { if (!inbox.isSelected) toolbarSearch.text = null if (recyclerView.adapter !== conversationsAdapter) recyclerView.adapter = conversationsAdapter - conversationsAdapter.flowable = state.page.data + conversationsAdapter.updateData(state.page.data) itemTouchHelper.attachToRecyclerView(recyclerView) menuItemAdapter.data = state.page.menu empty.setText(R.string.inbox_empty_text) @@ -210,7 +210,7 @@ class MainActivity : QkThemedActivity(), MainView { is Archived -> { if (!archived.isSelected) toolbarSearch.setText(R.string.title_archived) if (recyclerView.adapter !== conversationsAdapter) recyclerView.adapter = conversationsAdapter - conversationsAdapter.flowable = state.page.data + conversationsAdapter.updateData(state.page.data) itemTouchHelper.attachToRecyclerView(null) menuItemAdapter.data = state.page.menu empty.setText(R.string.archived_empty_text) diff --git a/presentation/src/main/java/feature/main/MainState.kt b/presentation/src/main/java/feature/main/MainState.kt index 1ec3a68b..210664ed 100644 --- a/presentation/src/main/java/feature/main/MainState.kt +++ b/presentation/src/main/java/feature/main/MainState.kt @@ -18,10 +18,9 @@ */ package feature.main -import io.reactivex.Flowable -import model.Conversation -import model.Message import common.MenuItem +import io.realm.RealmResults +import model.Message data class MainState( val page: MainPage = Inbox(), @@ -33,13 +32,13 @@ sealed class MainPage data class Inbox( val showClearButton: Boolean = false, - val data: Flowable>>? = null, + val data: RealmResults? = null, val empty: Boolean = false, val menu: List = ArrayList(), val showArchivedSnackbar: Boolean = false) : MainPage() data class Archived( - val data: Flowable>>?, + val data: RealmResults? = null, val empty: Boolean = false, val menu: List = ArrayList()) : MainPage() diff --git a/presentation/src/main/java/feature/main/MainViewModel.kt b/presentation/src/main/java/feature/main/MainViewModel.kt index 70907a4e..5c0c198f 100644 --- a/presentation/src/main/java/feature/main/MainViewModel.kt +++ b/presentation/src/main/java/feature/main/MainViewModel.kt @@ -26,8 +26,11 @@ import android.support.v4.content.ContextCompat import com.moez.QKSMS.R import com.uber.autodispose.android.lifecycle.scope import com.uber.autodispose.kotlin.autoDisposable -import injection.appComponent +import common.MenuItem +import common.Navigator +import common.base.QkViewModel import common.util.filter.ConversationFilter +import injection.appComponent import interactor.DeleteConversation import interactor.MarkAllSeen import interactor.MarkArchived @@ -39,11 +42,10 @@ import io.reactivex.rxkotlin.plusAssign import io.reactivex.rxkotlin.withLatestFrom import io.realm.Realm import model.SyncLog -import common.MenuItem -import common.Navigator -import common.base.QkViewModel import repository.MessageRepository import util.Preferences +import util.extensions.asObservable +import util.extensions.mapNotNull import util.extensions.toFlowable import java.util.concurrent.TimeUnit import javax.inject.Inject @@ -63,23 +65,27 @@ class MainViewModel : QkViewModel(MainState()) { @Inject lateinit var prefs: Preferences private val conversations by lazy { - messageRepo.getConversations() - .withLatestFrom(state.toFlowable(), { conversations, state -> - (state.page as? Inbox)?.let { page -> - newState { it.copy(page = page.copy(empty = conversations.isEmpty())) } - } - conversations - }) + messageRepo.getConversations().also { + it.asFlowable() + .withLatestFrom(state.toFlowable(), { conversations, state -> + (state.page as? Inbox)?.let { page -> + newState { it.copy(page = page.copy(empty = conversations.isEmpty())) } + } + conversations + }) + } } private val archivedConversations by lazy { - messageRepo.getConversations(true) - .withLatestFrom(state.toFlowable(), { conversations, state -> - (state.page as? Archived)?.let { page -> - newState { it.copy(page = page.copy(empty = conversations.isEmpty())) } - } - conversations - }) + messageRepo.getConversations(true).also { + it.asFlowable() + .withLatestFrom(state.toFlowable(), { conversations, state -> + (state.page as? Archived)?.let { page -> + newState { it.copy(page = page.copy(empty = conversations.isEmpty())) } + } + conversations + }) + } } private val menuArchive by lazy { MenuItem(context.getString(R.string.menu_archive), 0) } @@ -137,11 +143,11 @@ class MainViewModel : QkViewModel(MainState()) { if (state.page is Inbox) { val conversations = when { query.isEmpty() -> conversations - else -> conversations.map { list -> list.filter { conversationFilter.filter(it.first, query) } } + else -> conversations.filter { conversationFilter.filter(it, query) } } - val page = state.page.copy(showClearButton = query.isNotEmpty(), data = conversations) - newState { it.copy(page = page) } + //val page = state.page.copy(showClearButton = query.isNotEmpty(), data = conversations) + //newState { it.copy(page = page) } } }) .autoDisposable(view.scope()) @@ -230,9 +236,8 @@ class MainViewModel : QkViewModel(MainState()) { .subscribe { threadId -> deleteConversation.execute(threadId) } val swipedConversation = view.swipeConversationIntent - .withLatestFrom(conversations.toObservable(), { position, conversations -> conversations[position] }) - .map { pair -> pair.first } - .map { conversation -> conversation.id } + .withLatestFrom(conversations.asObservable(), { position, conversations -> conversations[position] }) + .mapNotNull { message -> message?.conversation?.id } swipedConversation .withLatestFrom(state, { threadId, state -> diff --git a/presentation/src/main/java/feature/widget/WidgetAdapter.kt b/presentation/src/main/java/feature/widget/WidgetAdapter.kt index 05a25fdd..7f87826d 100644 --- a/presentation/src/main/java/feature/widget/WidgetAdapter.kt +++ b/presentation/src/main/java/feature/widget/WidgetAdapter.kt @@ -38,7 +38,6 @@ import common.util.extensions.dpToPx import feature.main.MainActivity import injection.appComponent import model.Contact -import model.Conversation import model.Message import model.PhoneNumber import repository.MessageRepository @@ -57,7 +56,7 @@ class WidgetAdapter(intent: Intent) : RemoteViewsService.RemoteViewsFactory { private val appWidgetId = intent.getIntExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, AppWidgetManager.INVALID_APPWIDGET_ID) private val smallWidget = intent.getBooleanExtra("small_widget", false) - private var conversations: List> = listOf() + private var conversations: List = listOf() private val appWidgetManager by lazy { AppWidgetManager.getInstance(context) } // Cache colors, load lazily. This entire class is recreated when the widget is recreated, @@ -98,8 +97,8 @@ class WidgetAdapter(intent: Intent) : RemoteViewsService.RemoteViewsFactory { } private fun getConversationView(position: Int): RemoteViews { - val conversation = conversations[position].first - val message = conversations[position].second + val message = conversations[position] + val conversation = message.conversation!! val remoteViews = RemoteViews(context.packageName, R.layout.widget_list_item)