레이블이 Android인 게시물을 표시합니다. 모든 게시물 표시
레이블이 Android인 게시물을 표시합니다. 모든 게시물 표시

2022년 7월 16일 토요일

22년 네번재 도서 리뷰 "오준석의 안드로이드 생존코딩"

"한빛미디어 <나는 리뷰어다> 활동을 위해서 책을 제공받아 작성된 서평입니다."


책 표지

올해 네 번째 도서 리뷰이다.

원래는 다섯번째여야 하는데 6월 리뷰를 건너뛰어서...


오랜만에 곧 앱 개발 프로젝트를 진행해야 한다.

그리고 오랜만에 안드로이드 책을 선택했다.



늘 그렇듯 목차부터 살펴 보았다.

1~4장은 대부분의 책과 비슷하게 시작하는 듯했고

5장부터는 예제 위주로 구성이 되어있는데 

다른 책들에서는 별로 다루지 않는 내용들로 예제가 구성되어 있었다.


'뭐 딱히 특별할 거 있을까?'라는 생각으로 책을 넘기기 시작했는데...

책 내용 구성이 너무 좋은 거 같았다.

특히 처음 시작하는 사람에게는 상당히 좋은 책이란 생각이 들었다.

개발환경 구축하는 내용부터 다루는데 드물게 MacOS에서 구축하는 내용도 있었고

그 내용도 실제 화면 캡처 내용을 가지고 구성을 하고 있어서

처음 해보는 사람도 쉽게 접근하고 따라 할 수 있도록 되어 있었다.


이후에도 실제 화면 캡처한 것을 가지고 아주 친절하게 따라올 수 있도록 구성되어 있는 것을 볼 수 있었다.

뭔가... 잘 만들어진 강의용 PPT와 그에 대한 설명으로 구성된 느낌?


아주 초급 개발자라도 쉽게 시작할 수 있을 거 같다는 생각이 들었다.


또한 책이 흑백이 아닌 컬러로 되어있고 배경색도 적절히 잘 사용하고 있어서 가독성이 매우 좋았다.


안드로이드 놓은 지가 좀 되어서 예전 감각을 되찾으려 하는 중인데

수평 측정기 예제나 전자액자 예제, 지도와 GPS예제, Todo 리스트 예제 등은 상당히 도움이 될 것 같다.


또 예제 안에서 사용하는 세부적인 내용들이 실무에서도 자주 쓰이는 내용들이 많은 것 같아서

여러모로 많은 도움을 받을 수 있을 책이라는 생각이 든다.


이 책 저자분 혹시 iOS책도 이렇게 내 보실 생각은 없으실까...?

2021년 1월 29일 금요일

Android Bluetooth serial socket 통신

디바이스와 modbus통신을 위해서 소캣통신을 구현해야 했다.

구글링해서 github에서 구한 소스를 조금 고쳐서 사용하는 것으로 해결 했다.


먼저 SerialSocket.java

package com...;

import android.app.Activity;
import android.bluetooth.BluetoothDevice;
import android.bluetooth.BluetoothSocket;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;

import java.io.IOException;
import java.security.InvalidParameterException;
import java.util.Arrays;
import java.util.UUID;
import java.util.concurrent.Executors;

public class SerialSocket implements Runnable {

    private static final UUID BLUETOOTH_SPP = UUID.fromString("00001101-0000-1000-8000-00805F9B34FB");

    private final BroadcastReceiver disconnectBroadcastReceiver;

    private Context context;
    private SerialListener listener;
    private BluetoothDevice device;
    private BluetoothSocket socket;
    private boolean connected;

    public SerialSocket(Context context, BluetoothDevice device) {
        if(context instanceof Activity)
            throw new InvalidParameterException("expected non UI context");
        this.context = context;
        this.device = device;
        disconnectBroadcastReceiver = new BroadcastReceiver() {
            @Override
            public void onReceive(Context context, Intent intent) {
                if(listener != null)
                    listener.onSerialIoError(new IOException("background disconnect"));
                disconnect(); // disconnect now, else would be queued until UI re-attached
            }
        };
    }

    String getName() {
        return device.getName() != null ? device.getName() : device.getAddress();
    }

    /**
     * connect-success and most connect-errors are returned asynchronously to listener
     */
    public void connect(SerialListener listener) throws IOException {
        this.listener = listener;
        context.registerReceiver(disconnectBroadcastReceiver, new IntentFilter(Constants.INTENT_ACTION_DISCONNECT));
        Executors.newSingleThreadExecutor().submit(this);
    }

    public void disconnect() {
        listener = null; // ignore remaining data and errors
        // connected = false; // run loop will reset connected
        if(socket != null) {
            try {
                socket.close();
            } catch (Exception ignored) {
            }
            socket = null;
        }
        try {
            context.unregisterReceiver(disconnectBroadcastReceiver);
        } catch (Exception ignored) {
        }
    }

    public void write(byte[] data) throws IOException {
        if (!connected)
            throw new IOException("not connected");
        socket.getOutputStream().write(data);
    }

    @Override
    public void run() { // connect & read
        try {
            socket = device.createRfcommSocketToServiceRecord(BLUETOOTH_SPP);
            socket.connect();
            if(listener != null)
                listener.onSerialConnect();
        } catch (Exception e) {
            if(listener != null)
                listener.onSerialConnectError(e);
            try {
                socket.close();
            } catch (Exception ignored) {
            }
            socket = null;
            return;
        }
        connected = true;
        try {
            byte[] buffer = new byte[1024];
            int len;
            //noinspection InfiniteLoopStatement
            while (true) {
                len = socket.getInputStream().read(buffer);
                byte[] data = Arrays.copyOf(buffer, len);
                if(listener != null)
                    listener.onSerialRead(data);
            }
        } catch (Exception e) {
            connected = false;
            if (listener != null)
                listener.onSerialIoError(e);
            try {
                socket.close();
            } catch (Exception ignored) {
            }
            socket = null;
        }
    }

}


SerialService.java

package com...;

import android.app.Notification;
import android.app.NotificationChannel;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.app.Service;
import android.content.Context;
import android.content.Intent;
import android.os.Binder;
import android.os.Build;
import android.os.Handler;
import android.os.IBinder;
import android.os.Looper;

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

import java.io.IOException;
import java.util.LinkedList;
import java.util.Queue;

/**
 * create notification and queue serial data while activity is not in the foreground
 * use listener chain: SerialSocket -> SerialService -> UI fragment
 */
public class SerialService extends Service implements SerialListener {

    public class SerialBinder extends Binder {
        public SerialService getService(){
            return SerialService.this;
        }
    }

    private enum QueueType {Connect, ConnectError, Read, IoError}

    private class QueueItem {
        QueueType type;
        byte[] data;
        Exception e;

        QueueItem(QueueType type, byte[] data, Exception e) { this.type=type; this.data=data; this.e=e; }
    }

    private final Handler mainLooper;
    private final IBinder binder;
    private final Queue<QueueItem> queue1, queue2;

    private SerialSocket socket;
    private SerialListener listener;
    private boolean connected;

    /**
     * Lifecylce
     */
    public SerialService() {
        mainLooper = new Handler(Looper.getMainLooper());
        binder = new SerialBinder();
        queue1 = new LinkedList<>();
        queue2 = new LinkedList<>();
    }

    @Override
    public void onDestroy() {
        cancelNotification();
        disconnect();
        super.onDestroy();
    }

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

    /**
     * Api
     */
    public void connect(SerialSocket socket) throws IOException {
        socket.connect(this);
        this.socket = socket;
        connected = true;
    }

    public void disconnect() {
        connected = false; // ignore data,errors while disconnecting
        cancelNotification();
        if(socket != null) {
            socket.disconnect();
            socket = null;
        }
    }

    public void write(byte[] data) throws IOException {
        if(!connected)
            throw new IOException("not connected");
        socket.write(data);
    }

    public void attach(SerialListener listener) {
        if(Looper.getMainLooper().getThread() != Thread.currentThread())
            throw new IllegalArgumentException("not in main thread");
        cancelNotification();
        // use synchronized() to prevent new items in queue2
        // new items will not be added to queue1 because mainLooper.post and attach() run in main thread
        synchronized (this) {
            this.listener = listener;
        }
        for(QueueItem item : queue1) {
            switch(item.type) {
                case Connect:       listener.onSerialConnect      (); break;
                case ConnectError:  listener.onSerialConnectError (item.e); break;
                case Read:          listener.onSerialRead         (item.data); break;
                case IoError:       listener.onSerialIoError      (item.e); break;
            }
        }
        for(QueueItem item : queue2) {
            switch(item.type) {
                case Connect:       listener.onSerialConnect      (); break;
                case ConnectError:  listener.onSerialConnectError (item.e); break;
                case Read:          listener.onSerialRead         (item.data); break;
                case IoError:       listener.onSerialIoError      (item.e); break;
            }
        }
        queue1.clear();
        queue2.clear();
    }

    public void detach() {
        if(connected)
            createNotification();
        // items already in event queue (posted before detach() to mainLooper) will end up in queue1
        // items occurring later, will be moved directly to queue2
        // detach() and mainLooper.post run in the main thread, so all items are caught
        listener = null;
    }

    private void createNotification() {
//        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
//            NotificationChannel nc = new NotificationChannel(Constants.NOTIFICATION_CHANNEL, "Background service", NotificationManager.IMPORTANCE_LOW);
//            nc.setShowBadge(false);
//            NotificationManager nm = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
//            nm.createNotificationChannel(nc);
//        }
//        Intent disconnectIntent = new Intent()
//                .setAction(Constants.INTENT_ACTION_DISCONNECT);
//        Intent restartIntent = new Intent()
//                .setClassName(this, Constants.INTENT_CLASS_MAIN_ACTIVITY)
//                .setAction(Intent.ACTION_MAIN)
//                .addCategory(Intent.CATEGORY_LAUNCHER);
//        PendingIntent disconnectPendingIntent = PendingIntent.getBroadcast(this, 1, disconnectIntent, PendingIntent.FLAG_UPDATE_CURRENT);
//        PendingIntent restartPendingIntent = PendingIntent.getActivity(this, 1, restartIntent,  PendingIntent.FLAG_UPDATE_CURRENT);
//        NotificationCompat.Builder builder = new NotificationCompat.Builder(this, Constants.NOTIFICATION_CHANNEL)
//                .setSmallIcon(R.drawable.ic_notification)
//                .setColor(getResources().getColor(R.color.colorPrimary))
//                .setContentTitle(getResources().getString(R.string.app_name))
//                .setContentText(socket != null ? "Connected to "+socket.getName() : "Background Service")
//                .setContentIntent(restartPendingIntent)
//                .setOngoing(true)
//                .addAction(new NotificationCompat.Action(R.drawable.ic_clear_white_24dp, "Disconnect", disconnectPendingIntent));
//        // @drawable/ic_notification created with Android Studio -> New -> Image Asset using @color/colorPrimaryDark as background color
//        // Android < API 21 does not support vectorDrawables in notifications, so both drawables used here, are created as .png instead of .xml
//        Notification notification = builder.build();
//        startForeground(Constants.NOTIFY_MANAGER_START_FOREGROUND_SERVICE, notification);
    }

    private void cancelNotification() {
        stopForeground(true);
    }

    /**
     * SerialListener
     */
    public void onSerialConnect() {
        if(connected) {
            synchronized (this) {
                if (listener != null) {
                    mainLooper.post(() -> {
                        if (listener != null) {
                            listener.onSerialConnect();
                        } else {
                            queue1.add(new QueueItem(QueueType.Connect, null, null));
                        }
                    });
                } else {
                    queue2.add(new QueueItem(QueueType.Connect, null, null));
                }
            }
        }
    }

    public void onSerialConnectError(Exception e) {
        if(connected) {
            synchronized (this) {
                if (listener != null) {
                    mainLooper.post(() -> {
                        if (listener != null) {
                            listener.onSerialConnectError(e);
                        } else {
                            queue1.add(new QueueItem(QueueType.ConnectError, null, e));
                            cancelNotification();
                            disconnect();
                        }
                    });
                } else {
                    queue2.add(new QueueItem(QueueType.ConnectError, null, e));
                    cancelNotification();
                    disconnect();
                }
            }
        }
    }

    public void onSerialRead(byte[] data) {
        if(connected) {
            synchronized (this) {
                if (listener != null) {
                    mainLooper.post(() -> {
                        if (listener != null) {
                            listener.onSerialRead(data);
                        } else {
                            queue1.add(new QueueItem(QueueType.Read, data, null));
                        }
                    });
                } else {
                    queue2.add(new QueueItem(QueueType.Read, data, null));
                }
            }
        }
    }

    public void onSerialIoError(Exception e) {
        if(connected) {
            synchronized (this) {
                if (listener != null) {
                    mainLooper.post(() -> {
                        if (listener != null) {
                            listener.onSerialIoError(e);
                        } else {
                            queue1.add(new QueueItem(QueueType.IoError, null, e));
                            cancelNotification();
                            disconnect();
                        }
                    });
                } else {
                    queue2.add(new QueueItem(QueueType.IoError, null, e));
                    cancelNotification();
                    disconnect();
                }
            }
        }
    }

}

SerialListener.java

package com....;

public interface SerialListener {
    void onSerialConnect      ();
    void onSerialConnectError (Exception e);
    void onSerialRead         (byte[] data);
    void onSerialIoError      (Exception e);
}



마지막으로 실제 사용 예시

package com...

import android.bluetooth.BluetoothAdapter
import android.bluetooth.BluetoothDevice
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.widget.Button
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity
import com...SerialListener
import com...SerialService
import com...SerialSocket
import com...CRC16Modbus
import com...TextUtil

const val STATE_SETTING = 1
const val STATE_MEASURE = 2
const val STATE_SET_VALUE = 3

class DataRelayActivityV2 : AppCompatActivity(), ServiceConnection, SerialListener {

    enum class Connected { False, Pending, True }

    lateinit var device: BluetoothDevice
    lateinit var deviceAddress: String
    private var service: SerialService? = null //SerialService()
    private var connected: Connected = Connected.False
    var initialStart: Boolean = true

    private val newline = TextUtil.newline_crlf

    lateinit var btnSetting: Button
    lateinit var btnMeasure: Button
    lateinit var btnSendSetting: Button

    val requestSetting: String = "0103000000384418"
    val requestMeasure: String = "010400000014F005"
    var requestSetVaule: String = "0106"

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_data_relay_v2)

        device = intent.getParcelableExtra("device")!!
        deviceAddress = intent.getStringExtra("deviceAddress").toString()

        init()
    }

    private fun init() {
        btnSetting = findViewById(R.id.btn_setting)
        btnSetting.setOnClickListener {
            send(requestSetting)
        }

        btnMeasure = findViewById(R.id.btn_measure)
        btnMeasure.setOnClickListener {
            send(requestMeasure)
        }

        btnSendSetting = findViewById(R.id.btn_send_setting)
        btnSendSetting.setOnClickListener {
            val crcCRC16Modbus: CRC16Modbus = CRC16Modbus()
            var cmd: String = "010600000002"
            var nCmd: IntArray = intArrayOf(0x01, 0x06, 0x00, 0x00, 0x00, 0x02)
            var sCmd: String = ""
            for (i in (nCmd.indices)) {
                sCmd += String.format("%02X", nCmd[i].toByte())
            }
            Log.d("TEST", "sCmd = $sCmd")
            // Command value
            var crc: ByteArray = crcCRC16Modbus.getCRC16Modbus(nCmd)
            sCmd += String.format("%02X", crc[0])
            sCmd += String.format("%02X", crc[1])
            Log.d("TEST", "after sCmd = $sCmd")
            Log.d("TEST", "toHexString = " + TextUtil.toHexString(crc))
        }
//        connect()
    }

    private fun connect() {
        Log.d("TEST", "connect called !!!")
        try {
            val btAdapter: BluetoothAdapter = BluetoothAdapter.getDefaultAdapter()
            val device: BluetoothDevice = btAdapter.getRemoteDevice(deviceAddress)
            Log.d("TEST", "device address = $deviceAddress")
            showToast("Connecting....")
            connected = Connected.Pending
            val socket = SerialSocket(this.applicationContext, device)
            service?.connect(socket)
        } catch (e: Exception) {
            onSerialConnectError(e)
        }
    }

    private fun disconnect() {
        connected = Connected.False
        service?.disconnect()
    }

    private fun send(str: String) {
        var data: ByteArray
        var msg: String
        if (connected == Connected.True) {
            var sb: StringBuilder = StringBuilder()
            TextUtil.toHexString(sb, TextUtil.fromHexString(str))
//            TextUtil.toHexString(sb, newline.toByteArray())
            msg = sb.toString()
            data = TextUtil.fromHexString(msg)
            Log.d("TEST", "send data = $msg")
            service?.write(data)
        } else {
            showToast("Not Connected")
        }
    }

    override fun onDestroy() {
        if (connected != Connected.False)
            disconnect()
        this.stopService(Intent(this, SerialService::class.java))
        super.onDestroy()
    }

    override fun onStart() {
        super.onStart()
        Log.d("TEST", "onStart Called !!!")
        if (service != null) {
            Log.d("TEST", "on start service is not null !!!")
            service?.attach(this)
        } else {
            Log.d("TEST", "on start service is null !!!")
//            startService(Intent( this, SerialService::class.java))
            Intent(this, SerialService::class.java).also { intent ->
                bindService(intent, this, Context.BIND_AUTO_CREATE)
            }
        }
    }

    override fun onStop() {
        if (service != null && !this.isChangingConfigurations) {
            service!!.detach()
        }
        super.onStop()
    }

    private fun receive(data: ByteArray): String {
        Log.d("TEST", "received called !!!")
        Log.d("TEST", "data = " + TextUtil.toHexString(data))
        return TextUtil.toHexString(data)
    }

//    fun onAttach(context: Context){
//        Log.d("TEST", "on Attach Called !!!")
//        super.onAttachedToWindow()
//        this.bindService(Intent(context, SerialService::class.java), this, Context.BIND_AUTO_CREATE)
//    }
//
//    fun onDetach(){
//        try{
//            this.unbindService(this)
//        } catch (e: Exception){
//            e.printStackTrace()
//        }
//        super.onDetachedFromWindow()
//    }

    override fun onResume() {
        super.onResume()
        if (initialStart && service != null) {
            initialStart = false
//            this.runOnUiThread(this::connect)
            runOnUiThread { connect() }
        }
    }

    override fun onServiceConnected(p0: ComponentName?, p1: IBinder?) {
        Log.d("TEST", "on Service Connected called !!!")
//        service =  (p1 as SerialService.SerialBinder).service
//        service = ((SerialService.SerialBinder) p1).getService()
        val binder = p1 as SerialService.SerialBinder
        service = binder.service
        service?.attach(this)
        if (initialStart) { // && isResumed()
            initialStart = false;
//            this.runOnUiThread(this::connect)
            runOnUiThread {
                connect()
            }
        }
    }

    override fun onServiceDisconnected(p0: ComponentName?) {
//        service = null
    }

    override fun onSerialConnect() {
        showToast("connected")
        connected = Connected.True
    }

    override fun onSerialConnectError(e: java.lang.Exception?) {
        if (e != null) {
            showToast("Connection failed! " + e.message.toString())
            e.printStackTrace()
        }
    }

    override fun onSerialRead(data: ByteArray?) {
        Log.d("TEST", "on Serial Read called !!!")
        if (data != null) {
            receive(data)
        }
    }

    override fun onSerialIoError(e: java.lang.Exception?) {
        if (e != null) {
            e.printStackTrace()
            showToast("Connection lost! " + e.message)
        }
    }

    private fun showToast(msg: String) {
        Toast.makeText(this, msg, Toast.LENGTH_SHORT).show()
    }
}

어쩌다가 역대급 긴 글이 된듯...

2019년 10월 9일 수요일

Kotlin으로 구현한 AES128 암호화

이번 프로젝트를 진행 하면서 서버와 통신시에 전송할 데이터를 암호화 하여 전송하기로 결정하였다.
그에 따라서 기존 자바 소스로 구현되어 있는 암호화를 코틀린으로 구현 하였다.

아래가 그 코드이다.

import android.util.Base64
import javax.crypto.Cipher
import javax.crypto.spec.SecretKeySpec

class CipherUtil(){

    fun encryptAES128(plainText:String):String{

        val cipher = Cipher.getInstance("AES/ECB/PKCS5Padding")
        val keySpec = SecretKeySpec(Common.CIPHER_KEY.toByteArray(), "AES")
        cipher.init(Cipher.ENCRYPT_MODE, keySpec)
        val crypted = cipher.doFinal(plainText.toByteArray())

        val encodedByte = Base64.encode(crypted, Base64.DEFAULT)
        return String(encodedByte)
    }

    fun decryptedAES128(crypted:String):String {
        var decodedByte:ByteArray = Base64.decode(crypted, Base64.DEFAULT)
        val keySpec = SecretKeySpec(Common.CIPHER_KEY.toByteArray(), "AES")
        val cipher = Cipher.getInstance("AES/ECB/PKCS5Padding")
        cipher.init(Cipher.DECRYPT_MODE, keySpec)
        val output = cipher.doFinal(decodedByte)

        return String(output)
    }
}


2019년 7월 12일 금요일

android firebase cloud message

firebase 사이트에 내용을 따라하는데 의존성 문제로 반나절을 삽질했다 ㅠㅠ

새로운 프로젝트에서 푸쉬를 사용해야 해서
백만년만에 구글 firebase를 이용해 보고자 차근차근 따라 하는데

appcompat-v7 과 firebase-core 17.0.1이 충돌하는것이었다.

검색과 삽질을 계속 한 끝에 원인을 찾을 수 있었다.

firebase 17 버전 부터는 androidx를 사용해야 한다는것!
아래처럼 AndroidX로 migration 해주는것으로 해결 하였다.


한동안 안드로이드 개발을 안하고 있었더니 변화가 생긴 모양이다.
어찌되었건 이 문제 해결 하면서 테스트 푸쉬 메시지는 잘 전송 되었다.

AndroidX가 뭐하는 녀석인지 알아봐야 할듯 하다.

2017년 1월 24일 화요일

Car Dock 앱 출시

결국 발디자인 앱을 출시하였다.


부디 에러나 버그가 없기를 ㅠㅠ

2017년 1월 16일 월요일

Car Dock beta test

회사에서 진행중인 프로잭트에 사용하게 될 앱인데
내 편의를 위해서 먼저 만들어 보고 있다.
기본적으로 나에게 필요한 기능들을 위주로 만들고 있는데
베타 테스트를 진행 하면서 기능수정을 해볼까 한다.

그나저나...역시 앱은 디자인의 비중이 ㅠㅠ


2016년 12월 22일 목요일

Android 설치된 앱 목록 가져오기

구글링해서 찾은것인데 작업후에 정리해 두려다보니
출처가 기억나지 않는다 ㅠㅠ

PackageManager packageManager;
Intent intent = new Intent(Intent.ACTION_MAIN);
intent.addCategory(Intent.CATEGORY_LAUNCHER);

List<ResolveInfo> list = packageManager.queryIntentActivities(intent, 0);

for (ResolveInfo info : list) {
    String appActivity = info.activityInfo.name;
    String appPackageName = info.activityInfo.packageName;
    String appName = info.loadLabel(packageManager).toString();

    Drawable drawable = info.activityInfo.loadIcon(packageManager);
    Log.d("TEST", "appName : " + appName + ", appActivity : "
+ appActivity + ", appPackageName : " + appPackageName);
}

packagemanager를 이용해서 목록을 가지고 오고 패키지명이나 아이콘 이미지를 가지고 오는 소스

2012년 12월 3일 월요일

REST 구현

자체개발건을 진행하면서 REST구현때문에 고민하다가 발견한 자료
예전엔 그냥 따라하기로 뭔지도 모르고 구현했는데
이번에 제대로 공부하게 된듯 하다.

근데 제대로 공부한거 맞나...;;;

찾은 사이트는 여기!!!




2012년 11월 14일 수요일

Android google map debug key for windows7

대부분 windows xp 환경에서 설명되어 있는데
windows7도 크게 다르지 않지만 경로등이 조금 다르다.
JAVA버전에 따라 keytool 명령어도 옵션을 추가해 주어야 한다.

먼저 keytool로 debug.keystore에서  키값을 받아야 하는데...
debug.keysotre위치 -> c:\user\user-name\.android\ 아래 있음(windows7)
keytool은 JDk설치 폴더 아래 bin아래

c:\user\user-name\.android\keytool -list -v -keystore debug.keystore
위 처럼 실행하면 전체 키 값을 볼 수 있다.

실행하면 비번을 입력하라고 나오는데 개발용 암호는 android로 고정되어 있다.
실행 결과는 아래와 같다.

키 저장소 비밀번호 입력:

키 저장소 유형: JKS

키 저장소 제공자: SUN

키 저장소에 1개의 항목이 포함되어 있습니다.


별칭 이름: androiddebugkey

생성 날짜: 2012. 11. 12
항목 유형: PrivateKeyEntry
인증서 체인 길이: 1
인증서[1]:
소유자: CN=Android Debug, O=Android, C=US
발행자: CN=Android Debug, O=Android, C=US
일련 번호: 1826273a
적합한 시작 날짜: Mon Nov 12 14:23:21 KST 2012, 종료 날짜: Wed Nov 05 14:23:21 K
ST 2042
인증서 지문:
         MD5: 5C:14:C7:FB:0A:72:CF:EE:40:8E:AA:CB:0F:16:B5:39
         SHA1: F2:3E:AE:0E:DD:ED:02:73:8F:C1:59:D0:8C:C8:88:D4:5D:D8:12:A6
         SHA256: 39:92:D4:56:A0:93:87:EF:44:88:E7:D0:88:E2:0D:98:99:AD:3A:B9:F0:
EE:06:29:82:0F:77:53:32:E0:C9:5A
         서명 알고리즘 이름: SHA256withRSA
         버전: 3

확장:


#1: ObjectId: 2.5.29.14 Criticality=false

SubjectKeyIdentifier [
KeyIdentifier [
0000: 59 67 6F 63 F6 DE EF 40   54 5C 7D D8 F8 13 15 30  Ygoc...@T\.....0
0010: 5D 6D 66 09                                        ]mf.
]
]

여기까지 실행후 아래 사이트로 가서 저 실행 결과 값의 MD5값을 입력하고 키 발급받기 클릭!
https://developers.google.com/maps/documentation/android/maps-api-signup
그러면 사용자 키와 더불어 친절하게 xml예제도 짧게 보여준다.

그런데 이거 언제부터 한글로 나왔었지...전엔 영문밖에 없었던듯 한데...


2012년 6월 11일 월요일

APK 디컴파일 방법.



오늘은 Android 최종 package 형태인 apk 파일에 대해서 Decompile 하는 방법을 소개 하겠습니다.
최종 APK 파일이 나오기 까지는 아래 그림처럼 많은 단계를 거칩니다.
대충 간단하게 정리해보면 리소스 파일들과 java 컴파일된 dex 파일 그리고 signature 정보가 들어가게 됩니다.
여튼 자세한 정보는 여기를 참고 하시고… http://developer.android.com/guide/developing/building/index.html

우리가 열심히 개발한 소스는 classes.dex 라는 파일 안에 들어가게 되는데.. 이 dex 파일을 풀면 class 파일들이 나옵니다.
이 class 파일을.. decompile 하면 소스 코드가 보이는 것이죠..

이런 코드가

이렇게까지 디컴파일 됩니다.

그럼 자세한 decompile 순서를 알아보겠습니다.

  • Apk 파일을 압축을 풉니다.
알집을 사용해도 되고.. 간단하게 jar xf hello.apk 하면 apk 파일이 풀립니다.
위 그림처럼 android manifest파일과 사용한 resource들 그리고 classes.dex 파일과 META-INF의 signature 정보들이 보입니다.

  • Dex를 jar로 변환합니다.
Classes.dex 파일은 android의 dalvik 에서 사용하기 위한 일종의 jar 패키지 형태인데.. 이를 다시 jar로 변환이 가능합니다.
여기에서 간단히 쓸 수 있는 변환툴을 다운로드 할 수 있다.
실행해보면
Jarfile이 하나 생긴 것을 알 수 있다.

  • Jar를 class로 압축을 풀어줍니다.
다시 jar xf classed_dex2jar.jar 하면 압축이 풀리면서.. class 파일들이 나온다.

Class 파일은 아시다시피 중간 바이너리 형태라 디컴파일이 가능하다.

  • Class 파일을 디컴파일 하여 보는 툴을 사용하여 소스를 본다.
소스를 보기위해.. jd 라는 툴을 사용하면 된다.
http://java.decompiler.free.fr/?q=jdgui 여기서 다운 받을수 있다.

다운받아 간단히 실행하여 class 파일을 열어보면 소스가 나온다..

정말 무서운 세상이다…

NDK addr2line 사용방법

FFmpeg프로젝트 중에 계속해서 build fingerprint 라는 알수없는 로그가 발생하면서
실행중인 앱이 죽어 버리는 증상이 발생하고 있다.

뭔가 알아 볼수 없는 주소값인건 알겠는데 어찌 읽어야 하나 방법을 찾다가 발견했다.

아래와 같이 로그가 발생 했다면

06-11 13:27:17.275: I/DEBUG(1435): *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** ***
06-11 13:27:17.275: I/DEBUG(1435): Build fingerprint: 'generic/broadcom_cmp/cmp/:2.2/FRF91/132:jp/release'
06-11 13:27:17.275: I/DEBUG(1435): pid: 9504, tid: 9504  >>> net.jbong.FFmpegBasic <<<
06-11 13:27:17.275: I/DEBUG(1435): signal 11 (SIGSEGV), fault addr 00000008
06-11 13:27:17.275: I/DEBUG(1435):  r0 00000000  r1 bef68370  r2 00000000  r3 00000001
06-11 13:27:17.275: I/DEBUG(1435):  r4 00000000  r5 bef68370  r6 00000c60  r7 81a50e20
06-11 13:27:17.275: I/DEBUG(1435):  r8 00000680  r9 4410eac8  10 4410eab4  fp 452090f4
06-11 13:27:17.275: I/DEBUG(1435):  ip 00000003  sp bef68370  lr 810b6624  pc 810b65dc  cpsr 60000010
06-11 13:27:17.425: I/DEBUG(1435):          #00  pc 000b65dc  /data/data/net.jbong.FFmpegBasic/lib/libbasicplayer.so
이하 생략


arm-linux-androideabi-addr2line.exe -f -e libbasicplayer.so 000b65dc
위와 같이 실행하면
다음과 같은 결과를 출력한다.

av_interleave_packet
D:\workspace2\FFmpegBasic/jni/ffmpeg/libavformat/utils.c:3239

위처럼 실행할때 so파일은 libs폴더가 아니고 obj/local/armeabi 아래 있는 파일을 사용해야 한다.

이제 어디가 문제인지 찾았으니 수정을 해야겠다.

2024년 여섯번째 도서 리뷰 [무엇이 1등 팀을 만드는가]

한빛미디어 <나는 리뷰어다> 활동을 위해서 책을 제공받아 작성된 서평입니다. 올해의 마지막 서평이다. 무엇이 1등 팀을 만드는가.... 시작하기에 앞서 제목이 과하지 않은가 하는 생각이 든다. 1등 팀이라....예전 어느 개그 프로에서 ...