E/AndroidRuntime: FATAL EXCEPTION: main
    Process: com.hyeonwoo.art_android, PID: 26068
    java.lang.RuntimeException: Unable to start activity ComponentInfo{com.hyeonwoo.art_android/com.hyeonwoo.art_android.ui.main.MainActivity}: android.view.InflateException: Binary XML file line #35 in com.hyeonwoo.art_android:layout/activity_main: Binary XML file line #35 in com.hyeonwoo.art_android:layout/activity_main: Error inflating class com.google.android.material.bottomnavigation.BottomNavigationView
        at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:3654)
        at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:3806)
        at android.app.servertransaction.LaunchActivityItem.execute(LaunchActivityItem.java:83)
        at android.app.servertransaction.TransactionExecutor.executeCallbacks(TransactionExecutor.java:135)
        at android.app.servertransaction.TransactionExecutor.execute(TransactionExecutor.java:95)
        at android.app.ActivityThread$H.handleMessage(ActivityThread.java:2267)
        at android.os.Handler.dispatchMessage(Handler.java:107)
        at android.os.Looper.loop(Looper.java:237)
        at android.app.ActivityThread.main(ActivityThread.java:8167)
        at java.lang.reflect.Method.invoke(Native Method)
        at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:496)
        at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:1100)
     Caused by: android.view.InflateException: Binary XML file line #35 in com.hyeonwoo.art_android:layout/activity_main: Binary XML file line #35 in com.hyeonwoo.art_android:layout/activity_main: Error inflating class com.google.android.material.bottomnavigation.BottomNavigationView
     Caused by: android.view.InflateException: Binary XML file line #35 in com.hyeonwoo.art_android:layout/activity_main: Error inflating class com.google.android.material.bottomnavigation.BottomNavigationView
     Caused by: java.lang.reflect.InvocationTargetException
        at java.lang.reflect.Constructor.newInstance0(Native Method)
        at java.lang.reflect.Constructor.newInstance(Constructor.java:343)
        at android.view.LayoutInflater.createView(LayoutInflater.java:854)
        at android.view.LayoutInflater.createViewFromTag(LayoutInflater.java:1006)
        at android.view.LayoutInflater.createViewFromTag(LayoutInflater.java:961)
        at android.view.LayoutInflater.rInflate(LayoutInflater.java:1123)
        at android.view.LayoutInflater.rInflateChildren(LayoutInflater.java:1084)
        at android.view.LayoutInflater.inflate(LayoutInflater.java:682)
        at android.view.LayoutInflater.inflate(LayoutInflater.java:534)
        at android.view.LayoutInflater.inflate(LayoutInflater.java:481)
        at androidx.appcompat.app.AppCompatDelegateImpl.setContentView(AppCompatDelegateImpl.java:710)
        at androidx.appcompat.app.AppCompatActivity.setContentView(AppCompatActivity.java:195)
        at androidx.databinding.DataBindingUtil.setContentView(DataBindingUtil.java:303)
        at androidx.databinding.DataBindingUtil.setContentView(DataBindingUtil.java:284)
        at com.hyeonwoo.art_android.ui.BaseActivity.onCreate(BaseActivity.kt:15)
        at com.hyeonwoo.art_android.ui.main.MainActivity.onCreate(MainActivity.kt:16)
        at android.app.Activity.performCreate(Activity.java:7963)
        at android.app.Activity.performCreate(Activity.java:7952)
        at android.app.Instrumentation.callActivityOnCreate(Instrumentation.java:1307)
        at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:3629)
        at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:3806)
        at android.app.servertransaction.LaunchActivityItem.execute(LaunchActivityItem.java:83)
        at android.app.servertransaction.TransactionExecutor.executeCallbacks(TransactionExecutor.java:135)
        at android.app.servertransaction.TransactionExecutor.execute(TransactionExecutor.java:95)
        at android.app.ActivityThread$H.handleMessage(ActivityThread.java:2267)
        at android.os.Handler.dispatchMessage(Handler.java:107)
        at android.os.Looper.loop(Looper.java:237)
        at android.app.ActivityThread.main(ActivityThread.java:8167)
        at java.lang.reflect.Method.invoke(Native Method)
E/AndroidRuntime:     at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:496)
        at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:1100)
     Caused by: android.content.res.Resources$NotFoundException: Resource ID #0x7f0601ea
        at android.content.res.ResourcesImpl.getValue(ResourcesImpl.java:276)
        at android.content.res.Resources.getValue(Resources.java:1452)
        at androidx.core.content.res.ResourcesCompat.isColorInt(ResourcesCompat.java:309)
        at androidx.core.content.res.ResourcesCompat.inflateColorStateList(ResourcesCompat.java:256)
        at androidx.core.content.res.ResourcesCompat.getColorStateList(ResourcesCompat.java:236)
        at androidx.core.content.ContextCompat.getColorStateList(ContextCompat.java:519)
        at androidx.appcompat.content.res.AppCompatResources.getColorStateList(AppCompatResources.java:48)
        at androidx.appcompat.widget.TintTypedArray.getColorStateList(TintTypedArray.java:179)
        at com.google.android.material.navigation.NavigationBarView.<init>(NavigationBarView.java:168)
        at com.google.android.material.bottomnavigation.BottomNavigationView.<init>(BottomNavigationView.java:108)
        at com.google.android.material.bottomnavigation.BottomNavigationView.<init>(BottomNavigationView.java:103)
        at com.google.android.material.bottomnavigation.BottomNavigationView.<init>(BottomNavigationView.java:98)
         ... 31 more
I/Process: Sending signal. PID: 26068 SIG: 9

 

AVD 장치에서는 오류가 없었는데, 실제 장치에서 실행 시 activity를 띄우는 도중 크래시가 발생했다 ㅠ

 

정확한 원인은 잘 모르겠지만, 오류 로그를 통해 문제가 발생하는 지점이 BottomNavigationView 라는걸 알수있었음!

 

        <com.google.android.material.bottomnavigation.BottomNavigationView
            android:id="@+id/main_bottom_navigation"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            app:itemBackground="@color/white"
            app:itemIconTint="@color/material_dynamic_tertiary50"
            app:itemTextColor="@color/black"
            app:menu="@menu/menus" />

수상해 보이는 건 메뉴 아니면 머테리얼 컬러인것 같아서 컬러 값을 변경해보니 정상 동작했다...

 

해당 material color의 실제 색상 Hex값으로 수정 후 실행하니 크래시가 발생하지 않았음 ~

        <com.google.android.material.bottomnavigation.BottomNavigationView
            android:id="@+id/main_bottom_navigation"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:background="#CAC1EA"
            app:itemBackground="@color/white"
            app:itemIconTint="#FF787296"
            app:itemTextColor="@color/black"
            app:menu="@menu/menus" />

 

더 좋은 방법있음 댓글 달아주쇼~

최근 개발중인 사이드 프로젝트 앱에서 안드로이드 스튜디오를 통해 앱을 실행할 경우, 실행은 되지만 앱이 설치가 되지않는 현상이 있었다. 

 

실행은 잘 됐는데

 

목록에는 없다...

 

해당 이슈의 원인은 바로! AndroidManifest 파일에 있었는데

        <activity
            android:name=".ui.main.MainActivity"
            android:exported="true">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <action android:name="android.intent.action.VIEW" />
                <category android:name="android.intent.category.LAUNCHER" />
                <category android:name="android.intent.category.DEFAULT"/>
                <category android:name="android.intent.category.BROWSABLE"/>

                <data
                    android:host="oauth"
                    android:scheme="kakao"/>
            </intent-filter>

            <meta-data
                android:name="android.app.lib_name"
                android:value="" />
        </activity>

위의 인텐트 필터를 보면 MAIN, LAUNCHER와 함께 여러 값들이 있는데, MAIN, LAUNCHER에는 데이터 태그를 처리할 수 없다고 한다... 그래서 아래처럼 인텐트 필터를 분리시켜주면 된다고 한다.

        <activity
            android:name=".ui.main.MainActivity"
            android:exported="true">
                        <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
            <intent-filter>
                <action android:name="android.intent.action.VIEW" />
                <category android:name="android.intent.category.BROWSABLE"/>
                <category android:name="android.intent.category.DEFAULT"/>
                <data
                    android:host="oauth"
                    android:scheme="kakao"/>
            </intent-filter>

            <meta-data
                android:name="android.app.lib_name"
                android:value="" />
        </activity>

 

이제 다시 설치해보자!!

정상적으로 앱이 설치된걸 확인했다.

 

Reference :

https://stackoverflow.com/questions/20285496/android-application-installed-but-wont-open-on-device

 

Android Application Installed but won't open on device

I created an application on Android. I am developing it on eclipse with ADT. It is about nfc. For the moment it only reads and writes tag. I run my application on my mobile device for testing and...

stackoverflow.com

 

Hilt를 사용하면서 만난 오류이다.

 

error: [Dagger/MissingBinding] @dagger.hilt.android.qualifiers.ActivityContext android.content.Context cannot be provided without an @Provides-annotated method.  public abstract static class SingletonC implements ArtOnApplication_GeneratedInjector,

 

에러가 난 코드는 아래와 같다.

 

class KakaoAuthService @Inject constructor(
    @ActivityContext private val context: Context
) {
    fun startKakaoLogin(kakaoLoginCallback: (OAuthToken?, Throwable?) -> Unit) {
        val kakaoLoginState = if (UserApiClient.instance.isKakaoTalkLoginAvailable(context)) {
            KAKAO_TALK_LOGIN
        } else {
            KAKAO_ACCOUNT_LOGIN
        }

        when (kakaoLoginState) {
            KAKAO_TALK_LOGIN -> UserApiClient.instance.loginWithKakaoTalk(
                context,
                callback = kakaoLoginCallback
            )
            KAKAO_ACCOUNT_LOGIN -> UserApiClient.instance.loginWithKakaoAccount(
                context,
                callback = kakaoLoginCallback
            )
        }
    }

    companion object {
        const val KAKAO_TALK_LOGIN = 0
        const val KAKAO_ACCOUNT_LOGIN = 1
    }
}

 

ActivityContext를 사용하는 부분에서 오류가 난 것인데, ActivityContext의 경우 @Provide 를이용해서 의존성을 주입하라는 것 같다. Stack Overflow를 통해 비슷한 오류를 찾아봤는데

 

@HiltViewModelViewModel이 구성 변경의 활동보다 오래 지속되기 때문에 누출이 발생할 수 있으므로 활동 컨텍스트(또는 활동 자체)를 삽입할 수 없습니다. (구글 번역기..)

 

대략 ActivityContext의 생명주기 보다, ViewModel의 생명주기가 더 길기 때문에 의존성 주입이 불가능하다.. 라고 하는 것 같다.

 

나의 경우 ActivityContext가 아닌, ApplicationContext를 이용해도 상관없어 보여서 ApplicationContext을 주입하도록 코드를 수정해주니 정상적으로 빌드되었다.

 

class KakaoAuthService @Inject constructor(
    @ApplicationContext private val context: Context
)

다른 좋은 방법이 있다면 공유 부탁드립니다..😜

 

이전시간에는 Android WebView를 통해 당근마켓 홈페이지를 표시하는 기능을 개발했습니다.

 

그러나 WebView 또다른 url로 이동하게 될 경우 WebView 내부가 아닌, 외부 브라우저를 통해 앱이 페이지가 표시되는 이슈가 있었습니다. 이를 해결해봅니다.

 

Android Developer 페이지의 WebView 문서에서 이 문제를 간단하게 해결할 수 있는 방법을 제시합니다.

 

사용자가 WebView에서 웹페이지의 링크를 클릭하면 URL을 처리하는 앱이 Android에서 실행되는 것이 기본 동작입니다. 대개 기본 웹브라우저에 도착 URL이 열리고 로드됩니다.

 

하지만 링크가 WebView내에서 열리도록 WebView의 이 동작을 재정의할 수 있습니다. 그러면 WebView에 의해 유지 관리되는 웹페이지 방문 기록을 통해 사용자가 앞뒤로 탐색할 수 있습니다.

 

webView 객체에 아래의 코드를 추가하여 사용자가 클릭한 링크가 WebView 내에서 로드되게 할 수 있습니다.

webView.webViewClient = WebViewClient() // 사용자가 클릭한 모든 링크가 WebView에 로드되게 함.

또한, 문서 바로아래에 뒤로가기 버튼을 클릭했을 때 WebView에서 이전 페이지를 표시하는 기능도 제공합니다.

MainActivity내에 아래의 코드를 추가해줍니다.

override fun onKeyDown(keyCode: Int, event: KeyEvent?): Boolean {
        // Check if the key event was the Back button and if there's history
        if (keyCode == KeyEvent.KEYCODE_BACK && webView.canGoBack()) {
            webView.goBack()
            return true
        }
        // If it wasn't the Back key or there's no web page history, bubble up to the default
        // system behavior (probably exit the activity)
        return super.onKeyDown(keyCode, event)
    }

이로서 간단하게 WebView 내에서 페이지를 이동하고, 뒤로가기 버튼 클릭시 WebView 내에서 뒤로가기로 동작하는 기능을 추가 했습니다.

 

reference :

https://developer.android.com/guide/webapps/webview?hl=ko#HandlingNavigation

 

WebView에서 웹 앱 빌드  |  Android 개발자  |  Android Developers

WebView에서 웹 앱 빌드 컬렉션을 사용해 정리하기 내 환경설정을 기준으로 콘텐츠를 저장하고 분류하세요. 웹 애플리케이션 또는 웹페이지만 클라이언트 애플리케이션의 일부로 제공하려는 경

developer.android.com

 

WebView 샘플 앱 개발 개요

현재 project내 front-end개발 인원에 비해 Android 앱 개발 인원이 부족하여 안드로이드 앱 네이티브 기능이 필요하지 않은 부분은 협의하에 WebView로 구현하여 프로젝트 일정에 부담을 덜어내기로 합니다.

Android WebView란 ?

웹 애플리케이션 또는 웹페이지만 클라이언트 애플리케이션의 일부로 제공하려는 경우 [WebView](<https://developer.android.com/reference/android/webkit/WebView?hl=ko>)를 사용하면 됩니다. [WebView](<https://developer.android.com/reference/android/webkit/WebView?hl=ko>)  클래스는 Android의 [View](<https://developer.android.com/reference/android/view/View?hl=ko>)클래스의 확장으로, 웹페이지를 활동 레이아웃의 일부로 표시할 수 있게 해 줍니다. 탐색 컨트롤이나 주소 표시줄 등 완전히 개발된 웹브라우저의 기능은 전혀 포함되어 있지 않습니다.

[WebView](<https://developer.android.com/reference/android/webkit/WebView?hl=ko>)의 모든 작업은 기본적으로 웹페이지를 표시하는 것입니다.

앱에 WebView 추가

[WebView](<https://developer.android.com/reference/android/webkit/WebView?hl=ko>)를 앱에 추가하려면 활동 레이아웃에서 <WebView> 요소를 포함하거나 onCreate() 에서 전체 활동 창을 WebView로 설정하면 됩니다.

AndroidManifest.xml 파일의 manifest 태그 내에 아래와 같이 추가합니다.

<uses-permission android:name="android.permission.INTERNET" />

MainActivity에는 webView를 로드합니다. 테스트로 당근마켓의 홈페이지를 로드했습니다.

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

        val webView = findViewById(R.id.webview)
        webView.loadUrl("<https://www.daangn.com/>")

    }
}

activity_main.xml 파일에는 WebView를 추가합니다.

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout 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">

    <WebView
        android:id="@+id/webview"
        android:layout_width="match_parent"
        android:layout_height="match_parent">

    </WebView>

</androidx.constraintlayout.widget.ConstraintLayout>

여기까지 설정 시 아래처럼 안드로이드 앱 실행 시 당근마켓의 홈 페이지를 표시합니다. 그러나 당웹뷰 내의 버튼을 클릭 시 앱 내에서 이동하지 않고 새로운 chrome 앱을 통해 페이지를 표시했습니다.

 

이번 포스팅에서는 웹뷰를 표시하는것 까지 확인하고, 다음 포스팅에서 앱 내에서 어떻게 웹 페이지를 이동할 수 있는지 알아보겠습니다.

지난시간에서 우리는 firebase 콘솔에서 FCM을 사용하기 위한 세팅을 완료했습니다.

이번시간에는 Android 앱에서 어떻게 푸쉬 알림을 수신하고 메세지를 표시하는지 알아봅시다.

먼저 firebase 메세지를 수신하기 위해 MyFirebaseMessagingService 클래스를 생성하고 아래와 같이 코드를 붙여넣습니다. 소스코드는 firebase 공식 github의 소스코드를 이용했습니다.

class MyFirebaseMessagingService : FirebaseMessagingService() {

    /**
     * Called when message is received.
     *
     * @param remoteMessage Object representing the message received from Firebase Cloud Messaging.
     */
    // [START receive_message]
    // 메세지 수신 function
    override fun onMessageReceived(remoteMessage: RemoteMessage) {
        // [START_EXCLUDE]
        // There are two types of messages data messages and notification messages. Data messages are handled
        // here in onMessageReceived whether the app is in the foreground or background. Data messages are the type
        // traditionally used with GCM. Notification messages are only received here in onMessageReceived when the app
        // is in the foreground. When the app is in the background an automatically generated notification is displayed.
        // When the user taps on the notification they are returned to the app. Messages containing both notification
        // and data payloads are treated as notification messages. The Firebase console always sends notification
        // messages. For more see: <https://firebase.google.com/docs/cloud-messaging/concept-options>
        // [END_EXCLUDE]

        // TODO(developer): Handle FCM messages here.
        // Not getting messages here? See why this may be: <https://goo.gl/39bRNJ>
        Log.d(TAG, "From: ${remoteMessage.from}")

        // Check if message contains a data payload.
        // firebase-cloud-message 알림 작성시 추가 옵션에 설정한 맞춤 데이터를 수신.
        if (remoteMessage.data.isNotEmpty()) {
            Log.d(TAG, "Message data payload: ${remoteMessage.data}")

            if (/* Check if data needs to be processed by long running job */ true) {
                // For long-running tasks (10 seconds or more) use WorkManager.
                scheduleJob()
            } else {
                // Handle message within 10 seconds
                handleNow()
            }
        }

        // Check if message contains a notification payload.
        remoteMessage.notification?.let {
            Log.d(TAG, "Message Notification Body: ${it.body}")
            it.body?.let { body -> sendNotification(body) }
        }

        // Also if you intend on generating your own notifications as a result of a received FCM
        // message, here is where that should be initiated. See sendNotification method below.
    }
    // [END receive_message]

    // [START on_new_token]
    /**
     * Called if the FCM registration token is updated. This may occur if the security of
     * the previous token had been compromised. Note that this is called when the
     * FCM registration token is initially generated so this is where you would retrieve the token.
     */

    override fun onDeletedMessages() {
        super.onDeletedMessages()
    }
    override fun onNewToken(token: String) {
        Log.d(TAG, "Refreshed token: $token")

        // If you want to send messages to this application instance or
        // manage this apps subscriptions on the server side, send the
        // FCM registration token to your app server.
        sendRegistrationToServer(token)
    }
    // [END on_new_token]

    /**
     * Schedule async work using WorkManager.
     */
    private fun scheduleJob() {
        // [START dispatch_job]
        val work = OneTimeWorkRequest.Builder(MyWorker::class.java).build()
        WorkManager.getInstance(this).beginWith(work).enqueue()
        // [END dispatch_job]
    }

    /**
     * Handle time allotted to BroadcastReceivers.
     */
    private fun handleNow() {
        Log.d(TAG, "Short lived task is done.")
    }

    /**
     * Persist token to third-party servers.
     *
     * Modify this method to associate the user's FCM registration token with any server-side account
     * maintained by your application.
     *
     * @param token The new token.
     */
    private fun sendRegistrationToServer(token: String?) {
        // TODO: Implement this method to send token to your app server.
        Log.d(TAG, "sendRegistrationTokenToServer($token)")
    }

    /**
     * Create and show a simple notification containing the received FCM message.
     *
     * @param messageBody FCM message body received.
     */
    private fun sendNotification(messageBody: String) {
        val intent = Intent(this, MainActivity::class.java)
        intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)
        val pendingIntent = PendingIntent.getActivity(this, 0 /* Request code */, intent,
            PendingIntent.FLAG_IMMUTABLE)

        val channelId = getString(R.string.default_notification_channel_id)
        val defaultSoundUri = RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION)
        val notificationBuilder = NotificationCompat.Builder(this, channelId)
            .setSmallIcon(R.drawable.ic_launcher_foreground)
            .setContentTitle(getString(R.string.fcm_message))
            .setContentText(messageBody)
            .setAutoCancel(true)
            .setSound(defaultSoundUri)
            .setContentIntent(pendingIntent)
            .setPriority(NotificationCompat.PRIORITY_HIGH)

        val notificationManager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager

        // Since android Oreo notification channel is needed.
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
            val channel = NotificationChannel(channelId,
                "Channel human readable title",
                NotificationManager.IMPORTANCE_HIGH)
            notificationManager.createNotificationChannel(channel)
        }

        notificationManager.notify(R.string.default_notification_channel_id /* ID of notification */, notificationBuilder.build())
    }

    companion object {

        private const val TAG = "MyFirebaseMsgService"
    }
}

또한 작업시간이 길어질 경우 백그라운드에서 작업할 수 있도록 MyWorker 클래스도 생성해줍니다.

class MyWorker(appContext: Context, workerParams: WorkerParameters) : Worker(appContext, workerParams) {

    override fun doWork(): ListenableWorker.Result {
        Log.d(TAG, "Performing long running task in scheduled job")
        // TODO(developer): add long running task here.
        return ListenableWorker.Result.success()
    }

    companion object {
        private val TAG = "MyWorker"
    }
}

그리고 AndroidManifest.xml 파일의 application 태그내에 아래의 코드를 추가합니다.

<service
      android:name=".MyFirebaseMessagingService"
      android:exported="false"
      android:directBootAware="true">
      <intent-filter>
          <action android:name="com.google.firebase.MESSAGING_EVENT" />
      </intent-filter>
  </service>

app 수준 gradle 파일의 dependency는 아래처럼 구성했습니다.

implementation platform('com.google.firebase:firebase-bom:31.0.0')
    implementation 'com.google.firebase:firebase-messaging-ktx'
    implementation 'com.google.firebase:firebase-analytics'
    implementation 'androidx.work:work-runtime:2.7.1'
    implementation 'com.google.firebase:firebase-messaging-directboot'

MainActivity에서 노티피케이션 채널을 등록해줍니다.

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

        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
            // Create channel to show notifications.
            val channelId = getString(R.string.default_notification_channel_id)
            val channelName = getString(R.string.default_notification_channel_name)
            val notificationManager = getSystemService(NotificationManager::class.java)
            notificationManager?.createNotificationChannel(
                NotificationChannel(channelId,
                channelName, NotificationManager.IMPORTANCE_HIGH)
            )
        }
		}

여기까지 설정 후 빌드 및 앱을 실행해줍니다.

 

궁금한점이나 잘못된 부분이 있다면 댓글로 알려주시면 감사하겠습니다!

 

reference

quickstart-android/messaging at master · firebase/quickstart-android

 

GitHub - firebase/quickstart-android: Firebase Quickstart Samples for Android

Firebase Quickstart Samples for Android. Contribute to firebase/quickstart-android development by creating an account on GitHub.

github.com

 

최근 기획하고 있는 앱에서 푸쉬알림 기능이 필요하여 구현한 내용을 공유합니다.

Firebase Cloud Messaging 라이브러리를 이용하여 개발했으며, 샘플 앱의 전체 소스코드는 아래의 링크를 통해 확인해주세요.

https://github.com/gogoadl/android-fcm-demo

 

GitHub - gogoadl/android-fcm-demo: android firebase cloud message sample

android firebase cloud message sample. Contribute to gogoadl/android-fcm-demo development by creating an account on GitHub.

github.com

푸쉬알림이란?

푸시 알림은 사용자가 스마트폰 또는 다른 디바이스에 설치된, 브랜드 앱에서 직접 수신하는 실시간 알림 입니다. 기업은 푸시 메시지를 사용하여 현재 브랜드와 상호작용하지 않는 고객의 관심을 유도할 수 있습니다.

 

Android 앱에서는 FCM (Firebase Cloud Messaging) 라이브러리를 통해 쉽게 푸쉬알림 기능을 구현할 수 있습니다. 지금부터 FCM의 기본적인 설정과 FCM을 통해 안드로이드 앱으로 푸쉬알림을 전송하는 기능을 구현해봅니다.

Firebase 콘솔 설정

먼저Firebase 콘솔로 이동하여 프로젝트를 생성해줍니다.

로그인

 

로그인 - Google 계정

이메일 또는 휴대전화

accounts.google.com

저는 아래처럼 프로젝트 이름을 설정했습니다.

애널리틱스 설정은 기본적으로 사용 설정이 권장됩니다. 저는 이대로 계속 버튼을 눌러 진행했습니다.

Google 애널리틱스 구성 역시 기본 설정으로 구성 후 프로젝트 만들기 버튼을 선택합니다.

프로젝트가 생성된 후 아래 안드로이드 로고 이미지를 선택하여 안드로이드 앱을 추가합니다.

이후 아래와 같은 창이 보이면 Android 패키지 이름 항목에 테스트용으로 생성한 앱의 패키지명을 추가해줍니다. 패키지명을 잘 모르시겠다면 Android 프로젝트의 AndroidManifest.xml 파일의 package 속성을 사용해주시면 됩니다. 앱 닉네임과 디버그 서명 인증서는 skip 합니다.

이후 앱 등록 시 아래와 같은 화면이 나옵니다. 설명에 따라 다운로드한 파일을 앱 모듈 루트 디렉토리에 위치시켜 줍니다.

FirebaseSDK 추가도 동일하게 상세한 설명을 참고하여 세팅을 완료합니다.

 

궁금한점이나 잘못된 부분이 있다면 댓글로 알려주시면 감사하겠습니다!

 

Reference

https://firebase.google.com/docs/cloud-messaging?hl=ko

 

Firebase 클라우드 메시징

Firebase 클라우드 메시징(FCM)은 무료로 메시지를 안정적으로 전송할 수 있는 크로스 플랫폼 메시징 솔루션입니다.

firebase.google.com

https://business.adobe.com/kr/glossary/push-notifications.html

 

푸시 알림이란 무엇입니까? | Adobe 용어 설명

푸시 알림은 스마트폰 또는 다른 디바이스에 설치된 앱에서 실시간으로 제공하는 알림입니다. 자세한 내용을 살펴보십시오.

business.adobe.com

 

BroadcastReceiver ?

Android 시스템 및 기타 Android 앱에서 브로드캐스트 메시지를 보내거나 받을 수 있습니다 . 이러한 브로드캐스트는 관심 있는 이벤트가 발생할 때 전송됩니다. 예를 들어 Android 시스템은 시스템이 부팅되거나 기기가 충전을 시작하는 등 다양한 시스템 이벤트가 발생할 때 브로드캐스트를 보냅니다. 예를 들어 앱은 사용자 지정 브로드캐스트를 전송하여 다른 앱에 관심이 있을 수 있는 항목(예: 일부 새 데이터가 다운로드됨)을 알릴 수도 있습니다.

BroadcastReceiver 등록 방법

// BroadcastReceiver class
public class MyBroadcastReceiver extends BroadcastReceiver {
        private static final String TAG = "MyBroadcastReceiver";
        @Override
        public void onReceive(Context context, Intent intent) {
            StringBuilder sb = new StringBuilder();
            sb.append("Action: " + intent.getAction() + "\\n");
            sb.append("URI: " + intent.toUri(Intent.URI_INTENT_SCHEME).toString() + "\\n");
            String log = sb.toString();
            Log.d(TAG, log);
            Toast.makeText(context, log, Toast.LENGTH_LONG).show();
        }
    }
// activity
BroadcastReceiver br = new MyBroadcastReceiver();
IntentFilter filter = new IntentFilter(APP_SPECIFIC_BROADCAST);

registerReceiver(context, br, filter);

unregisterReceiver(br);

LocalBroadcastManager

[LocalBroadcastManager.sendBroadcast](<https://developer.android.com/reference/androidx/localbroadcastmanager/content/LocalBroadcastManager#sendBroadcast(android.content.Intent)>)메서드는 발신자와 동일한 앱에 있는 수신자에게 브로드캐스트를 보냅니다. 앱 간에 브로드캐스트를 보낼 필요가 없으면 로컬 브로드캐스트를 사용하세요. 구현이 훨씬 더 효율적이며(프로세스 간 통신이 필요하지 않음) 브로드캐스트를 수신하거나 보낼 수 있는 다른 앱과 관련된 보안 문제에 대해 걱정할 필요가 없습니다.

브로드캐스트 수신기를 내보내고 장치의 다른 앱에서 볼 수 있는지 여부를 선택합니다. 이 수신기가 시스템이나 다른 앱(귀하가 소유한 다른 앱 포함)에서 보낸 브로드캐스트를 수신하는 경우 android:exported="true"

<receiver android:name=".MyBroadcastReceiver" android:exported="true">

 

Reference

https://developer.android.com/guide/components/broadcasts?hl=ko 

 

브로드캐스트 개요  |  Android 개발자  |  Android Developers

브로드캐스트 개요 컬렉션을 사용해 정리하기 내 환경설정을 기준으로 콘텐츠를 저장하고 분류하세요. Android 앱은 Android 시스템 및 기타 Android 앱에서 게시-구독 디자인 패턴과 유사한 브로드캐

developer.android.com

 

 

Android 장치에 블루투스 마우스 장치를 연결하면 화면에 마우스 커서가 표시됩니다.

이 커서를 Android 7.0 이상부터 커서를 숨기거나, 모양을 바꿀 수 있게 되었습니다.

필자의 경우 마우스 커서를 숨기는 기능에 사용했었는데, 이때 사용한 코드를 공유합니다.

블루투스 마우스 커서의 경우 setPointerIcon 메서드를 호출한 뷰 계층에서는 마우스 커서 모양이 변경되는것으로 보이며,

시스템 UI위에서는 기본 마우스 커서가 보이게 됩니다.

아래는 제가 사용했던 코드 일부를 공유합니다.
@Override
public boolean dispatchGenericMotionEvent(MotionEvent motionEvent) {
    int index = motionEvent.getActionIndex();

    if (motionEvent.getToolType(index) == MotionEvent.TOOL_TYPE_MOUSE) {
        if (rootLayout != null && Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
            PointerIcon pointerIcon = PointerIcon.getSystemIcon(this, -1);
            rootLayout.setPointerIcon(pointerIcon);
        }
    }
    return super.dispatchGenericMotionEvent(motionEvent);
}

 

SDK 외부 배포 시 발생한 오류이며 배포한 SDK의 proguard 옵션 설정으로 오류 해결 가능하다.

 

오류의 원인은 proguard 설정에서 패키지명, 클래스명이 난독화되면서 a.a.a.... 처럼 알파벳 순서대로 패키지명이 생성된다. 이 때 내가 배포한 SDK를 사용하는 유저가 사용하는 다른 라이브러리에서도 이러한 기본 난독화 패키지명을 사용할 경우 두 SDK의 패키지명, 클래스명이 모두 a.a... 등으로 표시되면서 클래스를 찾지 못하게 된다.

 

이 때 프로가드에서 flattenpackagehierarchy 옵션을 통해 기본 패키지명 앞에 식별가능한 prefix 패키지명을 붙일 수 있다.

-flattenpackagehierarchy "com.hyeonwoo.handsome"

이 프로가드 설정은 consumer-rule이 아닌 proguard-rule에 적용해야 한다.

 

Kakao Android SDK에서 해당 설정이 적용되지 않은것을 발견해 이슈 리포트를 했으나 내부에서 이슈 파악 후 수정된 버전을 배포준비중이라고 했다. 내가 도움이 되었다면 더 기뻤겠지만, 해당 이슈를 kakao 에서도 버그로 인식하는 것으로 봐서 올바른 수정사항이라고 어느정도 확신을 가질 수 있을 것 같다.

 

https://devtalk.kakao.com/t/android-sdk-com-kakao-sdk2-11-0/124909

 

Android SDK com.kakao.sdk:v2-friend:2.11.0에서 발생가능한 문제에 대해 문의드립니다

SDK 버전 : com.kakao.sdk:v2-friend:2.11.0 SDK Proguard 설정으로인해 발생가능한 문제가 있어보여 확인 부탁드립니다. External Libraries 에서 해당 라이브러리 jar 파일을 확인해봤는데요. 패키지명이 기본적으

devtalk.kakao.com

 

+ Recent posts