728x90

last Update : 2019.8.30


Firebase 클라우드 메시징(FCM)은 메시지를 무료로 안정적으로 전송할 수 있는 교차 플랫폼 메시징 솔루션이다.

구글이 PUSH 메시징 플랫폼을 GCM(Google Cloud Message) 에서 FCM(Firebase Cloud Message) 로 변경을 권고하면서 구글에서 GCM 메시지 전송을 위한 설정 세팅 정보 연결을 찾기가 어렵다. (2017.4.11 기준)

안드로이드 9.0 부터는 GCM 은 동작하지 않는다.


FCM(Firebase Cloud Message) 를 활용하여 PUSH 알림 메시지를 구현하고자 한다.

- 공지 전송 전용 게시판에 글을 등록하여 지정된 회원에게 PUSH 메시지 전송

- 서버 게시판에서 특정 글을 등록하면 지정된 회원에게 PUSH 메시지 자동 전송



https://firebase.google.com/docs/cloud-messaging/ 에 동영상으로 설명이 나오고, 주요기능 및 작동원리, 구현경로에 대한 설명이 잘되어 있다.


가장 먼저 해야 할 일은 구글 계정에서 FCM 등록을 하는 법이다.


1. https://console.firebase.google.com/ 에 접속한다.

   프로젝트를 추가한다.  




안드로이드 앱의 패키지명을 입력한다.


위 그림에서 잘 보면 파일의 위치가 해당 앱 폴더 아래에 위치한다.

해당 앱 모듈의 build.gradle 이 있는 폴더에 google-services.json 파일을 넣어주어야 한다.



모듈 앱에 빠진 부분을 추가한다.

https://firebase.google.com/docs/android/setup 에 가면 가장 최신 Firebase 라이브러리 파일 정보가 나온다.


프로젝트 수준 build.gradle

// Top-level build file where you can add configuration options common to all sub-projects/modules.

buildscript {
   
    repositories {
        google()
        jcenter()
    }
    dependencies {
        classpath 'com.android.tools.build:gradle:3.5.0'
        classpath 'com.google.gms:google-services:4.2.0'       
    }
}

allprojects {
    repositories {
        google()
        jcenter()
    }
}

task clean(type: Delete) {
    delete rootProject.buildDir
}


앱 build.gradle

apply plugin: 'com.android.application'

android {
    compileSdkVersion 28
    buildToolsVersion = '28.0.3'

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

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

}

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

    implementation 'com.android.support:appcompat-v7:28.0.0'
    implementation 'com.android.support.constraint:constraint-layout:1.1.3'
    implementation 'gun0912.ted:tedpermission:2.0.0'
    implementation 'com.android.volley:volley:1.1.1'
    implementation 'com.github.bumptech.glide:glide:3.8.0' // 이미지 라이브러리
    implementation 'com.google.firebase:firebase-core:16.0.8'
    implementation 'com.google.firebase:firebase-messaging:19.0.1'

}

apply plugin: 'com.google.gms.google-services'  // Google Play services Gradle plugin


gradle.properties

android.useAndroidX=true
android.enableJetifier=true
org.gradle.jvmargs=-Xmx1536m


These two lines automatically resolved my dependency conflicts between google's files and third party dependencies.

두 줄을 추가하면 구글 파일과 서드파티 dependencies 사이의 충돌이 나서 빨간 줄 표시나는 걸 해결해준다.


이제 안드로이드 앱에서 구현할 소스와 PHP(Web) 소스를 구현하면 된다.

728x90
블로그 이미지

Link2Me

,
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


728x90
블로그 이미지

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

728x90
블로그 이미지

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/



728x90
블로그 이미지

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 로 변경한 것이 제대로 동작이 안될 수도 있다.


728x90
블로그 이미지

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

728x90
블로그 이미지

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








728x90
블로그 이미지

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)를 이용하여 정보를 주고 받는다.

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


728x90
블로그 이미지

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 (추천)



728x90
블로그 이미지

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 에 있다.

728x90
블로그 이미지

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


728x90
블로그 이미지

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

    }
}


728x90
블로그 이미지

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/



728x90
블로그 이미지

Link2Me

,
728x90

안드로이드에서 설정(Setting) 정보를 변경하는 Activity 를 만들어 두어야 할 때 필요할 거 같아서 적어둔다.


준비사항

토글버튼 이미지 2개


toggle_drawable.zip


토글버튼 이미지 2개를 drawable 폴더에 복사하고, toggle_selector.xml 파일을 만든다.

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

    <item android:drawable="@drawable/toggle_on" android:state_checked="true"/>
    <item android:drawable="@drawable/toggle_off" android:state_checked="false"/>

</selector>


activity_setting.xml 은 Layout을 정한다.

여러개의 환경설정 정보가 필요하다면 TableLayout 을 선택하는 것이 좋을 거 같다.

 <RelativeLayout 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:paddingBottom="@dimen/activity_vertical_margin"
    android:paddingLeft="@dimen/activity_horizontal_margin"
    android:paddingRight="@dimen/activity_horizontal_margin"
    android:paddingTop="@dimen/activity_vertical_margin"
    tools:context=".MainActivity" >

    <ToggleButton
        android:id="@+id/btn_toggle1"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_centerHorizontal="true"
        android:layout_centerVertical="true"
        android:background="@drawable/toggle_selector"
        android:checked="true"
        android:text=""
        android:textOff=""
        android:textOn="" />

    <TextView
        android:id="@+id/textView1"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_below="@id/btn_toggle1"
        android:layout_centerHorizontal="true"
        android:layout_marginTop="20dp"
        android:text=""
        android:textAppearance="?android:attr/textAppearanceMedium" />

</RelativeLayout>


SettingActivity.java

토글버튼을 선택함에 따라 SharedPreferences 에 값이 변경되도록 하여 관리정보를 알 수 있도록 한다.

import android.app.Activity;
import android.content.SharedPreferences;
import android.os.Bundle;
import android.view.Menu;
import android.widget.CompoundButton;
import android.widget.CompoundButton.OnCheckedChangeListener;
import android.widget.TextView;
import android.widget.ToggleButton;

public class SettingActivity extends Activity {

    SharedPreferences pref;
    ToggleButton btn_choice;
    TextView text;

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

        btn_choice = (ToggleButton) findViewById(R.id.btn_toggle1);
        text = (TextView) findViewById(R.id.textView1);
        
        pref = getSharedPreferences("pref", Activity.MODE_PRIVATE);
        if(pref.getString("choice", "").equals("1")){
            text.setText("ON");
        } else {
            text.setText("OFF");
        }

        btn_choice.setOnCheckedChangeListener(new OnCheckedChangeListener() {

            @Override
            public void onCheckedChanged(CompoundButton arg0, boolean isChecked) {
                if(isChecked == true){
                    text.setText("ON");
                    SharedPreferences.Editor editor = pref.edit();
                    editor.putString("choice", "1");
                    editor.commit();
                } else {
                    text.setText("OFF");
                    System.out.println("토글버튼 해제");
                    SharedPreferences.Editor editor = pref.edit();
                    editor.putString("choice", "0");
                    editor.commit();
                }
            }
        });

    }

}



728x90
블로그 이미지

Link2Me

,
728x90

Eclipse 툴에서 패키지명을 변경할 때 해당되는 걸 적어둔다.

번호 1, 2 위에서 마우스 우클릭을 하고 Refactor -> Rename 을 선택하여 변경한다.

번호 3 에서는 패키지명을 직접 변경한다.

번호 4에서는 App 명을 변경하는 걸로 변경한다.




종료하고 다시 읽어들이면 Project Name 이 변경되어 있는 걸 확인할 수 있다.




728x90
블로그 이미지

Link2Me

,
728x90

삼성폰에서 Unrecognized profile 2130706433 for video/avc 메시지가 출력된다.

구글링을 해보니까 해결책은 아직 없는것 처럼 나오고 이런 증상을 보이는 폰의 종류들이 나온다.


Samsung Galaxy S6 and Samsung Galaxy S6 Edge+.


Same issue with note 5 upgraded to Android 6.0.1., i have another note 5 with Android 5.1 and is working fine.


Samsung S7 running Android 6.0.1. Screen is black or sometimes flickers dark green on VrVideoView player.


해결책인지는 모르겠는데 적어둔다.

https://developers.google.com/vr/android/reference/com/google/vr/sdk/base/Constants



728x90
블로그 이미지

Link2Me

,
728x90

최초 작성일 : 2017.7.18


@Override
public boolean onKeyDown(int keyCode, KeyEvent event){
  if ( event.getAction() == KeyEvent.ACTION_DOWN ){
      if ( keyCode == KeyEvent.KEYCODE_BACK ){
          AlertDialog.Builder alert_confirm = new AlertDialog.Builder(Main.this);
          alert_confirm.setMessage("프로그램을 종료 하시겠습니까?")
          .setCancelable(false)
          .setPositiveButton("종료",
          new DialogInterface.OnClickListener() {
              @Override
              public void onClick(DialogInterface dialog, int which) {
                  // 'YES'
                  finish();
              }
          }).setNeutralButton("취소",
          new DialogInterface.OnClickListener() {
              @Override
              public void onClick(DialogInterface dialog, int which) {                 
                  //finish();
              }
          }).setNegativeButton("백그라운드",
          new DialogInterface.OnClickListener() {
              @Override
              public void onClick(DialogInterface dialog, int which) {
                  // 'No'
                  System.out.println("백그라운드 상태로 동작합니다");
                  Intent intent = new Intent();
                  intent.setAction(Intent.ACTION_MAIN);
                  intent.addCategory(Intent.CATEGORY_HOME);
                  startActivity(intent);
              return;
              }
          });
          AlertDialog alert = alert_confirm.create();
          alert.show();
      }
      if ( keyCode == KeyEvent.KEYCODE_HOME ){
         
      }
  }
  return super.onKeyDown(keyCode, event);
}


인터넷에서 찾아서 사용해본 종료에 대한 메소드다.

과연 제대로 종료가 되는지 다른 어플에서 테스트를 해보니 어플이 완전 종료되지 않았다.

finish(); : 현재 Activity 종료



Intent intent = new Intent();
intent.setAction(Intent.ACTION_MAIN);
intent.addCategory(Intent.CATEGORY_HOME);
startActivity(intent);


어플 종료라기 보다는 홈화면이 나오도록 하는 코드다.

어플이 종료되어 있는지 확인하면 종료되지 않고 남아있더라.


폭풍검색을 해서 찾아보니 어플 완전종료는 정말 쉽지 않더라.


'기본적'으로 안드로이드에는 '어플리케이션 종료' 라는 개념 자체가 없다.
한 번 실행된 프로세스는 메모리가 허락하는 한 영원히 죽지 않는다. 


http://m.blog.daum.net/creazier/15309783 에 관련 내용이 비교적 잘 정리되어 있다.

좀 오래된 내용이라 현재 기준 안드로이드에서 잘 동작하는지 여부는 아직 테스트를 못해봤다.


activity .finish(); // 현재 Activity 만 종료 처리
android.os.Process.killProcess(android.os.Process.myPid());

하면 종료될 것이라고 해서 테스트해보면 죽지 않고 좀비처럼 다시 살아난다. ㅠㅠ

Activity 가 1개만 떠있는 경우에만 정상적으로 죽는다고 하던데, 다른 Activity 를 실행하지 않은 상태에서도 테스트해보면 죽지 않고 살아 있더라.


어플 완전 종료가 되는 걸 찾게되면 수정해서 작성하련다. (2017.8.23)


드디어 완전하게 처리되는 걸 찾아냈다. (2017.8.31)

정말 우연히 Audio Player 종료처리하는 걸 테스트하다가 완전 종료되는 걸 확인했다.


if (Build.VERSION.SDK_INT >= 21)
    finishAndRemoveTask();
else
    finish();
System.exit(0);


안드로이드 스튜디오에서 컴파일하는 API 가 21보다 큰 경우인데 계속 예전 버전 종료인 finish(); 로만 처리를 하고 있었던 거다.

위 코드를 넣고 처리하니까 화면상에 남아있지 않았다.


finishAffinity()
Finish this activity as well as all activities immediately below it in the current task that have the same affinity.

finishAndRemoveTask()
Call this when your activity is done and should be closed and the task should be completely removed as a part of finishing the root activity of the task.

728x90
블로그 이미지

Link2Me

,
728x90
SQLiteDB에 있는 자료를 읽어서 HashMap 메모리 상에 올리는 예제이다.

HashMap<String, SQLite_Item> sqliteDBMap = new HashMap<String, SQLite_Item>();
public void SQLiteDB2ArrayList(){
    sqliteDBMap.clear(); // 메모리 초기화
    sqLiteDBHandler = new SQLiteDBHandler(Main.this);
    
    SQLiteDatabase db = sqLiteDBHandler.getReadableDatabase();
    db.beginTransaction();
    
    Cursor cursor = sqLiteDBHandler.LoadSQLiteDBCursor();
    try {
        cursor.moveToFirst();
        System.out.println("SQLiteDB 개수 = " + cursor.getCount());
        while (!cursor.isAfterLast()) {
            SQLite_Item item = new SQLite_Item();
            item.setIdx(cursor.getString(0));
            item.setUserNM(cursor.getString(1));
            item.setMobileNO(cursor.getString(2));
            item.setTelNO(cursor.getString(3));
            item.setTeam(cursor.getString(4));
            item.setPosition(cursor.getString(5));
            item.setCheckState(cursor.getInt(6));
            // HashMap 에 추가
            sqliteDBMap.put(cursor.getString(0), item);
            cursor.moveToNext();
        }
        db.setTransactionSuccessful();
    } catch (Exception e) {
        e.printStackTrace();
    } finally {
        cursor.close();
        db.endTransaction();
    }        
}


sqLiteDBHandler = new SQLiteDBHandler(context); // BroadcastReceiver 에서 사용할 때


728x90
블로그 이미지

Link2Me

,
728x90

안드로이드폰에서 전화수신시 앱에서 수신전화의 상태를 감지하여 전화수신 팝업을 띄우기 위한 기본 코드라고 보면 된다.

전화벨이 울리면 메시지가 2번씩 수신된다. 전화에 대한 정보를 2번씩 보내주나 보다.

그래서 팝업창 구현 등을 하려면 1번만 메시지가 수신되어야 하므로 1번만 띄우도록 처리하는 로직이 포함되었다.


--- AndroidManifest.xml 파일 추가할 사항 ---

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

<receiver
    android:name=".CallStateReceiver">
    <intent-filter>
        <action android:name="android.intent.action.PHONE_STATE"/>
    </intent-filter>
</receiver>

 --- CallStateReceiver.java ---

import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.telephony.PhoneNumberUtils;
import android.telephony.PhoneStateListener;
import android.telephony.TelephonyManager;
import android.util.Log;

public class CallStateReceiver extends BroadcastReceiver {
    static String mLastState;
    static final String TAG = "CallStateListner";
    
    @Override
    public void onReceive(Context context, Intent intent) {
        Log.d(TAG, "CallStateReceiver >>>>>>> 전화 수신");
        
        // 전화 수신 체크
        CallReceivedChk(context, intent);
    }
    
    private void CallReceivedChk(Context context, Intent intent) {
        TelephonyManager telephony = (TelephonyManager)context.getSystemService(Context.TELEPHONY_SERVICE);
        telephony.listen(new PhoneStateListener(){
            @Override
            public void onCallStateChanged(int state, String incomingNumber) {
                String mState = String.valueOf(state);
                if (mState.equals(mLastState)) { // 두번 호출되는 문제 해결 목적
                    return;
                } else {
                    mLastState = mState;
                }
                
                switch(state) {
                    case TelephonyManager.CALL_STATE_IDLE:
                        Log.d(TAG,"전화 수신 상태가 아닙니다 : CALL_IDLE");
                        break;
                    case TelephonyManager.CALL_STATE_OFFHOOK:
                        Log.d(TAG, "전화를 받았습니다 : CALL_OFFHOOK");
                        break;
                    case TelephonyManager.CALL_STATE_RINGING:
                        Log.d(TAG, "CALL_RINGING, 수신 전화번호 : " + PhoneNumberUtils.formatNumber(incomingNumber));
                        // 처리하고자 하는 코드 추가하면 된다.
                        break;            
                }
            }    
        }, PhoneStateListener.LISTEN_CALL_STATE);
    }

}


위 코드를 가지고 활용을 해서 팝업창을 띄우는 코드를 새롭게 만들거나

http://gun0912.tistory.com/46 에 있는 코드를 활용하면 팝업창을 띄울 수 있다.

위 코드를 그대로 사용하면 앱이 죽는 증상이 있다.
코드가 100% 완벽하게 동작되도록 하려면 개발자 본인이 수정 보완해야 한다.

코드에 대한 완벽한 분석과 기초지식이 있어야 가능하더라.


스마트폰 스크린이 잠겨있는 OFF 상태에서 전화가 수신되면 수신 팝업창을 ON 상태로 바꿔 사용자에게 메시지를 알려주려면 어떻게 해야 할까?


안드로이드 시스템에서는 장비가 휴면모드로 들어가게 되면 배터리 소모를 최소화하기 위하여 불필요한 CPU나 와이파이를 포함한 모든 기능들은 정지시키려고 한다.
Service 가 지속적으로 실행이 되고 있는 것을 확실하게 보장하기 위해서 "WAKE Locks"를 사용해야 한다. Wake Lock는 앱에서 특정 기능들을 계속 활성화 시켜놓아야 한다는 것을 시스템에 알려주는 역할을 하는 것이다.


private static PowerManager.WakeLock wakeLock;
private void WakeLock(Context context, Intent intent) {
    // 잠든 화면 깨우기
    if (wakeLock != null) {
        return;
    }

    PowerManager pm = (PowerManager) context.getSystemService(Context.POWER_SERVICE);
    wakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK | PowerManager.ACQUIRE_CAUSES_WAKEUP
            | PowerManager.ON_AFTER_RELEASE, "hi");
    wakeLock.acquire();  
}


이 메소드를 BroadcastReceiver 에 추가한다.


START_STICKY : 재생성과 on StartCommand() 호출(with null intent)
START_NOT_STICKY : 서비스 재 실행하지 않음
START_REDELIVER_INTENT : 재생성과 on StartCommand() 호출(with same intent)
 


읽어볼만한 자료

배터리를 잡아먹는 주원인 WakeLock : http://widzard.tistory.com/36

728x90
블로그 이미지

Link2Me

,
728x90

Android Audio Player 관련 기능을 테스트 해보고 있는 중이다.

어떤 방식으로 하느냐에 따라 구현할 내용이 달라질 거 같은데 방향 잡기가 쉽지 않다.


먼저 AndroidManifest.xml file 에 권한을 추가한다.

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


이미 안드로이드에서는 음악, 주소록같은 기본적인 어플리케이션이 존재한다.
이런 기본적인 앱안에는 정보들을 담은 DB가 구현되어 있다.
이런 앱들은 자신의 앱의 데이터베이스에 접근할 수 있도록 도와주는 컨텐트 프로바이더(Content Provider)란 것을 제공한다.
앱이 ContentProvider를 접근할 때에는 ContentResolver를 이용한다.
ContentResolver는 기본적으로 CRUD(Create Retreive Update Delete) 함수들을 제공하며 이를 통해 다른 어플리케이션에 있는 데이터베이스를 조작할 수 있다.


휴대폰에 있는 오디오 파일을 읽어오는 함수는 아래와 같다.

private void loadAudio() {
    ContentResolver contentResolver = getContentResolver();

    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));
            long mDuration = cursor.getLong(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();
}



listViewAdapter.addItem 을 위한 정의서인 Song_Item 클래스를 만든다.

담을 내용을 정하는 것은 개발자 몫이다.

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

    public Song_Item() {
    }

    public Song_Item(long mId, long albumId, String title, String artist, String album, long 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 long getDuration() {
        return Duration;
    }

    public void setDuration(long 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;
    }
}


MainActivity.java 에서 구현할 내용 일부를 발췌하여 적어둔다.

ListViewAdapter 구현 내용이라고 보면 된다.

ListView 상에 가져오는 코드를 작성했는데 Player.java 코드를 아직 제대로 구현하지 못한 상태다.

구글링해서 얻은 코드들을 발췌하고 내가 원하는 기능으로 구현해보는 중이다.

구현하려는 방법이 달라서 기존 코드를 그대로 사용하지는 않았다.

일부 코드는 그대로 활용해도 너무 좋은 코드가 있어서 그냥 사용한 부분도 있다.


ListViewAdapter 에 아무것도 담지 않은 방법으로 먼저 생성한 다음에 loadAudio() 을 읽어들이는 방식은 장점이 있다. loadAudio() 을 전체 오디오 파일을 가져오게 할 수도 있지만 원하는 것만 검색해서 가져오게 하는 방법도 가능하다. 즉 검색기능을 활용하여 데이터를 가져오는 방법이 가능하여 편리하여 이런 방법을 사용하고 있다.

이 부분은 개발자마다 코드 구현 방식이 다른 것을 구글링을 해보면 알 수 있다.


 public class MainActivity extends AppCompatActivity {
    private ListView audiolistView; // 리스트뷰
    private ArrayList<Song_Item> songsList = null; // 데이터 리스트
    private ListViewAdapter listViewAdapter = null; // 리스트뷰에 사용되는 ListViewAdapter

    Context context;

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

        context = this.getBaseContext();

        // Adapter에 추가 데이터를 저장하기 위한 ArrayList
        songsList = new ArrayList<Song_Item>(); // ArrayList 생성
        audiolistView = (ListView) findViewById(R.id.my_listView);
        listViewAdapter = new ListViewAdapter(this); // Adapter 생성
        audiolistView.setAdapter(listViewAdapter); // 어댑터를 리스트뷰에 세팅
        audiolistView.setChoiceMode(ListView.CHOICE_MODE_SINGLE);
        audiolistView.setOnItemClickListener(songslistener);

        // 체크박스 보임/안보임 관련 코드는 생략했음...

        loadAudio();

    }

    class ViewHolder {
        public ImageView mImgAlbumArt;
        public TextView mTitle;
        public TextView mSubTitle;
        public TextView mDuration;
        public CheckBox checkbox;
    }

    private class ListViewAdapter extends BaseAdapter {
        Context context;

        public ListViewAdapter(Context context) {
            this.context = context;
        }

        // 음악 데이터 추가를 위한 메소드
        public void addItem(long mId, long AlbumId, String Title, String Artist, String Album,long 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);
        }

        // CheckBox를 모두 선택하는 메서드
        public void setAllChecked(final boolean ischeked) {
            final int tempSize = songsList.size();
            if(isAll == true){
                for (int i = 0; i < tempSize; i++) {
                    songsList.get(i).setCheckBoxState(true);
                }
            } else {
                for (int i = 0; i < tempSize; i++) {
                    songsList.get(i).setCheckBoxState(false);
                }
            }
        }

        @Override
        public int getCount() {
            return songsList.size(); // 데이터 개수 리턴
        }

        @Override
        public Object getItem(int position) {
            return songsList.get(position);
        }

        // 지정한 위치(position)에 있는 데이터 리턴
        @Override
        public long getItemId(int position) {
            return position;
        }

        // position에 위치한 데이터를 화면에 출력하는데 사용될 View를 리턴
        @Override
        public View getView(final int position, View convertView, ViewGroup parent) {
            final ViewHolder viewHolder;
            final Context context = parent.getContext();
            final Integer index = Integer.valueOf(position);

            // 화면에 표시될 View
            if(convertView == null){
                viewHolder = new ViewHolder();

                LayoutInflater inflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
                convertView = inflater.inflate(R.layout.song_item,parent,false);

                convertView.setBackgroundColor(0x00FFFFFF);
                convertView.invalidate();

                // 화면에 표시될 View 로부터 위젯에 대한 참조 획득
                viewHolder.mImgAlbumArt = (ImageView) convertView.findViewById(R.id.album_Image);
                viewHolder.mTitle = (TextView) convertView.findViewById(R.id.txt_title);
                viewHolder.mSubTitle = (TextView) convertView.findViewById(R.id.txt_subtitle);
                viewHolder.mDuration = (TextView) convertView.findViewById(R.id.txt_duration);
                viewHolder.checkbox = (CheckBox) convertView.findViewById(R.id.list_cell_checkbox);

                convertView.setTag(viewHolder);
            } else {
                viewHolder = (ViewHolder) convertView.getTag();
            }

            // PersonData 에서 position 에 위치한 데이터 참조 획득
            final Song_Item songItem = songsList.get(position);

            // 아이템 내 각 위젯에 데이터 반영
            Bitmap albumArt = MainActivity.getArtworkQuick(context, (int)songItem.getAlbumId(), 100, 100);
            if(albumArt != null){
                viewHolder.mImgAlbumArt.setImageBitmap(albumArt);
            }
            viewHolder.mTitle.setText(songItem.getTitle());
            viewHolder.mSubTitle.setText(songItem.getArtist());

            int dur = (int) songItem.getDuration();
            int hrs = (dur / 3600000);
            int mns = (dur / 60000) % 60000;
            int scs = dur % 60000 / 1000;
            String songTime = String.format("%02d:%02d:%02d", hrs,  mns, scs);
            if(hrs == 0){
                songTime = String.format("%02d:%02d", mns, scs);
            }
            viewHolder.mDuration.setText(songTime);

            if (isMSG == false) {
                viewHolder.checkbox.setVisibility(View.GONE);
                convertView.setOnClickListener(new View.OnClickListener() {
                    @Override
                    public void onClick(View view) {
                        Toast.makeText(getApplicationContext(), "노래 재생합니다" + songItem.getAlbumId(), Toast.LENGTH_SHORT).show();

                        Intent intent = new Intent(MainActivity.this, Player.class);
                        intent.putExtra("path", songItem.getDataPath());
                        intent.putExtra("mTitle", songItem.getTitle());
                        intent.putExtra("mDuration", songItem.getDuration());
                        startActivity(intent);
                    }
                });
            } else {
                if (isMSG == true) {
                    viewHolder.checkbox.setVisibility(View.VISIBLE);
                    viewHolder.checkbox.setTag(position); // This line is important.

                    viewHolder.checkbox.setOnClickListener(new View.OnClickListener() {
                        @Override
                        public void onClick(View v) {
                            if(songsList.get(position).isCheckBoxState() == true){
                                songsList.get(position).setCheckBoxState(false);
                            } else {
                                songsList.get(position).setCheckBoxState(true);
                            }
                        }
                    });

                    convertView.setOnClickListener(new View.OnClickListener() {
                        @Override
                        public void onClick(View v) {
                            if(viewHolder.checkbox.isChecked() == false){
                                viewHolder.checkbox.setChecked(true);
                                songsList.get(position).setCheckBoxState(true);
                            } else {
                                viewHolder.checkbox.setChecked(false);
                                songsList.get(position).setCheckBoxState(false);
                            }
                        }
                    });

                }
            }

            // 재사용 문제 해결
            if(songsList.get(position).isCheckBoxState() == true){
                viewHolder.checkbox.setChecked(true);
            } else {
                viewHolder.checkbox.setChecked(false);
            }

            return convertView;
        }

    }

    /* Album ID로 부터 Bitmap 이미지를 생성해 리턴해 주는 메소드 */
    private static final BitmapFactory.Options sBitmapOptionsCache = new BitmapFactory.Options();
    private static final Uri sArtworkUri = Uri.parse("content://media/external/audio/albumart");

    // Get album art for specified album. This method will not try to
    // fall back to getting artwork directly from the file, nor will it attempt to repair the database.
    private static Bitmap getArtworkQuick(Context context, int album_id, int w, int h) {
        w -= 2;
        h -= 2;
        ContentResolver res = context.getContentResolver();
        Uri uri = ContentUris.withAppendedId(sArtworkUri, album_id);
        if (uri != null) {
            ParcelFileDescriptor fd = null;
            try {
                fd = res.openFileDescriptor(uri, "r");
                int sampleSize = 1;

                sBitmapOptionsCache.inJustDecodeBounds = true;
                BitmapFactory.decodeFileDescriptor(
                        fd.getFileDescriptor(), null, sBitmapOptionsCache);
                int nextWidth = sBitmapOptionsCache.outWidth >> 1;
                int nextHeight = sBitmapOptionsCache.outHeight >> 1;
                while (nextWidth>w && nextHeight>h) {
                    sampleSize <<= 1;
                    nextWidth >>= 1;
                    nextHeight >>= 1;
                }

                sBitmapOptionsCache.inSampleSize = sampleSize;
                sBitmapOptionsCache.inJustDecodeBounds = false;
                Bitmap b = BitmapFactory.decodeFileDescriptor(
                        fd.getFileDescriptor(), null, sBitmapOptionsCache);

                if (b != null) {
                    // finally rescale to exactly the size we need
                    if (sBitmapOptionsCache.outWidth != w || sBitmapOptionsCache.outHeight != h) {
                        Bitmap tmp = Bitmap.createScaledBitmap(b, w, h, true);
                        b.recycle();
                        b = tmp;
                    }
                }

                return b;
            } catch (FileNotFoundException e) {
            } finally {
                try {
                    if (fd != null)
                        fd.close();
                } catch (IOException e) {
                }
            }
        }
        return null;
    }

}


song_item.xml 파일과 activity_main.xml 파일은 첨부파일로 올린다.

layout.zip


이런 형태로 데이터를 가져온 모습을 볼 수 있게 된다.


Player.java 파일 구현할 내용에 따라 넘겨줄 내용이 달라지므로 그 부분까지 완성되면 코드 전체를 첨부해볼 생각이다.

아직은 배워야 할 기능들이 많아서 완성된 코드까지는 많은 시간이 걸릴 것 같다.


두고 두고 읽어보고 싶은 블로그 글을 검색으로 알게되어 적어둔다. 나같은 초보에겐 주옥같은 글이다.

http://unikys.tistory.com/350


앞으로 참고할 사이트

https://www.sitepoint.com/a-step-by-step-guide-to-building-an-android-audio-player-app/


728x90
블로그 이미지

Link2Me

,