/** * A simple {@link Fragment} subclass. * Use the {@link BoardAppendFragment#newInstance} factory method to * create an instance of this fragment. */ public class BoardAppendFragment extends Fragment implements OnBackPressedListener {
...
ClipboardManager clipboard ; ClipData clipData ;
public BoardAppendFragment() { // Required empty public constructor }
ListView 에서는 SetOnItemClick 이벤트의 활용이 넘나 쉬었다. 그냥 선언하고 구현만 하면 되었으니, 그런데, RecycleView 을 사용해서 구현하다 보니 이런일이 생긴다. RecycleView 에서는 클릭 이벤트 등을 구현하는게 힘들다. 그래도 어쩌라 구현은 해야 되고.
폭풍 구글링~~~ 그래서 찾아낸 것은 이런 것들이다. RecycleView 에서 Holder 을 이용해서 item 을 배치 하고 그 Holder 의 item 을 클릭하는 것을 이용 하도록 하는 것이다.
public class ChatBotAdapter extends RecyclerView.Adapter<ChatBotAdapter.ViewHolder> {
private static final String TAG = "ChatAdapter"; private final ArrayList<ChatMsgModel> chatMsgModels; String userEmail ;
@Override public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { View view = LayoutInflater.from(parent.getContext()) .inflate(R.layout.custom_chat_msg, parent, false); return new ViewHolder(view); }
@Override public void onBindViewHolder(final ViewHolder holder, int position) {
ChatMsgModel vo = chatMsgModels.get(position);
...
holder.userid_tv.setText(vo.getNickName()); // userId 대신 nickName 으로 대체 holder.date_tv.setText(vo.getCrt_dt()); holder.content_tv.setText(vo.getContent()); }
@Override public int getItemCount() { return chatMsgModels.size(); }
public class ViewHolder extends RecyclerView.ViewHolder { public ConstraintLayout my_cl, other_cl; public TextView userid_tv, date_tv, content_tv
이렇게 MainActivity 의 onCreate 에서 구현을 해 보면 ListView 에서는 해당 ListView.setOnClickListener 을 활용했다고 하면, RecycleView 에서는 RecycleView.Adapter 을 이용해서 adapter.setOnItemClickListener 을 활용하는 방법이 될 수 있다고 생각이 든다.
chatMsgModels = new ArrayList<>(); chatRoom = FirebaseDatabase.getInstance().getReference(chatRoomKey);
.....
adapter = new ChatAdapter(chatMsgModels, userEmail); binding.rv.setLayoutManager(new LinearLayoutManager(ChatRoomActivity.this)); binding.rv.setAdapter(adapter);
chatRoom.orderByChild("crt_dt").startAfter(now).addChildEventListener(new ChildEventListener() { @Override public void onChildAdded(@NonNull DataSnapshot dataSnapshot, @Nullable String previousChildName) { // Database 의 정보를 ChatMsgVO 객체에 담음 ChatMsgModel chatMsgVO = dataSnapshot.getValue(ChatMsgModel.class); Log.e(TAG, "crt_dt=" + chatMsgVO.getCrt_dt()) ; chatMsgModels.add(chatMsgVO);
// 채팅 메시지 배열에 담고 RecyclerView 다시 그리기 adapter = new ChatAdapter(chatMsgModels, userEmail); binding.rv.setAdapter(adapter); binding.rv.scrollToPosition(chatMsgModels.size()-1); Log.e(TAG, chatMsgModels.size()+""); }
@Override public void onChildChanged(@NonNull DataSnapshot snapshot, @Nullable String previousChildName) {
}
@Override public void onChildRemoved(@NonNull DataSnapshot snapshot) {
}
@Override public void onChildMoved(@NonNull DataSnapshot snapshot, @Nullable String previousChildName) {
}
@Override public void onCancelled(@NonNull DatabaseError error) {
} });
..... }
흔히 하는 실수일 것 같다. adapter 을 구현해 사용하고자 하는 경우
adapter = new ChatAdapter(chatMsgModels, userEmail); 을 선언해 구현하는데, 간혹 데이터를 읽어오고 다시 넣어주는 시점에 다시 new 아덥터를 구현하는 경우 ... 위 예시와 같은 구성을 하는 경우에...
아래 부분 어딘가에서 adapter 을 이용한 이벤트를 실행하고자 하면, 동작을 하지 않는 것 같은 느낌이 올때가 있다. 왜 일까 ? 참 여러시간 고민을 했던 기억이 있어서 이렇게 글로 남겨 보는 건데. new 아답터 해서 생성하는 것은 첨에 한번만 하는 것으로 해야지 위 에서와 같이 onChildAdded 내부에서 다시 new ChatAdapter 을 해서 생성하는 것은 새로 객체가 생성이 되었기 때문에 아래 부분에서 adapter.SetOnClickListener 등을 구현하게 되었을 때, 혼돈의 구렁텅이에 빠지게 될 것이다.
chatMsgModels = new ArrayList<>(); chatRoom = FirebaseDatabase.getInstance().getReference(chatRoomKey);
.....
adapter = new ChatAdapter(chatMsgModels, userEmail); binding.rv.setLayoutManager(new LinearLayoutManager(ChatRoomActivity.this)); binding.rv.setAdapter(adapter);
chatRoom.orderByChild("crt_dt").startAfter(now).addChildEventListener(new ChildEventListener() { @Override public void onChildAdded(@NonNull DataSnapshot dataSnapshot, @Nullable String previousChildName) { // Database 의 정보를 ChatMsgVO 객체에 담음 ChatMsgModel chatMsgVO = dataSnapshot.getValue(ChatMsgModel.class); Log.e(TAG, "crt_dt=" + chatMsgVO.getCrt_dt()) ; chatMsgModels.add(chatMsgVO);
// 채팅 메시지 배열에 담고 RecyclerView 다시 그리기 binding.rv.setAdapter(adapter); binding.rv.scrollToPosition(chatMsgModels.size()-1); Log.e(TAG, chatMsgModels.size()+""); }
@Override public void onChildChanged(@NonNull DataSnapshot snapshot, @Nullable String previousChildName) {
}
@Override public void onChildRemoved(@NonNull DataSnapshot snapshot) {
}
@Override public void onChildMoved(@NonNull DataSnapshot snapshot, @Nullable String previousChildName) {
}
@Override public void onCancelled(@NonNull DatabaseError error) {
#스하리1000명프로젝트, In Korea verloren? Auch wenn Sie kein Koreanisch sprechen, hilft Ihnen diese App dabei, sich problemlos fortzubewegen. Sprechen Sie einfach Ihre Sprache – es übersetzt, sucht und zeigt Ergebnisse in Ihrer Sprache an. Ideal für Reisende! Unterstützt mehr als 10 Sprachen, darunter Englisch, Japanisch, Chinesisch, Vietnamesisch und mehr. Probieren Sie es jetzt aus! https://play.google.com/store/apps/details?id=com.billcoreatech.opdgang1127
/** Firebase 클라우드 메시징(FCM)을 사용하여 iOS, Android 및 웹의 클라이언트에 메시지를 보낼 수 있습니다.
이 샘플은 FCM을 사용하여 `news`를 구독하는 클라이언트에 두 가지 유형의 메시지를 보냅니다. 주제. 메시지의 한 유형은 단순 알림 메시지(디스플레이 메시지)입니다. 다른 하나는 플랫폼별 사용자 정의가 포함된 알림 메시지(알림 표시), 예를 들어, iOS 장치로 전송되는 메시지에 배지가 추가됩니다. */ public class Messaging {
/** * project_id 는 firebase 에 등록한 나의 project ID */ private static final String PROJECT_ID = "my-application-f80fb"; private static final String BASE_URL = "https://fcm.googleapis.com"; private static final String FCM_SEND_ENDPOINT = "/v1/projects/" + PROJECT_ID + "/messages:send";
private static final String MESSAGING_SCOPE = "https://www.googleapis.com/auth/firebase.messaging"; private static final String[] SCOPES = { MESSAGING_SCOPE }; private static final String TAG = "FCM Messaging";
public static String TITLE = "FCM Notification"; public static String BODY = "Notification from FCM"; public static String URL = "https://billcorea.tistory.com"; public static String IMAGEURL = ""; public static final String MESSAGE_KEY = "message";
public static String tokenExam = "fIkVvZbATHu6Ilv1PNhp_8:APA91bG2q8MmfQvNqeP5afzKZRuLsTu1Mu6MVuYXuGlBhhgXZ3QGvP5EjoUBhVmmaBsT3CTKwKpUy5odRhzp5N46NB01txhxOFTUZnP8-LwnUovCx3hMGqNZtYW9jZ7FT-kCV37m7V2r";
/** FCM REST에 대한 요청을 승인하는 데 사용할 수 있는 유효한 액세스 토큰을 검색합니다. API. * * https://console.firebase.google.com/u/1/project/fcm.......40/settings/serviceaccounts/adminsdk * 이 url 에서 비공개생성키 을 클릭 해서 생성된 파일은 assets 폴더에 넣는다. * 파일 이름은 소문자로만 적용한다. (파일 이름이 너무 길면 줄이는 것이 좋다) * * @return Access token. * @throws IOException */ // [START retrieve_access_token] public String getAccessToken(Context context) throws IOException {
/** * 검색 및 게시 모두에 사용할 수 있는 HttpURLConnection을 만듭니다. * * @return Base HttpURLConnection. * @throws IOException */ public HttpURLConnection getConnection(Context context) throws IOException { // [START use_access_token] URL url = new URL(BASE_URL + FCM_SEND_ENDPOINT); HttpURLConnection httpURLConnection = (HttpURLConnection) url.openConnection(); httpURLConnection.setRequestProperty("Authorization", "Bearer " + getAccessToken(context)); httpURLConnection.setRequestProperty("Content-Type", "application/json; UTF-8"); return httpURLConnection; // [END use_access_token] }
/** HTTP를 사용하여 FCM 메시지에 요청을 보냅니다. UTF-8로 인코딩되고 특수 문자를 지원합니다. * * @param fcmMessage Body of the HTTP request. * @throws IOException */ public void sendMessage(JsonObject fcmMessage, Context context) throws IOException { HttpURLConnection connection = getConnection(context); connection.setDoOutput(true); OutputStreamWriter writer = new OutputStreamWriter(connection.getOutputStream(), "UTF-8"); writer.write(fcmMessage.toString()); writer.flush(); writer.close();
int responseCode = connection.getResponseCode(); if (responseCode == 200) { String response = inputstreamToString(connection.getInputStream()); Log.e(TAG, "Message sent to Firebase for delivery, response:"); Log.e(TAG, response); } else { Log.e(TAG, "Unable to send message to Firebase:"); String response = inputstreamToString(connection.getErrorStream()); Log.e(TAG, response); } }
/** 공통 FCM 필드를 사용하여 모든 사용자에게 알림 메시지를 보내는 메시지를 보냅니다. 플랫폼. 또한 플랫폼별 재정의는 메시지가 표시되는 방식을 사용자 지정하는 데 사용됩니다. 안드로이드와 iOS에서 받았습니다. * * @throws IOException */ public void sendOverrideMessage() throws IOException { JsonObject overrideMessage = buildOverrideMessage(); Log.e(TAG, "FCM request body for override message:"); prettyPrint(overrideMessage); sendMessage(overrideMessage, context); }
/** FCM 요청의 본문을 작성합니다. 이 본문은 공통 알림 개체를 정의합니다. android 및 apns 개체를 사용하는 플랫폼별 사용자 정의도 가능합니다. * * @return JSON representation of the FCM request body. */ public JsonObject buildOverrideMessage() { JsonObject jNotificationMessage = buildNotificationMessage();
/** * Android에서 메시지가 수신되는 방식을 사용자 지정하는 Android 페이로드를 빌드합니다. * * @return android payload of an FCM request. */ public JsonObject buildAndroidOverridePayload() { /** * 이부분은 notify 알림에 표시될 제목, 내용, 보여줄 이미지의 URL 경로 */ JsonObject androidNotification = new JsonObject(); androidNotification.addProperty("body", BODY); androidNotification.addProperty("title", TITLE); androidNotification.addProperty("image", IMAGEURL);
/** * 사용자 viewAcitivy 로 전달할 내용 과 URL * * FcmReceiveService 에서 URL, BODY 을 읽어서 ViewActivity 로 전달함. */ JsonObject data = new JsonObject(); data.addProperty("URL", URL); data.addProperty("BODY", BODY);
JsonObject androidNotificationPayload = new JsonObject(); androidNotificationPayload.add("notification", androidNotification); androidNotificationPayload.add("data", data);
return androidNotificationPayload; }
/** * iOS에서 메시지가 수신되는 방식을 사용자 지정하는 apns 페이로드를 빌드합니다. * * @return apns payload of an FCM request. */ public JsonObject buildApnsHeadersOverridePayload() { JsonObject apnsHeaders = new JsonObject(); apnsHeaders.addProperty("apns-priority", "10");
JsonObject apsPayload = new JsonObject(); apsPayload.add("aps", badgePayload);
return apsPayload; }
/** * 등록된 장치에 전달하기 위해 FCM에 알림 메시지를 보냅니다. * * @throws IOException */ public void sendCommonMessage(Context context) throws IOException { JsonObject notificationMessage = buildNotificationMessage(); Log.e(TAG, "FCM request body for message using common notification object:"); prettyPrint(notificationMessage); sendMessage(notificationMessage, context); }
/** * 알림 메시지 요청의 본문을 구성합니다. * * @return JSON of notification message. */ public JsonObject buildNotificationMessage() { /** * notify 알림에 사용할 제목과 내용 */ JsonObject jNotification = new JsonObject(); jNotification.addProperty("title", TITLE); jNotification.addProperty("body", BODY);
JsonObject jMessage = new JsonObject(); if (sp.getBoolean("pushOne", false)) { jMessage.addProperty("token", tokenExam); } else { jMessage.addProperty("topic", "allDevices"); } jMessage.add("notification", jNotification);
JsonObject jFcm = new JsonObject(); jFcm.add(MESSAGE_KEY, jMessage);
return jFcm; }
/** * InputStream의 내용을 String으로 읽습니다. * * @param inputStream InputStream to read. * @return String containing contents of InputStream. * @throws IOException */ public String inputstreamToString(InputStream inputStream) throws IOException { StringBuilder stringBuilder = new StringBuilder(); Scanner scanner = new Scanner(inputStream); while (scanner.hasNext()) { stringBuilder.append(scanner.nextLine()); } return stringBuilder.toString(); }
/** * JsonObject를 예쁘게 인쇄하십시오. * * @param jsonObject JsonObject to pretty print. */ public void prettyPrint(JsonObject jsonObject) { Gson gson = new GsonBuilder().setPrettyPrinting().create(); Log.e(TAG, gson.toJson(jsonObject) + "\n"); }
}
이 소스는 사실 구글링을 통해서 github 에서 퍼온 자료인데, 한국어 번역만 구글 번역을 통해서 해 보았다.
이 소스에서 봐야할 부분은
- 서버인증을 위한 부분 인데... 여기서 중요한 것은 assetManager 의 자원을 가져오는 것이다. fcmdemo.json 이라고 적었는데, 그것은 firebase 의 프로젝트 설정에서 얻어온다. 이것을 얻어와서 저장을 하는 것은 FCM 전송을 위한 서버 구현을 하기 위해서는 FCM 에 인증을 받아야 하는데, 개발가이드를 보면 서버에 보내고 받고 하는 과정이 있어야 한다고 되어 있기도 하지만, 아래 함수를 이용해서 token 을 받아오면 서버 인증절차가 한번에 해소가 되는 것을 확인하였다.
public String getAccessToken(Context context) throws IOException {
내 앱을 사용하기 위해서 구성한 firebase 의 프로젝트 설정에 보면 서비스 계정 이라고 있는데, 그곳에 보면 새 비공개키 생성이라고 버튼이 있다. 그 버튼을 클릭하면 기~인 이름의 json 파일 하나를 만들어 준다. 그럼 그것을 이름을 줄여서 저장하고.
firebase 프로젝트 설정
내 앱 프로젝트에 assets 으로 넣어준다. 그런데, 여기서 하나 걸리는 부분이 그냥 넣어주고 빌드해 실행해 보면 오류가 발생한다.
실행시 오류
json 파일이 들어 있는데, 찾을 수 없다는 것이다. 왜 ? 빌드를 하면 압축(?)을 하나 보다. 그래서 json 파일을 제대로 읽어서 처리를 할 수 없는 것이다. 그래서 앱의 gradle 설정에 다음과 같이 추가 하였다.
gradle (module)
resources 는 noCompress 'json' 이라고... 압축을 하지 말라는 옵션이러니...
- 그 다음은 상수로 선언된 project-id : 이것은 나의 프로젝트 일반에 있는 것을 가져다 적는다.
/** * project_id 는 firebase 에 등록한 나의 project ID */ private static final String PROJECT_ID = "my-application-f80fb";
프로젝트 일반
그리고 인증을 하기 위해서는 HttpURLConnection 을 이용하고 있기 때문이기도 하지만, manifest 에 user-permission 을 설정해 주는 것도 있지는 말아야겠다.
internet permission 설정
다음은 메세지를 보내는 함수 부분인데, 난 android 로만 메시지를 전송할 것이라서 buildAndroidOverridePayload함수만 사용한다. 그리고 그안에서 전달하고자 하는 파라미터등을 설정한다. body, title, image, URL, BODY 을 key로 설정해서 전달하고자 하는 값을 넣어 준다. 값을 global 변수를 이용해서 넣을 수 도 있고... 뭐 암튼...
/** FCM 요청의 본문을 작성합니다. 이 본문은 공통 알림 개체를 정의합니다. android 및 apns 개체를 사용하는 플랫폼별 사용자 정의도 가능합니다. * * @return JSON representation of the FCM request body. */ public JsonObject buildOverrideMessage() { JsonObject jNotificationMessage = buildNotificationMessage();
/** * Android에서 메시지가 수신되는 방식을 사용자 지정하는 Android 페이로드를 빌드합니다. * * @return android payload of an FCM request. */ public JsonObject buildAndroidOverridePayload() { /** * 이부분은 notify 알림에 표시될 제목, 내용, 보여줄 이미지의 URL 경로 */ JsonObject androidNotification = new JsonObject(); androidNotification.addProperty("body", BODY); androidNotification.addProperty("title", TITLE); androidNotification.addProperty("image", IMAGEURL);
/** * 사용자 viewAcitivy 로 전달할 내용 과 URL * * FcmReceiveService 에서 URL, BODY 을 읽어서 ViewActivity 로 전달함. */ JsonObject data = new JsonObject(); data.addProperty("URL", URL); data.addProperty("BODY", BODY);
JsonObject androidNotificationPayload = new JsonObject(); androidNotificationPayload.add("notification", androidNotification); androidNotificationPayload.add("data", data);
return androidNotificationPayload; }
그럼 메시지 수신을 하는 FcmReceiveService 을 잠깐 살펴 보자. onMessageReceived 부분을 보면 payload 에서 들어간 내용 URL, BODY 이런 것들이 키로 수신이 되는 것을 볼 수 있다. 그래서 그것을 저정하거나 해서 잘 활용하면
// 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]
Log.d(TAG, "Short lived task is done."); Intent intent = new Intent(FcmReceiveService.this, ViewActivity.class); intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); startActivity(intent);
}
/** 타사 서버에 토큰을 유지합니다.
이 방법을 수정하여 사용자의 FCM 등록 토큰을 임의의 애플리케이션에서 유지 관리하는 서버 측 계정. * * @param token The new token. */ private void sendRegistrationToServer(String token) { // TODO: Implement this method to send token to your app server. }
/** * 수신된 FCM 메시지가 포함된 간단한 알림을 만들고 표시합니다. * * @param messageBody FCM message body received. */ private void sendNotification(String msgTitle, String messageBody) {
수신된 알림이 notification 으로 저장이 되는 경우, 사용자는 알림창에 나와 있는 알림을 클릭했을 때 내 앱이 실행 되면서 그 내용을 확인할 수 있을 것이다. 알려야 하는 내용을 전달하는 방법은 메시지만 전달하거나, 미리 전달할 내용을 realtimedatabase 등에 저장해 두었다가, 확인해 볼 수 있도록 앱을 구성해 볼 수 있을 것 같다.
알림 수신을 위한 FcmReceiveService는 manifest 에 service 로 등록해 주면 된다. 이런 부분들은 구글링을 해 보면 많이 나오고 해서 추가 설명은 생략해 보겠다.
Intent intent = getIntent(); Bundle extras = intent.getExtras(); # FcmreceiveService 에서 extra 로 전달하면 그 값을 확인하기 위해서 추가 if (extras != null) { Log.e(TAG, "===" + extras.getString("BODY"));
..... }
# 안드로이드 버전이후 에서는 channel ID 을 설정해 주어야 한다. if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { // Create channel to show notifications. String channelId = getString(R.string.default_notification_channel_id); String channelName = getString(R.string.default_notification_channel_name); NotificationManager notificationManager = getSystemService(NotificationManager.class); notificationManager.createNotificationChannel(new NotificationChannel(channelId, channelName, NotificationManager.IMPORTANCE_LOW)); } }
binding.btnSend.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View view) { if (sp.getBoolean("pushOne", false) && getChkCnt(userInfos) < 1) { Toast.makeText(getApplicationContext(), getString(R.string.msgCheckOnlyOne), Toast.LENGTH_LONG).show(); return; } pushBinding = PushentryBinding.inflate(getLayoutInflater()); pushBinding.edURL.setText("billcorea.tistory.com"); pushBinding.edMesg.setText("test send messages "); AlertDialog.Builder builder = new AlertDialog.Builder(MainActivity.this, R.style.myDialog); builder.setTitle(getString(R.string.pushTitle)) .setMessage(getString(R.string.msgPushMessages)) .setView(pushBinding.getRoot()) .setPositiveButton(getString(R.string.OK), new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) {
# 메시지 전송을 위한 호출 SendNotify(pushBinding.edMesg.getText().toString(), pushBinding.edURL.getText().toString()) ; } }) .setNegativeButton(getString(R.string.CANCEL), null); AlertDialog dialog = builder.create(); dialog.show(); } }); }
/** * 전송할 메시지와 url 을 입력 받아옴. fragment 에서 개인/전체 발송 구분은 선택해서 sp 에 저장 되어 있다고 봄 * @param msg * @param url */ private void SendNotify(String msg, String url) {
# 쓰레드을 사용하는 건 http 전송을 하기 때문에... new Thread(() -> {
Messaging messaging = new Messaging(getApplicationContext());
# 전송하고 싶은 값들을 여기서 선언해 준다. # 추가가 필요하면 Message class 에 선언해 주면서 추가 하면됨. messaging.TITLE = getString(R.string.app_name) ; messaging.BODY = msg ; messaging.URL = url ;
FirebaseMessaging.getInstance().getToken() .addOnCompleteListener(new OnCompleteListener<String>() { @Override public void onComplete(@NonNull Task<String> task) { if (!task.isSuccessful()) { Log.w(TAG, "Fetching FCM registration token failed", task.getException()); return; } // Get new FCM registration token String token = task.getResult(); getRegistryPhoneNumber(token); } }); }
}
처음에 보았던 client 토큰 값을 이용해서 개별에게 전달을 하거나, 구독 설정한 topic 을 통해서 전체 전달을 할 수 있다.
여기서 중요한 것은 개별 전송을 위한 token 을 메시지를 보내는 쪽에서 알아야 한다는 것인데, 그걸 해소하기 위해서는 추천하는 방법으로는 realtime database 에 저장을 해서 공유하는 방법이 실시간으로 데이터를 공유할 수 있지 않을 까 생각이 든다. 관리자는 모든 사용자의 token 값을 알고 있어야 하고, 서로 모르는 상대방에게 메시지를 보낸다면 특정인의 token을 알 수 있는 방법이 있어야 하기 떄문이다 .