Android Studio 에서 WebSocket을 이용하여 실시간 도난의심 알림이 오게 해보자

박해인·2023년 8월 23일
0

저번시간에 WebSocket을 이용한 실시간 서버 프로그래밍을 완료하였다.

👁️ websocket을 사용한 spirng 서버 개발하기

그렇다면 안드로이드 스튜디오에에서 실시간 알림을 받는 작업을 구현해보자.
사실 안드로이드 스튜디오에서 WebSocket으로 수신한 데이터를 출력하는것은 그리 어렵지 않다. OkHttp Library를 사용하면된다.

하지만 우리팀이 구현하고자하는 시스템은 "실시간"이다. 웹소켓만 구현했다고 실시간이 되는줄 알았지만.... 큰 오산이였다.

AndroidStudio의 생명주기를 살펴보자.

알다시피 안드로이드 스튜디오 어플을 실행하지 않으면, Activity launched가 성립이 되지 않는다. 그러니 백번 Activity에 OkHttp를 이용하여 데이터를 받아오려해도 Activity가 실행되지 않으니 말짱 도루묵인 셈이다..

그리고 OkHttp를 구현한 Activity를 launch 한다고 하더라도, 다른 Activity로 넘어가는 순간 onPause 상태가 되기 때문에 여전히 완전한 "실시간 통신"은 어렵다. 즉 WebSocket을 이용하는 의미가 없어진다.

그래서 생각해낸 방법은 안드로이드 컴포넌트중 하나인 Service를 이용하는것이다.

🫡 Service란?

백그라운드에서 오래 실행되는 작업을 수행할 수 있는 애플리케이션 구성 요소이며 사용자 인터페이스를 제공하지 않습니다. 다른 애플리케이션 구성 요소가 서비스를 시작할 수 있으며, 이는 사용자가 다른 애플리케이션으로 전환하더라도 백그라운드에서 계속해서 실행됩니다.

그렇다. 그래서 나는 Service 컴포넌트를 사용하기로 하였다.
하지만 Service종류에는 여러가지가 있는데, 어플에 실시간으로 알림을 주는 notification 기능을 줄것이기 때문에 Service중에서도 foreground Service를 이용하였다.

또 한가지 문제점이 있다. 나는 Fragment를 사용하고 있다.
아래는 실제로 내가 구현한 모습이다.
한 Activity내에 3개의 Fragment가 탭메뉴로 구성된것을 알 수 있다.

그러니까 이게 뭐가 문제냐면...
Service에서 WebSocket으로 도난 정보를 받아오고 notification 알람도 발생해야하고 또, 실시간으로 Fragment에게도 도난정보를 전달해줘야하고, 더불어 UI도 실시간으로 바뀌어야 한다.

그렇다면 Service와 Fragment가 통신할 수 있어야 한다는건데..
그래서 생각해낸 방법은 BroadcastReceiver 이다.

한눈에 보자면 아래와 같다.

MyService.java

package kr.ac.duksung.websocketexer;

import static androidx.constraintlayout.helper.widget.MotionEffect.TAG;
import android.app.Activity;
import android.app.Notification;
import android.app.NotificationChannel;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.app.Service;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.os.Build;
import android.os.Handler;
import android.os.IBinder;
import android.os.Looper;
import android.util.Log;
import android.widget.ArrayAdapter;
import android.widget.TextView;

import androidx.annotation.Nullable;
import androidx.core.app.NotificationCompat;
import androidx.core.app.NotificationCompat;
import androidx.localbroadcastmanager.content.LocalBroadcastManager;
import android.app.Notification;
import android.app.NotificationChannel;
import android.app.NotificationManager;

import org.greenrobot.eventbus.EventBus;

import java.util.ArrayList;

import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.Response;
import okhttp3.WebSocket;
import okhttp3.WebSocketListener;


public class MyService extends Service {
    //private OkHttpClient mWebSocketClient;
    private Handler mHandler;
    private Runnable mRunnable;


    private TextView mTextView;
    // Channel에 대한 id 생성 : Channel을 구부하기 위한 ID 이다.
    private static final String CHANNEL_ID = "duksungelec";
    // Channel을 생성 및 전달해 줄 수 있는 Manager 생성
    private NotificationManager mNotificationManager;

    private static final int NOTIFICATION_ID = 0;

    static ArrayList<String> stringArrayList = new ArrayList<>();

    private static final int NOTIFICATION_ID2 = 1;

    private static final String CHANNEL_ID2 = "ForegroundServiceChannel";

    public MyService() {
    }

    //Data를 Fragment로 보내는 method
    private void sendDataToFragment(String data) {
        Intent intent = new Intent("my-event");
        intent.putExtra("message", data);
        LocalBroadcastManager.getInstance(this).sendBroadcast(intent);
        Log.d("확인","전달 시작");
    }

    @Override
    public void onCreate() {
        super.onCreate();
        createNotificationChannel();
        Notification notification = buildNotification();
        //서비스를 Foreground 상태로 설정하고 알림 표시
        startForeground(NOTIFICATION_ID2, notification);
    }

/*
    @Override
    public IBinder onBind(Intent intent) {

        // TODO: Return the communication channel to the service.
        throw new UnsupportedOperationException("Not yet implemented");

    }*/


    @Override
    public int onStartCommand(Intent intent, int flags, int startId) {

        try {
            //예외 발생 예상 코드
            sendDataToFragment("hi");
        } catch (Exception e) {
            //예외 발생시 처리할 내용4
            Log.d("확인:","전달 실패");
        }

        mNotificationManager = (NotificationManager) getSystemService(NOTIFICATION_SERVICE);
        // 기기(device)의 SDK 버전 확인 ( SDK 26 버전 이상인지 - VERSION_CODES.O = 26)
        if(android.os.Build.VERSION.SDK_INT
                >= android.os.Build.VERSION_CODES.O){
            //Channel 정의 생성자( construct 이용 )
            NotificationChannel notificationChannel = new NotificationChannel(CHANNEL_ID,"Test Notification",mNotificationManager.IMPORTANCE_HIGH);
            //Channel에 대한 기본 설정
            notificationChannel.enableLights(true);
            notificationChannel.enableVibration(true);
            notificationChannel.setDescription("Notification from Mascot");
            // Manager을 이용하여 Channel 생성
            mNotificationManager.createNotificationChannel(notificationChannel);
        }

        Log.d("확인","onStartCommand()");

        // 백그라운드 스레드에서 WebSocket 연결
        OkHttpClient client = new OkHttpClient();
        Request request = new Request.Builder()
                .url("ws://125.128.219.177:8080/text")
                .build();



        WebSocket webSocket = client.newWebSocket(request, new WebSocketListener() {

            @Override
            public void onOpen(WebSocket webSocket, Response response) {
                Log.d("확인", "Received message: 연결성공");

            }

            @Override
            public void onMessage(WebSocket webSocket, String text) {

                buildNotification();



                //String data = "Hello from Service";

                Intent notificationIntent = new Intent(getApplicationContext(), MainActivity.class);
                PendingIntent notificationPendingIntent = PendingIntent.getActivity(getApplicationContext(), NOTIFICATION_ID, notificationIntent, PendingIntent.FLAG_CANCEL_CURRENT);
                // WebSocket으로부터 텍스트 메시지를 받았을 때 호출됩니다.
                super.onMessage(webSocket, text);

                NotificationCompat.Builder notifyBuilder = new NotificationCompat.Builder(getApplicationContext(), CHANNEL_ID)
                        .setContentTitle("도난의심")
                        .setContentText("삐용삐용")
                        .setSmallIcon(R.drawable.admin)
                        .setContentIntent(notificationPendingIntent) //추가된 부분
                        .setAutoCancel(true);
                //notification을 탭 했을경우 notification을 없앤다.//추가된 부분

                //notification을 탭 했을경우 notification을 없앤다.//notification을 탭 했을경우 notification을 없앤다.
                //notification을 탭 했을경우 notification을 없앤다

                mNotificationManager.notify(NOTIFICATION_ID, notifyBuilder.build());

                Intent intent = new Intent("my-event");
                //intent.putExtra("message", "전달할 데이터");
                // LocalBroadcastManager를 사용하여 Broadcast 전송
                LocalBroadcastManager.getInstance(getApplicationContext()).sendBroadcast(intent);
                stringArrayList.add(text);


                try{
                    intent.putStringArrayListExtra("message",stringArrayList);
                    Log.d("확인","전달 성공");
                }
                catch(Exception e){
                    Log.d("확인","전달 실패");
                }




            }

            @Override
            public void onClosed(WebSocket webSocket, int code, String reason) {

                Log.d("확인", "Received message: 연결끊김");

            }

            @Override
            public void onFailure(WebSocket webSocket, Throwable t, Response response) {
                System.out.println("연결실패");
                Log.e(TAG, "확인" + t.getMessage(), t);

            }
        });

       // return super.onStartCommand(intent, flags, startId);
        return START_STICKY;
    }

    @Override
    public void onDestroy() {
        Log.d("확인", "onDestroy()");
        stopForeground(true);
        super.onDestroy();
        //mHandler.removeCallbacks(mRunnable);
    }


    @Nullable
    @Override
    public IBinder onBind(Intent intent) {
        return null;
    }

    private void createNotificationChannel() {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
            NotificationChannel channel = new NotificationChannel(CHANNEL_ID,
                    "Foreground Service Channel",
                    NotificationManager.IMPORTANCE_DEFAULT);
            NotificationManager manager = getSystemService(NotificationManager.class);
            manager.createNotificationChannel(channel);
        }
    }

    private Notification buildNotification() {
        NotificationCompat.Builder builder = new NotificationCompat.Builder(this, CHANNEL_ID)
                .setContentTitle("Foreground Service")
                .setContentText("Service is running...")
                .setSmallIcon(R.drawable.admin);

        return builder.build();
    }



}

Service를 실행한
MainActivity.java

package kr.ac.duksung.websocketexer;



import androidx.annotation.RequiresApi;
import androidx.appcompat.app.AppCompatActivity;
import android.content.Intent;
import android.os.Build;
import android.os.Bundle;
import android.view.View;
import android.widget.ImageView;



public class MainActivity extends AppCompatActivity {


    Intent intent2;
    ImageView imageView;


    @RequiresApi(api = Build.VERSION_CODES.O)
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);


        setContentView(R.layout.activity_main);

        //textView = (TextView) findViewById(R.id.text);
        intent2 = new Intent(this.getApplicationContext(), MyService.class);

     
        //foregroundService 시작
        startForegroundService(intent2);

        imageView = (ImageView) findViewById(R.id.imageView);




        imageView.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                Intent intent = new Intent(getApplicationContext(),MainActivity2.class);
               

            }
        });

    }

마지막으로 실제 도난의심상황의 정보가 표시될 fragment의 코드이다.

PageTwoFragment.java

package kr.ac.duksung.websocketexer;



import android.os.Bundle;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.fragment.app.Fragment;

import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;



import android.widget.ArrayAdapter;
import android.widget.ListView;

import com.google.firebase.database.DataSnapshot;
import com.google.firebase.database.DatabaseError;
import com.google.firebase.database.DatabaseReference;
import com.google.firebase.database.FirebaseDatabase;
import com.google.firebase.database.ValueEventListener;

import java.util.ArrayList;

public class PageThreeFragment extends Fragment {
    ListView listView;
    ArrayList userList;
    ArrayAdapter adapter;
    DatabaseReference mDatabase;


        @Override
        public void onCreate(@Nullable Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);





        }



        @Override
        public void onDestroy() {
            super.onDestroy();
            // BroadcastReceiver를 해제합니다.

        }


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

        ViewGroup rootView = (ViewGroup)inflater.inflate(R.layout.fragment_page_three, container, false);

        listView = rootView.findViewById(R.id.listView);

        userList = new ArrayList<>();





        mDatabase = FirebaseDatabase.getInstance().getReference().child("users");
        ValueEventListener valueEventListener = new ValueEventListener() {
            @Override
            public void onDataChange(@NonNull DataSnapshot dataSnapshot) {

                userList.clear(); // 기존 리스트 초기화

                for (DataSnapshot childSnapshot : dataSnapshot.getChildren()) {
                    User newUser = childSnapshot.getValue(User.class);
                    Log.d("Firebase","data받기 시작");
                    if (newUser != null) {
                        String userNumber = newUser.getUserNumber();
                        Log.d("Firebase",userNumber);
                        String timestamp = newUser.getTimestamp();
                        String userInfo = "사용자: " + "0" + userNumber + "\n출입시간: " + timestamp;
                        userList.add(userInfo);

                    }
                    else{
                        Log.d("확인","null");
                    }
                }

                adapter.notifyDataSetChanged(); // 리스트뷰 갱신
            }

            // db에서 데이터를 읽어올때 예외 발생하면 처리
            @Override
            public void onCancelled(@NonNull DatabaseError databaseError) {
                Log.d("Firebase", "Failed to retrieve users: " + databaseError.getMessage());
            }
        };

        adapter = new ArrayAdapter<>(getContext(), android.R.layout.simple_list_item_1, userList);

        listView.setAdapter(adapter);

        mDatabase.addValueEventListener(valueEventListener);

        return rootView;
    }

    @Override
    public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstaceState){
        super.onViewCreated(view,savedInstaceState);


    }




}

실시간은.... 어려운거구나...

profile
갓생살고싶어라

0개의 댓글