Repo created

This commit is contained in:
Fr4nz D13trich 2025-11-24 18:55:42 +01:00
parent a629de6271
commit 3cef7c5092
2161 changed files with 246605 additions and 2 deletions

View file

@ -0,0 +1,27 @@
plugins {
id(ThunderbirdPlugins.Library.android)
}
dependencies {
implementation(projects.app.ui.legacy)
implementation(projects.app.core)
implementation(libs.timber)
}
android {
namespace = "app.k9mail.ui.widget.list"
buildFeatures {
buildConfig = true
}
buildTypes {
debug {
manifestPlaceholders["appAuthRedirectScheme"] = "FIXME: override this in your app project"
}
release {
manifestPlaceholders["appAuthRedirectScheme"] = "FIXME: override this in your app project"
}
}
}

View file

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<application>
<service
android:name=".MessageListWidgetService"
android:permission="android.permission.BIND_REMOTEVIEWS" />
</application>
</manifest>

View file

@ -0,0 +1,8 @@
package app.k9mail.ui.widget.list
import org.koin.dsl.module
val messageListWidgetModule = module {
single { MessageListWidgetManager(context = get(), messageListRepository = get(), config = get()) }
factory { MessageListLoader(preferences = get(), messageListRepository = get(), messageHelper = get()) }
}

View file

@ -0,0 +1,12 @@
package app.k9mail.ui.widget.list
import com.fsck.k9.Account.SortType
import com.fsck.k9.search.LocalSearch
internal data class MessageListConfig(
val search: LocalSearch,
val showingThreadedList: Boolean,
val sortType: SortType,
val sortAscending: Boolean,
val sortDateAscending: Boolean
)

View file

@ -0,0 +1,22 @@
package app.k9mail.ui.widget.list
import com.fsck.k9.controller.MessageReference
internal data class MessageListItem(
val displayName: String,
val displayDate: String,
val subject: String,
val preview: String,
val isRead: Boolean,
val hasAttachments: Boolean,
val threadCount: Int,
val accountColor: Int,
val messageReference: MessageReference,
val uniqueId: Long,
val sortSubject: String?,
val sortMessageDate: Long,
val sortInternalDate: Long,
val sortIsStarred: Boolean,
val sortDatabaseId: Long
)

View file

@ -0,0 +1,62 @@
package app.k9mail.ui.widget.list
import com.fsck.k9.Account
import com.fsck.k9.controller.MessageReference
import com.fsck.k9.helper.MessageHelper
import com.fsck.k9.mailstore.MessageDetailsAccessor
import com.fsck.k9.mailstore.MessageMapper
import com.fsck.k9.ui.helper.DisplayAddressHelper
import java.util.Calendar
import java.util.Locale
internal class MessageListItemMapper(
private val messageHelper: MessageHelper,
private val account: Account
) : MessageMapper<MessageListItem> {
private val calendar: Calendar = Calendar.getInstance()
override fun map(message: MessageDetailsAccessor): MessageListItem {
val fromAddresses = message.fromAddresses
val toAddresses = message.toAddresses
val previewResult = message.preview
val previewText = if (previewResult.isPreviewTextAvailable) previewResult.previewText else ""
val uniqueId = createUniqueId(account, message.id)
val showRecipients = DisplayAddressHelper.shouldShowRecipients(account, message.folderId)
val displayAddress = if (showRecipients) toAddresses.firstOrNull() else fromAddresses.firstOrNull()
val displayName = if (showRecipients) {
messageHelper.getRecipientDisplayNames(toAddresses.toTypedArray()).toString()
} else {
messageHelper.getSenderDisplayName(displayAddress).toString()
}
return MessageListItem(
displayName = displayName,
displayDate = formatDate(message.messageDate),
subject = message.subject.orEmpty(),
preview = previewText,
isRead = message.isRead,
hasAttachments = message.hasAttachments,
threadCount = message.threadCount,
accountColor = account.chipColor,
messageReference = MessageReference(account.uuid, message.folderId, message.messageServerId),
uniqueId = uniqueId,
sortSubject = message.subject,
sortMessageDate = message.messageDate,
sortInternalDate = message.internalDate,
sortIsStarred = message.isStarred,
sortDatabaseId = message.id
)
}
private fun formatDate(date: Long): String {
calendar.timeInMillis = date
val dayOfMonth = calendar.get(Calendar.DAY_OF_MONTH)
val month = calendar.getDisplayName(Calendar.MONTH, Calendar.SHORT, Locale.getDefault())
return String.format("%d %s", dayOfMonth, month)
}
private fun createUniqueId(account: Account, messageId: Long): Long {
return ((account.accountNumber + 1).toLong() shl 52) + messageId
}
}

View file

@ -0,0 +1,149 @@
package app.k9mail.ui.widget.list
import com.fsck.k9.Account
import com.fsck.k9.Account.SortType
import com.fsck.k9.Preferences
import com.fsck.k9.helper.MessageHelper
import com.fsck.k9.mailstore.MessageColumns
import com.fsck.k9.mailstore.MessageListRepository
import com.fsck.k9.search.SqlQueryBuilder
import com.fsck.k9.search.getAccounts
import timber.log.Timber
internal class MessageListLoader(
private val preferences: Preferences,
private val messageListRepository: MessageListRepository,
private val messageHelper: MessageHelper
) {
fun getMessageList(config: MessageListConfig): List<MessageListItem> {
return try {
getMessageListInfo(config)
} catch (e: Exception) {
Timber.e(e, "Error while fetching message list")
// TODO: Return an error object instead of an empty list
emptyList()
}
}
private fun getMessageListInfo(config: MessageListConfig): List<MessageListItem> {
val accounts = config.search.getAccounts(preferences)
val messageListItems = accounts
.flatMap { account ->
loadMessageListForAccount(account, config)
}
.sortedWith(config)
return messageListItems
}
private fun loadMessageListForAccount(account: Account, config: MessageListConfig): List<MessageListItem> {
val accountUuid = account.uuid
val sortOrder = buildSortOrder(config)
val mapper = MessageListItemMapper(messageHelper, account)
return if (config.showingThreadedList) {
val (selection, selectionArgs) = buildSelection(account, config)
messageListRepository.getThreadedMessages(accountUuid, selection, selectionArgs, sortOrder, mapper)
} else {
val (selection, selectionArgs) = buildSelection(account, config)
messageListRepository.getMessages(accountUuid, selection, selectionArgs, sortOrder, mapper)
}
}
private fun buildSelection(account: Account, config: MessageListConfig): Pair<String, Array<String>> {
val query = StringBuilder()
val queryArgs = mutableListOf<String>()
SqlQueryBuilder.buildWhereClause(config.search.conditions, query, queryArgs)
val selection = query.toString()
val selectionArgs = queryArgs.toTypedArray()
return selection to selectionArgs
}
private fun buildSortOrder(config: MessageListConfig): String {
val sortColumn = when (config.sortType) {
SortType.SORT_ARRIVAL -> MessageColumns.INTERNAL_DATE
SortType.SORT_ATTACHMENT -> "(${MessageColumns.ATTACHMENT_COUNT} < 1)"
SortType.SORT_FLAGGED -> "(${MessageColumns.FLAGGED} != 1)"
SortType.SORT_SENDER -> MessageColumns.SENDER_LIST // FIXME
SortType.SORT_SUBJECT -> "${MessageColumns.SUBJECT} COLLATE NOCASE"
SortType.SORT_UNREAD -> MessageColumns.READ
SortType.SORT_DATE -> MessageColumns.DATE
else -> MessageColumns.DATE
}
val sortDirection = if (config.sortAscending) " ASC" else " DESC"
val secondarySort = if (config.sortType == SortType.SORT_DATE || config.sortType == SortType.SORT_ARRIVAL) {
""
} else {
if (config.sortDateAscending) {
"${MessageColumns.DATE} ASC, "
} else {
"${MessageColumns.DATE} DESC, "
}
}
return "$sortColumn$sortDirection, $secondarySort${MessageColumns.ID} DESC"
}
private fun List<MessageListItem>.sortedWith(config: MessageListConfig): List<MessageListItem> {
val comparator = when (config.sortType) {
SortType.SORT_DATE -> {
compareBy(config.sortAscending) { it.sortMessageDate }
}
SortType.SORT_ARRIVAL -> {
compareBy(config.sortAscending) { it.sortInternalDate }
}
SortType.SORT_SUBJECT -> {
compareStringBy<MessageListItem>(config.sortAscending) { it.sortSubject.orEmpty() }
.thenByDate(config)
}
SortType.SORT_SENDER -> {
compareStringBy<MessageListItem>(config.sortAscending) { it.displayName }
.thenByDate(config)
}
SortType.SORT_UNREAD -> {
compareBy<MessageListItem>(config.sortAscending) { it.isRead }
.thenByDate(config)
}
SortType.SORT_FLAGGED -> {
compareBy<MessageListItem>(!config.sortAscending) { it.sortIsStarred }
.thenByDate(config)
}
SortType.SORT_ATTACHMENT -> {
compareBy<MessageListItem>(!config.sortAscending) { it.hasAttachments }
.thenByDate(config)
}
}.thenByDescending { it.sortDatabaseId }
return this.sortedWith(comparator)
}
}
private inline fun <T> compareBy(sortAscending: Boolean, crossinline selector: (T) -> Comparable<*>?): Comparator<T> {
return if (sortAscending) {
compareBy(selector)
} else {
compareByDescending(selector)
}
}
private inline fun <T> compareStringBy(sortAscending: Boolean, crossinline selector: (T) -> String): Comparator<T> {
return if (sortAscending) {
compareBy(String.CASE_INSENSITIVE_ORDER, selector)
} else {
compareByDescending(String.CASE_INSENSITIVE_ORDER, selector)
}
}
private fun Comparator<MessageListItem>.thenByDate(config: MessageListConfig): Comparator<MessageListItem> {
return if (config.sortDateAscending) {
thenBy { it.sortMessageDate }
} else {
thenByDescending { it.sortMessageDate }
}
}

View file

@ -0,0 +1,127 @@
package app.k9mail.ui.widget.list
import android.content.Context
import android.graphics.Typeface
import android.text.SpannableString
import android.text.style.StyleSpan
import android.view.View
import android.widget.RemoteViews
import android.widget.RemoteViewsService.RemoteViewsFactory
import androidx.core.content.ContextCompat
import com.fsck.k9.Account.SortType
import com.fsck.k9.K9
import com.fsck.k9.activity.MessageList
import com.fsck.k9.search.LocalSearch
import com.fsck.k9.search.SearchAccount
import org.koin.core.component.KoinComponent
import org.koin.core.component.inject
import com.fsck.k9.ui.R as UiR
internal class MessageListRemoteViewFactory(private val context: Context) : RemoteViewsFactory, KoinComponent {
private val messageListLoader: MessageListLoader by inject()
private lateinit var unifiedInboxSearch: LocalSearch
private var messageListItems = emptyList<MessageListItem>()
private var senderAboveSubject = false
private var readTextColor = 0
private var unreadTextColor = 0
override fun onCreate() {
unifiedInboxSearch = SearchAccount.createUnifiedInboxAccount().relatedSearch
senderAboveSubject = K9.isMessageListSenderAboveSubject
readTextColor = ContextCompat.getColor(context, R.color.message_list_widget_text_read)
unreadTextColor = ContextCompat.getColor(context, R.color.message_list_widget_text_unread)
}
override fun onDataSetChanged() {
loadMessageList()
}
private fun loadMessageList() {
// TODO: Use same sort order that is used for the Unified Inbox inside the app
val messageListConfig = MessageListConfig(
search = unifiedInboxSearch,
showingThreadedList = K9.isThreadedViewEnabled,
sortType = SortType.SORT_DATE,
sortAscending = false,
sortDateAscending = false
)
messageListItems = messageListLoader.getMessageList(messageListConfig)
}
override fun onDestroy() = Unit
override fun getCount(): Int = messageListItems.size
override fun getViewAt(position: Int): RemoteViews {
val remoteView = RemoteViews(context.packageName, R.layout.message_list_widget_list_item)
val item = messageListItems[position]
val displayName = if (item.isRead) item.displayName else bold(item.displayName)
val subject = if (item.isRead) item.subject else bold(item.subject)
if (senderAboveSubject) {
remoteView.setTextViewText(R.id.sender, displayName)
remoteView.setTextViewText(R.id.mail_subject, subject)
} else {
remoteView.setTextViewText(R.id.sender, subject)
remoteView.setTextViewText(R.id.mail_subject, displayName)
}
remoteView.setTextViewText(R.id.mail_date, item.displayDate)
remoteView.setTextViewText(R.id.mail_preview, item.preview)
if (item.threadCount > 1) {
remoteView.setTextViewText(R.id.thread_count, item.threadCount.toString())
remoteView.setInt(R.id.thread_count, "setVisibility", View.VISIBLE)
} else {
remoteView.setInt(R.id.thread_count, "setVisibility", View.GONE)
}
val textColor = getTextColor(item)
remoteView.setTextColor(R.id.sender, textColor)
remoteView.setTextColor(R.id.mail_subject, textColor)
remoteView.setTextColor(R.id.mail_date, textColor)
remoteView.setTextColor(R.id.mail_preview, textColor)
if (item.hasAttachments) {
remoteView.setInt(R.id.attachment, "setVisibility", View.VISIBLE)
} else {
remoteView.setInt(R.id.attachment, "setVisibility", View.GONE)
}
val intent = MessageList.actionDisplayMessageTemplateFillIntent(item.messageReference)
remoteView.setOnClickFillInIntent(R.id.mail_list_item, intent)
remoteView.setInt(R.id.chip, "setBackgroundColor", item.accountColor)
return remoteView
}
override fun getLoadingView(): RemoteViews {
return RemoteViews(context.packageName, R.layout.message_list_widget_list_item_loading).apply {
// Set the text here instead of in the layout so the app language override is used
setTextViewText(R.id.loadingText, context.getString(UiR.string.message_list_widget_list_item_loading))
}
}
override fun getViewTypeCount(): Int = 2
override fun getItemId(position: Int): Long = messageListItems[position].uniqueId
override fun hasStableIds(): Boolean = true
private fun bold(text: String): CharSequence {
return SpannableString(text).apply {
setSpan(StyleSpan(Typeface.BOLD), 0, text.length, 0)
}
}
private fun getTextColor(messageListItem: MessageListItem): Int {
return if (messageListItem.isRead) readTextColor else unreadTextColor
}
}

View file

@ -0,0 +1,5 @@
package app.k9mail.ui.widget.list
interface MessageListWidgetConfig {
val providerClass: Class<out MessageListWidgetProvider>
}

View file

@ -0,0 +1,106 @@
package app.k9mail.ui.widget.list
import android.appwidget.AppWidgetManager
import android.content.ComponentName
import android.content.Context
import android.content.Intent
import com.fsck.k9.core.BuildConfig
import com.fsck.k9.mailstore.MessageListChangedListener
import com.fsck.k9.mailstore.MessageListRepository
import timber.log.Timber
class MessageListWidgetManager(
private val context: Context,
private val messageListRepository: MessageListRepository,
private val config: MessageListWidgetConfig
) {
private lateinit var appWidgetManager: AppWidgetManager
private var listenerAdded = false
private val listener = MessageListChangedListener {
onMessageListChanged()
}
fun init() {
appWidgetManager = AppWidgetManager.getInstance(context)
if (isAtLeastOneMessageListWidgetAdded()) {
resetMessageListWidget()
registerMessageListChangedListener()
}
}
private fun onMessageListChanged() {
try {
triggerMessageListWidgetUpdate()
} catch (e: RuntimeException) {
if (BuildConfig.DEBUG) {
throw e
} else {
Timber.e(e, "Error while updating message list widget")
}
}
}
internal fun onWidgetAdded() {
Timber.v("Message list widget added")
registerMessageListChangedListener()
}
internal fun onWidgetRemoved() {
Timber.v("Message list widget removed")
if (!isAtLeastOneMessageListWidgetAdded()) {
unregisterMessageListChangedListener()
}
}
@Synchronized
private fun registerMessageListChangedListener() {
if (!listenerAdded) {
listenerAdded = true
messageListRepository.addListener(listener)
Timber.v("Message list widget is now listening for message list changes…")
}
}
@Synchronized
private fun unregisterMessageListChangedListener() {
if (listenerAdded) {
listenerAdded = false
messageListRepository.removeListener(listener)
Timber.v("Message list widget stopped listening for message list changes.")
}
}
private fun isAtLeastOneMessageListWidgetAdded(): Boolean {
return getAppWidgetIds().isNotEmpty()
}
private fun triggerMessageListWidgetUpdate() {
val appWidgetIds = getAppWidgetIds()
if (appWidgetIds.isNotEmpty()) {
appWidgetManager.notifyAppWidgetViewDataChanged(appWidgetIds, R.id.listView)
}
}
private fun resetMessageListWidget() {
val appWidgetIds = getAppWidgetIds()
if (appWidgetIds.isNotEmpty()) {
val intent = Intent(context, config.providerClass).apply {
action = AppWidgetManager.ACTION_APPWIDGET_UPDATE
putExtra(AppWidgetManager.EXTRA_APPWIDGET_IDS, appWidgetIds)
}
context.sendBroadcast(intent)
}
}
private fun getAppWidgetIds(): IntArray {
val componentName = ComponentName(context, config.providerClass)
return appWidgetManager.getAppWidgetIds(componentName)
}
}

View file

@ -0,0 +1,85 @@
package app.k9mail.ui.widget.list
import android.app.PendingIntent
import android.appwidget.AppWidgetManager
import android.appwidget.AppWidgetProvider
import android.content.Context
import android.content.Intent
import android.widget.RemoteViews
import com.fsck.k9.activity.MessageCompose
import com.fsck.k9.activity.MessageList
import com.fsck.k9.activity.MessageList.Companion.intentDisplaySearch
import com.fsck.k9.helper.PendingIntentCompat.FLAG_IMMUTABLE
import com.fsck.k9.helper.PendingIntentCompat.FLAG_MUTABLE
import com.fsck.k9.search.SearchAccount.Companion.createUnifiedInboxAccount
import org.koin.core.component.KoinComponent
import org.koin.core.component.inject
import com.fsck.k9.ui.R as UiR
open class MessageListWidgetProvider : AppWidgetProvider(), KoinComponent {
private val messageListWidgetManager: MessageListWidgetManager by inject()
override fun onEnabled(context: Context) {
messageListWidgetManager.onWidgetAdded()
}
override fun onDisabled(context: Context) {
messageListWidgetManager.onWidgetRemoved()
}
override fun onUpdate(context: Context, appWidgetManager: AppWidgetManager, appWidgetIds: IntArray) {
for (appWidgetId in appWidgetIds) {
updateAppWidget(context, appWidgetManager, appWidgetId)
}
}
private fun updateAppWidget(context: Context, appWidgetManager: AppWidgetManager, appWidgetId: Int) {
val views = RemoteViews(context.packageName, R.layout.message_list_widget_layout)
views.setTextViewText(R.id.folder, context.getString(UiR.string.integrated_inbox_title))
val intent = Intent(context, MessageListWidgetService::class.java)
views.setRemoteAdapter(R.id.listView, intent)
val viewAction = viewActionTemplatePendingIntent(context)
views.setPendingIntentTemplate(R.id.listView, viewAction)
val composeAction = composeActionPendingIntent(context)
views.setOnClickPendingIntent(R.id.new_message, composeAction)
val headerClickAction = viewUnifiedInboxPendingIntent(context)
views.setOnClickPendingIntent(R.id.top_controls, headerClickAction)
appWidgetManager.updateAppWidget(appWidgetId, views)
}
private fun viewActionTemplatePendingIntent(context: Context): PendingIntent {
val intent = MessageList.actionDisplayMessageTemplateIntent(
context,
openInUnifiedInbox = true,
messageViewOnly = true
)
return PendingIntent.getActivity(context, 1, intent, PendingIntent.FLAG_UPDATE_CURRENT or FLAG_MUTABLE)
}
private fun viewUnifiedInboxPendingIntent(context: Context): PendingIntent {
val unifiedInboxAccount = createUnifiedInboxAccount()
val intent = intentDisplaySearch(
context = context,
search = unifiedInboxAccount.relatedSearch,
noThreading = true,
newTask = true,
clearTop = true
)
return PendingIntent.getActivity(context, -1, intent, PendingIntent.FLAG_UPDATE_CURRENT or FLAG_IMMUTABLE)
}
private fun composeActionPendingIntent(context: Context): PendingIntent {
val intent = Intent(context, MessageCompose::class.java).apply {
action = MessageCompose.ACTION_COMPOSE
}
return PendingIntent.getActivity(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT or FLAG_IMMUTABLE)
}
}

View file

@ -0,0 +1,10 @@
package app.k9mail.ui.widget.list
import android.content.Intent
import android.widget.RemoteViewsService
class MessageListWidgetService : RemoteViewsService() {
override fun onGetViewFactory(intent: Intent): RemoteViewsFactory {
return MessageListRemoteViewFactory(applicationContext)
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 94 KiB

View file

@ -0,0 +1,46 @@
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<LinearLayout
android:id="@+id/top_controls"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@color/message_list_widget_header_background"
android:gravity="center_vertical"
android:orientation="horizontal">
<TextView
android:id="@+id/folder"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:paddingBottom="12dp"
android:paddingLeft="8dp"
android:paddingRight="8dp"
android:paddingTop="12dp"
android:textSize="20sp"
android:textColor="@color/message_list_widget_header_text"
tools:text="Unified Inbox" />
<ImageButton
android:id="@+id/new_message"
android:layout_width="56dp"
android:layout_height="match_parent"
android:background="?android:attr/selectableItemBackground"
android:contentDescription="@string/compose_action"
android:scaleType="center"
android:src="@drawable/ic_envelope" />
</LinearLayout>
<ListView android:id="@+id/listView"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@android:color/white"
android:divider="@color/message_list_widget_divider"
android:dividerHeight="0.5dp" />
</LinearLayout>

View file

@ -0,0 +1,93 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/mail_list_item"
android:layout_width="match_parent"
android:layout_height="wrap_content"
tools:background="#fff">
<!-- A regular View breaks things for some reason, but a TextView does the job -->
<TextView
android:id="@+id/chip"
android:layout_width="8dip"
android:layout_height="match_parent"
tools:background="#0099CC"
android:visibility="visible" />
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="@dimen/widget_padding">
<TextView
android:id="@+id/mail_date"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentEnd="true"
android:layout_marginStart="4dp"
tools:text="25 May" />
<ImageView
android:id="@+id/attachment"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignBottom="@+id/mail_date"
android:layout_centerInParent="true"
android:layout_marginStart="4dp"
android:layout_toStartOf="@+id/mail_date"
android:src="@drawable/ic_messagelist_attachment_light"
android:visibility="gone"
tools:visibility="visible" />
<TextView
android:id="@+id/thread_count"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_toStartOf="@id/attachment"
android:layout_marginStart="4dp"
android:maxLines="1"
android:paddingRight="4dip"
android:paddingBottom="1dip"
android:paddingLeft="4dip"
android:textSize="16sp"
android:textColor="?android:attr/colorBackground"
android:background="@drawable/thread_count_box_light"
tools:text="3" />
<TextView
android:id="@+id/sender"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentStart="true"
android:layout_alignParentTop="true"
android:layout_gravity="start"
android:layout_toStartOf="@id/thread_count"
android:ellipsize="end"
android:maxLines="1"
android:textSize="16sp"
tools:text="Kinda long subject that should be long enough to exceed the available display space" />
<TextView
android:id="@+id/mail_subject"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@+id/sender"
android:layout_alignParentStart="true"
android:ellipsize="end"
android:maxLines="1"
android:paddingBottom="2dp"
android:textSize="15sp"
tools:text="Wikipedia" />
<TextView
android:id="@+id/mail_preview"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@+id/mail_subject"
android:layout_alignParentStart="true"
android:maxLines="1"
android:textSize="13sp"
tools:text="Towel Day is celebrated every year on 25 May as a tribute to the author Douglas Adams by his fans." />
</RelativeLayout>
</LinearLayout>

View file

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/loadingText"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:ellipsize="end"
android:gravity="center"
android:maxLines="1"
android:padding="16dp"
android:textSize="15sp"
tools:text="@string/message_list_widget_list_item_loading" />

View file

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@android:color/white"
android:gravity="center"
android:padding="16dp"
android:text="@string/message_list_widget_initializing"
android:textSize="18sp" />

View file

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="message_list_widget_header_background">#737373</color>
<color name="message_list_widget_header_text">#e4e4e4</color>
<color name="message_list_widget_divider">#e5e5e5</color>
<color name="message_list_widget_text_read">#444444</color>
<color name="message_list_widget_text_unread">#000000</color>
</resources>

View file

@ -0,0 +1,13 @@
<?xml version="1.0" encoding="utf-8"?>
<appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android"
android:initialKeyguardLayout="@layout/message_list_widget_loading"
android:initialLayout="@layout/message_list_widget_loading"
android:minHeight="180dp"
android:minWidth="250dp"
android:minResizeWidth="110dp"
android:minResizeHeight="110dp"
android:previewImage="@drawable/message_list_widget_preview"
android:resizeMode="horizontal|vertical"
android:updatePeriodMillis="86400000"
android:widgetCategory="home_screen|keyguard">
</appwidget-provider>