콘텐츠 프로바이더 구조
콘텐츠 프로바이더는 앱 간의 데이터 공유를 목적으로 사용되는 컴포넌트이다.
데이터를 가지는 A라는 앱이 있고 이 앱의 데이터를 이용해야 하는 B라는 앱이 있다.
A 앱이 가지고 있는 데이터는 파일 데이터, 데이터베이스, Preference등 다양하다.
이 데이터를 B앱에서 데이터에 직접 접근할 수 있다면 보안상 큰 문제가 된다.
물론 파일 데이터가 외장 메모리 공간에 저장되어 있다면 다른 앱에서도 경로와 파일명만 알면
얼마든지 접근할 수 있다.
하지만 여기서 이야기하는 파일, 데이터베이스, Preference는 모두 내장 메모리 공간에 저장된 데이터이다.
위 데이터들은 앱의 패키지명으로 된 디렉터리에 저장되므로 위부 앱이 접근할 수 없다.
하지만 콘텐츠 프로바이더를 이용하면 접근할 수 있다.
B앱에서 A앱의 데이터를 이용하려면,
우선 데이터를 가지고 있는 A앱의 개발자가 콘텐츠 프로아비어를 만들어 주어야 한다.
즉 자신의 앱에 콘텐츠 프로바이더를 만든다는 것은 자신의 데이터를 일정 정도 외부 앱에 오픈하겠다는 의미이다.
그리고 외부 앱인 B앱은 A앱의 데이터를 직접 접근하는 것이 아니라,
A앱에서 만들어준 콘텐츠 프로바이더의 함수를 이용하여 데이터를 이용하는 구조이다.
결국 콘텐츠 프로바이더를 매개로 해서 데이터와 데이터를 이용하는 외부 앱이 직접 연결되지 않게 하려는 의도이다.
외부 앱에서 데이터를 이용한다고 하더라도 콘텐츠 프로바이더의 함수를 이용하는 구조이므로
콘텐츠 프로바이더를 만든 개발자가 얼마든지 제어할 수 있다.
외부 앱에 데이터를 공개하기 싫으면 콘텐츠 프로바이더를 만즐지 않으면 되고,
외부에 일정의 데이터를 오픈하려고 콘텐츠 프로바이더를 만들었다고 하더라도
콘텐츠 프로바이더의 함수를 이용하는 구조이므로 개발자 알고리즘으로 얼마든지 오픈되는 데이터를 제어할 수 있으며
보안상 문제가 되는 데이터를 제외하고 오픈하는 등의 작업이 가능하다
콘텐츠 프로바이더 작성법
콘텐츠 프로바이더를 작성하는 방법은 안드로이드 DBMS 프로그램과 유사하다.
물론 API가 DBMS 프로그램과 유사하다는 거지 콘텐츠 프로바이더에서
꼭 데이터 베이스의 데이터만을 외부에 공유해야 한다는 건 아니다.
콘텐츠 프로바이더 내부에서 접근하는 데이터는 파일, 데이터베이스, Preference 혹은 메모리의 데이터 일 수도 있다.
파일을 공유하는 방법은 개발한 앱에서 임의이 경로에 파일을 하나 만드는 것으로 시작한다.
해당 파일의 경로를 카메라 앱에 전달하고, 카메라 앱에서 촬영 데이터 정보를 파일에 쓰고( write )
성공여부를 반환하는 방식을 사용한다.
#1. 저장 경로 지정하기
FileProvider 클래스는 androidx에서 제공하는 콘텐츠 프로바이더로,
XML 설정을 기반으로 파일에 대한 Content URI를 생성해 준다.
FilePorvider를 이용하려면 res/xml폴더에 임의의 이름으로된 XML 파일을 만들어 주어야 한다.
xml/file_paths.xml
<?xml version="1.0" encoding="utf-8"?>
<paths xmlns:android="http://schemas.android.com/apk/res/android">
<external-path name="myfiles" path="Android/data/패키지_경로/files/Pictures"/>
</paths>
<external-path> 태그는 외부 저장공간의 파일을 공유하기 위해 사용된다.
( 내부 저장공간에 대한 공유는 <file-path> 태그를 이용한다. )
이렇게 작성한 XML 파일을 AndroidManifest.xml에서 FileProvider를 등록할 때 설정한다.
#2. FilePorvider 설정
AndroidManifext.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">
<!-- 외부 저장소 접근 권한 부여( 파일 읽기 ) -->
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
<!-- 외부 저장소 접근 권한 부여( 파일 쓰기 ) -->
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
<!-- 카메라 사용 권한 부여 -->
<uses-permission android:name="android.permission.CAMERA"/>
<application
android:allowBackup="true"
android:dataExtractionRules="@xml/data_extraction_rules"
android:fullBackupContent="@xml/backup_rules"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.FileProvider"
tools:targetApi="31">
<!-- 콘텐츠 프로바이더 등록 -->
<provider
android:name="androidx.core.content.FileProvider"
android:authorities="App Package Path(유일성이 확보된 문자열 식별자 사용)"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/file_paths">
</meta-data>
</provider>
<activity
android:name=".MainActivity"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
<meta-data
android:name="android.app.lib_name"
android:value="" />
</activity>
</application>
</manifest>
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.CAMERA"/>
<provider
android:name="androidx.core.content.FileProvider"
android:authorities="App Package Path(유일성이 확보된 문자열 식별자 사용)"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/file_paths">
</meta-data>
</provider>
① FileProvider는 개발자가 작성한 콘텐츠 프로바이더가 아닌 androidx 라이브러리에서 제공하는 클래스이다.
② authorities 속성에 유일성이 확보된 식별자 문자열을 하나 선언해 주어야 한다.
( 위의 코드에서는 Application의 패키지명을 선언하였다. )
③ <meta-data>태그로 정의한 XML 파일의 정보를 설정한다.
이미지 사이즈 지정
values/dimens.xml
<?xml version="1.0" encoding="utf-8"?>
<resources>
<dimen name="imgSize">240dp</dimen>
</resources>
MainActivity.java
import androidx.activity.result.ActivityResult;
import androidx.activity.result.ActivityResultCallback;
import androidx.activity.result.ActivityResultLauncher;
import androidx.activity.result.contract.ActivityResultContracts;
import androidx.appcompat.app.AppCompatActivity;
import androidx.core.app.ActivityCompat;
import androidx.core.content.FileProvider;
import android.Manifest;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.os.Environment;
import android.provider.MediaStore;
import android.util.Log;
import android.view.View;
import org.example.camerfileprovider.databinding.ActivityMainBinding;
import java.io.File;
import java.io.InputStream;
import java.text.SimpleDateFormat;
import java.util.Date;
public class MainActivity extends AppCompatActivity {
String filePath = null;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
ActivityMainBinding binding = ActivityMainBinding.inflate(getLayoutInflater());
setContentView(binding.getRoot());
ActivityResultLauncher<Intent> cameraFileLauncher = registerForActivityResult(
new ActivityResultContracts.StartActivityForResult()
, new ActivityResultCallback<ActivityResult>()
{
@Override
public void onActivityResult(ActivityResult result) {
int calRatio = calculateInSampleSize(
Uri.fromFile(new File(filePath))
, getResources().getDimensionPixelSize(R.dimen.imgSize)
, getResources().getDimensionPixelSize(R.dimen.imgSize)
);
BitmapFactory.Options option = new BitmapFactory.Options();
option.inSampleSize = calRatio;
Bitmap bitmap = BitmapFactory.decodeFile(filePath, option);
if(bitmap != null){
binding.imgThumbnail.setImageBitmap(bitmap);
}
}
});
binding.btnCamera.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
if (checkSelfPermission(Manifest.permission.CAMERA) == PackageManager.PERMISSION_GRANTED && checkSelfPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED) {
Log.d("tag", "권한 설정 완료");
try {
String timeStamp = new SimpleDateFormat("yyyyMMdd_HHmmss").format(new Date());
File storageDir = getExternalFilesDir(Environment.DIRECTORY_PICTURES);
File file = File.createTempFile(
"JPEG_"+timeStamp+"_"
, ".jpg"
, storageDir
);
filePath = file.getAbsolutePath();
Uri photoURI = FileProvider.getUriForFile(
getApplicationContext()
, Application 패키지 경로
, file
);
Intent intent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
intent.putExtra(MediaStore.EXTRA_OUTPUT, photoURI);
cameraFileLauncher.launch(intent);
} catch (Exception e) {
e.printStackTrace();
}
} else {
Log.d("tag", "권한 설정 요청");
ActivityCompat.requestPermissions(MainActivity.this, new String[]{Manifest.permission.CAMERA, Manifest.permission.WRITE_EXTERNAL_STORAGE}, 1);
}
}
}
});
}
private int calculateInSampleSize(Uri fileUri, int reqWidth, int reqHeight) {
BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true;
try {
InputStream inputStream = getContentResolver().openInputStream(fileUri);
// inJustDecodeBounds 값을 true 로 설정한 상태에서 decodeXXX() 를 호출.
// 로딩 하고자 하는 이미지의 각종 정보가 options 에 설정 된다.
BitmapFactory.decodeStream(inputStream, null, options);
inputStream.close();
} catch (Exception e) {
e.printStackTrace();
}
// 비율 계산
int width = options.outWidth;
int height = options.outHeight;
int inSampleSize = 1;
//inSampleSize 비율 계산
if (height > reqHeight || width > reqWidth) {
int halfHeight = height / 2;
int halfWidth = width / 2;
while (halfHeight / inSampleSize >= reqHeight && halfWidth / inSampleSize >= reqWidth) {
inSampleSize *= 2;
}
}
return inSampleSize;
}
}
레이아웃 구성
layout/activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
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/imgThumbnail"
android:layout_width="240dp"
android:layout_height="240dp"
android:layout_marginTop="80dp"
android:layout_weight="1"
android:src="@drawable/user_thumbnail"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
/>
<Button
android:id="@+id/btnCamera"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="20dp"
android:text="카메라"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/imgThumbnail"
tools:ignore="MissingConstraints"
/>
</androidx.constraintlayout.widget.ConstraintLayout>
'Android > Java Code' 카테고리의 다른 글
[Android] Activity에서 Dark Theme 비활성화기 (0) | 2023.01.04 |
---|---|
[Android] 갤러리 이미지 가져오기 및 썸네일 생성 (0) | 2022.11.10 |
[Android] setOnClickListener(this)를 이용한 버튼 클릭 이벤트 (0) | 2022.11.09 |
[Android] Application 설치 여부 및 확인 (0) | 2022.08.05 |
[Android] 액션바에 검색기능 활성화 (0) | 2022.07.19 |