ScrollView

안드로이드/Layout 2019. 12. 20. 21:57
728x90

Android Layout 을 작성하다보면 종종 ScrollView를 사용하는 경우가 있다. ScrollView는 길어지는 높이로 인해 화면에서 일부 View가 잘리는 현상을 상하 스크롤을 통해 해소할 수 있는 View이다.

 

주의할점: 스크롤뷰에는 단 하나의 자식 View만 포함되어야 한다!!
그래서, 여러개의 View를 넣으려면 스크롤뷰 안에 LinearLayout(
리니어레이아웃) 또는 RelativeLayout 등의 ViewGroup을 자식 View로 지정하고 그 안에 다양한 View를 넣는 구조를 택하여 해결할 수 있다.

 

<ScrollView
    android:id="@+id/scrollview"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:gravity ="center"
    android:fillViewport="true"
    android:layout_marginTop="0dp"
    android:background="#000000">
 
    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="vertical">
 
        <TextView
            android:id="@+id/info"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_marginLeft="10dp"
            android:gravity="left"
            android:textColor="#FFFFFF"/>
 
        <TextView
            android:id="@+id/consoleText"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_marginLeft="10dp"
            android:ellipsize="start"
            android:gravity="left"
            android:textColor="#FFFFFF"
            android:textStyle="bold" />
    </LinearLayout>
</ScrollView> 

 

아래 코드는 콘솔 앱에 사용된 코드 일부이다.

 

public class MainActivity extends AppCompatActivity {
    private static final String TAG = "MainActivity";
    Context mContext;
    TextView mTitleTextView;
    TextView mDumpTextView;
    ScrollView mScrollView;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        mContext = MainActivity.this;
        initView();
    }
    private void initView() {
        mTitleTextView = (TextView) findViewById(R.id.info);
        mDumpTextView = (TextView) findViewById(R.id.consoleText);
        mScrollView = findViewById(R.id.scrollview);
        mScrollView.setVerticalScrollBarEnabled(true); // 수직방향 스크롤바 사용 가능하도록 설정
        mDumpTextView.setMovementMethod(new ScrollingMovementMethod());
        mDumpTextView.setTextSize(Constants.mTextFontSize);
        mDumpTextView.setOnLongClickListener(new View.OnLongClickListener() {
            @Override
            public boolean onLongClick(View v) {
                mDumpTextView.setTextIsSelectable(true); // 복사 가능
                mDumpTextView.setCursorVisible(true);
                SpannableString highlightString = new SpannableString(mDumpTextView.getText());
                highlightString.setSpan(new BackgroundColorSpan(ContextCompat.getColor(mContext, R.color.mediumspringgreen))
                        , 0, mDumpTextView.getText().length(), Spanned.SPAN_INCLUSIVE_INCLUSIVE);
                return false;
            }
        });
        mDumpTextView.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                mDumpTextView.setTextIsSelectable(true); // 복사 가능
                mDumpTextView.setCursorVisible(false);
            }
        });
    }
    private void updateReceivedData(String data) {
        final String message = data;
        mDumpTextView.setTextColor(Color.WHITE);
        mDumpTextView.append(message);
        mDumpTextView.setTextIsSelectable(false); // 텍스트 클립보드 복사
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
            mDumpTextView.setElegantTextHeight(true);
        }
        mDumpTextView.setImeOptions(EditorInfo.IME_FLAG_NO_ENTER_ACTION);
        mDumpTextView.setInputType(InputType.TYPE_TEXT_FLAG_MULTI_LINE);
        mDumpTextView.setSingleLine(false);
        refreshView();
    }
    private void refreshView(){
        mScrollView.postDelayed(new Runnable() {
            @Override
            public void run() {
                mScrollView.fullScroll(ScrollView.FOCUS_DOWN);
            }
        },100);
        mScrollView.smoothScrollTo(0, mDumpTextView.getBottom());
    }

 

728x90

'안드로이드 > Layout' 카테고리의 다른 글

CardView Layout 예제  (0) 2020.04.12
Meterial Design 로그인 Layout 예제  (0) 2020.03.21
Dynamic Layouts  (0) 2019.01.06
Android Fragment 기본 예제  (0) 2018.09.11
FloatingActionButton(FAB)  (0) 2018.08.15
블로그 이미지

Link2Me

,
728x90

안드로이드 바인딩 서비스 개요

startService() 메소드 대신 bindService() 메소드를 통해 시작되는 서비스를 서비스 바인딩이라 한다.

바인딩 서비스는 Activity가 클라이언트 역할을 하고, 서비스가 서버 역할을 한다.

startService() 메소드 대신 bindService() 메소드를 통해 시작되는 서비스를 서비스 바인딩이라 한다.

이 서비스는 Activity가 클라이언트 역할을 하고, 서비스가 서버 역할을 한다.



출처: https://link2me.tistory.com/1343 [소소한 일상 및 업무TIP 다루기]

BindService는 startService()를 통해 시작되는 UnBound Service와는 다르게
Activity / Fragment와 서비스간에 데이터를 주고 받을 수 있으며, 프로세스간의 통신에도 사용된다.

백그라운드에서 무한히 실행되지 않도록 Activity 종료시 자동으로 서비스를 종료시킬 수 있도록 구현해야 한다.

서비스 바인딩은 연결된 Activity가 사라지면 서비스도 소멸된다.
하나의 서비스에 다수의 Activity 연결이 가능하다.
서비스에 연결된 Activity(컴포넌트)가 하나도 남아있지 않으면 서비스는 종료된다.

서비스 바인딩 및 구현 동작

연결을 유지하고 데이터를 전송 받기 위한 ServiceConnection() 객체와 IBinder 인터페이스 객체가 필요하다.

ServiceConnection() 는 Service를 호출하는 Activity 에 만들어야 하고, IBinder 는 Service 에서 생성한 후 리턴해야 한다.


startService()를 호출하여 서비스를 시작하고 이를 통해 서비스가 무한히 실행되도록 할 수 있으며, bindService()를 호출하면 클라이언트(Activity)가 해당 서비스에 바인딩되도록 할 수 있다는 의미다.


서비스가 시작되고 바인딩되도록 허용한 다음, 서비스가 실제로 시작되면 시스템은 Activity가 모두 바인딩을 해제해도 서비스를 소멸시키지 않는다. 그 대신 서비스를 직접 확실히 중단해야 한다. 그러려면 stopSelf() 또는 stopService()를 호출하면 된다.


보통은 onBind() 또는 onStartCommand() 중 한 가지만 구현하지만, 둘 모두 구현해야 할 때도 있다.
클라이언트는 bindService()를 호출하여 서비스에 바인딩된다.
이때 반드시 서비스와의 연결을 모니터링하는 ServiceConnection의 구현을 제공해야 한다.
Android 시스템이 클라이언트와 서비스 사이에 연결을 설정하면 ServiceConnection에서 onServiceConnected()을 호출한다.
onServiceConnected() 메서드에는 IBinder 인수가 포함되고 클라이언트는 이를 사용하여 바인딩된 서비스와 통신한다.
바인딩된 서비스를 구현할 때 가장 중요한 부분은 onBind() 콜백 메서드가 반환하는 인터페이스를 정의하는 것이다.

바인딩 서비스 예제

안드로이드 서비스 예제를 통해 개념을 익히고 활용할 수 있도록 예제를 테스트하고 적어둔다.

https://link2me.tistory.com/1343 에 기본 서비스 예제가 있으며, 비교하여 달라진 점을 확인해 볼 수 있다.

bindService를 추가하면 서비스 종료시에 반드시 unbindService(mConnection) 처리를 해줘야만 제대로 종료가 됨을 확인할 수 있다.


layout.xml

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    tools:context=".MainActivity">

    <Button
        android:id="@+id/btn_servicestart"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:gravity="center_vertical"
        android:layout_gravity="center"
        android:text="서비스 시작"
        android:textSize="14sp"
        android:textAllCaps="false"
        android:layout_marginTop="30dp"/>

    <Button
        android:id="@+id/btn_servicestop"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="서비스 종료"
        android:layout_gravity="center"
        android:gravity="center"
        android:textSize="14sp"
        android:textAllCaps="false"
        android:layout_marginTop="20dp"/>

    <Button
        android:id="@+id/btn_bindservice"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="바인드 값"
        android:layout_gravity="center"
        android:gravity="center"
        android:textSize="14sp"
        android:textAllCaps="false"
        android:layout_marginTop="20dp"/>

</LinearLayout>



MyService.java

import android.app.Service;
import android.content.Context;
import android.content.Intent;
import android.os.Binder;
import android.os.IBinder;
import android.util.Log;

public class MyService extends Service {
    private static final String TAG = MyService.class.getSimpleName();
    private Context context;
    private int mCount = 0;
    private Thread mThread;
    public static boolean SERVICE_CONNECTED = false;

    public MyService() {
    }

    private IBinder mBinder = new MyBinder();
    public class MyBinder extends Binder {
        public MyService getService(){
            Log.e(TAG, "MyService MyBinder return.");
            return MyService.this;  // 서비스 객체를 리턴
        }
    }

    @Override
    public IBinder onBind(Intent intent) {
        Log.e(TAG, "MyService IBinder onBind");
        // Service 객체와 (화면단 Activity 사이에서) 데이터를 주고받을 때 사용하는 메서드
        // Activity에서 bindService() 를 실행하면 호출됨
        // 데이터를 전달할 필요가 없으면 return null;
        return mBinder; // 리턴한 mBinder 객체는 서비스와 클라이언트 사이의 인터페이스를 정의한다.
    }

    public int getCount(){
        return mCount;
    }

    @Override
    public void onCreate() {
        Log.e(TAG, "MyService Started");
        this.context = this;
        MyService.SERVICE_CONNECTED = true;
    }

    @Override
    public int onStartCommand(Intent intent, int flags, int startId) {
        Log.e(TAG, "MyService onStartCommand startID === "+startId); // 계속 증가되는 값
        if(mThread == null){
            mThread = new Thread("My Thread"){
                @Override
                public void run() {
                    while (!Thread.currentThread().isInterrupted()){
                        try {
                            mCount++;
                            Thread.sleep(1000);
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                            this.interrupt();
                        }
                        Log.e("My Thread", "서비스 동작 중 " + mCount);
                    }
                }
            };
            mThread.start();
        }
        return START_STICKY;
    }

    @Override
    public void onDestroy() {
        Log.e(TAG,"MyService onDestroy");
        super.onDestroy();
        if(mThread != null && mThread.isAlive() ){
            mThread.interrupt();
            mThread = null;
            mCount = 0;
        }
        SERVICE_CONNECTED = false;
    }
}


MainActivity.java

import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.ServiceConnection;
import android.os.Bundle;
import android.os.IBinder;
import android.util.Log;
import android.view.View;
import android.widget.Button;
import android.widget.Toast;

import androidx.appcompat.app.AppCompatActivity;

import java.util.Set;

public class MainActivity extends AppCompatActivity {
    private static final String TAG = MainActivity.class.getSimpleName();
    Context mContext;
    private Button btnServiceStart;
    private Button btnServiceStop;
    private Button btnBindService;

    private MyService myService;
    private boolean mBound = false;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        mContext = MainActivity.this;

        btnServiceStart = findViewById(R.id.btn_servicestart);
        btnServiceStop = findViewById(R.id.btn_servicestop);
        btnBindService = findViewById(R.id.btn_bindservice);

        btnServiceStart.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                Toast.makeText(mContext, "서비스를 시작합니다.", Toast.LENGTH_SHORT).show();
                Intent intent = new Intent(mContext,MyService.class);
                startService(intent);
            }
        });

        btnServiceStop.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                Toast.makeText(mContext, "서비스를 종료합니다.", Toast.LENGTH_SHORT).show();
                Log.e(TAG,"MainActivity ServiceStop Button Clicked.");
                if(mBound){
                    unbindService(mConnection);
                    mBound = false;
                }
                stopService(new Intent(mContext,MyService.class));
            }
        });

        btnBindService.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                if(mBound){
                    Toast.makeText(mContext, "카운팅 : " + myService.getCount(), Toast.LENGTH_SHORT).show();
                }
            }
        });
    }

    @Override
    protected void onDestroy() {
        Log.e(TAG,"MainActivity onDestroy");
        if(mBound){
            unbindService(mConnection);
            mBound = false;
        }
        stopService(new Intent(this,MyService.class));
        super.onDestroy();
    }

    @Override
    protected void onStart() {
        super.onStart();
        Intent intent = new Intent(this,MyService.class);
        bindService(intent,mConnection,BIND_AUTO_CREATE); //
startService 버튼 클릭시 시작
//        startService(MyService.class, mConnection,null); // Activity 시작시
startService 자동 실행
    }

    private ServiceConnection mConnection = new ServiceConnection() {
        @Override
        public void onServiceConnected(ComponentName name, IBinder service) {
            MyService.MyBinder binder = (MyService.MyBinder) service;
            myService = binder.getService();
            mBound = true;
        }

        @Override
        public void onServiceDisconnected(ComponentName name) {
            // 예기치 않은 종료(안드로이드 OS에 의한 종료)

            mBound = false;
        }
    };

    private void startService(Class<?> service, ServiceConnection serviceConnection, Bundle extras) {
        // onStart 메소드에서 서비스 자동 실행 처리할 목적
        if (!MyService.SERVICE_CONNECTED) {
            Intent startService = new Intent(this, service);
            if (extras != null && !extras.isEmpty()) {
                Set<String> keys = extras.keySet();
                for (String key : keys) {
                    String extra = extras.getString(key);
                    startService.putExtra(key, extra);
                }
            }
            startService(startService);
        }
        Intent bindingIntent = new Intent(this, service);
        bindService(bindingIntent, serviceConnection, Context.BIND_AUTO_CREATE);
    }
}


Log 를 통해서 동작 순서에 대한 이해를 할 수 있다.

Activity가 실행되면서 자동으로 서비스를 실행하도록 한 경우의 Log 메시지다.


728x90
블로그 이미지

Link2Me

,
728x90

Interface 처리에 대한 사항을 정리차원에서 실 사용 예제에서 발췌하여 적어둔다.

3번 객체에 해당하는 부분은 여러 Class 에서 하나의 개발코드에 접근할 수 있도록 인터페이스 상속 처리를 했다.


1. 인터페이스 선언

public interface ISerialListener {
    void onReceive(int msg, int arg0, int arg1, String arg2, Object arg3);
}



2. SerialConnector 코드 발췌

public class SerialConnector {
    private Context mContext;
    private ISerialListener mListener; // 인터페이스 처리
    private Handler mHandler;
    private SerialMonitorThread mSerialThread;

    public SerialConnector(Context context, ISerialListener listener, Handler handler) {
        mContext = context;
        mListener = listener;
        mHandler = handler;
    }

    public void initialize() {
        List<UsbSerialDriver> availableDrivers = UsbSerialProber.getDefaultProber().findAllDrivers(sUsbManager);
        if (availableDrivers.isEmpty()) {
            mListener.onReceive(Constants.MSG_SERIAL_ERROR, 0, 0, "Error: There is no available device. \n", null);
            return;
        }

        sDriver = availableDrivers.get(0);
        if(sDriver == null) {
            mListener.onReceive(Constants.MSG_SERIAL_ERROR, 0, 0, "Error: Driver is Null \n", null);
            return;
        }

        try {
            sPort.open(mConnection);
            if(BaudRate.isEmpty()){ // 통신 속도 설정값 가져와서 세팅
                sPort.setParameters(9600, 8, 1, 0);  // baudrate:9600, dataBits:8, stopBits:1, parity:N
            } else {
                sPort.setParameters(Integer.parseInt(BaudRate), Integer.parseInt(DataBit), Integer.parseInt(StopBit), Integer.parseInt(Parity));
            }
        } catch (IOException e) {
            mListener.onReceive(Constants.MSG_SERIAL_ERROR, 0, 0, "Error: Cannot open port \n" + e.toString() + "\n", null);
        } finally {
        }

        startThread();
    }

    public void finalize() {
        try {
            sDriver = null;
            stopThread();

            sPort.close();
            sPort = null;
        } catch(Exception ex) {
            mListener.onReceive(Constants.MSG_SERIAL_ERROR, 0, 0, "Error: Cannot finalize serial connector \n" + ex.toString() + "\n", null);
        }
    }

    /*****************************************************
     *    private methods
     ******************************************************/
    // start thread
    private void startThread() {
        mListener.onReceive(Constants.MSG_SERIAL_ERROR, 0, 0, "Start serial monitoring thread \n", null);
        if(mSerialThread == null) {
            mSerialThread = new SerialMonitorThread();
            mSerialThread.start();
        }
    }
    // stop thread
    private void stopThread() {
        if(mSerialThread != null && mSerialThread.isAlive())
            mSerialThread.interrupt();
        if(mSerialThread != null) {
            mSerialThread = null;
        }
    }

    /*****************************************************
     *    Sub classes, Handler, Listener
     ******************************************************/
    public class SerialMonitorThread extends Thread {
        @Override
        public void run() {
            byte readBuffer[] = new byte[4096];

            while(!Thread.interrupted()) {
                if(sPort != null) {
                    Arrays.fill(readBuffer, (byte)0x00);

                    try {
                        // Read and Display to Terminal
                        int numBytesRead = sPort.read(readBuffer, 1000);
                        if(numBytesRead > 0) {

                            // Print message length
                            Message msg = mHandler.obtainMessage(Constants.MSG_READ_DATA_COUNT, numBytesRead, 0, new String(readBuffer));
                            mHandler.sendMessage(msg);

                        } // End of if(numBytesRead > 0)
                    } catch (IOException e) {
                        Message msg = mHandler.obtainMessage(Constants.MSG_SERIAL_ERROR, 0, 0, "Error # run: " + e.toString() + "\n");
                        mHandler.sendMessage(msg);
                    }
                }

                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                    break;
                }

            }    // End of while() loop
            finalizeThread();
        }    // End of run()
    }    // End of SerialMonitorThread

}


3. 객체 구현

핵심사항만 발췌하여 유사한 코드 구현시 활용 차원으로 적어둔다.

public class AAA extends AppCompatActivity {
    private static final String TAG = "AAA";
    Context mContext;

    private ActivityHandler mHandler = null;
    private SerialListener mSerialListener = null;
    private SerialConnector mSerialConnector = null;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_tam_ss);
        mContext = AAA.this;
        initView();
    }

    private void initView() {
        connectUsb();
    }

    @Override
    protected void onNewIntent(Intent intent) {
        super.onNewIntent(intent);
        connectUsb();
    }

    @Override
    protected void onDestroy() {
        releaseUsb();
        super.onDestroy();
    }

    private void connectUsb() {
        searchEndPoint();
        if (usbInterfaceFound != null) {
            setupUsbComm();
        }
    }

    private void releaseUsb() {
        textStatus.setText("releaseUsb()");
        mSerialConnector.finalize();
    }

    private boolean setupUsbComm() {
        boolean success = false;

        UsbManager manager = (UsbManager) getSystemService(Context.USB_SERVICE);
        Boolean permitToRead = manager.hasPermission(deviceFound);

        if (permitToRead) {
            usbDeviceConnection = manager.openDevice(deviceFound);
            if (usbDeviceConnection != null) {
                // Initialize
                mSerialListener = new SerialListener();
                mHandler = new ActivityHandler();

                // Initialize Serial connector and starts Serial monitoring thread.
                mSerialConnector = new SerialConnector(mContext, mSerialListener, mHandler);
                mSerialConnector.initialize();
            }
        } else {
            manager.requestPermission(deviceFound, mPermissionIntent);
            textStatus.setText("Permission: " + permitToRead);
        }
        return success;
    }

    public class SerialListener implements ISerialListener {
        public void onReceive(int msg, int arg0, int arg1, String arg2, Object arg3) {
            switch(msg) {
                case Constants.MSG_DEVICD_INFO:
                    updateReceivedData(arg2);
                    break;
                case Constants.MSG_DEVICE_COUNT:
                    updateReceivedData(Integer.toString(arg0) + " device(s) found \n");
                    break;
                case Constants.MSG_READ_DATA_COUNT:
                    updateReceivedData(Integer.toString(arg0) + " buffer received \n");
                    break;
                case Constants.MSG_READ_DATA:
                    if(arg3 != null) {
                        updateReceivedData((String)arg3);
                    }
                    break;
                case Constants.MSG_SERIAL_ERROR:
                    updateReceivedData(arg2);
                    break;
                case Constants.MSG_FATAL_ERROR_FINISH_APP:
                    finish();
                    break;
            }
        }
    }

    public class ActivityHandler extends Handler {
        @Override
        public void handleMessage(Message msg) {
            switch(msg.what) {
                case Constants.MSG_DEVICD_INFO:
                    updateReceivedData((String)msg.obj);
                    break;
                case Constants.MSG_DEVICE_COUNT:
                    updateReceivedData(Integer.toString(msg.arg1) + " device(s) found \n");
                    break;
                case Constants.MSG_READ_DATA_COUNT:
                    updateReceivedData(((String)msg.obj));
                    break;
                case Constants.MSG_READ_DATA:
                    if(msg.obj != null) {
                        updateReceivedData((String)msg.obj);
                    }
                    break;
                case Constants.MSG_INPUT_READ:
                    updateReceivedData((String)msg.obj);
                    mDumpTextView.setTextColor(Color.BLUE);
                    break;
                case Constants.MSG_SERIAL_ERROR:
                    updateReceivedData((String)msg.obj);
                    break;
            }
        }
    }
}


728x90

'안드로이드 > Interface' 카테고리의 다른 글

Java Interface 예제  (0) 2019.11.18
Android Interface AsyncTask 예제  (0) 2019.11.05
Java Interface Example  (0) 2019.09.05
Java 인터페이스(interface) 개요  (0) 2019.08.20
Android Interface 예제 ★★★  (0) 2018.08.22
블로그 이미지

Link2Me

,
728x90
전세계적으로 개발자가 자바 언어만큼 많이 사용하는 또다른 프로그램 개발 언어는 C언어와 C++언이이다.
C/C++언어는 자바와 달리 특정 플랫폼에 최적화시킬 수 있는 장점을 제공한다.
java만 사용하여 필요 기능과 성능을 모두 만족시키기는 힘들다. 안드로이드는 자바 기반의 언어다. 자바는 JVM(Java Virtual Machine)에서 동작하기 때문에 실제적인 디바이스에 접근이 불가능하다. 그래서 나온 것이 안드로이드 NDK이다.
NDK을 사용하여 디바이스에 접근 제어할 수 있는 C/C++ 함수들을 호출할 수 있도록 하여 안드로이드 개발자들이 좀 더 효율성 있게 개발할 수 있도록 도와주는 역할을 한다.

C, C++ 로 작성된 프로그램을 안드로이드에서 사용할 수 있도록 JDK에서 제공하는 것이 JNI(Java Native Interface) 이다.

Java에는 상응하는 것이 없는 수많은 C/C++ 라이브러리가 있다. 이러한 라이브러리를 이용하려면 NDK를 사용하는 것이 좋다.

안드로이드는 C/C++언어로 개발된 프로그램을 Java와 구분하기 위해 네이티브 코드라고 하며, 네이티브 코드로 작성되어 실행이 가능한 메소드를 일반 Java 메소드와 구분하여 네이티브 함수라 부른다.


안드로이드 스튜디오에서 C++ 코드를 import 하여 사용하기 위해 필요한 설정이다.

화면 캡처는 맥북에서 했다. 맥 윈도우 8.1 에서 CMake 인식이 안되어서 맥북 모드로 다시 확인하니까 맥북에서는 잘 인식되어 installed 했다.

최신 알파버전 Android Studio 4.0  을 다른 폴더에 설치하고 실행해서 보니 안보이던 cmake 선택창이 보여서 설치했더니, 이제는 Android Studio 3.5.2 에서도 잘 보인다. 흐미....



https://developer.android.com/ndk/guides?hl=ko 에 설명이 잘 되어 있다.

NDK(Native Development Kit)는 Android에서 C 및 C++ 코드를 사용할 수 있게 해주는 일련의 도구 모음으로, 네이티브 액티비티를 관리하고 센서 및 터치 입력과 같은 물리적 기기 구성요소에 액세스하는 데 사용할 수 있는 플랫폼 라이브러리를 제공한다.

- 기기에서 최대한의 성능을 도출하여 짧은 지연 시간을 달성해야 하는 경우

- 게임 또는 물리학 시뮬레이션과 같은 연산 집약적인 애플리케이션을 실행하는 경우

- 본인 또는 다른 개발자의 C 또는 C++ 라이브러리를 재사용하는 경우


ㅇCMake: Gradle과 함께 작동하여 네이티브 라이브러리를 빌드하는 외부 빌드 도구다.

   (Android Studio’s default build tool for native libraries is CMake.)

   ndk-build만 사용하려는 경우에는 이 구성요소가 필요하지 않다.

  

ㅇLLDB: Android 스튜디오에서 네이티브 코드를 디버깅하는 데 사용하는 디버거다.



C++ 코드 사용 가능한 프로젝트 생성




설치된 폴더를 확인해보면, NDKTest 라는 프로젝트명이 생겼다.


소스 폴더에 java, res 폴더 외에 cpp 라는 폴더가 추가로 생긴 걸 확인할 수 있다.


cpp 폴더안에 native-lib.cpp 파일과 CMakeLists.txt 파일 기본 생성되었다.


모든 C++ 파일과 라이브러리는 cpp 폴더 안에 있어야 한다.


cpp 파일 작성법

default 로 생성된 파일을 분석해보자.

#include <jni.h>
#include <string>

extern "C" JNIEXPORT jstring JNICALL
Java_com_link2me_android_ndktest_MainActivity_stringFromJNI(
        JNIEnv *env,
        jobject /* this */) {
    std::string hello = "Hello from C++";
    return env->NewStringUTF(hello.c_str());
}


#include <jni.h> // 다수의 매크로 정의, 타입, 구조체, 함수를 포함하는 헤더 파일로 반드시 포함해야 한다.

JNI(Java Native Interface)를 통해 접근할 수 있는 C++ 함수명은 다음과 같은 형태여야 한다.

'JNIEXPORT <함수 타입> JNICALL Java_<패키지 명>_<클래스 명>_<함수명>(JNIEnv*, jobject, 파라미터) 이다.

Java_com_link2me_android_ndktest_MainActivity_stringFromJNI(JNIEnv *env, jobject) {

Java는 접두어이고,

package="com.link2me.android.ndktest" 패키지명에서 . 대신에 _ 로 바꾼다.

자바 클래스명 : MainActivity

함수명 : stringFromJNI

JNIEnv*, jobject는 네이티브에서 자바로 접근하는 포인트가 된다.


extern "C" JNIEXPORT jstring JNICALL
Java_com_link2me_android_ndktest_MainActivity_intFromJNI(
        JNIEnv *env, jobject b, jint a, jint b) {
    // 연산결과 = 연산식
    return env->NewStringUTF(연산결과);
}


public native int sum(int a, int b);

매개변수가 추가된 경우에는 네이티브 코드에 자주색처럼 추가해주면 된다.



MainActivity.java 파일 추가 사항을 살펴보자.

public class MainActivity extends AppCompatActivity {

    // 네이티브 함수를 사용하기 위하여 동적 라이브러리를 로딩
    // 동적라이브러리는 stringFromJNI메서드가 호출되기 전에 로딩되어야 하므로 static으로 초기화
    static {
        System.loadLibrary("native-lib"); // 라이브러리명은 소문자를 사용해야 한다.
    }

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        // Example of a call to a native method
        TextView tv = findViewById(R.id.sample_text);
        tv.setText(stringFromJNI());
    }

    /**
     * A native method that is implemented by the 'native-lib' native library,
     * which is packaged with this application.
     */
    // java에서 사용할 네이티브 함수의 선언
    // native라는 키워드는 이 메서드가 java가 아닌 다른 언어로 작성된 것임을 암시
    public native String stringFromJNI();
}

자바 프로그램내 선언한 메소드에 native 라는 키워드를 추가하지 않는다면, 자바 컴파일러는 메소드들이 모두 자바 언어로 작성되어 있다고 가정한다.

native 선언은 메소드는 자바 언어가 아닌 C나 C++ 언어로 작성되어 있다는 사실을 알려준다.

자바 프로그램의 특성상 메소드의 선언은 C/C++ 언어와 달리 클래스의 어디에 위치해도 좋다.

네이티브 메소드는 커널에 의해 실행되기 때문에 자바 가상 머신에서 관리하지 않는다.

자바를 실행시키는 가상머신은 기본적으로 모든 문자열을 UTF-16 이라는 유니코드(Unicode)로 처리한다.


앱 build.gradle

cpp가 포함되지 않은 앱 build.gradle 에 비해 자주색으로 표시된 부분이 추가되어 있다.

apply plugin: 'com.android.application'

android {
    compileSdkVersion 29
    buildToolsVersion "29.0.2"
    defaultConfig {
        applicationId "com.link2me.android.ndktest"
        minSdkVersion 21
        targetSdkVersion 29
        versionCode 1
        versionName "1.0"
        externalNativeBuild {
            cmake {
                cppFlags ""
            }
        }
    }
    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
        }
    }
    externalNativeBuild {
        cmake {
            path "src/main/cpp/CMakeLists.txt"
            version "3.10.2"
        }
    }
}

dependencies {
    implementation fileTree(dir: 'libs', include: ['*.jar'])
    implementation 'androidx.appcompat:appcompat:1.1.0'
    implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
}


참고자료

https://programmingfbf7290.tistory.com/entry/NDK2-%EC%95%88%EB%93%9C%EB%A1%9C%EC%9D%B4%EB%93%9C-%EC%8A%A4%ED%8A%9C%EB%94%94%EC%98%A4-NDK-JNI-%EA%B8%B0%EB%B3%B8-%EC%98%88%EC%A0%9C?category=666988


참고하면 좋은 책 : 안드로이드 C-C++ 프로그래밍

728x90
블로그 이미지

Link2Me

,
728x90

인코딩 문제인지 파이썬에서 개발했다고 하는 ANSI escape sequences 코드 적용된 장비 접속에서 회신이 제대로 오지 않는 문제로 골머리가 아프다.

PC/노트북용 Putty 에서는 정상적으로 처리되는데 Android 앱에서 구현한 코드로는 특정 명령어는 100% 회신이 오는데, 특정 명령어는 50% 성공율이 발생한다. 문제 해결을 위해서 C++ 유투브 강좌도 열심히 들어보고 있다.

Android NDK 를 이용한 방법으로 해결할 수 있지 않을까 생각과 더불어 NDK 처리 방법을 배우면 유용할 거 같아서다.

아직 문제 해결이 안된 상태인데 인코딩 문제인가 싶어서 관련 내용을 찾아보고 정리해둔다.


인코딩(encoding)은 문자셋을 컴퓨터가 이해할 수 있는 바이트와 매핑하는 규칙이다.
UTF-8은 모든 유니 코드 문자를 나타낼 수있는 멀티 바이트 인코딩이다.
ISO 8859-1은 첫 번째 256 유니 코드 문자를 나타낼 수있는 단일 바이트 인코딩이다.

ASCII(American Standard Code for Information Interchange)는 7비트로 엄격하게 제한되었으며, 표현 가능한 7개의 이진수로 비트 패턴을 사용함을 의미하는데, 10진수로는 0부터 127까지의 범위를 제공한다.
여기에는 32개의 보이지 않는 제어 문자가 있는데, 0부터 31까지의 범위에 있으며, 마지막 제어 문자는 DEL또는 delete이며, 이 제어문자는 127번에 할당되어 있다.
32번 부터 126번 까지는 공백 문자, 문장 부호, 라틴 문자, 숫자와 같은 눈에 보이는 문자로 구성되어 있다.
ASCII의 8번째 비트는 본디 오류 검출을 위한 패리티 비트로 활용했다.
오류 검사를 고려하지 않으면 0으로 그대로 둔다. ASCII에서 각 문자를 단일 바이트로 표현했음을 의미한다.
ㅇ 7 bit 코드 : 0x00 ~ 0x7F 까지 128개
ㅇ 첫 1bit 는 패러티 비트 : 에러 검출 목적

강세부호가 들어간 문자가 있는 다른 유럽어권의 문자를 ASCII 로 의사소통할 수 없어 ISO 8859 표준을 개발했다.
현재 ISO 8859 표준의 15가지 변형 표준이 있다.
ㅇ 8bit 코드 : 0x00 ~ 0xFF 까지 256개
ㅇ ASCII 에서 쓸 수 없는 문자들까지 포함
ㅇ 언어권에 따라 여러가지 변형
ㅇ 서유럽용 IOS-8859-1 을 가장 많이 씀


UTF-8

ㅇ 유니코드 인코딩 방식중 하나

ㅇ 영어는 1byte, 한글은 3byte


https://developer.android.com/training/articles/perf-jni#java 에 "자바 프로그래밍 언어는 UTF-16을 사용한다." 고 나온다. UTF-8 이 아니고???


The ANSI character set, also known as Windows-1252, has become a Microsoft proprietary character set;
it is a superset of ISO-8859-1 with the addition of 27 characters in locations that ISO designates for control codes.

ASCII 가  1byte 인코딩 방식이라고 하면, UTF-8 은 멀티방식 인코딩이지만 영문은 1byte 이므로 동일할 거 같고, ISO-8859-1 인코딩 방식이랑 차이가 없을거 같은 생각이 드는데 맞나??


아무튼 문제 해결을 위한 지식과 코드를 분석해보면서 해결책을 찾아야겠다.


Update : 2019.12.20

해결 결과 : 인코딩 문제가 아니라 흐름제어(Flow Control) 문제였다.


728x90
블로그 이미지

Link2Me

,
728x90

안드로이드 폰에서 SSH 어플을 사용하고자 한다면, 검색해보면 가장 추천하는 앱은 JuiceSSH 이고, open source project로 진행되는 ConnectBot 이 있다는 걸 알았다.  좀 더 실력이 키워야 분석 및 활용이 가능할 것 같다.


Java 언어로 개발된 SSH2 라이브러리는 JSCH(Java Secure Channel) 를 확인할 수 있다.

구글링하면 예제 많이 나오는데 제대로 동작되는 걸 해보려면 쉽지 않더라.

http://www.jcraft.com/jsch/ 에 가면 jsch-0.1.55.jar 파일을 다운로드할 수 있다.


https://github.com/jonghough/AndroidSSH 에서 소스 코드를 다운로드 받아서 어플을 실행해 볼 수 있다.


테스트 결과 LG G5 안드로이드 6.0 운영체제에서는 잘 동작을 했다.

그런데 삼성 갤럭시 S7 안드로이드 8.0, 삼성 갤럭시 S10 안드로이드 9.0 운영체제 폰에서는 동작이 제대로 안되더라.

코드가 오래전에 올려진 것이라서 위험권한을 추가하고 동작시켜도 동작이 안된다.

쓰레드 처리에 대한 학습을 좀 하고 나서 몇가지 사항을 수정해서 동작되도록 했다.


앱 build.gradle

android {
    compileSdkVersion 28


   // 중간 생략


dependencies {
    implementation fileTree(dir: 'libs', include: ['*.jar'])

    implementation 'androidx.appcompat:appcompat:1.1.0'
    implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
    implementation 'gun0912.ted:tedpermission:2.0.0'
    implementation 'androidx.multidex:multidex:2.0.0' // minSdkVersion 이 21 이하인 경우 사용
    implementation 'com.jcraft:jsch:0.1.55'
    implementation 'org.bouncycastle:bcprov-jdk16:1.46'
}


MainActivity.java 수정사항

- 위험권한 설정 처리코드 추가 구현 필요


ShellController.java 수정사항

    public void writeToOutput(final String command) {
        if (mDataOutputStream != null) {
            new Thread(new Runnable() {
                @Override
                public void run() {
                    try {
                        mDataOutputStream.writeBytes(command + "\r\n");
                        mDataOutputStream.flush();
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                }
            }).start();

        }
    }


   public void openShell(final Session session, Handler handler, EditText editText) throws JSchException, IOException {
        Log.e(TAG, "openShell start!");
        if (session == null) throw new NullPointerException("Session cannot be null!");
        if (!session.isConnected()) throw new IllegalStateException("Session must be connected.");
        final Handler myHandler = handler;
        final EditText myEditText = editText;
        new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    mChannel = session.openChannel("shell");
                    mChannel.connect();
                    try {
                        mBufferedReader = new BufferedReader(new InputStreamReader(mChannel.getInputStream()));
                        mDataOutputStream = new DataOutputStream(mChannel.getOutputStream());
                    } catch (IOException e) {
                        e.printStackTrace();
                    }

                } catch (JSchException e) {
                    e.printStackTrace();
                }

                try {
                    String line;
                    while (true) {
                        while ((line = mBufferedReader.readLine()) != null) {
                            final String result = line;
                            if (mSshText == null) mSshText = result;
                            Log.e(TAG, "result : " + result);
                            myHandler.post(new Runnable() {
                                public void run() {
                                    synchronized (myEditText) {
                                        ((SshEditText)myEditText).setPrompt(result); //set the prompt to be the current line, so eventually it will be the last line.
                                        myEditText.setText(myEditText.getText().toString() + "\r\n" + result + "\r\n"+fetchPrompt(result));
                                        Log.e(TAG, "LINE : " + result);
                                    }
                                }
                            });
                        }

                    }
                } catch (Exception e) {
                    Log.e(TAG, " Exception " + e.getMessage() + "." + e.getCause() + "," + e.getClass().toString());
                }
            }
        }).start();
    }
 


내가 테스트한 GitHub 소스는 칼라 코드 지원은 안된다.

SFTP 기능 구현은 테스트 하지 않았다.



참고하면 도움될 게시글
싱글톤(Singleton) 개념 이해 : https://link2me.tistory.com/1528
Java Thread 이해 및 Thread Life Cycle : https://link2me.tistory.com/1730
Java Thread 상태 제어 : https://link2me.tistory.com/1731
Java Thread 동기화 : https://link2me.tistory.com/1732


728x90
블로그 이미지

Link2Me

,
728x90

안드로이드와 PHP간에 암호화 통신을 하는 알고리즘을 찾아서 테스트를 했다.

본 게시글은 2017년도에 작성했다가 암호화 코드 부분을 바로 활용할 수 있도록 사용중인 코드를 오픈한다.


AES256Cipher.java  (2019.11.24일 수정)

import android.util.Base64;

import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.security.spec.AlgorithmParameterSpec;

import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;

public class AES256Cipher {
    public static byte[] ivBytes = { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 };


    public static String AES_Encode(String str, String key)    throws java.io.UnsupportedEncodingException, NoSuchAlgorithmException, NoSuchPaddingException, InvalidKeyException, InvalidAlgorithmParameterException, IllegalBlockSizeException, BadPaddingException {

        byte[] textBytes = str.getBytes("UTF-8");
        AlgorithmParameterSpec ivSpec = new IvParameterSpec(ivBytes);
        SecretKeySpec newKey = new SecretKeySpec(key.getBytes("UTF-8"), "AES");
        Cipher cipher = null;
        cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
        cipher.init(Cipher.ENCRYPT_MODE, newKey, ivSpec);

        return Base64.encodeToString(cipher.doFinal(textBytes), 0);
    }

    public static String AES_Decode(String str, String key)    throws java.io.UnsupportedEncodingException, NoSuchAlgorithmException, NoSuchPaddingException, InvalidKeyException, InvalidAlgorithmParameterException, IllegalBlockSizeException, BadPaddingException {

        byte[] textBytes =Base64.decode(str,0);
        //byte[] textBytes = str.getBytes("UTF-8");
        AlgorithmParameterSpec ivSpec = new IvParameterSpec(ivBytes);
        SecretKeySpec newKey = new SecretKeySpec(key.getBytes("UTF-8"), "AES");
        Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
        cipher.init(Cipher.DECRYPT_MODE, newKey, ivSpec);
        return new String(cipher.doFinal(textBytes), "UTF-8");
    }

}

import android.Manifest;
import android.annotation.SuppressLint;
import android.content.Context;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.provider.Settings;
import android.telephony.PhoneNumberUtils;
import android.telephony.TelephonyManager;

import androidx.appcompat.app.AppCompatActivity;
import androidx.core.app.ActivityCompat;

import java.io.UnsupportedEncodingException;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Locale;

import javax.crypto.BadPaddingException;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.NoSuchPaddingException;

public class Value extends AppCompatActivity {
    public static final String APKNAME = "ABC.apk"; // APK name
    public static final String IPADDRESS = "http://100.100.100.100/"; // SERVER IP (수정 요)
    private static String key = "abcdefghijkltuz12
mnopqrs34vwxy56"; // AES 암호화  키(수정 요)

    // key 대신에 AES256Key 라고 적을 수도 있다.


    // 암호화
    public static String encrypt(String data) {
        try {
            data = AES256Cipher.AES_Encode(data, Value.key);
        } catch (UnsupportedEncodingException e) {
            e.printStackTrace();
        } catch (NoSuchAlgorithmException e) {
            e.printStackTrace();
        } catch (NoSuchPaddingException e) {
            e.printStackTrace();
        } catch (InvalidKeyException e) {
            e.printStackTrace();
        } catch (InvalidAlgorithmParameterException e) {
            e.printStackTrace();
        } catch (IllegalBlockSizeException e) {
            e.printStackTrace();
        } catch (BadPaddingException e) {
            e.printStackTrace();
        }
        return data;
    }

    // 복호화
    public static String decrypt(String data) {
        try {
            data = AES256Cipher.AES_Decode(data, Value.key);
        } catch (UnsupportedEncodingException e) {
            e.printStackTrace();
        } catch (NoSuchAlgorithmException e) {
            e.printStackTrace();
        } catch (NoSuchPaddingException e) {
            e.printStackTrace();
        } catch (InvalidKeyException e) {
            e.printStackTrace();
        } catch (InvalidAlgorithmParameterException e) {
            e.printStackTrace();
        } catch (IllegalBlockSizeException e) {
            e.printStackTrace();
        } catch (BadPaddingException e) {
            e.printStackTrace();
        }
        return data;
    }

    public static String getVersionName(Context context) {
        PackageInfo packageInfo = null;
        try {
            packageInfo = context.getPackageManager().getPackageInfo(context.getPackageName(), 0);
        } catch (PackageManager.NameNotFoundException e) {
            e.printStackTrace();
        }

        int versionCode = packageInfo.versionCode;
        String versionName = packageInfo.versionName;
        return versionName;
    }
   
}


Login.java 파일 내용 전체를 참고하려면 http://link2me.tistory.com/1230 보면 된다.

로그인 기본 정보를 안드로이드폰에 저장하고 다음부터 로그인할 때에는 저장된 로그인 정보를 이용하여 자동 로그인처리를 하도록 구현한다.


 === Login.java ===

// 전달할 인자들
params[0] = AES256Cipher.AES_Encode(params[0], Value.AES256Key);
params[1] = AES256Cipher.AES_Encode(params[1], Value.AES256Key);

Uri.Builder builder = new Uri.Builder()
        .appendQueryParameter("loginID", params[0])
        .appendQueryParameter("loginPW", params[1])
        .appendQueryParameter("deviceID", getDeviceID)
        .appendQueryParameter("tokenID", getToken);
String urlParameters = builder.build().getEncodedQuery();


 === PHP loginChk.php ===

 <?php
if(!isset($_SESSION)) {
    session_start();
}

@extract($_POST); // POST 전송으로 전달받은 값 처리
if(isset($loginID) && !empty($loginID) && isset($loginPW) && !empty($loginPW)) {

    $deviceID = $deviceID ? $deviceID : '';

    if(isset($deviceID) && !empty($deviceID)){ // 모바일 접속이면
        require_once 'db.info.php';
        require_once 'phpclass/dbClass.php';
        $conn=new MySQLiDbClass();
        $dbconn = $conn->isConnectDb($DB); // 안드로이드폰에서는 반드시 객체로 생성해야 정상접속
        require_once 'phpclass/loginClass.php';
        $c=new LoginClass();

        $loginID=$c->AES_decrypt($loginID); // 안드로이드 암호화를 복호화
        $loginPW=$c->AES_decrypt($loginPW);

        $result = $c->MobileUserAuthCheck($loginID,$loginPW,$deviceID);
        if($result > 0 ) {
            session_save_path('./_tmp/session');

            $_SESSION['userID'] = $loginID;
            $_SESSION['userPW'] = md5($loginPW);
            $_SESSION['ip'] = $_SERVER['REMOTE_ADDR'];
            $_SESSION['ua'] = $_SERVER['HTTP_USER_AGENT'];

            // 토큰 등록 및 갱신
            if(isset($tokenID) && strlen($tokenID) > 63){
                $c->registerToken($tokenID,$loginID,$deviceID); // DB에 토큰 저장
            }
            echo 1;
        } else if($result == 0) {
            echo 0; // 로그인 정보 틀림
        } else {
            echo '-1'; // Phone mismatch
        }
    }

}
?>


AES 암호화 복호화 함수

<?php
class LoginClass {

    // AES 암호화
    function AES_encrypt($plain_text){
        global $key;
        $encryptedMessage = openssl_encrypt($plain_text, "aes-256-cbc", $key, true,str_repeat(chr(0), 16));
        return base64_encode($encryptedMessage);
    }

    // AES 복호화
    function AES_decrypt($base64_text){
        global $key;
        $decryptedMessage = openssl_decrypt(base64_decode($base64_text), "aes-256-cbc", $key, true, str_repeat(chr(0), 16));
        return $decryptedMessage;
    }


   // 다른 함수들은 생략

}


도움이 되셨다면 광고 클릭 해 주세요. 좋은 글 작성에 큰 힘이 됩니다.

출처: https://link2me.tistory.com/1110 [소소한 일상 및 업무TIP 다루기]

도움이 되셨다면 댓글 달아주세요. 좋은 글 작성에 큰 힘이 됩니다.

728x90
블로그 이미지

Link2Me

,
728x90

이것이 자바다 유투브 강좌에 나온 내용중에서 인터페이스 부분을 정리해서 적어둔다.

https://www.youtube.com/watch?v=UPWiPIROG-k

 

 

인터페이스 선언

- 추상메서드의 집합

- 구현된 것이 전혀 없는 설계도

- 모든 멤버가 public 이라 생략 가능 (메서드는 public abstract, 상수는 public static final 생략 가능)

- 변수, 생성자를 가질 수 없다.

interface 인터페이스이름 {

    public abstract 메서드이름(매개변수목록);

}

 

인터페이스 구현 (→ 미완성 설계도 완성하기)

- 인터페이스에 정의된 추상 메서드를 완성시키는 것

class 클래스이름 implements 인터페이스이름 {

     // 인터페이스에 정의된 추상메서드를 모두 구현해야 한다.

}

- 만약 일부만 구현하는 경우, 클래스 앞에 abstract를 붙여야 한다.

 

특정한 Class 내에서만 사용하는 인터페이스를 정의한 걸 중첩 인터페이스라고 한다.

main 메서드에서 Button 클래스 객체를 생성하고 setOnClickListener() 메소드를 호출할 때 해당 인터페이스를 구현하는 자식 클래스로 값을 넘겨 준다. 그러면 다형성의 효과로 onClick() 메소드가 override한 결과로 나오게 된다.

 

public class Button {
    OnClickListener listener; // 중첩 인터페이스 타입으로 선언
   
    interface OnClickListener {
        void onClick(); // 추상 메소드
    }
   
    void setOnClickListener(OnClickListener listener) {
        this.listener = listener;
        // 외부에서 구현 객체를 받아서 필드에 저장한다.
    }
   
    void touch() {
        listener.onClick();
    }
   
}

import InterfaceEX.Button.OnClickListener;
public class CallListener implements OnClickListener {
    @Override
    public void onClick() {
        System.out.println("전화를 겁니다.");
    }
}

import InterfaceEX.Button.OnClickListener;
public class MessageListener implements OnClickListener {
    @Override
    public void onClick() {
        System.out.println("메시지를 보냅니다.");
    }
}

public class ButtonEx {

    public static void main(String[] args) {

        Button btn = new Button();
       
        btn.setOnClickListener(new CallListener()); // 내부 인터페이스 객체 생성
        btn.touch();
       
        btn.setOnClickListener(new MessageListener());
        btn.touch();

        Button.OnClickListener listener = new Button.OnClickListener() {
            @Override
            public void onClick() {
                System.out.println("사진을 찍습니다.");
            }
        };
        btn.setOnClickListener(listener);
        btn.touch();
       
        btn.setOnClickListener(new Button.OnClickListener() { // 익명 구현 객체
            // 익명 객체는 클래스를 상속하거나 인터페이스를 구현해야만 생성할 수 있다.
            // UI 이벤트 처리 객체나, 쓰레드 객체를 간편하게 생성할 목적으로 주로 사용한다.   
            // 익명 객체는 부모 타입 변수에 대입되므로 부모 타입에 선언된 것만 사용할 수 있다.
            // 외부에서는 익명 객체의 필드와 메소드에 접근할 수 없다.
            @Override // 부모 클래스(Button)의 메소드 재정의
            public void onClick() {
                System.out.println("이미지를 클릭합니다.");
            }
        });
        btn.touch();
    }
}

 

 

아래 코드는 자바의 정석 동영상 강좌를 듣고 구글링 여러 예제를 하나의 파일로 조합하여 작성한 것인다.

 

interface Printable {
    int x=10;
    void print(); // 추상메소드
}
 
interface Drawable {
    void draw(); // 추상메소드
    default void msg(){ // Java 1.8 이상에서 제공
        System.out.println("Drawable default method");
    }
}
 
interface Showable extends Printable {
    // 인터페이스 상속(Inheritance)
    void show(); // 추상메소드
}
 
class Parent {
    public void method2() {
        System.out.println("This is method2() in Parent Class");
    }
}
 
class Child extends Parent implements Printable, Drawable {
    @Override
    public void print() { // 인터페이스(추상 메소드) 구현
        System.out.println("Hello");
    }
 
    @Override
    public void draw() { // 인터페이스(추상 메소드) 구현
        System.out.println("drawing rectangle");
    }
}
 
class CustomDialog {
    // 특정한 Class 내에서만 사용하는 인터페이스를 정의한 걸 중첩 인터페이스라고 한다.
    interface CustomDialogListener {
        void onAgreeButtonClicked(String smsotp); // 추상 메소드
    }
 
    private CustomDialogListener customDialogListener;
    public void setCustomDialogListener(CustomDialogListener listener){
        customDialogListener = listener;
        // 외부에서 구현 객체를 받아서 변수에 저장한다.
    }
 
    String smsotp;
 
    public void Cick(){
        smsotp = "123456";
        customDialogListener.onAgreeButtonClicked(smsotp);
    }
}
 
public class Interface_EX implements Showable {
    @Override
    public void print() {
        System.out.println("print interface method implemented.");
    }
 
    @Override
    public void show() {
        System.out.println("show interface method implemented.");
    }
 
    public static void main(String[] args) {
 
        // 객체  생성 1
        Child child = new Child();
        child.print();
        child.draw();
        child.msg();
        child.method2();
 
        System.out.println(Printable.x);
 
        // 객체  생성 2
        Interface_EX interface_ex = new Interface_EX();
        interface_ex.print();
        interface_ex.show();
 
        // 객체  생성 3
        CustomDialog dialog = new CustomDialog();
        dialog.setCustomDialogListener(new CustomDialog.CustomDialogListener() { // 익명 구현 객체
            // 익명 객체는 클래스를 상속하거나 인터페이스를 구현해야만 생성할 수 있다.
            // 익명 객체는 부모 타입 변수에 대입되므로 부모 타입에 선언된 것만 사용할 수 있다.
            // 외부에서는 익명 객체의 변수와 메소드에 접근할 수 없다.
            @Override
            public void onAgreeButtonClicked(String smsotp) {
                System.out.println("customdialog result : "+smsotp);
            }
        });
        dialog.Cick();
 
    }
}
 

 

 

728x90
블로그 이미지

Link2Me

,
728x90

동영상 강좌를 듣다보니 디버그 서명 인증서 SHA-1 을 쉽게 등록하는 방법이 나온다.

CMD 창에서 등록하는 방법 등도 나오는데 쉽게 할 수 있지 않다.


Android Studio 우측에 있는 Gradle 를 누르고 해당 앱에서 android → signingReport 를 선택한다.





728x90
블로그 이미지

Link2Me

,
728x90

Java Thread Synchronized(동기화)란 여러 개의 Thread가 한 개의 자원을 사용하고자 할 때,
해당 Thread만 제외하고 나머지는 접근을 못하도록 막는 것이다.


Multi-Thread로 인하여 동기화를 제어해야하는 경우가 생긴다.
synchronized 키워드를 사용하면, Multi-Thread 상태에서 동일한 자원을 동시에 접근하게 되었을 때 동시 접근을 막게 된다.
synchronized를 사용하는 방법은 아래와 같다.
1. 메서드에 synchronized 하기

    - synchronized 를 붙이면 메소드 전체가 임계 영역으로 설정된다.

    - 쓰레드는 synchronized 메소드가 호출된 시점부터 해당 메소드가 포함된 객체의 Lock을 얻어 작업을 수행하다가 메소드가 종료되면 Lock 을 반환한다.
2. 블록에 synchronized 하기

   - 메소드 내의 코드 일부를 블럭{} 으로 감싸고 블럭 앞에 synchronized를 붙이는 것이다.

   - 이때 참조변수는 Lock 을 걸고자 하는 객체를 참조하는 것이어야 한다.

   - 이 영역 안으로 들어가면서부터 쓰레드는 지정된 객체의 Lock 을 얻게 되고, 이 블럭을 벗어나면 Lock 을 반납한다.


synchronized : 단 하나의 쓰레드만 실행할 수 있는 메소드 또는 블록을 말한다.
- 다른 쓰레드는 메소드나 블록이 실행이 끝날 때까지 대기해야 한다.
- wait(), notify(), notifyAll() 은 동기화 메소드 또는 블록에서만 호출 가능한 Object의 메소드
  두개의 쓰레드가 교대로 번갈아 가며 실행해야 할 경우에 주로 사용한다.
- wait() 호출한 쓰레드는 일시 정지가 된다.
- notify() or notifyAll()을 호출하면
다른 쓰레드가 실행 대기 상태가 된다.

- synchronized를 대충 사용하면 퍼포먼스 저하, 예상치 못한 동작이 발생할 수 있다.

- 임계 영역은 멀티쓰레드 프로그램의 성능을 좌우하기 때문에 가능하면 메소드 전체에 Lock을 거는 것보다 synchronized 블럭으로 임계 영역을 최소화해서 보다 효율적인 프로그램이 되도록 노력해야 한다.

- 쓰레드는 자신의 run() 메소드가 모두 실행되면 자동적으로 종료된다.




아래 예제를 실행해서 결과가 나오는 걸 이해해야 한다.

synchronized 를 붙인 경우와 붙이지 않은 경우에 결과를 비교해보면 명확하게 이해가 될 것이다.


예제1

public class SynchThread extends Thread {
    int total = 0;

    @Override
    public void run() {
        synchronized (this) { // 해당 객체(this)에 Lock 이 걸린다.
            for (int i = 0; i < 5; i++) {
                System.out.println(i + "를 더한다.");
                total += i;

                try {
                    Thread.sleep(500);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            notify(); // 위 작업이 모두 끝나면 notify()를 호출하여 다른 쓰레드를 실행 대기 상태로 만든다.
        }
    }
}

public class SyncMainThread {

    public static void main(String[] args) {
        SynchThread syncThread = new SynchThread();
        syncThread.start();
       
        synchronized (syncThread) {
            System.out.println("syncThread 가 완료될 때까지 기다린다.");
            try {
                syncThread.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("Total Sum : " + syncThread.total);
        }
    }

}

실행결과

syncThread 가 완료될 때까지 기다린다.
0를 더한다.
1를 더한다.
2를 더한다.
3를 더한다.
4를 더한다.
Total Sum : 10



예제2 (동기화 개념을 명확히 이해하고자 한다면 아래 동영상 강좌를 꼭 들어라)

두개의 쓰레드가 교대로 번갈아 가며 실행해야 하는 경우에 주로 사용한다.

"이것이 자바다" 유투브 강좌 https://www.youtube.com/watch?v=hao05jNL2m8 35분부터 참조하면 설명이 잘 되어 있다. 아래 코드는 https://www.youtube.com/watch?v=7pNZr8cmjis 강좌에 나온 내용이다.


- notify() 는 현재 waiting 된 다른 쓰레드를 실행대기 상태로 만든다.

- wait() 를 호출해서 자기 자신은 일시 정지가 된다.


public class DataBox {
    private String data;

    public synchronized String getData() { // 소비자 쓰레드가 호출
        if (this.data == null) { // 데이터를 읽을 수 없으니까 wait 으로 만든다.
            try {
                wait(); // 생성자 쓰레드가 데이터를 넣어줄 때가지 일시 정지된다.

            } catch (InterruptedException e) {
            }
        }
        String returnValue = data;
        System.out.println("ConsummerThread가 읽은 데이터: " + returnValue);
        data = null; // 데이터를 읽었으니까 null 로 만든다.
        notify(); //
notify()를 호출해서 현재 일시정지 상태에 있는 생성자 쓰레드를 실행 대기 상태로 만든다.
        return returnValue;
    }

    public synchronized void setData(String data) { // 생성자 쓰레드가 호출
        if (this.data != null) { // 소비자 쓰레드가 아직 데이터를 읽지 않았다면
            try {
                wait(); //

            } catch (InterruptedException e) {
            }
        }
        this.data = data;
// 데이터를 공유 객체에 저장한다.

        System.out.println("ProducerThread가 생성한 데이터: " + data);
        notify(); // notify()를 호출해서 소비자 쓰레드를 실행 대기 상태로 만들어 data를 읽어갈 수 있게 한다.

    }
}

public class ProducerThread extends Thread {
    private DataBox dataBox;

    public ProducerThread(DataBox dataBox) {
        this.dataBox = dataBox;
    }

    @Override
    public void run() {
        for (int i = 1; i <= 3; i++) {
            String data = "Data-" + i;
            dataBox.setData(data);
        }
    }

}

public class ConsumerThread extends Thread {
    private DataBox dataBox;

    public ConsumerThread(DataBox dataBox) {
        this.dataBox = dataBox;
    }

    @Override
    public void run() {
        for (int i = 1; i <= 3; i++) {
            String data = dataBox.getData();
        }
    }

}

public class WaitNotifyExam {

    public static void main(String[] args) {
        DataBox dataBox = new DataBox();

        ProducerThread producerThread = new ProducerThread(dataBox);
        ConsumerThread consumerThread = new ConsumerThread(dataBox);

        producerThread.start();
        consumerThread.start();
    }
}

실행 결과

ProducerThread가 생성한 데이터: Data-1
ConsummerThread가 읽은 데이터: Data-1
ProducerThread가 생성한 데이터: Data-2
ConsummerThread가 읽은 데이터: Data-2
ProducerThread가 생성한 데이터: Data-3
ConsummerThread가 읽은 데이터: Data-3


자바의 정석 13장 예제

public class SyncEx {

    public static void main(String[] args) {
        Runnable runnable = new RunnableSync();
        new Thread(runnable).start();
        new Thread(runnable).start();
    }

}

class Account {
    private int balance = 1000; // private으로 해야 동기화 의미가 있다.

    public  int getBalance() {
        return balance;
    }

    public synchronized void withdraw(int money){ // synchronized로 메서드를 동기화
        if(balance >= money) {
            try {
                Thread.sleep(1000);
            } catch(InterruptedException e) {}
            balance -= money;
        }
    } // withdraw
}

class RunnableSync implements Runnable {
    Account acc = new Account();

    public void run() {
        while(acc.getBalance() > 0) {
            // 100, 200, 300중의 한 값을 임으로 선택해서 출금(withdraw)
            int money = (int)(Math.random() * 3 + 1) * 100;
            acc.withdraw(money);
            System.out.println("balance:"+acc.getBalance());
        }
    } // run()
}
 


728x90
블로그 이미지

Link2Me

,
728x90

쓰레드는 자신의 run() 메소드가 모두 실행되면 자동적으로 종료된다.

하지만, 경우에 따라서는 실행 중인 쓰레드를 즉시 종료할 필요가 있다.

public class StopThread extends Thread {
    public void run() {
        while(true) {
            System.out.println("실행 중");
        }
        System.out.println("자원 정리");
        System.out.println("실행 종료");
    }
}

StopThread는 while(true) 이므로 무한 반복을 하게 된다.


쓰레드를 즉시 종료시키기 위해서 stop() 메소드를 제공하고 있는데, 이는 쓰지 않는다(deprecated 됨).
stop() 메소드는 쓰레드가 사용 중이던 자원들이 불완전한 상태로 남겨지기 때문이다.


안전하게 Thread를 종료시키는 방법은

boolean 변수를 사용하는 방법과 interrupted() 메소드를 이용하는 방법이 있다.



interrupted() 메소드를 이용하는 방법 (권장)

interrupt() 메소드는 일시 정지 상태일 때 정지가 된다.
Thread.sleep(1); // 일시 정지 상태일 경우 interruptedException을 발생시킨다.
실행대기 또는 실행상태에서는 interruptedException이 발생하지 않는다.
일시 정지 상태를 만들지 않고 while문을 빠져나오는 방법 즉 쓰레드를 종료시키는 방법이 있다.
Thread.interrupted() 메소드를 이용하면 된다.

public class StopThread extends Thread {

    public void run() {
        while(true) {
            System.out.println("실행 중");
            if(Thread.interrupted()) {
                break;
            }
        }
        System.out.println("자원 정리");
        System.out.println("실행 종료");
    }
}

public class ThreadStopExample {

    public static void main(String[] args) {
        Thread thread = new StopThread();
        thread.start();
       
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
       
        thread.interrupt();
    }
}

1초 뒤에 호출되는 thread.interrupt()에 의해 무한 루프는 종료된다.

하지만 더 깔끔하게 처리하는 방법은 StopThread 의 while 문을 아래와 같이 하는 것이다.

public class StopThread extends Thread {

    public void run() {
        while(!Thread.currentThread().isInterrupted()) {
            System.out.println("실행 중");
        }
        System.out.println("자원 정리");
        System.out.println("실행 종료");
    }
}



boolean 변수를 사용하는 방법

public class StopThread extends Thread {
    private boolean flag = false;
   
    StopThread() {
        this.flag = true; // 생성자에 flag 설정
    }
   
    public void Action(boolean flag) {
        this.flag = flag;
    }

    @Override
    public void run() {
        while(flag) {
            System.out.println("실행 중");
        }
        System.out.println("자원 정리");
        System.out.println("실행 종료");
    }
}

public class ThreadStopExample {

    public static void main(String[] args) {
        Thread thread = new StopThread();
        thread.start();
       
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
       
        ((StopThread) thread).Action(false);
    }
}


728x90
블로그 이미지

Link2Me

,
728x90

실행중인 하나의 프로그램을 프로세스(Process)라고 한다.

프로그램을 실행하면 OS로부터 실행에 필요한 자원(메모리)를 할당받아 프로세스가 된다.

프로세스는 프로그램을 수행하는 데 필요한 데이터와 메모리 등의 자원 그리고 쓰레드로 구성되어 있으며, 프로세스의 자원을 이용해서 실제로 작업을 수행하는 것이 바로 쓰레드이다.

   - 하나의 프로세스 내부에서 독립적으로 실행되는 하나의 작업 단위를 Thread(쓰레드)라고 한다.

   - 모든 프로세스에는 최소한 하나 이상의 쓰레드가 존재하며,

   - 둘 이상의 쓰레드를 가진 프로세스를 멀티 프로세스라고 한다.
     메신저로 채팅을 하면서 파일을 다운로드 받을 수 있는 것은 멀티 쓰레드로 작성되었기 때문이다.

 

Java에서 쓰레드를 구현하는 방법은 두가지가 있다.

public class ThreadBasic {
    public static void main(String[] args) {
        // 1. Thread Class를 상속받는 방법
        Thread t1 = new MyThread1(); // 쓰레드의 생성
        t1.start(); // 쓰레드의 실행. run()이 종료되면 MyThread1 소멸된다.
 
        // 2. Runnable 인터페이스를 구현하는 방법
        Thread t2 = new Thread(new MyThread2());
        t2.start(); // 쓰레드의 실행. run()이 종료되면 MyThread2 소멸된다.
 
    }
}
 
// 1. Thread Class를 상속받는 방법
class MyThread1 extends Thread {
    @Override
    public void run() {
        //작업할 내용
    }
}
 
// 2. Runnable 인터페이스를 구현하는 방법
class MyThread2 implements Runnable {
    @Override
    public void run() {
        //작업할 내용
    }
}
 

 

 

Thread Class를 상속받으면 다른 클래스를 상속 받을 수 없기때문에,

Runnable 인터페이스를 구현하는게 일반적인 방법이다.

Runnable 인터페이스는 run()만 정의되어 있는 간단한 인터페이스이다.

 

Runnable 인터페이스를 구현한 경우, Runnable 인터페이스를 구현한 클래스의 인스턴스를 생성한 다음, 이 인스턴스를 Thread 클래스의 생성자의 매개변수로 제공해야 한다.

 

쓰레드를 생성했다고 해서 자동으로 실행되는 것은 아니다. start() 메소드를 호출해야만 쓰레드가 실행된다.

쓰레드 동작시 run()이 자동적으로 호출된다.

main 메소드의 작업을 수행하는 것도 쓰레드이며, 이를 main 쓰레드라고 한다.

 

 

 

작업할 내용에는 무한 루프 while(flag) { } 를 사용하여 쓰레드의 동작을 제어하도록 한다.

thread를 중단시킬 수 있는 키 값(flag)을 꼭 세팅하여 메인 쓰레드가 종료될 때 같이 종료되도록 하여야 한다.

 

예제1) 쓰레드 기본 동작 확인

public class ThreadEX1 {
    static long startTime = 0;
    public static void main(String[] args) { // ==> main 메소드의 코드를 수행하는 쓰레드
        // 모든 프로세스(실행중인 프로그램)는 최소한 하나의 쓰레드(프로세스 내에서 실제 작업을 수행)를 갖고 있다.
        // 대부분의 프로그램이 멀티쓰레드로 작성되어 있다.
        // 멀티쓰레드 장점 : 1. 시스템 자원의 효율적 사용
        // 2. 사용자에 대한 응답성 향상(채팅 중에 파일 전송)
        // 3. 작업이 분리되어 코드가 간결해진다.
        // 멀티쓰레드 단점 : 1. 동기화에 주의해야 한다. 2. 교착상태가 발생하지 않도록 해야 한다.
        // 3. 각 쓰레드가 효율적으로 고르게 실행될 수 있게 해야 한다.
 
        // 쓰레드를 생성한 후에 start()를 호출해야 쓰레드가 작업을 시작한다.
        ThreadEx1_A t1 = new ThreadEx1_A(); // 쓰레드의 생성
 
        Runnable r = new ThreadEx1_B();
        Thread t2 = new Thread(r);
 
        t1.start(); // 쓰레드 t1을 실행시킨다. ==> 새로운 호출스택 생성
        t2.start(); // 쓰레드 t2를 실행시킨다. ==> 새로운 호출스택 생성
        // 쓰레드는 순차적으로 실행되지 않고 섞여서 실행된다.
        // 실행순서는 OS 스케줄러가 결정한다.
        // 쓰레드는 '사용자 쓰레드' 와 '데몬 쓰레드' 두 종류가 있다.
        // 실행중인 사용자 쓰레드가 하나도 없을 때 프로그램은 종료된다.
 
        startTime = System.currentTimeMillis();
        try {
            t1.join();    // main쓰레드가 t1의 작업이 끝날 때까지 기다린다.
            t2.join();    // main쓰레드가 t2의 작업이 끝날 때까지 기다린다.
        } catch(InterruptedException e) {}
 
        System.out.print("소요시간:" + (System.currentTimeMillis() - ThreadEX1.startTime));
    }
}
 
class ThreadEx1_A extends Thread { // 1. 쓰레드 Class 상속
    public void run() { // Thread 클래스의 run()을 오버라이딩
        for(int i=0; i < 300; i++) {
            //System.out.println(getName()); // 조상인 Thread의 getName()을 호출
            System.out.print(new String("-"));
        }
    }
}
 
class ThreadEx1_B implements Runnable { // 2. Runnable  인터페이스 구현
    public void run() { // Runnable 인터페이스의 추상메소드 run()을 구현
        // run() : 쓰레드가 수행할 작업을 작성
        for(int i=0; i < 300; i++) {
            // Thread.currentThread() - 현재 실행중인 Thread를 반환한다.
            //System.out.println(Thread.currentThread().getName());
            System.out.print(new String("|"));
        }
    }
}

 

예제2) 싱글 쓰레드

public class SingleThread {
    public static void main(String[] args) {
        // main 쓰레드 내에서 for 문이 순차적으로 실행되는 걸 확인할 수 있다.
        long startTime = System.currentTimeMillis();
 
        for(int i=0; i < 300; i++)
            System.out.printf("%s"new String("-"));
 
        System.out.print("소요시간1:" +(System.currentTimeMillis()- startTime));
 
        System.out.println(); // 줄바꿈 표시 목적
 
        for(int i=0; i < 300; i++)
            System.out.printf("%s"new String("|"));
 
        System.out.print("소요시간2:"+(System.currentTimeMillis() - startTime));
    }
}
 

 

예제3) 멀티쓰레드

public class MultiThread {
    static long startTime = 0;
    public static void main(String[] args) {
        // 실행할 때마다 출력되는 두 작업의 결과가 섞이는 걸 확인할 수 있다.
        // 두개의 작업이 번갈아 가면서 실행될 때 context switching이 발생하여
        // 멀티쓰레드시 작업시간이 싱글쓰레드보다 약간 더 소요된다.
        // 하지만 동시 작업 수행이 가능하다(채팅 중에서 파일 다운로드)
        ThreadEx_C t1 = new ThreadEx_C();
        t1.start();
        startTime = System.currentTimeMillis();
 
        for(int i=0; i < 300; i++)
            System.out.printf("%s"new String("-"));
 
        System.out.print("소요시간1:" + (System.currentTimeMillis() - MultiThread.startTime));
    }
}
 
class ThreadEx_C extends Thread {
    public void run() {
        for(int i=0; i < 300; i++)
            System.out.printf("%s"new String("|"));
 
        System.out.print("소요시간2:" + (System.currentTimeMillis() - MultiThread.startTime));
    }
}
 

 

 

main thread
- 모든 자바 프로그램은 메인 쓰레드가 main() 메소드를 실행하면서 시작한다.
- return 을 만나면 실행을 종료한다.
- main 쓰레드는 작업 쓰레드를 만들어서 병렬로 코드를 실행할 수 있다.
  작업 쓰레드를 만들려면, Thread 클래스를 상속하거나 Runnable 인터페이스를 구현한다.
- 멀티 쓰레드는 실행중인 쓰레드가 하나라도 있다면, 프로세스는 종료되지 않는다.
- 멀티 쓰레드 작업 시에는 각 쓰레드 끼리 정보를 주고받을 수 있어 처리 과정의 오류를 줄일 수 있다.
- 프로세스끼리는 정보를 주고 받을 수 없다.


 

데몬(daemon) 쓰레드

- 일반 쓰레드(non-daemon thread)의 작업을 돕는 보조적인 역할을 수행한다.
- 일반 쓰레드가 모두 종료되면 자동적으로 종료된다.
- 가비지 컬렉터, 자동저장, 화면자동갱신 등에 사용된다.
- 무한루프와 조건문을 이용해서 실행 후 대기하다가 특정조건이 만족되면 작업을 수행하고 다시 대기하도록 작성한다.

- setDaemon(boolean on)은 반드시 start()를 호출하기 전에 실행되어야 한다.

 

 

public class DaemonThreadEx implements Runnable {
    static boolean autoSave = false;
 
    public static void main(String[] args) {
        Thread t = new Thread(new DaemonThreadEx()); // Thread(Runnable r)
        // run() 메소드를 가지고 있는 클래스의 객체를 매개변수로 준다.
        t.setDaemon(true); // 이 부분이 없으면 종료되지 않는다.
        t.start();
 
        for(int i=1; i <= 20; i++) {
            try {
                Thread.sleep(1000);
            } catch(InterruptedException e) {
 
            }
            System.out.println(i);
 
            if(i == 5) autoSave = true;
        }
        System.out.println("프로그램을 종료합니다.");
    }
 
    public void run() {
        while(true) { // 무한루프
            try {
                Thread.sleep(3 * 1000); // 3초 마다
            } catch(InterruptedException e) {
 
            }
 
            // autoSava 의 값이 true 이면 autoSave()를 호출한다.
            if(autoSave) autoSave();
        }
    }
 
    private void autoSave() {
        System.out.println("작업 파일이 자동 저장되었습니다.");
    }
}
 
 

 

쓰레드의 생명 주기(Java Thread Life Cycle and Thread States)

 

 

1. New

   - 객체 생성

   - 쓰레드가 만들어진 상태로 아직 start() 메소드가 호출되지 않은 상태다.

 

2. Runnable (실행대기)
   - 쓰레드가 실행되기 위한 준비단계이다.
   - 코드 상에서 start() 메소드를 호출하면 run() 메소드에 설정된 쓰레드가 Runnable 상태로 진입한다.

3. Running (실행상태)
   - CPU를 점유하여 실행하고 있는 상태이며 run() 메서드는 JVM만이 호출 가능하다.
   - Runnable(준비상태)에 있는 여러 쓰레드 중 우선 순위를 가진 쓰레드가 결정되면

   - JVM이 자동으로 run() 메소드를 호출하여 쓰레드가 Running 상태로 진입한다.

4. Terminated (실행종료)
   - Running 상태에서 쓰레드가 모두 실행되고 난 후 완료 상태다.

   - run() 메소드 완료시 쓰레드가 종료되며, 그 쓰레드는 다시 시작할 수 없게 된다.

 

5. Blocked (일시 정지)
   - 사용하고자 하는 객체의 Lock이 풀릴 때까지 기다리는 상태다.
   - wait() 메소드에 의해 Blocked 상태가 된 쓰레드는 notify() 메소드가 호출되면 Runnable 상태로 간다.
   - sleep(시간) 메소드에 의해 Blocked 상태가 된 쓰레드는 지정된 시간이 지나면 Runnable 상태로 간다.

 

join() 메소드는 다른 쓰레드의 종료를 기다리는 것이다.

계산 작업을 하는 쓰레드가 모든 계산 작업을 마쳤을 때, 계산 결과값을 받아 이용하는 경우에 주로 사용한다.

 

 
public class JoinMain {
    public static void main(String[] args) {
        JoinWorkerThread thread = new JoinWorkerThread();
        thread.start(); // 작업 쓰레드 시작
 
        System.out.println("Thread가 종료될때까지 기다린다.");
        try {
            // 별도의 쓰레드에서 처리한 작업이 완료될 때까지 기다려야 하는 경우
            thread.join(); // 해당 작업 쓰레드가 종료될 때까지 main 쓰레드가 대기한다.
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("Thread가 종료되었다.");
    }
}
 
class JoinWorkerThread extends Thread {
 
    @Override
    public void run() {
        for(int i = 0; i < 5; i++){
            System.out.println("JoinWorkerThread : "+ i);
            try {
                Thread.sleep(300);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
 
}

 

실행 결과

Thread가 종료될때까지 기다린다.
JoinWorkerThread : 0
JoinWorkerThread : 1
JoinWorkerThread : 2
JoinWorkerThread : 3
JoinWorkerThread : 4
Thread가 종료되었다.

 

Android 어플을 GitHub 에서 받아서 테스트 해보니 제대로 동작이 잘 안되어 수정해보려고 했으나 쓰레드에 대한 이해가 부족하여 쓰레드 자료/강좌를 찾아서 공부하고 있다.

"이것이 자바다" 유투브 동영상 강좌(https://www.youtube.com/watch?v=1mut1RwpEvw)에 설명이 잘 되어 있다.

Do IT 안드로이드 앱 프로그래밍 책 저자의 동영상을 들었을 때, 자바를 몰라도 안드로이드 어플 만들 수 있다고 한다.

물론 간단한 앱 테스트하는 것은 얼마든지 만들 수 있다.

하지만 응용을 해야 한다거나, 난이도 있는 어플을 만들려고 하면 그 분야의 지식을 깊게 만들어야 어플이 문제없이 동작된다는 걸 절감하게 된다.

728x90
블로그 이미지

Link2Me

,
728x90

"이것이 자바다" 유투브 동영상 강좌(https://www.youtube.com/watch?v=twd6mwUS1Bc)에 나오는 코드를 적고 테스트해보고 기록해둔다.

Socket을 활용하여 Clinet측에서 Server로 일대일 연결을 유지하면서 Client측에서 보낸 메세지를 Server측에서 수신하여 수신 받은 메세지를 다시 Client측으로 송신하는 프로그램 구현이다.




서버 소켓 코드

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.InetSocketAddress;
import java.net.ServerSocket;
import java.net.Socket;

public class ChatServer {

    public static void main(String[] args) {
        ServerSocket serverSocket = null;
        Socket socket = null;

        try {
            // 소켓 생성
            serverSocket = new ServerSocket();
            // 포트 바인딩
            serverSocket.bind(new InetSocketAddress("localhost", 5001));

            while (true) {
                System.out.println("[연결 기다림]");
                // 연결 수락
                socket = serverSocket.accept(); // 클라이언트가 접속해 오기를 기다리고, 접속이 되면 통신용 socket 을 리턴한다.
                // 연결된 클라이언트 IP 주소 얻기
                InetSocketAddress isa = (InetSocketAddress) socket.getRemoteSocketAddress();
                System.out.println("[연결 수락함] " + isa.getHostName());

                byte[] bytes = null;
                String message = null;

                // 데이터 받기
                InputStream is = socket.getInputStream();
                bytes = new byte[100];
                int readByteCount = is.read(bytes);
                message = new String(bytes, 0, readByteCount, "UTF-8");
                System.out.println("[데이터 받기 성공] " + message);

                // 데이터 보내기
                OutputStream os = socket.getOutputStream();
                message = "Hello Client";
                bytes = message.getBytes("UTF-8");
                os.write(bytes);
                os.flush();
                System.out.println("[데이터 보내기 성공]");

                is.close();
                os.close();
                socket.close();
            }
        } catch (Exception e) {
            e.printStackTrace();
        }

        if (!serverSocket.isClosed()) {
            try {
                socket.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }

    }

}



클라이언트 코드

import java.io.InputStream;
import java.io.OutputStream;
import java.net.InetSocketAddress;
import java.net.Socket;

public class ChatClient {

    public static void main(String[] args) {

        Socket socket = null;

        try {
            socket = new Socket();
            System.out.println("[연결 요청]");
            socket.connect(new InetSocketAddress("localhost", 5001));
            System.out.println("[연결 성공]");
           
            byte[] bytes = null;
            String message = null;

            OutputStream os = socket.getOutputStream();
            message = "Hello Server, I'm Client.";
            bytes = message.getBytes("UTF-8");
            os.write(bytes);
            os.flush();
            System.out.println("[데이터 보내기 성공]");
           
            InputStream is = socket.getInputStream();
            bytes = new byte[100];
            int readByteCount = is.read(bytes);
            message = new String(bytes,0,readByteCount,"UTF-8");
            System.out.println("[데이터 받기 성공] " + message);
           
            os.close();
            is.close();
           
        } catch (Exception e) {
            e.printStackTrace();
        }
       
        if (!socket.isClosed()) {
            try {
                socket.close();
            } catch (Exception e) {
                e.printStackTrace();
            }
        }

    }

}


코드를 작성하고 실행하면 콘솔에 나오는 출력 결과를 확인하고 알 수 있다.


GitHub 에 간단하면서도 좋은 예제가 있다.

https://gist.github.com/junsuk5/f0ff2298e17853dc48e89f2dfc7bd985

728x90
블로그 이미지

Link2Me

,
728x90

안드로이드 인터페이스 처리하는 걸 많이 다뤄보지 않아서 그런지 인터페이스 사용을 자유롭게 하지 못한다.


"이것이 자바다" 를 유투브에서 인터페이스 강좌를 찾아서 들어보면 설명을 참 잘 해준다.

https://www.youtube.com/watch?v=8GcOYOd67KY


구글 검색으로 블로그에 적혀 있는 내용만으로 손쉽게 인터페이스 처리 코드를 만들줄 알면 고민되지 않겠지만 대게는 단순한 구현 예시만 나와 아직 초보를 탈출하지 못한 나에겐 어렵다.


로그인한 세션 정보를 받아서 다음 단계를 처리해야 하는 어플을 테스트하다보니 인터페이스 부분이 너무 약해서 막힌다.


가장 먼저 인터페이스를 정의한다.

OnCallbackListener.java

public interface OnCallbackListener<T> {
    void onSuccess(T object);
    void onFailure(Exception e);
}
 


두번째 단계는 위 그림에서 개발코드에 해당되는 SessionProcessTask.java 파일을 구현한다.

import android.content.Context;
import android.os.AsyncTask;

public class SessionProcessTask extends AsyncTask<Void, Void, String> {
    private Context mContext;
    private OnCallbackListener<String> mCallBack;
    public Exception mException;


    // 변수를 먼저 선언하고 ALT+ Insert 키를 생성자 생성시 아래와 같은 순서로 생성자 자동 생성 가능

    public SessionProcessTask(Context mContext, OnCallbackListener<String> mCallBack) {
        this.mContext = mContext;
        this.mCallBack = mCallBack;
    }

    @Override
    protected String doInBackground(Void... params) {

        try {
            // 백그라운드 작업할 코드를 여기에서 구현한다.
            return "Welcome to my blog.";

        } catch (Exception e) {
            mException = e;
        }
        return null;
    }

    @Override
    protected void onPostExecute(String result) {
        if (mCallBack != null) {
            if (mException == null) {
                mCallBack.onSuccess(result);
            } else {
                mCallBack.onFailure(mException);
            }
        }
    }
}


객체를 구현할 MainActivity.java 파일의 구현 내용이다.

import androidx.appcompat.app.AppCompatActivity;

import android.content.Context;
import android.os.Bundle;
import android.widget.Toast;

public class MainActivity extends AppCompatActivity {
    Context mContext;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        mContext = MainActivity.this;

        // 객체 생성
        SessionProcessTask processTask = new SessionProcessTask(mContext, new OnCallbackListener<String>() {
            @Override
            public void onSuccess(String object) {
                Toast.makeText(mContext, "SUCCESS: "+object, Toast.LENGTH_LONG).show();
                ActionActivity android = new ActionActivity(mContext);
                android.btnTouch();
            }

            @Override
            public void onFailure(Exception e) {
                Toast.makeText(mContext, "ERROR: " + e.getMessage(), Toast.LENGTH_LONG).show();
            }
        });
        processTask.execute();
    }

}


onSuccess 결과를 받으면 후속으로 

ActionActivity android = new ActionActivity(mContext);

android.btnTouch();

를 실행하도록 했다.


이제 이와 관련된 코드를 또다른 인터페이스를 통해 처리되도록 구현했다.

// OnLoadListener.java 파일

public interface OnLoadListener { 
    public void onLoadFinish();
}

// ImageLoader.java 파일

import android.content.Context;
import android.widget.Toast;

public class ImageLoader {
    private Context mContext;
    private OnLoadListener loadListener;

    public ImageLoader(Context context, OnLoadListener loadListener) {
        mContext = context;
        this.loadListener = loadListener;
    }

    public void start(){
        try {
            System.out.println("이미지를 로딩합니다.");
            Toast.makeText(mContext, "이미지를 로딩합니다.", Toast.LENGTH_LONG).show();
            Thread.sleep(1000);
            loadListener.onLoadFinish();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

// ActionActivity.java 파일

import android.content.Context;
import android.widget.Toast;

public class ActionActivity implements OnLoadListener {
    private Context mContext;

    public ActionActivity(Context mContext) {
        this.mContext = mContext;
    }

    public void btnTouch(){
        ImageLoader imageLoader = new ImageLoader(mContext,this);
        imageLoader.start();
    }

    @Override
    public void onLoadFinish() {
        Toast.makeText(mContext, "토스트 팝업을 띄웁니다.", Toast.LENGTH_LONG).show();
        System.out.println("토스트 팝업을 띄웁니다.");
    }
}


Log 메시지를 통해서 확인할 수도 있지만, Toast 메시지 창을 띄워서 순차적으로 실행되는 결과를 확인할 수 있게 했다.

아직 응용력이 떨어지는 나를 위해 테스트하고 적어둔다.

구글링하다가 발견한 좋은 글이 와 닿는다.

인터넷에서 짜집기한 것만 읽지 말고, 책을 읽어라. 같은 책을 여러 번 읽어라. 꾸준히 해라.

하지만, 요즈음에는 "유투브 강좌를 열심히 봐라" 라고 하는게 더 맞을 거 같다.

좋은 강좌들이 너무 많이 오픈되어 있다.


테스트에 사용한 코드

Interface_src.zip


728x90

'안드로이드 > Interface' 카테고리의 다른 글

Android 인터페이스 상속 ISerialListener 예제  (0) 2019.12.12
Java Interface 예제  (0) 2019.11.18
Java Interface Example  (0) 2019.09.05
Java 인터페이스(interface) 개요  (0) 2019.08.20
Android Interface 예제 ★★★  (0) 2018.08.22
블로그 이미지

Link2Me

,
728x90

https://developers.kakao.com/docs/android/user-management 에 나온 public class GlobalApplication extends Application {} 부분을 참조하여 코드를 구현한다.

 

테스트에 사용한 소스 코드는 맨 하단에 첨부했다.


GlobalApplication.java

package com.link2me.android.auth_kakao;

import android.app.Application;
import android.content.Context;

import com.kakao.auth.ApprovalType;
import com.kakao.auth.AuthType;
import com.kakao.auth.IApplicationConfig;
import com.kakao.auth.ISessionConfig;
import com.kakao.auth.KakaoAdapter;
import com.kakao.auth.KakaoSDK;

public class GlobalApplication extends Application {
    private static GlobalApplication instance;

    public static final GlobalApplication getGlobalApplicationContext() {
        if (instance == null)
            throw new IllegalStateException("this application does not inherit com.kakao.GlobalApplication");
        return instance;
    }

    protected static class KakaoSDKAdapter extends KakaoAdapter {
        /**
         * Session Config에 대해서는 default값들이 존재한다.
         * 필요한 상황에서만 override해서 사용하면 됨.
         *
         * @return Session의 설정값.
         */
        @Override
        public ISessionConfig getSessionConfig() {
            return new ISessionConfig() {
                @Override
                public AuthType[] getAuthTypes() {
                    return new AuthType[]{AuthType.KAKAO_LOGIN_ALL};
                }
                @Override
                public boolean isUsingWebviewTimer() {
                    return false;
                }
                @Override
                public boolean isSecureMode() {
                    return false;
                }
                @Override
                public ApprovalType getApprovalType() {
                    return ApprovalType.INDIVIDUAL;
                }
                @Override
                public boolean isSaveFormData() {
                    return true;
                }
            };
        }

        @Override
        public IApplicationConfig getApplicationConfig() {
            return new IApplicationConfig() {
                @Override
                public Context getApplicationContext() {
                    return GlobalApplication.getGlobalApplicationContext();
                }
            };
        }
    }

    @Override
    public void onCreate() {
        super.onCreate();
        instance = this;
        KakaoSDK.init(new KakaoSDKAdapter());
    }

}


activity_kakao_login.xml

<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".SampleLoginActivity">

    <com.kakao.usermgmt.LoginButton
        android:id="@+id/com_kakao_login"
        android:layout_width="300dp"
        android:layout_height="wrap_content"
        android:layout_marginLeft="20dp"
        android:layout_marginRight="20dp"
        android:layout_marginTop="250dp"
        android:layout_marginBottom="30dp"
        android:layout_weight="1"/>

</FrameLayout>



LoginActivity.java

package com.link2me.android.auth_kakao;

import android.content.Context;
import android.content.Intent;
import android.os.Bundle;

import androidx.appcompat.app.AppCompatActivity;

import com.kakao.auth.ISessionCallback;
import com.kakao.auth.Session;
import com.kakao.util.exception.KakaoException;
import com.kakao.util.helper.log.Logger;

public class LoginActivity extends AppCompatActivity {
    static final String TAG = LoginActivity.class.getSimpleName();
    Context mContext;
    private SessionCallback callback;
    /**
     * 로그인 버튼을 클릭 했을시 access token을 요청하도록 설정한다.
     *
     * @param savedInstanceState 기존 session 정보가 저장된 객체
     */

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        mContext = LoginActivity.this;

        callback = new SessionCallback();
        Session.getCurrentSession().addCallback(callback);
        Session.getCurrentSession().checkAndImplicitOpen();
    }

    @Override
    protected void onActivityResult(int requestCode, int resultCode, Intent data) {
        if (Session.getCurrentSession().handleActivityResult(requestCode, resultCode, data)) {
            return;
        }
        super.onActivityResult(requestCode, resultCode, data);
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        Session.getCurrentSession().removeCallback(callback);
    }

    private class SessionCallback implements ISessionCallback {
        @Override
        public void onSessionOpened() {
            redirectSignupActivity();
        }
        @Override
        public void onSessionOpenFailed(KakaoException exception) {
            if (exception != null) {
                Logger.e(exception);
            }
        }
    }

    protected void redirectSignupActivity() {
        final Intent intent = new Intent(this, MainActivity.class);
        startActivity(intent);
        finish();
    }

}


activity_main.xml

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    tools:context=".MainActivity">

    <TextView
        android:id="@+id/tv_signup"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_gravity="center"
        android:gravity="center"
        android:layout_marginTop="50dp"
        android:textSize="14sp"
        android:text="SignupActivity" />

    <Button
        android:id="@+id/btn_logout"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="center"
        android:gravity="center"
        android:layout_marginTop="20dp"
        android:text="Logout"/>

</LinearLayout>
 



MainActivity.java

package com.link2me.android.auth_kakao;

import android.content.DialogInterface;
import android.content.Intent;
import android.os.Bundle;
import android.util.Log;
import android.view.View;
import android.widget.Button;
import android.widget.TextView;

import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.app.AppCompatActivity;

import com.kakao.network.ErrorResult;
import com.kakao.usermgmt.UserManagement;
import com.kakao.usermgmt.callback.LogoutResponseCallback;
import com.kakao.usermgmt.callback.MeV2ResponseCallback;
import com.kakao.usermgmt.callback.UnLinkResponseCallback;
import com.kakao.usermgmt.response.MeV2Response;
import com.kakao.util.helper.log.Logger;

import java.util.ArrayList;
import java.util.List;

public class MainActivity extends AppCompatActivity {
    static final String TAG = MainActivity.class.getSimpleName();

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        TextView textView = findViewById(R.id.tv_signup);
        textView.setText("로그인 성공");
        requestMe();

        Button logout_btn = findViewById(R.id.btn_logout);
        logout_btn.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                onClickLogout();
            }
        });
    }

    private void onClickLogout() {
        UserManagement.getInstance().requestLogout(new LogoutResponseCallback() {
            @Override
            public void onCompleteLogout() {
                redirectLoginActivity();
            }
        });
    }

    private void redirectLoginActivity() {
        final Intent intent = new Intent(this, LoginActivity.class);
        startActivity(intent);
        finish();
    }

    private void requestMe() {
        // https://developers.kakao.com/apps/361201/settings/user
        // 앱 생성된 이름 선택(테스트 앱) → 사용자 관리 → 접근권한관리항목 설정
        List<String> keys = new ArrayList<>();
        keys.add("properties.nickname");
        keys.add("properties.profile_image");
        keys.add("kakao_account.email");

        UserManagement.getInstance().me(keys, new MeV2ResponseCallback() {
            @Override
            public void onFailure(ErrorResult errorResult) {
                String message = "failed to get user info. msg=" + errorResult;
                Logger.d(message);
            }

            @Override
            public void onSessionClosed(ErrorResult errorResult) {
                redirectLoginActivity();
            }

            @Override
            public void onSuccess(MeV2Response response) {
                Log.e(TAG,"user id : " + response.getId());
                Log.e(TAG,"nickname : " + response.getNickname());
                Log.e(TAG,"email: " + response.getKakaoAccount().getEmail());
                //Logger.d("profile image: " + response.getKakaoAccount().getProfileImagePath());
                //redirectMainActivity();
            }

        });
    }

    private void onClickUnlink() {
        // 앱 연결 해제는 카카오 플랫폼에 연결된 사용자와 앱의 연결을 영구 해제함으로서 일반적으로 사용자가 앱 탈퇴 요청을 하는 경우와 비슷하다.
        // 앱 연결 해제가 수행된 사용자는 영구적으로 복구가 불가능하며 카카오 플랫폼 서비스를 더이상 사용할 수 없다.
        // 단, 다시 앱 연결을 통해 새로운 데이터로 카카오 플랫폼 서비스를 이용할 수는 있다.
        final String appendMessage = getString(R.string.com_kakao_confirm_unlink);
        new AlertDialog.Builder(this)
                .setMessage(appendMessage)
                .setPositiveButton(getString(R.string.com_kakao_ok_button),
                        new DialogInterface.OnClickListener() {
                            @Override
                            public void onClick(DialogInterface dialog, int which) {
                                UserManagement.getInstance().requestUnlink(new UnLinkResponseCallback() {
                                    @Override
                                    public void onFailure(ErrorResult errorResult) {
                                        Logger.e(errorResult.toString());
                                    }

                                    @Override
                                    public void onSessionClosed(ErrorResult errorResult) {
                                        redirectLoginActivity();
                                    }

                                    @Override
                                    public void onNotSignedUp() {
                                        LoginActivity loginActivity = new LoginActivity();
                                        loginActivity.redirectSignupActivity();
                                    }

                                    @Override
                                    public void onSuccess(Long userId) {
                                        redirectLoginActivity();
                                    }
                                });
                                dialog.dismiss();
                            }
                        })
                .setNegativeButton(getString(R.string.com_kakao_cancel_button),
                        new DialogInterface.OnClickListener() {
                            @Override
                            public void onClick(DialogInterface dialog, int which) {
                                dialog.dismiss();
                            }
                        }).show();

    }

}



소스코드

- src/main/res/values/strings.xml 을 수정한다.

<string name="kakao_app_key">본인의 앱 키를 적으세요</string>


auth_kakao.zip


728x90
블로그 이미지

Link2Me

,
728x90

https://developers.kakao.com/ 에 접속하면 연동에 필요한 기본 사항이 설명되어 있다.

 

스크롤해서 내리면 Gradle 환경에서 사용하기 부분이 나온다.

 

 

프로젝트 build.gradle 에 추가할 사항

allprojects {
    repositories {
        google()
        jcenter()
        //kakao
        mavenCentral()
        maven { url 'http://devrepo.kakao.com:8088/nexus/content/groups/public/' }
    }
}

 

앱 build.gradel 에 추가할 사항

dependencies {
    implementation fileTree(dir: 'libs', include: ['*.jar'])

    implementation 'androidx.appcompat:appcompat:1.1.0'
    implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
    implementation group: 'com.kakao.sdk', name: 'usermgmt', version: '1.14.0'
}
 

 

AndroidManifest.xml

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.link2me.android.auth_kakao">

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

    <application
        android:name=".GlobalApplication"
        android:allowBackup="false"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:supportsRtl="true"
        android:theme="@style/AppTheme">
        <activity android:name=".SampleLoginActivity">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>

        <meta-data
            android:name="com.kakao.sdk.AppKey"
            android:value="@string/kakao_app_key" />

        <activity
            android:name="com.kakao.auth.authorization.authcode.KakaoWebViewActivity"
            android:launchMode="singleTop"
            android:windowSoftInputMode="adjustResize">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.DEFAULT" />
            </intent-filter>
        </activity>
    </application>

</manifest>
 

 

 

앱 생성

1. 내 애플리케이션 > 앱 만들기를 통해 앱을 생성한다.

   - 테스트 앱으로 생성했다.

 

2. Android SDK 사용시 네이티브 앱 키

   - 생성한 앱의 res/values/strings.xml 파일 내에 키를 복사하여 붙여넣기 한다.

<resources>
    <string name="app_name">Auth_KaKaO</string>
    <string name="kakao_app_key">1e6fd36c1b12AAA2dc22346c3544516f</string>
</resources>

 

3. 플랫폼 추가

   - 플랫폼 추가를 눌러서 패키지명을 등록한다.

 

 

키 해시를 등록해야 하는데

Commandline으로 키해시 구하기 로 했더니 openssl 에 대한 에러 메시지가 나오더라.

그래서 앱 내 자바 코드로 키 해시 구하기 메소드를 이용해서 구했다.

 

import android.content.pm.Signature;
import android.os.Bundle;
import android.util.Base64;
import android.util.Log;

import androidx.appcompat.app.AppCompatActivity;

import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;

import static com.kakao.util.helper.Utility.getPackageInfo;

public class GetHashKey extends AppCompatActivity {
    static final String TAG = GetHashKey.class.getSimpleName();

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_get_hash_key);

        System.out.println("key Hash : " +  getKeyHash(GetHashKey.this));
    }

    public static String getKeyHash(final Context context) {
        PackageInfo packageInfo = getPackageInfo(context, PackageManager.GET_SIGNATURES);
        if (packageInfo == null)
            return null;

        for (Signature signature : packageInfo.signatures) {
            try {
                MessageDigest md = MessageDigest.getInstance("SHA");
                md.update(signature.toByteArray());
                return Base64.encodeToString(md.digest(), Base64.NO_WRAP);
            } catch (NoSuchAlgorithmException e) {
                Log.w(TAG, "Unable to get MessageDigest. signature=" + signature, e);
            }
        }
        return null;
    }
}

 

728x90
블로그 이미지

Link2Me

,
728x90

GridCellAdapter 대신에 RecyclerView 기반 CalendarAdapter를 만들면 어떤 사항들이 변경되는지 알아보자.


RecyclerView 는 ListView 보다 메모리, 성능이 개선된 것으로 데이터 양이 많은 경우 스크롤을 효율적으로 수행할 수 있는 위젯이다.


앱 build.gradle 수정사항

dependencies {
    implementation fileTree(dir: 'libs', include: ['*.jar'])
    implementation 'androidx.appcompat:appcompat:1.1.0'
    implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
    implementation 'androidx.recyclerview:recyclerview:1.0.0'
    implementation 'androidx.cardview:cardview:1.0.0'
    implementation 'gun0912.ted:tedpermission:2.0.0'
    implementation 'androidx.multidex:multidex:2.0.0' // minSdkVersion 이 21 이하인 경우 사용
    implementation files('libs/icu4j-4_8_2.jar')
}



activity_main.xml

<GridView
    android:id="@+id/calendar"
    android:layout_width="fill_parent"
    android:layout_height="wrap_content"
    android:scrollbars="horizontal"
    android:numColumns="7" >
</GridView>

<androidx.recyclerview.widget.RecyclerView
    android:id="@+id/calendar"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:layout_centerVertical="true"
    android:adjustViewBounds="false"
    android:focusableInTouchMode="true"
    android:scrollbars="vertical" />


MainActivity.java

    private GridView calendarView;
    private GridCellAdapter adapter;


        calendarView = (GridView) this.findViewById(R.id.calendar);

        makeCalendarList(month, year);

        // Initialised
        adapter = new GridCellAdapter(mContext, calList);
        adapter.notifyDataSetChanged();
        calendarView.setAdapter(adapter);
    }

    private void setGridCellAdapterToDate(int month, int year) {
        makeCalendarList(month, year);
        adapter = new GridCellAdapter(mContext, calList);
        adapter.notifyDataSetChanged();
        calendarView.setAdapter(adapter);
        // 상단 화면 날짜 출력
        cal.set(year, month -1, cal.get(Calendar.DAY_OF_MONTH));
        currentMonth.setText(DateFormat.format(dateTemplate, cal.getTime()));
    }

    private RecyclerView calendarView;
    private RecyclerView.Adapter adapter;

        calendarView = this.findViewById(R.id.calendar);
        calendarView.setHasFixedSize(true);
        calendarView.setLayoutManager(new GridLayoutManager(mContext,7));

        makeCalendarList(month, year);

        // Initialised
        adapter = new CalendarAdapter(mContext, calList);
        adapter.notifyDataSetChanged();
        calendarView.setAdapter(adapter);
    }

    private void setGridCellAdapterToDate(int month, int year) {
        makeCalendarList(month, year);
        adapter = new CalendarAdapter(mContext, calList);
        adapter.notifyDataSetChanged();
        calendarView.setAdapter(adapter);
        // 상단 화면 날짜 출력
        cal.set(year, month -1, cal.get(Calendar.DAY_OF_MONTH));
        currentMonth.setText(DateFormat.format(dateTemplate, cal.getTime()));
    }


이제 CalendarAdapter 만드는 방법이다.

public class CalendarAdapter extends RecyclerView.Adapter<CalendarAdapter.CustomViewHolder> {

    @NonNull
    @Override
    public CalendarAdapter.CustomViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
        return null;
    }

    @Override
    public void onBindViewHolder(@NonNull CalendarAdapter.CustomViewHolder holder, int position) {

    }

    @Override
    public int getItemCount() {
        return 0;
    }

    public class CustomViewHolder extends RecyclerView.ViewHolder {
        public CustomViewHolder(@NonNull View itemView) {
            super(itemView);
        }
    }
} 


아래 이미지와 같은 과정을 거치면 위와 같은 코드가 만들어진다.








이제 완성된 코드를 적는다.

package com.link2me.android.adpater;

import android.content.Context;
import android.graphics.Color;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.LinearLayout;
import android.widget.TextView;

import androidx.annotation.NonNull;
import androidx.recyclerview.widget.RecyclerView;

import com.link2me.android.calendar.R;
import com.link2me.android.item.Calendar_Item;
import com.link2me.android.util.CalendarHelper;

import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.LinkedHashMap;

public class CalendarAdapter extends RecyclerView.Adapter<CalendarAdapter.CustomViewHolder> {
    static final String tag = CalendarAdapter.class.getSimpleName();
    Context mContext;

    private LinkedHashMap<String, Calendar_Item> calList;
    private String[] mKeys;

    private final SimpleDateFormat dateFormatter = new SimpleDateFormat("yyyyMMdd");

    public CalendarAdapter(Context context, LinkedHashMap<String, Calendar_Item> list) {
        mContext = context;
        calList = list;
        mKeys = calList.keySet().toArray(new String[list.size()]);
    }

    @NonNull
    @Override
    public CalendarAdapter.CustomViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
        View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.calendar_cell, parent, false);
        CustomViewHolder holder = new CustomViewHolder(view);
        return holder;
    }

    @Override
    public void onBindViewHolder(@NonNull final CalendarAdapter.CustomViewHolder holder, int position) {
        String theday = calList.get(mKeys[position]).getDay();
        String themonth = calList.get(mKeys[position]).getMonth();
        String theyear = calList.get(mKeys[position]).getYear();
        String holiday = calList.get(mKeys[position]).getEvent();

        holder.gridcell_Day.setText(theday);
        holder.gridcell_LunarDay.setText(CalendarHelper.Sol2Lun(theyear, String.valueOf(Integer.parseInt(themonth)-1),theday)); //0 ~ 11월로 인식하므로 - 1
        if(holiday.length()>0){
            holder.gridcell_Event.setText(holiday);
        } else {
            holder.gridcell_Event.setText("");
        }
        holder.gridcell_layout.setTag(theday + "-" + themonth + "-" + theyear);

        if(calList.get(mKeys[position]).getColor().equals("GRAY")){
            holder.gridcell_Day.setTextColor(Color.LTGRAY);
        }
        if(calList.get(mKeys[position]).getColor().equals("BLACK")){
            holder.gridcell_Day.setTextColor(Color.BLACK);
        }
        if(calList.get(mKeys[position]).getColor().equals("RED")){
            holder.gridcell_Day.setTextColor(Color.RED);
            holder.gridcell_Event.setTextColor(Color.RED);
        }
        if(calList.get(mKeys[position]).getColor().equals("BLUE")){
            holder.gridcell_Day.setTextColor(Color.BLUE);
        }
        if(calList.get(mKeys[position]).getColor().equals("CYAN")){
            holder.gridcell_layout.setBackgroundColor(Color.CYAN);
        }

        String tagData = theyear+themonth+theday;
        holder.itemView.setTag(tagData);
        holder.itemView.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                String date_month_year = (String) view.getTag();
                Log.e("Selected date", date_month_year);
                try {
                    Date parsedDate = dateFormatter.parse(date_month_year);
                    Log.d(tag, "Parsed Date: " + parsedDate.toString());

                } catch (ParseException e) {
                    e.printStackTrace();
                }
            }
        });
    }

    @Override
    public int getItemCount() {
        return calList.size();
    }

    public class CustomViewHolder extends RecyclerView.ViewHolder {
        protected LinearLayout gridcell_layout;
        protected TextView gridcell_Day;
        protected TextView gridcell_LunarDay;
        protected TextView gridcell_Event;

        public CustomViewHolder(@NonNull View itemView) {
            super(itemView);
            gridcell_layout = (LinearLayout) itemView.findViewById(R.id.calendar_day_gridcell);
            gridcell_Day = itemView.findViewById(R.id.dayTV);
            gridcell_LunarDay = itemView.findViewById(R.id.lunardayTV);
            gridcell_Event = itemView.findViewById(R.id.eventTV);
        }
    }
}
 


커스텀 달력 만드는 방법에 대한 충분한 설명이 되었을 것이라고 본다.


https://www.youtube.com/watch?v=kNq9w1_nhL4 를 강좌를 보면 RecyclerView 에 대한 설명을 잘 해주고 있으니 사용법에 대해 잘 모르면 보면 도움될 수 있다.

728x90
블로그 이미지

Link2Me

,
728x90

RecyclerView: No layout manager attached; skipping layout

라는 로그를 출력하고 있다.

무엇이 문제일까?


calendarView = this.findViewById(R.id.calendar);

바로 밑에다가


calendarView.setHasFixedSize(true);
calendarView.setLayoutManager(new GridLayoutManager(mContext,7));

두줄을 추가해주면 해결된다.


728x90
블로그 이미지

Link2Me

,
728x90

안드로이드 AlarmManager 를 이용한 알람 예제를 테스트 하고 적어둔다.

서비스 개념을 이해하는데에는 도움이 되지만 완벽한 앱 알람 기능을 해결해주진 않는거 같다.

Job Scheduler 로 기능을 구현해 봐야 하나 싶은건 아닌가 싶기도 한데 아직 안해봐서 ....


출처는 https://sh-itstory.tistory.com/64 에서 제공한 https://github.com/aiex1234/PleaseWakeMeUp 에서 파일을 다운로드 받았다. 코드 덕분에 좀 더 편하게 활용 테스트가 가능했다.


삼성 갤럭시 S10(Android 9) 와 LG G5(Android 6.01) 두가지 폰에서 테스트했고 여타 자료를 참조하여 코드를 보강하였다.

알람을 설정한 시간이 되면 음악이 자동 재생되고, Stop 버튼을 누르면 재생이 멈추는 기능은 완벽하게 동작한다.

서비스에 대한 개념 이해는 https://link2me.tistory.com/1343 참조하면 된다.


Notification 메시지 보내는 것은 https://link2me.tistory.com/1514 에 올려진 파일을 받아서 androidX 로 변환해서 사용하면 100% 동작한다.

알람메시지를 받으면 띄워줄 Activity 가 MainActivity 이므로 MainActivity.class 로 수정하고, MainActivity.java 파일내에

        NotificationHelper notificationHelper = new NotificationHelper(mContext);
        notificationHelper.cancelNotification(mContext,0);
를 추가해주면 어플이 실행되고 있지 않아도 지정된 시간이 되면 Broadcast 메시지가 동작하여 음악을 재생하고 Notification 을 보내준다.

누르면 MainActivity 화면이 나오고, 종료 버튼을 누르면 재생되던 음악이 종료된다.


날짜를 지정하고, 시간을 설정하는 것까지 추가하면 알람 앱 기본 기능으론 충분하다고 본다.


앱 build.gradle

apply plugin: 'com.android.application'

android {
    compileSdkVersion 28

    defaultConfig {
        applicationId "com.link2me.android.wakemeup"
        minSdkVersion 19
        targetSdkVersion 28
        versionCode 1
        versionName "1.0"
    }

    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
        }
    }

}

dependencies {
    implementation fileTree(dir: 'libs', include: ['*.jar'])
    implementation 'androidx.appcompat:appcompat:1.0.2'
    implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
}


기본적으로 디바이스를 종료하면 모든 알람은 취소된다.

이러한 상황이 벌어지는 것을 막기 위해, 유저가 디바이스를 재부팅했을 때 앱이 자동적으로 알람을 재구동 하도록 설계할 수 있다.

앱 매니페스트에 RECEIVE_BOOT_COMPLETED 권한을 설정한다. 이 액션은 시스템 부팅 완료 후 브로드캐스트인 ACTION_BOOT_COMPLETED 을 받을 수 있도록 해준다.

이와 관련된 코드 구현은 하지 않았다.


AndroidManifest.xml

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.link2me.android.wakemeup">

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

    <application
        android:allowBackup="true"
        android:icon="@drawable/icon_watch"
        android:label="@string/app_name"
        android:supportsRtl="true"
        android:theme="@style/AppTheme">
        <activity android:name=".MainActivity">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
        <receiver android:name=".AlarmReceiver" />

        <service
            android:name=".RingtoneService"
            android:enabled="true"></service>

    </application>

</manifest>


activity_main.xml

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout 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"
    android:orientation="vertical"
    android:gravity="top">

    <LinearLayout
        android:orientation="vertical"
        android:layout_width="match_parent"
        android:layout_height="wrap_content">

        <TimePicker
            android:id="@+id/time_picker"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content" />

    </LinearLayout>

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_gravity="center"
        android:gravity="center">

        <Button
            android:layout_width="60dp"
            android:layout_height="50dp"
            android:id="@+id/btn_alarmStart"
            android:text="시작"/>

        <Button
            android:layout_width="60dp"
            android:layout_height="50dp"
            android:id="@+id/btn_alarmFinish"
            android:text="종료" />

    </LinearLayout>

    <TextView
        android:id="@+id/tv_alarmON"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_gravity="center"
        android:gravity="center"
        android:layout_marginTop="30dp"
        android:text="알람 예정 시간"
        android:textSize="14dp"
        android:textColor="@color/colorAccent"/>

</LinearLayout>


MainActivity.java

package com.link2me.android.wakemeup;

import android.app.Activity;
import android.app.AlarmManager;
import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.os.Build;
import android.os.Bundle;
import android.util.Log;
import android.view.View;
import android.widget.Button;
import android.widget.TextView;
import android.widget.TimePicker;
import android.widget.Toast;

import androidx.annotation.RequiresApi;
import androidx.appcompat.app.AppCompatActivity;

import com.link2me.android.common.BackPressHandler;

import java.util.Calendar;

public class MainActivity extends AppCompatActivity {
    private static final String TAG = MainActivity.class.getSimpleName();
    Context mContext;
    TextView textView;
    TimePicker alarm_timepicker;
    AlarmManager alarm_manager;
    String am_pm;
    int getHourTimePicker = 0;
    int getMinuteTimePicker = 0;

    Intent alarmIntent;
    PendingIntent pendingIntent;
    private static final int REQUEST_CODE = 1111;
    SharedPreferences pref;

    private BackPressHandler backPressHandler;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        mContext = MainActivity.this;
        backPressHandler = new BackPressHandler(this); // 뒤로 가기 버튼 이벤트

        textView = findViewById(R.id.tv_alarmON);
        textView.setText("");

        // 알람매니저 설정
        alarm_manager = (AlarmManager) mContext.getSystemService(ALARM_SERVICE);
        // 알람매니저가 유용한 이유는, Alarm이 한번 등록되면 어플리케이션의 생명주기와 관계없이
        // 어플리케이션이 종료되어있는 경우에도 지정해놓은 operation에 대해 어김없이 실행할 수 있다는 것

        // 타임피커 설정
        alarm_timepicker = findViewById(R.id.time_picker);
        alarm_timepicker.setIs24HourView(true);

        // 알람 Receiver 인텐트 생성
        alarmIntent = new Intent(mContext, AlarmReceiver.class);

        // Button Alarm ON
        Button alarm_on = findViewById(R.id.btn_alarmStart);
        alarm_on.setOnClickListener(new View.OnClickListener() {
            @RequiresApi(api = Build.VERSION_CODES.M)
            @Override
            public void onClick(View v) {
                setAlarm(mContext);
            }
        });

        Button alarm_off = findViewById(R.id.btn_alarmFinish);
        alarm_off.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                releaseAlarm(mContext);
            }
        });
    }

    private void setAlarm(Context context){
        // Calendar 객체 생성
        final Calendar calendar = Calendar.getInstance();

        // calendar에 시간 셋팅
        if (Build.VERSION.SDK_INT < 23) {
            // 시간 가져옴
            getHourTimePicker = alarm_timepicker.getCurrentHour();
            getMinuteTimePicker = alarm_timepicker.getCurrentMinute();
        } else {
            // 시간 가져옴
            getHourTimePicker = alarm_timepicker.getHour();
            getMinuteTimePicker = alarm_timepicker.getMinute();
        }

        // 현재 지정된 시간으로 알람 시간 설정
        calendar.setTimeInMillis(System.currentTimeMillis());
        calendar.set(Calendar.HOUR_OF_DAY, getHourTimePicker);
        calendar.set(Calendar.MINUTE, getMinuteTimePicker);
        calendar.set(Calendar.SECOND, 0);

        pref = getSharedPreferences("pref", Activity.MODE_PRIVATE);
        SharedPreferences.Editor editor = pref.edit();
        editor.putInt("set_hour", getHourTimePicker);
        editor.putInt("set_min", getMinuteTimePicker);
        editor.putString("state", "ALARM_ON");
        editor.commit();

        // reveiver에 string 값 넘겨주기
        alarmIntent.putExtra("state","ALARM_ON");

        // receiver를 동작하게 하기 위해 PendingIntent의 인스턴스를 생성할 때, getBroadcast 라는 메소드를 사용
        // requestCode는 나중에 Alarm을 해제 할때 어떤 Alarm을 해제할지를 식별하는 코드
        pendingIntent = PendingIntent.getBroadcast(mContext,REQUEST_CODE,alarmIntent,PendingIntent.FLAG_UPDATE_CURRENT);

        long currentTime = System.currentTimeMillis(); // 현재 시간
        //long triggerTime = SystemClock.elapsedRealtime() + 1000*60;
        long triggerTime = calendar.getTimeInMillis(); // 알람을 울릴 시간
        long interval = 1000 * 60 * 60  * 24; // 하루의 시간

        while(currentTime > triggerTime){ // 현재 시간보다 작다면
            triggerTime += interval; // 다음날 울리도록 처리
        }
        Log.e(TAG, "set Alarm : " + getHourTimePicker + " 시 " + getMinuteTimePicker + "분");

        // 알림 세팅 : AlarmManager 인스턴스에서 set 메소드를 실행시키는 것은 단발성 Alarm을 생성하는 것
        // RTC_WAKEUP : UTC 표준시간을 기준으로 하는 명시적인 시간에 intent를 발생, 장치를 깨움
        if (Build.VERSION.SDK_INT < 23) {
            if (Build.VERSION.SDK_INT >= 19) {
                alarm_manager.setExact(AlarmManager.RTC_WAKEUP, triggerTime, pendingIntent);
            } else {
                // 알람셋팅
                alarm_manager.set(AlarmManager.RTC_WAKEUP, triggerTime, pendingIntent);
            }
        } else {  // 23 이상
            alarm_manager.setExactAndAllowWhileIdle(AlarmManager.RTC_WAKEUP, triggerTime, pendingIntent);
            //alarm_manager.set(AlarmManager.RTC_WAKEUP, triggerTime,pendingIntent);
            //알람 매니저를 통한 반복알람 설정
            //alarm_manager.setRepeating(AlarmManager.RTC, triggerTime, interval, pendingIntent);
            // interval : 다음 알람이 울리기까지의 시간
        }

        // Unable to find keycodes for AM and PM.
        if(getHourTimePicker > 12){
            am_pm = "오후";
            getHourTimePicker = getHourTimePicker - 12;
        } else {
            am_pm ="오전";
        }
        textView.setText("알람 예정 시간 : "+ am_pm +" "+ getHourTimePicker + "시 " + getMinuteTimePicker + "분");
    }

    public void releaseAlarm(Context context)  {
        Log.e(TAG, "unregisterAlarm");

        pref = getSharedPreferences("pref", Activity.MODE_PRIVATE);
        SharedPreferences.Editor editor = pref.edit();
        editor.putString("state", "ALARM_OFF");
        editor.commit();

        // 알람매니저 취소
        alarm_manager.cancel(pendingIntent);
        alarmIntent.putExtra("state","ALARM_OFF");

        // 알람 취소
        sendBroadcast(alarmIntent);

        Toast.makeText(MainActivity.this,"Alarm 종료",Toast.LENGTH_SHORT).show();
        textView.setText("");
    }

    @Override
    public void onBackPressed() {
        backPressHandler.onBackPressed();
    }

}


AlarmReceiver.java

package com.link2me.android.wakemeup;

import android.annotation.SuppressLint;
import android.app.Activity;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.media.MediaPlayer;
import android.os.PowerManager;
import android.util.Log;

public class AlarmReceiver extends BroadcastReceiver{
    private static final String TAG = AlarmReceiver.class.getSimpleName();
    Context mContext;

    PowerManager powerManager;
    private static PowerManager.WakeLock wakeLock;
    SharedPreferences pref;
    String get_state;
    private MediaPlayer mediaPlayer;

    @Override
    public void onReceive(Context context, Intent intent) {
        mContext = context;
        pref = context.getSharedPreferences("pref", Activity.MODE_PRIVATE);
        get_state = pref.getString("state","");
        Log.e(TAG, "Alarm state : " + get_state);

        AlarmReceiverChk(context, intent);
    }

    private void AlarmReceiverChk(final Context context, final Intent intent){
        Log.d(TAG, "Alarm Receiver started!");
        switch (get_state){
            case "ALARM_ON":
                acquireCPUWakeLock(context, intent);
                // RingtoneService 서비스 intent 생성
                Intent serviceIntent = new Intent(mContext, RingtoneService.class);
                serviceIntent.putExtra("state", get_state);
                if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O){
                    context.startForegroundService(serviceIntent);
                } else {
                    context.startService(serviceIntent);
                }
                break;
            case "ALARM_OFF": // stopService 가 동작하지 않아서 startService 로 처리하고 나서....
                releaseCpuLock();
                Intent stopIntent = new Intent(context, RingtoneService.class);
                stopIntent.putExtra("state", get_state);
                if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O){
                    context.startForegroundService(stopIntent);
                } else {
                    context.startService(stopIntent);
                }
                break;
        }
    }

    @SuppressLint("InvalidWakeLockTag")
    private void acquireCPUWakeLock(Context context, Intent intent) {
        // 잠든 화면 깨우기
        if (wakeLock != null) {
            return;
        }
        powerManager = (PowerManager) context.getSystemService(Context.POWER_SERVICE);
        wakeLock = powerManager.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK | PowerManager.ACQUIRE_CAUSES_WAKEUP
                | PowerManager.ON_AFTER_RELEASE, "WAKELOCK");
        wakeLock.acquire();
        Log.e("PushWakeLock", "Acquire cpu WakeLock = " + wakeLock);
    }

    private void releaseCpuLock() {
        Log.e("PushWakeLock", "Releasing cpu WakeLock = " + wakeLock);

        if (wakeLock != null) {
            wakeLock.release();
            wakeLock = null;
        }
    }
}


RingtoneService.java

package com.link2me.android.wakemeup;

import android.app.Notification;
import android.app.NotificationChannel;
import android.app.NotificationManager;
import android.app.Service;
import android.content.Context;
import android.content.Intent;
import android.graphics.Color;
import android.media.MediaPlayer;
import android.os.Build;
import android.os.IBinder;
import android.util.Log;

import androidx.annotation.Nullable;
import androidx.core.app.NotificationCompat;


public class RingtoneService extends Service{
    private static final String TAG = RingtoneService.class.getSimpleName();
    MediaPlayer mediaPlayer;

    @Nullable
    @Override
    public IBinder onBind(Intent intent) {
        // Service 객체와 (화면단 Activity 사이에서) 데이터를 주고받을 때 사용하는 메서드
        // 데이터를 전달할 필요가 없으면 return null;
        return null;
    }

    @Override
    public void onCreate() {
        super.onCreate();
        // 서비스에서 가장 먼저 호출됨(최초에 한번만)
        Log.i(TAG, "RingtoneService Started");

        NotificationHelper notificationHelper = new NotificationHelper(getApplicationContext());
        notificationHelper.createNotification("알람시작","알람음이 재생됩니다.");

        // https://link2me.tistory.com/1514 에 첨부된 파일 받아서 수정 사용하면 해결됨

    }


    @Override
    public int onStartCommand(Intent intent, int flags, int startId) {
        Log.i(TAG, "onStartCommand startID === "+startId); // 계속 증가되는 값

        String getState = intent.getExtras().getString("state");
        Log.e("TAG","onStartCommand getState : " + getState);

        switch (getState) {
            case "ALARM_ON":
                if(mediaPlayer == null){
                    mediaPlayer = MediaPlayer.create(this,R.raw.ouu);
                    mediaPlayer.start();

                    mediaPlayer.setOnCompletionListener(new MediaPlayer.OnCompletionListener() {
                        @Override
                        public void onCompletion(MediaPlayer mp) {
                            Log.e(TAG, "mediaPlayer Completed!");
                            mediaPlayer.stop();
                            mediaPlayer.reset();
                            mediaPlayer.release();
                        }
                    });
                }
                break;
            case "ALARM_OFF":
                Log.e(TAG, "onStartCommand Stoped!");

                if (mediaPlayer != null) {
                    if(mediaPlayer.isPlaying() == true){
                        mediaPlayer.stop();
                        mediaPlayer.release(); // 자원 반환
                        mediaPlayer = null;
                    }
                }
                stopSelf();
                break;
            default:
                break;
        }
        return START_NOT_STICKY;
    }

    @Override
    public void onDestroy() {
        Log.i(TAG, "Action Service Ended");
        super.onDestroy();
        stopForeground(true);
    }
}



도움이 되셨다면 ... 해 주세요. 좋은 글 작성에 큰 힘이 됩니다.

me.tistory.com/1110 [소소한 일상 및 업무TIP 다루기]


728x90
블로그 이미지

Link2Me

,
728x90

이번에는 MainActivity.java 에서

ArrayList<HashMap<String, String>> calList = new ArrayList<HashMap<String, String>>();

LinkedHashMap<String, Calendar_Item> calList = new LinkedHashMap<String, Calendar_Item>();

로 변경하면 어떤 것들을 수정해줘야 할까?


먼저 https://link2me.tistory.com/1717 게시글을 읽고 나서 아래 비교 내용을 보면 좀 더 이해하는데 도움이 될 것으로 본다.

HashMap 을 사용하면 입력한 순서대로 출력이 될 것을 기대하지만 뒤죽박죽으로 결과를 보여준다.


MainActivity.java 수정사항

                if(CurrentMonth-1 == thisMonth){ // 현재월이면
                    if(i == thisDay){
                        Log.e(TAG, "key := " + key);
                        int index = getIndexOfCalList(key);
                        HashMap<String, String> item = calendarItem(String.valueOf(yy),String.valueOf(CurrentMonth),String.valueOf(i),weekday,"CYAN","",key);
                        calList.set(index,item);
                    }
                }


    private HashMap<String, String> calendarItem(String year, String month, String day, int weekday, String color, String name, String key){
        HashMap<String, String> map = new HashMap<String, String>();
        map.put("year",year);
        map.put("month",month);
        map.put("day",day);
        map.put("weekday", String.valueOf(weekday));
        map.put("color",color);
        map.put("event",name);
        map.put("key",key);
        return map;
    }

    private void addCalendarItem(String year, String month, String day, int weekday, String color,String name, String key){
        HashMap<String, String> item = calendarItem(year,month,day,weekday,color,name,key);
        calList.add(item);
    }

    private int getIndexOfCalList(String search_key) {
        for (int temp = 0; temp < calList.size(); temp++) {
            String key = calList.get(temp).get("key");
            if (key != null && key.equals(search_key)) {
                return temp;
            }
        }
        return -1;
    }

                if(CurrentMonth-1 == thisMonth){ // 현재월이면
                    if(i == thisDay){
                        Log.e(TAG, "key := " + key);
                        addCalendarItem(String.valueOf(yy),String.valueOf(CurrentMonth),String.valueOf(i),weekday,"CYAN","",key);
                    }
                }

    private Calendar_Item calendarItem(String year, String month, String day, int weekday, String color, String name, String key){
        Calendar_Item item = new Calendar_Item();
        item.setYear(year);
        item.setMonth(month);
        item.setDay(day);
        item.setWeekday(weekday);
        item.setColor(color);
        item.setEvent(name);
        return item;
    }

    private void addCalendarItem(String year, String month, String day, int weekday, String color,String name, String key){
        Calendar_Item item = calendarItem(year,month,day,weekday,color,name,key);
        calList.put(key,item);
    }


GridCellAdapter.java 수정사항

    private final ArrayList<HashMap<String, String>> calList;

    public GridCellAdapter(Context context, ArrayList<HashMap<String, String>> list) {
        mContext = context;
        calList = list;
    }

    public HashMap<String, String> getItem(int position) {
        return calList.get(position);
    }

        String theday = calList.get(position).get("day");
        int themonth = Integer.parseInt(calList.get(position).get("month"));
        String theyear = calList.get(position).get("year");
        String holiday = calList.get(position).get("event");

    private final LinkedHashMap<String, Calendar_Item> calList;
    private String[] mKeys;

    public GridCellAdapter(Context context, LinkedHashMap<String, Calendar_Item> list) {
        mContext = context;
        calList = list;
        mKeys = calList.keySet().toArray(new String[list.size()]);
    }

    public Calendar_Item getItem(int position) {
        return calList.get(mKeys[position]);
    }

        String theday = calList.get(mKeys[position]).getDay();
        int themonth = Integer.parseInt(calList.get(mKeys[position]).getMonth());
        String theyear = calList.get(mKeys[position]).getYear();
        String holiday = calList.get(mKeys[position]).getEvent();


어떤 것을 사용하든지 검색 속도가 느리지 않으면서 확장성을 고려한 메소드를 적절하게 사용하면 된다.


공휴일 등록, 기념일 등록, 일정 등록 등의 루틴은 여기에는 적지 않았다.


도움이 되셨다면 ... 해 주세요. 좋은 글 작성에 큰 힘이 됩니다.

728x90
블로그 이미지

Link2Me

,