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

Baron Widget Newbie Project PR #614

Merged
merged 11 commits into from
Apr 27, 2024
Merged
2 changes: 2 additions & 0 deletions PennMobile/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,8 @@ dependencies {
implementation 'com.squareup.retrofit2:converter-moshi:2.9.0'
implementation 'com.squareup.retrofit2:converter-scalars:2.9.0'
implementation 'com.squareup.retrofit2:converter-gson:2.9.0'
implementation 'io.reactivex.rxjava2:rxandroid:2.0.2'
baronhsieh2005 marked this conversation as resolved.
Show resolved Hide resolved
implementation 'com.squareup.retrofit2:adapter-rxjava2:2.4.0'

}

Expand Down
53 changes: 36 additions & 17 deletions PennMobile/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,22 @@
android:versionName="3.1.4">

<uses-permission android:name="android.permission.WAKE_LOCK" />

<uses-feature
android:glEsVersion="0x00020000"
android:required="true" />

<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" /> <!-- for Penn Transit API -->
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="com.google.android.providers.gsf.permission.READ_GSERVICES" /> <!-- vibration for laundry alert -->
<uses-permission android:name="com.pennapps.labs.pennmobile.permission.MAPS_RECEIVE" /> <!-- adding new contacts -->
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<uses-permission android:name="android.permission.VIBRATE" />
<uses-permission android:name="android.permission.READ_CONTACTS" />
<uses-permission android:name="android.permission.WRITE_CONTACTS" />

baronhsieh2005 marked this conversation as resolved.
Show resolved Hide resolved
<application
android:name="androidx.multidex.MultiDexApplication"
android:allowBackup="true"
Expand All @@ -13,18 +29,35 @@
android:theme="@style/AppTheme.Launcher"
android:usesCleartextTraffic="true"
tools:targetApi="m">
<receiver
android:name="com.pennapps.labs.pennmobile.DiningHallWidget"
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/dining_hall_widget_info" />
</receiver>

<activity
android:name=".MainActivity"
android:exported="true"
android:label="@string/app_name"
android:screenOrientation="portrait"
android:windowSoftInputMode="adjustPan"
android:exported="true">
android:windowSoftInputMode="adjustPan">
<intent-filter>
<action android:name="android.intent.action.MAIN" />

<category android:name="android.intent.category.LAUNCHER" />

<action android:name="android.intent.action.VIEW" />
</intent-filter>
</activity>
<service android:name=".adapters.DiningHallWidgetAdapter"
android:permission="android.permission.BIND_REMOTEVIEWS" />

<meta-data
android:name="com.google.android.gms.version"
android:value="@integer/google_play_services_version" />
Expand All @@ -33,8 +66,7 @@
android:value="18a765536e6539a73a15dd36c369ed29cfb91aa1" />
<meta-data
android:name="preloaded_fonts"
android:resource="@array/preloaded_fonts"
/>
android:resource="@array/preloaded_fonts" />

<receiver
android:name=".LaundryBroadcastReceiver"
Expand All @@ -44,18 +76,5 @@
android:name="com.google.android.maps"
android:required="false" />
</application>
<uses-feature
android:glEsVersion="0x00020000"
android:required="true" />
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" /> <!-- for Penn Transit API -->
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="com.google.android.providers.gsf.permission.READ_GSERVICES" /> <!-- vibration for laundry alert -->
<uses-permission android:name="com.pennapps.labs.pennmobile.permission.MAPS_RECEIVE" /> <!-- adding new contacts -->
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<uses-permission android:name="android.permission.VIBRATE" />
<uses-permission android:name="android.permission.READ_CONTACTS" />
<uses-permission android:name="android.permission.WRITE_CONTACTS" />

</manifest>
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.pennapps.labs.pennmobile

import android.annotation.SuppressLint
baronhsieh2005 marked this conversation as resolved.
Show resolved Hide resolved
import android.os.Bundle
import android.util.Log
import androidx.fragment.app.Fragment
Expand Down Expand Up @@ -130,7 +131,7 @@ class DiningFragment : Fragment() {
} else {
binding.internetConnectionDining.visibility = View.GONE
}

// Map each item in the list of venues to a Venue Observable, then map each Venue to a DiningHall Observable
mStudentLife.venues()
.flatMap { venues -> Observable.from(venues) }
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
package com.pennapps.labs.pennmobile

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.adapters.DiningHallWidgetAdapter
import com.pennapps.labs.pennmobile.api.DiningRequest
import com.pennapps.labs.pennmobile.api.Serializer
import com.pennapps.labs.pennmobile.classes.DiningHall
import com.pennapps.labs.pennmobile.classes.Venue
import com.squareup.okhttp.OkHttpClient
import retrofit.RestAdapter
import retrofit.client.OkClient
import retrofit.converter.GsonConverter
import java.util.concurrent.TimeUnit

/**
* Implementation of App Widget functionality.
*/
class DiningHallWidget : AppWidgetProvider() {
baronhsieh2005 marked this conversation as resolved.
Show resolved Hide resolved
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("Widget_Tab_Switch", 2)
baronhsieh2005 marked this conversation as resolved.
Show resolved Hide resolved
val serviceIntent = Intent(context, DiningHallWidgetAdapter::class.java)
val pendingIntent : PendingIntent = PendingIntent.getActivity(context, appWidgetId, mainActivityIntent, PendingIntent.FLAG_IMMUTABLE)
serviceIntent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId)
serviceIntent.setData(Uri.parse(serviceIntent.toUri(Intent.URI_INTENT_SCHEME)))
val views = RemoteViews(context.packageName, R.layout.dining_hall_widget)
views.setRemoteAdapter(R.id.stackview, serviceIntent)
views.setEmptyView(R.id.stackview, R.id.emptyview)
views.setPendingIntentTemplate(R.id.stackview, pendingIntent)
views.setOnClickPendingIntent(R.id.emptyview, pendingIntent)
appWidgetManager.notifyAppWidgetViewDataChanged(appWidgetId, R.id.stackview)
appWidgetManager.updateAppWidget(appWidgetId, views)
}
}

override fun onEnabled(context: Context) {
baronhsieh2005 marked this conversation as resolved.
Show resolved Hide resolved
// 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
}

companion object {
private var mDiningRequest: DiningRequest? = null
val ACTION_AUTO_UPDATE = "AUTO_UPDATE"
@JvmStatic
val diningRequestInstance: DiningRequest
get() {
if (mDiningRequest == null) {
val gsonBuilder = GsonBuilder()
gsonBuilder.registerTypeAdapter(DiningHall::class.java,
Serializer.MenuSerializer()
baronhsieh2005 marked this conversation as resolved.
Show resolved Hide resolved
)
gsonBuilder.registerTypeAdapter(object : TypeToken<MutableList<Venue?>?>() {}.type,
Serializer.VenueSerializer()
)

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()
mDiningRequest = restAdapter.create(DiningRequest::class.java)
}
return mDiningRequest!!
}
}
}


Original file line number Diff line number Diff line change
Expand Up @@ -95,11 +95,18 @@ class MainActivity : AppCompatActivity() {
// Show HomeFragment if logged in, otherwise show LoginFragment
val pennKey = mSharedPrefs.getString(getString(R.string.pennkey), null)
val guestMode = mSharedPrefs.getBoolean(getString(R.string.guest_mode), false)
var diningWidgetBroadCast = 0
baronhsieh2005 marked this conversation as resolved.
Show resolved Hide resolved
if (pennKey == null && !guestMode) {
startLoginFragment()
} else {
startHomeFragment()
}
if (intent != null) {
diningWidgetBroadCast = intent.getIntExtra("Widget_Tab_Switch", -1)
}
if (diningWidgetBroadCast != -1) {
setTab(DINING_ID)
}
}

private fun onExpandableBottomNavigationItemSelected() {
Expand Down Expand Up @@ -268,6 +275,7 @@ class MainActivity : AppCompatActivity() {

val HOME_ID = R.id.nav_home
val GSR_ID = R.id.nav_gsr
val DINING_ID = R.id.nav_dining

private var mStudentLife: StudentLife? = null
private var mPlatform: Platform? = null
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
package com.pennapps.labs.pennmobile.adapters

import android.annotation.SuppressLint
baronhsieh2005 marked this conversation as resolved.
Show resolved Hide resolved
import android.appwidget.AppWidgetManager
import android.content.Context
import android.content.Intent
import android.graphics.Color
import android.os.Bundle
import android.util.Log
import android.widget.RemoteViews
import android.widget.RemoteViewsService
import com.pennapps.labs.pennmobile.DiningHallWidget
import com.pennapps.labs.pennmobile.R
import com.pennapps.labs.pennmobile.api.DiningRequest
import com.pennapps.labs.pennmobile.classes.DiningHall
import com.pennapps.labs.pennmobile.classes.Venue
import rx.Observable

// For detailed documentation about app widgets using xml layout, check out this link below:
// https://programmer.group/app-widgets-details-four-remoteviews-remoteviews-service-and-remoteviews-factory.html

class DiningHallWidgetAdapter : RemoteViewsService(){

override fun onGetViewFactory(p0: Intent): RemoteViewsFactory {
return diningWidgetFactory(applicationContext, p0)
}

class diningWidgetFactory(private val context: Context, intent: Intent) : RemoteViewsFactory {
private var mDiningRequest: DiningRequest? = null
private var appWidgetId: Int = intent.getIntExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, AppWidgetManager.INVALID_APPWIDGET_ID)
private var dataSet: List<DiningHall> = emptyList()

// Connection to data source
override fun onCreate() {
//connect to data source
mDiningRequest = DiningHallWidget.diningRequestInstance
getWidgetDiningHalls()
}

// The place where we fetch data from the source set new data and update collection widget accordingly
override fun onDataSetChanged() {
//refresh data -> Update data every 30 minutes
baronhsieh2005 marked this conversation as resolved.
Show resolved Hide resolved
}

override fun onDestroy() {
//close connection to data source
}

override fun getCount(): Int {
Log.d("msg", "${dataSet.size}")
baronhsieh2005 marked this conversation as resolved.
Show resolved Hide resolved
return dataSet.size
}

override fun getViewAt(position: Int): RemoteViews {
val views = RemoteViews(context.packageName, R.layout.dining_hall_widget_item)
Log.d("msg", "position: ${position}, name: ${dataSet[position].name}")
baronhsieh2005 marked this conversation as resolved.
Show resolved Hide resolved
views.setTextViewText(R.id.appwidget_text, dataSet[position].name)
views.setImageViewResource(R.id.diningBackground, dataSet[position].image)
if (dataSet[position].isOpen) {
Log.d("msg", "Open right now")
baronhsieh2005 marked this conversation as resolved.
Show resolved Hide resolved
views.setImageViewResource(R.id.imageView, R.drawable.baseline_check_circle_24)
views.setInt(R.id.textView3, "setBackgroundColor", Color.parseColor("#6DB786"))
if (dataSet[position].openMeal() != "all" && dataSet[position].openMeal() != null) {
baronhsieh2005 marked this conversation as resolved.
Show resolved Hide resolved
val resources = context.resources
val open_label : String = resources.getString(getOpenStatusLabel(dataSet[position].openMeal() ?: ""))
views.setTextViewText(R.id.textView3, open_label)
baronhsieh2005 marked this conversation as resolved.
Show resolved Hide resolved
}
}
else {
baronhsieh2005 marked this conversation as resolved.
Show resolved Hide resolved
views.setImageViewResource(R.id.imageView, R.drawable.baseline_cancel_24)
views.setInt(R.id.textView3, "setBackgroundColor", Color.parseColor("#990000"))
if (dataSet[position].openTimes().isEmpty()) {
views.setTextViewText(R.id.textView3, "Closed Today")
}
else {
baronhsieh2005 marked this conversation as resolved.
Show resolved Hide resolved
views.setTextViewText(R.id.textView3, "Closed")
}
}
val extras = Bundle()
extras.putInt("key_data", position)
baronhsieh2005 marked this conversation as resolved.
Show resolved Hide resolved
val fillInIntent = Intent()
fillInIntent.putExtras(extras)
baronhsieh2005 marked this conversation as resolved.
Show resolved Hide resolved
views.setOnClickFillInIntent(R.id.widgetBackground, fillInIntent)
return views
}

override fun getLoadingView(): RemoteViews? {
return null
}

override fun getViewTypeCount(): Int {
return 1
}

override fun getItemId(position: Int): Long {
return position.toLong()
}

override fun hasStableIds(): Boolean {
return true
}

@SuppressLint("CheckResult")
baronhsieh2005 marked this conversation as resolved.
Show resolved Hide resolved
private fun getWidgetDiningHalls() {
if (mDiningRequest != null) {
mDiningRequest!!.venues()
.flatMap { venues -> Observable.from(venues) }
.flatMap { venue ->
val hall = createHall(venue)
Observable.just(hall) }
.toList()
.subscribe { diningHalls ->
dataSet = diningHalls
Log.d("msg", "Request sent ${dataSet.size}")
baronhsieh2005 marked this conversation as resolved.
Show resolved Hide resolved
val appWidgetManager: AppWidgetManager = AppWidgetManager.getInstance(context)
appWidgetManager.notifyAppWidgetViewDataChanged(appWidgetId, R.id.stackview)
}
}
}

private fun getOpenStatusLabel(openMeal: String): Int {
return when (openMeal) {
"Breakfast" -> R.string.dining_hall_breakfast
"Brunch" -> R.string.dining_hall_brunch
"Lunch" -> R.string.dining_hall_lunch
"Dinner" -> R.string.dining_hall_dinner
"Late Night" -> R.string.dining_hall_late_night
else -> R.string.dining_hall_open
}
}

companion object {
fun createHall(venue: Venue): DiningHall {
when (venue.id){
593 -> return DiningHall(venue.id, venue.name, venue.isResidential, venue.getHours(), venue, R.drawable.dining_commons)
636 -> return DiningHall(venue.id, venue.name, venue.isResidential, venue.getHours(), venue, R.drawable.dining_hill_house)
637 -> return DiningHall(venue.id, venue.name, venue.isResidential, venue.getHours(), venue, R.drawable.dining_kceh)
638 -> return DiningHall(venue.id, "Falk Kosher\nDining", venue.isResidential, venue.getHours(), venue, R.drawable.dining_hillel)
639 -> return DiningHall(venue.id, venue.name, venue.isResidential, venue.getHours(), venue, R.drawable.dining_houston)
640 -> return DiningHall(venue.id, venue.name, venue.isResidential, venue.getHours(), venue, R.drawable.dining_marks)
641 -> return DiningHall(venue.id, venue.name, venue.isResidential, venue.getHours(), venue, R.drawable.dining_accenture)
642 -> return DiningHall(venue.id, venue.name, venue.isResidential, venue.getHours(), venue, R.drawable.dining_joes_cafe)
1442 -> return DiningHall(venue.id, venue.name, venue.isResidential, venue.getHours(), venue, R.drawable.dining_nch)
747 -> return DiningHall(venue.id, venue.name, venue.isResidential, venue.getHours(), venue, R.drawable.dining_mcclelland)
1057 -> return DiningHall(venue.id, venue.name, venue.isResidential, venue.getHours(), venue, R.drawable.dining_gourmet_grocer)
1058 -> return DiningHall(venue.id, "Tortas Frontera", venue.isResidential, venue.getHours(), venue, R.drawable.dining_tortas)
1163 -> return DiningHall(venue.id, venue.name, venue.isResidential, venue.getHours(), venue, R.drawable.dining_commons)
1731 -> return DiningHall(venue.id, venue.name, venue.isResidential, venue.getHours(), venue, R.drawable.dining_nch)
1732 -> return DiningHall(venue.id, venue.name, venue.isResidential, venue.getHours(), venue, R.drawable.dining_mba_cafe)
1733 -> return DiningHall(venue.id, "Pret a Manger Locust", venue.isResidential, venue.getHours(), venue, R.drawable.dining_pret_a_manger)
else -> return DiningHall(venue.id, venue.name, venue.isResidential, venue.getHours(), venue, R.drawable.dining_commons)
}
}
}
}
}
Loading
Loading