Firebase Storage 이미지 업로드 및 Firebase Cloud Messaging(FCM) 푸시 알림

승준·2023년 5월 26일
0
post-thumbnail

Firebase 앱 추가

1. 프로젝트 생성

Firebase 홈페이지 이동


2. 앱 추가


프로젝트 단계와 앱 단계 build.gradle 수정

앱 닉네임과 SHA-1은 선택사항

SHA-1은 터미널에서 ./gradlew signingReport입력 후 ctrl + Enter로 간단하게 얻을 수 있다.

Firebase Cloud Storage

1. 버킷 생성


2. 규칙 수정

Storage 규칙을 수정하여 인증된 사용자만 Firebase 저장소 버킷에 파일을 업로드할 수 있도록 한다.

3. Firebase Admin SDK

프로젝트 설정 -> 서비스 계정 -> 새 비공개 키 생성

4. 패키지 설치

pip install firebase-admin

5. 앱 초기화

Firebase Admin SDK 사용자 인증, 앱 초기화, storageBucket 주소 지정

import firebase_admin
from firebase_admin import credentials
from firebase_admin import storage

# Firebase Key Path
cred = credentials.Certificate("firebase-adminsdk.json")
    
# Firebase App init
firebase_admin.initialize_app(cred,{
'storageBucket': 'boar-detection.appspot.com'
})
버킷이 하나일 경우 버킷 주소는 (프로젝트 이름).appspot.com

6. 이미지 업로드

226줄 이미지 저장하는 코드 아래에 작성

cv2.imwrite(save_path, im0)

# firebase upload
if os.path.isfile(save_path):
	try:
		bucket = storage.bucket()
		blob = bucket.blob(f"images/{os.path.basename(save_path)}")
		blob.upload_from_filename(save_path)
	except Exception as e:
		print(f"Error uploading file: {e}")
else:
	print(f"No file found at {save_path}")

save_path에 파일이 존재하면 스토리지 버킷의 images 폴더 내에 파일의 기본 이름으로 업로드

파일 이름에 "/", "?", "#" 등의 기호를 사용하면 URL에 문제가 발생할 수 있어서 사용을 피하는 게 좋다
# Remove saved image
if os.path.isfile(save_path):
	os.remove(save_path)

라즈베리파이에 파일이 쌓이지 않도록 파일 업로드 이후에 제거했다.

7. 업로드 결과


images 폴더에 이미지가 업로드되었고, 클릭하면 이미지를 확인할 수 있다.

Firebase Realtime Database

DB 생성 및 규칙 수정



FCM token을 라즈베리파이에서 가져올 수 있도록 Realtime Database를 사용한다.

implementation 'com.google.firebase:firebase-database'

Firebase Cloud Messaging

1. 현재 등록 토큰 가져오기

Firebase Messaging 의존성 추가, [Sync Now] 클릭

implementation 'com.google.firebase:firebase-messaging'

기기의 FCM 토큰 확인하고 Realtime Database에 저장

// FCM 토큰 가져오기
        FirebaseMessaging.getInstance().getToken()
                .addOnCompleteListener(task -> {
                    if (!task.isSuccessful()) {
                        Log.w(TAG, "Fetching FCM registration token failed", task.getException());
                        return;
                    }

                    // 토큰 가져오기 성공
                    String token = task.getResult();
                    Log.d(TAG, "FCM registration token: " + token);

                    // Firebase Realtime Database에서 토큰 조회
                    DatabaseReference tokensRef = FirebaseDatabase.getInstance().getReference("tokens");
                    Query query = tokensRef.orderByValue().equalTo(token);
                    query.addListenerForSingleValueEvent(new ValueEventListener() {
                        @Override
                        public void onDataChange(@NonNull DataSnapshot dataSnapshot) {
                            if (dataSnapshot.exists()) {
                                // 이미 저장된 토큰이 있을 경우 처리할 로직 추가
                                Log.d(TAG, "Token already exists");
                            } else {
                                // Firebase Realtime Database에 토큰 저장
                                tokensRef.push().setValue(token);
                                Log.d(TAG, "Token saved to database");
                            }
                        }

                        @Override
                        public void onCancelled(@NonNull DatabaseError databaseError) {
                            Log.e(TAG, "Database error: " + databaseError.getMessage());
                        }
                    });
                });
    }

토큰은 기기가 변경되거나 앱이 재설치 될 때마다 새로 발급된다.

2. 기기에 푸시 알림 표시

AndroidManifest.xml 파일에 인터넷 권한 추가

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

AndroidManifest.xml 파일에 FirebaseMessagingService를 상속받는 서비스 클래스 추가

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

MyFirebaseMessagingService 클래스에 알림 표시 기능 추가
Notification.Builder , NotificationCompat.Builder

public class MyFirebaseMessagingService extends FirebaseMessagingService {
	@Override
    public void onNewToken(String token) {
        // FCM 토큰이 갱신되었을 때 필요한 작업 수행
        Log.d("FCM Log", "Refreshed token: " + token);
    }
    @Override
    public void onMessageReceived(RemoteMessage remoteMessage) {
        // FCM 메시지 title과 body를 수신, sendNotification 메서드에서 처리
        if (remoteMessage.getNotification() != null) {
            String title = remoteMessage.getNotification().getTitle();
            String body = remoteMessage.getNotification().getBody();
            sendNotification(title, body);
        }
    }
    private void sendNotification(String title, String body) {
    	// 알림 채널
    	String channelId = "my_channel_id";
        String channelName = "My Channel";
        int importance = NotificationManager.IMPORTANCE_HIGH;
        
  		// 알림 클릭 시 열릴 액티비티  
        Intent intent = new Intent(this, MainActivity.class);
        intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
        PendingIntent pendingIntent = PendingIntent.getActivity(this, 0, intent, PendingIntent.FLAG_IMMUTABLE);
        
        // 알림 소리
        Uri soundUri = RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION);
        
        // 알림 생성
        NotificationCompat.Builder builder = new NotificationCompat.Builder(this, channelId)
        		.setSmallIcon(R.drawable.ic_notification)
                .setContentTitle(title)
                .setContentText(body)
                .setAutoCancel(true) // 알림 클릭 시 사라지기
                .setSound(soundUri)
                .setContentIntent(pendingIntent)
                
		// 알림 매니저를 통해 알림 표시
        NotificationManagerCompat notificationManager = NotificationManagerCompat.from(this);
        notificationManager.createNotificationChannel(new NotificationChannel(channelId, channelName, importance));
        if (ContextCompat.checkSelfPermission(this, android.Manifest.permission.POST_NOTIFICATIONS)
                == PackageManager.PERMISSION_GRANTED) {
            notificationManager.notify(0, builder.build());
        }
    }

알림 정보 , 런타임 권한 요청

Android 13 이상에서 런타임 알림 권한 요청
알림을 받으려면 알림 권한을 허용해야 한다. UI로 알림을 사용했을 때의 기능을 설명하고 사용자가 확인하면 알림 권한을 요청. 허용 시 앱에서 알림을 표시할 수 있도록 한다.

TIRAMISU 이상에서 알림 권한을 요청하는 다이얼로그를 표시하고,
알림 권한 거부 시에 권한 설정을 요청하는 스낵바를 표시하도록 했다.

private static final int PERMISSION_REQUEST_NOTIFICATION = 1;

private void checkNotificationPermission() {
        if (ContextCompat.checkSelfPermission(this, Manifest.permission.POST_NOTIFICATIONS)
                != PackageManager.PERMISSION_GRANTED) {
            // 권한이 없는 경우 권한 요청
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
                ActivityCompat.requestPermissions(this,
                        new String[]{Manifest.permission.POST_NOTIFICATIONS},
                        PERMISSION_REQUEST_NOTIFICATION);
            }
        }
    }

    @Override
    public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
        super.onRequestPermissionsResult(requestCode, permissions, grantResults);
        if (requestCode == PERMISSION_REQUEST_NOTIFICATION) {
            if (grantResults.length > 0 && grantResults[0] != PackageManager.PERMISSION_GRANTED) {
                // 권한이 거부된 경우 스낵바 또는 다이얼로그 등으로 알림을 표시하여 사용자에게 권한 필요성 알리기
                showPermissionSnackbar();
            }
        }
    }

    private void showPermissionSnackbar() {
        Snackbar.make(findViewById(android.R.id.content), "알림 권한이 필요합니다.", Snackbar.LENGTH_LONG)
                .setAction("권한 설정", new View.OnClickListener() {
                    @Override
                    public void onClick(View v) {
                        // 앱 설정 화면으로 이동하여 알림 권한 설정을 요청할 수 있음
                        Intent intent = new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS);
                        Uri uri = Uri.fromParts("package", getPackageName(), null);
                        intent.setData(uri);
                        startActivity(intent);
                    }
                })
                .show();
    }
    
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        // 알림 권한 체크 및 요청
        checkNotificationPermission();
        ...
    }
        

알림 권한 요청을 구분하기 위한 식별자 상수 변수 PERMISSION_REQUEST_NOTIFICATION = 1;

권한을 체크하는 checkNotificationPermission() 메서드에서 ContextCompat.checkSelfPermission()으로 PERMISSION_GRANTED를 확인하고 아닐 시에 SDK 33 TIRAMISU 이상이면 ActivityCompat.requestPermissions()POST_NOTIFICATIONS 알림 권한을 요청하는 다이얼로그를 표시하고 사용자의 응답을 기다린다.

이후에 응답 결과가 onRequestPermissionsResult() 호출되어 requestCode에는 식별자가 grandResults에는 권한의 허용 여부 PackageManager.PERMISSION_GRANTED 또는 PackageManager.PERMISSION_DENIED가 전달된다.

grantResults 배열에서 PackageManager.PERMISSION_GRANTED가 아닐 시에 권한이 거부되었다고 판단하여 showPermissionSnackbar()를 호출하여 권한 설정을 요청하는 스낵바를 띄운다.

<!--  background에서 사용할 아이콘 설정  -->
<meta-data
	android:name="com.google.firebase.messaging.default_notification_icon"
    android:resource="@drawable/ic_notification" />

3. 알림 테스트



테스트 메시지 전송 버튼 클릭 -> 토큰 입력 후 + 버튼 클릭 -> 기기가 등록 된다.

4. 토큰으로 기기에 메시지 발송 (Python)

detect.py 알림 생성 주기를 제어할 변수 previous_notification_time,
FCM 토큰 입력 변수 registration_token

import firebase_admin
from firebase_admin import credentials
from firebase_admin import storage
from firebase_admin import messaging
from firebase_admin import db
import time

previous_notification_time = 0

# Firebase Key Path
cred = credentials.Certificate("firebase-adminsdk.json")

# Firebase App init
firebase_admin.initialize_app(cred,{
'storageBucket': 'boar-detection.appspot.com',
'databaseURL': 'https://boar-detection-default-rtdb.firebaseio.com'
})

# Firebase Realtime Database에서 토큰 가져오기
ref = db.reference('tokens')
tokens = ref.get()

이미지 업로드 아래 메시지 전송 코드 추가
객체가 검출될 때마다 알림이 가면 너무 남발이기 때문에 10초마다 알림을 보내도록 한다.

# send message
current_time = time.time()

if current_time - previous_notification_time >= 10:
	for token_key in tokens:
		token = tokens[token_key]
		message = messaging.Message(
			notification=messaging.Notification(
				title='멧돼지가 검출되었습니다!',
				body='앱을 접속하여 확인하세요',
			),
			token=token,
		)

		response = messaging.send(message)
		print('Successfully sent message:', response)
                        
	previous_notification_time = current_time

라즈베리파이4에서 카메라에 멧돼지가 검출되면 이미지를 저장하고 Storage에 업로드,
FCM으로 앱에 푸시 알림이 가도록 완료했다.

profile
student

1개의 댓글

comment-user-thumbnail
2023년 12월 2일

안드로이드 스튜디오 관련해서 처음 써봐서 그런데 혹시 위 코드가 어디에 추가되어야 하는지 알려주실수 있나요?

답글 달기