본문 바로가기

[Android]/허접 Programming Tips

Android 7.0 Nougat OS 이미지 사진 촬영 캡처 및 자르기 (2/2)


어이없는 Android 7.0 NOS 사진 촬영, 캡처 및 자르기.

아래 포스트에서 Uri 관련하여 보안이 강화되어 기존 인터넷에 떠도는 코드를 사용하여 해당 기능을 구현하기는 무리가 있습니다.

저와 같은 뻘(?)짓 혹은 어려움을 겪는 분들을 위해 코드를 공유해드립니다.



제가 실력이 부족하여 하루에 한 ~ 두시간씩 투자해서 약 2주간 고생해서 완성한 코드를 공유드리도록 합니다.

진심 이방법, 저방법을 이용하며 알아낸 방법입니다. 저도 사실 이 코드가 왜 완벽히 되는지는 더 공부해야 될 것 같네요ㅜ.



1) onActivityResult를 위해 사용할 변수 선언


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


2) Uri 변수 전역변수로 선언! 지역변수로 선언하여 각 함수에다가 넘겨주고 return하는 형태로 하려다가 머리 터질 것 같아

쉬운 방식으로 가겠습니다.

Uri photoUri; 


3) 번외. Android MOS(마쉬멜로우 운영체제)부터는 위험한 권한을 사용함에 있어 사용자에게 허락(?)을 받아야 합니다. 그래서 아래와 같은 작업을 해줍니다.
Camera와 File System에 접근하기 위해서는 읽고 / 쓰기 즉, 아래와 같은 권한이 필요합니다.
READ_EXTERNAL_STORAGE, WRITE_EXTERNAL_STORAGE, CAMERA


private String[] permissions = {Manifest.permission.READ_EXTERNAL_STORAGE, Manifest.permission.WRITE_EXTERNAL_STORAGE, Manifest.permission.CAMERA}; //권한 설정 변수
private static final int MULTIPLE_PERMISSIONS = 101; //권한 동의 여부 문의 후 CallBack 함수에 쓰일 변수

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.dlg_select_photo);
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()) { //권한이 추가되었으면 해당 리스트가 empty가 아니므로 request 즉 권한을 요청합니다. ActivityCompat.requestPermissions(this, permissionList.toArray(new String[permissionList.size()]), MULTIPLE_PERMISSIONS); return false; } return true; }

//아래는 권한 요청 Callback 함수입니다. PERMISSION_GRANTED로 권한을 획득했는지 확인할 수 있습니다. 아래에서는 !=를 사용했기에 //권한 사용에 동의를 안했을 경우를 if문으로 코딩되었습니다. @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[0])) { if (grantResults[i] != PackageManager.PERMISSION_GRANTED) { showNoPermissionToastAndFinish(); } } else if (permissions[i].equals(this.permissions[1])) { if (grantResults[i] != PackageManager.PERMISSION_GRANTED) { showNoPermissionToastAndFinish(); } } else if (permissions[i].equals(this.permissions[2])) { if (grantResults[i] != PackageManager.PERMISSION_GRANTED) { showNoPermissionToastAndFinish(); } } } } else { showNoPermissionToastAndFinish(); } return; } } }

//권한 획득에 동의를 하지 않았을 경우 아래 Toast 메세지를 띄우며 해당 Activity를 종료시킵니다. private void showNoPermissionToastAndFinish() { Toast.makeText(this, "권한 요청에 동의 해주셔야 이용 가능합니다. 설정에서 권한 허용 하시기 바랍니다.", Toast.LENGTH_SHORT).show(); finish(); }



4) Camera로 사진 찍기



 private void takePhoto() {
    Intent intent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE); //사진을 찍기 위하여 설정합니다.
    File photoFile = null;
    try {
        photoFile = createImageFile();
    } catch (IOException e) {
        Toast.makeText(SelectPhotoDialogActivity.this, "이미지 처리 오류! 다시 시도해주세요.", Toast.LENGTH_SHORT).show();              finish();
    }
    if (photoFile != null) {
        photoUri = FileProvider.getUriForFile(SelectPhotoDialogActivity.this,
                "com.example.test.provider", photoFile); //FileProvider의 경우 이전 포스트를 참고하세요.
        intent.putExtra(MediaStore.EXTRA_OUTPUT, photoUri); //사진을 찍어 해당 Content uri를 photoUri에 적용시키기 위함
        startActivityForResult(intent, PICK_FROM_CAMERA);
    }
 }

 // Android M에서는 Uri.fromFile 함수를 사용하였으나 7.0부터는 이 함수를 사용할 시 FileUriExposedException이
 // 발생하므로 아래와 같이 함수를 작성합니다. 이전 포스트에 참고한 영문 사이트를 들어가시면 자세한 설명을 볼 수 있습니다.

 private File createImageFile() throws IOException {
    // Create an image file name
    String timeStamp = new SimpleDateFormat("HHmmss").format(new Date());
    String imageFileName = "IP" + timeStamp + "_";
    File storageDir = new File(Environment.getExternalStorageDirectory() + "/test/"); //test라는 경로에 이미지를 저장하기 위함
    if (!storageDir.exists()) {
        storageDir.mkdirs();
    }
    File image = File.createTempFile(
            imageFileName,
            ".jpg",
            storageDir
    );
    return image;
 }




5) 앨범에서 이미지 선택


private void goToAlbum() {

Intent intent = new Intent(Intent.ACTION_PICK); //ACTION_PICK 즉 사진을 고르겠다!
intent.setType(MediaStore.Images.Media.CONTENT_TYPE);
startActivityForResult(intent, PICK_FROM_ALBUM); } 


6) onActivityResult 콜백 함수 및 가장 중요한 Crop 자르기 함수 부분


 @Override
 protected void onActivityResult(int requestCode, int resultCode, Intent data) {
    if (resultCode != RESULT_OK) {
        Toast.makeText(SelectPhotoDialogActivity.this, "이미지 처리 오류! 다시 시도해주세요.", Toast.LENGTH_SHORT).show();
    }
    if (requestCode == PICK_FROM_ALBUM) {
        if(data==null){
            return;
        }
        photoUri = data.getData();
        cropImage();
    } else if (requestCode == PICK_FROM_CAMERA) {
        cropImage();
        MediaScannerConnection.scanFile(SelectPhotoDialogActivity.this, //앨범에 사진을 보여주기 위해 Scan을 합니다.
                new String[]{photoUri.getPath()}, null,
                new MediaScannerConnection.OnScanCompletedListener() {
                    public void onScanCompleted(String path, Uri uri) {
                    }
                });
    } else if (requestCode == CROP_FROM_CAMERA) {
        try { //저는 bitmap 형태의 이미지로 가져오기 위해 아래와 같이 작업하였으며 Thumbnail을 추출하였습니다.

            Bitmap bitmap = MediaStore.Images.Media.getBitmap(this.getContentResolver(), photoUri);
            Bitmap thumbImage = ThumbnailUtils.extractThumbnail(bitmap, 128, 128);
            ByteArrayOutputStream bs = new ByteArrayOutputStream();
            thumbImage.compress(Bitmap.CompressFormat.JPEG, 100, bs); //이미지가 클 경우 OutOfMemoryException 발생이 예상되어 압축
            

            //여기서는 ImageView에 setImageBitmap을 활용하여 해당 이미지에 그림을 띄우시면 됩니다.

            mImageView.setImageBitmap(thumbImage);
        } catch (Exception e) {
            Log.e("ERROR", e.getMessage().toString());
        }
    }
 }


//Android N crop image (이 부분에서 몇일동안 정신 못차렸습니다 ㅜ)

//모든 작업에 있어 사전에 FALG_GRANT_WRITE_URI_PERMISSION과 READ 퍼미션을 줘야 uri를 활용한 작업에 지장을 받지 않는다는 것이 핵심입니다.
public void cropImage() {
this.grantUriPermission("com.android.camera", photoUri,
Intent.FLAG_GRANT_WRITE_URI_PERMISSION | Intent.FLAG_GRANT_READ_URI_PERMISSION);
Intent intent = new Intent("com.android.camera.action.CROP");
intent.setDataAndType(photoUri, "image/*");

List<ResolveInfo> list = getPackageManager().queryIntentActivities(intent, 0);
grantUriPermission(list.get(0).activityInfo.packageName, photoUri,
Intent.FLAG_GRANT_WRITE_URI_PERMISSION | Intent.FLAG_GRANT_READ_URI_PERMISSION);
int size = list.size();
if (size == 0) {
Toast.makeText(this, "취소 되었습니다.", Toast.LENGTH_SHORT).show();
return;
} else {
Toast.makeText(this, "용량이 큰 사진의 경우 시간이 오래 걸릴 수 있습니다.", Toast.LENGTH_SHORT).show();
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
intent.addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
intent.putExtra("crop", "true");
intent.putExtra("aspectX", 4);
intent.putExtra("aspectY", 3);
intent.putExtra("scale", true);
File croppedFileName = null;
try {
croppedFileName = createImageFile();
} catch (IOException e) {
e.printStackTrace();
}

File folder = new File(Environment.getExternalStorageDirectory() + "/test/");
File tempFile = new File(folder.toString(), croppedFileName.getName());

photoUri = FileProvider.getUriForFile(SelectPhotoDialogActivity.this,
"com.example.test.provider", tempFile);

intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
intent.addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION);


intent.putExtra("return-data", false);
intent.putExtra(MediaStore.EXTRA_OUTPUT, photoUri);
intent.putExtra("outputFormat", Bitmap.CompressFormat.JPEG.toString()); //Bitmap 형태로 받기 위해 해당 작업 진행

Intent i = new Intent(intent);
ResolveInfo res = list.get(0);
i.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
i.addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
grantUriPermission(res.activityInfo.packageName, photoUri,
Intent.FLAG_GRANT_WRITE_URI_PERMISSION | Intent.FLAG_GRANT_READ_URI_PERMISSION);

i.setComponent(new ComponentName(res.activityInfo.packageName, res.activityInfo.name));
startActivityForResult(i, CROP_FROM_CAMERA);


}

}

CropImage()부분에 모든 intent에 FLAG 값들을 넣어주었습니다. uri를 이용하여 작업을 하려고 하면 항상 저 권한을 넣어줘야 작동이 되는 것을 알게 되었습니다. 사실 정확한 이유는 결국 파일 시스템 보안 강화 때문이겠지만, 정확이 모든 부분에 저렇게 넣어야 하는지는 아직도 긴가민가합니다. 현재 인터넷에 나와있는 코드들이 대부분 7.0 이전에 이미지 처리 코드들이기 때문에 혹시라도 이 코드가 도움이 될까해서 올립니다. 


혹시라도 문제가 있거나 작동이 되지 않거나 더 많은 정보가 있을 시 질문 남겨주세요~:) 그리고 이 코드는 전체 코드 중에 일부이니 전체 xml부터 버튼 구현까지의 코드가 필요하시면 알려주시기 바랍니다.



[예시화면]





전체 프로젝트 및 코드는 아래 Github에서 받아보실 수 있습니다.

GitHub : https://github.com/DJDrama/CameraNOSTest