Launcher widget

This commit is contained in:
Moez Bhatti
2018-03-27 01:12:30 -04:00
parent 38ea0f8a78
commit bb8b2e6cdc
17 changed files with 718 additions and 1 deletions
@@ -0,0 +1,200 @@
/*
* Copyright (C) 2017 Moez Bhatti <moez.bhatti@gmail.com>
*
* This file is part of QKSMS.
*
* QKSMS is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* QKSMS is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with QKSMS. If not, see <http://www.gnu.org/licenses/>.
*/
package feature.widget
import android.app.PendingIntent
import android.appwidget.AppWidgetManager
import android.content.Context
import android.content.Intent
import android.graphics.Typeface
import android.telephony.PhoneNumberUtils
import android.text.Spannable
import android.text.SpannableString
import android.text.style.StyleSpan
import android.view.View
import android.widget.RemoteViews
import android.widget.RemoteViewsService
import com.moez.QKSMS.R
import common.util.Colors
import common.util.DateFormatter
import common.util.GlideApp
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
import javax.inject.Inject
class WidgetAdapter(intent: Intent) : RemoteViewsService.RemoteViewsFactory {
companion object {
private const val MAX_CONVERSATIONS_COUNT = 25
}
@Inject lateinit var context: Context
@Inject lateinit var colors: Colors
@Inject lateinit var dateFormatter: DateFormatter
@Inject lateinit var messageRepo: MessageRepository
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<Pair<Conversation, Message>> = listOf()
private val appWidgetManager by lazy { AppWidgetManager.getInstance(context) }
// Cache colors, load lazily. This entire class is recreated when the widget is recreated,
// so if the theme changes then these values will be correct in the new instance
private val theme by lazy { colors.theme.blockingFirst() }
private val background by lazy { colors.background.blockingFirst() }
private val textPrimary by lazy { colors.textPrimary.blockingFirst() }
private val textSecondary by lazy { colors.textSecondary.blockingFirst() }
private val textTertiary by lazy { colors.textTertiary.blockingFirst() }
private val textPrimaryOnTheme by lazy { colors.textPrimaryOnTheme.blockingFirst() }
override fun onCreate() {
appComponent.inject(this)
}
override fun onDataSetChanged() {
conversations = messageRepo.getConversationsForWidget()
val remoteViews = RemoteViews(context.packageName, R.layout.widget)
appWidgetManager.partiallyUpdateAppWidget(appWidgetId, remoteViews)
}
/**
* Returns the number of rows to show. If there are more conversations than the max count,
* we'll return the max count + 1, where the last row just shows "View more conversations"
*/
override fun getCount(): Int {
val count = Math.min(conversations.size, MAX_CONVERSATIONS_COUNT)
val shouldShowViewMore = count < conversations.size
return count + if (shouldShowViewMore) 1 else 0
}
override fun getViewAt(position: Int): RemoteViews {
return when {
position >= MAX_CONVERSATIONS_COUNT -> getOverflowView()
else -> getConversationView(position)
}
}
private fun getConversationView(position: Int): RemoteViews {
val conversation = conversations[position].first
val message = conversations[position].second
val remoteViews = RemoteViews(context.packageName, R.layout.widget_list_item)
// Avatar
remoteViews.setViewVisibility(R.id.avatar, if (smallWidget) View.GONE else View.VISIBLE)
remoteViews.setInt(R.id.avatar, "setBackgroundColor", theme)
remoteViews.setTextColor(R.id.initial, textPrimaryOnTheme)
remoteViews.setInt(R.id.icon, "setColorFilter", textPrimaryOnTheme)
remoteViews.setInt(R.id.avatarMask, "setColorFilter", background)
val contact = conversation.recipients.map { recipient ->
recipient.contact ?: Contact().apply { numbers.add(PhoneNumber().apply { address = recipient.address }) }
}.firstOrNull()
// Use the icon if there's no name, otherwise show an initial
if (contact?.name.orEmpty().isNotEmpty()) {
remoteViews.setTextViewText(R.id.initial, contact?.name?.substring(0, 1))
remoteViews.setViewVisibility(R.id.icon, View.GONE)
} else {
remoteViews.setTextViewText(R.id.initial, null)
remoteViews.setViewVisibility(R.id.icon, View.VISIBLE)
}
remoteViews.setImageViewBitmap(R.id.photo, null)
contact?.numbers?.firstOrNull()?.address?.let { address ->
val futureGet = GlideApp.with(context)
.asBitmap()
.load(PhoneNumberUtils.stripSeparators(address))
.submit(48.dpToPx(context), 48.dpToPx(context))
try {
remoteViews.setImageViewBitmap(R.id.photo, futureGet.get())
} catch (e: Exception) {
}
}
// Name
remoteViews.setTextColor(R.id.name, textPrimary)
remoteViews.setTextViewText(R.id.name, boldText(conversation.getTitle(), !message.read))
// Date
remoteViews.setTextColor(R.id.date, if (message.read) textTertiary else textPrimary)
remoteViews.setTextViewText(R.id.date, boldText(dateFormatter.getConversationTimestamp(message.date), !message.read))
// Snippet
remoteViews.setTextColor(R.id.snippet, if (message.read) textTertiary else textPrimary)
remoteViews.setTextViewText(R.id.snippet, boldText(message.getSummary(), !message.read))
// Launch conversation on click
val clickIntent = Intent().putExtra("threadId", conversation.id)
remoteViews.setOnClickFillInIntent(R.id.conversation, clickIntent)
return remoteViews
}
private fun getOverflowView(): RemoteViews {
val view = RemoteViews(context.packageName, R.layout.widget_loading)
val intent = Intent(context, MainActivity::class.java)
val pendingIntent = PendingIntent.getActivity(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT)
view.setTextColor(R.id.loadingText, textSecondary)
view.setTextViewText(R.id.loadingText, context.getString(R.string.widget_more))
view.setOnClickPendingIntent(R.id.loading, pendingIntent)
return view
}
private fun boldText(text: String, shouldBold: Boolean): CharSequence {
return if (shouldBold) {
SpannableString(text).apply {
setSpan(StyleSpan(Typeface.BOLD), 0, length, Spannable.SPAN_INCLUSIVE_INCLUSIVE)
}
} else {
text
}
}
override fun getLoadingView(): RemoteViews {
val view = RemoteViews(context.packageName, R.layout.widget_loading)
view.setTextViewText(R.id.loadingText, context.getText(R.string.widget_loading))
return view
}
override fun getViewTypeCount(): Int {
return 2
}
override fun getItemId(position: Int): Long {
return position.toLong()
}
override fun hasStableIds(): Boolean {
return false
}
override fun onDestroy() {
}
}
@@ -0,0 +1,140 @@
/*
* Copyright (C) 2017 Moez Bhatti <moez.bhatti@gmail.com>
*
* This file is part of QKSMS.
*
* QKSMS is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* QKSMS is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with QKSMS. If not, see <http://www.gnu.org/licenses/>.
*/
package feature.widget
import android.app.PendingIntent
import android.appwidget.AppWidgetManager
import android.appwidget.AppWidgetProvider
import android.content.ComponentName
import android.content.Context
import android.content.Intent
import android.net.Uri
import android.os.Bundle
import android.widget.RemoteViews
import com.moez.QKSMS.R
import common.util.Colors
import feature.compose.ComposeActivity
import feature.main.MainActivity
import injection.appComponent
import manager.WidgetManager
import timber.log.Timber
import javax.inject.Inject
class WidgetProvider : AppWidgetProvider() {
@Inject lateinit var colors: Colors
init {
appComponent.inject(this)
}
override fun onReceive(context: Context, intent: Intent) {
when (intent.action) {
WidgetManager.ACTION_NOTIFY_DATASET_CHANGED -> updateData(context)
else -> super.onReceive(context, intent)
}
}
/**
* Update all widgets in the list
*/
override fun onUpdate(context: Context, appWidgetManager: AppWidgetManager, appWidgetIds: IntArray) {
super.onUpdate(context, appWidgetManager, appWidgetIds)
for (appWidgetId in appWidgetIds) {
updateWidget(context, appWidgetId, isSmallWidget(appWidgetManager, appWidgetId))
}
}
/**
* Notify all the widgets that they should update their adapter data
*/
private fun updateData(context: Context) {
val appWidgetManager = AppWidgetManager.getInstance(context)
val appWidgetIds = appWidgetManager.getAppWidgetIds(ComponentName(context, WidgetProvider::class.java))
// We need to update all Mms appwidgets on the home screen.
appWidgetManager.notifyAppWidgetViewDataChanged(appWidgetIds, R.id.conversations)
}
/**
* Update widget when widget size changes
*/
override fun onAppWidgetOptionsChanged(context: Context, appWidgetManager: AppWidgetManager, appWidgetId: Int, newOptions: Bundle) {
updateWidget(context, appWidgetId, isSmallWidget(appWidgetManager, appWidgetId))
super.onAppWidgetOptionsChanged(context, appWidgetManager, appWidgetId, newOptions)
}
/**
* Returns true when widget has less than 4 columns, else false
*/
private fun isSmallWidget(appWidgetManager: AppWidgetManager, appWidgetId: Int): Boolean {
val options = appWidgetManager.getAppWidgetOptions(appWidgetId)
val size = options.getInt(AppWidgetManager.OPTION_APPWIDGET_MIN_WIDTH)
var n = 2
while (70 * n - 30 < size) {
++n
}
val columns = n - 1
return columns < 4
}
/**
* Update the widget appWidgetId
*/
private fun updateWidget(context: Context, appWidgetId: Int, smallWidget: Boolean) {
Timber.v("updateWidget appWidgetId: $appWidgetId")
val remoteViews = RemoteViews(context.packageName, R.layout.widget)
// Apply colors from theme
remoteViews.setInt(R.id.background, "setColorFilter", colors.background.blockingFirst())
remoteViews.setInt(R.id.toolbar, "setColorFilter", colors.toolbarColor.blockingFirst())
remoteViews.setTextColor(R.id.title, colors.textPrimary.blockingFirst())
remoteViews.setInt(R.id.compose, "setColorFilter", colors.theme.blockingFirst())
// Set adapter for conversations
val intent = Intent(context, WidgetService::class.java)
.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId)
.putExtra("small_widget", smallWidget)
intent.data = Uri.parse(intent.toUri(Intent.URI_INTENT_SCHEME))
remoteViews.setRemoteAdapter(R.id.conversations, intent)
// Main intent
val mainIntent = Intent(context, MainActivity::class.java)
val mainPI = PendingIntent.getActivity(context, 0, mainIntent, PendingIntent.FLAG_UPDATE_CURRENT)
remoteViews.setOnClickPendingIntent(R.id.title, mainPI)
// Compose intent
val composeIntent = Intent(context, ComposeActivity::class.java)
val composePI = PendingIntent.getActivity(context, 0, composeIntent, PendingIntent.FLAG_UPDATE_CURRENT)
remoteViews.setOnClickPendingIntent(R.id.compose, composePI)
// Conversation intent
val startActivityIntent = Intent(context, ComposeActivity::class.java)
val startActivityPendingIntent = PendingIntent.getActivity(context, 0, startActivityIntent, PendingIntent.FLAG_UPDATE_CURRENT)
remoteViews.setPendingIntentTemplate(R.id.conversations, startActivityPendingIntent)
AppWidgetManager.getInstance(context).updateAppWidget(appWidgetId, remoteViews)
}
}
@@ -0,0 +1,31 @@
/*
* Copyright (C) 2017 Moez Bhatti <moez.bhatti@gmail.com>
*
* This file is part of QKSMS.
*
* QKSMS is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* QKSMS is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with QKSMS. If not, see <http://www.gnu.org/licenses/>.
*/
package feature.widget
import android.content.Intent
import android.widget.RemoteViewsService
class WidgetService : RemoteViewsService() {
override fun onGetViewFactory(intent: Intent): RemoteViewsService.RemoteViewsFactory {
return WidgetAdapter(intent)
}
}
@@ -20,7 +20,6 @@ package injection
import common.QKApplication
import common.util.ContactImageLoader
import dagger.Component
import common.widget.AvatarView
import common.widget.MmsPreviewView
import common.widget.PagerTitleView
@@ -29,6 +28,7 @@ import common.widget.QkEditText
import common.widget.QkSwitch
import common.widget.QkTextView
import common.widget.Separator
import dagger.Component
import feature.blocked.BlockedActivity
import feature.blocked.BlockedViewModel
import feature.compose.ComposeActivity
@@ -50,6 +50,8 @@ import feature.setup.SetupActivity
import feature.setup.SetupViewModel
import feature.themepicker.ThemePickerActivity
import feature.themepicker.ThemePickerViewModel
import feature.widget.WidgetAdapter
import feature.widget.WidgetProvider
import receiver.DefaultSmsChangedReceiver
import receiver.MarkReadReceiver
import receiver.MarkSeenReceiver
@@ -93,6 +95,9 @@ interface AppComponent {
fun inject(receiver: RemoteMessagingReceiver)
fun inject(receiver: SmsProviderChangedReceiver)
fun inject(receiver: SmsReceiver)
fun inject(receiver: WidgetProvider)
fun inject(service: WidgetAdapter)
fun inject(view: AvatarView)
fun inject(view: DetailedChipView)