2026/02/19

오늘의 이야기

안드로이드 개발자 문서에서는 아래 링크와 같이 예시를 볼 수 있다.  오늘은 그중에서 그냥 쉽게 EditBox 에서 입력한 내용을 클립보드에 저장해서 이용하는 예제를 만들어 보겠다. 


 


https://developer.android.com/guide/topics/text/copy-paste?hl=ko#java 



 


복사하여 붙여넣기  |  Android 개발자  |  Android Developers


복사하여 붙여넣기 Android는 복사하여 붙여넣기를 지원하는 강력한 클립보드 기반 프레임워크를 제공합니다. 텍스트 문자열, 복잡한 데이터 구조, 텍스트 및 바이너리 스트림 데이터, 애플리케


developer.android.com




 


먼저 내가 만드는 앱에서 이용할 부분을 찾아보자.  




자주 사용할 수 있는 부분이 Editbox 가 될 것 같다.


예을 들어 오른쪽 그림과 같은 예시 화면이 있다면


 


제목 에 입력된 내용을 클립보드에 넣었다가


내용에 붙여넣는 것을 해 보기로 하겠다.


 


 


 


 


 


 


 


 


 


import android.content.ClipData;
import android.content.ClipboardManager;
import android.content.Context;
import android.os.Bundle;

...

/**
* 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
}

...

@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
...

clipboard = (ClipboardManager) getActivity().getSystemService(Context.CLIPBOARD_SERVICE);

...

binding.edTitle.setOnLongClickListener(new View.OnLongClickListener() {
@Override
public boolean onLongClick(View view) {
clipData = ClipData.newPlainText("title", binding.edTitle.getText().toString());
clipboard.setPrimaryClip(clipData);
Toast.makeText(getContext(), getString(R.string.msgClipboardCutText), Toast.LENGTH_SHORT).show();
return true;
}
});
return binding.getRoot();
}

...
}

내용은 간단하다... Clipboard 와 Clipdata 을 선언하고 clipbaord 을 구현했다.  그리고 화면에 있는 editbox 의 longclick 이벤트을 이용해서 editbox 에 입력된 텍스트를 클릭보드에 담아내는 것 까지만 구현을 했다. 


 


붙여넣기는 붙여넣을 곳에 가서 마찬가지로 long click 을 하면 붙여넣기 메뉴가 안드로이드에서 표시를 해 주니 그것을 이용하면 된다 


 





실행되는 모습은 위 동영상과 같이 제목에 글자를 입력하고 long click 을 해서 클립보드에 붙여진 것을 확인후 


내용에 가서 long click 으로 붙여 넣기를 해 보는 것이다.   


 


 


 





오늘의 이야기


#billcorea #운동동아리관리앱
🏸 Schneedle,羽毛球俱乐部必备应用!
👉 比洞赛 – 记录分数并寻找对手 🎉
适合任何地方,独自一人、与朋友一起或在俱乐部! 🤝
如果你喜欢羽毛球,一定要尝试一下

前往应用程序👉 https://play.google.com/store/apps/details?id=com.billcorea.matchplay




오늘의 이야기

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 ;

public ChatBotAdapter(ArrayList<ChatMsgModel> items, String userEmail) {
this.chatMsgModels = items;
this.userEmail = 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

public ViewHolder(View view) {
super(view);

userid_tv = view.findViewById(R.id.userid_tv);
date_tv = view.findViewById(R.id.date_tv);
content_tv = view.findViewById(R.id.content_tv);

...


// 2021.11.01 item 클릭 처리를 위해서 추가
itemView.setOnClickListener(new View.OnClickListener() {
@Override public void onClick(View v) {
int pos = getAdapterPosition() ;
if (pos != RecyclerView.NO_POSITION) {
// 리스너 객체의 메서드 호출.
if (mListener != null) {
mListener.onItemClick(v, pos) ;
}
}
}
});
}
}

// 2021.11.01 리스너 객체 참조를 저장하는 변수
private ChatBotAdapter.OnItemClickListener mListener = null ;

// OnItemClickListener 리스너 객체 참조를 어댑터에 전달하는 메서드
public void setOnItemClickListener(ChatBotAdapter.OnItemClickListener listener) {
this.mListener = listener ;
}

public interface OnItemClickListener {
void onItemClick(View v, int position) ;
}
}

이렇게 adapter class 에서 OnItemClickListener 을 구현하고 그것을 호출해서 사용하면 되게 되는 것이다. 


생각해 보니 이전에도 기술했던 내용인 것 같기는 한데... 뭐 어쩌랴... ㅋㅋㅋ 기억은 항상 되새김질을 해야 하는 것이고, 오랜만에 보게 되면 새로 보는 느낌이 든다. 


 


 


이제 구현된 것을 활용하는 소스를 구현해 보자.


     ...

ChatBotAdapter adapter ;

...

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
binding = ActivityChatRoomBinding.inflate(getLayoutInflater());
setContentView(binding.getRoot());

...

adapter = new ChatAdapter(chatMsgModels, userEmail);
binding.rv.setLayoutManager(new LinearLayoutManager(ChatRoomActivity.this));
binding.rv.setAdapter(adapter);

...

adapter.setOnItemClickListener(new ChatAdapter.OnItemClickListener() {
@Override
public void onItemClick(View v, int position) {

...

}
});

Log.e(TAG, "onCreate -----------------------------------------") ;
}

이렇게 MainActivity 의 onCreate 에서 구현을 해 보면 ListView 에서는 해당 ListView.setOnClickListener 을 활용했다고 하면, RecycleView  에서는 RecycleView.Adapter 을 이용해서 adapter.setOnItemClickListener 을 활용하는 방법이 될 수 있다고 생각이 든다. 


 


그럼... 이번에도 즐~ 코딩 하시길...





오늘의 이야기

Recycleview 을 사용하다보니, RecycleAdapter 을 구성해서 화면을 구현 하게 된다.  당연한...


잠깐 소스를 볼까 ?


 


<잘못된 예시>


    @Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
binding = ActivityChatRoomBinding.inflate(getLayoutInflater());
setContentView(binding.getRoot());

setSupportActionBar(binding.mytoolBar);

sp = getSharedPreferences(getPackageName(), MODE_PRIVATE);

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 등을 구현하게 되었을 때, 혼돈의 구렁텅이에 빠지게 될 것이다. 


 


<올바른 예시>


    @Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
binding = ActivityChatRoomBinding.inflate(getLayoutInflater());
setContentView(binding.getRoot());

setSupportActionBar(binding.mytoolBar);

sp = getSharedPreferences(getPackageName(), MODE_PRIVATE);

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




2026/02/18

오늘의 이야기



보호되어 있는 글입니다.
내용을 보시려면 비밀번호를 입력하세요.













오늘의 이야기



#스치니1000프로젝트 #재미 #행운기원 #Compose #Firebase

🎯 야 너 토요일마다 로또 확인하냐?
나도 맨날 "혹시나~" 하면서 봤거든 ㅋㅋ

근데 이제는 그냥 안 해
AI한테 맡겼어 🤖✨

그것도 구글 Gemini로다가!

그래서 앱 하나 만들었지
👉 "로또 예상번호 by Gemini" 🎱

AI가 분석해서 번호 딱! 뽑아줌
그냥 보고 참고만 하면 됨

재미로 해도 좋고…
혹시 모르는 거잖아? 😏


https://play.google.com/store/apps/details?id=com.billcorea.gptlotto1127




오늘의 이야기

Fcm 으로 메시지를 수신하는 예제들은 많이 찾아 볼 수 있으나, 보내는 건 ? 그것도 안드로이드 앱으로 그런 예제는 없는 것 같아서 정리를 해 보겠다. 다만, 전체를 다 정리하는 것이 아니라 꼭 필요한 부분만...


 



  1. MainActivity 에 아래 함수를 넣고 앱이 실행 되는 동안에 처리를 하자.   


   - 목적은 allDevices 라는 것은 나중에 메시지 전송을 할 때 사용할 Topic 이다.  


     subscribeToTopoc 을 이용해서 내가 구동하는 메시지중에서 해당 Topic 으로 전송되는 것을 구독(?)할 수 있도록 등록을 해 두는 것이다. 


   - 두번째 목적은 getToken 함수를 이용해서 특정앱에게만 메시지를 보내고자 할 떄 token 값으로 구분 하여 메시지 수신자를 지정하기 위함이다.


    public void onRegistryToken() {

FirebaseMessaging.getInstance().subscribeToTopic("allDevices")
.addOnCompleteListener(new OnCompleteListener<Void>() {
@Override
public void onComplete(@NonNull Task<Void> task) {
Log.e(TAG, "allDevices subscribed ...");
}
})
.addOnFailureListener(new OnFailureListener() {
@Override
public void onFailure(@NonNull Exception e) {
Toast.makeText(getApplicationContext(), getString(R.string.nonSyncTopic), Toast.LENGTH_LONG).show();
}
});

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);
}
});
}

 


2. 다음은 Message 전송을 위한 함수 코드을 만들어 두는 것이다. 


   - 아래는 전체 코드이고 package 이름만 숨김했다.


package com.bill..............tils;

import android.content.Context;
import android.content.SharedPreferences;
import android.content.res.AssetFileDescriptor;
import android.content.res.AssetManager;
import android.util.Log;

import com.google.auth.oauth2.GoogleCredentials;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.JsonObject;

import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStreamWriter;
import java.net.HttpURLConnection;
import java.net.URL;
import java.util.Arrays;
import java.util.Scanner;

/**
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";

Context context ;
SharedPreferences sp ;

public Messaging(Context context) {
sp = context.getSharedPreferences(context.getPackageName(), Context.MODE_PRIVATE) ;
this.context = context ;
}

/**
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 {

AssetManager assetManager = context.getAssets();
AssetFileDescriptor fileDescriptor = assetManager.openFd("fcmdemo.json");
FileInputStream fileInputStream = fileDescriptor.createInputStream();

GoogleCredentials googleCredentials = GoogleCredentials
.fromStream(fileInputStream)
.createScoped(Arrays.asList(SCOPES));
return googleCredentials.refreshAccessToken().getTokenValue();
}
// [END retrieve_access_token]

/**
* 검색 및 게시 모두에 사용할 수 있는 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();

JsonObject messagePayload = jNotificationMessage.get(MESSAGE_KEY).getAsJsonObject();
messagePayload.add("android", buildAndroidOverridePayload());

jNotificationMessage.add(MESSAGE_KEY, messagePayload);

return jNotificationMessage;
}

/**
* 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");

return apnsHeaders;
}

/**
전송되는 메시지에 배지 필드를 추가할 앱 페이로드를 빌드합니다.
iOS 기기.
*
* @return JSON object with aps payload defined.
*/
public JsonObject buildApsOverridePayload() {
JsonObject badgePayload = new JsonObject();
badgePayload.addProperty("badge", 1);

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 {

AssetManager assetManager = context.getAssets();
AssetFileDescriptor fileDescriptor = assetManager.openFd("fcmdemo.json");
FileInputStream fileInputStream = fileDescriptor.createInputStream();

GoogleCredentials googleCredentials = GoogleCredentials
.fromStream(fileInputStream)
.createScoped(Arrays.asList(SCOPES));
return googleCredentials.refreshAccessToken().getTokenValue();
}

내 앱을 사용하기 위해서 구성한 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();

JsonObject messagePayload = jNotificationMessage.get(MESSAGE_KEY).getAsJsonObject();
messagePayload.add("android", buildAndroidOverridePayload());

jNotificationMessage.add(MESSAGE_KEY, messagePayload);

return jNotificationMessage;
}

/**
* 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 이런 것들이 키로 수신이 되는 것을 볼 수 있다.  그래서 그것을 저정하거나 해서 잘 활용하면 


수신된 메시지를 나의 앱으로 전달하는 방법으로 처리를 할 수 있을 것이다.


package com.bil.......................tils;

import android.app.NotificationChannel;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.graphics.Color;
import android.media.RingtoneManager;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.util.Log;

import androidx.core.app.NotificationCompat;
import androidx.core.app.NotificationManagerCompat;

import com.billcoreatech.msgfcm1020.MainActivity;
import com.billcoreatech.msgfcm1020.R;
import com.billcoreatech.msgfcm1020.ViewActivity;
import com.google.firebase.messaging.FirebaseMessagingService;
import com.google.firebase.messaging.RemoteMessage;

import java.util.Map;

public class FcmReceiveService extends FirebaseMessagingService {

private static final String TAG = "FcmReceiveService";
SharedPreferences sp ;
SharedPreferences.Editor editor ;

@Override
public void onMessageReceived(RemoteMessage remoteMessage) {

// TODO(developer): Handle FCM messages here.
// 여기에 메시지가 수신되지 않습니까? 이것이 가능한 이유를 참조하십시오 : https://goo.gl/39bRNJ
Log.e(TAG, "From: " + remoteMessage.getFrom());

// 메시지에 데이터 페이로드가 포함되어 있는지 확인하십시오.
if (remoteMessage.getData().size() > 0) {
Log.e(TAG, "Message data payload: " + remoteMessage.getData());

sp = getSharedPreferences(getPackageName(), MODE_PRIVATE);
editor = sp.edit() ;

Map<String, String> strMap = remoteMessage.getData();
Log.e(TAG, "URL=" + strMap.get("URL"));
Log.e(TAG, "BODY=" + strMap.get("BODY"));

editor.putString("URL", strMap.get("URL").contains("http") ? strMap.get("URL") : "https://" + strMap.get("URL"));
editor.putString("BODY", strMap.get("BODY"));
editor.putBoolean("FcmTy", true) ;
editor.commit();

handleNow();

}

// 메시지에 알림 페이로드가 포함되어 있는지 확인합니다.
if (remoteMessage.getNotification() != null) {
Log.e(TAG, "Message Notification Body: " + remoteMessage.getNotification().getBody());
sendNotification(remoteMessage.getNotification().getTitle(), remoteMessage.getNotification().getBody()) ;
}

onDeletedMessages();
}
// [END receive_message]


@Override
public void onNewToken(String token) {
Log.e(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]

/**
* BroadcastReceivers에 할당된 시간을 처리합니다.
*/
private void handleNow() {

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) {

sp = getSharedPreferences(getPackageName(), MODE_PRIVATE);
editor = sp.edit() ;
editor.putString("BODY", messageBody);
editor.putBoolean("FcmTy", true) ;
editor.commit();

Log.e(TAG, "---------------------------------------------------------------------");
Bundle bundle = new Bundle();
bundle.putString("TITLE", msgTitle);
bundle.putString("BODY", messageBody);

Intent notifyIntent = new Intent(this, MainActivity.class);
notifyIntent.putExtras(bundle);
// Set the Activity to start in a new, empty task
notifyIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK
| Intent.FLAG_ACTIVITY_CLEAR_TASK);
PendingIntent notifyPendingIntent = PendingIntent.getActivity(
this, 0, notifyIntent, PendingIntent.FLAG_UPDATE_CURRENT
);
String channelId = getString(R.string.default_notification_channel_id);
CharSequence channelName = getString(R.string.default_notification_channel_name);
Uri defaultSoundUri = RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION);

NotificationCompat.Builder builder = new NotificationCompat.Builder(this, channelId);
builder.setContentIntent(notifyPendingIntent)
.setSmallIcon(R.drawable.ic_launcher_foreground)
.setContentTitle(getString(R.string.fcm_message))
.setContentText(messageBody)
.setAutoCancel(true)
.setSound(defaultSoundUri);

NotificationManagerCompat notificationManager = NotificationManagerCompat.from(this);

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
NotificationChannel channel = new NotificationChannel(channelId,
"Channel human readable title",
NotificationManager.IMPORTANCE_HIGH);
channel.enableLights(true);
channel.enableVibration(true);
channel.setLightColor(Color.BLUE);
channel.setVibrationPattern(new long[]{100, 200, 300, 400, 500, 400, 300, 200, 400});
notificationManager.createNotificationChannel(channel);
}

notificationManager.notify(Integer.parseInt(getString(R.string.default_notification_channel_id)), builder.build());

}

}

 


수신된 알림이 notification 으로 저장이 되는 경우, 사용자는 알림창에 나와 있는 알림을 클릭했을 때 내 앱이 실행 되면서 그 내용을 확인할 수 있을 것이다.  알려야 하는 내용을 전달하는 방법은 메시지만 전달하거나, 미리 전달할 내용을 realtimedatabase 등에 저장해 두었다가, 확인해 볼 수 있도록 앱을 구성해 볼 수 있을 것 같다. 


 


알림 수신을 위한 FcmReceiveService는 manifest 에 service 로 등록해 주면 된다. 이런 부분들은 구글링을 해 보면 많이 나오고 해서 추가 설명은 생략해 보겠다.


        <meta-data
android:name="com.google.firebase.messaging.default_notification_icon"
android:resource="@drawable/ic_launcher_foreground" />
<meta-data
android:name="com.google.firebase.messaging.default_notification_color"
android:resource="@color/purple_500" />
<meta-data
android:name="com.google.firebase.messaging.default_notification_channel_id"
android:value="@string/default_notification_channel_id" />

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

 


3.  MainActivity 에서 메시지 전송을 구현해 보자...


    아래 소스 처럼 필요한 부분만 살펴 보도록 하겠다.


package co.......................020;

import android.Manifest;
import android.annotation.SuppressLint;
import android.app.NotificationChannel;
import android.app.NotificationManager;
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.SharedPreferences;
import android.content.pm.PackageManager;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.provider.Settings;
import android.telephony.TelephonyManager;
import android.util.Log;
import android.view.View;
import android.widget.AdapterView;
import android.widget.CompoundButton;
import android.widget.Toast;

import androidx.annotation.NonNull;
import androidx.appcompat.app.AlertDialog;
........


import java.io.IOException;
import java.util.ArrayList;
import java.util.Set;

public class MainActivity extends AppCompatActivity {

private static final String TAG = "MainActivity";

......


String strFCM = "" ;

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
binding = ActivityMainBinding.inflate(getLayoutInflater());
setContentView(binding.getRoot());

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));
}
}

@Override
protected void onStart() {
super.onStart();

...

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 ;

Log.e(TAG, "Send Messages " + msg);
Log.e(TAG, "pushOne=" + sp.getBoolean("pushOne", false));
Log.e(TAG, "pushAll=" + sp.getBoolean("pushAll", false));
Log.e(TAG, "userInfo=" + userInfo.getUserToken());

if (sp.getBoolean("pushOne", false)) {
if (userInfo == null) {
Toast.makeText(getApplicationContext(), getString(R.string.nonSelectUser), Toast.LENGTH_LONG).show();
return ;
}

if (!userInfo.isUseTy()) {
Toast.makeText(getApplicationContext(), getString(R.string.nonSelectUser), Toast.LENGTH_LONG).show();
return ;
}

/**
* 개인에게 만 전송할 떄 : 미리 수집된 token 값으로 전달하고
*/
messaging.tokenExam = userInfo.getUserToken() ;
try {
messaging.sendOverrideMessage();
} catch (IOException e) {
e.printStackTrace();
}
} else {

/**
* 전체에게 전송할 떄 : 앱 실행시 구독설정한 topic 으로 전송을 하게 됨
*/
try {
messaging.sendOverrideMessage();
} catch (IOException e) {
e.printStackTrace();
}
}

}).start();

}

# 토근 얻어오기 및 구독 설정 하기
public void onRegistryToken() {

FirebaseMessaging.getInstance().subscribeToTopic("allDevices")
.addOnCompleteListener(new OnCompleteListener<Void>() {
@Override
public void onComplete(@NonNull Task<Void> task) {
Log.e(TAG, "allDevices subscribed ...");
}
})
.addOnFailureListener(new OnFailureListener() {
@Override
public void onFailure(@NonNull Exception e) {
Toast.makeText(getApplicationContext(), getString(R.string.nonSyncTopic), Toast.LENGTH_LONG).show();
}
});

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을 알 수 있는 방법이 있어야 하기 떄문이다 .


 


 


p.s : 2021.11.15 오늘 공개할 python 이야기도 잠시 참고해 보시길.


 


https://billcoreapython.tistory.com/29


 





오늘의 이야기

앱을 만들면서 사용자 인증을 하는 경우 여러 가지의 인증 기능을 이용할 수 있을 것 같다.  그러다가 오늘의 이야기를 하게 된 이유는 


 


https://stackoverflow.com/questions/47619229/google-sign-in-failed-com-google-android-gms-common-api-apiexception-10



 


Google sign in failed com.google.android.gms.common.api.ApiException: 10:


So I'm Stuck on this frustrating issue. I am quite new to Google Auth on Firebase but I done everything the firebase docs instructed in how to integrate the Google SignIn Auth, yet I'm still receiv...


stackoverflow.com




 


이런 것과 같이 APIException 10 에러를 만났다는 것 때문이다. 이런 경우에는 어떻게 할 것인가?


 


https://firebase.google.com/docs/auth/android/account-linking?hl=ko 



 


Android에서 계정에 여러 인증 제공업체 연결하기  |  Firebase


Google은 흑인 공동체를 위한 인종적 평등을 추구하기 위해 노력하고 있습니다. 자세히 알아보기 의견 보내기 Android에서 계정에 여러 인증 제공업체 연결하기 인증 제공업체의 사용자 인증 정보


firebase.google.com




 


가이드 북이 말해 주는 것은 여러 가지 인증을 할 수 있다고 되어 있지만, 실제 만들고 있는 앱에서 오류가 발생하고 있다면... 난 어떻게 해결을 해야 하는 가?


 


https://stackoverflow.com/questions/54557479/flutter-and-google-sign-in-plugin-platformexceptionsign-in-failed-com-google/58644909#58644909



 


Flutter and google_sign_in plugin: PlatformException(sign_in_failed, com.google.android.gms.common.api.ApiException: 10: , null)


The dialog (Google form) for the credentials is opened successfully, but after I fill my credentials I'm getting this error. I followed the instructions from here. Created a Firebase project, enabl...


stackoverflow.com




 


이런저런 설정에 대한 이야기들이 나와 있다. 


 


google signin 오류



이렇게 API Exception 12500 이 나오는 경우가 있는데, 이런 경우는 firebase의 콘솔에서 확인을 해 볼 필요가 있다.


 


firebase authentication 설정



이렇게 gmail의 사용 설정이 되어 있지 않는 경우인 것으로 보인다.


그럼 새 제공업체 추가를 클릭해서 설정을 추가해 보자.


 


sns 로그인 설정



이렇게 여러 가지 sns 계정을 활용할 수 있으니 필요에 따라 설정을 해 볼 필요가 있겠다.


난 구글을 이용할 거니까 구글을 클릭한다.


 


구글 설정 화면



사용 설정을 클릭하면 다음과 같이 프로젝트 이름이 나오고 프로젝트 지원 이메일 설정을 하도록 나온다. 지원 이메일은 내가 사용하는 gmail 계정을 입력하면 된다.


 


sns 로그인 설정



저장을 클릭하고 나면 다음과 같이 google-services.json 을 내려받을 수 있도록 나온다. 새로 받고, 나의 project에 적용해서 rebuild을 해 보아야겠다.


 


google 로그인 로그



 


 


로그인된 앱 화면



 


 


오호라... 이제 로그인된다. ㅎㅎㅎ


 


firebase의 콘솔에서 나의 프로젝트를 들어가서 추가한 앱을 살펴보았다. 


 




 


여기도 잘 되어 있는 것 같다.   이것으로 끝인가?


 


글쎄... 아직은 release 하지 않아서 모르지만... 이런 경우 대부분 release을 하게 되면 또 같은 문제를 마주하게 될 것이다. 그때는 당황하지 말고 sha 인증서 지문을 찾아서 저 앱 설정 부분에 추가해 주도록 하자.


 


인증서 지문이 sha1, sha256으로 나누어지는 것도 있고, debug와 release로 구분되어 있다.  잊지 말자.


 





오늘의 이야기


#스하리1000명프로젝트

스치니들!
내가 만든 이 앱은, 내 폰에 오는 알림 중에서 중요한 키워드가 있는 경우
등록해둔 친구에게 자동으로 전달해주는 앱이야 📲

예를 들어, 카드 결제 알림을 와이프나 자녀에게 보내주거나
이번 달 지출을 달력처럼 확인할 수도 있어!

앱을 함께 쓰려면 친구도 설치 & 로그인해줘야 해.
그래야 친구 목록에서 서로 선택할 수 있으니까~
서로 써보고 불편한 점 있으면 알려줘 🙏

👉 https://play.google.com/store/apps/details?id=com.nari.notify2kakao





오늘의 이야기

bottomnavigationview 을 구현하다가 클릭을 했을 때 버튼이 적용 되지 않는 상황이 발생 했다. 왜 ? 무엇 떄문에 이런 상황이 벌어지는 것인가 ? 


 


bottomnavigationview



원래 메뉴를 클릭할 때 마다 선택한 메뉴가 옮겨져 가야 하는 것인데... 도대체 왜 ?


 


        bottomNavigationView.setOnNavigationItemSelectedListener(new BottomNavigationView.OnNavigationItemSelectedListener()
{
@Override
public boolean onNavigationItemSelected(@NonNull MenuItem item) {
Log.e(TAG, "onNavigationItemSelected=" + item.getItemId()) ;
switch (item.getItemId()) {
case R.id.menu_search:
Log.d(TAG, "onNavigationItemSelected") ;
getSupportFragmentManager().beginTransaction()
.replace(R.id.main_layout, boardSelectFragment).commitNow() ;
return true ;
case R.id.menu_chatlist:
getSupportFragmentManager().beginTransaction()
.replace(R.id.main_layout, chatListFragment).commitNow() ;
return true ;
case R.id.menu_mypage:
getSupportFragmentManager().beginTransaction()
.replace(R.id.main_layout, accountFragment).commitNow() ;
return true ;
default:
return false ;
}
}
});

 


원인은 딱 하나 해당 메뉴를 클릭했을 떄... return true ; 끝나야 하는 것인데... return false 로 끝을 내었더니만...


메뉴를 클릭해도 선택이 되지 않았던 것이다.


 


다시는 이런 실수를 하지 않기를 바라며...





오늘의 이야기

앱을 구현 하다 보면 알림을 보여 주어야 하는 경우가 있게 된다. 그럴때 마다 쉽게 했던 방식은 Toast 알림을 이용하는 방식으로 구현하는 것이였다.     Toast 알림 예시 Toast 의 경우의 위 영상에서 20초 이후 구간에 보이는 화면 하단...