카메라가 하드웨어를 조작하는 일이다보니

기기별로 많은 동작의 차이를 보이더이다.

하여 기기별로 식별해서 다른 소스를 줄까도 생각했지만 최대한 하나의 소스로 호환성을 맞춰보자는 취지로 연구를 했지요.

물론 모든 기기에 호환되지 않지만 꽤 대다수의 기기에서 호환되는 것으로 집계되고 있습니다. (자체 집계;;;)


본 소스는 단영님의 

카메라를 이용하여 사진찍고, Bitmap 받아오고 ImageView에 뿌리기

와 위 글에 달린 청바지님의 댓글에 의해 완성되었습니다.

잠깐 설명을 드리자면 Bitmap으로 받아올 경우 썸네일을 반환하는 기기가 있고,
MediaStore.EXTRA_OUTPUT은 정말 원문대로 기기별로 멋대로 동작하는 이유로
청바지님의 소스를 이용하여 EXTRA_OUTPUT없이 진행을 했고요.
마지막에 저장된 DB의 레코드를 읽어와 Uri를 취한 후 파일을 복사(앱 내에서 쓸 임시 파일용)하여
사용하고 있습니다.

우선 특정 이벤트시 카메라 인텐트를 호출합니다.

Intent cameraIntent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
startActivityForResult(cameraIntent,CAMERA);
//CAMERA는 상수로서, onActivityResult에서 requestCode로 돌려받을 때 사용되는 정수값입니다. 단순히 구분을 위함이니
//여러분들 마음대로 사용하시면 되겠습니다.

그럼 카메라 화면이 켜집니다.

여기서 사진을 찍으면 잠시 후 저장하는 화면이 나옵니다.(갤럭시S나 몇몇 기종은 그렇고, 몇몇 기종은 찍은 상태에서 바로 저장, 첨부, 확인 등의 버튼을 눌러 나옵니다.)


그럼 onActivityResult 화면으로 가게 됩니다.

여기서 마지막에 DB에 저장된 레코드(최신 레코드)를 불러옵니다.


 protected void onActivityResult(int requestCode, int resultCode, Intent data) {
	Intent intent;
	File toFile = null; //임시파일을 복사할 경로를 지정하기 위해 선언
	File fromFile = null; //불러올 파일의 경로를 저장하기 위해 선언
	FileInputStream inputStream = null; //임시파일을 복사할 때 원본 파일을 읽어오기 위해 선언
	FileOutputStream outputStream = null; //임시파일을 복사할 때 원본 파일을 임시파일에 쓰기 위해 선언
	switch (requestCode) { //여기서 아까 인텐트를 부를 때 보냈던 CAMERA라는 상수가 requestCode에 실려 되돌아옵니다.
	case CAMERA:
		if (resultCode == RESULT_CANCELED) return; //만약 사용자가 촬영을 안했거나 다른 이유에 의해 인텐트가 예기치
		//않게 종료되었을 경우 아래의 과정을 무시한채 종료합니다.

		if (cropedImageUri != null) { //만약 복사하려는 임시파일이 존재하면 삭제합니다. (임시파일이 쌓이는 것을 방지)
		//이 옵션은 선택입니다. 임시파일의 경로를 상수로 두셨다면 상관 없을지도 모릅니다.
		//저는 실행할 때마다 임시파일의 파일명이 바뀌기에 이와 같이 합니다.
			File f = new File(cropedImageUri.getPath());
			if (f.exists()) {
				f.delete();
			}
		}
		//넥서스S에서 Uri를 제대로 얻어오질 못하는데, 이 부분을 수정하면 가능 할지도 모르리라 의심만 해봅니다;
		//우리가 찾고자 하는 마지막에 등록된 레코드가 있을 테이블입니다.
		Uri uriImages = MediaStore.Images.Media.EXTERNAL_CONTENT_URI;
		int id = -1; //촬영된 이미지의 DB 테이블상의 ID를 저장할 변수입니다.

		//이는 DB의 해당 테이블에서 조건에 맞는 레코드의 어떤 필드값을 불러올지를 지정해줍니다.
		//우리는 해당 레코드의 모든 필드값이 필요한 것이 아니기에 이렇게 한정합니다.
		String[] IMAGE_PROJECTION = {
		MediaStore.Images.ImageColumns.DATA, //데이터의 파일 절대 경로(문자열형)
		MediaStore.Images.ImageColumns._ID, //데이터의 테이블상의 아이디(정수형)
	};
	try {
		//테이블에서 조건 없이 필드값을 한정해서 불러옵니다. 그럼, 갤러리에 보이는 사진이 100개면
		//Cursor는 100번이나 움직일 수 있는겁니다. 제가 맞나요?
		Cursor cursorImages = getContentResolver().query(uriImages, IMAGE_PROJECTION, null, null, null);
		//cursor가 무리없이 열렸고, 마지막으로 이동되었으면 경로를 얻어옵니다.
		if (cursorImages != null && cursorImages.moveToLast()) {
			fromFile = new File(cursorImages.getString(0)); //이 것이 경로
			id = cursorImages.getInt(1); //이 것이 아이디입니다.
			//사실 아이디는 필요 없을지 모르지만, 저 같은 경우는 원본을 남기지 않기 때문에 이 아이디가 필요합니다.
			cursorImages.close(); //커서 사용이 끝나면 꼭 닫아줍시다.
		}
	} catch(Exception e) {
		e.printStackTrace();
	}
	//임시파일의 경로로서, 저같은 경우는 겹치지 않게 아래와 같이 했습니다. 불필요한 작업일지도 모르나 우선 패스;
	toFile = new File(Environment.getExternalStorageDirectory().getAbsolutePath() + "/" + System.currentTimeMillis() + ".jpg");

	try {
		//이 부분이 파일을 복사하는 부분입니다.
		//이 부분이 없으면 CROP할 때 SK W폰에서 다른 이미지를 불러옵니다.
		//이유는 모르지만 원본이 소실되거나 DB에서 소실되는 것 같습니다.
		inputStream = new FileInputStream(fromFile);
		outputStream = new FileOutputStream(toFile);
		FileChannel fcin = inputStream.getChannel();
		FileChannel fcout = outputStream.getChannel();

		long size = fcin.size();

		fcin.transferTo(0, size, fcout);

		fcout.close();
		fcin.close();
		outputStream.close();
		inputStream.close();
	} catch (FileNotFoundException e) {
		// TODO Auto-generated catch block
		e.printStackTrace();
	} catch (IOException e) {
		// TODO Auto-generated catch block
		e.printStackTrace();
	}
	//그리고 원본은 지웁니다.
	fromFile.delete();
	//DB상에 남아있는 원본 레코드도 지웁니다. 그렇지 않으면 파일만 지워졌기 때문에 갤러리 등에 들어가면
	//검은색 썸네일 또는 망가진 이미지 아이콘의 썸네일이 뜹니다. (미관상 안좋음)
	getBaseContext().getContentResolver().delete(MediaStore.Images.Media.EXTERNAL_CONTENT_URI,										MediaStore.Images.ImageColumns._ID + "=" + id, null);
	//임시파일 경로를 Uri로 변환합니다.
	cropedImageUri = Uri.fromFile(toFile);
	//그리고 크롭을 실행합니다.
	intent = new Intent("com.android.camera.action.CROP");
	intent.setDataAndType(cropedImageUri, "image/*");
	//intent.putExtra("outputX", GlobalData.dip(this, 460));
	//intent.putExtra("outputY", GlobalData.dip(this, 345));
	//intent.putExtra("aspectX", 4);
	//intent.putExtra("aspectY", 3);
	intent.putExtra("scale", true);
	//output의 경로가 임시파일과 같기 때문에 잘린 이미지는 임시파일으 덮어쓰게 되어있습니다.
	intent.putExtra("output", cropedImageUri);
	startActivityForResult(intent, CROP); //CROP역시 클래스 내에서 임의로 만든 상수입니다.
	break;
	case : CROP
	
	//...이하 생략


위에서 사용된 파일 복사 과정은 서비님의 블로그를 참고했습니다.

자바 파일복사 코드와 성능 1 :: Java File Copy Code & Perfomance Issue. part 1

자바 파일복사 코드와 성능 2 :: Java File Copy Code & Perfomance Issue. part 2

마지막 즈음에 startActivityForResult(intent, CROP); 을 하게 되면 자르기 인텐트가 열립니다.

다음 소스는 위 소스의 case : CROP에 이어서 생각하시면 됩니다.

	case CROP: 
		if (resultCode == RESULT_OK)
		{
			//저는 잘린 이미지를 특정 View에 넣어야 하기 때문이 이와 같이 작업했습니다.
			//cropedImageUri는 클래스 변수이므로 선언이나 정의 없이 바로 사용할 수 있습니다.
			cameraButton.setImageURI(cropedImageUri);
		}
		break;
	}
}


이와 같이 했을 때 모든 기기를 테스트해 본 것은 아니지만 대부분의 기기에서 정상작동 하였고,

현재 확인된 문제의 기기는

넥서스S : 촬영 후 자르기로 가면 다른 이미지가 들어와 있습니다.

아트릭스 : 활영 후 자르기로 가면 회전 보정이 되어 있지 않고, 잘려서 View에는 정상적으로 들어가나 홈 배경화면이 촬영한 화면으로 바뀝니다. (CROP으로 갈 때의 문제로 보임)


테스트폰은 없어서 아직 문제는 공존합니다 -_-; 마켓에 올라갔는데 어뜨케 ㅠ


여튼 이러한 방법으로 사용중입니다.

혹시 더 나은 방법이나 호환성에 대해 해결방법 아시는 분 계시면 공유 부탁드립니다!

lifecluee@gmail.com