Skip to content

Commit ae522d7

Browse files
committed
Ask for notification permission upfront. Keep one notification instead of cancelling and showing a new one as Activities pause/resume.
1 parent 94fb7e4 commit ae522d7

File tree

3 files changed

+87
-80
lines changed

3 files changed

+87
-80
lines changed

firebase-appdistribution-api/src/main/java/com/google/firebase/appdistribution/FirebaseAppDistribution.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -177,7 +177,7 @@ public interface FirebaseAppDistribution {
177177
* @param screenshot URI to a bitmap containing a screenshot that will be included with the
178178
* report, or null to not include a screenshot
179179
*/
180-
void startFeedback(@NonNull CharSequence infoText, @Nullable Uri screenshotUri);
180+
void startFeedback(@NonNull CharSequence infoText, @Nullable Uri screenshot);
181181

182182
/** Gets the singleton {@link FirebaseAppDistribution} instance. */
183183
@NonNull

firebase-appdistribution/test-app/src/main/java/com/googletest/firebase/appdistribution/testapp/MainActivity.kt

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@ import android.widget.ArrayAdapter
1313
import android.widget.AutoCompleteTextView
1414
import android.widget.ProgressBar
1515
import android.widget.TextView
16-
import androidx.activity.result.ActivityResultLauncher
1716
import androidx.appcompat.app.AppCompatActivity
1817
import androidx.appcompat.widget.AppCompatButton
1918
import androidx.core.widget.doOnTextChanged
@@ -47,7 +46,6 @@ class MainActivity : AppCompatActivity() {
4746
lateinit var feedbackTriggerMenu: TextInputLayout
4847

4948
var updateTask: Task<Void>? = null
50-
var notificationPermissionLauncher: ActivityResultLauncher<String>? = null
5149

5250
override fun onCreate(savedInstanceState: Bundle?) {
5351
super.onCreate(savedInstanceState)
@@ -67,7 +65,10 @@ class MainActivity : AppCompatActivity() {
6765
progressPercent = findViewById(R.id.progress_percentage)
6866
signInStatus = findViewById(R.id.sign_in_status)
6967
progressBar = findViewById(R.id.progress_bar)
70-
notificationPermissionLauncher = NotificationFeedbackTrigger.registerPermissionLauncher(this)
68+
69+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
70+
NotificationFeedbackTrigger.requestPermission(this)
71+
}
7172

7273
// Set up feedback trigger menu
7374
feedbackTriggerMenu = findViewById(R.id.feedbackTriggerMenu)
@@ -100,7 +101,7 @@ class MainActivity : AppCompatActivity() {
100101
FeedbackTrigger.NOTIFICATION.label -> {
101102
disableAllFeedbackTriggers()
102103
Log.i(TAG, "Enabling notification trigger")
103-
NotificationFeedbackTrigger.enable(this, notificationPermissionLauncher)
104+
NotificationFeedbackTrigger.enable(this)
104105
}
105106
}
106107
}

firebase-appdistribution/test-app/src/main/java/com/googletest/firebase/appdistribution/testapp/NotificationFeedbackTrigger.kt

Lines changed: 81 additions & 75 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,8 @@ import android.os.Build
1313
import android.os.Bundle
1414
import android.util.Log
1515
import androidx.activity.result.ActivityResultCaller
16-
import androidx.activity.result.ActivityResultLauncher
1716
import androidx.activity.result.contract.ActivityResultContracts
17+
import androidx.annotation.RequiresApi
1818
import androidx.core.app.NotificationCompat
1919
import androidx.core.app.NotificationManagerCompat
2020
import androidx.core.content.ContextCompat
@@ -33,8 +33,16 @@ object NotificationFeedbackTrigger : Application.ActivityLifecycleCallbacks {
3333

3434
private var isEnabled = false
3535
private var hasRequestedPermission = false
36-
private var currentActivity: Activity? = null
37-
36+
private var currentActivity: Activity? = null // Activity to be used for screenshot
37+
38+
/**
39+
* Initialize the notification trigger for this application.
40+
*
41+
* This should be called during [Application.onCreate].
42+
* [enable] should then be called when you want to actually show the notification.
43+
*
44+
* @param application the [Application] object
45+
*/
3846
fun initialize(application: Application) {
3947
// Create the NotificationChannel, but only on API 26+ because
4048
// the NotificationChannel class is new and not in the support library
@@ -43,7 +51,7 @@ object NotificationFeedbackTrigger : Application.ActivityLifecycleCallbacks {
4351
NotificationChannel(
4452
FEEBACK_NOTIFICATION_CHANNEL_ID,
4553
application.getString(R.string.feedback_notification_channel_name),
46-
NotificationManager.IMPORTANCE_LOW
54+
NotificationManager.IMPORTANCE_HIGH
4755
)
4856
channel.description =
4957
application.getString(R.string.feedback_notification_channel_description)
@@ -54,15 +62,27 @@ object NotificationFeedbackTrigger : Application.ActivityLifecycleCallbacks {
5462
application.registerActivityLifecycleCallbacks(this)
5563
}
5664

57-
fun <T> registerPermissionLauncher(activity: T): ActivityResultLauncher<String>? where
58-
T : Activity,
59-
T : ActivityResultCaller {
65+
/**
66+
* Requests permission to show notifications for this application.
67+
*
68+
* This must be called during [Activity.onCreate].
69+
* [enable] should then be called when you want to actually show the notification.
70+
*
71+
* @param activity the [Activity] object
72+
*/
73+
@RequiresApi(Build.VERSION_CODES.TIRAMISU)
74+
fun <T> requestPermission(activity: T) where T : Activity, T : ActivityResultCaller {
75+
if (ContextCompat.checkSelfPermission(activity, POST_NOTIFICATIONS) == PERMISSION_GRANTED) {
76+
Log.i(TAG, "Already has permission.")
77+
return
78+
}
79+
6080
if (hasRequestedPermission) {
6181
Log.i(TAG, "Already request permission; Not trying again.")
62-
return null
82+
return
6383
}
6484

65-
return activity.registerForActivityResult(ActivityResultContracts.RequestPermission()) {
85+
val launcher = activity.registerForActivityResult(ActivityResultContracts.RequestPermission()) {
6686
isGranted: Boolean ->
6787
if (!isEnabled) {
6888
Log.w(TAG, "Trigger disabled after permission check. Abandoning notification.")
@@ -76,81 +96,71 @@ object NotificationFeedbackTrigger : Application.ActivityLifecycleCallbacks {
7696
// message after each time we try to post a notification.
7797
}
7898
}
99+
100+
if (activity.shouldShowRequestPermissionRationale(POST_NOTIFICATIONS)) {
101+
Log.i(TAG, "Showing customer rationale for requesting permission.")
102+
AlertDialog.Builder(activity)
103+
.setMessage(
104+
"Using a notification to initiate feedback to the developer. " +
105+
"To enable this feature, allow the app to post notifications."
106+
)
107+
.setPositiveButton("OK") { _, _ ->
108+
Log.i(TAG, "Launching request for permission.")
109+
launcher.launch(POST_NOTIFICATIONS)
110+
}
111+
.show()
112+
} else {
113+
Log.i(TAG, "Launching request for permission without rationale.")
114+
launcher.launch(POST_NOTIFICATIONS)
115+
}
116+
hasRequestedPermission = true
79117
}
80118

81-
fun enable(activity: Activity? = null, launcher: ActivityResultLauncher<String>? = null) {
119+
/**
120+
* Show notifications.
121+
*
122+
* This could be called during [Activity.onCreate].
123+
*
124+
* @param activity the [Activity] object
125+
*/
126+
fun enable(activity: Activity) {
82127
currentActivity = activity
83128
isEnabled = true
84-
if (activity != null) {
85-
if (hasPermission(activity)) {
86-
showNotification(activity)
87-
} else {
88-
if (launcher != null) {
89-
requestPermission(activity, launcher)
90-
} else {
91-
Log.i(TAG, "Not requesting permission, because of no launcher was provided.")
92-
}
93-
}
94-
}
129+
showNotification(activity)
95130
}
96131

132+
/** Hide notifications. */
97133
fun disable() {
98-
isEnabled = false
99134
val activity = currentActivity
100-
currentActivity = null
101135
if (activity != null) {
102136
cancelNotification(activity)
103137
}
138+
isEnabled = false
139+
currentActivity = null
104140
}
105141

106-
private fun showNotification(activity: Activity) {
107-
val intent = Intent(activity, TakeScreenshotAndTriggerFeedbackActivity::class.java)
142+
private fun showNotification(context: Context) {
143+
val intent = Intent(context, TakeScreenshotAndTriggerFeedbackActivity::class.java)
108144
intent.addFlags(Intent.FLAG_ACTIVITY_NO_HISTORY)
109145
val pendingIntent =
110146
PendingIntent.getActivity(
111-
activity,
147+
context,
112148
/* requestCode = */ 0,
113149
intent,
114150
PendingIntent.FLAG_IMMUTABLE
115151
)
116152
val builder =
117-
NotificationCompat.Builder(activity, FEEBACK_NOTIFICATION_CHANNEL_ID)
153+
NotificationCompat.Builder(context, FEEBACK_NOTIFICATION_CHANNEL_ID)
118154
.setSmallIcon(R.mipmap.ic_launcher)
119-
.setContentTitle(activity.getText(R.string.feedback_notification_title))
120-
.setContentText(activity.getText(R.string.feedback_notification_text))
121-
.setPriority(NotificationCompat.PRIORITY_LOW)
155+
.setContentTitle(context.getText(R.string.feedback_notification_title))
156+
.setContentText(context.getText(R.string.feedback_notification_text))
157+
.setPriority(NotificationCompat.PRIORITY_HIGH)
122158
.setContentIntent(pendingIntent)
123-
val notificationManager = NotificationManagerCompat.from(activity)
159+
val notificationManager = NotificationManagerCompat.from(context)
124160
Log.i(TAG, "Showing notification")
125161
notificationManager.notify(FEEDBACK_NOTIFICATION_ID, builder.build())
126162
}
127163

128-
private fun hasPermission(activity: Activity) =
129-
ContextCompat.checkSelfPermission(activity, POST_NOTIFICATIONS) == PERMISSION_GRANTED
130-
131-
private fun requestPermission(activity: Activity, launcher: ActivityResultLauncher<String>) {
132-
if (hasRequestedPermission) {
133-
Log.i(TAG, "Already request permission; Not trying again.")
134-
return
135-
}
136-
if (activity.shouldShowRequestPermissionRationale(POST_NOTIFICATIONS)) {
137-
Log.i(TAG, "Showing customer rationale for requesting permission.")
138-
AlertDialog.Builder(activity)
139-
.setMessage(
140-
"Using a notification to initiate feedback to the developer. " +
141-
"To enable this feature, allow the app to post notifications."
142-
)
143-
.setPositiveButton("OK") { _, _ ->
144-
Log.i(TAG, "Launching request for permission.")
145-
launcher.launch(POST_NOTIFICATIONS)
146-
}
147-
.show()
148-
} else {
149-
Log.i(TAG, "Launching request for permission without rationale.")
150-
launcher.launch(POST_NOTIFICATIONS)
151-
}
152-
hasRequestedPermission = true
153-
}
154164

155165
private fun cancelNotification(context: Context) {
156166
val notificationManager = NotificationManagerCompat.from(context)
@@ -163,14 +173,10 @@ object NotificationFeedbackTrigger : Application.ActivityLifecycleCallbacks {
163173
if (activity !is TakeScreenshotAndTriggerFeedbackActivity) {
164174
Log.d(TAG, "setting current activity")
165175
currentActivity = activity
166-
showNotification(activity)
167176
}
168177
}
169178
}
170179

171-
override fun onActivityPaused(activity: Activity) {
172-
cancelNotification(activity)
173-
}
174180

175181
override fun onActivityDestroyed(activity: Activity) {
176182
if (activity == currentActivity) {
@@ -182,28 +188,28 @@ object NotificationFeedbackTrigger : Application.ActivityLifecycleCallbacks {
182188
// Other lifecycle methods
183189
override fun onActivityCreated(activity: Activity, savedInstanceState: Bundle?) {}
184190
override fun onActivityStarted(activity: Activity) {}
191+
override fun onActivityPaused(activity: Activity) {}
185192
override fun onActivityStopped(activity: Activity) {}
186193
override fun onActivitySaveInstanceState(activity: Activity, outState: Bundle) {}
187194

188195
fun takeScreenshot() {
189196
val activity = currentActivity
190-
if (activity != null) {
191-
val view = activity.window.decorView.rootView
192-
val bitmap = Bitmap.createBitmap(view.width, view.height, Bitmap.Config.RGB_565)
193-
val canvas = Canvas(bitmap)
194-
view.draw(canvas)
195-
try {
196-
activity.openFileOutput(SCREENSHOT_FILE_NAME, Context.MODE_PRIVATE).use { outputStream ->
197-
bitmap.compress(Bitmap.CompressFormat.PNG, /* quality = */ 100, outputStream)
198-
}
199-
Log.i(TAG, "Wrote screenshot to $SCREENSHOT_FILE_NAME")
200-
} catch (e: IOException) {
201-
Log.e(TAG, "Can't write $SCREENSHOT_FILE_NAME", e)
202-
}
203-
} else {
197+
if (activity == null) {
204198
Log.e(TAG, "Can't take screenshot because current activity is unknown")
205199
return
206200
}
201+
val view = activity.window.decorView.rootView
202+
val bitmap = Bitmap.createBitmap(view.width, view.height, Bitmap.Config.RGB_565)
203+
val canvas = Canvas(bitmap)
204+
view.draw(canvas)
205+
try {
206+
activity.openFileOutput(SCREENSHOT_FILE_NAME, Context.MODE_PRIVATE).use { outputStream ->
207+
bitmap.compress(Bitmap.CompressFormat.PNG, /* quality = */ 100, outputStream)
208+
}
209+
Log.i(TAG, "Wrote screenshot to $SCREENSHOT_FILE_NAME")
210+
} catch (e: IOException) {
211+
Log.e(TAG, "Can't write $SCREENSHOT_FILE_NAME", e)
212+
}
207213
}
208214
}
209215

0 commit comments

Comments
 (0)