1 package host.exp.exponent.notifications
2 
3 import android.app.NotificationChannel
4 import android.app.NotificationManager
5 import android.app.PendingIntent
6 import android.content.Context
7 import android.content.Intent
8 import android.graphics.Bitmap
9 import android.graphics.Color
10 import android.net.Uri
11 import android.os.Build
12 import android.os.SystemClock
13 import android.text.format.DateUtils
14 import androidx.core.app.NotificationCompat
15 import de.greenrobot.event.EventBus
16 import expo.modules.core.errors.InvalidArgumentException
17 import expo.modules.jsonutils.getNullable
18 import expo.modules.manifests.core.Manifest
19 import host.exp.exponent.Constants
20 import host.exp.exponent.ExponentManifest
21 import host.exp.exponent.ExponentManifest.BitmapListener
22 import host.exp.exponent.analytics.EXL
23 import host.exp.exponent.fcm.FcmRegistrationIntentService
24 import host.exp.exponent.kernel.ExperienceKey
25 import host.exp.exponent.kernel.ExponentUrls
26 import host.exp.exponent.kernel.KernelConstants
27 import host.exp.exponent.network.ExpoHttpCallback
28 import host.exp.exponent.network.ExpoResponse
29 import host.exp.exponent.network.ExponentNetwork
30 import host.exp.exponent.storage.ExponentDB
31 import host.exp.exponent.storage.ExponentDB.ExperienceResultListener
32 import host.exp.exponent.storage.ExponentDBObject
33 import host.exp.exponent.storage.ExponentSharedPreferences
34 import host.exp.exponent.utils.AsyncCondition
35 import host.exp.exponent.utils.AsyncCondition.AsyncConditionListener
36 import host.exp.exponent.utils.ColorParser
37 import host.exp.exponent.utils.JSONUtils.getJSONString
38 import host.exp.expoview.R
39 import okhttp3.MediaType
40 import okhttp3.RequestBody
41 import org.json.JSONException
42 import org.json.JSONObject
43 import java.io.IOException
44 import java.text.DateFormat
45 import java.text.SimpleDateFormat
46 import java.util.*
47 
48 object NotificationHelper {
49   private val TAG = NotificationHelper::class.java.simpleName
50 
51   fun getColor(
52     colorString: String?,
53     manifest: Manifest,
54     exponentManifest: ExponentManifest
55   ): Int {
56     val colorStringLocal = colorString ?: manifest.getNotificationPreferences()?.getNullable(ExponentManifest.MANIFEST_NOTIFICATION_COLOR_KEY)
57     return if (colorString != null && ColorParser.isValid(colorStringLocal)) {
58       Color.parseColor(colorString)
59     } else {
60       exponentManifest.getColorFromManifest(manifest)
61     }
62   }
63 
64   fun loadIcon(
65     url: String?,
66     manifest: Manifest,
67     exponentManifest: ExponentManifest,
68     bitmapListener: BitmapListener?
69   ) {
70     val notificationPreferences = manifest.getNotificationPreferences()
71     var iconUrl: String?
72     if (url == null) {
73       iconUrl = manifest.getIconUrl()
74       if (notificationPreferences != null) {
75         iconUrl = notificationPreferences.getNullable(ExponentManifest.MANIFEST_NOTIFICATION_ICON_URL_KEY)
76       }
77     } else {
78       iconUrl = url
79     }
80 
81     exponentManifest.loadIconBitmap(iconUrl, bitmapListener!!)
82   }
83 
84   @JvmStatic fun getPushNotificationToken(
85     deviceId: String,
86     experienceId: String?,
87     projectId: String?,
88     exponentNetwork: ExponentNetwork,
89     exponentSharedPreferences: ExponentSharedPreferences,
90     listener: TokenListener
91   ) {
92     if (Constants.FCM_ENABLED) {
93       FcmRegistrationIntentService.getTokenAndRegister(exponentSharedPreferences.context)
94     }
95 
96     AsyncCondition.wait(
97       ExponentNotificationIntentService.DEVICE_PUSH_TOKEN_KEY,
98       object : AsyncConditionListener {
99         override fun isReady(): Boolean {
100           return (exponentSharedPreferences.getString(ExponentSharedPreferences.ExponentSharedPreferencesKey.FCM_TOKEN_KEY) != null || ExponentNotificationIntentService.hasTokenError)
101         }
102 
103         override fun execute() {
104           val sharedPreferencesToken = exponentSharedPreferences.getString(ExponentSharedPreferences.ExponentSharedPreferencesKey.FCM_TOKEN_KEY)
105           if (sharedPreferencesToken == null || sharedPreferencesToken.isEmpty()) {
106             var message = "No device token found."
107             if (!Constants.FCM_ENABLED) {
108               message += " You need to enable FCM in order to get a push token. Follow this guide to set up FCM for your standalone app: https://docs.expo.io/versions/latest/guides/using-fcm"
109             }
110             listener.onFailure(Exception(message))
111             return
112           }
113 
114           val params = JSONObject().apply {
115             try {
116               put("deviceId", deviceId)
117               put("appId", exponentSharedPreferences.context.applicationContext.packageName)
118               put("deviceToken", sharedPreferencesToken)
119               put("type", "fcm")
120               put("development", false)
121 
122               when {
123                 projectId !== null -> {
124                   put("projectId", projectId)
125                 }
126                 experienceId !== null -> {
127                   put("experienceId", experienceId)
128                 }
129                 else -> {
130                   listener.onFailure(Exception("Must supply either experienceId or projectId"))
131                   return
132                 }
133               }
134             } catch (e: JSONException) {
135               listener.onFailure(Exception("Error constructing request"))
136               return@execute
137             }
138           }
139 
140           val body = RequestBody.create(MediaType.parse("application/json; charset=utf-8"), params.toString())
141           val request = ExponentUrls.addExponentHeadersToUrl("https://exp.host/--/api/v2/push/getExpoPushToken")
142             .header("Content-Type", "application/json")
143             .post(body)
144             .build()
145           exponentNetwork.client.call(
146             request,
147             object : ExpoHttpCallback {
148               override fun onFailure(e: IOException) {
149                 listener.onFailure(e)
150               }
151 
152               @Throws(IOException::class)
153               override fun onResponse(response: ExpoResponse) {
154                 if (!response.isSuccessful) {
155                   listener.onFailure(Exception("Couldn't get android push token for device"))
156                   return
157                 }
158 
159                 try {
160                   val result = JSONObject(response.body().string())
161                   val data = result.getJSONObject("data")
162                   listener.onSuccess(data.getString("expoPushToken"))
163                 } catch (e: Exception) {
164                   listener.onFailure(e)
165                 }
166               }
167             }
168           )
169         }
170       }
171     )
172   }
173 
174   @JvmStatic fun createChannel(
175     context: Context,
176     experienceKey: ExperienceKey,
177     channelId: String,
178     channelName: String?,
179     details: HashMap<*, *>
180   ) {
181     if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
182       val description: String? = if (details.containsKey(NotificationConstants.NOTIFICATION_CHANNEL_DESCRIPTION)) {
183         details[NotificationConstants.NOTIFICATION_CHANNEL_DESCRIPTION] as String?
184       } else {
185         null
186       }
187       val importance: String? = if (details.containsKey(NotificationConstants.NOTIFICATION_CHANNEL_PRIORITY)) {
188         details[NotificationConstants.NOTIFICATION_CHANNEL_PRIORITY] as String?
189       } else {
190         null
191       }
192       val sound: Boolean? = if (details.containsKey(NotificationConstants.NOTIFICATION_CHANNEL_SOUND)) {
193         details[NotificationConstants.NOTIFICATION_CHANNEL_SOUND] as Boolean?
194       } else {
195         null
196       }
197       val vibrate: Any? = if (details.containsKey(NotificationConstants.NOTIFICATION_CHANNEL_VIBRATE)) {
198         details[NotificationConstants.NOTIFICATION_CHANNEL_VIBRATE]
199       } else {
200         null
201       }
202       val badge: Boolean? = if (details.containsKey(NotificationConstants.NOTIFICATION_CHANNEL_BADGE)) {
203         details[NotificationConstants.NOTIFICATION_CHANNEL_BADGE] as Boolean?
204       } else {
205         null
206       }
207 
208       createChannel(
209         context,
210         experienceKey,
211         channelId,
212         channelName,
213         description,
214         importance,
215         sound,
216         vibrate,
217         badge
218       )
219     } else {
220       // since channels do not exist on Android 7.1 and below, we'll save the settings in shared
221       // preferences and apply them to individual notifications that have this channelId from now on
222       // this is essentially a "polyfill" of notification channels for Android 7.1 and below
223       // and means that devs don't have to worry about supporting both versions of Android at once
224       ExponentNotificationManager(context).saveChannelSettings(experienceKey, channelId, details)
225     }
226   }
227 
228   @JvmStatic fun createChannel(
229     context: Context,
230     experienceKey: ExperienceKey,
231     channelId: String,
232     details: JSONObject
233   ) {
234     try {
235       // we want to throw immediately if there is no channel name
236       val channelName = details.getString(NotificationConstants.NOTIFICATION_CHANNEL_NAME)
237       val description: String? = details.getNullable(NotificationConstants.NOTIFICATION_CHANNEL_DESCRIPTION)
238       val priority: String? = details.getNullable(NotificationConstants.NOTIFICATION_CHANNEL_PRIORITY)
239       val sound: Boolean? = details.getNullable(NotificationConstants.NOTIFICATION_CHANNEL_SOUND)
240       val badge: Boolean? = if (!details.isNull(NotificationConstants.NOTIFICATION_CHANNEL_BADGE)) {
241         details.optBoolean(NotificationConstants.NOTIFICATION_CHANNEL_BADGE, true)
242       } else {
243         null
244       }
245 
246       val vibrateJsonArray = details.optJSONArray(NotificationConstants.NOTIFICATION_CHANNEL_VIBRATE)
247       val vibrate = if (vibrateJsonArray != null) {
248         val vibrateArrayList = ArrayList<Double>()
249         for (i in 0 until vibrateJsonArray.length()) {
250           vibrateArrayList.add(vibrateJsonArray.getDouble(i))
251         }
252         vibrateArrayList
253       } else {
254         details.optBoolean(NotificationConstants.NOTIFICATION_CHANNEL_VIBRATE, false)
255       }
256 
257       createChannel(
258         context,
259         experienceKey,
260         channelId,
261         channelName,
262         description,
263         priority,
264         sound,
265         vibrate,
266         badge
267       )
268     } catch (e: Exception) {
269       EXL.e(TAG, "Could not create channel from stored JSON Object: " + e.message)
270     }
271   }
272 
273   private fun createChannel(
274     context: Context,
275     experienceKey: ExperienceKey,
276     channelId: String,
277     channelName: String?,
278     description: String?,
279     importanceString: String?,
280     sound: Boolean?,
281     vibrate: Any?,
282     badge: Boolean?
283   ) {
284     if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
285       val importance = when (importanceString) {
286         NotificationConstants.NOTIFICATION_CHANNEL_PRIORITY_MAX -> NotificationManager.IMPORTANCE_MAX
287         NotificationConstants.NOTIFICATION_CHANNEL_PRIORITY_HIGH -> NotificationManager.IMPORTANCE_HIGH
288         NotificationConstants.NOTIFICATION_CHANNEL_PRIORITY_LOW -> NotificationManager.IMPORTANCE_LOW
289         NotificationConstants.NOTIFICATION_CHANNEL_PRIORITY_MIN -> NotificationManager.IMPORTANCE_MIN
290         else -> NotificationManager.IMPORTANCE_DEFAULT
291       }
292 
293       val channel = NotificationChannel(
294         ExponentNotificationManager.getScopedChannelId(experienceKey, channelId),
295         channelName,
296         importance
297       )
298 
299       // sound is now on by default for channels
300       if (sound == null || !sound) {
301         channel.setSound(null, null)
302       }
303 
304       if (vibrate != null) {
305         if (vibrate is ArrayList<*>) {
306           val pattern = LongArray(vibrate.size)
307           for (i in vibrate.indices) {
308             pattern[i] = (vibrate[i] as Double).toInt().toLong()
309           }
310           channel.vibrationPattern = pattern
311         } else if (vibrate is Boolean && vibrate) {
312           channel.vibrationPattern = longArrayOf(0, 500)
313         }
314       }
315 
316       if (description != null) {
317         channel.description = description
318       }
319 
320       if (badge != null) {
321         channel.setShowBadge(badge)
322       }
323 
324       ExponentNotificationManager(context).createNotificationChannel(experienceKey, channel)
325     }
326   }
327 
328   @JvmStatic fun maybeCreateLegacyStoredChannel(
329     context: Context,
330     experienceKey: ExperienceKey,
331     channelId: String,
332     details: HashMap<*, *>
333   ) {
334     // no version check here because if we're on Android 7.1 or below, we still want to save
335     // the channel in shared preferences
336     val existingChannel = ExponentNotificationManager(context).getNotificationChannel(experienceKey, channelId)
337     if (existingChannel == null && details.containsKey(NotificationConstants.NOTIFICATION_CHANNEL_NAME)) {
338       createChannel(
339         context,
340         experienceKey,
341         channelId,
342         details[NotificationConstants.NOTIFICATION_CHANNEL_NAME] as String?,
343         details
344       )
345     }
346   }
347 
348   @JvmStatic fun deleteChannel(context: Context?, experienceKey: ExperienceKey?, channelId: String?) {
349     if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
350       ExponentNotificationManager(context!!).deleteNotificationChannel(experienceKey!!, channelId!!)
351     } else {
352       // deleting a channel on O+ still retains all its settings, so doing nothing here emulates that
353     }
354   }
355 
356   @JvmStatic fun showNotification(
357     context: Context,
358     id: Int,
359     details: HashMap<*, *>,
360     exponentManifest: ExponentManifest,
361     listener: Listener
362   ) {
363     val manager = ExponentNotificationManager(context)
364     val notificationScopeKey = details[NotificationConstants.NOTIFICATION_EXPERIENCE_SCOPE_KEY_KEY] as String?
365     val experienceScopeKey = notificationScopeKey ?: (details[NotificationConstants.NOTIFICATION_EXPERIENCE_ID_KEY] as String?)!!
366 
367     ExponentDB.experienceScopeKeyToExperience(
368       experienceScopeKey,
369       object : ExperienceResultListener {
370         override fun onSuccess(exponentDBObject: ExponentDBObject) {
371           Thread(
372             Runnable {
373               val manifest = exponentDBObject.manifest
374               val experienceKey = try {
375                 ExperienceKey.fromManifest(manifest)
376               } catch (e: JSONException) {
377                 listener.onFailure(Exception("Couldn't deserialize JSON for experience scope key $experienceScopeKey"))
378                 return@Runnable
379               }
380 
381               val builder = NotificationCompat.Builder(
382                 context,
383                 ExponentNotificationManager.getScopedChannelId(
384                   experienceKey,
385                   NotificationConstants.NOTIFICATION_DEFAULT_CHANNEL_ID
386                 )
387               ).apply {
388                 setSmallIcon(if (Constants.isStandaloneApp()) R.drawable.shell_notification_icon else R.drawable.notification_icon)
389                 setAutoCancel(true)
390               }
391 
392               val data = details["data"] as HashMap<*, *>
393               if (data.containsKey("channelId")) {
394                 val channelId = data["channelId"] as String
395                 builder.setChannelId(
396                   ExponentNotificationManager.getScopedChannelId(
397                     experienceKey,
398                     channelId
399                   )
400                 )
401 
402                 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
403                   // if we don't yet have a channel matching this ID, check shared preferences --
404                   // it's possible this device has just been upgraded to Android 8+ and the channel
405                   // needs to be created in the system
406                   if (manager.getNotificationChannel(experienceKey, channelId) == null) {
407                     val storedChannelDetails = manager.readChannelSettings(experienceKey, channelId)
408                     if (storedChannelDetails != null) {
409                       createChannel(context, experienceKey, channelId, storedChannelDetails)
410                     }
411                   }
412                 } else {
413                   // on Android 7.1 and below, read channel settings for sound, priority, and vibrate from shared preferences
414                   // and apply these settings to the notification individually, since channels do not exist
415                   val storedChannelDetails = manager.readChannelSettings(experienceKey, channelId)
416                   if (storedChannelDetails != null) {
417                     if (storedChannelDetails.optBoolean(NotificationConstants.NOTIFICATION_CHANNEL_SOUND, false)
418                     ) {
419                       builder.setDefaults(NotificationCompat.DEFAULT_SOUND)
420                     }
421 
422                     builder.priority = when (storedChannelDetails.getNullable<String>(NotificationConstants.NOTIFICATION_CHANNEL_PRIORITY)) {
423                       "max" -> NotificationCompat.PRIORITY_MAX
424                       "high" -> NotificationCompat.PRIORITY_HIGH
425                       "low" -> NotificationCompat.PRIORITY_LOW
426                       "min" -> NotificationCompat.PRIORITY_MIN
427                       else -> NotificationCompat.PRIORITY_DEFAULT
428                     }
429 
430                     try {
431                       val vibrateJsonArray = storedChannelDetails.optJSONArray(NotificationConstants.NOTIFICATION_CHANNEL_VIBRATE)
432                       if (vibrateJsonArray != null) {
433                         val pattern = LongArray(vibrateJsonArray.length())
434                         for (i in 0 until vibrateJsonArray.length()) {
435                           pattern[i] = vibrateJsonArray.getDouble(i).toInt().toLong()
436                         }
437                         builder.setVibrate(pattern)
438                       } else if (storedChannelDetails.optBoolean(NotificationConstants.NOTIFICATION_CHANNEL_VIBRATE, false)) {
439                         builder.setVibrate(longArrayOf(0, 500))
440                       }
441                     } catch (e: Exception) {
442                       EXL.e(
443                         TAG,
444                         "Failed to set vibrate settings on notification from stored channel: " + e.message
445                       )
446                     }
447                   } else {
448                     EXL.e(TAG, "No stored channel found for $experienceScopeKey: $channelId")
449                   }
450                 }
451               } else {
452                 // make a default channel so that people don't have to explicitly create a channel to see notifications
453                 createChannel(
454                   context,
455                   experienceKey,
456                   NotificationConstants.NOTIFICATION_DEFAULT_CHANNEL_ID,
457                   context.getString(R.string.default_notification_channel_group),
458                   HashMap<Any?, Any?>()
459                 )
460               }
461 
462               if (data.containsKey("title")) {
463                 val title = data["title"] as String
464                 builder.setContentTitle(title)
465                 builder.setTicker(title)
466               }
467 
468               if (data.containsKey("body")) {
469                 val body = data["body"] as String
470                 builder.setContentText(body)
471                 builder.setStyle(NotificationCompat.BigTextStyle().bigText(body))
472               }
473 
474               if (data.containsKey("count")) {
475                 builder.setNumber((data["count"] as Double).toInt())
476               }
477 
478               if (data.containsKey("sticky")) {
479                 builder.setOngoing((data["sticky"] as Boolean))
480               }
481 
482               val intent = if (data.containsKey("link")) {
483                 Intent(Intent.ACTION_VIEW, Uri.parse(data["link"] as String))
484               } else {
485                 val activityClass = KernelConstants.MAIN_ACTIVITY_CLASS
486                 Intent(context, activityClass).apply {
487                   putExtra(KernelConstants.NOTIFICATION_MANIFEST_URL_KEY, exponentDBObject.manifestUrl)
488                 }
489               }
490 
491               val body: String = try {
492                 if (data.containsKey("data")) getJSONString(data["data"]!!) else ""
493               } catch (e: JSONException) {
494                 listener.onFailure(Exception("Couldn't deserialize JSON for experience scope key $experienceScopeKey"))
495                 return@Runnable
496               }
497 
498               val notificationEvent = ReceivedNotificationEvent(experienceScopeKey, body, id, isMultiple = false, isRemote = false)
499 
500               intent.putExtra(KernelConstants.NOTIFICATION_KEY, body) // deprecated
501               intent.putExtra(KernelConstants.NOTIFICATION_OBJECT_KEY, notificationEvent.toJSONObject(null).toString())
502 
503               val contentIntent = PendingIntent.getActivity(context, id, intent, PendingIntent.FLAG_UPDATE_CURRENT)
504               builder.setContentIntent(contentIntent)
505 
506               if (data.containsKey("categoryId")) {
507                 val manifestUrl = exponentDBObject.manifestUrl
508 
509                 NotificationActionCenter.setCategory(
510                   data["categoryId"] as String,
511                   builder,
512                   context,
513                   object : IntentProvider {
514                     override fun provide(): Intent {
515                       return Intent(context, KernelConstants.MAIN_ACTIVITY_CLASS).apply {
516                         putExtra(KernelConstants.NOTIFICATION_MANIFEST_URL_KEY, manifestUrl)
517 
518                         val notificationEventInner = ReceivedNotificationEvent(experienceScopeKey, body, id, isMultiple = false, isRemote = false)
519                         putExtra(KernelConstants.NOTIFICATION_KEY, body) // deprecated
520                         putExtra(KernelConstants.NOTIFICATION_OBJECT_KEY, notificationEventInner.toJSONObject(null).toString())
521                       }
522                     }
523                   }
524                 )
525               }
526 
527               builder.color = getColor(
528                 if (data.containsKey("color")) data["color"] as String? else null,
529                 manifest,
530                 exponentManifest
531               )
532 
533               loadIcon(
534                 if (data.containsKey("icon")) data["icon"] as String? else null,
535                 manifest,
536                 exponentManifest,
537                 object : BitmapListener {
538                   override fun onLoadBitmap(bitmap: Bitmap?) {
539                     if (data.containsKey("icon")) {
540                       builder.setLargeIcon(bitmap)
541                     }
542                     manager.notify(experienceKey, id, builder.build())
543                     EventBus.getDefault().post(notificationEvent)
544                     listener.onSuccess(id)
545                   }
546                 }
547               )
548             }
549           ).start()
550         }
551 
552         override fun onFailure() {
553           listener.onFailure(Exception("No experience found or invalid manifest for scope key $experienceScopeKey"))
554         }
555       }
556     )
557   }
558 
559   @JvmStatic fun scheduleLocalNotification(
560     context: Context?,
561     id: Int,
562     data: HashMap<String?, Any?>,
563     options: HashMap<*, *>,
564     experienceKey: ExperienceKey,
565     listener: Listener
566   ) {
567     val details = hashMapOf(
568       "data" to data,
569       NotificationConstants.NOTIFICATION_EXPERIENCE_ID_KEY to experienceKey.scopeKey,
570       NotificationConstants.NOTIFICATION_EXPERIENCE_SCOPE_KEY_KEY to experienceKey.scopeKey
571     )
572 
573     var time: Long = 0
574 
575     if (options.containsKey("time")) {
576       try {
577         when (val suppliedTime = options["time"]) {
578           is Number -> time = suppliedTime.toLong() - System.currentTimeMillis()
579           is String -> { // TODO: DELETE WHEN SDK 32 IS DEPRECATED
580             val format: DateFormat = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'")
581             format.timeZone = TimeZone.getTimeZone("UTC")
582             time = format.parse(suppliedTime as String?).time - System.currentTimeMillis()
583           }
584           else -> throw InvalidArgumentException("Invalid time provided: $suppliedTime")
585         }
586       } catch (e: Exception) {
587         listener.onFailure(e)
588         return
589       }
590     }
591 
592     time += SystemClock.elapsedRealtime()
593 
594     val manager = ExponentNotificationManager(context!!)
595 
596     val interval = when {
597       options.containsKey("repeat") -> {
598         when (options["repeat"] as String?) {
599           "minute" -> DateUtils.MINUTE_IN_MILLIS
600           "hour" -> DateUtils.HOUR_IN_MILLIS
601           "day" -> DateUtils.DAY_IN_MILLIS
602           "week" -> DateUtils.WEEK_IN_MILLIS
603           "month" -> DateUtils.DAY_IN_MILLIS * 30
604           "year" -> DateUtils.DAY_IN_MILLIS * 365
605           else -> {
606             listener.onFailure(Exception("Invalid repeat interval specified"))
607             return
608           }
609         }
610       }
611       options.containsKey("intervalMs") -> options["intervalMs"] as Long?
612       else -> null
613     }
614 
615     try {
616       manager.schedule(experienceKey, id, details, time, interval)
617       listener.onSuccess(id)
618     } catch (e: Exception) {
619       listener.onFailure(e)
620     }
621   }
622 
623   interface Listener {
624     fun onSuccess(id: Int)
625     fun onFailure(e: Exception)
626   }
627 
628   interface TokenListener {
629     fun onSuccess(token: String)
630     fun onFailure(e: Exception)
631   }
632 }
633