728x90

오디오 플레이어를 테스트 하다보니 화면 자동꺼짐 설정시간에 따라 음악 재생중에 화면이 꺼지면서 재생이 되지 않는 현상을 발견했다.


그래서 음악 재생중에는 화면 꺼짐이 발생하지 않게 처리하기 위한 코드를 추가했다.


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

    // 화면 꺼짐 방지
    getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);

    // 재생할 목록 배열에 저장하기
    Intent intent = getIntent();
    list =(ArrayList<Song_Item>) intent.getSerializableExtra("playList");

    mediaPlayer = new MediaPlayer();

} 


위와 같이 한줄을 추가해주면 된다.


선택한 노래가 5곡인데 5곡이 모두 재생되고 나면 화면 꺼짐 방지 설정 때문에 아무런 조치를 안하면 화면이 계속 껴진 상태로 남아있더라.

그러므로 화면 해제 설정을 해주어야 한다.

getWindow().clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);


화면 설정 해제 명령어를 넣어줬음에도 불구하고 화면(SCREEN)이 계속 ON 상태가 되는 현상이 있어서 Log를 찍어보니까 songProgressBar 핸들러 처리가 잘못되어 문제가 생겼다.

그래서 해당 코드를 수정하고 http://link2me.tistory.com/1350 내용을 업데이트 했다.


음악 재생이 완료되지 마자 자동꺼짐 설정 모드로 변경되지는 않는다.

만약 그렇게 하고 싶다면

@SuppressLint("Wakelock")
private void acquireCPUWakeLock(Context context) {
    // 잠든 화면 깨우기
    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();
}

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

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


위 코드를 적절히 활용하면 될 것으로 본다.

mediaPlayer.setOnCompletionListener(new MediaPlayer.OnCompletionListener() {
    @Override
    public void onCompletion(MediaPlayer mp) {
        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","음악종료");
                seekHandler.removeCallbacks(mUpdateTimeTask);
                position = 0;
                acquireCPUWakeLock(mContext);
                releaseCpuLock();
                //getWindow().clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
            }
        }

    }
});
 


블로그 이미지

Link2Me

,
728x90

ArrayList 로 선택한 노래만 팝업 메뉴로 만드는 방법을 구현하고 적어둔다.


팝업은 API 레벨 11 이상에서만 사용할 수 있다.
PopupMenu(Context context, View anchor)
- 두번째 인수는 팝업 메뉴를 표시할 앵커 뷰를 지정


popupMenu.getMenu().add(group_id, item_id, order, "노래타이틀");

popupMenu.getMenu().add(Menu.NONE, i, Menu.NONE, list.get(i).getTitle());

Menu.NONE 대신에 0 을 넣어도 된다.



//import android.support.v7.widget.PopupMenu; // ==> 이줄 추가하면 에러 발생함
import android.widget.PopupMenu;  // ==> 에러 발생 안함
import android.view.Menu;
import android.view.MenuItem;


 //import android.support.v7.widget.PopupMenu;
import android.widget.PopupMenu;
import android.view.Menu;
import android.view.MenuItem;

public class Player extends AppCompatActivity implements View.OnClickListener {
    private ImageButton btnPlaylist;

    private ArrayList<Song_Item> list;
    private MediaPlayer mediaPlayer;
    private int position = 0; // 현재 재생곡 위치

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

        Intent intent = getIntent();
        list =(ArrayList<Song_Item>) intent.getSerializableExtra("playList");
        mediaPlayer = new MediaPlayer();

        // All player buttons
        btnPlaylist = (ImageButton) findViewById(R.id.btnPlaylist);

        btnPlaylist.setOnClickListener(this); // 곡 선택창

        System.out.println("선택된 노래곡수 = " + list.size());
        playMusic(list.get(position)); // 노래 곡
        seekBar_Progressing();
    }

    public void onClick(View v) {
        switch (v.getId()){
            case R.id.btnPlaylist:
                // 팝업 메뉴 나오게 하는 방법
                PopupMenu popupMenu = new PopupMenu(this, v);
                Menu menu = popupMenu.getMenu();
                for(int i=0; i < list.size();i++){

                    //Popup Menu에 들어갈 MenuItem 추가

                    menu.add(0,i,0,list.get(i).getTitle());
                }

                //메뉴가 클릭했을때 처리하는 부분

                popupMenu.setOnMenuItemClickListener(new PopupMenu.OnMenuItemClickListener() {
                    @Override
                    public boolean onMenuItemClick(MenuItem item) {
                        int i = item.getItemId();
                        Toast.makeText(Player.this, item.getTitle() + " 선택했습니다", Toast.LENGTH_SHORT).show();
                        playMusic(list.get(i)); // 노래 곡
                        return true;
                    }
                });
                popupMenu.show();
                break;
        }
    }

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

            seek_bar.setMax(mediaPlayer.getDuration()); // 음악파일의 전체길이
            seek_bar.setProgress(mediaPlayer.getCurrentPosition());
            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);

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



미리 정해진 xml 을 메뉴로 만드는 방법

https://www.tutlane.com/tutorial/android/android-popup-menu-with-examples


https://www.javatpoint.com/android-popup-menu-example 를 참조하면 도움된다.


블로그 이미지

Link2Me

,
728x90

안드로이드에서 이미지 파일 다운로드 소스 테스트한 것을 적어둔다.


이미지가 있는 url 에서 파일명만 추출하여 파일명을 자동으로 저장하기 위해서

private String getFileName(String url) {
    // url 에서 파일 이름만 자르기(확장자 제외)
   String fileName = url.substring( url.lastIndexOf('/')+1, url.length() );
   return fileName.substring(0, fileName.lastIndexOf('.'));
}

private String getFileName(String url) {
    // url 에서 파일만 자르기
   return url.substring( url.lastIndexOf('/')+1, url.length() );
}


이미지 파일을 저장할 폴더(디렉토리)를 생성하는 메소드

private File file, dir;
private String saveFolderName = "ImageTemp";

private void MakePhtoDir(){
    //saveFolderName = "/Android/data/" + getPackageName();
    //dir = new File(Environment.getExternalStorageDirectory().getAbsolutePath(), saveFolderName);
    dir = new File(Environment.getExternalStorageDirectory(), saveFolderName);
    if (!dir.exists())
        dir.mkdirs(); // make dir
}


이미지 파일은 다운로드 완료한 후에는 갤러리를 업데이트 해주어야 볼 수 있다.

sendBroadcast(new Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE, Uri.fromFile(file)));

이 한줄을 생략하면 USB 케이블 연결해서 해당 폴더를 확인해도 파일명을 다운로드 했는지 여부를 알 수가 없더라.

파일 다운로드 여부를 굳이 알 수 있게 할 것이 아니라면 생략해도 된다.


ImageView 에 출력하는 것은 아래 4줄만 적어주면 된다.

File file = new File(dir + "/" + tempFileName);
sendBroadcast(new Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE, Uri.fromFile(file)));
Bitmap photoBitmap = BitmapFactory.decodeFile(file.getAbsolutePath() );
imageView.setImageBitmap(photoBitmap);


갤러리에서 저장한 이미지를 확인하려면

//저장한 이미지 확인
Intent i = new Intent(Intent.ACTION_VIEW);
i.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
i.setDataAndType(Uri.fromFile(file), "image/*"); //type 지정 (이미지)
getApplicationContext().startActivity(i);



전체 테스트 코드


Androidmanifest.xml 파일에 추가할 사항

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

<?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">

    <Button
        android:id="@+id/btn_photodownload"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="center_horizontal"
        android:layout_marginTop="30dp"
        android:text="사진다운로드"
        tools:layout_editor_absoluteX="239dp"
        tools:layout_editor_absoluteY="31dp" />

    <ImageView
        android:id="@+id/DNImageView"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_gravity="center"
        android:adjustViewBounds="true"
        android:layout_marginTop="30dp"
        android:background="@drawable/profile_noimage"
        android:src="@drawable/photo_base" />
</LinearLayout>

import android.app.Activity;
import android.content.Context;
import android.content.Intent;
import android.database.Cursor;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.net.Uri;
import android.os.AsyncTask;
import android.os.Bundle;
import android.os.Environment;
import android.provider.MediaStore;
import android.view.View;
import android.widget.Button;
import android.widget.ImageView;
import android.widget.ProgressBar;
import android.widget.TextView;
import android.widget.Toast;

import java.io.BufferedInputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.URL;
import java.net.URLConnection;

public class PhotoDownload extends Activity {
    private static String TAG = "Photo";
    TextView textView;
    ImageView imageView;
    Context context;

    ProgressBar progressBar;
    private File file, dir;
    private String savePath= "ImageTemp";
    private String FileName = null;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.photo_download);
        context = this.getBaseContext();

        MakePhtoDir();

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

        Button photo_download = (Button) findViewById(R.id.btn_photodownload);
        photo_download.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                String imgUrl = "http://img.naver.net/static/www/u/2013/0731/nmms_224940510.gif";
                FileName = imgUrl.substring( imgUrl.lastIndexOf('/')+1, imgUrl.length() );
                DownloadPhotoFromURL downloadPhotoFromURL = new DownloadPhotoFromURL();
                // 동일한 파일이 있는지 검사
                if(new File(dir.getPath() + File.separator + FileName).exists() == false){
                    downloadPhotoFromURL.execute(imgUrl,FileName);
                } else {
                    Toast.makeText(context, "파일이 이미 존재합니다", Toast.LENGTH_SHORT).show();

                    File file = new File(dir + "/" + FileName);
                    Bitmap photoBitmap = BitmapFactory.decodeFile(file.getAbsolutePath() );
                    imageView.setImageBitmap(photoBitmap);
                }
            }
        });
    }

    private void MakePhtoDir(){
        //savePath = "/Android/data/" + getPackageName();
        //dir = new File(Environment.getExternalStorageDirectory().getAbsolutePath(), savePath);
        dir = new File(Environment.getExternalStorageDirectory(), savePath );
        if (!dir.exists())
            dir.mkdirs(); // make dir
    }

    public String getRealPathFromURI(Uri contentUri) {
        // 갤러리 이미지 파일의 실제 경로 구하기
        String[] proj = { MediaStore.Images.Media.DATA };
        Cursor cursor = getContentResolver().query(contentUri, proj, null, null, null);
        int column_index = cursor.getColumnIndexOrThrow(MediaStore.Images.Media.DATA);
        cursor.moveToFirst();
        return cursor.getString(column_index);
    }

    class DownloadPhotoFromURL extends AsyncTask<String, Integer, String> {
        int count;
        int lenghtOfFile = 0;
        InputStream input = null;
        OutputStream output = null;
        String tempFileName;

        @Override
        protected void onPreExecute() {
            super.onPreExecute();
            //progressBar.setProgress(0);
        }

        @Override
        protected String doInBackground(String... params) {
            tempFileName = params[1];
            file = new File(dir, params[1]); // 다운로드할 파일명
            try {
                URL url = new URL(params[0]);
                URLConnection connection = url.openConnection();
                connection.connect();

                lenghtOfFile = connection.getContentLength(); // 파일 크기를 가져옴

//                if (file.exists()) {
//                    file.delete();
//                    Log.d(TAG, "file deleted...");
//                }

                input = new BufferedInputStream(url.openStream());
                output = new FileOutputStream(file);
                byte data[] = new byte[1024];
                long total = 0;

                while ((count = input.read(data)) != -1) {
                    if (isCancelled()) {
                        input.close();
                        return String.valueOf(-1);
                    }
                    total = total + count;
                    if (lenghtOfFile > 0) { // 파일 총 크기가 0 보다 크면
                        publishProgress((int) (total * 100 / lenghtOfFile));
                    }
                    output.write(data, 0, count); // 파일에 데이터를 기록
                }

                output.flush();

            } catch (Exception e) {
                e.printStackTrace();
            } finally {
                if (input != null) {
                    try {
                        input.close();
                    }
                    catch(IOException ioex) {
                    }
                }
                if (output != null) {
                    try {
                        output.close();
                    }
                    catch(IOException ioex) {
                    }
                }
            }
            return null;
        }

        protected void onProgressUpdate(Integer... progress) {
            super.onProgressUpdate(progress);
            // 백그라운드 작업의 진행상태를 표시하기 위해서 호출하는 메소드
            //progressBar.setProgress(progress[0]);
            //textView.setText("다운로드 : " + progress[0] + "%");
        }

        protected void onPostExecute(String result) {
            // pdLoading.dismiss();
            if (result == null) {
                Toast.makeText(getApplicationContext(), "다운로드 완료되었습니다.", Toast.LENGTH_LONG).show();

                File file = new File(dir + "/" + tempFileName);
                //이미지 스캔해서 갤러리 업데이트
                sendBroadcast(new Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE, Uri.fromFile(file)));
                Bitmap photoBitmap = BitmapFactory.decodeFile(file.getAbsolutePath() );
                imageView.setImageBitmap(photoBitmap);
            } else {
                Toast.makeText(getApplicationContext(), "다운로드 에러", Toast.LENGTH_LONG).show();
            }
        }
    }
}


블로그 이미지

Link2Me

,
728x90

이미지 파일 전송시 서버에서 구현할 PHP 소스는 다음과 같다.

개인 프로필 사진을 업로드할 경우의 수정사항이다.


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

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


안드로이드 수정사항

Button btn_UploadPhoto = (Button) findViewById(R.id.btn_Upload); // 사진 업로드
btn_UploadPhoto.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View view) {
        if (TextUtils.isEmpty(imagePath)) {
            Toast.makeText(getApplicationContext(), "업로드할 사진을 선택해주세요", Toast.LENGTH_SHORT).show();
        } else {
            if (NetworkHelper.checkConnection(mContext)) { // 인터넷 연결 체크
                Log.d("Photo", "Photo Upload Task Start");
                String ImageUploadURL = Value.IPADDRESS + "/upload.php";
                SharedPreferences pref = getSharedPreferences("pref", Activity.MODE_PRIVATE);
                String idx = pref.getString("idx", "");
                ImageUploadTask imageUploadTask = new ImageUploadTask();
                imageUploadTask.execute(ImageUploadURL, imagePath, idx);
            } else {
                Toast.makeText(mContext, "인터넷 연결을 확인하세요", Toast.LENGTH_LONG).show();
            }
        }
    }
});


 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],params[2]);
            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();
            // 임시 파일 삭제 (카메라로 사진 촬영한 이미지)
            if(mImageCaptureUri != null){
                File file = new File(mImageCaptureUri.getPath());
                if(file.exists()) {
                    file.delete();
                }
                mImageCaptureUri = null;
            }
            imagePath = null;
        }  else{
            Toast.makeText(getApplicationContext(), "파일 업로드 실패", Toast.LENGTH_LONG).show();
        }

    }
}

 public class JSONParser {

    public static JSONObject uploadImage(String imageUploadUrl, String sourceImageFile, String idx) {
        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("idx", idx)
                    .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", "Success : " + 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;
    }
}


블로그 이미지

Link2Me

,
728x90

안드로이드 스튜디오에서 사진촬영 및 특정부분 이미지를 CROP 해서 업로드하는 법과 갤러리(앨범)에서 사진을 선택해서 업로드하는 걸 테스트하고 있다.


 삼성 갤럭시 S7 (7.0)

 사진 촬영후 CROP 전송 OK,

 갤러리 사진 선택후 CROP하면 기존 파일을 덮어쓰는 현상(해결 필요)

 삼성 갤럭시 A5 (6.0.1)

 사진 촬영후 CROP 전송 OK, 갤러리 CROP 후 전송 OK

 삼성 갤럭시 S4 (5.0.1)

 사진 촬영후 CROP 전송 OK, 갤러리 CROP 후 전송 OK


안드로이드 7.0 이상 운영체제에서 문제가 있는 것인지 집중적인 테스트가 필요할 거 같다.

지난번 테스트보다는 기능이 개선되었지만, 좀 더 보완이 필요하다.

7.0 에서 테스트한 결과는 이미지를 축소 저장하는 saveCropImage 메소드가 없이 자체적으로(?) 이미지를 축소하기 때문에 원본 갤러리 파일에 문제가 생기는 거 같다.

사진 촬영후 전송할 경우 같은 myphoto.jpg 로 저장하도록 해서 문제 없이 서버에 축소된 이미지가 업로드된다.

문제는 갤러리 사진을 업로드할 경우 CROP 까지는 잘되는데, CROP 한 후에 원본 이미지를 훼손하는 문제가 생긴다. 갤럭시 S4, A5에서는 정상동작한다. 코드 상으로도 정상 동작되어야 맞는거 같은데.....

7.0 에서 뭔가 내가 놓치는 것이 뭘까??? 좀 더 찾아보고 해결해보련다.


음.... 확인해보니 Build.VERSION.SDK_INT 가 24이상인 경우에는 다른 방법으로 구현해야 하더라.

일일이 로그를 찍어가면서 확인해보니, CROP 이후에 값 자체가 넘어오지 않고 그냥 임시로 생성한 파일만 지정한 폴더(디렉토리)에 저장되는 걸 확인했다.

내가 구현한 방식은 한마디로 편법으로 7.0 에서 동작되었던 것이다.

임시로 저장된 파일을 다시 읽어서 그 파일을 서버로 업로드하는 거다.


 package com.tistory.link2me.imageupload;

import android.Manifest;
import android.app.Activity;
import android.app.ActivityManager;
import android.app.AlertDialog;
import android.app.ProgressDialog;
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.database.Cursor;
import android.graphics.Bitmap;
import android.net.Uri;
import android.os.AsyncTask;
import android.os.Build;
import android.os.Bundle;
import android.os.Environment;
import android.provider.MediaStore;
import android.provider.Settings;
import android.support.v4.app.ActivityCompat;
import android.support.v4.content.ContextCompat;
import android.text.TextUtils;
import android.util.Log;
import android.view.View;
import android.widget.Button;
import android.widget.ImageView;
import android.widget.Toast;

import com.squareup.picasso.Picasso;
import com.tistory.link2me.common.BackPressHandler;
import com.tistory.link2me.common.JSONParser;
import com.tistory.link2me.common.NetworkHelper;

import org.json.JSONException;
import org.json.JSONObject;

import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;

public class PhotoActivity extends Activity {
    Context mContext;

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

    private static Uri mImageCaptureUri; // Stactic 으로 설정해야 에러 발생하지 않음.
    private String imagePath;
    private ImageView imageView;
    private Bitmap photoBitmap;
    String saveFolderName = "cameraTemp";
    File mediaFile = null;
    String saveFileName = "myphoto.jpg";

    ProgressDialog mProgressDialog;
    private BackPressHandler backPressHandler;

    private String[] permissions = {
            Manifest.permission.READ_EXTERNAL_STORAGE,
            Manifest.permission.WRITE_EXTERNAL_STORAGE,
            Manifest.permission.CAMERA,
            Manifest.permission.RECORD_AUDIO
    };
    private static final int MULTIPLE_PERMISSIONS = 101;

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

        System.out.println("프로세스당 메모리 용량 : " + ((ActivityManager)getSystemService(Context.ACTIVITY_SERVICE)).getMemoryClass());
        System.out.println("largeHeap 크기 : " + ((ActivityManager)getSystemService(Context.ACTIVITY_SERVICE)).getLargeMemoryClass());
        System.out.println("패키지 설치 경로 : " + getApplicationContext().getFilesDir().getAbsolutePath().replace("files", ""));

        checkPermissions();
        initView();
    }

    private void initView() {
        imageView = (ImageView) findViewById(R.id.croppedImageView);
        imageView.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                AlertDialog.Builder builder = new AlertDialog.Builder(PhotoActivity.this);
                builder.setMessage("업로드할 이미지 선택");
                DialogInterface.OnClickListener cameraListener = new DialogInterface.OnClickListener() {
                    @Override
                    public void onClick(DialogInterface dialog, int which) {
                        doTakePhotoAction();
                    }
                };

                DialogInterface.OnClickListener albumListener = new DialogInterface.OnClickListener() {
                    @Override
                    public void onClick(DialogInterface dialog, int which) {
                        getGallery(); // 갤러리(앨범)에서 이미지 가져오기
                    }
                };

                DialogInterface.OnClickListener cancelListener = new DialogInterface.OnClickListener() {
                    @Override
                    public void onClick(DialogInterface dialog, int which) {
                        dialog.dismiss();
                    }

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

            }
        });

        Button btn_UploadPhoto = (Button) findViewById(R.id.btn_Upload);
        btn_UploadPhoto.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                if (TextUtils.isEmpty(imagePath)) {
                    Toast.makeText(getApplicationContext(), "업로드할 사진을 선택해주세요", Toast.LENGTH_SHORT).show();
                } else {
                    if (NetworkHelper.checkConnection(mContext)) { // 인터넷 연결 체크
                        Log.d("Photo", "Photo Upload Task Start");
                        String ImageUploadURL = Value.IPADDRESS + "/upload.php";
                        ImageUploadTask imageUploadTask = new ImageUploadTask();
                        imageUploadTask.execute(ImageUploadURL, imagePath);
                    } else {
                        Toast.makeText(mContext, "인터넷 연결을 확인하세요", Toast.LENGTH_LONG).show();
                    }
                }
            }
        });

    }

    private Boolean isAirModeOn() {
        Boolean isAirplaneMode;
        if(Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN_MR1){
            isAirplaneMode = Settings.System.getInt(getContentResolver(),
                    Settings.System.AIRPLANE_MODE_ON, 0) == 1;
        }else{
            isAirplaneMode = Settings.Global.getInt(getContentResolver(),
                    Settings.Global.AIRPLANE_MODE_ON, 0) == 1;
        }
        return isAirplaneMode;
    }

    /**
     * 카메라에서 사진 촬영
     */
    private void doTakePhotoAction() {  // 카메라 앱을 이용하여 사진 찍기
        // Intent를 사용하여 안드로이드에서 기본적으로 제공해주는 카메라를 이용하는 방법 이용
        Intent cameraIntent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
        // 이미지가 저장될 파일은 카메라 앱이 구동되기 전에 세팅해서 넘겨준다.
        cameraIntent.putExtra(MediaStore.EXTRA_OUTPUT, getImageUri(saveFileName)); // null
        cameraIntent.putExtra("return-data", true);
        startActivityForResult(cameraIntent, PICK_FROM_CAMERA); // 7.0 에서는 에러가 발생함. (API26 으로 컴파일 한 경우)
    }

    // 사진 선택을 위해 갤러리를 호출
    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");
        galleryIntent.setType(MediaStore.Images.Media.CONTENT_TYPE);
        //startActivityForResult(chooserIntent, PICK_FROM_File);
        startActivityForResult(chooserIntent, PICK_FROM_ALBUM);
    }

    private Uri getImageUri(String saveFile) {
        // 임시로 사용할 파일의 경로를 생성
        File mediaStorageDir = new File(Environment.getExternalStorageDirectory() + "/DCIM", saveFolderName);
        if (! mediaStorageDir.exists()){
            if (! mediaStorageDir.mkdirs()){
                Log.d("MyCameraApp", "failed to create directory");
                return null;
            }
        }

        String timeStamp = new SimpleDateFormat("yyyyMMdd_HHmmss").format(new Date());
        if(saveFile != null){
            mediaFile = new File(mediaStorageDir.getPath() + File.separator + saveFile);
        } else {
            mediaFile = new File(mediaStorageDir.getPath() + File.separator + "pic_"+ timeStamp + ".jpg");
        }
        mImageCaptureUri = Uri.fromFile(mediaFile);
        imagePath = mImageCaptureUri.getPath();
        Log.e("mImageCaptureUri : ", mImageCaptureUri.toString());
        Log.e("imagePath : ", imagePath);

        return mImageCaptureUri;
    }

    public String getRealPathFromURI(Uri contentUri) {
        String[] proj = { MediaStore.Images.Media.DATA };
        Cursor cursor = getContentResolver().query(contentUri, proj, null, null, null);
        int column_index = cursor.getColumnIndexOrThrow(MediaStore.Images.Media.DATA);
        cursor.moveToFirst();
        return cursor.getString(column_index);
    }

    @Override
    protected void onActivityResult(int requestCode, int resultCode, Intent data) {
        super.onActivityResult(requestCode, resultCode, data);
        if(resultCode != Activity.RESULT_OK) return;
        switch (requestCode){
            case PICK_FROM_ALBUM: {
                mImageCaptureUri = data.getData();
                Log.e("앨범이미지 CROP",mImageCaptureUri.getPath().toString());
                imagePath = getRealPathFromURI(mImageCaptureUri); // 실제 파일이 존재하는 경로
                Log.e("앨범이미지 경로",imagePath);
            }

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

                // CROP 된 이미지를 저장하기 위한 File 경로 설정
                String filePath = getImageUri(saveFileName).getPath();
                Log.e("mImageCaptureUri : ", "Croped " + mImageCaptureUri.toString());

                imagePath = filePath;

                if(extras != null) {
                    //photoBitmap = extras.getParcelable("data"); // CROP된 BITMAP
                    photoBitmap = (Bitmap)data.getExtras().get("data"); // CROP된 BITMAP
                    // 레이아웃의 이미지칸에 CROP된 BITMAP을 보여줌
                    saveCropImage(photoBitmap,imagePath); //  CROP 된 이미지를 외부저장소, 앨범에 저장한다.
                    // sendBroadcast를 통해 Crop된 사진을 앨범에 보이도록 갱신한다.
                    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
                        sendBroadcast(new Intent( Intent.ACTION_MEDIA_SCANNER_SCAN_FILE, Uri.fromFile(mediaFile)) );
                    } else {
                        sendBroadcast(new Intent(Intent.ACTION_MEDIA_MOUNTED, Uri.parse("file://"+ Environment.getExternalStorageDirectory())));
                    }
                }

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

                break;
            }
            case PICK_FROM_File: {
                // URI 정보를 이용하여 이미지(사진) 정보를 가져온다.
                if (data == null) {
                    Toast.makeText(mContext, "Unable to Pickup Image", Toast.LENGTH_SHORT).show();
                    return;
                }
                Uri selectedImageUri = data.getData();
                Log.e("IMAGE SEL", "" + selectedImageUri);
                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]));
                    Log.e("IMAGE SEL imagePath : ", imagePath);                 

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

                }

                break;
            }
        }

    }

    // Bitmap 을 저장하는 메소드
    private void saveCropImage(Bitmap bitmap, String filePath) {
        //read image file
        File copyFile = new File(filePath);
        BufferedOutputStream bos = null;
        try {
            copyFile.createNewFile();
            int quality = 100;
            bos = new BufferedOutputStream(new FileOutputStream(copyFile));
            bitmap.compress(Bitmap.CompressFormat.JPEG, quality, bos);
            // 이미지가 클 경우 OutOfMemoryException 발생이 예상되어 압축
            bos.flush();
            bos.close();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

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

        @Override
        protected void onPreExecute() {
            super.onPreExecute();
            progressDialog = new ProgressDialog(PhotoActivity.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;
            }

            if (aBoolean){
                imagePath = null;
            }
            
        }

    }

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

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

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

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


앱 build.Gradle 설정 내용

apply plugin: 'com.android.application'

android {
    compileSdkVersion 22
    buildToolsVersion "25.0.3"

    defaultConfig {
        applicationId "com.tistory.link2me.photoupload"
        minSdkVersion 19
        targetSdkVersion 22
        versionCode 1
        versionName "1.0"

        testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"

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

dependencies {
    compile fileTree(dir: 'libs', include: ['*.jar'])
    androidTestCompile('com.android.support.test.espresso:espresso-core:2.2.2', {
        exclude group: 'com.android.support', module: 'support-annotations'
    })
    compile 'com.android.support:appcompat-v7:22.0.0'
    compile 'com.android.support.constraint:constraint-layout:1.0.2'
    compile 'com.squareup.okhttp3:okhttp:3.9.0'
    compile 'com.squareup.picasso:picasso:2.5.2'
    compile 'com.google.firebase:firebase-messaging:10.2.6'
    testCompile 'junit:junit:4.12'
}


public class NetworkHelper {

    /** CHECK WHETHER INTERNET CONNECTION IS AVAILABLE OR NOT */
    public static boolean checkConnection(Context context) {
        return  ((ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE)).getActiveNetworkInfo() != null;
    }
}

 public class OKHttpComm {
    //GET network request
    //@RequiresApi(api = Build.VERSION_CODES.KITKAT)
    public static String GET(String url) throws IOException {
        OkHttpClient client = new OkHttpClient();
        Request request = new Request.Builder()
                .url(url)
                .build();

        Response response = null;
        try {
            response = client.newCall(request).execute();
            return response.body().string();
        } catch (IOException e) {
            Log.e("OKHttp", "error in getting response get request okhttp");
        }
        return null;
    }

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

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

            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());
            return sb.toString().trim();  
            // 수행이 끝나고 리턴하는 값은 다음에 수행될 onProgressUpdate 의 파라미터가 된다
        } catch(Exception e){  
            return new String("Exception: " + e.getMessage());
        }   
 
    }

}



2017.9.20 수정 사항

안드로이드 7.0 (삼성 갤럭시 기준)에서 이미지 저장하는 것이 기존 것과 달라서 다른 방법으로 시도를 해봤지만 compileSdkVersion 22 로 해서인지 다른 블로그에서 해보라는 방식으로 해도 안된다.

수정 테스트한 것만 발췌하여 적어둔다.




2017.9.21 LG LGM-X600K 테스트

LG폰을 구해서 사진 촬영 및 이미지 업로드 테스트를 했더니 기존 소스 코드로 해야만 동작이 되더라.

그래서 아래 부분을 보완했다.

대부분 KIKAT 이상 단말이므로 sendBroadcast(new Intent( Intent.ACTION_MEDIA_SCANNER_SCAN_FILE, Uri.fromFile(mediaFile)) ); 부분만 코드에 추가해도 될 것으로 본다.


case CROP_FROM_CAMERA:  {
    // CROP 된 이후의 이미지를 넘겨 받음
    final Bundle extras = data.getExtras();

    // CROP 된 이미지를 저장하기 위한 File 경로 설정
    String filePath = getFileUri(saveFileName).getPath();
    imagePath = filePath;

    if(extras != null) {
        //photoBitmap = extras.getParcelable("data"); // CROP된 BITMAP
        photoBitmap = (Bitmap)data.getExtras().get("data"); // CROP된 BITMAP
        saveCropImage(photoBitmap,filePath); //  CROP 된 이미지를 외부저장소, 앨범에 저장한다.
        // sendBroadcast를 통해 Crop된 사진을 앨범에 보이도록 갱신한다.
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
            sendBroadcast(new Intent( Intent.ACTION_MEDIA_SCANNER_SCAN_FILE, Uri.fromFile(mediaFile)) );
        } else {
            sendBroadcast(new Intent(Intent.ACTION_MEDIA_MOUNTED, Uri.parse("file://"+ Environment.getExternalStorageDirectory())));
        }
    }

    photoBitmap = BitmapFactory.decodeFile(imagePath );
    // 레이아웃의 이미지칸에 CROP된 BITMAP을 보여줌
    imageView.setImageBitmap(photoBitmap);

    break;
}


블로그 이미지

Link2Me

,
728x90

사진을 촬영하여 이미지를 업로드하기 위한 기능을 테스트하려고 한다.

사진 촬영하여 CROP 하는 곳에서 제대로 처리를 못하고 있어서 라이브러리를 이용해보려고 한다.


안드로이드 스튜디오에서 앱 build.Gradle 을 아래와 같이 수정했더니 에러가 발생한다.


 apply plugin: 'com.android.application'

android {
    compileSdkVersion 22
    buildToolsVersion "25.0.0"

    defaultConfig {
        applicationId "com.tistory.link2me.imageupload"
        minSdkVersion 16
        targetSdkVersion 22
        versionCode 1
        versionName "1.0"

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

dependencies {
    compile fileTree(dir: 'libs', include: ['*.jar'])
    compile 'com.android.support:support-v4:22.0.0'
    compile 'com.android.support:appcompat-v7:22.0.0'
    compile 'com.android.support.constraint:constraint-layout:1.0.2'
    compile 'com.squareup.okhttp3:okhttp:3.9.0'
    compile 'com.squareup.picasso:picasso:2.5.2'
    compile 'com.google.firebase:firebase-messaging:10.2.6'
    compile 'com.isseiaoki:simplecropview:1.1.6'
}


roundIcon이 API 25에서 추가된 것이라 에러가 발생한다.

AndroidMenifest.xml 파일 내에 있는 android:roundIcon="@mipmap/ic_launcher"
를 제거해주고 Clean Project를 하면 에러가 사라진다.


AppCompatActivity는 안드로이드 하위버전을 지원하는 액티비티다.
Support Library에 있는 클래스들은 안드로이드 하위버전을 지원하기 위해 존재한다.
안드로이드 4.0 이상부터 지원하겠다고 하면 AppCompatActivity을 쓸 이유가 없다. 그냥 Activity를 쓰면 된다.

public class MainActivity extends AppCompatActivity {

public class MainActivity extends Activity {
로 변경했다.


2017.9.21

- simplecropview 라이브러리를 추가해서 CROP 하는 것은 실패했다.

  구글, 블로그에 나온 방법으로 코드를 구현하여 해결했다.

블로그 이미지

Link2Me

,
728x90

OKHttp 라이브러리를 이용하여 간단하게 POST 로 데이터를 전송할 경우 알아두어야 할 사항을 적어둔다.

OKHttp 라이브러리 사용조건이 최소 KIKAT 버전 이상인 거 같더라. (확실한 것은 더 테스트를 해봐야....)


RequestBody 객체 부분이 파일을 첨부하는 경우와 일반 텍스트를 전송하는 경우가 약간 다르다.

나머지 부분은 동일하다.  파일을 업로드하는 경우에는 OKHttp 라이브러리를 사용하는 것이 HttpURLConnection 으로 하는 것보다 코드도 심플하고 잘 동작한다.


httpClient 는 Handler를 사용하지 않는 곳에서도 동작이 되는데, OKHttpClient는 AsyncTask 또는 Handler 에서만 동작되더라.

로그인 버튼을 누르면 httpClient 가 동작되도록 되어 있는 코드를 그냥 단순하게 OKHttp 라이브러리를 사용해서 변환하면 동작이 안된다.


httpClient = new DefaultHttpClient();
localContext = new BasicHttpContext();
httpPost = new HttpPost(Value.IPADDRESS + "/loginChk.php");

entity = new MultipartEntity(HttpMultipartMode.BROWSER_COMPATIBLE);

try {
    entity.addPart("loginID", new StringBody(Value.encrypt(ID)));
    entity.addPart("loginPW", new StringBody(Value.encrypt(PW)));
} catch (UnsupportedEncodingException e1) {
    e1.printStackTrace();
}

httpPost.setEntity(entity);
try {
    response = httpClient.execute(httpPost, localContext);
} catch (ClientProtocolException e1) {
    e1.printStackTrace();
} catch (IOException e1) {
    e1.printStackTrace();
}
try {
    reader = new BufferedReader(new InputStreamReader(response.getEntity().getContent(), "UTF-8"));
} catch (UnsupportedEncodingException e1) {
    e1.printStackTrace();
} catch (IllegalStateException e1) {
    e1.printStackTrace();
} catch (IOException e1) {
    e1.printStackTrace();
}

try {
    Response = reader.readLine();
    Log.d(TAG, "응답 ====" + Response);
} catch (IOException e1) {
    e1.printStackTrace();
}


위 코드는 Eclipse 기반의 minSdkVersion 8에서 동작되는 코드인데 Android Studio minSdkVersion 16 이상으로 변환하고 FCM을 연동하니까 동작이 안되더라. FCM(Firebase Cloud Message)는 테스트를 하다보니 minSdkVersion 10 인거 같더라.

HttpClient 는 Android SDK API 23 부터는 더이상 지원을 하지 않는다.

앱 Gradle에서 compileSdkVersion 23으로 한 경우에는 useLibrary 'org.apache.http.legacy'를 하라고 나온다.

내가 테스트한 환경은 compileSdkVersion 22로 했다. 퍼미션 지정하는 것이 귀찮기도 하고 Eclipse 툴로 컴파일 한 것이 잘 동작하는 걸 Android Studio 기반으로 다시 코드를 수정 개발해야 하는 상황이라 수정을 조금이라도 덜 하는 방법을 찾아보려고 했다.

구글링을 해보면 HttpClient 를 사용할 수 있는 방법이 나오는데 그 방법으로 하는데 에러가 나면서 문제가 생긴다.

그래서 코드를 수정 사용해야 하는데 AsyncTask 기반의 OKHttp 또는 HttpURLConnection 로 변환해야 정상 동작된다.


PHPComm.getJson(params[0], params[1]) 코드는 http://link2me.tistory.com/1247 를 참조하면 된다.

OKHttp 로그인 예제 http://link2me.tistory.com/1353 를 보면서 이 내용을 보면 이해할 수 있다.


Uri.Builder builder = new Uri.Builder()
    .appendQueryParameter("loginID", Value.encrypt(ID))
    .appendQueryParameter("loginPW", Value.encrypt(PW));
String postParams = builder.build().getEncodedQuery();
AsyncTaskLogin asyncLogin = new AsyncTaskLogin();
asyncLogin.execute(Value.IPADDRESS + "/loginChk.php", postParams);

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

    @Override
    protected String doInBackground(String... params) {
        // 이곳에 OKHttp 라이브러리 기반 코드를 작성
        // 또는 HttpURLConnection 코드를 작성한다.
        try {
            return PHPComm.getJson(params[0], params[1]);
        } catch (Exception e) {
            return new String("Exception: " + e.getMessage());
        }
    }

    protected void onPostExecute(String result) {

    }
}


HttpClient 를 사용하는 방법이 http://goodnut.tistory.com/60 에 나오는데 이 방법으로는 테스트를 안해봤다.


아래 코드는 단독으로는 동작이 안되고 에러가 발생한다.

반드시 Handler or AsyncTask 에서 동작하도록 코드를 작성해야 한다.


void Agree() {
    String url = Value.IPADDRESS + "/regAssent.php";

    RequestBody requestBody = new FormBody.Builder()
            .add("idx", idx)
            .add("assent", "1")
            .build();

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

    OkHttpClient client = new OkHttpClient();
    try {
        Response response = client.newCall(request).execute();
        Log.d(TAG, "응답 ====" + response);
    } catch (IOException e) {
        e.printStackTrace();
    }
}
 


참고하면 도움되는 사이트


http://www.vogella.com/tutorials/JavaLibrary-OkHttp/article.html


http://www.zoftino.com/android-okhttp-example


블로그 이미지

Link2Me

,
728x90

배터리 교체 등으로 스마트폰의 전원을 OFF 후 부팅시 특정 앱이 자동으로 실행되도록 하는 코드다.


AndroidManifest.xml 파일에 추가할 사항

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

<receiver
    android:name=".AutoRun"
    android:enabled="true"
    android:exported="false">
    <intent-filter>
        <action android:name="android.intent.action.BOOT_COMPLETED"/>
    </intent-filter>
</receiver>


android:enabled="true" : 시스템이 알아서 AutoRun Receiver를 실행한다.
android:exported="false" : 외부 어플리케이션에서는 사용할 수 없다.


AutoRun 브로드캐스트 리시버를 생성한다.

Intent intent = new Intent(context, Intro.class); // 부팅후 자동실행할 Activity 는 Intro.java 파일


import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;

public class AutoRun extends BroadcastReceiver {

    @Override
    public void onReceive(Context context, Intent intent) {
        // 배터리 교체 등으로 부팅시 앱 자동실행 시키기
        String action = intent.getAction();
        if(action.equals("android.intent.action.BOOT_COMPLETED")){
            Intent ii = new Intent(context, Intro.class);
           
ii.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
            context.startActivity(
ii);
        }
    }
}
 




블로그 이미지

Link2Me

,
728x90

안드로이드 어플 사용자에게 PUSH 메시지를 발송하기 위해서는 PHP 서버에서 관련 코드를 구현해야 한다.

앱이 백그라운드에서 실행중이거나 앱이 죽어있을 때에도 동작하게 하려면 FCM API를 직접 호출해주는 방식을 사용해야 한다.


api url :: https://fcm.googleapis.com/fcm/send


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





서버키 4번을 복사해서 아래 코드에 붙여넣기를 한다.


=== fcmpush.php ====

 <?php
require_once 'dbconnect.php'; // db접속
//데이터베이스에 접속해서 토큰들을 가져와서 FCM에 발신요청
$sql = "select tokenID From member_data where LENGTH(tokenID)>63";
$result = mysqli_query($dbconn,$sql);
$tokens = array();
if(mysqli_num_rows($result) > 0 ){
    while ($row = mysqli_fetch_assoc($result)) {
        $tokens[] = $row['tokenID'];
    }
} else {
    echo 'There are no Transfer Datas';
    exit;
}

mysqli_close($dbconn);

$title = isset($_POST['title'])? $_POST['title'] : "PUSH TEST";
$message = isset($_POST['message'])? $_POST['message'] : "새글이 등록되었습니다";

$arr = array();
$arr['title'] = $title;
$arr['message'] = $message;

$message_status = Android_Push($tokens, $arr);
//echo $message_status;
// 푸시 전송 결과 반환.
$obj = json_decode($message_status);

// 푸시 전송시 성공 수량 반환.
$cnt = $obj->{"success"};

echo $cnt;

function Android_Push($tokens, $message) {
    $url = 'https://fcm.googleapis.com/fcm/send';
    $apiKey = "서버키";

    $fields = array('registration_ids' => $tokens,'data' => $message);
    $headers = array('Authorization:key='.$apiKey,'Content-Type: application/json');

    $ch = curl_init();
    curl_setopt($ch, CURLOPT_URL, $url);
    curl_setopt($ch, CURLOPT_POST, true);
    curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
    curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
    curl_setopt ($ch, CURLOPT_SSL_VERIFYHOST, 0);
    curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
    curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($fields));
    $result = curl_exec($ch);
    if ($result === FALSE) {
        die('Curl failed: ' . curl_error($ch));
    }
    curl_close($ch);
    return $result;
}
?>


=== fcmsender.php 샘플 파일 ===

<!DOCTYPE html>
<head>
<title>FCM Sender</title>
    <meta charset="utf-8" />
</head>
<body>
    <form action="fcmpush.php" method="post">
        <table style="width:100;padding:0;margin:0px;border:none">
        <tr>
            <th style="width:20%">제목</th>
            <td style="width:80%"><input type="text" name="title" value="" style="width:100%"></td>
        </tr>
        <tr>
            <th>내용</th>
            <td>
            <textarea name="message" rows=5 cols=42></textarea>
            </td>
        </tr>
        <tr>
            <td colspan=2 style="text-align: center;"><input type="submit" value="Send Notification" /></td>
        </tr>
        </table>
    </form>
</body>
</html>


FCM PUSH PHP 서버 샘플 소스를 첨부한다.


FCM_Push_Sample.zip


블로그 이미지

Link2Me

,
728x90

last Update : 2019.9.2

사용자 기기의 고유 토큰 정보를 획득하는 방법이 변경되었다.

// 사용자 기기의 고유 토큰 정보를 획득
FirebaseInstanceId.getInstance().getInstanceId().addOnSuccessListener( Login.this,  new OnSuccessListener<InstanceIdResult>() {
    @Override
    public void onSuccess(InstanceIdResult instanceIdResult) {
        newToken = instanceIdResult.getToken();
        Log.e("newToken",newToken);

    }
});

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()) {
            Uri.Builder builder = new Uri.Builder()
                    .appendQueryParameter("loginID", Value.encrypt(loginID))
                    .appendQueryParameter("loginPW", Value.encrypt(loginPW))
                    .appendQueryParameter("uID", Value.encrypt(Value.getPhoneUID(context)))
                    .appendQueryParameter("AppVersion", Value.VERSION)
                    .appendQueryParameter("phoneVersion", Build.VERSION.RELEASE)
                    .appendQueryParameter("phoneBrand", Build.BRAND)
                    .appendQueryParameter("phoneModel", Build.MODEL)
                    .appendQueryParameter("mfoneNo", Value.encrypt(getPhoneNumber()))
                    .appendQueryParameter("tokenID", newToken)
                    .appendQueryParameter("keyword", Value.encrypt(Value.URLkey()));
            String urlParameters = builder.build().getEncodedQuery();

            new AsyncLogin().execute(Value.IPADDRESS + "/loginChk.php", urlParameters);
        }
    }
});


========================================================================

Update : 2017.9.2


안드로이드 스튜디오에서 FCM 관련 코드를 구현했으면 이제 구글에서 생성해주는 토큰을 PHP 서버에 등록해야 한다.


토큰(Token)을 등록하는 방법은 로그인 정보에 같이 포함해서 보내는 것이 가장 편리하다.


// 단말기의 ID 정보를 얻기 위해서는 READ_PHONE_STATE 권한이 필요
TelephonyManager mTelephony = (TelephonyManager) getSystemService(Context.TELEPHONY_SERVICE);
if (mTelephony.getDeviceId() != null){
    uID = mTelephony.getDeviceId();  // 스마트폰 기기 정보
} else {
    uID = Settings.Secure.getString(getApplicationContext().getContentResolver(), Settings.Secure.ANDROID_ID);
}

// 사용자 기기의 고유 토큰 정보를 획득
String getToken = FirebaseInstanceId.getInstance().getToken();

System.out.println("DeviceID : " + uID);
System.out.println("Intro Token : " + getToken);

// 전달할 인자들
String keyword = Value.URLkey();
Uri.Builder builder = new Uri.Builder()
        .appendQueryParameter("loginID", Value.encrypt(params[0]))
        .appendQueryParameter("loginPW", Value.encrypt(params[1]))
        .appendQueryParameter("uID", Value.encrypt(uID))
        .appendQueryParameter("phoneVersion", Build.VERSION.RELEASE)
        .appendQueryParameter("phoneBrand", Build.BRAND)
        .appendQueryParameter("phoneModel", Build.MODEL)
        .appendQueryParameter("tokenID", getToken)
        .appendQueryParameter("keyword", Value.encrypt(Value.URLkey()));
String urlParameters = builder.build().getEncodedQuery();
 


토큰에 대한 정보를 획득하여 로그인하면서 서버로 전송하면 된다.


이제 PHP 서버에서는 이 정보를 사용자 DB에 저장해야 한다.


loginChk.php 파일에서 안드로이드 접속 인증 체크를 한다고 가정하자.

loginID, loginPW 인증에 성공하면

// 토큰 등록 및 갱신
if(isset($tokenID) && strlen($tokenID) > 63){
    $c->registerToken($tokenID,$loginID,$deviceID); // DB에 토큰 저장
}


함수를 포함시켜서 DB에 토큰 정보가 저장되고 Update 되도록 한다.

아래 코드는 토큰 저장에 필요한 함수만 발췌했다.

<?php
class TokenClass {
    // 토큰 등록 및 갱신
    function registerToken($token,$userID,$deviceID){
        global $dbconn;
        $rs = $this->getDeviceChk($userID,$deviceID); // memberuid 또는 0
        if($rs > 0 ){
            $gettokenID = $this->getTokenIDChk($rs); // 0 또는 DB 등록 토큰 반환
            if(($gettokenID == '0') || ($gettokenID !== $token)){ // 등록안되었거나 토큰 불일치
                $sql ="UPDATE member_data SET tokenID='".$token."'";
                $sql.=" where memberuid='".$rs."'";
                if($result = mysqli_query($dbconn,$sql)){
                    return 1;
                } else {
                    return 0;
                }
            }
        }
    }

    function getTokenIDChk($rs){
        global $dbconn;
        $sql ="select tokenID from member_data where memberuid='".$rs."'";
        if($result = mysqli_query($dbconn,$sql)){
            $row = mysqli_fetch_row($result);
            if($row[0] == NULL){
                return 0;
            } else {
                return $row[0];
            }
        } else {
            return 0;
        }
    }

    function getDeviceChk($userID,$deviceID){
        global $dbconn;
        // 사용자 기준 장치 정보 검사
        $sql ="select count(phoneID),memberuid from member_data";
        $sql.=" where memberuid=(select uid from member_id where id='".$userID."') and phoneID='".$deviceID."'";
        if($result = mysqli_query($dbconn,$sql)){
            $row = mysqli_fetch_row($result);
            if($row[0] == 1){
                return $row[1]; // 있으면 memberuid 반환
            } else {
                return $row[0];
            }
        } else {
            return 0;
        }
    }

}//end class
?>
 



DB 테이블 구조

테이블 구조는 참조하여 필요한 것만 발췌하여 이용하면 된다.

member_id.uid = member_data.memberuid


CREATE TABLE IF NOT EXISTS `member_data` (
  `memberuid` int(11) NOT NULL,
  `site` int(11) NOT NULL DEFAULT '0',
  `auth` tinyint(4) NOT NULL DEFAULT '0',
  `level` int(11) NOT NULL DEFAULT '0',
  `admin` tinyint(4) NOT NULL DEFAULT '0',
  `email` varchar(50) NOT NULL DEFAULT '',
  `name` varchar(30) NOT NULL DEFAULT '',
  `nic` varchar(50) NOT NULL DEFAULT '',
  `grade` varchar(20) NOT NULL DEFAULT '',
  `photo` varchar(200) NOT NULL DEFAULT '',
  `sex` tinyint(4) NOT NULL DEFAULT '0',
  `officeNO` varchar(14) NOT NULL DEFAULT '',
  `mobileNO` varchar(14) NOT NULL DEFAULT '',
  `num_login` int(11) NOT NULL DEFAULT '0',
  `d_regis` varchar(14) NOT NULL DEFAULT '',
  `tokenID` varchar(200) DEFAULT NULL,
  `phoneID` varchar(60) DEFAULT NULL,
  `phoneVersion` varchar(20) DEFAULT NULL,
  `phoneBrand` varchar(20) DEFAULT NULL,
  `phoneModel` varchar(20) DEFAULT NULL,
  PRIMARY KEY (`memberuid`),
  KEY `site` (`site`),
  KEY `auth` (`auth`),
  KEY `level` (`level`),
  KEY `admin` (`admin`),
  KEY `email` (`email`),
  KEY `name` (`name`),
  KEY `nic` (`nic`),
  KEY `sex` (`sex`),
  KEY `d_regis` (`d_regis`)
) ENGINE=MyISAM DEFAULT CHARSET=utf8;


CREATE TABLE IF NOT EXISTS `member_id` (
  `uid` int(11) NOT NULL AUTO_INCREMENT,
  `site` int(11) NOT NULL DEFAULT '0',
  `id` varchar(50) NOT NULL DEFAULT '',
  `pw` varchar(50) NOT NULL DEFAULT '',
  `code` int(6) NOT NULL DEFAULT '0',
  `admin` int(2) NOT NULL DEFAULT '0',
  PRIMARY KEY (`uid`),
  UNIQUE KEY `id` (`id`),
  KEY `code` (`code`)
) ENGINE=MyISAM  DEFAULT CHARSET=utf8 AUTO_INCREMENT=1 ;


블로그 이미지

Link2Me

,
728x90

안드로이드에서 구현할 사항은 newToken 생성하여 DB에 저장되도록 하는 것, PUSH 메시지를 수신하면 팝업된 메시지 창을 누르면 해당 게시글로 이동하여 해당 게시글을 읽어서 내용을 확인하도록 하는 것이 주요 목적이다.


last Update : 2019.8.31

https://firebase.google.com/docs/cloud-messaging/android/client?authuser=0 를 참고하여 파일을 구현한다.


https://medium.com/android-school/firebaseinstanceidservice-is-deprecated-50651f17a148

FirebaseInstanceIdService is deprecated 내용이 나온다.

이걸 참조해서 newToken 생성하는 코드를 추가한다. (다음 게시글에 코드 추가했음)


AndroidManifest.xml 파일 수정사항

<service
    android:name=".FCMListenerService"
    android:stopWithTask="false">
    <intent-filter>
        <action android:name="com.google.firebase.MESSAGING_EVENT" />
    </intent-filter>
</service>
 


FCMListenerService.java 파일

public class FCMListenerService extends FirebaseMessagingService {
    private static final String TAG = "FCM";

    @Override
    public void onNewToken(String s) {
        super.onNewToken(s);
        Log.d(TAG, "token[" + s + "]" );
        /*
         * 기존의 FirebaseInstanceIdService에서 수행하던 토큰 생성, 갱신 등의 역할은 이제부터
         * FirebaseMessaging에 새롭게 추가된 위 메소드를 사용하면 된다.
         */
    }

    // [START receive_message]
    @Override
    public void onMessageReceived(RemoteMessage remoteMessage) {
        // 메시지를 받았을 때 동작하는 메소드
        String title = remoteMessage.getData().get("title");
        String message = remoteMessage.getData().get("message");
        Log.d(TAG, "From: " + remoteMessage.getFrom());
        Log.d(TAG, "Title: " + title);
        Log.d(TAG, "Message: " + message);

        NotificationHelper notificationHelper = new NotificationHelper(getApplicationContext());
        notificationHelper.createNotification(title,message);
    }
}


Android 8.0(API 수준 26) 이상부터는 알림 채널이 지원 및 권장된다. FCM은 기본적인 설정으로 기본 알림 채널을 제공한다.


NotificationHelper 는 https://link2me.tistory.com/1514 코드를 참조하면 된다.


===========================================================================

Update : 2017.9.2

FCM(Firebase Clouding Messaging) 을 서버(PHP)에서 PUSH 메시지를 보내면 안드로이드폰에서 받기 위해서는 관련 코드를 구현해야 한다.


GCM 에서 FCM 으로 Migrate 하는 방법에 대해서는 https://developers.google.com/cloud-messaging/android/android-migrate-fcm 에 자세히 설명되어 있다.


1. 토큰 등록 코드

파일명은 본인이 지정하고 싶은데로 지정하면 된다.

이 코드에 추가해서 member id 에 연계하여 토큰정보를 수집하도록 해도 되고, 로그인 코드에 추가해도 된다.


public class FCMInstanceIDListenerService extends FirebaseInstanceIdService {
private static final String TAG = "MyFirebaseIDService";

@Override
public void onTokenRefresh() {
    // Get updated InstanceID token.
    String token = FirebaseInstanceId.getInstance().getToken();
    Log.d(TAG, "Refreshed token: " + token);
}


2. FCM 메시지를 수신 받아서 처리하는 코드

    보통 메시지를 수신하면 메시지 제목만 팝업 메시지로 띄우고,

    클릭하면 메시지 내용을 볼 수 있는 UI로 이동하도록 코드를 구현한다.

 public class FCMListenerService extends com.google.firebase.messaging.FirebaseMessagingService {
    private static final String TAG = "FCM";

    int mLastId = 0;
    ArrayList<Integer> mActiveIdList = new ArrayList<Integer>();
    NotificationManager nm;

    // [START receive_message]
    @Override
    public void onMessageReceived(RemoteMessage remoteMessage) {
        // 메시지를 받았을 때 동작하는 메소드
        String title = remoteMessage.getData().get("title");
        String message = remoteMessage.getData().get("message");
        Log.d(TAG, "From: " + remoteMessage.getFrom());
        Log.d(TAG, "Title: " + title);
        Log.d(TAG, "Message: " + message);

        sendPushNotification(title,message);
    }

    private void createNotificationId() {
        int id = ++mLastId;
        mActiveIdList.add(id);
    }

    public void sendPushNotification(String title, String message) {
        nm = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
        nm.cancel(mLastId);
        createNotificationId();

        Uri defaultSoundUri= RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION);

        // 하위 호환성을 위해 NotificationCompat.Builder 사용

        NotificationCompat.Builder mBuilder = new NotificationCompat.Builder(this)
                .setSmallIcon(R.mipmap.icon)
                .setContentTitle(title)
                .setSound(defaultSoundUri)
                .setLights(000000255,500,2000)
                .setAutoCancel(true)
                .setWhen(System.currentTimeMillis())
                .setContentText(message);

        Intent popupIntent = new Intent(getApplicationContext(), Popup_Noti.class);
        popupIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TOP);
        popupIntent.putExtra("msg", title);
        popupIntent.putExtra("LastId", mLastId);
        startActivity(popupIntent); // 메시지 팝업창을 바로 띄운다.

        PendingIntent resultPendingIntent =PendingIntent.getActivity(this, 0, popupIntent, PendingIntent.FLAG_UPDATE_CURRENT);
        mBuilder.setContentIntent(resultPendingIntent);

        nm.notify(mLastId, mBuilder.build());
    }
}


getActivity(Context, int, Intent, int) -> Activity를 시작하는 인텐트를 생성함
getBroadcast(Context, int, Intent, int) -> BroadcastReceiver를 시작하는 인텐트를 생성함
getService(Context, int, Intent, int)  -> Service를 시작하는 인텐트를 생성함


사용자가 알림 창의 알림 텍스트를 클릭했을 때 Activity를 시작하려면, setContentIntent()를 호출하여 PendingIntent를 추가한다.


참조 : https://developer.android.com/guide/topics/ui/notifiers/notifications?hl=ko



 public class Popup_Noti extends Activity {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        requestWindowFeature(Window.FEATURE_NO_TITLE);
        // 키잠금 해제 및 화면 켜기
        getWindow().addFlags(WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED
                | WindowManager.LayoutParams.FLAG_DISMISS_KEYGUARD
                | WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON);

        AlertDialog.Builder builder = new AlertDialog.Builder(Popup_Noti.this);
        builder.setTitle(R.string.app_name);  // 앱의 이름
        builder.setMessage(getIntent().getStringExtra("msg")); // 넘겨받은 메시지 제목
        builder.setCancelable(false);
        builder.setPositiveButton("내용 보기", new DialogInterface.OnClickListener() {
            @Override
            public void onClick(DialogInterface dialog, int which) {
                Intent intent = new Intent(Popup_Noti.this, Notice.class); // 공지사항 정보 팝업 및 파싱처리
                startActivityForResult(intent, 0);
                NotificationManager nm = (NotificationManager) getSystemService(NOTIFICATION_SERVICE);
                nm.cancel(getIntent().getIntExtra("LastId", -1));
                finish();
            }
        });
        builder.setNegativeButton("닫기", new DialogInterface.OnClickListener() {
            @Override
            public void onClick(DialogInterface dialog, int which) {
                Intent intent = new Intent(Popup_Noti.this, Intro.class);
                intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP
                        | Intent.FLAG_ACTIVITY_SINGLE_TOP);
                startActivity(intent);
                finish();
            }
        });
        AlertDialog alertDialog = builder.create();
        alertDialog.show();
    }
}


3. AndroidManifest.xml 파일에 추가할 사항

 <service android:name="com.tistory.link2me.fcm.FCMListenerService">
    <intent-filter>
        <action android:name="com.google.firebase.MESSAGING_EVENT" />
    </intent-filter>
</service>
<service android:name="com.tistory.link2me.fcm.FCMInstanceIDListenerService">
    <intent-filter>
        <action android:name="com.google.firebase.INSTANCE_ID_EVENT" />
    </intent-filter>
</service>




블로그 이미지

Link2Me

,
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) 소스를 구현하면 된다.

블로그 이미지

Link2Me

,