본문 바로가기

안드로이드

[Kotlin] 백그라운드에서 FCM 알림 받기

728x90

저번 글에 이어서 데이터베이스의 데이터에 변경이 생길 경우 앱에 알림이 가도록 구현하고자 한다.

 

toy9910.tistory.com/31

 

[Kotlin] 안드로이드와 실시간 데이터베이스 데이터 읽고 쓰기

toy9910.tistory.com/29 [Kotlin] 안드로이드와 파이어베이스 실시간 데이터베이스 연동 우선 안드로이드와 파이어베이스가 연동이 되어있다는 가정 하에 진행한다. 만약 아직 연동이 안되어 있다면 toy9

toy9910.tistory.com

 

일단 푸시 알림으로는 2가지 유형이 있다. Notification은 앱이 포그라운드일 때만 푸시 알림이 오고 Data는 포그라운드와 백그라운드 둘 다 푸시 알림이 온다. 필자는 백그라운드에서 알림을 받는 것을 구현할 예정이므로 Data를 사용해 앱에 알림을 받고자 한다.

 

우선 앱에 FCM 기능을 추가해야한다.

 

상단 'Tools' 메뉴에서 'Firebase'를 클릭한다.

 

목록들 중에서 'Cloud Messaging'을 찾아 'Set up Firebase Cloud Messaging'을 누른다.

 

데이터베이스와 연동이 되어있다면 1번은 완료가 되었을 것이고 안되어있으면 해준다. 

 

프로젝트 수준의 'Build.gradle'에서 dependencies 부분을 위와 같이 바꿔준다.

 

앱 수준의 'Build.gradle' 파일에서 defaultConfig와 dependencies 부분을 위와 같이 바꿔준다.

 

res폴더에 strings.xml에 위 내용을 추가한다.

 

백그라운드인 상태에서 알림을 받으려면 Service를 사용하여 계속해서 앱을 실행시키고 있어야 한다.

 

FirebaseMessagingService를 상속받아 MyFirebaseMessagingService를 구현한다.

 

data를 통해 받은 제목과 내용을 알림창에 설정하여 알림을 띄우는 코드다.

 

파이어베이스 홈페이지에서 보내는 알림은 notification 으로 보내는 알림이라 백그라운드 상태에서 받을 수 없다. 그래서 notification이 없고 data만 있게 해서 보내야한다. 그 방법을 겨우 찾았다. 해당 방법을 이용하기 위해서는 파이썬이 깔려있고 파이참이 있어야한다.

 

 

파이참을 실행 후 File-Settings-Project:pythonProject-Python Interpreter 에 들어가서 좌측 하단에 '+'를 누른다.

 

검색 창에 'pyfcm' 이라고 친 후 해당 패키지를 다운 받는다.

 

'Realtime Database'에 '클라우드 메시징'으로 들어가면 서버 키를 알 수 있다. 해당 서버 키는 이따가 사용할 예정이니 따로 복사해서 저장해두자.

 

현재 이 상태에서 앱을 실행하면 Logcat에 토큰이 뜰 것이다.

토큰도 복사해두자.

 

파이참 코드를 위와 같이 입력한 후 앱을 실행을 하고 홈 버튼을 눌러 백그라운드로 전환한다. 이제 파이썬 코드를 실행시키면 data가 전송되고 백그라운드 상태에서 알림이 뜨는 것을 볼 수 있다.

 

알림을 누르면 메인 액티비티로 넘어간다

전체 코드

 

MainActivity

더보기

class MainActivity : AppCompatActivity() {
lateinit var firebaseDatabase: FirebaseDatabase
lateinit var databaseReference: DatabaseReference
lateinit var mArrayList : ArrayList<PersonData>
lateinit var mAdapter: PersonAdapter

val TAG = "hy"

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

mArrayList = arrayListOf<PersonData>()
mAdapter = PersonAdapter(mArrayList)

person_list.layoutManager = LinearLayoutManager(this)
person_list.adapter = mAdapter
person_list.addItemDecoration(
DividerItemDecoration(applicationContext,
LinearLayoutManager(this).orientation)
)

mArrayList.clear()
mAdapter.notifyDataSetChanged()

initDatabase()

btn_send.setOnClickListener {
val name = et_name.text.toString()
val phone = et_phone.text.toString()

// 데이터베이스에 데이터 삽입
val databaseReference1 = firebaseDatabase.getReference("person")
databaseReference1.child(name).setValue(phone)

et_name.setText("")
et_phone.setText("")
}

databaseReference.addValueEventListener(object : ValueEventListener {
override fun onDataChange(snapshot: DataSnapshot) {
mArrayList.clear()
for(shot in snapshot.children) {
val data = shot.key
val data2 = shot.value.toString()
val p_name = data
val p_phone = data2
val p = PersonData(p_name, p_phone)
mArrayList.add(p)
}
mAdapter.notifyDataSetChanged()
}
override fun onCancelled(error: DatabaseError) {
TODO("Not yet implemented")
}
})
}

fun initDatabase() {
// 파이어베이스 인스턴스 생성
firebaseDatabase = FirebaseDatabase.getInstance()
databaseReference = firebaseDatabase.getReference("person")

// 토큰 확인
FirebaseInstanceId.getInstance().instanceId.addOnCompleteListener(object :
OnCompleteListener<InstanceIdResult> {
override fun onComplete(p0: Task<InstanceIdResult>) {
if(!p0.isSuccessful) {
return
}
val token = p0.result?.token

val msg = getString(R.string.msg_token_fmt, token)
Log.d(TAG, msg)
}
})
}
}

MyFirebaseMessagingService

더보기

class MyFirebaseMessagingService : FirebaseMessagingService() {
val TAG = "hy"

override fun onMessageReceived(p0: RemoteMessage) {
Log.d(TAG, "msg : ${p0.toString()}")

if(p0.data.isNotEmpty()) {
Log.d(TAG, "data : ${p0.data.toString()}")
sendTopNotification(p0.data["title"].toString(), p0.data["body"].toString())
if(true) {
scheduleJob()
} else {
handleNow()
}
}
}

fun scheduleJob() {
val work = OneTimeWorkRequest.Builder(MyWorker::class.java).build()
WorkManager.getInstance().beginWith(work).enqueue()
}

fun handleNow() {
Log.d(TAG, "Short lived task is done.")
}


private fun sendTopNotification(title: String?, body: String) {
val CHANNEL_DEFAULT_IMPORTANCE = "channel_id"
val ONGOING_NOTIFICATION = 1

// 알림 클릭시 앱 화면 띄우는 intent 생성
val notificationIntent = Intent(this,MainActivity::class.java)
notificationIntent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)
val pendingIntent = PendingIntent.getActivity(this,0,notificationIntent,0)

// 알림 생성
val notification = Notification.Builder(this,CHANNEL_DEFAULT_IMPORTANCE)
.setContentTitle(title)
.setContentText(body)
.setSmallIcon(R.mipmap.ic_launcher)
.setContentIntent(pendingIntent)
.build()

val notificationManager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager

if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val channel = NotificationChannel(CHANNEL_DEFAULT_IMPORTANCE,"Channel human readable title", NotificationManager.IMPORTANCE_DEFAULT)
notificationManager.createNotificationChannel(channel)
}

notificationManager.notify(ONGOING_NOTIFICATION,notification)
}

override fun onNewToken(p0: String) {
super.onNewToken(p0)
Log.i(TAG, "Refreshed token: +$p0")
}
}

MyWorker

더보기

class MyWorker(context: Context, workerParams: WorkerParameters) : Worker(context, workerParams) {
override fun doWork(): Result {
return Result.success()
}
}

PersonAdapter.kt

더보기

class PersonAdapter (list: ArrayList<PersonData>) : RecyclerView.Adapter<CustomViewHolder>() {
var mList : ArrayList<PersonData> = list

override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): CustomViewHolder {
val view = LayoutInflater.from(parent.context).inflate(R.layout.person_list,parent,false)
return CustomViewHolder(view)
}

override fun getItemCount(): Int {
return mList.size
}

override fun onBindViewHolder(holder: CustomViewHolder, position: Int) {
val p = mList.get(position)
holder.setHolder(p)
}

}

class CustomViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
fun setHolder(personData: PersonData) {
itemView.tv_name.text = personData.name
itemView.tv_phone.text = personData.phone
}
}

PersonData

더보기

data class PersonData (var name : String?, var phone : String?){}

activity_main.xml

더보기

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity"
android:orientation="vertical">

<androidx.recyclerview.widget.RecyclerView
android:id="@+id/person_list"
android:layout_width="match_parent"
android:layout_height="wrap_content" />



<EditText
android:id="@+id/et_name"
android:layout_width="167dp"
android:layout_height="wrap_content"
android:hint="이름을 입력하세요." />

<EditText
android:id="@+id/et_phone"
android:layout_width="167dp"
android:layout_height="wrap_content"
android:hint="번호를 입력하세요." />
<Button
android:id="@+id/btn_send"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="등록" />

</LinearLayout>

person_list.xml

더보기

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:orientation="horizontal"
android:layout_height="30dp">

<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="5dp"
android:id="@+id/tv_name"/>

<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="5dp"
android:id="@+id/tv_phone"/>

</LinearLayout>

build.gradle(Project)

더보기

// Top-level build file where you can add configuration options common to all sub-projects/modules.
buildscript {
ext.kotlin_version = "1.3.72"
repositories {
google()
jcenter()
}
dependencies {
classpath "com.android.tools.build:gradle:4.1.1"
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
classpath 'com.google.gms:google-services:4.3.5'


// NOTE: Do not place your application dependencies here; they belong
// in the individual module build.gradle files
}
}

allprojects {
repositories {
google()
jcenter()
}
}

task clean(type: Delete) {
delete rootProject.buildDir
}

build.gradle(App)

더보기

plugins {
id 'com.android.application'
id 'kotlin-android'
id 'kotlin-android-extensions'
}

android {
compileSdkVersion 30
buildToolsVersion "30.0.3"

defaultConfig {
applicationId "com.example.fbtest"
minSdkVersion 28
targetSdkVersion 30
versionCode 1
versionName "1.0"

testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
}

buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
kotlinOptions {
jvmTarget = '1.8'
}
}

dependencies {

implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
implementation 'androidx.core:core-ktx:1.3.2'
implementation 'androidx.appcompat:appcompat:1.2.0'
implementation 'com.google.android.material:material:1.3.0'
implementation 'androidx.constraintlayout:constraintlayout:2.0.4'
implementation 'com.google.firebase:firebase-analytics:18.0.3'
implementation 'com.google.firebase:firebase-database:19.7.0'
implementation 'com.google.firebase:firebase-messaging:21.1.0'
implementation 'androidx.work:work-runtime-ktx:2.4.0'

testImplementation 'junit:junit:4.+'
androidTestImplementation 'androidx.test.ext:junit:1.1.2'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0'

compile 'com.google.firebase:firebase-database:11.8.0'
}

apply plugin: 'com.google.gms.google-services'

728x90
반응형