1
+ package com.googletest.firebase.appdistribution.testapp
2
+
3
+ import android.Manifest.permission.POST_NOTIFICATIONS
4
+ import android.annotation.SuppressLint
5
+ import android.app.*
6
+ import android.content.Context
7
+ import android.content.Intent
8
+ import android.content.pm.PackageManager.PERMISSION_GRANTED
9
+ import android.graphics.Bitmap
10
+ import android.graphics.Canvas
11
+ import android.net.Uri
12
+ import android.os.Build
13
+ import android.os.Bundle
14
+ import android.util.Log
15
+ import androidx.activity.result.ActivityResultCaller
16
+ import androidx.activity.result.ActivityResultLauncher
17
+ import androidx.activity.result.contract.ActivityResultContracts
18
+ import androidx.appcompat.app.AppCompatActivity
19
+ import androidx.core.app.NotificationCompat
20
+ import androidx.core.app.NotificationManagerCompat
21
+ import androidx.core.content.ContextCompat
22
+ import com.google.firebase.appdistribution.ktx.appDistribution
23
+ import com.google.firebase.ktx.Firebase
24
+ import com.googletest.firebase.appdistribution.testapp.NotificationFeedbackTrigger.SCREENSHOT_FILE_NAME
25
+ import com.googletest.firebase.appdistribution.testapp.NotificationFeedbackTrigger.takeScreenshot
26
+ import java.io.IOException
27
+
28
+ @SuppressLint(" StaticFieldLeak" ) // Reference to Activity is set to null in onActivityDestroyed
29
+ object NotificationFeedbackTrigger : Application.ActivityLifecycleCallbacks {
30
+ private const val TAG : String = " NotificationFeedbackTrigger"
31
+ private const val FEEBACK_NOTIFICATION_CHANNEL_ID = " InAppFeedbackNotification"
32
+ private const val FEEDBACK_NOTIFICATION_ID = 1
33
+ const val SCREENSHOT_FILE_NAME =
34
+ " com.googletest.firebase.appdistribution.testapp.screenshot.png"
35
+
36
+ private var isEnabled = false
37
+ private var hasRequestedPermission = false
38
+ private var currentActivity: Activity ? = null
39
+ private var requestPermissionLauncher: ActivityResultLauncher <String >? = null
40
+
41
+ fun initialize (application : Application ) {
42
+ // Create the NotificationChannel, but only on API 26+ because
43
+ // the NotificationChannel class is new and not in the support library
44
+ if (Build .VERSION .SDK_INT >= Build .VERSION_CODES .O ) {
45
+ val channel = NotificationChannel (
46
+ FEEBACK_NOTIFICATION_CHANNEL_ID ,
47
+ application.getString(R .string.feedback_notification_channel_name),
48
+ NotificationManager .IMPORTANCE_HIGH
49
+ )
50
+ channel.description =
51
+ application.getString(R .string.feedback_notification_channel_description)
52
+ application.getSystemService(NotificationManager ::class .java)
53
+ .createNotificationChannel(channel)
54
+ }
55
+ application.registerActivityLifecycleCallbacks(this )
56
+ }
57
+
58
+ fun enable (currentActivity : Activity ? = null) {
59
+ this .currentActivity = currentActivity
60
+ isEnabled = true
61
+ if (currentActivity != null ) {
62
+ showNotification(currentActivity)
63
+ }
64
+ }
65
+
66
+ fun disable () {
67
+ isEnabled = false
68
+ val activity = currentActivity
69
+ currentActivity = null
70
+ if (activity != null ) {
71
+ cancelNotification(activity)
72
+ }
73
+ }
74
+
75
+ private fun showNotification (activity : Activity ) {
76
+ if (ContextCompat .checkSelfPermission(activity, POST_NOTIFICATIONS ) == PERMISSION_GRANTED ) {
77
+ val intent = Intent (activity, TakeScreenshotAndTriggerFeedbackActivity ::class .java)
78
+ intent.addFlags(Intent .FLAG_ACTIVITY_NO_HISTORY )
79
+ val pendingIntent = PendingIntent .getActivity(
80
+ activity, /* requestCode= */ 0 , intent, PendingIntent .FLAG_IMMUTABLE
81
+ )
82
+ val builder = NotificationCompat .Builder (activity, FEEBACK_NOTIFICATION_CHANNEL_ID )
83
+ .setSmallIcon(R .mipmap.ic_launcher)
84
+ .setContentTitle(activity.getText(R .string.feedback_notification_title))
85
+ .setContentText(activity.getText(R .string.feedback_notification_text))
86
+ .setPriority(NotificationCompat .PRIORITY_HIGH )
87
+ .setContentIntent(pendingIntent)
88
+ val notificationManager = NotificationManagerCompat .from(activity)
89
+ Log .i(TAG , " Showing notification" )
90
+ notificationManager.notify(FEEDBACK_NOTIFICATION_ID , builder.build())
91
+ } else {
92
+ // no permission to post notifications - try to get it
93
+ if (hasRequestedPermission) {
94
+ Log .i(
95
+ TAG ,
96
+ " We've already request permission. Not requesting again for the life of the activity."
97
+ )
98
+ } else {
99
+ requestPermission(activity)
100
+ }
101
+ }
102
+ }
103
+
104
+ private fun requestPermission (activity : Activity ) {
105
+ var launcher = requestPermissionLauncher
106
+ if (launcher == null ) {
107
+ Log .i(TAG , " Not requesting permission, because of inability to register for result." )
108
+ } else {
109
+ if (activity.shouldShowRequestPermissionRationale(POST_NOTIFICATIONS )) {
110
+ Log .i(TAG , " Showing customer rationale for requesting permission." )
111
+ AlertDialog .Builder (activity)
112
+ .setMessage(
113
+ " Using a notification to initiate feedback to the developer. "
114
+ + " To enable this feature, allow the app to post notifications."
115
+ )
116
+ .setPositiveButton(" OK" ) { _, _ ->
117
+ Log .i(TAG , " Launching request for permission." )
118
+ launcher.launch(POST_NOTIFICATIONS )
119
+ }
120
+ .show()
121
+ } else {
122
+ Log .i(TAG , " Launching request for permission without rationale." )
123
+ launcher.launch(POST_NOTIFICATIONS )
124
+ }
125
+ hasRequestedPermission = true
126
+ }
127
+ }
128
+
129
+ private fun cancelNotification (context : Context ) {
130
+ val notificationManager = NotificationManagerCompat .from(context)
131
+ Log .i(TAG , " Cancelling notification" )
132
+ notificationManager.cancel(FEEDBACK_NOTIFICATION_ID )
133
+ }
134
+
135
+ override fun onActivityResumed (activity : Activity ) {
136
+ if (isEnabled) {
137
+ if (activity !is TakeScreenshotAndTriggerFeedbackActivity ) {
138
+ Log .d(TAG , " setting current activity" )
139
+ currentActivity = activity
140
+ }
141
+ showNotification(activity)
142
+ }
143
+ }
144
+
145
+ override fun onActivityPaused (activity : Activity ) {
146
+ requestPermissionLauncher = null
147
+ cancelNotification(activity)
148
+ }
149
+
150
+ override fun onActivityDestroyed (activity : Activity ) {
151
+ Log .d(TAG , " clearing current activity" )
152
+ currentActivity = null
153
+ }
154
+
155
+ override fun onActivityCreated (activity : Activity , savedInstanceState : Bundle ? ) {
156
+ if (activity is ActivityResultCaller && ! hasRequestedPermission) {
157
+ requestPermissionLauncher =
158
+ activity.registerForActivityResult(ActivityResultContracts .RequestPermission ()) { isGranted: Boolean ->
159
+ if (! isEnabled) {
160
+ Log .w(
161
+ TAG ,
162
+ " Trigger disabled after permission check. Abandoning notification."
163
+ )
164
+ } else if (isGranted) {
165
+ showNotification(activity)
166
+ } else {
167
+ Log .i(TAG , " Permission not granted" )
168
+ // TODO: Ideally we would show a message indicating the impact of not
169
+ // enabling the permission, but there's no way to know if they've
170
+ // permanently denied the permission, and we don't want to show them a
171
+ // message after each time we try to post a notification.
172
+ }
173
+ }
174
+ } else {
175
+ Log .w(
176
+ TAG ,
177
+ " Not showing notification because this activity can't register for permission request results: $activity "
178
+ )
179
+ }
180
+ }
181
+
182
+ // Other lifecycle methods
183
+ override fun onActivityStarted (activity : Activity ) {}
184
+ override fun onActivityStopped (activity : Activity ) {}
185
+ override fun onActivitySaveInstanceState (activity : Activity , outState : Bundle ) {}
186
+
187
+ fun takeScreenshot () {
188
+ val activity = currentActivity
189
+ if (activity != null ) {
190
+ val view = activity.window.decorView.rootView
191
+ val bitmap = Bitmap .createBitmap(view.width, view.height, Bitmap .Config .RGB_565 )
192
+ val canvas = Canvas (bitmap)
193
+ view.draw(canvas)
194
+ try {
195
+ activity.openFileOutput(SCREENSHOT_FILE_NAME , Context .MODE_PRIVATE )
196
+ .use { outputStream ->
197
+ bitmap.compress(
198
+ Bitmap .CompressFormat .PNG , /* quality = */ 100 , outputStream
199
+ )
200
+ }
201
+ Log .i(TAG , " Wrote screenshot to $SCREENSHOT_FILE_NAME " )
202
+ } catch (e: IOException ) {
203
+ Log .e(TAG , " Can't write $SCREENSHOT_FILE_NAME " , e)
204
+ }
205
+ } else {
206
+ Log .e(TAG , " Can't take screenshot because current activity is unknown" )
207
+ return
208
+ }
209
+ }
210
+ }
211
+
212
+ class TakeScreenshotAndTriggerFeedbackActivity : AppCompatActivity () {
213
+ override fun onCreate (savedInstanceState : Bundle ? ) {
214
+ super .onCreate(savedInstanceState)
215
+ takeScreenshot()
216
+ }
217
+
218
+ override fun onResume () {
219
+ super .onResume()
220
+ val screenshotUri = Uri .fromFile(getFileStreamPath(SCREENSHOT_FILE_NAME ))
221
+ Firebase .appDistribution.startFeedback(R .string.terms_and_conditions, screenshotUri)
222
+ }
223
+ }
0 commit comments