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

Introducing Notifications #637

Open
wants to merge 6 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions PennMobile/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,7 @@ dependencies {
testImplementation platform(libs.androidx.compose.bom)
androidTestImplementation platform(libs.androidx.compose.bom)
implementation platform(libs.firebase.bom)
implementation(libs.firebase.messaging)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why does this have parens but the other imports don't? Can we standardize formatting?


implementation libs.bundles.compose
implementation libs.bundles.material
Expand Down
19 changes: 18 additions & 1 deletion PennMobile/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,11 @@
<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.POST_NOTIFICATIONS" />

<application
android:name="androidx.multidex.MultiDexApplication"
android:label="Penn Mobile"
android:allowBackup="true"
android:hardwareAccelerated="true"
android:icon="@mipmap/ic_launcher"
Expand Down Expand Up @@ -55,7 +57,12 @@
</activity>
<service android:name=".dining.widget.DiningHallWidgetAdapter"
android:permission="android.permission.BIND_REMOTEVIEWS" />

<service android:name=".notifications.PushNotificationService"
android:exported="false">
<intent-filter>
<action android:name="com.google.firebase.MESSAGING_EVENT" />
</intent-filter>
</service>
<meta-data
android:name="com.google.android.gms.version"
android:value="@integer/google_play_services_version" />
Expand All @@ -65,6 +72,16 @@
<meta-data
android:name="preloaded_fonts"
android:resource="@array/preloaded_fonts" />
<meta-data
android:name="com.google.firebase.messaging.default_notification_channel_id"
android:value="default_channel_id"/>
<meta-data
android:name="com.google.firebase.messaging.default_notification_icon"
android:resource="@mipmap/ic_launcher_foreground" />
<meta-data
android:name="com.google.firebase.messaging.default_notification_color"
android:resource="@color/penn_red" />


<receiver
android:name=".laundry.LaundryBroadcastReceiver"
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package com.pennapps.labs.pennmobile

import StudentLifeRf2
import android.Manifest
import android.content.Context
import android.content.SharedPreferences
import android.content.pm.PackageManager
Expand All @@ -20,8 +21,10 @@ import android.view.inputmethod.InputMethodManager
import android.webkit.CookieManager
import android.widget.TextView
import android.widget.Toast
import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.app.AppCompatActivity
import androidx.coordinatorlayout.widget.CoordinatorLayout
import androidx.core.content.ContextCompat
import androidx.core.graphics.ColorUtils
import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentManager
Expand All @@ -35,6 +38,7 @@ import com.google.firebase.analytics.FirebaseAnalytics
import com.google.gson.GsonBuilder
import com.google.gson.reflect.TypeToken
import com.pennapps.labs.pennmobile.api.CampusExpress
import com.pennapps.labs.pennmobile.api.NotificationAPI
import com.pennapps.labs.pennmobile.api.OAuth2NetworkManager
import com.pennapps.labs.pennmobile.api.Platform
import com.pennapps.labs.pennmobile.api.Serializer
Expand Down Expand Up @@ -77,6 +81,18 @@ class MainActivity : AppCompatActivity() {
private lateinit var mFirebaseAnalytics: FirebaseAnalytics
val mNetworkManager by lazy { OAuth2NetworkManager(this) }

// Declare the launcher at the top of your Activity/Fragment:
private val requestPermissionLauncher =
registerForActivityResult(
ActivityResultContracts.RequestPermission(),
) { isGranted: Boolean ->
if (isGranted) {
// FCM SDK (and your app) can post notifications.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If isGranted is true, does that just mean you can send notifications even though nothing is written here.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

doubling this question

Is this something where we can choose to do something (ex: sending a "success!" message to the user), but it's also okay not to do anything (since notifications will be shown as enabled in the background?)

Copy link
Member

@meiron03 meiron03 Nov 22, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think we have to do anything, but having TODOs in main is generally considered bad practice. What behavior should we expect if notifications aren't enabled? Is this information generally available or should it be stored somewhere? (oops, answered by scrolling down LOL)

I don't think we have to do anything then. How do other ppl generally use this function?

} else {
// TODO: Inform user that that your app will not show notifications.
}
}

override fun onCreate(savedInstanceState: Bundle?) {
if (Build.VERSION.SDK_INT > 28) {
setTheme(R.style.DarkModeApi29)
Expand All @@ -91,6 +107,7 @@ class MainActivity : AppCompatActivity() {
setTheme(R.style.DarkBackground)
}
Utils.getCurrentSystemTime()
askNotificationPermission()
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why are we asking for notification permission every time the activity starts? It is probably annoying if they don't want notifications and the app keeps asking... I feel like the app should just ask once.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

udp: I didn't realize that android had a "never ask again" feature. Someone correct me if I'm wrong, but this case should be handled right.


setSupportActionBar(binding.include.toolbar)
fragmentManager = supportFragmentManager
Expand Down Expand Up @@ -132,6 +149,25 @@ class MainActivity : AppCompatActivity() {
}
}

private fun askNotificationPermission() {
// This is only necessary for API level >= 33 (TIRAMISU)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
if (ContextCompat.checkSelfPermission(this, Manifest.permission.POST_NOTIFICATIONS) ==
PackageManager.PERMISSION_GRANTED
) {
// FCM SDK (and your app) can post notifications.
} else if (shouldShowRequestPermissionRationale(Manifest.permission.POST_NOTIFICATIONS)) {
// TODO: display an educational UI explaining to the user the features that will be enabled
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there some method to keep track of the user already rejecting notifications?

Copy link
Contributor

@Akula112233 Akula112233 Nov 19, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It seems the "shouldShowRequestPermissionRationale" function actually checks the Manifest.permission.POST_NOTIFICATIONS to see whether these permissions was just rejected (meaning ask again on-create), or if it was rejected + "don't show again" box was checked (meaning don't ask again on-create)

When a user rejects, the specific type of rejection is stored within Manifest.permission.POST_NOTIFICATIONS. Pretty Cool

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

again, try to clean up TODOs. Personally, I don't believe we really need an explanation for this and am comfortable with doing nothing.

// by them granting the POST_NOTIFICATION permission. This UI should provide the user
// "OK" and "No thanks" buttons. If the user selects "OK," directly request the permission.
// If the user selects "No thanks," allow the user to continue without notifications.
} else {
// Directly ask for the permission
requestPermissionLauncher.launch(Manifest.permission.POST_NOTIFICATIONS)
}
}
}

private fun onExpandableBottomNavigationItemSelected() {
binding.include.expandableBottomBar.setOnNavigationItemSelectedListener { item ->
val position =
Expand Down Expand Up @@ -319,6 +355,7 @@ class MainActivity : AppCompatActivity() {
private var mStudentLifeRf2: StudentLifeRf2? = null
private var mPlatform: Platform? = null
private var mCampusExpress: CampusExpress? = null
private var mNotificationAPI: NotificationAPI? = null

@JvmStatic
val campusExpressInstance: CampusExpress
Expand Down Expand Up @@ -383,6 +420,31 @@ class MainActivity : AppCompatActivity() {
return mStudentLifeRf2!!
}

val notificationAPIInstance: NotificationAPI
get() {
if (mNotificationAPI == null) {
val okHttpClient =
OkHttpClient
.Builder()
.connectTimeout(35, TimeUnit.SECONDS)
.readTimeout(35, TimeUnit.SECONDS)
.writeTimeout(35, TimeUnit.SECONDS)
.build()

val retrofit =
Retrofit
.Builder()
.baseUrl("https://pennmobile.org/api/")
.client(okHttpClient)
.addConverterFactory(ScalarsConverterFactory.create())
.addConverterFactory(GsonConverterFactory.create())
.addCallAdapterFactory(RxJava2CallAdapterFactory.create())
.build()
mNotificationAPI = retrofit.create(NotificationAPI::class.java)
}
return mNotificationAPI!!
}

@JvmStatic
val studentLifeInstance: StudentLife
get() {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package com.pennapps.labs.pennmobile.api

import okhttp3.ResponseBody
import retrofit2.Response
import retrofit2.http.DELETE
import retrofit2.http.Header
import retrofit2.http.POST
import retrofit2.http.Path

interface NotificationAPI {
@POST("user/notifications/tokens/android/{token}/")
suspend fun sendNotificationToken(
@Header("Authorization") bearerToken: String,
@Path("token") token: String,
): Response<ResponseBody>

@DELETE("user/notifications/tokens/android/{token}/")
suspend fun deleteNotificationToken(
@Path("token") token: String,
): Response<ResponseBody>
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ import android.widget.Button
import android.widget.LinearLayout
import android.widget.Toast
import androidx.fragment.app.Fragment
import androidx.fragment.app.viewModels
import androidx.lifecycle.lifecycleScope
import androidx.preference.PreferenceManager
import com.google.firebase.analytics.FirebaseAnalytics
import com.google.firebase.crashlytics.FirebaseCrashlytics
Expand All @@ -29,6 +31,9 @@ import com.pennapps.labs.pennmobile.api.StudentLife
import com.pennapps.labs.pennmobile.api.classes.AccessTokenResponse
import com.pennapps.labs.pennmobile.api.classes.Account
import com.pennapps.labs.pennmobile.api.classes.GetUserResponse
import com.pennapps.labs.pennmobile.api.viewmodels.LoginWebviewViewmodel
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import org.apache.commons.lang3.RandomStringUtils
import retrofit.Callback
import retrofit.RetrofitError
Expand All @@ -55,6 +60,7 @@ class LoginWebviewFragment : Fragment() {
lateinit var platformAuthUrl: String
lateinit var clientID: String
lateinit var redirectUri: String
private val loginWebviewViewmodel: LoginWebviewViewmodel by viewModels()

override fun onCreateView(
inflater: LayoutInflater,
Expand Down Expand Up @@ -206,6 +212,7 @@ class LoginWebviewFragment : Fragment() {
editor.putLong(getString(R.string.token_expires_at), currentTime + expiresInInt)
editor.apply()
getUser(accessToken)
sendNotifToken()
}
}

Expand Down Expand Up @@ -260,6 +267,20 @@ class LoginWebviewFragment : Fragment() {
}
}

private fun sendNotifToken() {
val mNotificationAPI = MainActivity.notificationAPIInstance

val bearerToken = "Bearer " + sp.getString(getString(R.string.access_token), "").toString()
val notifToken = sp.getString(getString(R.string.notification_token), "").toString()

Log.d("Notification Token", notifToken)
val notGuest = !sp.getBoolean(mActivity.getString(R.string.guest_mode), false)

lifecycleScope.launch(Dispatchers.IO) {
loginWebviewViewmodel.sendToken(mNotificationAPI, notGuest, bearerToken, notifToken)
}
}

private fun getCodeChallenge(codeVerifier: String): String {
// Hash the code verifier
val md = MessageDigest.getInstance("SHA-256")
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package com.pennapps.labs.pennmobile.api.viewmodels

import android.util.Log
import androidx.lifecycle.ViewModel
import com.pennapps.labs.pennmobile.api.NotificationAPI

// Currently only include logic for notifications, would add more network handling afterwards (TBD)

class LoginWebviewViewmodel : ViewModel() {
suspend fun sendToken(
mNotificationAPI: NotificationAPI,
notGuest: Boolean,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can we negate this here. (I know our guest mode stuff is kinda fucked up but notGuest is just an extremely cursed variable name)

bearerToken: String,
notifToken: String,
) {
try {
if (notGuest) {
val response = mNotificationAPI.sendNotificationToken(bearerToken, notifToken)
if (response.isSuccessful) {
Log.i("Notification Token", "Successfully updated token")
} else {
Log.i("Notification Token", "Error updating token: ${response.code()} ${response.message()}")
}
}
} catch (e: Exception) {
e.printStackTrace()
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,21 @@ class MoreFragment : Fragment() {
savedInstanceState: Bundle?,
) {
super.onViewCreated(view, savedInstanceState)
val initials =
PreferenceManager
.getDefaultSharedPreferences(mActivity)
.getString(getString(R.string.initials), null)
if (initials != null && initials.isNotEmpty()) {
binding.initials.text = initials
} else {
binding.profileBackground.setImageDrawable(
ResourcesCompat.getDrawable(
resources,
R.drawable.ic_guest_avatar,
context?.theme,
),
)
}
childFragmentManager
.beginTransaction()
.replace(R.id.more_frame, PreferenceFragment())
Expand Down
Loading
Loading