diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/timetablewidget/TimetableWidgetFactory.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/timetablewidget/TimetableWidgetFactory.kt index 9c5abe1c2..d545413da 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/timetablewidget/TimetableWidgetFactory.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/timetablewidget/TimetableWidgetFactory.kt @@ -124,14 +124,12 @@ class TimetableWidgetFactory( val lessonStartTime = lesson.start.toFormattedString(TIME_FORMAT_STYLE) val lessonEndTime = lesson.end.toFormattedString(TIME_FORMAT_STYLE) - val roomText = "${context.getString(R.string.timetable_room)} ${lesson.room}" val remoteViews = RemoteViews(context.packageName, R.layout.item_widget_timetable).apply { setTextViewText(R.id.timetableWidgetItemNumber, lesson.number.toString()) setTextViewText(R.id.timetableWidgetItemTimeStart, lessonStartTime) setTextViewText(R.id.timetableWidgetItemTimeFinish, lessonEndTime) setTextViewText(R.id.timetableWidgetItemSubject, lesson.subject) - setTextViewText(R.id.timetableWidgetItemRoom, roomText) setTextViewText(R.id.timetableWidgetItemTeacher, lesson.teacher) setTextViewText(R.id.timetableWidgetItemDescription, lesson.info) setOnClickFillInIntent(R.id.timetableWidgetItemContainer, Intent()) @@ -140,6 +138,12 @@ class TimetableWidgetFactory( updateTheme() clearLessonStyles(remoteViews) + if (lesson.room.isBlank()) { + remoteViews.setViewVisibility(R.id.timetableWidgetItemRoom, GONE) + } else { + remoteViews.setTextViewText(R.id.timetableWidgetItemRoom, lesson.room) + } + when { lesson.canceled -> applyCancelledLessonStyles(remoteViews) lesson.changes or lesson.info.isNotBlank() -> applyChangedLessonStyles( diff --git a/app/src/main/java/io/github/wulkanowy/ui/modules/timetablewidget/TimetableWidgetProvider.kt b/app/src/main/java/io/github/wulkanowy/ui/modules/timetablewidget/TimetableWidgetProvider.kt index 624ca30f4..cc48539a5 100644 --- a/app/src/main/java/io/github/wulkanowy/ui/modules/timetablewidget/TimetableWidgetProvider.kt +++ b/app/src/main/java/io/github/wulkanowy/ui/modules/timetablewidget/TimetableWidgetProvider.kt @@ -8,6 +8,7 @@ import android.content.Context import android.content.Intent import android.content.Intent.FLAG_ACTIVITY_CLEAR_TASK import android.content.Intent.FLAG_ACTIVITY_NEW_TASK +import android.graphics.Bitmap import android.widget.RemoteViews import androidx.appcompat.content.res.AppCompatResources import androidx.core.graphics.drawable.DrawableCompat @@ -76,110 +77,151 @@ class TimetableWidgetProvider : BroadcastReceiver() { override fun onReceive(context: Context, intent: Intent) { GlobalScope.launch { when (intent.action) { - ACTION_APPWIDGET_UPDATE -> onUpdate(context, intent) - ACTION_APPWIDGET_DELETED -> onDelete(intent) + ACTION_APPWIDGET_UPDATE -> onWidgetUpdate(context, intent) + ACTION_APPWIDGET_DELETED -> onWidgetDeleted(intent) } } } - private suspend fun onUpdate(context: Context, intent: Intent) { - if (intent.getStringExtra(EXTRA_BUTTON_TYPE) == null) { - val isFromConfigure = intent.getBooleanExtra(EXTRA_FROM_CONFIGURE, false) - val appWidgetIds = intent.getIntArrayExtra(EXTRA_APPWIDGET_IDS) ?: return + private suspend fun onWidgetUpdate(context: Context, intent: Intent) { + val pressedButton = intent.getPressedButton() - appWidgetIds.forEach { appWidgetId -> - val student = - getStudent(sharedPref.getLong(getStudentWidgetKey(appWidgetId), 0), appWidgetId) - val savedDataEpochDay = sharedPref.getLong(getDateWidgetKey(appWidgetId), 0) - - val dateToLoad = if (isFromConfigure && savedDataEpochDay != 0L) { - LocalDate.ofEpochDay(savedDataEpochDay) - } else { - getWidgetDefaultDateToLoad(appWidgetId) - } - - updateWidget(context, appWidgetId, dateToLoad, student) - } + if (pressedButton == null) { + val updatedWidgetIds = intent.getWidgetIds() ?: return + updatedWidgetIds.forEach { updateWidgetLayout(context, it) } } else { - val buttonType = intent.getStringExtra(EXTRA_BUTTON_TYPE) - val toggledWidgetId = intent.getIntExtra(EXTRA_TOGGLED_WIDGET_ID, 0) - val student = getStudent( - sharedPref.getLong(getStudentWidgetKey(toggledWidgetId), 0), toggledWidgetId - ) - val savedDate = - LocalDate.ofEpochDay(sharedPref.getLong(getDateWidgetKey(toggledWidgetId), 0)) - val date = when (buttonType) { - BUTTON_RESET -> getWidgetDefaultDateToLoad(toggledWidgetId) - BUTTON_NEXT -> savedDate.nextSchoolDay - BUTTON_PREV -> savedDate.previousSchoolDay - else -> getWidgetDefaultDateToLoad(toggledWidgetId) - } - if (!buttonType.isNullOrBlank()) { - analytics.logEvent( - "changed_timetable_widget_day", "button" to buttonType - ) - } - updateWidget(context, toggledWidgetId, date, student) + val widgetId = intent.getToggledWidgetId() ?: return + reportChangedDay(pressedButton) + updateSavedWidgetDate(widgetId, pressedButton) + updateWidgetLayout(context, widgetId) } } - private fun onDelete(intent: Intent) { - val appWidgetId = intent.getIntExtra(EXTRA_APPWIDGET_ID, 0) + private fun Intent.getPressedButton(): String? { + return getStringExtra(EXTRA_BUTTON_TYPE) + } - if (appWidgetId != 0) { - with(sharedPref) { - delete(getStudentWidgetKey(appWidgetId)) - delete(getDateWidgetKey(appWidgetId)) - } + private fun Intent.getWidgetIds(): IntArray? { + return getIntArrayExtra(EXTRA_APPWIDGET_IDS) + } + + private fun Intent.getToggledWidgetId(): Int? { + val toggledWidgetId = getIntExtra(EXTRA_TOGGLED_WIDGET_ID, INVALID_APPWIDGET_ID) + return toggledWidgetId.takeIf { it != INVALID_APPWIDGET_ID } + } + + private fun reportChangedDay(buttonType: String) { + if (buttonType.isNotBlank()) { + analytics.logEvent("changed_timetable_widget_day", "button" to buttonType) } } - private fun updateWidget( - context: Context, appWidgetId: Int, date: LocalDate, student: Student? + private fun updateSavedWidgetDate(widgetId: Int, buttonType: String) { + val savedDate = getSavedWidgetDate(widgetId) + val newDate = savedDate?.let { getNewDate(it, widgetId, buttonType) } + ?: getWidgetDefaultDateToLoad(widgetId) + setWidgetDate(widgetId, newDate) + } + + private fun getSavedWidgetDate(widgetId: Int): LocalDate? { + val epochDay = sharedPref.getLong(getDateWidgetKey(widgetId), 0) + return if (epochDay == 0L) null else LocalDate.ofEpochDay(epochDay) + } + + private fun getNewDate( + currentDate: LocalDate, + widgetId: Int, + selectedButton: String + ): LocalDate { + return when (selectedButton) { + BUTTON_NEXT -> currentDate.nextSchoolDay + BUTTON_PREV -> currentDate.previousSchoolDay + else -> getWidgetDefaultDateToLoad(widgetId) + } + } + + private fun setWidgetDate(widgetId: Int, dateToSet: LocalDate) { + val widgetDateKey = getDateWidgetKey(widgetId) + sharedPref.putLong(widgetDateKey, dateToSet.toEpochDay(), true) + } + + private fun getWidgetDefaultDateToLoad(widgetId: Int): LocalDate { + val lastLessonEndDateTime = getLastLessonDateTime(widgetId) + + val todayDate = LocalDate.now() + val isLastLessonToday = lastLessonEndDateTime.toLocalDate() == todayDate + val isEndOfLessons = LocalDateTime.now() > lastLessonEndDateTime + + return if (isLastLessonToday && isEndOfLessons) { + todayDate.nextSchoolDay + } else { + todayDate.nextOrSameSchoolDay + } + } + + private fun getLastLessonDateTime(widgetId: Int): LocalDateTime { + val lastLessonTimestamp = sharedPref + .getLong(getTodayLastLessonEndDateTimeWidgetKey(widgetId), 0) + return LocalDateTime.ofEpochSecond(lastLessonTimestamp, 0, ZoneOffset.UTC) + } + + private suspend fun updateWidgetLayout( + context: Context, widgetId: Int ) { - val nextNavIntent = createNavIntent(context, appWidgetId, appWidgetId, BUTTON_NEXT) - val prevNavIntent = createNavIntent(context, -appWidgetId, appWidgetId, BUTTON_PREV) - val resetNavIntent = - createNavIntent(context, Int.MAX_VALUE - appWidgetId, appWidgetId, BUTTON_RESET) - val adapterIntent = Intent(context, TimetableWidgetService::class.java).apply { - putExtra(EXTRA_APPWIDGET_ID, appWidgetId) - action = appWidgetId.toString() //make Intent unique - } - val appIntent = PendingIntent.getActivity( - context, - TIMETABLE_PENDING_INTENT_ID, - SplashActivity.getStartIntent(context, Destination.Timetable()), - PendingIntent.FLAG_UPDATE_CURRENT or PendingIntentCompat.FLAG_IMMUTABLE - ) + val widgetRemoteViews = RemoteViews(context.packageName, R.layout.widget_timetable) + // Apply the click action intent + val appIntent = createPendingAppIntent(context) + widgetRemoteViews.setPendingIntentTemplate(R.id.timetableWidgetList, appIntent) + + // Display saved date + val date = getSavedWidgetDate(widgetId) ?: getWidgetDefaultDateToLoad(widgetId) val formattedDate = date.toFormattedString("EEE, dd.MM").capitalise() - val remoteView = RemoteViews(context.packageName, R.layout.widget_timetable).apply { - setEmptyView(R.id.timetableWidgetList, R.id.timetableWidgetEmpty) - setTextViewText(R.id.timetableWidgetDate, formattedDate) - setRemoteAdapter(R.id.timetableWidgetList, adapterIntent) + widgetRemoteViews.setTextViewText(R.id.timetableWidgetDate, formattedDate) + + // Apply intents to the date switcher buttons + val nextNavIntent = createNavButtonIntent(context, widgetId, widgetId, BUTTON_NEXT) + val prevNavIntent = createNavButtonIntent(context, -widgetId, widgetId, BUTTON_PREV) + val resetNavIntent = + createNavButtonIntent(context, Int.MAX_VALUE - widgetId, widgetId, BUTTON_RESET) + widgetRemoteViews.run { setOnClickPendingIntent(R.id.timetableWidgetNext, nextNavIntent) setOnClickPendingIntent(R.id.timetableWidgetPrev, prevNavIntent) setOnClickPendingIntent(R.id.timetableWidgetDate, resetNavIntent) - setPendingIntentTemplate(R.id.timetableWidgetList, appIntent) } - student?.let { - setupAccountView(context, student, remoteView, appWidgetId) + // Setup the lesson list adapter + val lessonListAdapterIntent = createLessonListAdapterIntent(context, widgetId) + // --- Ensure the selected date is stored in the shared preferences, + // --- on which the TimetableWidgetFactory relies + setWidgetDate(widgetId, date) + // --- + widgetRemoteViews.apply { + setEmptyView(R.id.timetableWidgetList, R.id.timetableWidgetEmpty) + setRemoteAdapter(R.id.timetableWidgetList, lessonListAdapterIntent) } - with(sharedPref) { - putLong(getDateWidgetKey(appWidgetId), date.toEpochDay(), true) + // Setup profile picture + getWidgetStudent(widgetId)?.let { student -> + setupAccountView(context, student, widgetRemoteViews, widgetId) } + // Apply updates with(appWidgetManager) { - partiallyUpdateAppWidget(appWidgetId, remoteView) - notifyAppWidgetViewDataChanged(appWidgetId, R.id.timetableWidgetList) + partiallyUpdateAppWidget(widgetId, widgetRemoteViews) + notifyAppWidgetViewDataChanged(widgetId, R.id.timetableWidgetList) } Timber.d("TimetableWidgetProvider updated") } - private fun createNavIntent( + private fun createPendingAppIntent(context: Context) = PendingIntent.getActivity( + context, TIMETABLE_PENDING_INTENT_ID, + SplashActivity.getStartIntent(context, Destination.Timetable()), + PendingIntent.FLAG_UPDATE_CURRENT or PendingIntentCompat.FLAG_IMMUTABLE + ) + + private fun createNavButtonIntent( context: Context, code: Int, appWidgetId: Int, buttonType: String ) = PendingIntent.getBroadcast( context, code, Intent(context, TimetableWidgetProvider::class.java).apply { @@ -189,6 +231,17 @@ class TimetableWidgetProvider : BroadcastReceiver() { }, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntentCompat.FLAG_IMMUTABLE ) + private fun createLessonListAdapterIntent(context: Context, widgetId: Int) = + Intent(context, TimetableWidgetService::class.java).apply { + putExtra(EXTRA_APPWIDGET_ID, widgetId) + action = widgetId.toString() //make Intent unique + } + + private suspend fun getWidgetStudent(widgetId: Int): Student? { + val studentId = sharedPref.getLong(getStudentWidgetKey(widgetId), 0) + return getStudent(studentId, widgetId) + } + private suspend fun getStudent(studentId: Long, appWidgetId: Int) = try { val students = studentRepository.getSavedStudents(false) val student = students.singleOrNull { it.student.id == studentId }?.student @@ -199,6 +252,7 @@ class TimetableWidgetProvider : BroadcastReceiver() { sharedPref.putLong(getStudentWidgetKey(appWidgetId), it.id) } } + else -> null } } catch (e: Exception) { @@ -208,60 +262,64 @@ class TimetableWidgetProvider : BroadcastReceiver() { null } - private fun getWidgetDefaultDateToLoad(appWidgetId: Int): LocalDate { - val lastLessonEndTimestamp = - sharedPref.getLong(getTodayLastLessonEndDateTimeWidgetKey(appWidgetId), 0) - val lastLessonEndDateTime = - LocalDateTime.ofEpochSecond(lastLessonEndTimestamp, 0, ZoneOffset.UTC) + private fun setupAccountView( + context: Context, student: Student, remoteViews: RemoteViews, widgetId: Int + ) { + val accountInitials = getAccountInitials(student.nickOrName) + val accountPickerPendingIntent = createAccountPickerPendingIntent(context, widgetId) - val todayDate = LocalDate.now() - val isLastLessonEndDateNow = lastLessonEndDateTime.toLocalDate() == todayDate - val isLastLessonEndDateAfterNowTime = LocalDateTime.now() > lastLessonEndDateTime + getAvatarBackgroundBitmap(context, student.avatarColor)?.let { + remoteViews.setImageViewBitmap(R.id.timetableWidgetAccountBackground, it) + } - return if (isLastLessonEndDateNow && isLastLessonEndDateAfterNowTime) { - todayDate.nextSchoolDay - } else { - todayDate.nextOrSameSchoolDay + remoteViews.apply { + setTextViewText(R.id.timetableWidgetAccountInitials, accountInitials) + setOnClickPendingIntent(R.id.timetableWidgetAccount, accountPickerPendingIntent) } } - private fun setupAccountView( - context: Context, - student: Student, - remoteViews: RemoteViews, - appWidgetId: Int - ) { - val accountInitials = student.nickOrName - .split(" ") - .mapNotNull { it.firstOrNull() }.take(2) - .joinToString(separator = "").uppercase() + private fun getAccountInitials(name: String): String { + val firstLetters = name.split(" ").mapNotNull { it.firstOrNull() } + return firstLetters.joinToString(separator = "").uppercase() + } - val accountPickerIntent = PendingIntent.getActivity( + private fun createAccountPickerPendingIntent(context: Context, widgetId: Int) = + PendingIntent.getActivity( context, - -Int.MAX_VALUE + appWidgetId, + -Int.MAX_VALUE + widgetId, Intent(context, TimetableWidgetConfigureActivity::class.java).apply { addFlags(FLAG_ACTIVITY_NEW_TASK or FLAG_ACTIVITY_CLEAR_TASK) - putExtra(EXTRA_APPWIDGET_ID, appWidgetId) + putExtra(EXTRA_APPWIDGET_ID, widgetId) putExtra(EXTRA_FROM_PROVIDER, true) }, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntentCompat.FLAG_IMMUTABLE ) - // Create background bitmap + private fun getAvatarBackgroundBitmap(context: Context, avatarColor: Long): Bitmap? { val avatarDrawableResource = R.drawable.background_timetable_widget_avatar - AppCompatResources.getDrawable(context, avatarDrawableResource)?.let { drawable -> + return AppCompatResources.getDrawable(context, avatarDrawableResource)?.let { drawable -> val screenDensity = context.resources.displayMetrics.density val avatarSize = (48 * screenDensity).toInt() - val backgroundBitmap = DrawableCompat.wrap(drawable).run { - DrawableCompat.setTint(this, student.avatarColor.toInt()) + DrawableCompat.wrap(drawable).run { + DrawableCompat.setTint(this, avatarColor.toInt()) toBitmap(avatarSize, avatarSize) } - remoteViews.setImageViewBitmap(R.id.timetableWidgetAccountBackground, backgroundBitmap) } + } - remoteViews.apply { - setTextViewText(R.id.timetableWidgetAccountInitials, accountInitials) - setOnClickPendingIntent(R.id.timetableWidgetAccount, accountPickerIntent) + private fun onWidgetDeleted(intent: Intent) { + val deletedWidgetId = intent.getWidgetId() + deleteWidgetPreferences(deletedWidgetId) + } + + private fun Intent.getWidgetId(): Int { + return getIntExtra(EXTRA_APPWIDGET_ID, INVALID_APPWIDGET_ID) + } + + private fun deleteWidgetPreferences(widgetId: Int) { + with(sharedPref) { + delete(getStudentWidgetKey(widgetId)) + delete(getDateWidgetKey(widgetId)) } } } diff --git a/app/src/main/res/drawable/background_timetable_widget_avatar.xml b/app/src/main/res/drawable/background_timetable_widget_avatar.xml index 7f64c4ebe..48298d675 100644 --- a/app/src/main/res/drawable/background_timetable_widget_avatar.xml +++ b/app/src/main/res/drawable/background_timetable_widget_avatar.xml @@ -2,5 +2,5 @@ - + diff --git a/app/src/main/res/drawable/background_widget_item_timetable.xml b/app/src/main/res/drawable/background_widget_item_timetable.xml index 096357584..510c70c0c 100644 --- a/app/src/main/res/drawable/background_widget_item_timetable.xml +++ b/app/src/main/res/drawable/background_widget_item_timetable.xml @@ -1,5 +1,5 @@ - + diff --git a/app/src/main/res/drawable/ic_timetable_widget_swap.xml b/app/src/main/res/drawable/ic_timetable_widget_swap.xml index 2f91489a9..09c50b4f1 100644 --- a/app/src/main/res/drawable/ic_timetable_widget_swap.xml +++ b/app/src/main/res/drawable/ic_timetable_widget_swap.xml @@ -1,6 +1,6 @@ @@ -16,23 +16,47 @@ android:layout_width="match_parent" android:layout_height="wrap_content" android:gravity="center_vertical" - android:orientation="horizontal" - android:paddingVertical="16dp"> + android:paddingTop="12dp" + android:paddingBottom="8dp"> + + + + + + + + - + android:paddingBottom="12dp"> - + android:background="@drawable/background_widget_item_timetable" + android:backgroundTint="?attr/colorSurface" + android:gravity="center_vertical" + android:minHeight="48dp" + android:orientation="horizontal" + android:paddingHorizontal="12dp" + android:paddingVertical="8dp"> - + + + + + + + + + + + + + + + + + + + + + + + + + + + android:background="@drawable/background_widget_item_timetable" + android:backgroundTint="?attr/colorSurface" + android:gravity="center_vertical" + android:minHeight="48dp" + android:orientation="horizontal" + android:paddingHorizontal="12dp" + android:paddingVertical="8dp"> - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + android:layout_height="wrap_content" + android:background="@drawable/background_widget_item_timetable" + android:backgroundTint="?attr/colorSurface" + android:gravity="center_vertical" + android:minHeight="48dp" + android:orientation="horizontal" + android:paddingHorizontal="12dp" + android:paddingVertical="8dp"> + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/item_widget_timetable.xml b/app/src/main/res/layout/item_widget_timetable.xml index 27c9db66c..01f4525ec 100644 --- a/app/src/main/res/layout/item_widget_timetable.xml +++ b/app/src/main/res/layout/item_widget_timetable.xml @@ -6,11 +6,12 @@ android:layout_width="match_parent" android:layout_height="wrap_content" android:background="@drawable/background_widget_item_timetable" - android:backgroundTint="?attr/backgroundColor" + android:backgroundTint="?attr/colorSurface" android:gravity="center_vertical" + android:minHeight="48dp" android:orientation="horizontal" - android:paddingHorizontal="16dp" - android:paddingVertical="12dp" + android:paddingHorizontal="12dp" + android:paddingVertical="8dp" android:theme="@style/Wulkanowy.Widget.Theme" tools:context=".ui.modules.timetablewidget.TimetableWidgetFactory"> @@ -18,15 +19,14 @@ android:id="@+id/timetableWidgetItemNumber" android:layout_width="wrap_content" android:layout_height="wrap_content" - android:textAppearance="?attr/textAppearanceHeadline6" - android:textSize="24sp" + android:textSize="22sp" tools:text="1" tools:textColor="?attr/colorTimetableChange" /> @@ -41,7 +41,7 @@ android:id="@+id/timetableWidgetItemTimeFinish" android:layout_width="wrap_content" android:layout_height="wrap_content" - android:layout_marginTop="4dp" + android:layout_marginTop="2dp" android:textAppearance="?attr/textAppearanceBodySmall" tools:text="09:45" /> @@ -60,7 +60,7 @@ android:layout_height="wrap_content" android:ellipsize="end" android:lines="1" - android:textAppearance="?attr/textAppearanceTitleMedium" + android:textSize="14sp" tools:text="Programowanie aplikacji mobilnych i desktopowych" /> + tools:context=".ui.modules.timetablewidget.TimetableWidgetProvider" + tools:targetApi="s"> + android:paddingTop="12dp" + android:paddingBottom="8dp"> + + + + + + + + + android:textSize="18sp" + tools:text="Friday, 19.05" /> - - - - - - @@ -111,5 +114,7 @@ android:text="@string/widget_timetable_no_items" android:textAppearance="?attr/textAppearanceBody1" android:visibility="gone" /> + + diff --git a/app/src/main/res/xml/provider_widget_timetable.xml b/app/src/main/res/xml/provider_widget_timetable.xml index 3cdad0c81..555d8cb10 100644 --- a/app/src/main/res/xml/provider_widget_timetable.xml +++ b/app/src/main/res/xml/provider_widget_timetable.xml @@ -3,15 +3,15 @@ xmlns:tools="http://schemas.android.com/tools" android:configure="io.github.wulkanowy.ui.modules.timetablewidget.TimetableWidgetConfigureActivity" android:initialLayout="@layout/widget_timetable" - android:minWidth="250dp" - android:minHeight="110dp" - android:minResizeWidth="250dp" - android:minResizeHeight="110dp" + android:minWidth="245dp" + android:minHeight="102dp" + android:minResizeWidth="245dp" + android:minResizeHeight="102dp" android:previewImage="@drawable/img_timetable_widget_preview" android:previewLayout="@layout/widget_timetable_preview" android:resizeMode="horizontal|vertical" android:targetCellWidth="3" - android:targetCellHeight="2" + android:targetCellHeight="3" android:updatePeriodMillis="3600000" android:widgetCategory="home_screen" tools:targetApi="s" />