-
-
Notifications
You must be signed in to change notification settings - Fork 22
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add Android Homescreen widget (#214)
* ⚗️ Add prayer time widget layout * 🐛 Fix incorrect id format * ➕ Add home_widget to update data from Dart side * ✨ Make widget update at midnight * ✨ Complete horizontal widget #9 * 💄 Add vertical layout widget * ✨ Vertical layout widget functionality * 🍱 Update widget preview images * ✨ (Widget) Add debug page to retrievd saved widget data start recording widget last updated timestamp * 🌐 Update widget description with translations * ✨ (Homescreen widget) Handle id stored data is out of date Seperate xml preview layout * 🔥 Remove unused imports
- Loading branch information
Showing
37 changed files
with
1,473 additions
and
22 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
233 changes: 233 additions & 0 deletions
233
android/app/src/main/java/live/iqfareez/waktusolatmalaysia/SolatHorizontalWidget.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,233 @@ | ||
package live.iqfareez.waktusolatmalaysia | ||
|
||
import android.app.AlarmManager | ||
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.content.SharedPreferences | ||
import android.os.Build | ||
import android.util.Log | ||
import android.view.View | ||
import android.widget.RemoteViews | ||
import es.antonborri.home_widget.HomeWidgetPlugin | ||
import org.json.JSONObject | ||
import java.text.SimpleDateFormat | ||
import java.time.YearMonth | ||
import java.time.format.DateTimeFormatter | ||
import java.util.Calendar | ||
import java.util.Date | ||
import java.util.Locale | ||
import java.util.TimeZone | ||
|
||
|
||
private const val ACTION_SCHEDULED_UPDATE = "live.iqfareez.waktusolatmalaysia.SCHEDULED_UPDATE" | ||
private const val LOG_TAG = "MPT_Widget_Horizontal" | ||
|
||
/** | ||
* Implementation of App Widget functionality. | ||
*/ | ||
class SolatHorizontalWidget : AppWidgetProvider() { | ||
override fun onUpdate( | ||
context: Context, | ||
appWidgetManager: AppWidgetManager, | ||
appWidgetIds: IntArray, | ||
) { | ||
val widgetData = HomeWidgetPlugin.getData(context) | ||
// There may be multiple widgets active, so update all of them | ||
for (appWidgetId in appWidgetIds) { | ||
Log.i(LOG_TAG, "onUpdate: SolatHorizontalWidget called") | ||
updateAppWidget(context, appWidgetManager, appWidgetId, widgetData, R.layout.solat_horizontal_widget) | ||
} | ||
|
||
scheduleNextUpdate(context); | ||
} | ||
|
||
override fun onEnabled(context: Context) { | ||
// Enter relevant functionality for when the first widget is created | ||
} | ||
|
||
override fun onDisabled(context: Context) { | ||
// Enter relevant functionality for when the last widget is disabled | ||
} | ||
|
||
override fun onReceive(context: Context, intent: Intent) { | ||
super.onReceive(context, intent); | ||
if (intent.action.equals(ACTION_SCHEDULED_UPDATE)) { | ||
val manager = AppWidgetManager.getInstance(context) | ||
val ids = | ||
manager.getAppWidgetIds(ComponentName(context, SolatHorizontalWidget::class.java)) | ||
onUpdate(context, manager, ids) | ||
} | ||
} | ||
} | ||
|
||
internal fun updateAppWidget( | ||
context: Context, | ||
appWidgetManager: AppWidgetManager, | ||
appWidgetId: Int, | ||
widgetData: SharedPreferences, | ||
layoutId: Int | ||
) { | ||
// Construct the RemoteViews object | ||
val views = RemoteViews(context.packageName, layoutId) | ||
|
||
val launchIntent = context.packageManager.getLaunchIntentForPackage(context.packageName) | ||
val pendingIntent = PendingIntent.getActivity( | ||
context, 0, launchIntent, | ||
PendingIntent.FLAG_IMMUTABLE | ||
) | ||
|
||
// Set the click listener for the widget | ||
views.setOnClickPendingIntent(android.R.id.background, pendingIntent) | ||
|
||
// Parse the JSON in SharedPreferences | ||
val prayerData = widgetData.getString("prayer_data", null); | ||
|
||
// If data not available, display outdated layout | ||
if (prayerData == null) { | ||
views.setViewVisibility(R.id.outdated_text, View.VISIBLE); | ||
views.setViewVisibility(R.id.prayer_layout, View.GONE); | ||
return; | ||
} | ||
|
||
val parsed = JSONObject(prayerData) | ||
|
||
// if the data is outdated (the month & year doesn't match), show outdated layout | ||
if (!isDateValid("${parsed.get("month")}-${parsed.get("year")}")) { | ||
Log.i(LOG_TAG, "updateAppWidget: Data ${parsed.get("month")}-${parsed.get("year")} is invalid"); | ||
views.setViewVisibility(R.id.outdated_text, View.VISIBLE); | ||
views.setViewVisibility(R.id.prayer_layout, View.GONE); | ||
return; | ||
} | ||
|
||
Log.i(LOG_TAG, "updateAppWidget: Reading SP json ${parsed.get("zone")}, ${parsed.get("month")}-${parsed.get("year")} ") | ||
|
||
val prayers = parsed.getJSONArray("prayers") | ||
|
||
val calendar = Calendar.getInstance() | ||
val todayIndex = calendar.get(Calendar.DAY_OF_WEEK) - 1; | ||
val todayPrayer: JSONObject = prayers.get(todayIndex) as JSONObject; | ||
|
||
val subuhTime = todayPrayer.getLong("fajr") | ||
val zohorTime = todayPrayer.getLong("dhuhr") | ||
val asarTime = todayPrayer.getLong("asr") | ||
val maghribTime = todayPrayer.getLong("maghrib") | ||
val isyakTime = todayPrayer.getLong("isha") | ||
|
||
val gmt8TimeZone = TimeZone.getTimeZone("GMT+8") | ||
val timeFormat = SimpleDateFormat("h:mm a") | ||
timeFormat.timeZone = gmt8TimeZone | ||
|
||
fun formatTime(timeInMillis: Long): String { | ||
val date = Date(timeInMillis) | ||
return timeFormat.format(date) | ||
} | ||
|
||
val formattedSubuhTime = formatTime(subuhTime) | ||
val formattedZohorTime = formatTime(zohorTime) | ||
val formattedAsarTime = formatTime(asarTime) | ||
val formattedMaghribTime = formatTime(maghribTime) | ||
val formattedIsyakTime = formatTime(isyakTime) | ||
|
||
val dateFormat = SimpleDateFormat("dd/MM/yyyy", Locale.getDefault()) | ||
dateFormat.timeZone = TimeZone.getTimeZone("Asia/Kuala_Lumpur") // Set Malaysia timezone | ||
val formattedDate = dateFormat.format(Date()) | ||
|
||
val widgetTitle = "${parsed.get("zone")}: ${widgetData.getString("widget_title", null)}" | ||
|
||
// Set content | ||
views.setTextViewText(R.id.widget_date, formattedDate) | ||
|
||
views.setTextViewText( | ||
R.id.widget_title, widgetTitle | ||
?: "Please open app to set widget data" | ||
) | ||
views.setTextViewText( | ||
R.id.subuh_time, formattedSubuhTime | ||
) | ||
views.setTextViewText( | ||
R.id.zuhur_time, formattedZohorTime | ||
) | ||
views.setTextViewText( | ||
R.id.asar_time, formattedAsarTime | ||
) | ||
views.setTextViewText( | ||
R.id.maghrib_time, formattedMaghribTime | ||
) | ||
views.setTextViewText( | ||
R.id.isyak_time, formattedIsyakTime | ||
) | ||
|
||
// Instruct the widget manager to update the widget | ||
appWidgetManager.updateAppWidget(appWidgetId, views) | ||
} | ||
|
||
// Credit to: https://stackoverflow.com/a/37901697/13617136 | ||
private fun scheduleNextUpdate(context: Context) { | ||
val alarmManager = context.getSystemService(Context.ALARM_SERVICE) as AlarmManager | ||
// Substitute AppWidget for whatever you named your AppWidgetProvider subclass | ||
val intent = Intent(context, SolatHorizontalWidget::class.java) | ||
intent.setAction(ACTION_SCHEDULED_UPDATE) | ||
val pendingIntent = PendingIntent.getBroadcast(context, 0, intent, PendingIntent.FLAG_IMMUTABLE) | ||
|
||
// Get a calendar instance for midnight tomorrow. | ||
val midnight: Calendar = Calendar.getInstance() | ||
midnight.set(Calendar.HOUR_OF_DAY, 0) | ||
midnight.set(Calendar.MINUTE, 0) | ||
// Schedule one second after midnight, to be sure we are in the right day next time this | ||
// method is called. Otherwise, we risk calling onUpdate multiple times within a few | ||
// milliseconds | ||
midnight.set(Calendar.SECOND, 1) | ||
midnight.set(Calendar.MILLISECOND, 0) | ||
midnight.add(Calendar.DAY_OF_YEAR, 1) | ||
|
||
// For API 19 and later, set may fire the intent a little later to save battery, | ||
// setExact ensures the intent goes off exactly at midnight. | ||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT) { | ||
alarmManager[AlarmManager.RTC_WAKEUP, midnight.getTimeInMillis()] = pendingIntent | ||
} else { | ||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { | ||
if (alarmManager.canScheduleExactAlarms()) { | ||
alarmManager.setExact( | ||
AlarmManager.RTC_WAKEUP, | ||
midnight.timeInMillis, | ||
pendingIntent | ||
) | ||
} else { | ||
alarmManager.setExact( | ||
AlarmManager.RTC_WAKEUP, | ||
midnight.timeInMillis, | ||
pendingIntent | ||
) | ||
} | ||
} | ||
} | ||
} | ||
|
||
// Function to check the validity of the given month and year | ||
fun isDateValid(jsonDate: String): Boolean { | ||
// The YearMonth.parse only accepts month with title-case eg Jan, Feb etc. | ||
fun toTitleCase(input: String): String { | ||
return input.lowercase().replaceFirstChar { it.uppercase() } | ||
} | ||
|
||
val jsonFixed = toTitleCase(jsonDate); | ||
return try { | ||
// Parse the JSON date into YearMonth | ||
val formatter = DateTimeFormatter.ofPattern("MMM-yyyy") | ||
val date = YearMonth.parse(jsonFixed, formatter) | ||
|
||
// Get the current month and year | ||
val currentMonthYear = YearMonth.now() | ||
|
||
// Check if the parsed date is after or equal to the current month and year | ||
!date.isBefore(currentMonthYear) && !date.isAfter(currentMonthYear); | ||
} catch (e: Exception) { | ||
// Handle parsing or other exceptions | ||
e.printStackTrace() | ||
false | ||
} | ||
} |
Oops, something went wrong.