diff --git a/PennMobile/src/main/AndroidManifest.xml b/PennMobile/src/main/AndroidManifest.xml index 27cc004d..09b7e79e 100644 --- a/PennMobile/src/main/AndroidManifest.xml +++ b/PennMobile/src/main/AndroidManifest.xml @@ -27,6 +27,7 @@ android:theme="@style/AppTheme.Launcher" android:usesCleartextTraffic="true" tools:targetApi="m"> + @@ -39,6 +40,19 @@ android:resource="@xml/dining_hall_widget_info" /> + + + + + + + + + > getGsrReservations( + @Header("Authorization") String bearerToken + ); +} diff --git a/PennMobile/src/main/java/com/pennapps/labs/pennmobile/gsr/widget/GsrReservationsWidget.kt b/PennMobile/src/main/java/com/pennapps/labs/pennmobile/gsr/widget/GsrReservationsWidget.kt new file mode 100644 index 00000000..2c7b4f73 --- /dev/null +++ b/PennMobile/src/main/java/com/pennapps/labs/pennmobile/gsr/widget/GsrReservationsWidget.kt @@ -0,0 +1,119 @@ +package com.pennapps.labs.pennmobile.gsr.widget + +import android.app.PendingIntent +import android.appwidget.AppWidgetManager +import android.appwidget.AppWidgetProvider +import android.content.Context +import android.content.Intent +import android.net.Uri +import android.widget.RemoteViews +import com.google.gson.GsonBuilder +import com.google.gson.reflect.TypeToken +import com.pennapps.labs.pennmobile.MainActivity +import com.pennapps.labs.pennmobile.R +import com.pennapps.labs.pennmobile.api.GsrReservationsRequest +import com.pennapps.labs.pennmobile.api.Serializer +import com.pennapps.labs.pennmobile.gsr.classes.GSRReservation +import com.squareup.okhttp.OkHttpClient +import retrofit.RestAdapter +import retrofit.client.OkClient +import retrofit.converter.GsonConverter +import java.util.concurrent.TimeUnit + +class GsrReservationsWidget : AppWidgetProvider() { + override fun onUpdate( + context: Context, + appWidgetManager: AppWidgetManager, + appWidgetIds: IntArray, + ) { + for (appWidgetId in appWidgetIds) { + val mainActivityIntent = Intent(context, MainActivity::class.java) + mainActivityIntent.apply { + flags += Intent.FLAG_ACTIVITY_NEW_TASK + flags += Intent.FLAG_ACTIVITY_CLEAR_TOP + } + + mainActivityIntent.putExtra("Gsr_Tab_Switch", 1) + + val serviceIntent = Intent(context, GsrReservationsWidgetAdapter::class.java) + val pendingIntent: PendingIntent = + PendingIntent.getActivity( + context, + appWidgetId, + mainActivityIntent, + PendingIntent.FLAG_IMMUTABLE, + ) + serviceIntent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId) + + // setData allows the system to distinguish between different service intents. Without + // setData, onGetViewFactory is called only once for multiple widgets and send + // the same intent to all of them. + serviceIntent.setData(Uri.parse(serviceIntent.toUri(Intent.URI_INTENT_SCHEME))) + + // Setting up the widget remoteViews; change cardview to something else + val views = RemoteViews(context.packageName, R.layout.gsr_reservations_widget) + views.setRemoteAdapter(R.id.gsr_reservation_widget_stack_view, serviceIntent) + views.setEmptyView( + R.id.gsr_reservation_widget_stack_view, + R.id.gsr_reservation_widget_empty_view, + ) + + // Setting up the intents for the remoteview for both when it is empty and + // when it loads the collection view (in this case we use setPendingIntentTemplate to + // the entire stackView that contains all the items). + views.setPendingIntentTemplate(R.id.gsr_reservation_widget_stack_view, pendingIntent) + views.setOnClickPendingIntent(R.id.gsr_reservation_widget_empty_view, pendingIntent) + + // Notify appwidgetviewdata has changed to call getViewAt to set up the widget UI + // and handle update for every appwidget item in the Collection widget. + appWidgetManager.notifyAppWidgetViewDataChanged( + appWidgetId, + R.id.gsr_reservation_widget_stack_view, + ) + appWidgetManager.updateAppWidget(appWidgetId, views) + } + } + + // onEnabled and onDisabled are typically used for alarmManager testing and logs to check whether + // appwidget is properly enabled/disabled. + override fun onEnabled(context: Context) { + } + + override fun onDisabled(context: Context) { + } + + companion object { + private var mGSRReservationsRequest: GsrReservationsRequest? = null + + @JvmStatic + val gsrReservationsRequestInstance: GsrReservationsRequest + get() { + if (mGSRReservationsRequest == null) { + val gsonBuilder = GsonBuilder() + + // RegisterTypeAdapter with GsrReservationSerializer + // since we are only accessing gsr reservations + gsonBuilder.registerTypeAdapter( + object : TypeToken?>() {}.type, + Serializer.GsrReservationSerializer(), + ) + + val gson = gsonBuilder.create() + val okHttpClient = OkHttpClient() + okHttpClient.setConnectTimeout(35, TimeUnit.SECONDS) // Connection timeout + okHttpClient.setReadTimeout(35, TimeUnit.SECONDS) // Read timeout + okHttpClient.setWriteTimeout(35, TimeUnit.SECONDS) // Write timeout + + val restAdapter = + RestAdapter + .Builder() + .setConverter(GsonConverter(gson)) + .setClient(OkClient(okHttpClient)) + .setEndpoint("https://pennmobile.org/api") + .build() + mGSRReservationsRequest = restAdapter.create(GsrReservationsRequest::class.java) + } + return mGSRReservationsRequest!! + } + } +} diff --git a/PennMobile/src/main/java/com/pennapps/labs/pennmobile/gsr/widget/GsrReservationsWidgetAdapter.kt b/PennMobile/src/main/java/com/pennapps/labs/pennmobile/gsr/widget/GsrReservationsWidgetAdapter.kt new file mode 100644 index 00000000..8cec89ea --- /dev/null +++ b/PennMobile/src/main/java/com/pennapps/labs/pennmobile/gsr/widget/GsrReservationsWidgetAdapter.kt @@ -0,0 +1,136 @@ +package com.pennapps.labs.pennmobile.gsr.widget + +import android.appwidget.AppWidgetManager +import android.content.Context +import android.content.Intent +import android.graphics.BitmapFactory +import android.os.Bundle +import android.widget.RemoteViews +import android.widget.RemoteViewsService +import androidx.preference.PreferenceManager +import com.google.firebase.crashlytics.FirebaseCrashlytics +import com.pennapps.labs.pennmobile.R +import com.pennapps.labs.pennmobile.api.GsrReservationsRequest +import com.pennapps.labs.pennmobile.gsr.classes.GSRReservation +import org.joda.time.format.DateTimeFormat +import org.joda.time.format.DateTimeFormatter +import rx.Observable +import java.net.HttpURLConnection +import java.net.URL + +class GsrReservationsWidgetAdapter : RemoteViewsService() { + override fun onGetViewFactory(intent: Intent): RemoteViewsFactory = GsrReservationWidgetFactory(applicationContext, intent) + + class GsrReservationWidgetFactory( + private val context: Context, + intent: Intent, + ) : RemoteViewsFactory { + private var mGsrReservationsRequest: GsrReservationsRequest? = null + private var appWidgetId: Int = + intent.getIntExtra( + AppWidgetManager.EXTRA_APPWIDGET_ID, + AppWidgetManager.INVALID_APPWIDGET_ID, + ) + private var dataSet: List = emptyList() + private var sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context) + + override fun onCreate() { + mGsrReservationsRequest = GsrReservationsWidget.gsrReservationsRequestInstance + getWidgetGsrReservations() + } + + // Not used since already handled + override fun onDataSetChanged() { + } + + override fun onDestroy() { + } + + override fun getCount(): Int = dataSet.size + + // TODO("Get building name(?), and hopefully support click behavior") + override fun getViewAt(index: Int): RemoteViews { + val reservation = dataSet[index] + val roomName = reservation.name + + val formatter: DateTimeFormatter = + DateTimeFormat.forPattern("yyyy-MM-dd'T'HH:mm:ssZ") + val from = formatter.parseDateTime(reservation.fromDate) + val to = formatter.parseDateTime(reservation.toDate) + val day = from.toString("EEEE, MMMM d") + val fromHour = from.toString("h:mm a") + val toHour = to.toString("h:mm a") + + val imageUrl = + reservation.info?.get("thumbnail") + ?: "https://s3.us-east-2.amazonaws.com/labs.api/dining/MBA+Cafe.jpg" + + val views = RemoteViews(context.packageName, R.layout.gsr_reservations_widget_item) + views.setTextViewText(R.id.gsr_reservation_widget_item_location_tv, roomName) + views.setTextViewText( + R.id.gsr_reservation_widget_item_time_tv, + "$day\n$fromHour-$toHour", + ) + + try { + val urlConnection = URL(imageUrl) + val connection = + urlConnection + .openConnection() as HttpURLConnection + connection.doInput = true + connection.connect() + val input = connection.inputStream + val myBitmap = BitmapFactory.decodeStream(input) + views.setImageViewBitmap(R.id.gsr_reservation_widget_item_iv, myBitmap) + } catch (e: Exception) { + e.printStackTrace() + } + + val extras = Bundle() + val fillInIntent = Intent() + fillInIntent.putExtras(extras) + views.setOnClickFillInIntent(R.id.gsr_reservation_widget_item_root, fillInIntent) + + return views + } + + override fun getLoadingView(): RemoteViews? = null + + override fun getViewTypeCount(): Int = 1 + + override fun getItemId(id: Int): Long = id.toLong() + + override fun hasStableIds(): Boolean = true + + private fun getWidgetGsrReservations() { + try { + if (mGsrReservationsRequest != null) { + val token = + sharedPreferences.getString( + context.getString(R.string.access_token), + "", + ) + mGsrReservationsRequest!! + .getGsrReservations("Bearer $token") + .flatMap { reservations -> Observable.from(reservations) } + .flatMap { reservation -> + Observable.just(reservation) + }.toList() + .subscribe { reservations -> + dataSet = reservations + println("subscribed") + val appWidgetManager: AppWidgetManager = + AppWidgetManager.getInstance(context) + appWidgetManager.notifyAppWidgetViewDataChanged( + appWidgetId, + R.id.gsr_reservation_widget_stack_view, + ) + } + } + } catch (e: Exception) { + FirebaseCrashlytics.getInstance().recordException(e) + e.printStackTrace() + } + } + } +} diff --git a/PennMobile/src/main/res/layout/fragment_dining_holder.xml b/PennMobile/src/main/res/layout/fragment_dining_holder.xml index 8a6cffdb..5ebf6f16 100644 --- a/PennMobile/src/main/res/layout/fragment_dining_holder.xml +++ b/PennMobile/src/main/res/layout/fragment_dining_holder.xml @@ -43,6 +43,10 @@ android:maxLines="1" /> + + + + + + + + \ No newline at end of file diff --git a/PennMobile/src/main/res/layout/gsr_reservations_widget_item.xml b/PennMobile/src/main/res/layout/gsr_reservations_widget_item.xml new file mode 100644 index 00000000..5a979c13 --- /dev/null +++ b/PennMobile/src/main/res/layout/gsr_reservations_widget_item.xml @@ -0,0 +1,54 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/PennMobile/src/main/res/values/ids.xml b/PennMobile/src/main/res/values/ids.xml index 419d846a..e889caf6 100644 --- a/PennMobile/src/main/res/values/ids.xml +++ b/PennMobile/src/main/res/values/ids.xml @@ -7,4 +7,7 @@ + + + diff --git a/PennMobile/src/main/res/xml/gsr_reservations_widget_info.xml b/PennMobile/src/main/res/xml/gsr_reservations_widget_info.xml new file mode 100644 index 00000000..08b5a4a9 --- /dev/null +++ b/PennMobile/src/main/res/xml/gsr_reservations_widget_info.xml @@ -0,0 +1,13 @@ + + \ No newline at end of file diff --git a/ktlint.jar b/ktlint.jar new file mode 100644 index 00000000..da6183c6 Binary files /dev/null and b/ktlint.jar differ