Moved logic for adding new contact to service.
This means the activity doesn't have to be held open.
This commit is contained in:
parent
c670588f98
commit
60a8dd59de
9 changed files with 252 additions and 163 deletions
|
@ -0,0 +1,55 @@
|
|||
package com.nutomic.ensichat.util
|
||||
|
||||
import android.content.Context
|
||||
import android.test.AndroidTestCase
|
||||
import com.nutomic.ensichat.protocol.messages._
|
||||
import com.nutomic.ensichat.protocol.{Address, Crypto, UserTest}
|
||||
import junit.framework.Assert._
|
||||
|
||||
class AddContactsHandlerTest extends AndroidTestCase {
|
||||
|
||||
private class MockContext(context: Context) extends DatabaseTest.MockContext(context) {
|
||||
override def getResources = context.getResources
|
||||
override def getSystemService(name: String) = context.getSystemService(name)
|
||||
override def getPackageName = context.getPackageName
|
||||
}
|
||||
|
||||
private lazy val handler =
|
||||
new AddContactsHandler(context, (address: Address) => UserTest.u1, UserTest.u1.address)
|
||||
|
||||
private lazy val context = new MockContext(getContext)
|
||||
|
||||
private lazy val database = new Database(context)
|
||||
|
||||
private lazy val crypto = new Crypto(getContext)
|
||||
|
||||
private lazy val header1 =
|
||||
new MessageHeader(RequestAddContact.Type, 0, UserTest.u1.address, crypto.localAddress, 0)
|
||||
private lazy val header2 =
|
||||
new MessageHeader(ResultAddContact.Type, 0, UserTest.u1.address, crypto.localAddress, 0)
|
||||
private lazy val header3 =
|
||||
new MessageHeader(ResultAddContact.Type, 0, crypto.localAddress, UserTest.u1.address, 0)
|
||||
|
||||
override def tearDown(): Unit = {
|
||||
super.tearDown()
|
||||
database.close()
|
||||
context.deleteDbFile()
|
||||
}
|
||||
|
||||
def testAddContact(): Unit = {
|
||||
assertFalse(database.getContact(UserTest.u1.address).isDefined)
|
||||
handler.onMessageReceived(new Message(header1, new RequestAddContact))
|
||||
handler.onMessageReceived(new Message(header2, new ResultAddContact(true)))
|
||||
handler.onMessageReceived(new Message(header3, new ResultAddContact(true)))
|
||||
assertTrue(database.getContact(UserTest.u1.address).isDefined)
|
||||
}
|
||||
|
||||
def testAddContactDenied(): Unit = {
|
||||
assertFalse(database.getContact(UserTest.u1.address).isDefined)
|
||||
handler.onMessageReceived(new Message(header1, new RequestAddContact))
|
||||
handler.onMessageReceived(new Message(header2, new ResultAddContact(true)))
|
||||
handler.onMessageReceived(new Message(header3, new ResultAddContact(false)))
|
||||
assertFalse(database.getContact(UserTest.u1.address).isDefined)
|
||||
}
|
||||
|
||||
}
|
|
@ -6,28 +6,38 @@ import android.content.Context
|
|||
import android.database.DatabaseErrorHandler
|
||||
import android.database.sqlite.SQLiteDatabase
|
||||
import android.test.AndroidTestCase
|
||||
import android.test.mock.MockContext
|
||||
import android.test.mock
|
||||
import com.nutomic.ensichat.protocol.UserTest
|
||||
import com.nutomic.ensichat.protocol.messages.MessageTest._
|
||||
import com.nutomic.ensichat.util.Database.OnContactsUpdatedListener
|
||||
import junit.framework.Assert._
|
||||
|
||||
class DatabaseTest extends AndroidTestCase {
|
||||
object DatabaseTest {
|
||||
|
||||
private class TestContext(context: Context) extends MockContext {
|
||||
/**
|
||||
* Provides a temporary database file that can be deleted with [[MockContext#deleteDbFile]].
|
||||
*
|
||||
* Does not work if multiple db files are opened!
|
||||
*/
|
||||
class MockContext(context: Context) extends mock.MockContext {
|
||||
private var dbFile: String = _
|
||||
override def openOrCreateDatabase(file: String, mode: Int, factory:
|
||||
SQLiteDatabase.CursorFactory, errorHandler: DatabaseErrorHandler): SQLiteDatabase = {
|
||||
dbFile = file + "-test"
|
||||
context.openOrCreateDatabase(dbFile, mode, factory, errorHandler)
|
||||
}
|
||||
def deleteDbFile() = context.deleteDatabase(dbFile)
|
||||
}
|
||||
|
||||
private var dbFile: String = _
|
||||
}
|
||||
|
||||
private var database: Database = _
|
||||
class DatabaseTest extends AndroidTestCase {
|
||||
|
||||
private lazy val context = new DatabaseTest.MockContext(getContext)
|
||||
|
||||
private lazy val database = new Database(context)
|
||||
|
||||
override def setUp(): Unit = {
|
||||
database = new Database(new TestContext(getContext))
|
||||
database.onMessageReceived(m1)
|
||||
database.onMessageReceived(m2)
|
||||
database.onMessageReceived(m3)
|
||||
|
@ -36,7 +46,7 @@ class DatabaseTest extends AndroidTestCase {
|
|||
override def tearDown(): Unit = {
|
||||
super.tearDown()
|
||||
database.close()
|
||||
getContext.deleteDatabase(dbFile)
|
||||
context.deleteDbFile()
|
||||
}
|
||||
|
||||
def testMessageCount(): Unit = {
|
||||
|
|
|
@ -35,7 +35,7 @@
|
|||
</activity>
|
||||
|
||||
<activity
|
||||
android:name=".activities.ConfirmAddContactDialog"
|
||||
android:name=".activities.ConfirmAddContactActivity"
|
||||
android:theme="@android:style/Theme.Translucent.NoTitleBar"
|
||||
android:excludeFromRecents="true" />
|
||||
|
||||
|
|
|
@ -51,8 +51,8 @@ class AddContactsActivity extends EnsiChatActivity with ChatService.OnConnection
|
|||
override def onItemClick(parent: AdapterView[_], view: View, position: Int, id: Long): Unit = {
|
||||
val contact = adapter.getItem(position)
|
||||
service.sendTo(contact.address, new RequestAddContact())
|
||||
val intent = new Intent(this, classOf[ConfirmAddContactDialog])
|
||||
intent.putExtra(ConfirmAddContactDialog.ExtraContactAddress, contact.address.toString)
|
||||
val intent = new Intent(this, classOf[ConfirmAddContactActivity])
|
||||
intent.putExtra(ConfirmAddContactActivity.ExtraContactAddress, contact.address.toString)
|
||||
startActivity(intent)
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,67 @@
|
|||
package com.nutomic.ensichat.activities
|
||||
|
||||
import android.app.AlertDialog
|
||||
import android.content.DialogInterface.OnClickListener
|
||||
import android.content.{Context, DialogInterface}
|
||||
import android.os.Bundle
|
||||
import android.util.Log
|
||||
import android.view.{ContextThemeWrapper, LayoutInflater}
|
||||
import android.widget.{ImageView, TextView, Toast}
|
||||
import com.nutomic.ensichat.R
|
||||
import com.nutomic.ensichat.protocol.ChatService.OnMessageReceivedListener
|
||||
import com.nutomic.ensichat.protocol.messages.{Message, RequestAddContact, ResultAddContact}
|
||||
import com.nutomic.ensichat.protocol.{Address, Crypto}
|
||||
import com.nutomic.ensichat.util.{Database, IdenticonGenerator}
|
||||
|
||||
object ConfirmAddContactActivity {
|
||||
|
||||
val ExtraContactAddress = "contact_address"
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Shows a dialog for adding a new contact (including key fingerprints).
|
||||
*/
|
||||
class ConfirmAddContactActivity extends EnsiChatActivity with OnClickListener {
|
||||
|
||||
private lazy val user = service.getUser(
|
||||
new Address(getIntent.getStringExtra(ConfirmAddContactActivity.ExtraContactAddress)))
|
||||
|
||||
override def onCreate(savedInstanceState: Bundle): Unit = {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
runOnServiceConnected(() => showDialog())
|
||||
}
|
||||
|
||||
/**
|
||||
* Shows a dialog to accept/deny adding a device as a new contact.
|
||||
*/
|
||||
private def showDialog(): Unit = {
|
||||
val inflater = getSystemService(Context.LAYOUT_INFLATER_SERVICE).asInstanceOf[LayoutInflater]
|
||||
val view = inflater.inflate(R.layout.dialog_add_contact, null)
|
||||
|
||||
val local = view.findViewById(R.id.local_identicon).asInstanceOf[ImageView]
|
||||
val remote = view.findViewById(R.id.remote_identicon).asInstanceOf[ImageView]
|
||||
val remoteTitle = view.findViewById(R.id.remote_identicon_title).asInstanceOf[TextView]
|
||||
|
||||
val localAddress = new Crypto(this).localAddress
|
||||
local.setImageBitmap(IdenticonGenerator.generate(localAddress, (150, 150), this))
|
||||
remote.setImageBitmap(IdenticonGenerator.generate(user.address, (150, 150), this))
|
||||
remoteTitle.setText(getString(R.string.remote_fingerprint_title, user.name))
|
||||
|
||||
new AlertDialog.Builder(new ContextThemeWrapper(this, R.style.AppTheme))
|
||||
.setTitle(getString(R.string.add_contact_dialog, user.name))
|
||||
.setView(view)
|
||||
.setCancelable(false)
|
||||
.setPositiveButton(android.R.string.yes, this)
|
||||
.setNegativeButton(android.R.string.no, this)
|
||||
.setCancelable(false)
|
||||
.show()
|
||||
}
|
||||
|
||||
override def onClick(dialogInterface: DialogInterface, i: Int): Unit = {
|
||||
service.sendTo(user.address, new ResultAddContact(i == DialogInterface.BUTTON_POSITIVE))
|
||||
finish()
|
||||
}
|
||||
|
||||
}
|
|
@ -1,127 +0,0 @@
|
|||
package com.nutomic.ensichat.activities
|
||||
|
||||
import android.app.AlertDialog
|
||||
import android.content.DialogInterface.OnClickListener
|
||||
import android.content.{Context, DialogInterface}
|
||||
import android.os.Bundle
|
||||
import android.util.Log
|
||||
import android.view.{ContextThemeWrapper, LayoutInflater}
|
||||
import android.widget.{ImageView, TextView, Toast}
|
||||
import com.nutomic.ensichat.R
|
||||
import com.nutomic.ensichat.protocol.ChatService.OnMessageReceivedListener
|
||||
import com.nutomic.ensichat.protocol.messages.{Message, RequestAddContact, ResultAddContact}
|
||||
import com.nutomic.ensichat.protocol.{Address, Crypto}
|
||||
import com.nutomic.ensichat.util.{Database, IdenticonGenerator}
|
||||
|
||||
object ConfirmAddContactDialog {
|
||||
|
||||
val ExtraContactAddress = "contact_address"
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Shows a dialog for adding a new contact (including key fingerprints).
|
||||
*/
|
||||
class ConfirmAddContactDialog extends EnsiChatActivity with OnMessageReceivedListener
|
||||
with OnClickListener {
|
||||
|
||||
private val Tag = "ConfirmAddContactDialog"
|
||||
|
||||
private lazy val database = new Database(this)
|
||||
|
||||
private lazy val user = service.getUser(
|
||||
new Address(getIntent.getStringExtra(ConfirmAddContactDialog.ExtraContactAddress)))
|
||||
|
||||
private var localConfirmed = false
|
||||
|
||||
private var remoteConfirmed = false
|
||||
|
||||
private lazy val crypto = new Crypto(this)
|
||||
|
||||
override def onCreate(savedInstanceState: Bundle): Unit = {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
runOnServiceConnected(() => {
|
||||
showDialog()
|
||||
service.registerMessageListener(this)
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Shows a dialog to accept/deny adding a device as a new contact.
|
||||
*/
|
||||
private def showDialog(): Unit = {
|
||||
val inflater = getSystemService(Context.LAYOUT_INFLATER_SERVICE).asInstanceOf[LayoutInflater]
|
||||
val view = inflater.inflate(R.layout.dialog_add_contact, null)
|
||||
|
||||
val local = view.findViewById(R.id.local_identicon).asInstanceOf[ImageView]
|
||||
val remote = view.findViewById(R.id.remote_identicon).asInstanceOf[ImageView]
|
||||
val remoteTitle = view.findViewById(R.id.remote_identicon_title).asInstanceOf[TextView]
|
||||
|
||||
local.setImageBitmap(IdenticonGenerator.generate(crypto.localAddress, (150, 150), this))
|
||||
remote.setImageBitmap(IdenticonGenerator.generate(user.address, (150, 150), this))
|
||||
remoteTitle.setText(getString(R.string.remote_fingerprint_title, user.name))
|
||||
|
||||
new AlertDialog.Builder(new ContextThemeWrapper(this, R.style.AppTheme))
|
||||
.setTitle(getString(R.string.add_contact_dialog, user.name))
|
||||
.setView(view)
|
||||
.setCancelable(false)
|
||||
.setPositiveButton(android.R.string.yes, this)
|
||||
.setNegativeButton(android.R.string.no, this)
|
||||
.setCancelable(false)
|
||||
.show()
|
||||
}
|
||||
|
||||
override def onClick(dialogInterface: DialogInterface, i: Int): Unit = {
|
||||
val result = i match {
|
||||
case DialogInterface.BUTTON_POSITIVE =>
|
||||
localConfirmed = true
|
||||
addContactIfBothConfirmed()
|
||||
true
|
||||
case DialogInterface.BUTTON_NEGATIVE =>
|
||||
finish()
|
||||
false
|
||||
}
|
||||
service.sendTo(user.address, new ResultAddContact(result))
|
||||
}
|
||||
|
||||
/**
|
||||
* Add the user to contacts if [[localConfirmed]] and [[remoteConfirmed]] are true.
|
||||
*/
|
||||
private def addContactIfBothConfirmed(): Unit = {
|
||||
if (localConfirmed && remoteConfirmed) {
|
||||
Log.i(Tag, "Adding new contact " + user.toString)
|
||||
// Get the user again, in case it was updated in the mean time.
|
||||
database.addContact(service.getUser(user.address))
|
||||
Toast.makeText(this, getString(R.string.contact_added, user.name), Toast.LENGTH_SHORT)
|
||||
.show()
|
||||
finish()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles incoming [[RequestAddContact]] and [[ResultAddContact]] messages.
|
||||
*
|
||||
* These are only handled here and require user action, so contacts can only be added if
|
||||
* the user is in this activity.
|
||||
*/
|
||||
override def onMessageReceived(msg: Message): Unit = {
|
||||
if (msg.Header.origin != user.address || msg.Header.target != crypto.localAddress)
|
||||
return
|
||||
|
||||
msg.Body match {
|
||||
case m: ResultAddContact =>
|
||||
if (m.accepted) {
|
||||
Log.i(Tag, user.toString + " accepted us as a contact, updating state")
|
||||
remoteConfirmed = true
|
||||
addContactIfBothConfirmed()
|
||||
} else {
|
||||
Log.i(Tag, user.toString + " denied us as a contact, showing toast")
|
||||
Toast.makeText(this, R.string.contact_not_added, Toast.LENGTH_LONG).show()
|
||||
finish()
|
||||
}
|
||||
case _ =>
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -7,12 +7,12 @@ import android.os.Handler
|
|||
import android.preference.PreferenceManager
|
||||
import android.util.Log
|
||||
import com.nutomic.ensichat.R
|
||||
import com.nutomic.ensichat.activities.{MainActivity, ConfirmAddContactDialog}
|
||||
import com.nutomic.ensichat.activities.{MainActivity, ConfirmAddContactActivity}
|
||||
import com.nutomic.ensichat.bluetooth.BluetoothInterface
|
||||
import com.nutomic.ensichat.fragments.SettingsFragment
|
||||
import com.nutomic.ensichat.protocol.ChatService.{OnConnectionsChangedListener, OnMessageReceivedListener}
|
||||
import com.nutomic.ensichat.protocol.messages._
|
||||
import com.nutomic.ensichat.util.{NotificationHandler, Database}
|
||||
import com.nutomic.ensichat.util.{AddContactsHandler, NotificationHandler, Database}
|
||||
|
||||
import scala.collection.mutable
|
||||
import scala.concurrent.ExecutionContext.Implicits.global
|
||||
|
@ -64,12 +64,12 @@ class ChatService extends Service {
|
|||
|
||||
private lazy val notificationHandler = new NotificationHandler(this)
|
||||
|
||||
private lazy val addContactsHandler = new AddContactsHandler(this, getUser, crypto.localAddress)
|
||||
|
||||
private lazy val router = new Router(connections, sendVia)
|
||||
|
||||
private lazy val seqNumGenerator = new SeqNumGenerator(this)
|
||||
|
||||
private val notificationIdAddContactGenerator = Stream.from(100).iterator
|
||||
|
||||
/**
|
||||
* For this (and [[messageListeners]], functions would be useful instead of instances,
|
||||
* but on a Nexus S (Android 4.1.2), these functions are garbage collected even when
|
||||
|
@ -97,11 +97,11 @@ class ChatService extends Service {
|
|||
pm.edit().putString(SettingsFragment.KeyUserName,
|
||||
BluetoothAdapter.getDefaultAdapter.getName).apply()
|
||||
|
||||
registerMessageListener(database)
|
||||
registerMessageListener(notificationHandler)
|
||||
|
||||
Future {
|
||||
crypto.generateLocalKeys()
|
||||
registerMessageListener(database)
|
||||
registerMessageListener(notificationHandler)
|
||||
registerMessageListener(addContactsHandler)
|
||||
|
||||
btInterface.create()
|
||||
Log.i(Tag, "Service started, address is " + crypto.localAddress)
|
||||
|
@ -177,25 +177,6 @@ class ChatService extends Service {
|
|||
database.changeContactName(contact)
|
||||
|
||||
callConnectionListeners()
|
||||
case _: RequestAddContact =>
|
||||
if (msg.Header.origin == crypto.localAddress)
|
||||
return
|
||||
|
||||
Log.i(Tag, "Remote device " + msg.Header.origin +
|
||||
" wants to add us as a contact, showing notification")
|
||||
val intent = new Intent(this, classOf[ConfirmAddContactDialog])
|
||||
intent.putExtra(ConfirmAddContactDialog.ExtraContactAddress, msg.Header.origin.toString)
|
||||
val pi = PendingIntent.getActivity(this, 0, intent,
|
||||
PendingIntent.FLAG_UPDATE_CURRENT)
|
||||
|
||||
val notification = new Notification.Builder(this)
|
||||
.setContentTitle(getString(R.string.notification_friend_request, getUser(msg.Header.origin)))
|
||||
.setSmallIcon(R.drawable.ic_launcher)
|
||||
.setContentIntent(pi)
|
||||
.setAutoCancel(true)
|
||||
.build()
|
||||
val nm = getSystemService(Context.NOTIFICATION_SERVICE).asInstanceOf[NotificationManager]
|
||||
nm.notify(notificationIdAddContactGenerator.next(), notification)
|
||||
case _ =>
|
||||
mainHandler.post(new Runnable {
|
||||
override def run(): Unit =
|
||||
|
|
|
@ -0,0 +1,102 @@
|
|||
package com.nutomic.ensichat.util
|
||||
|
||||
import android.app.{Notification, NotificationManager, PendingIntent}
|
||||
import android.content.{Context, Intent}
|
||||
import android.util.Log
|
||||
import android.widget.Toast
|
||||
import com.nutomic.ensichat.R
|
||||
import com.nutomic.ensichat.activities.ConfirmAddContactActivity
|
||||
import com.nutomic.ensichat.protocol.ChatService.OnMessageReceivedListener
|
||||
import com.nutomic.ensichat.protocol.messages.{RequestAddContact, Message, ResultAddContact}
|
||||
import com.nutomic.ensichat.protocol.{Address, User}
|
||||
|
||||
/**
|
||||
* Handles [[RequestAddContact]] and [[ResultAddContact]] messages, adds new contacts.
|
||||
*
|
||||
* @param getUser Returns info about a given address.
|
||||
* @param localAddress Address of the local device.
|
||||
*/
|
||||
class AddContactsHandler(context: Context, getUser: (Address) => User, localAddress: Address)
|
||||
extends OnMessageReceivedListener {
|
||||
|
||||
private val Tag = "AddContactsHandler"
|
||||
|
||||
private val notificationIdAddContactGenerator = Stream.from(100).iterator
|
||||
|
||||
private lazy val database = new Database(context)
|
||||
|
||||
private var currentlyAdding = Map[Address, AddContactInfo]()
|
||||
|
||||
private case class AddContactInfo(localConfirmed: Boolean, remoteConfirmed: Boolean)
|
||||
|
||||
def onMessageReceived(msg: Message): Unit = {
|
||||
val remote =
|
||||
if (msg.Header.origin == localAddress)
|
||||
msg.Header.target
|
||||
else
|
||||
msg.Header.origin
|
||||
|
||||
msg.Body match {
|
||||
case _: RequestAddContact =>
|
||||
Log.i(Tag, "Remote device " + remote + " wants to add us as a contact")
|
||||
currentlyAdding += (remote -> new AddContactInfo(false, false))
|
||||
|
||||
// Don't show notification for requests coming from local device.
|
||||
if (msg.Header.origin == localAddress)
|
||||
return
|
||||
|
||||
val intent = new Intent(context, classOf[ConfirmAddContactActivity])
|
||||
intent.putExtra(ConfirmAddContactActivity.ExtraContactAddress, msg.Header.origin.toString)
|
||||
val pi = PendingIntent.getActivity(context, 0, intent,
|
||||
PendingIntent.FLAG_UPDATE_CURRENT)
|
||||
|
||||
val notification = new Notification.Builder(context)
|
||||
.setContentTitle(context.getString(R.string.notification_friend_request, getUser(remote)))
|
||||
.setSmallIcon(R.drawable.ic_launcher)
|
||||
.setContentIntent(pi)
|
||||
.setAutoCancel(true)
|
||||
.build()
|
||||
|
||||
val nm = context.getSystemService(Context.NOTIFICATION_SERVICE).asInstanceOf[NotificationManager]
|
||||
nm.notify(notificationIdAddContactGenerator.next(), notification)
|
||||
case res: ResultAddContact =>
|
||||
if (!currentlyAdding.contains(remote)) {
|
||||
Log.w(Tag, "ResultAddContact without previous RequestAddContact, ignoring")
|
||||
return
|
||||
}
|
||||
|
||||
val newInfo =
|
||||
if (msg.Header.origin == localAddress)
|
||||
new AddContactInfo(res.accepted, currentlyAdding(remote).remoteConfirmed)
|
||||
else
|
||||
new AddContactInfo(currentlyAdding(remote).localConfirmed, res.accepted)
|
||||
currentlyAdding += (remote -> newInfo)
|
||||
|
||||
if (res.accepted)
|
||||
addContactIfBothConfirmed(remote)
|
||||
else {
|
||||
Toast.makeText(context, R.string.contact_not_added, Toast.LENGTH_LONG).show()
|
||||
currentlyAdding -= remote
|
||||
}
|
||||
case _ =>
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds the given address as a new contact, if local and remote device sent a [[ResultAddContact]]
|
||||
* message with accepted = true.
|
||||
*/
|
||||
private def addContactIfBothConfirmed(address: Address): Unit = {
|
||||
val info = currentlyAdding(address)
|
||||
val user = getUser(address)
|
||||
if (info.localConfirmed && info.remoteConfirmed) {
|
||||
Log.i(Tag, "Adding new contact " + user.toString)
|
||||
database.addContact(user)
|
||||
Toast
|
||||
.makeText(context, context.getString(R.string.contact_added, user.name), Toast.LENGTH_SHORT)
|
||||
.show()
|
||||
currentlyAdding -= address
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -32,6 +32,7 @@ class NotificationHandler(context: Context) extends OnMessageReceivedListener {
|
|||
val nm = context.getSystemService(Context.NOTIFICATION_SERVICE)
|
||||
.asInstanceOf[NotificationManager]
|
||||
nm.notify(notificationIdNewMessage, notification)
|
||||
case _ =>
|
||||
}
|
||||
|
||||
}
|
||||
|
|
Reference in a new issue