728x90

안드로이드 스튜디오에서 OKHttp 라이브러리를 활용하여 사진 촬영 및 이미지를 서버에 업로드하는 기능을 테스트하고 있다.


http://link2me.tistory.com/1354 보다 약간 내용을 보강했고, 사진 촬영에 대한 메소드를 추가했다.


테스트 환경 : 삼성 갤럭시 S7 (정상 동작), 삼성 갤럭시 S4(비정상 에러, 사진 촬영후 CROP으로 넘기는 부분)

사진 촬영을 하고 이미지 CROP 하는 부분에서 비정상적으로 처리를 하면서 문제가 발생한다.


OKHttp 라이브러리를 이용하면 서버로 업로드를 텍스트, 파일첨부 등을 완벽하고 쉽게 해준다.

public class JSONParser {

    public static JSONObject uploadImage(String imageUploadUrl, String sourceImageFile) {
        final MediaType MEDIA_TYPE_PNG = MediaType.parse("image/*");

        try {
            File sourceFile = new File(sourceImageFile);
            Log.d("TAG", "File...::::" + sourceFile + " : " + sourceFile.exists());
            String filename = sourceImageFile.substring(sourceImageFile.lastIndexOf("/")+1);

            // OKHTTP3
            RequestBody requestBody = new MultipartBody.Builder()
                    .setType(MultipartBody.FORM)
                    .addFormDataPart("uploaded_file", filename, RequestBody.create(MEDIA_TYPE_PNG, sourceFile))
                    .addFormDataPart("result", "photo_image")
                    .build();

            Request request = new Request.Builder()
                    .url(imageUploadUrl)
                    .post(requestBody)
                    .build();

            OkHttpClient client = new OkHttpClient();
            Response response = client.newCall(request).execute();
            if (response != null) {
                if (response.isSuccessful()) {
                    String res = response.body().string();
                    Log.e("TAG", "Error: " + res);
                    return new JSONObject(res);
                }
            }
        } catch (UnknownHostException | UnsupportedEncodingException e) {
            Log.e("TAG", "Error: " + e.getLocalizedMessage());
        } catch (Exception e) {
            Log.e("TAG", "Other Error: " + e.getLocalizedMessage());
        }
        return null;
    }
}


퍼미션 체크하는 부분은 좀 더 수정하여 완벽하게 동작하도록 만드는 것이 필요하다.


public class MainActivity extends AppCompatActivity {
   
    Context mContext;

    private static final int PICK_FROM_CAMERA = 1; //카메라 촬영으로 사진 가져오기
    private static final int PICK_FROM_ALBUM = 2; //앨범에서 사진 가져오기
    private static final int CROP_FROM_CAMERA = 3; // 가져온 사진을 자르기 위한 변수

    private Uri mImageCaptureUri;
    private Bitmap mImageBitmap;
    String imagePath;

    ImageView imageView;
    TextView textView;

    PermissionsChecker checker;
    Toolbar toolbar;

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

        //상태바 없애기 -- 아직 제대로 동작 안됨
        getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN,
                WindowManager.LayoutParams.FLAG_FULLSCREEN);

        getWindow().setFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON,
                WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);


        setContentView(R.layout.activity_main);

        mContext = getApplicationContext();

        if (Build.VERSION.SDK_INT >= 23) {
            PermissionCheck permissionCheck = new PermissionCheck(MainActivity.this);
            if(permissionCheck.checkPermissions() == false){
                finish();
            }
        }

        toolbar = (Toolbar) findViewById(R.id.toolbar);
        setSupportActionBar(toolbar);

        textView = (TextView) findViewById(R.id.textView);
        textView.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                DialogInterface.OnClickListener cameraListener = new DialogInterface.OnClickListener() {
                    @Override
                    public void onClick(DialogInterface dialogInterface, int i) {
                        // 카메라에서 사진 촬영
                        doTakePhotoAction();
                    }
                };
                DialogInterface.OnClickListener albumListener = new DialogInterface.OnClickListener() {
                    @Override
                    public void onClick(DialogInterface dialogInterface, int i) {
                        getGallery(); // 갤러리(앨범)에서 이미지 가져오기
                    }
                };
                DialogInterface.OnClickListener cancelListener = new DialogInterface.OnClickListener() {
                    @Override
                    public void onClick(DialogInterface dialogInterface, int i) {
                        dialogInterface.dismiss();
                    }
                };

                new AlertDialog.Builder(MainActivity.this)
                        .setTitle("업로드할 이미지 선택")
                        .setPositiveButton("사진촬영",cameraListener)
                        .setNeutralButton("앨범선택",albumListener)
                        .setNegativeButton("취소",cancelListener)
                        .show();
            }
        });

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

        // 선택된 사진을 받아 서버에 업로드한다.
        FloatingActionButton fab = (FloatingActionButton) findViewById(R.id.fab);
        fab.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                if (!TextUtils.isEmpty(imagePath)) {
                    if (NetworkHelper.checkConnection(mContext)) { // 인터넷 연결 체크
                        String ImageUploadURL = "http://192.168.0.100/upload/upload.php";
                        new ImageUploadTask().execute(ImageUploadURL, imagePath);
                    } else {
                        Toast.makeText(mContext, "인터넷 연결을 확인하세요", Toast.LENGTH_LONG).show();
                    }
                } else {
                    Toast.makeText(mContext, "먼저 업로드할 파일을 선택하세요", Toast.LENGTH_SHORT).show();
                }
            }
        });
    }

    private  class ImageUploadTask extends AsyncTask<String, Integer, Boolean> {
        ProgressDialog progressDialog; // API 26에서 deprecated

        @Override
        protected void onPreExecute() {
            super.onPreExecute();
            progressDialog = new ProgressDialog(MainActivity.this);
            progressDialog.setMessage("이미지 업로드중....");
            progressDialog.show();
        }

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

            try {
                JSONObject jsonObject = JSONParser.uploadImage(params[0],params[1]);
                if (jsonObject != null){
                    return jsonObject.getString("result").equals("success");
                }
            } catch (JSONException e) {
                Log.i("TAG", "Error : " + e.getLocalizedMessage());
            }
            return false;
        }

        @Override
        protected void onPostExecute(Boolean aBoolean) {
            super.onPostExecute(aBoolean);
            if (progressDialog != null)
                progressDialog.dismiss();

            if (aBoolean){
                Toast.makeText(getApplicationContext(), "파일 업로드 성공", Toast.LENGTH_LONG).show();
            }  else{
                Toast.makeText(getApplicationContext(), "파일 업로드 실패", Toast.LENGTH_LONG).show();
            }

            // 임시 파일 삭제 (카메라로 사진 촬영한 이미지)
            if(mImageCaptureUri != null){
                File file = new File(mImageCaptureUri.getPath());
                if(file.exists()) {
                    file.delete();
                }
                mImageCaptureUri = null;
            }

            imagePath = "";
            textView.setVisibility(View.VISIBLE);
            imageView.setVisibility(View.INVISIBLE);

        }
    }

    // 사진 선택을 위해 갤러리를 호출
    private void getGallery() {
        // File System.
        final Intent galleryIntent = new Intent();
        galleryIntent.setType("image/*");
        galleryIntent.setAction(Intent.ACTION_PICK);

        // Chooser of file system options.
        final Intent chooserIntent = Intent.createChooser(galleryIntent, "Select Image");
        startActivityForResult(chooserIntent, PICK_FROM_ALBUM);
    }

    private void doTakePhotoAction() {  // 카메라 앱을 이용하여 사진 찍기
        // Intent를 사용하여 안드로이드에서 기본적으로 제공해주는 카메라를 이용하는 방법 이용
        Intent cameraIntent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);

        // 임시로 사용할 파일의 경로를 생성
        String url = "CameraPicture_" + String.valueOf(new SimpleDateFormat( "yyyyMMdd_HHmmss").format( new Date())) + ".jpg";
        mImageCaptureUri = Uri.fromFile(new File(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES), url));
        imagePath = mImageCaptureUri.getPath();
        // 이미지가 저장될 파일은 카메라 앱이 구동되기 전에 세팅해서 넘겨준다.
        cameraIntent.putExtra(android.provider.MediaStore.EXTRA_OUTPUT, mImageCaptureUri);
        cameraIntent.putExtra("return-data", true);

        startActivityForResult(cameraIntent, PICK_FROM_CAMERA); // 7.0 에서는 에러가 발생함. (API26 으로 컴파일 한 경우)
    }

    @Override
    protected void onActivityResult(int requestCode, int resultCode, Intent data) {
        super.onActivityResult(requestCode, resultCode, data);
        if(resultCode != RESULT_OK) return;
        switch (requestCode){
            case PICK_FROM_ALBUM: {
                // URI 정보를 이용하여 이미지(사진) 정보를 가져온다.
                if (data == null) {
                    Toast.makeText(mContext, "Unable to Pickup Image", Toast.LENGTH_SHORT).show();
                    return;
                }
                Uri selectedImageUri = data.getData();
                String[] filePathColumn = {MediaStore.Images.Media.DATA};

                Cursor cursor = getContentResolver().query(selectedImageUri, filePathColumn, null, null, null);

                if (cursor != null) {
                    cursor.moveToFirst();

                    imagePath = cursor.getString(cursor.getColumnIndex(filePathColumn[0]));

                    Picasso.with(mContext).load(new File(imagePath))
                            .into(imageView); // 피카소 라이브러를 이용하여 선택한 이미지를 imageView에 출력
                    cursor.close();

                } else {
                    Snackbar.make(findViewById(R.id.parentView), "Unable to Load Image", Snackbar.LENGTH_LONG).setAction("Try Again", new View.OnClickListener() {
                        @Override
                        public void onClick(View v) {
                            getGallery();
                        }
                    }).show();
                }

                textView.setVisibility(View.GONE);
                imageView.setVisibility(View.VISIBLE);
                break;
            }

            case PICK_FROM_CAMERA: {
                // 이미지를 가져온 이후의 리사이즈할 이미지 크기를 결정
                Intent intent = new Intent("com.android.camera.action.CROP");
                intent.setDataAndType(mImageCaptureUri, "image/*");

                // CROP할 이미지를 125*170 크기로 저장
//                intent.putExtra("outputX", 125); // CROP한 이미지의 x축 크기
//                intent.putExtra("outputY", 170); // CROP한 이미지의 y축 크기
//                intent.putExtra("aspectX", 1); // CROP 박스의 X축 비율
//                intent.putExtra("aspectY", 1); // CROP 박스의 Y축 비율
                intent.putExtra("scale", true);
                intent.putExtra("return-data", true);

                startActivityForResult(intent, CROP_FROM_CAMERA); // CROP_FROM_CAMERA case문 이동
                break;
            }

            case CROP_FROM_CAMERA:  {
                // CROP 된 이후의 이미지를 넘겨 받음
                final Bundle extras = data.getExtras();
                if(extras != null) {
                    //mImageBitmap = extras.getParcelable("data"); // CROP된 BITMAP
                    mImageBitmap = (Bitmap)data.getExtras().get("data"); // CROP된 BITMAP
                    // 레이아웃의 이미지칸에 CROP된 BITMAP을 보여줌
                    saveCropImage(mImageBitmap,imagePath); //  CROP 된 이미지를 외부저장소, 앨범에 저장
                    break;
                }

                Picasso.with(mContext).load(new File(imagePath))
                        .into(imageView); // 피카소 라이브러를 이용하여 선택한 이미지를 imageView에 출력

                textView.setVisibility(View.GONE);
                imageView.setVisibility(View.VISIBLE);
                break;
            }

        }

    }

    // 저장된 사진을 사진 갤러리에 추가 (아직 테스트를 제대로 못한 부분)
    private void galleryAddPic(){
        Intent mediaScanIntent = new Intent( Intent.ACTION_MEDIA_SCANNER_SCAN_FILE);
        File file = new File( imagePath);
        Uri contentUri = Uri.fromFile(file);
        mediaScanIntent.setData( contentUri);
        this.sendBroadcast( mediaScanIntent);
    }

    // Bitmap 을 저장하는 메소드
    private void saveCropImage(Bitmap bitmap, String filePath) {
        File copyFile = new File(filePath);
        BufferedOutputStream bos = null;
        try {
            copyFile.createNewFile();
            bos = new BufferedOutputStream(new FileOutputStream(copyFile));
            bitmap.compress(Bitmap.CompressFormat.JPEG, 100, bos); // 이미지가 클 경우 OutOfMemoryException 발생이 예상되어 압축
            // sendBroadcast를 통해 Crop된 사진을 앨범에 보이도록 갱신한다.
            sendBroadcast(new Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE, Uri.fromFile(copyFile)));

            bos.flush();
            bos.close();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
   
}


테스트에 사용한 소스와 Gradle 를 첨부한 파일


OKHttpUpload.zip


PHP 소스는 이전 게시글에 언급했으므로 그걸 이용하면 된다.


API 26 으로 컴파일 한 경우, 사진 촬영부분이 CROP 으로 넘기는 부분에서 에러가 발생하는 원인 등을 찾아서 완벽하게 수정하면 게시글 내용을 수정할 것이다.


증명사진 정도의 이미지로 잘라서 업로드하는 부분은 주석처리하는 것을 없애면 된다.


                // CROP할 이미지를 125*170 크기로 저장
                intent.putExtra("outputX", 125); // CROP한 이미지의 x축 크기
                intent.putExtra("outputY", 170); // CROP한 이미지의 y축 크기
                intent.putExtra("aspectX", 1); // CROP 박스의 X축 비율
                intent.putExtra("aspectY", 1); // CROP 박스의 Y축 비율
                intent.putExtra("scale", true);
                intent.putExtra("return-data", true);



PHP 서버의 upload.php 파일 코드를 여기에도 적는다. (2018.05.31)

json_encode 는 파일 Encoding Mode 가 UTF-8 이어야만 한다.


 <?php
if(isset($_POST['idx']) && $_POST['idx']>0){
    $idx=$_POST['idx'];

    $file_path='./photos/'.$idx.'.jpg';//이미지화일명은 인덱스번호
    //$file_path = "";
    //$file_path = $file_path . basename( $_FILES['uploaded_file']['name']);
    if(move_uploaded_file($_FILES['uploaded_file']['tmp_name'], $file_path)) {
        $result = array("result" => "success");
    } else{
        $result = array("result" => "error");
    }
    echo json_encode($result);
}
?>


그리고 파일도 전체 테스트한 걸 모두 압축해서 올린다.

okhttpupload_1.zip


블로그 이미지

Link2Me

,
728x90

제목 : 개도국 배려한 페북 안드로이드 앱 업뎃 화제
관련기사 : http://www.zdnet.co.kr/news/news_view.asp?artice_id=20140620103700&type=det&re=

OkHttp는 안드로이드 및 자바 애플리케이션용 HTTP 및 SPDY 프로토콜 클라이언트다.
HTTP 프로토콜의 효율성을 높여주고 전체 네트워킹 속도를 향상시키면서 대역폭을 절감시켜준다.
스퀘어에서 만든 이 오픈소스 클라이언트는 전격적으로 페이스북 안드로이드앱에 채택됐다.
OkHttp로 교체하자 뉴스피드의 이미지 로드 실패가 줄어들었다.


안드로이드 스튜디오 기반 OKHttp 라이브러리를 이용하여 갤러리에서 이미지를 선택하여 서버(PHP)에 업로드하는 괜찮은 소스를 찾았다.


https://github.com/pratikbutani/OKHTTPUploadImage 에서 파일을 다운로드 받아서 Import 하면 예제를 실행해 볼 수 있다.


나중에 다른 소스에 활용하기 위해 내가 사용하는 코드 명칭으로 일부 수정했고, 해당 메소드에 대한 설명을 추가해서 테스트 했다.


gradle 추가

    compile 'com.squareup.okhttp3:okhttp:3.8.1'
    compile 'com.squareup.picasso:picasso:2.5.2'

서버에 업로드하려면 인터넷 연결이 되어야 하고 갤러리에 접속하려면 권한이 부여되어야 한다.

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


// 선택된 사진을 받아 서버에 업로드한다.
FloatingActionButton fab = (FloatingActionButton) findViewById(R.id.fab);
fab.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View view) {
        if (!TextUtils.isEmpty(imagePath)) {
            if (NetworkHelper.checkConnection(mContext)) { // 인터넷 연결 체크
                String ImageUploadURL = "http://192.168.0.100/upload/upload.php";
                new ImageUploadTask().execute(ImageUploadURL, imagePath);
            } else {
                Toast.makeText(mContext, "인터넷 연결을 확인하세요", Toast.LENGTH_LONG).show();
            }
        } else {
            Toast.makeText(mContext, "먼저 업로드할 파일을 선택하세요", Toast.LENGTH_SHORT).show();
        }
    }
});

private  class ImageUploadTask extends AsyncTask<String, Integer, Boolean> {
    ProgressDialog progressDialog;

    @Override
    protected void onPreExecute() {
        super.onPreExecute();
        progressDialog = new ProgressDialog(MainActivity.this);
        progressDialog.setMessage("이미지 업로드중....");
        progressDialog.show();
    }

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

        try {
            JSONObject jsonObject = JSONParser.uploadImage(params[0],params[1]);
            if (jsonObject != null)
                return jsonObject.getString("result").equals("success");

        } catch (JSONException e) {
            Log.i("TAG", "Error : " + e.getLocalizedMessage());
        }
        return false;
    }

    @Override
    protected void onPostExecute(Boolean aBoolean) {
        super.onPostExecute(aBoolean);
        if (progressDialog != null)
            progressDialog.dismiss();

        if (aBoolean)
            Toast.makeText(getApplicationContext(), "파일 업로드 성공", Toast.LENGTH_LONG).show();
        else
            Toast.makeText(getApplicationContext(), "파일 업로드 실패", Toast.LENGTH_LONG).show();

        imagePath = "";
        textView.setVisibility(View.VISIBLE);
        imageView.setVisibility(View.INVISIBLE);
    }
}

// 사진 선택을 위해 갤러리를 호출
private void getGallery() {
    // File System.
    final Intent galleryIntent = new Intent();
    galleryIntent.setType("image/*");
    galleryIntent.setAction(Intent.ACTION_PICK);

    // Chooser of file system options.
    final Intent chooserIntent = Intent.createChooser(galleryIntent, getString(R.string.string_choose_image));
    startActivityForResult(chooserIntent, 1010);
}

@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
    // URI 정보를 이용하여 사진 정보를 가져온다.
    super.onActivityResult(requestCode, resultCode, data);
    if (resultCode == RESULT_OK && requestCode == 1010) {
        if (data == null) {
            Snackbar.make(findViewById(R.id.parentView), "Unable to Pickup Image", Snackbar.LENGTH_INDEFINITE).show();
            return;
        }
        Uri selectedImageUri = data.getData();
        String[] filePathColumn = {MediaStore.Images.Media.DATA};

        Cursor cursor = getContentResolver().query(selectedImageUri, filePathColumn, null, null, null);

        if (cursor != null) {
            cursor.moveToFirst();

            int columnIndex = cursor.getColumnIndex(filePathColumn[0]);
            imagePath = cursor.getString(columnIndex);

            Picasso.with(mContext).load(new File(imagePath))
                    .into(imageView); // 피카소 라이브러를 이용하여 선택한 이미지를 imageView에  전달.
            cursor.close();

        } else {
            Snackbar.make(findViewById(R.id.parentView), "Unable to Load Image", Snackbar.LENGTH_LONG).setAction("Try Again", new View.OnClickListener() {
                @Override
                public void onClick(View v) {
                    getGallery();
                }
            }).show();
        }

        textView.setVisibility(View.GONE);
        imageView.setVisibility(View.VISIBLE);
    }
}



public class JSONParser {

    public static JSONObject uploadImage(String imageUploadUrl, String sourceImageFile) {

        try {
            File sourceFile = new File(sourceImageFile);
            Log.d("TAG", "File...::::" + sourceFile + " : " + sourceFile.exists());
            final MediaType MEDIA_TYPE_PNG = MediaType.parse("image/*");
            String filename = sourceImageFile.substring(sourceImageFile.lastIndexOf("/")+1);

            // OKHTTP3
            RequestBody requestBody = new MultipartBody.Builder()
                    .setType(MultipartBody.FORM)
                    .addFormDataPart("uploaded_file", filename, RequestBody.create(MEDIA_TYPE_PNG, sourceFile))
                    .addFormDataPart("result", "photo_image")
                    .build();

            Request request = new Request.Builder()
                    .url(imageUploadUrl)
                    .post(requestBody)
                    .build();

            OkHttpClient client = new OkHttpClient();
            Response response = client.newCall(request).execute();
            String res = response.body().string();
            Log.e("TAG", "Error: " + res);
            return new JSONObject(res);

        } catch (UnknownHostException | UnsupportedEncodingException e) {
            Log.e("TAG", "Error: " + e.getLocalizedMessage());
        } catch (Exception e) {
            Log.e("TAG", "Other Error: " + e.getLocalizedMessage());
        }
        return null;
    }
}


MediaType.parse("image/*")  // add ImageFile
MediaType.parse("image/jpeg") // add JPEG ImageFile
MediaType.parse("text/plain") // add TextFile
MediaType.parse("application/zip")  // add ZipFile
MediaType.parse("application/json; charset=utf-8")


PHP 파일

<?php
    $file_path = "";
    $file_path = $file_path . basename( $_FILES['uploaded_file']['name']);
    if(move_uploaded_file($_FILES['uploaded_file']['tmp_name'], $file_path)) {
        // 동일한 파일명이면 덮어쓰기를 한다.
        $result = array("result" => "success");
    } else{
        $result = array("result" => "error");
    }
    echo json_encode($result);
?>



=== OKHttp 라이브러리를 이용한 파일 전송 핵심코드 ===

final MediaType MEDIA_TYPE_PNG = MediaType.parse("image/*");
String filename = ImagePath.substring(ImagePath.lastIndexOf("/") + 1);

RequestBody requestBody = new MultipartBody.Builder()
        .setType(MultipartBody.FORM)
        .addFormDataPart("image", filename, RequestBody.create(MEDIA_TYPE_PNG, sourceFile))
        .addFormDataPart("result", "my_image")
        .addFormDataPart("username", name)
        .addFormDataPart("password", password)
        .addFormDataPart("email", email)
        .addFormDataPart("phone", phone)
        .build();

Request request = new Request.Builder()
        .url(BASE_URL + "signup")
        .post(requestBody)
        .build();

OkHttpClient client = new OkHttpClient();
okhttp3.Response response = client.newCall(request).execute();
res = response.body().string();

블로그 이미지

Link2Me

,
728x90

OKHttp 라이브러리를 이용한 로그인 예제를 만들어봤다.


http://square.github.io/okhttp/ 에 가면 최신버전을 확인할 수 있다.


1. Gradle 추가

    compile 'com.squareup.okhttp3:okhttp:3.8.1'

    gradle 에 추가하고 Sync 까지 해준다.


2. AndroidManifest.xml


3. OKHttpAPICAll 메소드

 import android.os.Build;

import com.tistory.link2me.common.AES256Cipher;

import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;

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

import okhttp3.FormBody;
import okhttp3.HttpUrl;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.RequestBody;
import okhttp3.Response;

public class OKHttpAPICall {
    //GET network request
    public static String GET(OkHttpClient client, HttpUrl url) throws IOException {
        Request request = new Request.Builder()
                .url(url)
                .build();
        Response response = client.newCall(request).execute();
        return response.body().string();
    }

    //POST network request
    public static String POST(OkHttpClient client, String url, RequestBody body) throws IOException {
        Request request = new Request.Builder()
                .url(url)
                .post(body)
                .build();
        Response response = client.newCall(request).execute();
        return response.body().string();
    }

    //Login request body
    public static RequestBody LoginBody(String loginID, String loginPW, String deviceID) {
        // 전달할 인자들
        String username = null;
        String password = null;
        try {
            username = AES256Cipher.AES_Encode(loginID, Value.AES256Key);
            password = AES256Cipher.AES_Encode(loginPW, Value.AES256Key);
        } 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 new FormBody.Builder()
                .add("action", "login")
                .add("format", "json")
                .add("loginID", username)
                .add("loginPW", password)
                .add("deviceID", deviceID)
                .add("phoneVersion", Build.VERSION.RELEASE)
                .add("phoneBrand", Build.BRAND)
                .add("phoneModel", Build.MODEL)
                .build();
    }

}


4. Login.java

import android.Manifest;
import android.app.Activity;
import android.app.AlertDialog;
import android.app.ProgressDialog;
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.SharedPreferences;
import android.content.pm.PackageManager;
import android.net.ConnectivityManager;
import android.net.NetworkInfo;
import android.os.AsyncTask;
import android.os.Build;
import android.os.Bundle;
import android.provider.Settings;
import android.support.v4.app.ActivityCompat;
import android.support.v4.content.ContextCompat;
import android.support.v7.app.AppCompatActivity;
import android.telephony.TelephonyManager;
import android.util.Log;
import android.view.View;
import android.widget.Button;
import android.widget.CheckBox;
import android.widget.EditText;
import android.widget.Toast;

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

import okhttp3.OkHttpClient;

import static com.tistory.link2me.okhttplogin.OKHttpAPICall.LoginBody;

public class Login extends AppCompatActivity {
    String getDeviceID; // 스마트기기의 장치 고유값
    EditText etId;
    EditText etPw;

    String loginID;
    String loginPW;
    CheckBox autologin;
    Boolean autologinchk;
    String idx;
    public SharedPreferences settings;

    private OkHttpClient client;
    String response;

    // 멀티 퍼미션 지정
    private String[] permissions = {
            Manifest.permission.READ_PHONE_STATE,
            Manifest.permission.CALL_PHONE, // 전화걸기 및 관리
            Manifest.permission.WRITE_CONTACTS, // 주소록 액세스 권한
            Manifest.permission.WRITE_EXTERNAL_STORAGE, // 기기, 사진, 미디어, 파일 엑세스 권한
            Manifest.permission.RECEIVE_SMS, // 문자 수신
            Manifest.permission.CAMERA
    };
    private static final int MULTIPLE_PERMISSIONS = 101;


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

        if (Build.VERSION.SDK_INT >= 23) { // 안드로이드 6.0 이상일 경우 퍼미션 체크
            checkPermissions();
        }

        // 네트워크 연결상태 체크
        if(NetworkConnection() == false){
            NotConnected_showAlert();
        }

        etId = (EditText) findViewById(R.id.login_id_edit);
        etPw = (EditText) findViewById(R.id.login_pw_edit);
        autologin = (CheckBox) findViewById(R.id.autologinchk);

        settings = getSharedPreferences("settings", Activity.MODE_PRIVATE);
        autologinchk = settings.getBoolean("autologin", false);
        if (autologinchk) {
            etId.setText(settings.getString("loginID", ""));
            etPw.setText(settings.getString("loginPW", ""));
            autologin.setChecked(true);
        }

        if(!settings.getString("loginID", "").equals("")) etPw.requestFocus();

        Button submit = (Button) findViewById(R.id.login_btn);
        submit.setOnClickListener(new Button.OnClickListener(){
            @Override
            public void onClick(View view) {
                loginID = etId.getText().toString().trim();
                loginPW = etPw.getText().toString().trim();

                if(loginID != null && !loginID.isEmpty() && loginPW != null && !loginPW.isEmpty()){
                    String url = Value.IPADDRESS + "/loginChk.php";
                    // 단말기의 ID 정보를 얻기 위해서는 READ_PHONE_STATE 권한이 필요
                    TelephonyManager mTelephony = (TelephonyManager) getSystemService(Context.TELEPHONY_SERVICE);
                    if (mTelephony.getDeviceId() != null){
                        getDeviceID = mTelephony.getDeviceId();  // 스마트폰 기기 정보
                    } else {
                        getDeviceID = Settings.Secure.getString(getApplicationContext().getContentResolver(), Settings.Secure.ANDROID_ID);
                    }
                    new AsyncLogin().execute(url,loginID, loginPW,getDeviceID);
                }
            }
        });
    }


    private  class AsyncLogin extends AsyncTask<String, Void, String> {
        ProgressDialog pdLoading = new ProgressDialog(Login.this);

        @Override
        protected void onPreExecute() {
            super.onPreExecute();
            //this method will be running on UI thread
            pdLoading.setMessage("\tValidating user...");
            pdLoading.setCancelable(false);
            pdLoading.show();
        }

        @Override
        protected String doInBackground(String... params) {
            client = new OkHttpClient();
            try {
                response = OKHttpAPICall.POST(client, params[0], LoginBody(params[1], params[2],params[3]));

                Log.d("Response", response);
                return response;
            } catch (IOException e) {
                e.printStackTrace();
            }
            return null;
        }

        protected void onPostExecute(String result){
            pdLoading.dismiss();
            if(Integer.parseInt(result) > 0){ // 로그인 정보 일치
                idx = result;
                Toast.makeText(getApplicationContext(),"로그인 성공", Toast.LENGTH_SHORT).show();
                startActivity(new Intent(getApplication(), MainActivity.class));
                finish(); // 현재 Activity 를 없애줌

            } else if(result.equalsIgnoreCase("-1")){ // 등록된 단말기와 불일치
                deviceDismatch_showAlert();
            } else if (result.equalsIgnoreCase("0")) {
                showAlert();
            } else {
                Toast.makeText(getApplicationContext(), "서버로부터 정보가 잘못 전송되었습니다", Toast.LENGTH_SHORT).show();
            }

        }

    }

    public void onStop(){
        // 어플리케이션이 화면에서 사라질때
        super.onStop();
        // 자동 로그인이 체크되어 있고, 로그인에 성공했으면 폰에 자동로그인 정보 저장
        if (autologin.isChecked()) {
            settings = getSharedPreferences("settings",Activity.MODE_PRIVATE);
            SharedPreferences.Editor editor = settings.edit();

            editor.putString("loginID", loginID);
            editor.putString("loginPW", loginPW);
            editor.putBoolean("autologin", true);
            editor.putString("idx", idx);

            editor.commit();
        } else {
            // 자동 로그인 체크가 해제되면 폰에 저장된 정보 모두 삭제
            settings = getSharedPreferences("settings",    Activity.MODE_PRIVATE);
            SharedPreferences.Editor editor = settings.edit();
            editor.clear(); // 모든 정보 삭제
            editor.commit();
        }

    }

    public void deviceDismatch_showAlert(){
        AlertDialog.Builder builder = new AlertDialog.Builder(Login.this);
        builder.setTitle("등록단말 불일치");
        builder.setMessage("최초 등록된 단말기가 아닙니다.\n" + "관리자에게 문의하여 단말기 변경신청을 하시기 바랍니다.")
                .setCancelable(false)
                .setPositiveButton("OK", new DialogInterface.OnClickListener() {
                    public void onClick(DialogInterface dialog, int id) {
                        dialog.dismiss();
                    }
                });
        AlertDialog alert = builder.create();
        alert.show();
    }

    public void showAlert(){
        AlertDialog.Builder builder = new AlertDialog.Builder(Login.this);
        builder.setTitle("로그인 에러");
        builder.setMessage("로그인 정보가 일치하지 않습니다.")
                .setCancelable(false)
                .setPositiveButton("OK", new DialogInterface.OnClickListener() {
                    public void onClick(DialogInterface dialog, int id) {
                        dialog.dismiss();
                    }
                });
        AlertDialog alert = builder.create();
        alert.show();
    }

    private void NotConnected_showAlert() {
        AlertDialog.Builder builder = new AlertDialog.Builder(Login.this);
        builder.setTitle("네트워크 연결 오류");
        builder.setMessage("사용 가능한 무선네트워크가 없습니다.\n" + "먼저 무선네트워크 연결상태를 확인해 주세요.")
                .setCancelable(false)
                .setPositiveButton("확인", new DialogInterface.OnClickListener() {
                    public void onClick(DialogInterface dialog, int id) {
                        finish(); // exit
                        //application 프로세스를 강제 종료
                        android.os.Process.killProcess(android.os.Process.myPid() );
                    }
                });
        AlertDialog alert = builder.create();
        alert.show();

    }

    private boolean NetworkConnection() {
        int[] networkTypes = {ConnectivityManager.TYPE_MOBILE, ConnectivityManager.TYPE_WIFI};
        try {
            ConnectivityManager manager = (ConnectivityManager) getSystemService (Context.CONNECTIVITY_SERVICE);
            for (int networkType : networkTypes) {
                NetworkInfo activeNetwork = manager.getActiveNetworkInfo();
                if(activeNetwork != null && activeNetwork.getType() == networkType){
                    return true;
                }
            }
        } catch (Exception e) {
            return false;
        }
        return false;
    }

    private boolean checkPermissions() {
        int result;
        List<String> permissionList = new ArrayList<>();
        for (String pm : permissions) {
            result = ContextCompat.checkSelfPermission(this, pm);
            if (result != PackageManager.PERMISSION_GRANTED) {
                permissionList.add(pm);
            }
        }
        if (!permissionList.isEmpty()) {
            ActivityCompat.requestPermissions(this, permissionList.toArray(new String[permissionList.size()]), MULTIPLE_PERMISSIONS);
            return false;
        }
        return true;
    }

    @Override
    public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) {
        switch (requestCode) {
            case MULTIPLE_PERMISSIONS: {
                if (grantResults.length > 0) {
                    for (int i = 0; i < permissions.length; i++) {
                        if (permissions[i].equals(this.permissions[i])) {
                            if (grantResults[i] != PackageManager.PERMISSION_GRANTED) {
                                showToast_PermissionDeny();
                            }
                        }
                    }
                } else {
                    showToast_PermissionDeny();
                }
                return;
            }
        }

    }

    private void showToast_PermissionDeny() {
        Toast.makeText(this, "권한 요청에 동의 해주셔야 이용 가능합니다. 설정에서 권한 허용 하시기 바랍니다.", Toast.LENGTH_SHORT).show();
        finish();
    }

}


5. Value.java

import android.app.Activity;

public class Value extends Activity {
    public static final String IPADDRESS = "http://192.168.0.100";
    public static final String VERSION = "1.0.0"; // App Version
    public static final String AES256Key = "hijklmn123456opqrstuvwxyz";
}


위 예제에 사용된 java 및 AndroidManifest.xml 소스 파일


OKHttp_Login.zip



참조 사이트

https://www.sitepoint.com/consuming-web-apis-in-android-with-okhttp/



블로그 이미지

Link2Me

,
728x90

명불허전 드라마를 보면서 조선제일침 허임에 관해 찾아봤다.

그동안 허준이 조선시대 침술의 최고봉인줄 알았는데, 허임이 조선시대 침술과 뜸의 대가였다.


허임(許任·1570~1647 추정)은 1570년에 전라도 나주 노비의 가정에서 태어났으며
부모의 병 때문에 의원의 집에서 잡일을 하면서 의술을 배우기 시작했다.

( 허임의 부모 묘소와 자식의 묘소가 충남 공주에 있는 것으로 봐서 공주가 허임의 삶의 터전과 연관이 있을 것이라는 얘기도 있다. 허임의 아버지(허억봉)가 당시 조정에서 악공을 시험하는 악보를 만드는 데 참여할 정도로 지명도가 높은 악공이었다는 기록도 있다.
이를 근거로 하면 허임의 아버지 대에서 이미 한양에서 거주하였을 가능성이 크고, 따라서 허임이 나서 자란 곳이 한양일 수도 있다.)

그의 아버지 억복(億福)은 강원도 양양 출신의 관노로, 피리 솜씨가 뛰어나 그 실력을 인정받아 이후 장악원에서 악공으로 일했다. 어머니는 정승 김귀영 집안의 사비였다.
비천한 가정에서 태어났으나 명성이 퍼져 나중에는 시의(侍醫)로 등용되기도 했다.
허임은 조선 시대를 대표하는 명의 허준(許浚·1539~1615)도 인정한 침술의 대가였다.
1612년에는 명의로 소문난 허준과 함께 의관록에 기록됐다.
뛰어난 침술로 소문이 난 허임은 선조의 편두통을 치료하여 정3품 당상관으로 파격 승진한다.
광해군이 보위에 오르면서 영평현령, 양주목사, 남양부사를 역임했다.


광해군 15년 (1623년) 허임이 50대 중반 정도 되던 해 의관들이 임금의 하교를 외부로 누설시켰다는 이유로 감봉을 당하는 것을 마지막으로 실록에서는 허임의 이름은 더 이상 등장하지 않았다.
허임이 조선왕조실록에 나타나지 않은 지 20년 후인 1644년, 허임이 70대에 이르러 조선시대 최고의 침구서적인 침구경험방이 간행된다.

침구경험방의 발문을 쓴 당시 춘추관이면서 내의원 제조를 맡고 있던 이경석은 당대의 문장가이다.
그는 발문에서 허임의 침술을 이렇게 표현하고 있다.

“태의 허임은 평소에 신의 의술로 일컬어졌고 평생 동안 치료한 사람은 손으로 꼽을 수가 없다.
그중에서는 죽어가는 사람을 살려낸 경우도 많아 일세에 명성을 떨쳤으며 침의들에게서는 으뜸으로
추앙되었다.
지금 이 경험방의 글은 눈으로 보고 마음에 담아두었다가 손으로 시험해 본 것이다.
분명치 않은 것은 분명하게 하고, 번거로운 것은 요약하고, 틀린 것은 바로잡았다.
질병의 원인과 치료의 중요한 묘방이 한번 책을 열면 곧바로 눈앞에 선명하니 간략하면서도
쉽고 요약되었으면서도 상세하다고 할 수 있다.”

허임은 그의 책 서문에서 “이제는 늙어서 그나마 올바른 법이 전해지지 못할까 근심하고 있다.
”며 노침구의원(老鍼灸醫員)의 심경을 피력하고, “감히 스스로를 옛사람의 저술에 견주려는 것이 아니라 단지 일생동안 고심한 것을 차마 버릴 수 없었기 때문이며, 읽는 사람들이 궁리해서 구급활명에 조금이라도 보탬이 있기를 바랄 뿐이다”라며 자신의 임상경험을 후대 사람들이 널리 활용하기를 기대하였다.

이러한 그의 기대는 그 후 침구경험방이 판본으로 여러 차례 간행되고, 전국각지에서 필사본으로도
수없이 만들어졌고, 언해본까지 나올 정도로 백성들의 생활의술로 자리를 잡았다. 그의 뜻은 이루지고 있었다.

허임이 세상을 떠난 반세기가 지난 1700년 전후 조선에 유학 왔던 일본 오오사카 출신 의사 산천순암
(山川淳菴)은 일본으로 돌아갈 때 침구경험방을 가지고 갔다.

그리고 1725년 일본판 침구경험방을 펴내면서 그는 서문에서 “조선의 침가(鍼家)들이 하나같이 다 허임의 경험방을 배우고 이용하여 좋은 효과를 거두는 것을 보았다”는 목격담을 전하고 다음과 같이 덧붙이고 있다.
“유독 조선을 침자(鍼刺)에 있어서는 최고라고 부른다. 평소 중국에까지 그 명성이 자자했다는 말이 정말 꾸며낸 말이 아니었다.”

또한 1800년대 말에는 청나라에서 '침구집성'이라는 책이 출판되었는데, 그 내용이 허임의 침구경험방을 거의 표절했을 정도다. 동양 삼국에서 모두 인정받는 침술이었던 것이다.

허임의 침구경험방은 허준의 동의보감과 함께 우리 민족의 자랑스러운 문화유산이다.


원래 허임의 9대조는 세종때 좌의정을 지낸 문경공 허조였다.

허조의 아들 허후(허임의 8대조인 허눌의 친형)는 수양대군의 정권장악에 반대하다 귀양을 가서 교형을 당하고,
그 허후의 아들인 허조(허임의 7대조인 허담의 친형,허후가 자식이 없어 허눌의 큰아들을 양자로 들였음)는 사육신 등과 함께 단종복위를 기도하다 발각되어 자결하였고, 이에 연루되어 허조의 두 아들도 교형을 당하였다.

그 일이 있던 세조 2년(1456년) 당시 허조의 손자 허충(許忠)은 어린 아이였으므로 괴산으로 유배되었다가 관노가 되는 것으로 하였고, 그 당시 허임의 조상도 장손 허충의 집안과 함께 괴산으로 유배되어 관노로 부처된 것이다.


선조실록 37년 9월 23일(1604년 음력 9월 23일) 기사를 보면, 왕의 편두통 증세가 위증한데도 허준은 침을 놓지 못하고 침의 허임이 올 때까지 기다리면서 왕이 "침을 놓는 것이 어떻겠는가?"라고 묻자 "증세가 긴급하니 상례에 구애받을 수는 없습니다. 여러 차례 침을 맞으시는 것이 미안한 듯하기는 합니다마는, 침의(針醫)들은 항상 말하기를 ‘반드시 침을 놓아 열기(熱氣)를 해소시킨 다음에야 통증이 감소된다’고 합니다. 『소신(小臣)은 침 놓는 법을 알지 못하오며(而小臣則不知針法)』, 그들의 말이 이러하기 때문에 아뢰는 것입니다(渠輩所言 如此故啓之矣). 허임도 평소에 말하기를 ‘경맥(經脈)을 이끌어낸 뒤에 아시혈(阿是穴)에 침을 놓을 수 있다’고 했는데, 이 말이 일리가 있는 듯 합니다(此言似有理)"라고 되어있다고 한다.


명불허전은

침을 든 조선 최고의 한의사 허임(김남길)과 메스를 든 현대 의학 신봉자 외과의 연경(김아중)이 400년을 뛰어넘어 펼치는 조선 왕복 메디활극이다.


명불허전에서 김남길(허임)과 김아중(최연경)의 연기력과 캐미에 매주 드라마 보는 재미가 솔솔하다.


명불허전 드라마 6회에서 허임은 아버지에게 천자문을 배우고 어머니한테 언문을 배웠다고 나온다. 세조가 양반집안을 천민으로 만들어 버렸지만, 대대손손 한문을 배우게 했다 보다.


신비한 침통을 본적이 있는 할아버지의 대사를 통해, 허준(엄효섭)이 과거에 조선에서 대한민국에 왔었다는 걸 짐작할 수 있게 한다.

조선왕복 메디 활극이 제목이니 만큼 앞으로도 조선시대로 다시 가는 계기가 계속 나올 것이라 생각된다.

매주 토요일, 일요일이 기다려진다. 다시 돌려봐도 너무 재미있다.




허준 (1539 ~ 1615) 1546년 생이라는 설이 유력하다고 하며, 경기도 양천에서 출생했다.

동의보감 저술

허준이 주치의로서 모셨던 선조는 그를 가리켜 "제서(諸書)에 널리 통달하여 약을 쓰는 데에 노련하다." 라고 평가한 바 있다.

양천 허씨의 시조인 허선문(許宣文)의 20세손으로 할아버지 허곤(許琨)은 무관으로 경상우수사를 지냈고 아버지 허론 역시 무관으로 용천부사를 지냈다.
그는 일찍이 글을 익혔으며, 다방면의 학문에 통달(通達)했는데, 그 중에서도 특히 유가(儒家),도가(道家),불가(佛家)를 어우르는 동양의 종합적 사상에 심취하였다고 한다. 이는 허준의 학문적 영역을 확대시키는 결과를 가져다 주었으며, 무엇보다도 훗날 동의보감의 집필에 지대한 영향을 미치게 된다.


드라마에 나온 유의태는 허준보다 100년 이상 늦은 시기에 활동했으며 허준과는 전혀 관계가 없다.


'드라마' 카테고리의 다른 글

도깨비 드라마 감상문  (0) 2017.01.22
블로그 이미지

Link2Me

,
728x90

Eclipse res (resource) 폴더에 drawable 폴더가 있다.

Android Studio res (resource) 폴더에 drawable 폴더와 mipmap 폴더가 있다.


mipmap/ 폴더는 런쳐 아이콘 이미지 자원만을 위한 폴더
drawable/ 폴더는 이미지 자원을 위한 폴더
drawable-hdpi
drawable-ldpi
drawable-mdpi
drawable-xhdpi
mdpi, hdpi, xhdip 등과 같은 단말의 밀도(density)에 따라 가장 적합한 이미지를 보여준다


Eclipse 기반으로 개발된 소스를 수작업으로 변경할 경우 이미지 폴더는 같은 폴더로 복사하여 옮기면 된다.

그리고 java 코드는 수작업으로 변경하는 편이 편하다.

import 로 변경한 것이 제대로 동작이 안될 수도 있다.


블로그 이미지

Link2Me

,
728x90

구글에서 Android Audio Player code 를 입력하여 검색하면 가장 먼저 검색되는 것이 https://www.androidhive.info/2012/03/android-building-audio-player-tutorial/ 게시글이다.

구글 계정을 허용하고 파일을 다운로드하면 Eclipse 기반 소스코드가 다운로드된다.

이 코드를 실행하면 바로 에러가 발생하면서 어플이 죽어버린다.


오디오 플레이 코드 구현을 통해서 기능을 익히고자 검색하고 몇가지 어플 코드를 참조하여 만들어보고 있는 중이다.


현재까지 구현한 APK 파일

linkaudioplayer-release.apk


설계 관점

1. 스마트폰에 있는 모든 음악파일을 검색해서 Custom ListView 에 보여준다.

   RecyclerView 가 ListView 의 단점을 개선한 ListView 인데 이 글 마지막 부분에 링크된 걸 참조하라.

2. Custom ListView 에 곡 정보를 클릭하면 AudioPlayer가 바로 재생된다.

    곡을 길게 클릭하면 전체선택, 선택한 리스트만 AudioPlayer ArrayList 에 담아서

    순차재생, 특정곡 반복재생, 랜덤재생을 할 수 있도록 한다.

3. 재생, 잠시멈춤, 5초 앞으로, 5초 뒤로, 이전곡, 다음곡 버튼을 제공한다.

    위 링크글에 구현되어 있으니 활용하면 된다.

4. 전화를 받으면 자동으로 재생되던 음악이 멈추고 통화가 끝나면 다시 음악이 재생된다.



1. 스마트폰에 있는 음악파일 리스트 가져오기

private void loadAudioList() {
     ContentResolver contentResolver = getContentResolver();
     // 음악 앱의 데이터베이스에 접근해서 mp3 정보들을 가져온다.

     Uri uri = MediaStore.Audio.Media.EXTERNAL_CONTENT_URI;
     String selection = MediaStore.Audio.Media.IS_MUSIC + "!= 0";
     String sortOrder = MediaStore.Audio.Media.TITLE + " ASC";
     Cursor cursor = contentResolver.query(uri, null, selection, null, sortOrder);
     cursor.moveToFirst();
     System.out.println("음악파일 개수 = " + cursor.getCount());
     if (cursor != null && cursor.getCount() > 0) {
         do {
             long track_id = cursor.getLong(cursor.getColumnIndex(MediaStore.Audio.Media._ID));
             long albumId = cursor.getLong(cursor.getColumnIndex(MediaStore.Audio.Media.ALBUM_ID));
             String title = cursor.getString(cursor.getColumnIndex(MediaStore.Audio.Media.TITLE));
             String album = cursor.getString(cursor.getColumnIndex(MediaStore.Audio.Media.ALBUM));
             String artist = cursor.getString(cursor.getColumnIndex(MediaStore.Audio.Media.ARTIST));
             Integer mDuration = cursor.getInt(cursor.getColumnIndex(MediaStore.Audio.Media.DURATION));
             String datapath = cursor.getString(cursor.getColumnIndex(MediaStore.Audio.Media.DATA));
             System.out.println("mId = " + track_id + " albumId = " + albumId + " title : " + title + " album : " + album + " artist: " + artist + " 총시간 : " + mDuration + " data : " + data);
             // Save to audioList
             listViewAdapter.addItem(track_id, albumId, title, artist, album, mDuration, datapath, false);
         } while (cursor.moveToNext());
     }
     cursor.close();
 }


ContentResolver 를 통해서 음악파일을 가져온다.

가져온 음악파일을 ArrayList 에 추가한다.

Song_Item 에 checkBoxState boolean 을 넣은 이유는 선택한 곡만 true, false 하기 위해서다.


 import java.io.Serializable;

public class Song_Item implements Serializable {
    private long mId; // 오디오 고유 ID
    private long AlbumId; // 오디오 앨범아트 ID
    private String Title; // 타이틀 정보
    private String Artist; // 아티스트 정보
    private String Album; // 앨범 정보
    private Integer Duration; // 재생시간
    private String DataPath; // 실제 데이터 위치
    boolean checkBoxState;

    public Song_Item() {
    }

    public Song_Item(long mId, long albumId, String title, String artist, String album, Integer duration, String dataPath, boolean checkBoxState) {
        this.mId = mId;
        AlbumId = albumId;
        Title = title;
        Artist = artist;
        Album = album;
        Duration = duration;
        DataPath = dataPath;
        this.checkBoxState = checkBoxState;
    }

    public long getmId() {
        return mId;
    }

    public void setmId(long mId) {
        this.mId = mId;
    }

    public long getAlbumId() {
        return AlbumId;
    }

    public void setAlbumId(long albumId) {
        AlbumId = albumId;
    }

    public String getTitle() {
        return Title;
    }

    public void setTitle(String title) {
        Title = title;
    }

    public String getArtist() {
        return Artist;
    }

    public void setArtist(String artist) {
        Artist = artist;
    }

    public String getAlbum() {
        return Album;
    }

    public void setAlbum(String album) {
        Album = album;
    }

    public Integer getDuration() {
        return Duration;
    }

    public void setDuration(Integer duration) {
        Duration = duration;
    }

    public String getDataPath() {
        return DataPath;
    }

    public void setDataPath(String dataPath) {
        DataPath = dataPath;
    }

    public boolean isCheckBoxState() {
        return checkBoxState;
    }

    public void setCheckBoxState(boolean checkBoxState) {
        this.checkBoxState = checkBoxState;
    }
}



listViewAdapter.addItem(track_id, albumId, title, artist, album, mDuration, datapath, false);

  // 음악 데이터 추가를 위한 메소드
 public void addItem(long mId, long AlbumId, String Title, String Artist, String Album, Integer Duration, String DataPath, boolean checkItem_flag) {
     Song_Item item = new Song_Item();
     item.setmId(mId);
     item.setAlbumId(AlbumId);
     item.setTitle(Title);
     item.setArtist(Artist);
     item.setAlbum(Album);
     item.setDuration(Duration);
     item.setDataPath(DataPath);
     item.setCheckBoxState(checkItem_flag);
     songsList.add(item);
 }


private ListView audiolistView; // 리스트뷰
private ArrayList<Song_Item> songsList = null; // 음악 전체 데이터 리스트
private ArrayList<Song_Item> playList = new ArrayList<>(); // 선택한 음악 데이터 리스트
private ListViewAdapter listViewAdapter = null; // 리스트뷰에 사용되는 ListViewAdapter


전체 선택, 선택 해제 버튼 구현 코드

// all checkbox
checkAll = (CheckBox) findViewById(R.id.lv_checkbox_all);
checkAll.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {
    @Override
    public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
        if (checkAll.isChecked() == true) {
            isAll = true;
            listViewAdapter.setAllChecked(checkAll.isChecked());
            listViewAdapter.notifyDataSetChanged();
        } else {
            isAll = false;
            listViewAdapter.setAllChecked(checkAll.isChecked());
            listViewAdapter.notifyDataSetChanged();
        }
    }
});

final Button calcel = (Button) findViewById(R.id.btn_cancle);
calcel.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View v) {
        isMSG = false;
        relative1.setVisibility(View.GONE);
        listViewAdapter.setAllChecked(false);
        checkAll.setChecked(false); // 전체 선택 체크박스 해제
        listViewAdapter.notifyDataSetChanged();
    }
});

 Button send = (Button) findViewById(R.id.btn_send); // 선택한 곡 Player.class 로 전송
send.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View v) {
        playList.clear();
        for(int i = 0; i< songsList.size(); i++){
            if(songsList.get(i).isCheckBoxState() == true){
                PlayList(songsList.get(i).getmId(), songsList.get(i).getAlbumId(), songsList.get(i).getTitle(), songsList.get(i).getArtist(),
                        songsList.get(i).getAlbum(), songsList.get(i).getDuration(), songsList.get(i).getDataPath());
            }
        }

        if(playList.size() ==0){
            Toast.makeText(MainActivity.this, "선택한 리스트가 없습니다.", Toast.LENGTH_SHORT).show();
            return;
        } else {
            Intent intent = new Intent(MainActivity.this, Player.class);
            intent.putExtra("playList", playList); // 배열 데이터
            startActivity(intent);

            calcel.performClick(); // 버튼 강제로 자동 실행
        }
    }
});


// 음악 재생 데이터 추가를 위한 메소드
public void PlayList(long mId, long AlbumId, String Title, String Artist, String Album, Integer Duration, String DataPath) {
    Song_Item item = new Song_Item();
    item.setmId(mId);
    item.setAlbumId(AlbumId);
    item.setTitle(Title);
    item.setArtist(Artist);
    item.setAlbum(Album);
    item.setDuration(Duration);
    item.setDataPath(DataPath);
    playList.add(item);
}


Player.Class 주요 코드


// 배열로 넘긴 데이터를 배열로 받는 방법
private ArrayList<Song_Item> list;
Intent intent = getIntent();
list =(ArrayList<Song_Item>) intent.getSerializableExtra("playList");

private int position = 0; // 현재 재생곡 위치
playMusic(list.get(position)); // 노래 곡

public void playMusic(Song_Item song_item) {
    try {
        songTitleLabel.setText(song_item.getTitle());
        mediaPlayer.reset();
        mediaPlayer.setDataSource(song_item.getDataPath());
        mediaPlayer.prepare();
        mediaPlayer.start(); // 노래 재생 시작

        if(mediaPlayer.isPlaying()){
            btnPlay.setVisibility(View.GONE);
            btnPause.setVisibility(View.VISIBLE);
        } else {
            btnPlay.setVisibility(View.VISIBLE);
            btnPause.setVisibility(View.GONE);
        }

        /* Album Art Bitmap을 얻는다. */
        final Uri artworkUri = Uri.parse("content://media/external/audio/albumart");
        ImageView mImgAlbumArt = (ImageView) findViewById(R.id.albumart);
        Uri albumArtUri = ContentUris.withAppendedId(artworkUri, song_item.getAlbumId());
        Picasso.with(Player.this)
                .load(albumArtUri)
                .resize(800,800)
                .into(mImgAlbumArt);

        // set Progress bar values
        songProgressBar.setProgress(0);
        songProgressBar.setMax(100);

        // Updating progress bar
        updateProgressBar();

    } catch (IOException e) {
        e.getMessage();
        Toast.makeText(Player.this, "Error!!", Toast.LENGTH_SHORT).show();
    }
}


Albumart 이미지를 얻는 방법

1

 ImageView mImgAlbumArt = (ImageView) findViewById(R.id.albumart);
 Bitmap albumArt = getArtworkQuick(Player.this, (int) song_item.getAlbumId(), 800, 800);
 mImgAlbumArt.setImageBitmap(albumArt);

 2

 final Uri artworkUri = Uri.parse("content://media/external/audio/albumart");
 ImageView mImgAlbumArt = (ImageView) findViewById(R.id.albumart);
 Uri albumArtUri = ContentUris.withAppendedId(artworkUri, song_item.getAlbumId());
 Picasso.with(Player.this).load(albumArtUri).into(mImgAlbumArt);


검색을 하면 getArtworkQuick 메소드를 이용해서 하는 방법이 검색될 것이다.

이걸로 컴파일한 것이 폰이 낮은 버전에서 제대로 동작이 안되는 증상(?)이 있었다.


2번째 방법으로 하면 쉽게 해결된다.

Picasso(피카소)는 안드로이드를 위한 이미지 라이브러리다.
이것은 Square에 의해 만들어졌고, 이미지를 불러오거나 처리할 수 있도록 해준다.
피카소는 이미지를 디스플레이하는 과정을 외부 장소를 통해 단순화 시킨다.
이 유용한 라이브러리를 이용하면 고작 몇 줄로 이미지를 로딩할 수 있다.
build.gradle 파일 안에 dependency 코드 안에 compile 'com.squareup.picasso:picasso:2.5.2' 을 추가하면 자동 설치된다.


SeekBar 에 대한 것은 구글링을 해서 코드를 좀 찾아서 수정하거나
https://www.androidhive.info/2012/03/android-building-audio-player-tutorial/ 에 첨부파일 참조해서 구현하면 된다.



public void updateProgressBar() {
    seekHandler.postDelayed(mUpdateTimeTask, 100);
}

private Runnable mUpdateTimeTask = new Runnable() {
    public void run() {
        if(mediaPlayer != null){
            long totalDuration = mediaPlayer.getDuration();
            long currentDuration = mediaPlayer.getCurrentPosition();

            // 총 재생시간 화면 표시
            songTotalDurationLabel.setText("" + UtilitiesHelper.milliSecondsToTimer(totalDuration));
            // 현 재생시간 화면 표시
            songCurrentDurationLabel.setText("" + UtilitiesHelper.milliSecondsToTimer(currentDuration));

            // Updating progress bar
            int progress = (int)(UtilitiesHelper.getProgressPercentage(currentDuration, totalDuration));
            songProgressBar.setProgress(progress);

            // Running this thread after 100 milliseconds
            seekHandler.postDelayed(this, 100);
        }
    }
};

SeekBar.OnSeekBarChangeListener seekbarListener = new SeekBar.OnSeekBarChangeListener() {
    @Override
    public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
        try {
            if(mediaPlayer.isPlaying() || mediaPlayer != null){
                if(fromUser)
                    mediaPlayer.seekTo(progress);
            } else if(mediaPlayer == null){
                Toast.makeText(getApplicationContext(), "Media is not running", Toast.LENGTH_SHORT).show();
                seekBar.setProgress(0);
            }
            if (seekBar.getMax()==progress) {
                songProgressBar.setProgress(0);
                long currentDuration = 0;
                songCurrentDurationLabel.setText("" + UtilitiesHelper.milliSecondsToTimer(currentDuration));
            }
        } catch (Exception e){
            Log.e("seek bar", "" + e);
            seekBar.setEnabled(false);
        }

    }

    @Override
    public void onStartTrackingTouch(SeekBar seekBar) {
        mediaPlayer.pause(); // 손으로 트랙바를 움직이는 동안 재생 중지
        seekHandler.removeCallbacks(mUpdateTimeTask);
    }

    @Override
    public void onStopTrackingTouch(SeekBar seekBar) {
        seekHandler.removeCallbacks(mUpdateTimeTask);
        int totalDuration = mediaPlayer.getDuration();
        int currentPosition = UtilitiesHelper.progressToTimer(seekBar.getProgress(), totalDuration);

        // 손으로 움직인 지점, forward, backward 지점부터 재생
        mediaPlayer.seekTo(currentPosition);
        if(seekBar.getProgress()>0 ){
            mediaPlayer.start();
        }
        // update timer progress again
        updateProgressBar();
    }
};

 mediaPlayer.setOnCompletionListener(new MediaPlayer.OnCompletionListener() {
    @Override
    public void onCompletion(MediaPlayer mp) {

        seekHandler.removeCallbacks(mUpdateTimeTask);
        if(isRepeat){ // 반복재생
            playMusic(list.get(position));
        } else if(isShuffle){ // 랜덤재생
            Random rand = new Random();
            position = rand.nextInt((list.size() - 1) - 0 + 1) + 0;
            playMusic(list.get(position));
        } else {
            if(position + 1 < list.size()) {
                position++;
                playMusic(list.get(position));
            } else {
                Log.e("Music_Off","음악종료");               
                position = 0;
                getWindow().clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);

                acquireCPUWakeLock(mContext);

                releaseCpuLock();
            }
        }

    }

});


AndroidManifest.xml

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

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

    <uses-permission android:name="android.permission.INTERNET" />
    <uses-permission android:name="android.permission.WAKE_LOCK" />
    <uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
    <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
    <uses-permission android:name="android.permission.READ_PHONE_STATE" />

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

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

</manifest>


ListView 방식을 RecyclerView 방식으로 변경해보면서 변경할 부분에 중점적으로 적어둔 것은 http://link2me.tistory.com/1374 에 있다.


앞으로 분석하면 도움이 될 사항이라 링크를 적어둔다.

https://github.com/googlesamples/android-UniversalMusicPlayer

블로그 이미지

Link2Me

,
728x90

2019.5월 구글에서 GCM removed 되어 FCM 으로 코딩 변경해야 사용 가능하다.

-----------------------------------------------------------------------------------------------------


구글 GCM(Google Cloud Message) 를 이용한 PUSH 메시지 전송을 하려면 https://console.developers.google.com 에서 API 를 등록하고 사용자를 등록해야 한다.


PUSH 메시지 개념은 서버에서 보내는 메시지를 스마트폰에서 받는 것으로 구글에서 제공하는 GCM(Google Cloud Message) 서비스를 이용한다.


현재는 FCM 으로 등록하라고 URL 을 제공하기 때문에 GCM 등록은 쉽지 않다.

FCM 으로 메시지 전송하는 걸 제대로 하려면 PHP 기반 게시판까지 연동해야 될 거 같아서 테스트하고 정리까지 하기에는 시간이 좀 걸릴거 같다.




사용자정보 등록방법



동일한 키를 가지고 서버 IP만 여러개 등록하면 어플 여러개에서 GCM 을 사용할 수 있다.



구글에서 등록할 기능은 위와같이 하면 끝난다.


GCM Sender.php 파일 일부 내용

function AndroidPush($registrationIDs){
    global $message;

    $apiKey ="AIzaSyCoq9ccKiIrovwqm4mY0Ss"; // 생성코드(일부 내용 수정했음)
    $url = 'https://android.googleapis.com/gcm/send';
    $headers = array( 'Authorization: key=' . $apiKey, 'Content-Type: application/json' );

    $data = array('registration_ids' => $registrationIDs, 'data' => array( "msg" => $message ) );

    // Open connection
    $ch = curl_init();

    // Set the url, number of POST vars, POST data
    curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
    curl_setopt($ch, CURLOPT_URL,$url);
    curl_setopt ($ch, CURLOPT_SSL_VERIFYHOST, 0);
    curl_setopt ($ch, CURLOPT_SSL_VERIFYPEER, 0);
    curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
    curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($data));

    // Execute post
    $reponse = curl_exec($ch);

    // Close connection
    curl_close($ch);
    $_rs=json_decode($reponse,true);
    return $_rs['success'];
}


Android 코드에서 사용할 서비스 계정 ID








블로그 이미지

Link2Me

,
728x90

PHP 서버에 있는 자료를 가져와서 안드로이드에서 처리하는 AsyncTask 함수다.

요청 : Text POST, 결과 : JSON, Text

테스트 환경 : Android Studio, Eclipse


 === PHPComm.java ===

 package com.tistory.link2me.common;

import java.io.BufferedReader;
import java.io.DataOutputStream;
import java.io.InputStreamReader;
import java.net.HttpURLConnection;
import java.net.URL;

import android.app.Activity;
import android.util.Log;
import android.webkit.CookieManager;

public class PHPComm extends Activity {

    // serverURL : JSON 요청을 받는 서버의 URL
    // postParams : POST 방식으로 전달될 입력 데이터
    // 반환 데이터 : 서버에서 전달된 JSON 데이터
    public static String getJson(String serverUrl, String postParams) throws Exception {

        BufferedReader bufferedReader = null;  
        try {  
            Thread.sleep(100);
            URL url = new URL(serverUrl);  
            HttpURLConnection conn = (HttpURLConnection) url.openConnection();
            // 세션 쿠키 전달
            String cookieString = CookieManager.getInstance().getCookie(Value.IPADDRESS);
            
            StringBuilder sb = new StringBuilder();  
            sb.setLength(0);

            if(conn != null){ // 연결되었으면
                //add request header
                conn.setRequestMethod("POST");
                conn.setRequestProperty("USER-AGENT", "Mozilla/5.0");
                conn.setRequestProperty("Content-Type", "application/x-www-form-urlencoded");
                conn.setRequestProperty("Accept-Language", "en-US,en;q=0.5");
                if (cookieString != null) {
                   conn.setRequestProperty("Cookie", cookieString);
                   Log.e("PHP_getCookie", cookieString);
                 }
                conn.setConnectTimeout(10000);
                conn.setReadTimeout(10000);
                conn.setUseCaches(false);
                conn.setDefaultUseCaches(false);
                conn.setDoOutput(true); // POST 로 데이터를 넘겨주겠다는 옵션
                conn.setDoInput(true);

                // Send post request
                DataOutputStream wr = new DataOutputStream(conn.getOutputStream());
                wr.writeBytes(postParams);
                wr.flush();
                wr.close();

                int responseCode = conn.getResponseCode();
                System.out.println("GET Response Code : " + responseCode);        
                if(responseCode == HttpURLConnection.HTTP_OK){ // 연결 코드가 리턴되면
                    bufferedReader = new BufferedReader(new InputStreamReader(conn.getInputStream()));
                    String json;
                    while((json = bufferedReader.readLine())!= null){
                        sb.append(json + "\n");
                    }      
                }
                bufferedReader.close();
            }
            System.out.println("PHP Comm Out Size : " + sb.length()); // 서버에서 보내준 결과의 길이
            System.out.println("PHP Comm Out : " + sb); // 서버에서 보내준 내용
            return sb.toString().trim();
            
        } catch(Exception e){  
            return new String("Exception: " + e.getMessage());
        }
 
    }
}


위 함수의 사용법 예제

 public class MainActivity extends Activity {
    String idx;
    String regdate;
    
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);

        SharedPreferences pref = getSharedPreferences("pref", Activity.MODE_PRIVATE);
        idx = pref.getString("idx", "");
        regdate = pref.getString("regdate", "");
        SQLiteDBChk(idx, regdate); // 서버 데이터를 가져와서 SQLiteDB에 저장하는 로직

    }

    private void SQLiteDBChk(String idx, String regdate) {
        Uri.Builder builder = new Uri.Builder()
            .appendQueryParameter("idx", idx)
            .appendQueryParameter("regdate", regdate);
        String postParams = builder.build().getEncodedQuery();

        getChartData getData = new getChartData();
        getData.execute(Value.IPADDRESS + "/SQLiteDB.php", postParams);

    }

    class getChartData extends AsyncTask<String, Void, String> {

        @Override
        protected String doInBackground(String... params) {
            try {
                return PHPComm.getJson(params[0], params[1]);
                // 수행이 끝나고 리턴하는 값은 다음에 수행될 onProgressUpdate 의 파라미터가 된다
            } catch (Exception e) {
                return new String("Exception: " + e.getMessage());
            }
        }

        protected void onPostExecute(String result) {
            // 서버에서 전송받은 결과를 파싱하여 처리

        }
    }

}


위 코드 파일

PHPComm.zip

위 코드는 Eclipse 에서는 쿠키값이 잘 전달되는데 Android Studio 에서는 쿠키정보가 성공적이지 못했다.

쿠키를 이용해서 정보를 주고받지 않고 다른 키(idx)를 이용하여 정보를 주고 받는다.

쿠키 부분 코드는 삭제해도 되고 그냥 두어도 된다.


블로그 이미지

Link2Me

,
728x90

CentOS SSH 접속포트를 변경하는 방법을 적어둔다.



#Port 22 라고 주석처리되어 있다는 건 기본으로 22번 포트를 사용하고 있다는 의미다.

주석 제거하고 변경하고 싶은 포트를 적어준다.

수정하고 :wq 로 저장하고 빠져나온다.


SSH 데몬을 재구동한다.


# service sshd restart

아래 그림에서 보면 둘 중 어느것을 하더라도 동일하다는 걸 알 수 있다.



포트를 변경하면 반드시 방화벽에서 해당 포트를 등록해줘야만 접속이 가능하다.


-A INPUT -p tcp -m state --state NEW -m tcp --dport 1922 -j ACCEPT


방화벽 변경은 아래 순서로 해준다.

vi /etc/sysconfig/iptables
// 방화벽 재시작
/etc/init.d/iptables restart
// 변경한 정보 저장(재시작을 하고 나서 저장해야 적용된 정보가 저장됨)
service iptables save

'리눅스' 카테고리의 다른 글

리눅스 시간 동기화  (0) 2018.04.30
iptime 공유기 포트포워딩  (0) 2018.04.19
phpize 실행시 에러  (0) 2017.06.08
PHP mcrypt 동적 모듈 추가  (0) 2017.06.08
php sockets 동적 모듈 추가 (phpize)  (0) 2017.06.08
블로그 이미지

Link2Me

,
728x90

How about : ~하는건 어때? 라는 제안할 때 사용하는 표현이다.
Use how about to make suggestion and open possiblities.

A : I have the day off from work tomorrow. What should we do?
B : How about spending the day in the city?
A : Nah, I don't feeling like traveling.
B : OK, then how about cleaning the house?
A : No way. I want to do something fun.
B : How about doing some shopping and then seeing a movie.

I'm going to have lunch. How about you?
나 점심 먹을거야. 너는 어때?


We'll need to talk about this again. How about meeting next week?
우리 이것에 대해 다시 얘기 좀 해야 돼. 다음주말 미팅 어때?



What about : 어떻게 돼? (상대방의 의견)을 물을 때 사용하는 표현이다.
Use What about to mention an objection or potential problem with the plan or idea.

A : Let's go camping this weekend.
B : But what about my guitar lesson on Saturday? (근데 토요일 내 기타 수업은 어쩌지?)
A : Just call the teacher and reschedule it.
B : And What about the English test on Monday? I haven't studied yet.
A : You can study on Sunday night when we get back.
B : OK, but what about the weather? Isn't it going to be rather cold?

What about the lost wallet? 지갑 잃어버린거 어떻게 됐어?



How about you? = What about you? (같은 의미)

A : How have you been?

B : Good. How about you?


'영어 > 뉘앙스 어휘' 카테고리의 다른 글

just vs only  (0) 2017.12.27
trust 와 believe 의 차이  (0) 2016.02.23
chance 와 opportunity 의 차이  (0) 2016.02.18
think 와 guess 의 차이  (0) 2016.02.07
in a rut  (0) 2016.01.29
블로그 이미지

Link2Me

,
728x90

안드로이드 6.0 버전 이상부터 권한 체크 기능을 선택할 수 있도록 만들었다.

앱에서 해당 권한이 필요할때마다 사용자로부터 권한을 허가받도록 변경되었다.

사용자가 권한을 허가했더라도 사용자는 설정화면(설정 > 애플리케이션 > 앱이름 > 권한)을 통해 언제든지 권한을 허용/거부 할 수 있다.


테드 퍼미션 설정 방법 코드 : https://link2me.tistory.com/1509

아래 코드로 해도 되지만 테드 퍼미션 사용하고 부터는 이걸로 활용하고 있다.


개별로 설정해서 사용하다가 다른 자료를 찾다가 코드에 포함된 걸 발견하고 테스트해보니 아주 잘된다.

이 코드만 활용하면 퍼미션 지정때문에 골치아플 일은 없을 것이다.


1. 먼저 알아야 할 사항은 AndroidManifest.xml 에 권한 옵션이 추가되어 있어야 한다.

    아래 코드가 전부 필요한 것이 아니다. 필요한 것만 이용하면 된다.

<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.CHANGE_NETWORK_STATE" />
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
<uses-permission android:name="android.permission.CHANGE_WIFI_STATE" />
<uses-permission android:name="android.permission.WAKE_LOCK" />
<uses-permission android:name="android.permission.VIBRATE" />
<!-- 전화걸려올경우 상대방 정보 확인 -->
<uses-permission android:name="android.permission.READ_PHONE_STATE" />
<uses-permission android:name="android.permission.PROCESS_OUTGOING_CALLS" />
<uses-permission android:name="android.permission.CALL_PHONE" />
<uses-permission android:name="android.permission.MODIFY_PHONE_READ" />
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW"/>
<uses-permission android:name="android.permission.READ_CONTACTS" />
<uses-permission android:name="android.permission.WRITE_CONTACTS" />
<uses-permission android:name="android.permission.RECEIVE_SMS" />
<uses-permission android:name="android.permission.RECEIVE_MMS" />
<uses-permission android:name="android.permission.READ_SMS" />
<uses-permission android:name="android.permission.WRITE_SMS" />
<uses-permission android:name="android.permission.SEND_SMS" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.CAMERA" />
<uses-feature android:name="android.hardware.camera" />
<uses-feature android:name="android.hardware.camera.autofocus" />


2. java 코드에서 멀티 퍼미션 설정코드

    멀티퍼미션에 항목이 6개 이지만 실제로는 5개를 설정하도록 나온다. (동일 권한체크를 알아서 배제하더라)

 public class Login extends Activity {
    // 멀티 퍼미션 지정
    private String[] permissions = {
            Manifest.permission.READ_PHONE_STATE,
            Manifest.permission.CALL_PHONE, // 전화걸기 및 관리
            Manifest.permission.WRITE_CONTACTS, // 주소록 액세스 권한
            Manifest.permission.WRITE_EXTERNAL_STORAGE, // 기기, 사진, 미디어, 파일 엑세스 권한
            Manifest.permission.RECEIVE_SMS, // 문자 수신
            Manifest.permission.CAMERA
            };
    private static final int MULTIPLE_PERMISSIONS = 101;

    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_login);

        if (Build.VERSION.SDK_INT >= 23) { // 안드로이드 6.0 이상일 경우 퍼미션 체크
            checkPermissions();
        }

    }

    private boolean checkPermissions() {
        int result;
        List<String> permissionList = new ArrayList<>();
        for (String pm : permissions) {
            result = ContextCompat.checkSelfPermission(this, pm);
            if (result != PackageManager.PERMISSION_GRANTED) {
                permissionList.add(pm);
            }
        }
        if (!permissionList.isEmpty()) {
            ActivityCompat.requestPermissions(this, permissionList.toArray(new String[permissionList.size()]), MULTIPLE_PERMISSIONS);
            return false;
        }
        return true;
    }

    @Override
    public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) {
        switch (requestCode) {
            case MULTIPLE_PERMISSIONS: {
                if (grantResults.length > 0) {
                    for (int i = 0; i < permissions.length; i++) {
                        if (permissions[i].equals(this.permissions[i])) {
                            if (grantResults[i] != PackageManager.PERMISSION_GRANTED) {
                                showToast_PermissionDeny();
                            }
                        }
                    }
                } else {
                    showToast_PermissionDeny();
                }
                return;
            }
        }

    }

    private void showToast_PermissionDeny() {
        Toast.makeText(this, "권한 요청에 동의 해주셔야 이용 가능합니다. 설정에서 권한 허용 하시기 바랍니다.", Toast.LENGTH_SHORT).show();
        finish();
    }

}


위 코드는 http://programmar.tistory.com/5 에 링크된 GitHub 소스에 포함되어 있던 건데 아주 약간 수정했다.


소스코드 첨부 (퍼미션 지정에 필요한 코드만 발췌 수정)

PermissionChk.java


개별로 사용하던 퍼미션 설정 코드도 첨부해둔다.

PermissionCheck_Indivisual.java



라이브러리를 사용하는 것이 훨씬 더 편하므로 아래 사이트를 참조하면 좋다.

https://brunch.co.kr/@babosamo/50


퍼미션 라이브러리 관련 사이트

https://www.androidhive.info/2017/12/android-easy-runtime-permissions-with-dexter/


https://thanosfisherman.github.io/posts/mayi/


http://gun0912.tistory.com/61  TED Permission (추천)


https://hanburn.tistory.com/173  RxPermission (추천)



블로그 이미지

Link2Me

,
728x90

Node.js 서버 환경이 구축되었으니 이제 간단한 웹서버를 만들어서 동작하는지 확인해보자.

 

준비사항

nodejs Web Sever 파일을 저장할 디렉토리를 정한다.

편의상 기존 Web 서버의 하위 디렉토리에 두는 것이 여러모로 편할 거 같아서 /usr/local/apache/htdocs/nodejs 디렉토리를 생성했다.

cd /usr/local/apache/htdocs/nodejs

 

이제 샘플코드를 https://nodejs.org/en/about/ 에서 샘플로 제공한 코드를 EditPlus 에 복사한 다음 코드를 수정했다.

 

const http = require('http');
// http 모듈에 들어있는 서버 기능을 사용하려면 require()메소드로 http모듈을 불러온다.
 
const hostname = '127.0.0.1'// 실제 사용 IP로 변경해야 정상 동작됨
const port = 3000;
 
// http 객체의 createServer() 메소드를 호출하면 서버 객체가 반환된다.
http.createServer((req, res) => {
  res.writeHead(200, { 'Content-Type''text/plain' });
  res.end('Welcome to Nodejs Server!!!\n');
}).listen(port, hostname, () => {
  console.log(`Server running at http://${hostname}:${port}/`);
});
// 이 서버 객체의 listen() 메소드를 호출하면 웹서버가 시작된다. 

 

 

vi webserver.js 파일을 생성한다.

위에 수정한 코드를 붙여넣기하고 :wq 로 저장하고 빠져나온다.

 

파일을 실행한다.

 

 

Server running 이라고 화면에 나온다.

 

이제 Web 브라우저에서 접속해보자.

라고 화면에 출력되는 것을 확인할 수 있다.

Node.js 가 잘 동작하는 걸 확인할 수 있다.

 

백그라운드 실행

#node webserver.js &

 

프로세스 동작 여부 확인

#ps -ef | grep node

 

프로세스 죽이기

#kill -p PID번호

 

#####################################################

 

express 설치를 해서 Web 서버 만들기

 

 

 

express 모듈은 request 이벤트 리스너를 연결하는데 use() 메소드를 사용한다.

use() 메소드는 여러번 사용할 수 있다.

use() 메소드의 매개변수에는 function(request, response, next) { } 형태의 함수를 입력한다.

use() 메소드의 매개변수에 입력하는 함수를 미들웨어라고 부른다.

요청의 응답을 완료하기 전까지 요청 중간 중간에 여러가지 일을 처리할 수 있다.

// server.js 파일 (Terminal 창에서 아래 두줄 실행한다)
// npm install express --save
// npm install ejs --save
 
const express = require('express');
const app = express();
 
const server = app.listen(3000,() => {
   console.log('Start Server : 3000 port');
});
 
app.set('views', __dirname + '/views');
app.set('view engine''ejs');
app.engine('html', require('ejs').renderFile);
 
app.get('/'function (req, res) {
res.render('index.html');
});
 
app.get('/about'function (req, res) {
    res.send('About Page');
});
 
app.get('/intro'function (req, res) {
    res.render('intro.html');
});
 

 

index.html 파일 → https://www.w3schools.com/ 사이트의 Bootstrap 예제를 가져온 예시 파일이다.

 

<!DOCTYPE html>
<html lang="en">
<head>
    <title>Bootstrap Example</title>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
</head>
<body>
 
<div class="container">
    <h1>Welcome to My Homepage</h1>
    <h2>Basic Table</h2>
    <p>The .table class adds basic styling (light padding and horizontal dividers) to a table:</p>
    <table class="table">
        <thead>
        <tr>
            <th>Firstname</th>
            <th>Lastname</th>
            <th>Email</th>
        </tr>
        </thead>
        <tbody>
        <tr>
            <td>John</td>
            <td>Doe</td>
            <td>john@example.com</td>
        </tr>
        <tr>
            <td>Mary</td>
            <td>Moe</td>
            <td>mary@example.com</td>
        </tr>
        <tr>
            <td>July</td>
            <td>Dooley</td>
            <td>july@example.com</td>
        </tr>
        </tbody>
    </table>
</div>
 
</body>
</html>

 

 

 

블로그 이미지

Link2Me

,
728x90

채팅 기능을 테스트해보고자 구글링했더니 node.js 서버 환경 구축이 필요하다.

CentOS 6.6 기반에서 node.js 설치하는 방법을 적어둔다.


1. https://www.nodejs.org  사이트에 접속하여 download 사이트로 이동한다.



2. Linux 64-bit 에서 마우스 우클릭하여 링크 주소를 복사한다.

   리눅스에서 다운로드할 경로를 미리 정한다.

   cd /usr/local/ 에서 다운로드 하고 압축을 푼다.  

   wget 복사한 주소를 붙여넣기 한다. 여기서 주의할 점은 https://nodejs.org/dist/v6.11.2/node-v6.11.2-linux-x64.tar.xz 로 나온다.

  wget https://nodejs.org/dist/v6.11.2/node-v6.11.2-linux-x64.tar.gz 로 변경해서 엔터키를 치면 다운로드된다.

  wget https://nodejs.org/dist/v8.12.0/node-v8.12.0-linux-x64.tar.gz


  ※ 2020.1.29일 12.9.1 버전을 받아서 설치했더니 에러가 발생하여 설치 포기하고 8.12.0 버전으로 설치했다.


3. 압축을 푼다.

    tar xvzf node-v6.11.2-linux-x64.tar.gz

    tar xvzf node-v8.12.0-linux-x64.tar.gz


4. mv node-v8.12.0-linux-x64 nodejs 로 디렉토리명을 변경해준다. 경로명을 기억하기 쉽게 하기 위해서....

   cd nodejs 하고 ll 을 하면 bin 폴더 등 이미 인스톨된 상태가 되어 있더라.


5. 실행파일 PATH 지정하기

    #vi /etc/profile

export NODE_HOME=/usr/local/nodejs
export PATH=$PATH:$NODE_HOME/bin

위 그림과 같이 입력하고 :wq 로 저장한다.


6. 작성한 PATH 가 적용되도록 아래 구문을 적어주고 엔터키를 친다.

# source /etc/profile


이제 nodejs 가 제대로 되었는지 확인해본다.


정상적으로 설치되었다면 위 그림처럼 입력한 결과를 출력해줄 것이다.

빠져나오는 것은 .exit 를 입력한다.


node.js 서버 환경 구축은 준비되었다.


7. 방화벽 포트 추가

통신을 하기 위해서는 통신할 포트를 정해야 한다.

포트(port)는 8080 으로 설정하겠다.

#cat /etc/sysconfig/iptables
로 방화벽 설정 상태를 확인한다.



#vi /etc/sysconfig/iptables 편집으로 한줄 추가한 다음 저장하고 빠져나온다.

-A INPUT -p tcp -m state --state NEW -m tcp --dport 8080 -j ACCEPT


방화벽 재시작
#/etc/init.d/iptables restart

변경한 정보 저장(재시작을 하고 나서 저장해야 적용된 정보가 저장됨)
#service iptables save


설치위치 확인
#which node
로 하면 설치 위치를 확인할 수 있다.

#node -v

#npm -v

방화벽 포트 설정까지 마쳤다.

이제 node.js 웹서버 테스트 파일로 통신이 되는지 확인해보자.

블로그 이미지

Link2Me

,
728x90

안드로이드 서비스 기능을 추가한 어플이 이상종료되는 증상이 발생한다.

코드 구현에서 고려하지 못한 사항이 있어서일까?

그래서 이번에 Service 에 대한 여러 자료를 참조하여 정리를 해둔다.


안드로이드 Application을 구성하는 4대 컴포넌트
1. Activity
2. Service : 백그라운드에서 동작하는 컴포넌트
3. Broadcast Receiver
4. Contents Provider


안드로이드에서 UI가 없어도 백그라운드에서 실행되는 동작이 필요할 때가 있다.
- 웹사이트 데이터 읽어오기 : AsyncTask 사용
- 음악재생, 전화수신 팝업 : Service 사용

Service는 Activity가 종료되어 있는 상태에서도 동작하기 위해서 만들어진 컴포넌트다.
앱이 종료되어도 서비스는 백그라운드에서 계속 실행된다.
만약 Service가 실행되고 있는 상태라면, 안드로이드 OS 에서는 해당 Process를 왠만하면 죽이지 않도록 방지하고 관리한다.
그렇기 때문에 메모리 부족이나, 특별한 경우를 제외하고는 Background 동작을 수행하도록 설계 되었다.
모든 컴포넌트(Activity, Service, Broadcast Receiver, Contents Provider)는 Main Thread(UI 작업을 처리해주는 Thread) 안에서 실행된다.



서비스 수명 주기.
왼쪽의 다이어그램은 서비스가 startService()로 생성된 경우의 수명 주기를 나타내며
오른쪽의 다이어그램은 서비스가 bindService()로 생성된 경우의 수명 주기를 나타낸다.

자세한 내용은 https://developer.android.com/guide/components/services.html 를 읽어보자.


- (액티비티와 같은) 컴포넌트가 startService()를 호출하면, 서비스는 "started" 상태가 된다.
서비스가 실행되면(started), 그 서비스를 실행한 컴포넌트가 종료되더라도 (할 일을 모두 마칠 때까지) 서비스는 종료되지 않는다.

- 일반적으로 서비스는 onCreate() → onStartCommand() → Service Running → onDestroy() 순으로 실행된다.

- onCreate() : 서비스에서 가장 먼저 최초 1번만 호출된다.

- Service 실행도중에 startService() 메서드를 실행하게 되면 Service의 onStartCommand() 메서드를 호출하게 한다.

- Service의 종료메서드인 stopService() 메서드를 호출하면 종료된다.

  stopService() 메소드를 호출하지 않으면 프로세스가 종료되더라도 다시 살아난다.

  서비스가 할 작업을 모두 마쳤을 때 stopSelf()나 stopService()를 호출하는 부분도 구현해야 한다.

- 프로세스에 의해 종료된 Service는 onCreate() -> onStartCommand() 순으로 실행된다.

- onStartCommand() 메서드의 3가지 return type

 START_STICKY

 Service가 강제 종료되었을 경우 시스템이 다시 Service를 재시작시켜 주지만 intent 값을 null로 초기화 시켜서 재시작

 START_NOT_STICKY

 시스템에 의해 강제로 종료된 Service가 재시작되지 않는다

 START_REDELIVER_INTENT

 Service가 강제 종료되었을 경우 시스템이 다시 Service를 재시작시켜 주며, intent 값을 그대로 유지시켜 준다.

- onDestroy() : 서비스가 종료될 때 실행되는 메소드



Service 구현
1. Service (*.java)를 만든다
2. AndroidManifest.xml 에 Service를 등록한다
3. Service 를 실행/중단하는 Intent 코드를 작성한다.


예제1.

- startService를 호출하면 서비스의 onStartCommand() 메소드가 동작된다.

- stopService 를 호출하면 서비스의 onDestroy() 메소드가 동작된다. 즉, 서비스를 종료시킨다.

<?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="Service Start"
        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="Service Stop"
        android:layout_gravity="center"
        android:gravity="center"
        android:textSize="14sp"
        android:textAllCaps="false"
        android:layout_marginTop="20dp"/>
</LinearLayout>


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

import androidx.appcompat.app.AppCompatActivity;

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

    private int mCount = 0;
    private Thread mThread;

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

        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();
                Intent intent = new Intent(mContext,MyService.class);
                stopService(intent);
            }
        });
    }

    @Override
    protected void onDestroy() {
        Log.e(TAG,"MainActivity onDestroy");
        stopService(new Intent(this,MyService.class)); // 현재 Activity가 종료되면 서비스 중지시킴
        super.onDestroy();
    }
}


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

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

    public MyService() {
    }

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

    @Override
    public int onStartCommand(Intent intent, int flags, int 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;
        }

    }
}

실행결과

Stop 버튼 클릭시


Activity 종료시



Bound Service

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

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

- Activity가 서비스에 어떤 요청을 하고, 서비스는 결과를 반환할 수 있다.

  앱 컴포넌트가 bindService()를 호출하면, 서비스는 "bound" 상태(바인드 된 상태)가 된다.

  바인드 된 서비스는, 앱 컴포넌트들이 서비스와 상호작용(요청 및 응답, 프로세스간 통신(IPC)) 할 수 있도록 해주는 클라이언트-서버 인터페이스를 제공해 준다.

- 하나의 서비스에 다수의 Activity(컴포넌트)가 바인드 될 수 있다.

- 서비스 바인딩은 연결된 Activity가 사라지면 서비스는 종료된다.


바인드 서비스 예제는 https://link2me.tistory.com/1763 에 있다.

블로그 이미지

Link2Me

,
728x90

Some file crunching failed, see logs for details afer update gradle

안드로이드 스튜디오에서 Activity 를 추가하는 도중에 이런 메시지가 나온다.


gradle.properties 파일에서 이렇게 수정하라고 Stackoverflow 에 나오길래 했는데도 안된다.
android.buildCacheDir=D:/android-studio/build-cache


app build.gradle file 에 android 부분에 아래 코드를 추가하니까 해결된다.

android {
    aaptOptions { 
        cruncherEnabled = false 
    }
}


블로그 이미지

Link2Me

,
728x90

AsyncTask 를 Activity 내 Inner Class 로 만든 경우에 메모리 누수를 방지하기 위하여 static 으로 처리하고 WeakReference 를 사용하는 예제를 찾아봤다.


https://gist.github.com/rorist/459787 에 나온 방법과 https://medium.com/google-developer-experts/finally-understanding-how-references-work-in-android-and-java-26a0d9c92f83 는 비슷한 방법이 나온다.


내가 참조하여 사용한 방법은 https://stackoverflow.com/questions/19551484/android-do-i-have-to-have-weakreference-for-inner-class-asynctask 에 나온 걸 활용했다.


잘못 사용하고 있는지 여부는 좀 더 테스트를 해보련다.


public static class saveContactsFromDbData extends AsyncTask<String, Void, String> {
    private WeakReference<SaveContactsActivity> weakActivity;
    SaveContactsActivity activity;

    long start; // 서버에서 가져오는 시간 측정
    @SuppressLint("SimpleDateFormat")
    SimpleDateFormat FORMATTER = new SimpleDateFormat("mm:ss.SSS");

    int newcnt = 0;
    int upcnt = 0;
    int no_cnt = 0;

    @Override
    protected void onPreExecute() {
        super.onPreExecute();           
        weakActivity = new WeakReference<SaveContactsActivity>(activity);
    }
   
    @Override
    protected String doInBackground(String... params) {

        start = System.currentTimeMillis();

        JSONArray peoples = null;
        try {
            JSONObject jsonObj = new JSONObject(params[0]);
            peoples = jsonObj.getJSONArray(TAG_RESULTS);

            return null;

        } catch (Exception e) {
            return new String("Exception: " + e.getMessage());
        }

    }

    protected void onPostExecute(String result) {
        super.onPostExecute(result);
        if (weakActivity.get() != null) {
            // 메모리 비우기
            contactMap.clear();
        }

    }
}


블로그 이미지

Link2Me

,
728x90

안드로이드는 모바일 디바이스에서 동작하기 때문에 제한된 메모리 용량을 지녀, 누수(leak)가 많이 발생하면 사용 가능한 메모리가 부족하게 된다.

앱에 메모리 누수가 있는 경우, 객체는 메모리에서 반환될 수 없다.
결과적으로 안드로이드 시스템은 더 많은 메모리를 요청한다. 그러나 한계가 있다.
결국 시스템은 앱에 더 많은 메모리를 할당하는 것을 거부한다. 이렇게 되면 앱은 메모리 부족으로 인해 강제 종료된다.


strong/weak reference
자바의 garbage collector는 더 이상 참조되지 않는 객체를 자동으로 수거하여 프로그래머가 직접 메모리를 관리하지 않아도 되도록 해준다. 하지만 모든 경우에 대해 garbage collector가 깨끗하게 사용하지 않는 객체들을 정리해주는 것은 아니다. 때로는 참조가 계속 남아있는 경우가 있을 수 있고 이런 경우에 객체는 수거되지 않고 memory leak으로 이어질 수 있다. 이와 같이 참조를 부주의하게 사용하여 발생할 수 있는 memory leak현상을 막기 위해서는 객체에 대한 참조를 유연하게 다루어 필요하지 않은 객체들이 수거될 수 있도록 해야한다.


GC(Garbage collector)는 참조 카운트가 0인 인스턴스들 즉, unReachable에 대한 객체에 대해서만 메모리를 회수한다.
만약 Activity 인스턴스가 Heap 루트로부터 어떤 strong reference에 묶여있는 경우라면, GC(Garbage collector)는 이를 메모리에서 제거할 수 없고, 해당 인스턴스는 leaked Activity 오브젝트가 된다.


메모리 누수가 발생하는 상황

- 프로세스-앱의 상태와 관계없이 글로벌 스태틱 오브젝트가 존재하고 액티비티의 참조들의 체인을 유지할 때

- 쓰레드-액티비티 생명주기를 무시하고 strong reference가 남아있을 때

- AsyncTask 를 선언하고 인스턴스화하는 것을 액티비티 내 익명 클래스에서 진행할 경우,

  Activity가 종료되어도(destroy) 백그라운드에서 계속 존재하게 되며 GC가 수거하지 못한다.

- AsyncTask를 생성했던 Activity가 먼저 destroy되는 경우(다른 UI로 전환) 메모리 릭이 발생할 수 있다.

  이미 자신을 실행시켰던 Activity가 존재하지 않는데 AsyncTask가 UI 처리같은 걸을 요구하면

  메모리상에 존재하지 않는 것을 참조하게 되어 메모리릭이 발생한다.


메모리 누수를 피하는 법

출처 : https://android-developers.googleblog.com/2009/01/avoiding-memory-leaks.html

- GC(garbage collector)가 메모리 누수에 대한 보험은 아니다

- 라이프 사이클을 제어할 수 없다면, Activity 내에서 non-static inner classes 를 피하라.

- AsyncTask는 doInBackground 메소드만 백그라운드 스레드에서 실행되며, 나머지는 메인 스레드에서 실행된다.
백그라운드 작업시간이 긴 경우에는 UI 에서 실행중임을 알 수 있도록 ProgressBar 를 보여줘서
Activity 가 전환되지 않도록 하라.

- context-activity 대신 context-application 을 사용하라.

  Activity Context의 Reference 생명 주기는 반드시 Activity 생명 주기와 동일해야 한다.

  뷰를 수정하거나 할때는 Activity Context를 나머지 경우에는 Application Context를 사용하라.

  Context 에는 2가지 종류가 있다. Application context, Activity context ==> http://dev.youngkyu.kr/36 참조


static 변수 사용을 지양하라.

- static은 객체 지향적이지 않다.
- 지나치게 많은 static 변수를 사용하게 되면 이들로부터 메모리 회수를 할 수 없어서 가상머신이 메모리 부족을 겪을 수 있다.
- static 변수는 프로그램이 실행되고 있는 내내 살아있게 된다.
- 하나의 인스턴스로 생성하게 되면, 호출을 시키게 되면 그 함수 호출이 끝난 후 인스턴스는 소멸된다.
  훨씬 메모리를 절약하게 된다.
- static 메서드는 interface를 구현하는데 사용될 수 없다.
- 여러개의 인스턴스를 만드는 것을 피하고 싶다면 싱글톤 디자인 패턴을 이용하는 것이 훌륭한 대안이 될 수 있다.


bitmap 메모리 누수 관련 글

http://www.androidpub.com/1282821



Context 메모리 누수 피하는 방법 관련 글

http://blog.naver.com/huewu/110082062273



참고 사이트

http://www.kmshack.kr/2017/03/%EC%95%88%EB%93%9C%EB%A1%9C%EC%9D%B4%EB%93%9C%EC%9D%98-%EB%A9%94%EB%AA%A8%EB%A6%AC-%EB%88%84%EC%88%98-%ED%8C%A8%ED%84%B4/


안드로이드 앱이 메모리 누수(Leak)를 만드는 8가지 방법

http://sjava.net/2016/05/%EB%B2%88%EC%97%AD-%EC%95%88%EB%93%9C%EB%A1%9C%EC%9D%B4%EB%93%9C-%EC%95%B1%EC%9D%B4-%EB%A9%94%EB%AA%A8%EB%A6%AC-%EB%88%84%EC%88%98leak%EB%A5%BC-%EB%A7%8C%EB%93%9C%EB%8A%94-8%EA%B0%80%EC%A7%80/



블로그 이미지

Link2Me

,