Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[FEAT] Add timeline widget for android #196

Draft
wants to merge 15 commits into
base: main
Choose a base branch
from
Draft
6 changes: 6 additions & 0 deletions android/app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -80,13 +80,19 @@ android {
kotlinOptions {
jvmTarget = "17"
}
buildFeatures {
viewBinding = true
}
}

flutter {
source = "../.."
}

dependencies {
implementation("com.squareup.okhttp3:okhttp:4.11.0")
implementation("androidx.preference:preference-ktx:1.2.1")
implementation("androidx.constraintlayout:constraintlayout:2.2.0")
testImplementation("junit:junit:4.13.2")
androidTestImplementation("androidx.test:runner:1.6.2")
androidTestImplementation("androidx.test.espresso:espresso-core:3.6.1")
Expand Down
80 changes: 55 additions & 25 deletions android/app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
@@ -1,49 +1,79 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<!-- io.flutter.app.FlutterApplication is an android.app.Application that

<!--
io.flutter.app.FlutterApplication is an android.app.Application that
calls FlutterMain.startInitialization(this); in its onCreate method.
In most cases you can leave this as-is, but you if you want to provide
additional functionality it is fine to subclass or reimplement
FlutterApplication and put your custom class here. -->
<uses-permission
android:name="android.permission.INTERNET"/>
FlutterApplication and put your custom class here.
-->
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission
android:name="android.permission.WRITE_EXTERNAL_STORAGE"
android:maxSdkVersion="28"/>
<uses-permission
android:name="android.permission.READ_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.POST_NOTIFICATIONS"/>
android:maxSdkVersion="28" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />

<application
android:name="${applicationName}"
android:label="OTL"
android:icon="@mipmap/ic_launcher"
android:allowBackup="true"
android:fullBackupContent="true">
android:fullBackupContent="true"
android:icon="@mipmap/ic_launcher"
android:label="OTL">
<receiver
android:name=".TimetableWidget"
android:exported="false">
<intent-filter>
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
</intent-filter>

<meta-data
android:name="android.appwidget.provider"
android:resource="@xml/timetable_widget_info" />
</receiver>
<receiver
android:name=".NextLectureWidget"
android:exported="false">
<intent-filter>
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
</intent-filter>

<meta-data
android:name="android.appwidget.provider"
android:resource="@xml/next_lecture_widget_info" />
</receiver>

<activity
android:name=".MainActivity"
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
android:exported="true"
android:hardwareAccelerated="true"
android:launchMode="singleTop"
android:screenOrientation="portrait"
android:theme="@style/LaunchTheme"
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
android:hardwareAccelerated="true"
android:windowSoftInputMode="adjustResize"
android:exported="true">
android:windowSoftInputMode="adjustResize">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
<action android:name="android.intent.action.MAIN" />

<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<service
android:name="com.kuku.channel_talk_flutter.PushInterceptService"
android:exported="true"
>

<service
android:name="com.kuku.channel_talk_flutter.PushInterceptService"
android:exported="true">
<intent-filter>
<action android:name="com.google.firebase.MESSAGING_EVENT"/>
<action android:name="com.google.firebase.MESSAGING_EVENT" />
</intent-filter>
</service>
<!-- Don't delete the meta-data below.
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
<!--
Don't delete the meta-data below.
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java
-->
<meta-data
android:name="flutterEmbedding"
android:value="2"/>
android:value="2" />
</application>

</manifest>
48 changes: 48 additions & 0 deletions android/app/src/main/java/org/sparcs/otlplus/NextLectureWidget.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
package org.sparcs.otlplus

import android.appwidget.AppWidgetManager
import android.appwidget.AppWidgetProvider
import android.content.Context
import android.widget.RemoteViews
import org.sparcs.otlplus.api.NextLectureData

/**
* Implementation of App Widget functionality.
*/

class NextLectureWidget : AppWidgetProvider() {
override fun onUpdate(
context: Context,
appWidgetManager: AppWidgetManager,
appWidgetIds: IntArray
) {
// There may be multiple widgets active, so update all of them
for (appWidgetId in appWidgetIds) {
updateNextLectureWidget(context, appWidgetManager, appWidgetId)
}
}

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
}
}

internal fun updateNextLectureWidget(
context: Context,
appWidgetManager: AppWidgetManager,
appWidgetId: Int
) {
// Construct the RemoteViews object
RemoteViews(context.packageName, R.layout.next_lecture_widget).let {
it.setTextViewText(R.id.nextLectureDate, NextLectureData.nextLectureDate)
it.setTextViewText(R.id.nextLectureName, NextLectureData.nextLectureName)
it.setTextViewText(R.id.nextLecturePlace, NextLectureData.nextLecturePlace)
it.setTextViewText(R.id.nextLectureProfessor, NextLectureData.nextLectureProfessor)
// Instruct the widget manager to update the widget
appWidgetManager.updateAppWidget(appWidgetId, it)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package org.sparcs.otlplus

import android.appwidget.AppWidgetManager
import android.content.ComponentName
import android.content.Context
import android.content.Intent
import android.content.SharedPreferences

class SharedPreferenceUpdateListener(context: Context) {
private val sharedPreferences = context.getSharedPreferences("FlutterSharedPreferences", Context.MODE_PRIVATE)

private val appWidgetManager = AppWidgetManager.getInstance(context)
private val componentName = ComponentName(context, TimetableWidget::class.java)

private val preferenceChangeListener = SharedPreferences.OnSharedPreferenceChangeListener { _, _ ->
val intent = Intent(context, TimetableWidget::class.java).apply {
action = AppWidgetManager.ACTION_APPWIDGET_UPDATE
putExtra(AppWidgetManager.EXTRA_APPWIDGET_IDS, appWidgetManager.getAppWidgetIds(componentName))
}
context.sendBroadcast(intent)
}

fun register() {
sharedPreferences.registerOnSharedPreferenceChangeListener(preferenceChangeListener)
}

fun unregister() {
sharedPreferences.unregisterOnSharedPreferenceChangeListener(preferenceChangeListener)
}
}
115 changes: 115 additions & 0 deletions android/app/src/main/java/org/sparcs/otlplus/TimetableWidget.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
package org.sparcs.otlplus

import android.annotation.SuppressLint
import android.appwidget.AppWidgetManager
import android.appwidget.AppWidgetProvider
import android.content.Context
import android.util.TypedValue
import android.widget.RemoteViews
import org.sparcs.otlplus.api.ApiLoader
import org.sparcs.otlplus.api.Lecture
import org.sparcs.otlplus.api.LocalTime
import org.sparcs.otlplus.api.TimetableData
import org.sparcs.otlplus.api.WeekDays
import org.sparcs.otlplus.constants.BlockColor

val timeTableColumns = listOf(
R.id.time_table_column_1,
R.id.time_table_column_2,
R.id.time_table_column_3,
R.id.time_table_column_4,
R.id.time_table_column_5,
)

data class TimeTableElement(
val length: Float,
val lecture: Lecture?
)

/**
* Implementation of App Widget functionality.
*/
class TimetableWidget : AppWidgetProvider() {
private val CHANNEL = "https://otl.sparcs.org/"

override fun onUpdate(
context: Context,
appWidgetManager: AppWidgetManager,
appWidgetIds: IntArray
) {
val apiLoader = ApiLoader(context)
val semestersUrl = "$CHANNEL/api/semesters"

apiLoader.get(semestersUrl) { dataString ->
println(dataString)
val timetableData = TimetableData(dataString)

for (appWidgetId in appWidgetIds) {
updateTimetableWidget(context, appWidgetManager, appWidgetId, timetableData)
}
}
}
}

@SuppressLint("NewApi")
internal fun updateTimetableWidget(
context: Context,
appWidgetManager: AppWidgetManager,
appWidgetId: Int,
timetableData: TimetableData,
) {
val views = RemoteViews(context.packageName, R.layout.timetable_widget)

for (timetableColumn in timeTableColumns) {
views.removeAllViews(timetableColumn)
}

val weekTimetable = createTimeTable(timetableData.lectures)

for ((weekday, dayTimetable) in weekTimetable.withIndex()) {
for (timeTableElement in dayTimetable) {
val blockView = when(timeTableElement.lecture) {
null -> RemoteViews(context.packageName, R.layout.blank_view)
else -> RemoteViews(context.packageName, BlockColor.getLayout(timeTableElement.lecture)).apply {
setTextViewText(R.id.timetable_block_lecture_name, timeTableElement.lecture.name)
}
}

blockView.setViewLayoutHeight(
R.id.timetable_block_root,
timeTableElement.length * 36,
TypedValue.COMPLEX_UNIT_DIP)

views.addView(timeTableColumns[weekday], blockView)
}
}

// Instruct the widget manager to update the widget
appWidgetManager.updateAppWidget(appWidgetId, views)
}

fun createTimeTable(lectures: List<Lecture>): List<List<TimeTableElement>> {
val timetable = List(5) { mutableListOf<TimeTableElement>() }

for (dayIndex in WeekDays.entries.toTypedArray().indices) {
val day = WeekDays.entries[dayIndex]

val dailyLectures = lectures.flatMap { lecture ->
lecture.timeBlocks.filter { it.weekday == day }.map { it to lecture }
}.sortedBy { it.first.start.hoursFloat }

var currentTime = LocalTime(9, 0)

for ((timeBlock, lecture) in dailyLectures) {
if (timeBlock.start.hoursFloat > currentTime.hoursFloat) {
val freeTimeLength = timeBlock.start.hoursFloat - currentTime.hoursFloat
timetable[dayIndex].add(TimeTableElement(freeTimeLength, null))
}
val lectureLength = timeBlock.end.hoursFloat - timeBlock.start.hoursFloat
timetable[dayIndex].add(TimeTableElement(lectureLength, lecture))
currentTime = timeBlock.end
}
}

return timetable
}
36 changes: 36 additions & 0 deletions android/app/src/main/java/org/sparcs/otlplus/api/ApiLoader.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package org.sparcs.otlplus.api

import android.content.Context
import okhttp3.*
import java.io.IOException
import java.util.concurrent.TimeUnit

class ApiLoader(context: Context) {
private val cookies = Cookies(context)
private val client = OkHttpClient.Builder()
.connectTimeout(30, TimeUnit.SECONDS) // Connection timeout
.readTimeout(30, TimeUnit.SECONDS) // Read timeout
.writeTimeout(30, TimeUnit.SECONDS) // Write timeout
.build()

private val cookieHeader = cookies.header

fun get(url: String, then: (String) -> Unit) {
val request = Request.Builder()
.url(url)
.addHeader("Cookie", cookieHeader ?: "")
.build()

client.newCall(request).enqueue(object: Callback {
override fun onFailure(call: Call, e: IOException) {
println("FAILURE @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@")
e.printStackTrace()
}

override fun onResponse(call: Call, response: Response) {
println("GET RESPONSE @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@")
then(response.body?.string() ?: "")
}
})
}
}
12 changes: 12 additions & 0 deletions android/app/src/main/java/org/sparcs/otlplus/api/Cookies.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package org.sparcs.otlplus.api

import android.content.Context

class Cookies(context: Context) {
var header: String?

init {
val sharedPreferences = context.getSharedPreferences("FlutterSharedPreferences", Context.MODE_PRIVATE)
header = sharedPreferences.getString("flutter.cookie_header", null)
}
}
Loading
Loading