오늘도 삽질중

AndroidQ 카메라 및 앨범예제 본문

안드로이드

AndroidQ 카메라 및 앨범예제

Choi3950 2020. 6. 18. 18:33
반응형

Android 버전 Q부터 저장소 정책이 변경되어

현재 많은 블로그에 있는 기존 카메라 및 앨범예제가 작동되지 않습니다.


저 또한 현재 서비스중인 앱에 카메라와 앨범기능이 존재하기 때문에

급하게 찾아봤지만 제가 원하는 카메라 및 앨범예제는 존재하지 않았습니다.


android:requestLegacyExternalStorage="true"


임시방편으로 위 플래그를 추가하면 기존 소스도 정상동작하지만 Q 이후 버전부터는 해당 플래그를 무시한다고 해서 대응을 해줘야 합니다.


실제로 위 플래그를 제거하면 Q 버전에서도 에러가 발생합니다.


그래서 정리목적으로 직접 샘플소스를 구현했습니다.

해당 소스는 APi 23레벨(6.0) 기종은 테스트가 안될수 있습니다.

단순히 SAF로 인해 바뀐 저장소 정책에 대응한 소스입니다.


변경된 로직의 핵심은 카메라 및 앨범으로 이미지를 선택시 별도의 캐시파일을 만들어서 업로드 및 이후 작업을 진행하게 됩니다.

그래서 반드시 캐시파일을 지워주는 로직이 필요합니다.


* 위 방법보다 더 좋은방법이나 효율적인 방법을 아시는 분은 댓글좀 부탁드립니다.

저는 도저희 못찾았습니다 ㅠㅠ




카메라 및 앨범 작업전에 해야되는 선행작업 2가지


AndroidManifest.xml

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

    //권한추가(저장소 권한 없이도 정상동작확인)
<uses-permission android:name="android.permission.CAMERA" />

<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"
tools:ignore="GoogleAppIndexingWarning">
<activity android:name=".AndroidQ_CameraAlbum_EX">
<intent-filter>
<action android:name="android.intent.action.MAIN" />

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

        //프로바이더 추가
<provider
android:name="androidx.core.content.FileProvider"
android:authorities="${applicationId}"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/provider_paths" />
</provider>


</application>

</manifest>



res -> xml(디렉토리생성) -> provider_paths.xml


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

<cache-path
name="cache"
path="." /> <!--Context.getCacheDir() 내부 저장소-->
<files-path
name="files"
path="." /> <!--Context.getFilesDir() 내부 저장소-->

<external-path
name="external"
path="."/> <!-- Environment.getExternalStorageDirectory() 외부 저장소-->
<external-cache-path
name="external-cache"
path="."/> <!-- Context.getExternalCacheDir() 외부 저장소-->
<external-files-path
name="external-files"
path="."/> <!-- Context.getExternalFilesDir() 외부 저장소-->


</paths>


보통 위 2가지는 다 하셨을꺼라 생각합니다.

이건 여러 블로그에도 내용이 많으니 궁금하신분은 찾아보시면 됩니다.




위 2가지 다 하셨다면 반드시 app Level build.gradle에 추가해주세요.

파일스트림을 복사하는 IOUtils을 사용했습니다.

implementation 'commons-io:commons-io:2.4'


AndroidQ_CameraAlubm.xml

단순히 버튼 3개와 이미지뷰만 들어가 있습니다.

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">

<ImageView
android:id="@+id/iamgeView"
android:layout_width="300dp"
android:layout_height="300dp"
android:layout_centerInParent="true" />


<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_alignParentBottom="true">

<Button
android:id="@+id/btnCamera"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="카메라" />

<Button
android:id="@+id/btnAlbum"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1"
android:text="앨범" />

<Button
android:id="@+id/btnCacheClear"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1"
android:text="캐시파일제거" />

</LinearLayout>

</RelativeLayout>





AndroidQ_CameraAlbum_EX.java

소스가 길지만 반복 소스가 일부분 존재하고 , 주석을 잘 참고하시면 도움이 될꺼라 생각합니다.


public class AndroidQ_CameraAlbum_EX extends AppCompatActivity implements View.OnClickListener {

private static final int REQUEST_CAMERA = 100;
private static final int REQUEST_ALBUM = 101;

//서버에 이미지 업로드를 파일로 하는 경우 해당 cacheFile 로 업로드 요청을 합니다.
// 흔히 RealPath 라 불리는 경로로 보내면 퍼미션 에러가 나서 업로드 진행이 안됩니다.
private String cacheFilePath = null;

ImageView imageView;
Button btnCamera;
Button btnAlbum;
Button btnCacheClear;

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

imageView = findViewById( R.id.iamgeView );
btnCamera = findViewById( R.id.btnCamera );
btnAlbum = findViewById( R.id.btnAlbum );
btnCacheClear = findViewById( R.id.btnCacheClear );

btnCamera.setOnClickListener( this );
btnAlbum.setOnClickListener( this );
btnCacheClear.setOnClickListener( this );
}

@Override
public void onClick( View v ) {
if ( v == btnCamera ) {
//권한요청
requestPermissions( new String[]{ Manifest.permission.CAMERA }, REQUEST_CAMERA );
} else if ( v == btnAlbum ) {
onAlbum( REQUEST_ALBUM );
} else if ( v == btnCacheClear ) {
//캐시파일에 존재하는 카메라 및 앨범 캐시이미지 제거
cacheDirFileClear( );
}
}

@Override
public void onRequestPermissionsResult( int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults ) {
super.onRequestPermissionsResult( requestCode, permissions, grantResults );
if ( requestCode == REQUEST_CAMERA ) {
for ( int g : grantResults ) {
if ( g == PackageManager.PERMISSION_DENIED ) {
//권한거부
return;
}
}
//임시파일 생성
File file = createImgCacheFile( );
cacheFilePath = file.getAbsolutePath( );
//카메라 호출
onCamera( REQUEST_CAMERA, file );
}
}

@Override
protected void onActivityResult( int requestCode, int resultCode, @Nullable Intent data ) {
super.onActivityResult( requestCode, resultCode, data );
if ( requestCode == REQUEST_CAMERA && resultCode == RESULT_OK ) {

AlbumAdd( cacheFilePath );
imageView.setImageBitmap( getBitmapCamera( imageView, cacheFilePath ) );

} else if ( requestCode == REQUEST_ALBUM && resultCode == RESULT_OK ) {

Uri albumUri = data.getData( );
String fileName = getFileName( albumUri );
try {

ParcelFileDescriptor parcelFileDescriptor = getContentResolver( ).openFileDescriptor( albumUri, "r" );
if ( parcelFileDescriptor == null ) return;
FileInputStream inputStream = new FileInputStream( parcelFileDescriptor.getFileDescriptor( ) );
File cacheFile = new File( this.getCacheDir( ), fileName );
FileOutputStream outputStream = new FileOutputStream( cacheFile );
IOUtils.copy( inputStream, outputStream );

cacheFilePath = cacheFile.getAbsolutePath( );

imageView.setImageBitmap( getBitmapAlbum( imageView, albumUri ) );

} catch ( Exception e ) {
e.printStackTrace( );
}

} else if ( requestCode == REQUEST_CAMERA && resultCode == RESULT_CANCELED ) {
fileDelete( cacheFilePath );
cacheFilePath = null;
}
}

/**
* 카메라 및 앨범관련 작업함수
*/
//캐시파일 생성
public File createImgCacheFile( ) {
File cacheFile = new File( getCacheDir( ), new SimpleDateFormat( "yyyyMMdd_HHmmss", Locale.US ).format( new Date( ) ) + ".jpg" );
return cacheFile;
}

//카메라 호출
public void onCamera( int requestCode, File createTempFile ) {
Intent intent = new Intent( MediaStore.ACTION_IMAGE_CAPTURE );
if ( intent.resolveActivity( getPackageManager( ) ) != null ) {
if ( createTempFile != null ) {
Uri photoURI = FileProvider.getUriForFile( this, BuildConfig.APPLICATION_ID, createTempFile );
intent.putExtra( MediaStore.EXTRA_OUTPUT, photoURI );
startActivityForResult( intent, requestCode );
}
}
}

//앨범 호출
public void onAlbum( int requestCode ) {
Intent intent = new Intent( Intent.ACTION_PICK );
intent.setType( MediaStore.Images.Media.CONTENT_TYPE );
startActivityForResult( intent, requestCode );
}

//앨범 저장
public void AlbumAdd( String cacheFilePath ) {
if ( cacheFilePath == null ) return;
BitmapFactory.Options options = new BitmapFactory.Options( );
ExifInterface exifInterface = null;

try {
exifInterface = new ExifInterface( cacheFilePath );
} catch ( Exception e ) {
e.printStackTrace( );
}

int exifOrientation;
int exifDegree = 0;

//사진 회전값 구하기
if ( exifInterface != null ) {
exifOrientation = exifInterface.getAttributeInt( ExifInterface.TAG_ORIENTATION, ExifInterface.ORIENTATION_NORMAL );

if ( exifOrientation == ExifInterface.ORIENTATION_ROTATE_90 ) {
exifDegree = 90;
} else if ( exifOrientation == ExifInterface.ORIENTATION_ROTATE_180 ) {
exifDegree = 180;
} else if ( exifOrientation == ExifInterface.ORIENTATION_ROTATE_270 ) {
exifDegree = 270;
}
}

Bitmap bitmap = BitmapFactory.decodeFile( cacheFilePath, options );
Matrix matrix = new Matrix( );
matrix.postRotate( exifDegree );

Bitmap exifBit = Bitmap.createBitmap( bitmap, 0, 0, bitmap.getWidth( ), bitmap.getHeight( ), matrix, true );

ContentValues values = new ContentValues( );
//실제 앨범에 저장될 이미지이름
values.put( MediaStore.Images.Media.DISPLAY_NAME, new SimpleDateFormat( "yyyyMMdd_HHmmss", Locale.US ).format( new Date( ) ) + ".jpg" );
values.put( MediaStore.Images.Media.MIME_TYPE, "image/*" );
//저장될 경로
values.put( MediaStore.Images.Media.RELATIVE_PATH, "DCIM/AndroidQ" );
values.put( MediaStore.Images.Media.ORIENTATION, exifDegree );
values.put( MediaStore.Images.Media.IS_PENDING, 1 );

Uri u = MediaStore.Images.Media.getContentUri( MediaStore.VOLUME_EXTERNAL );
Uri uri = getContentResolver( ).insert( u, values );

try {
ParcelFileDescriptor parcelFileDescriptor = getContentResolver( ).openFileDescriptor( uri, "w", null );
if ( parcelFileDescriptor == null ) return;

ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream( );
exifBit.compress( Bitmap.CompressFormat.JPEG, 100, byteArrayOutputStream );
byte[] b = byteArrayOutputStream.toByteArray( );
InputStream inputStream = new ByteArrayInputStream( b );

ByteArrayOutputStream buffer = new ByteArrayOutputStream( );
int bufferSize = 1024;
byte[] buffers = new byte[ bufferSize ];

int len = 0;
while ( ( len = inputStream.read( buffers ) ) != -1 ) {
buffer.write( buffers, 0, len );
}

byte[] bs = buffer.toByteArray( );
FileOutputStream fileOutputStream = new FileOutputStream( parcelFileDescriptor.getFileDescriptor( ) );
fileOutputStream.write( bs );
fileOutputStream.close( );
inputStream.close( );
parcelFileDescriptor.close( );

getContentResolver( ).update( uri, values, null, null );

} catch ( Exception e ) {
e.printStackTrace( );
}

values.clear( );
values.put( MediaStore.Images.Media.IS_PENDING, 0 );
getContentResolver( ).update( uri, values, null, null );
}

//이미지뷰에 뿌려질 앨범 비트맵 반환
public Bitmap getBitmapAlbum( View targetView, Uri uri ) {
try {
ParcelFileDescriptor parcelFileDescriptor = getContentResolver( ).openFileDescriptor( uri, "r" );
if ( parcelFileDescriptor == null ) return null;
FileDescriptor fileDescriptor = parcelFileDescriptor.getFileDescriptor( );
if ( fileDescriptor == null ) return null;

int targetW = targetView.getWidth( );
int targetH = targetView.getHeight( );

BitmapFactory.Options options = new BitmapFactory.Options( );
options.inJustDecodeBounds = true;

BitmapFactory.decodeFileDescriptor( fileDescriptor, null, options );

int photoW = options.outWidth;
int photoH = options.outHeight;

int scaleFactor = Math.min( photoW / targetW, photoH / targetH );
if ( scaleFactor >= 8 ) {
options.inSampleSize = 8;
} else if ( scaleFactor >= 4 ) {
options.inSampleSize = 4;
} else {
options.inSampleSize = 2;
}
options.inJustDecodeBounds = false;

Bitmap reSizeBit = BitmapFactory.decodeFileDescriptor( fileDescriptor, null, options );

ExifInterface exifInterface = null;
try {
if ( Build.VERSION.SDK_INT >= Build.VERSION_CODES.N ) {
exifInterface = new ExifInterface( fileDescriptor );
}
} catch ( IOException e ) {
e.printStackTrace( );
}

int exifOrientation;
int exifDegree = 0;

//사진 회전값 구하기
if ( exifInterface != null ) {
exifOrientation = exifInterface.getAttributeInt( ExifInterface.TAG_ORIENTATION, ExifInterface.ORIENTATION_NORMAL );

if ( exifOrientation == ExifInterface.ORIENTATION_ROTATE_90 ) {
exifDegree = 90;
} else if ( exifOrientation == ExifInterface.ORIENTATION_ROTATE_180 ) {
exifDegree = 180;
} else if ( exifOrientation == ExifInterface.ORIENTATION_ROTATE_270 ) {
exifDegree = 270;
}
}

parcelFileDescriptor.close( );
Matrix matrix = new Matrix( );
matrix.postRotate( exifDegree );

Bitmap reSizeExifBitmap = Bitmap.createBitmap( reSizeBit, 0, 0, reSizeBit.getWidth( ), reSizeBit.getHeight( ), matrix, true );
return reSizeExifBitmap;

} catch ( Exception e ) {
e.printStackTrace( );
return null;
}
}

//이미지뷰에 뿌려질 카메라 비트맵 반환
public Bitmap getBitmapCamera( View targetView, String filePath ) {
int targetW = targetView.getWidth( );
int targetH = targetView.getHeight( );

// Get the dimensions of the bitmap
BitmapFactory.Options bmOptions = new BitmapFactory.Options( );
bmOptions.inJustDecodeBounds = true;

BitmapFactory.decodeFile( filePath, bmOptions );

int photoW = bmOptions.outWidth;
int photoH = bmOptions.outHeight;

double scaleFactor = Math.min( photoW / targetW, photoH / targetH );
if ( scaleFactor >= 8 ) {
bmOptions.inSampleSize = 8;
} else if ( scaleFactor >= 4 ) {
bmOptions.inSampleSize = 4;
} else {
bmOptions.inSampleSize = 2;
}


bmOptions.inJustDecodeBounds = false;

Bitmap originalBitmap = BitmapFactory.decodeFile( filePath, bmOptions );

ExifInterface exifInterface = null;
try {
if ( Build.VERSION.SDK_INT >= Build.VERSION_CODES.N ) {
exifInterface = new ExifInterface( filePath );
}
} catch ( IOException e ) {
e.printStackTrace( );
}

int exifOrientation;
int exifDegree = 0;

//사진 회전값 구하기
if ( exifInterface != null ) {
exifOrientation = exifInterface.getAttributeInt( ExifInterface.TAG_ORIENTATION, ExifInterface.ORIENTATION_NORMAL );

if ( exifOrientation == ExifInterface.ORIENTATION_ROTATE_90 ) {
exifDegree = 90;
} else if ( exifOrientation == ExifInterface.ORIENTATION_ROTATE_180 ) {
exifDegree = 180;
} else if ( exifOrientation == ExifInterface.ORIENTATION_ROTATE_270 ) {
exifDegree = 270;
}
}

Matrix matrix = new Matrix( );
matrix.postRotate( exifDegree );

Bitmap reSizeExifBitmap = Bitmap.createBitmap( originalBitmap, 0, 0, originalBitmap.getWidth( ), originalBitmap.getHeight( ), matrix, true );
return reSizeExifBitmap;

}

//앨범에서 선택한 사진이름 가져오기
public String getFileName( Uri uri ) {
Cursor cursor = getContentResolver( ).query( uri, null, null, null, null );
try {
if ( cursor == null ) return null;
cursor.moveToFirst( );
String fileName = cursor.getString( cursor.getColumnIndex( OpenableColumns.DISPLAY_NAME ) );
cursor.close( );
return fileName;

} catch ( Exception e ) {
e.printStackTrace( );
cursor.close( );
return null;
}
}

//파일삭제
public void fileDelete( String filePath ) {
if ( filePath == null ) return;
try {
File f = new File( filePath );
if ( f.exists( ) ) {
f.delete( );
}
} catch ( Exception e ) {
e.printStackTrace( );
}
}

//실제 앨범경로가 아닌 앱 내에 캐시디렉토리에 존재하는 이미지 캐시파일삭제
//확장자 .jpg 필터링해서 제거
public void cacheDirFileClear( ) {
File cacheDir = new File( getCacheDir( ).getAbsolutePath( ) );
File[] cacheFiles = cacheDir.listFiles( new FileFilter( ) {
@Override
public boolean accept( File pathname ) {
return pathname.getName( ).endsWith( "jpg" );
}
} );
if ( cacheFiles == null ) return;
for ( File c : cacheFiles ) {
fileDelete( c.getAbsolutePath( ) );
}
}
}



실행결과




카메라로 사진을 찍고 저장하면 

앨범에 AndroidQ 라는 폴더가 생성되어 사진이 저장되있습니다.




코틀린은 나중에 귀찮..


반응형
Comments