Skip to content

Simplify custom notification trigger example and move to activity #4255

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

Merged
merged 2 commits into from
Oct 31, 2022
Merged
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
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
</activity>
<activity android:name="com.googletest.firebase.appdistribution.testapp.SecondActivity" />
<activity android:name="com.googletest.firebase.appdistribution.testapp.ScreenshotDetectionActivity" />
<activity android:name="com.googletest.firebase.appdistribution.testapp.TakeScreenshotAndTriggerFeedbackActivity" />
<activity android:name="com.googletest.firebase.appdistribution.testapp.CustomNotificationActivity" />
<activity android:name="com.googletest.firebase.appdistribution.testapp.CustomNotificationTakeScreenshotActivity" />
</application>
</manifest>
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,7 @@ class AppDistroTestApplication : Application() {
override fun onCreate() {
super.onCreate()

// Perform any required trigger initialization here
CustomNotificationFeedbackTrigger.initialize(this);

// Default feedback triggers can optionally be enabled application-wide here
// The shake detection feedback trigger can optionally be enabled application-wide here
// ShakeDetectionFeedbackTrigger.enable(this)
// ScreenshotDetectionFeedbackTrigger.enable()
// NotificationFeedbackTrigger.enable()
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
package com.googletest.firebase.appdistribution.testapp

import android.Manifest.permission.POST_NOTIFICATIONS
import android.app.AlertDialog
import android.content.pm.PackageManager.PERMISSION_DENIED
import android.os.Bundle
import android.util.Log
import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.app.AppCompatActivity
import androidx.core.content.ContextCompat
import java.util.*

class CustomNotificationActivity : AppCompatActivity() {

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_custom_notification)

val launcher =
registerForActivityResult(ActivityResultContracts.RequestPermission()) { isGranted: Boolean ->
if (isGranted) {
Log.i(TAG, "Permission granted, showing notification")
CustomNotificationFeedbackTrigger.showNotification(this)
} else {
Log.i(TAG, "Permission not granted")
AlertDialog.Builder(this)
.setMessage(
"Because the notification permission has been denied, the app will not show a " +
"notification that can be tapped to send feedback to the developer."
)
.setPositiveButton("OK") { _, _ -> }
.show()
}
}

if (ContextCompat.checkSelfPermission(this, POST_NOTIFICATIONS) == PERMISSION_DENIED) {
if (shouldShowRequestPermissionRationale(POST_NOTIFICATIONS)) {
Log.i(TAG, "Showing customer rationale for requesting permission.")
AlertDialog.Builder(this)
.setMessage(
"Using a notification to initiate feedback to the developer. " +
"To enable this feature, allow the app to post notifications."
)
.setPositiveButton("OK") { _, _ ->
Log.i(TAG, "Launching request for permission.")
launcher.launch(POST_NOTIFICATIONS)
}
.setNegativeButton("No thanks") { _, _ -> Log.i(TAG, "User denied permission request.") }
.show()
} else {
Log.i(TAG, "Launching request for permission without rationale.")
launcher.launch(POST_NOTIFICATIONS)
}
} else {
CustomNotificationFeedbackTrigger.showNotification(this)
}
}

override fun onDestroy() {
CustomNotificationFeedbackTrigger.cancelNotification()
super.onDestroy()
}

companion object {
private const val TAG = "CustomNotificationActivity"
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,196 +6,99 @@ import android.app.*
import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager.PERMISSION_DENIED
import android.content.pm.PackageManager.PERMISSION_GRANTED
import android.graphics.Bitmap
import android.graphics.Canvas
import android.net.Uri
import android.os.Build
import android.os.Bundle
import android.util.Log
import androidx.activity.result.ActivityResultCaller
import androidx.activity.result.contract.ActivityResultContracts
import androidx.annotation.RequiresApi
import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat
import androidx.core.content.ContextCompat
import com.google.firebase.appdistribution.ktx.appDistribution
import com.google.firebase.ktx.Firebase
import java.io.IOException

/**
* Shows an ongoing notification that the user can tap to take a screenshot and send feedback to the
* developer.
*/
@SuppressLint("StaticFieldLeak") // Reference to Activity is set to null in onActivityDestroyed
object CustomNotificationFeedbackTrigger : Application.ActivityLifecycleCallbacks {
private const val TAG: String = "NotificationFeedbackTrigger"
private const val FEEDBACK_NOTIFICATION_CHANNEL_ID = "InAppFeedbackNotification"
object CustomNotificationFeedbackTrigger {
private const val TAG: String = "CustomNotificationFeedbackTrigger"
private const val FEEDBACK_NOTIFICATION_CHANNEL_ID = "CustomNotificationFeedbackTrigger"
private const val FEEDBACK_NOTIFICATION_ID = 1

private var isEnabled = false
private var hasRequestedPermission = false
var activityToScreenshot: Activity? = null

internal var activityToScreenshot: Activity? = null

/**
* Initialize the notification trigger for this application.
* Show an ongoing notification that the user can tap to take a screenshot of the current activity
* and send feedback to the developer.
*
* This should be called during [Application.onCreate].
* [enable] should then be called when you want to actually show the notification.
* The passed in activity must call [cancelNotification] in its [Activity.onDestroy].
*
* @param application the [Application] object
* @param activity the current activity, which will be captured by the screenshot
*/
fun initialize(application: Application) {
fun showNotification(activity: Activity) {
if (ContextCompat.checkSelfPermission(activity, POST_NOTIFICATIONS) == PERMISSION_DENIED) {
Log.w(TAG, "Not showing notification because permission has not been granted.")
return
}

// Create the NotificationChannel, but only on API 26+ because
// the NotificationChannel class is new and not in the support library
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val channel =
NotificationChannel(
FEEDBACK_NOTIFICATION_CHANNEL_ID,
application.getString(R.string.feedbackTriggerNotificationChannelName),
activity.getString(R.string.feedbackTriggerNotificationChannelName),
NotificationManager.IMPORTANCE_HIGH
)
channel.description =
application.getString(R.string.feedbackTriggerNotificationChannelDescription)
application
activity.getString(R.string.feedbackTriggerNotificationChannelDescription)
activity
.getSystemService(NotificationManager::class.java)
.createNotificationChannel(channel)
}
application.registerActivityLifecycleCallbacks(this)
}

/**
* Requests permission to show notifications for this application.
*
* This must be called during [Activity.onCreate].
* [enable] should then be called when you want to actually show the notification.
*
* @param activity the [Activity] object
*/
@RequiresApi(Build.VERSION_CODES.TIRAMISU)
fun <T> requestPermission(activity: T) where T : Activity, T : ActivityResultCaller {
if (ContextCompat.checkSelfPermission(activity, POST_NOTIFICATIONS) == PERMISSION_GRANTED) {
Log.i(TAG, "Already has permission.")
return
}

if (hasRequestedPermission) {
Log.i(TAG, "Already request permission; Not trying again.")
return
}

val launcher = activity.registerForActivityResult(ActivityResultContracts.RequestPermission()) {
isGranted: Boolean ->
if (!isEnabled) {
Log.w(TAG, "Trigger disabled after permission check. Abandoning notification.")
} else if (isGranted) {
showNotification(activity)
} else {
Log.i(TAG, "Permission not granted")
// TODO: Ideally we would show a message indicating the impact of not
// enabling the permission, but there's no way to know if they've
// permanently denied the permission, and we don't want to show them a
// message after each time we try to post a notification.
}
}

if (activity.shouldShowRequestPermissionRationale(POST_NOTIFICATIONS)) {
Log.i(TAG, "Showing customer rationale for requesting permission.")
AlertDialog.Builder(activity)
.setMessage(
"Using a notification to initiate feedback to the developer. " +
"To enable this feature, allow the app to post notifications."
)
.setPositiveButton("OK") { _, _ ->
Log.i(TAG, "Launching request for permission.")
launcher.launch(POST_NOTIFICATIONS)
}
.show()
} else {
Log.i(TAG, "Launching request for permission without rationale.")
launcher.launch(POST_NOTIFICATIONS)
}
hasRequestedPermission = true
}

/**
* Show notifications.
*
* This could be called during [Activity.onCreate].
*
* @param activity the [Activity] object
*/
fun enable(activity: Activity) {
activityToScreenshot = activity
isEnabled = true
showNotification(activity)
}

/** Hide notifications. */
fun disable() {
val activity = activityToScreenshot
if (activity != null) {
cancelNotification(activity)
}
isEnabled = false
activityToScreenshot = null
}

private fun showNotification(context: Context) {
if (ContextCompat.checkSelfPermission(context, POST_NOTIFICATIONS) == PERMISSION_DENIED) {
Log.w(TAG, "Not showing notification because permission has not been granted.")
return
}

val intent = Intent(context, TakeScreenshotAndTriggerFeedbackActivity::class.java)
val intent = Intent(activity, CustomNotificationTakeScreenshotActivity::class.java)
intent.addFlags(Intent.FLAG_ACTIVITY_NO_HISTORY)
val pendingIntent =
PendingIntent.getActivity(
context,
activity,
/* requestCode = */ 0,
intent,
PendingIntent.FLAG_IMMUTABLE
)
val builder =
NotificationCompat.Builder(context, FEEDBACK_NOTIFICATION_CHANNEL_ID)
NotificationCompat.Builder(activity, FEEDBACK_NOTIFICATION_CHANNEL_ID)
.setSmallIcon(R.mipmap.ic_launcher)
.setContentTitle(context.getText(R.string.feedbackTriggerNotificationTitle))
.setContentText(context.getText(R.string.feedbackTriggerNotificationText))
.setContentTitle(activity.getText(R.string.feedbackTriggerNotificationTitle))
.setContentText(activity.getText(R.string.feedbackTriggerNotificationText))
.setPriority(NotificationCompat.PRIORITY_HIGH)
.setContentIntent(pendingIntent)
val notificationManager = NotificationManagerCompat.from(context)
.setOngoing(true)
val notificationManager = NotificationManagerCompat.from(activity)
Log.i(TAG, "Showing notification")
notificationManager.notify(FEEDBACK_NOTIFICATION_ID, builder.build())
activityToScreenshot = activity
}

private fun cancelNotification(context: Context) {
val notificationManager = NotificationManagerCompat.from(context)
Log.i(TAG, "Cancelling notification")
notificationManager.cancel(FEEDBACK_NOTIFICATION_ID)
}

override fun onActivityResumed(activity: Activity) {
if (isEnabled) {
if (activity !is TakeScreenshotAndTriggerFeedbackActivity) {
Log.d(TAG, "setting current activity")
activityToScreenshot = activity
}
}
}

override fun onActivityDestroyed(activity: Activity) {
if (activity == activityToScreenshot) {
Log.d(TAG, "clearing current activity")
activityToScreenshot = null
/**
* Hide the notification.
*
* This must be called from the [Activity.onDestroy] of the activity showing the notification.
*/
fun cancelNotification() {
activityToScreenshot?.let {
Log.i(TAG, "Cancelling notification")
NotificationManagerCompat.from(it).cancel(FEEDBACK_NOTIFICATION_ID)
}
}

// Other lifecycle methods
override fun onActivityCreated(activity: Activity, savedInstanceState: Bundle?) {}
override fun onActivityStarted(activity: Activity) {}
override fun onActivityPaused(activity: Activity) {}
override fun onActivityStopped(activity: Activity) {}
override fun onActivitySaveInstanceState(activity: Activity, outState: Bundle) {}
}

class TakeScreenshotAndTriggerFeedbackActivity : Activity() {
class CustomNotificationTakeScreenshotActivity : Activity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val activity = CustomNotificationFeedbackTrigger.activityToScreenshot
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -66,10 +66,6 @@ class MainActivity : AppCompatActivity() {
signInStatus = findViewById(R.id.sign_in_status)
progressBar = findViewById(R.id.progress_bar)

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
CustomNotificationFeedbackTrigger.requestPermission(this)
}

// Set up feedback trigger menu
feedbackTriggerMenu = findViewById(R.id.feedbackTriggerMenu)
val items = listOf(
Expand Down Expand Up @@ -97,8 +93,7 @@ class MainActivity : AppCompatActivity() {
}
FeedbackTrigger.CUSTOM_NOTIFICATION.label -> {
disableAllFeedbackTriggers()
Log.i(TAG, "Enabling notification trigger (custom)")
CustomNotificationFeedbackTrigger.enable(this)
startActivity(Intent(this, CustomNotificationActivity::class.java))
}
FeedbackTrigger.SHAKE.label -> {
disableAllFeedbackTriggers()
Expand All @@ -108,9 +103,6 @@ class MainActivity : AppCompatActivity() {
FeedbackTrigger.SCREENSHOT.label -> {
disableAllFeedbackTriggers()
startActivity(Intent(this, ScreenshotDetectionActivity::class.java))
// Set the selection back to None since once we're back to this activity the
// trigger will be disabled
autoCompleteTextView.setText(FeedbackTrigger.NONE.label, false)
}
}
}
Expand All @@ -124,7 +116,6 @@ class MainActivity : AppCompatActivity() {
private fun disableAllFeedbackTriggers() {
Log.i(TAG, "Disabling all feedback triggers")
firebaseAppDistribution.cancelFeedbackNotification()
CustomNotificationFeedbackTrigger.disable()
ShakeDetectionFeedbackTrigger.disable(application)
}

Expand Down
Loading