

// 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">
<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.Android37_ApplicationClass"
tools:targetApi="31"
android:name=".AppClass">
<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>
</activity>
</application>
</manifest>
// AppClass.kt
package kr.co.lion.android37_applicationclass
import android.app.Application
class AppClass : Application() {
var value1 = 0
var value2 = 0.0
lateinit var value3:String
}
안드로이드는 애프릴케이션이 데이터를 저장할 수 있는 저장소를 두가지로 제공하고 있다.
내부 저장소 : 애플리케이션을 통해서만 접근이 가능하다
외부 저장소 : 단말기 내부의 공유 영역으로 모든 애플리케이션이 접근 가능하다. 단말기를 컴퓨터에 연결하면 탐색기를 통해 접근할 수 있는 영역을 의미한다.
최근에는 단말기에서 SD카드를 지원하지 않는 추세
저가형 또는 오래된 단말기에서는 SD카드를 사용하고 있음
이제는 컴퓨터에 안드로이드폰을 연결하면 아래와 같이 창이 뜨는데

허용을 눌러줘야 컴퓨터에서 안드로이드 외부 저장소에 접근이 가능해진다.
Scoped Storage는 아래와 같은 형태로 구성된다.

미디어 파일 : 사진, 동영상, 음원파일들을 저장하는 장소. 직접 접근하기 위해서는 단말기에서 접근 허용을 해줘야 함
공용 파일 : Download폴더. 이 폴더에 저장된 파일은 모두 애플리케이션이 접근할 수 있다. 코드를 통한 직접 접근은 불가능하고 단말기에 설치된 파일 관리 어플을 통해서만 접근 가능

외부와 데이터를 주고 받을 때 Stream을 이용하여 데이터를 주고 받을 수 있다
Stream을 얻어오는 방식만 파악하면 된다.
Data Input/Output Stream 객체를 내부로
Object Input/Output Stream 객체를 외부로
// 내부 저장소 : 다른 애플리케이션이 접근할 수 없다. 소량의 데이터를 저장하는 목적으로 사용한다.
// openFileOutput 메서드와 openFileInput 메서드로 스트림을 추출한다.
// button, button2 예제
activityMainBinding.apply {
// 내부 저장소
// data/data/애플리케이션 패키지/files 폴더에 저장된다.
// 저장된 애플리케이션에서만 접근할 수 있다.
// 저장한 애플리케이션에서만 접근할 수 있기 때문에 코드상에서의 자유로운 접근이 가능하며
// 필요한 권한도 없다. 다른 애플리케이션이 접근할 수 없으며 사용자가 파일에 접근하는 것도 불가능
// 하다.
button.setOnClickListener {
// MODE_PRIVATE : 덮어 씌운다.
// MODE_APPEND : 이어서 쓴다.
// Stream을 가져온다.
val fileOutputStream = openFileOutput("data1.dat", MODE_PRIVATE)
val dataOutputStream = DataOutputStream(fileOutputStream)
dataOutputStream.writeInt(100)
dataOutputStream.writeDouble(11.11)
dataOutputStream.writeBoolean(true)
dataOutputStream.writeUTF("문자열1")
dataOutputStream.flush()
dataOutputStream.close()
textView.text = "내부 저장소 쓰기 완료"
}
}
// 내부 저장소 : 다른 애플리케이션이 접근할 수 없다. 소량의 데이터를 저장하는 목적으로 사용한다.
// openFileOutput 메서드와 openFileInput 메서드로 스트림을 추출한다.
// button, button2 예제
// 내부 저장소 읽기
button2.setOnClickListener {
// Stream을 가져온다.
val fileInputStream = openFileInput("data1.dat")
val dataInputStream = DataInputStream(fileInputStream)
// 데이터를 읽어온다.
val data1 = dataInputStream.readInt()
val data2 = dataInputStream.readDouble()
val data3 = dataInputStream.readBoolean()
val data4 = dataInputStream.readUTF()
dataInputStream.close()
fileInputStream.close()
textView.apply {
text = "data1 : ${data1}\n"
append("data2 : ${data2}\n")
append("data3 : ${data3}\n")
append("data4 : ${data4}")
}
}
외부 저장소 영역의 Android/data/ 폴더에 저장된다.

AndroidManifest.xml에 2줄 추가
// AndroidManifest.xml
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
// MainActivity.kt
// 외부 저장소의 Android/data : 다른 애플리케이션이 접근할 수 없다. 대량의 데이터를 저장하는 목적으로 사용한다.
// getExternalFilesDir 메서드를 통해 외부 저장소까지의 경로를 얻어오고 이를 통해 스트림을 직접 생성하여 사용한다.
// button3, button4 예제
class MainActivity : AppCompatActivity() {
lateinit var activityMainBinding: ActivityMainBinding
// 확인 받을 권한 목록
val permissionList = arrayOf(
Manifest.permission.READ_EXTERNAL_STORAGE,
Manifest.permission.WRITE_EXTERNAL_STORAGE
)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
activityMainBinding = ActivityMainBinding.inflate(layoutInflater)
setContentView(activityMainBinding.root)
// 권한 확인
// 안드로이드11부터는 권한을 확인 받지 않지만 그 이전 버전까지는 권한 확인을 받는다.
requestPermissions(permissionList, 0)
activityMainBinding.apply {
// 외부 저장소
// 외부 저장소 영역의 Android/data/ 폴더에 저장된다.
// Android/data/ 경로에 애플리케이션 패키지명으로 폴더가 만들어지고 files 폴더도 만들어진다.
// 여기에 파일이 저장된다.
button3.setOnClickListener {
// 외부 저장소까지의 경로를 가져온다.
// null을 넣어주면 files 까지의 경로를 가져온다.
val filePath = getExternalFilesDir(null).toString()
// 이를 통해 스트림을 생성한다.
val fileOutputStream = FileOutputStream("${filePath}/data2.dat")
val dataOutputStream = DataOutputStream(fileOutputStream)
dataOutputStream.writeInt(200)
dataOutputStream.writeDouble(22.22)
dataOutputStream.writeBoolean(false)
dataOutputStream.writeUTF("문자열2")
dataOutputStream.flush()
dataOutputStream.close()
textView.text = "외부 저장소 앱 데이터 폴더에 저장"
}
}
}
}

예전에는 Android폴더를 제외한 나머지 외부 저장소의 모든 폴더를 getExternalFilesDir메서드로 접근이 가능했으나
지금은 코드 상의 접근은 차단하고 파일관리 어플을 통해 사용자에게 직접 경로를 찾아 설정할 수 있도록 제한함
// 외부 저장소의 Android/data : 다른 애플리케이션이 접근할 수 없다. 대량의 데이터를 저장하는 목적으로 사용한다.
// getExternalFilesDir 메서드를 통해 외부 저장소까지의 경로를 얻어오고 이를 통해 스트림을 직접 생성하여 사용한다.
// button3, button4 예제
button4.setOnClickListener {
// 외부 저장소까지의 경로
val filePath = getExternalFilesDir(null).toString()
// 스트림을 생성한다.
val fileInputStream = FileInputStream("${filePath}/data2.dat")
val dataInputStream = DataInputStream(fileInputStream)
// 데이터를 읽어온다.
val data1 = dataInputStream.readInt()
val data2 = dataInputStream.readDouble()
val data3 = dataInputStream.readBoolean()
val data4 = dataInputStream.readUTF()
dataInputStream.close()
fileInputStream.close()
textView.apply {
text = "data1 : ${data1}\n"
append("data2 : ${data2}\n")
append("data3 : ${data3}\n")
append("data4 : ${data4}")
}
}
파일에 저장되어 있는 데이터의 양식이 무엇인지를 나타내는 문자열
원하는 형식의 파일만 가져오려 할 때 MIME 타입을 설정해줄 수 있다.
https://www.iana.org/assignments/media-types/media-types.xhtml#audio
모든 파일 : /
모든 이미지 파일 : image/
모든 영상 파일 : video/
모든 소리 파일 : audio/*
// MainActivity.kt
// 외부 저정소의 Android/data를 제외한 모든 곳 : 애플리케이션에서 파일에 직접 접근하는 것이 불가능하다.
// 단말기에 설치되어 있는 파일 관리 어플리케이션의 Activity를 실행하고 사용자가 선택한 파일의 경로를 얻어올 수 있다.
// 이를 통해 스트림을 직접 생성하여 사용한다.
// button5, button6 예제
class MainActivity : AppCompatActivity() {
lateinit var activityMainBinding: ActivityMainBinding
// 확인 받을 권한 목록
val permissionList = arrayOf(
Manifest.permission.READ_EXTERNAL_STORAGE,
Manifest.permission.WRITE_EXTERNAL_STORAGE
)
// 파일 앱을 실행하기 위한 런처
lateinit var writeActivityLauncher:ActivityResultLauncher<Intent>
lateinit var readActivityResultLauncher:ActivityResultLauncher<Intent>
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
activityMainBinding = ActivityMainBinding.inflate(layoutInflater)
setContentView(activityMainBinding.root)
// 권한 확인
// 안드로이드11부터는 권한을 확인 받지 않지만 그 이전 버전까지는 권한 확인을 받는다.
requestPermissions(permissionList, 0)
// 외부 저장소 접근을 위한 런처
val contract1 = ActivityResultContracts.StartActivityForResult()
writeActivityLauncher = registerForActivityResult(contract1){
// 파일을 선택하면 resultCode는 RESULT_OK가 들어온다.
if(it.resultCode == RESULT_OK){
// 사용자가 선택한 파일의 정보를 가지고 있는 Intent로부터 파일 정보를 가져온다.
if(it.data != null){
// 저장할 파일에 접근할 수 있는 객체로부터 파일 정보를 가져온다.
// w : 쓰기, a : 이어쓰기, r : 읽기
val descriptor = contentResolver?.openAssetFileDescriptor(it.data?.data!!, "w")
// 스트림을 생성
val fileOutputStream = FileOutputStream(descriptor?.fileDescriptor)
val dataOutputStream = DataOutputStream(fileOutputStream)
dataOutputStream.writeInt(300)
dataOutputStream.writeDouble(33.33)
dataOutputStream.writeBoolean(true)
dataOutputStream.writeUTF("문자열3")
dataOutputStream.flush()
dataOutputStream.close()
}
}
}
activityMainBinding.apply {
// 파일 어플을 통한 접근
// 외부 저장소의 Android/data 폴더를 제외한 모든 폴더의 접근은
// 파일 어플을 실행하여 사용자가 파일을 골라줘야지만 접근이 가능하다.
button5.setOnClickListener {
// 파일 관리 어플의 Activity를 실행한다.
val fileIntent = Intent(Intent.ACTION_CREATE_DOCUMENT)
fileIntent.addCategory(Intent.CATEGORY_OPENABLE)
// Mimetype을 설정해준다.
// 파일에 저장되어 있는 데이터의 양식이 무엇인지를 나타내는 문자열
// https://www.iana.org/assignments/media-types/media-types.xhtml#audio
// 모든 파일 : */*
// 모든 이미지 파일 : image/*
// 모든 영상 파일 : video/*
// 모든 소리 파일 : audio/*
fileIntent.type = "*/*"
writeActivityLauncher.launch(fileIntent)
}
}
}
}

버튼을 클릭하면

파일매니저앱이 실행되고 항상 처음 폴더는 Downloads 폴더가 나온다.

이름을 정하고 SAVE를 눌러주면 창이 꺼짐

다시 들어가 확인해보면 파일이 생성되어있다.
```kotlin
// MainActivity.kt
// 외부 저정소의 Android/data를 제외한 모든 곳 : 애플리케이션에서 파일에 직접 접근하는 것이 불가능하다.
// 단말기에 설치되어 있는 파일 관리 어플리케이션의 Activity를 실행하고 사용자가 선택한 파일의 경로를 얻어올 수 있다.
// 이를 통해 스트림을 직접 생성하여 사용한다.
// button5, button6 예제
class MainActivity : AppCompatActivity() {
lateinit var activityMainBinding: ActivityMainBinding
// 확인 받을 권한 목록
val permissionList = arrayOf(
Manifest.permission.READ_EXTERNAL_STORAGE,
Manifest.permission.WRITE_EXTERNAL_STORAGE
)
// 파일 앱을 실행하기 위한 런처
lateinit var writeActivityLauncher:ActivityResultLauncher<Intent>
lateinit var readActivityResultLauncher:ActivityResultLauncher<Intent>
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
activityMainBinding = ActivityMainBinding.inflate(layoutInflater)
setContentView(activityMainBinding.root)
// 권한 확인
// 안드로이드11부터는 권한을 확인 받지 않지만 그 이전 버전까지는 권한 확인을 받는다.
requestPermissions(permissionList, 0)
// 외부 저장소 접근을 위한 런처
val contract2 = ActivityResultContracts.StartActivityForResult()
readActivityResultLauncher = registerForActivityResult(contract2){
// 파일을 선택하면 resultCode는 RESULT_OK가 들어온다.
if(it.resultCode == RESULT_OK) {
// 사용자가 선택한 파일의 정보를 가지고 있는 Intent로부터 파일 정보를 가져온다.
if (it.data != null) {
// 읽어올 파일에 접근할 수 있는 객체로부터 파일 정보를 가져온다.
// w : 쓰기, a : 이어쓰기, r : 읽기
val descriptor = contentResolver?.openFileDescriptor(it.data?.data!!, "r")
// 스트림을 생성
val fileInputStream = FileInputStream(descriptor?.fileDescriptor)
val dataInputStream = DataInputStream(fileInputStream)
// 데이터를 읽어온다.
val data1 = dataInputStream.readInt()
val data2 = dataInputStream.readDouble()
val data3 = dataInputStream.readBoolean()
val data4 = dataInputStream.readUTF()
dataInputStream.close()
fileInputStream.close()
activityMainBinding.textView.apply {
text = "data1 : ${data1}\n"
append("data2 : ${data2}\n")
append("data3 : ${data3}\n")
append("data4 : ${data4}")
}
}
}
}
activityMainBinding.apply {
// 파일 어플을 통한 접근
// 외부 저장소의 Android/data 폴더를 제외한 모든 폴더의 접근은
// 파일 어플을 실행하여 사용자가 파일을 골라줘야지만 접근이 가능하다.
// 읽어오기
button6.setOnClickListener {
// 파일 관리 어플의 Activity를 실행한다.
// OPEN_DOCUMENT는 읽기용
val fileIntent = Intent(Intent.ACTION_OPEN_DOCUMENT)
fileIntent.type = "*/*"
readActivityResultLauncher.launch(fileIntent)
}
}
}
}

버튼을 눌러 파일매니저 액티비티가 실행되고

저장된 파일을 선택하면

저장된 파일의 값이 보여진다.
안드로이드 4대 구성 요소 중 하나로 애플리케이션이 저장한 데이터를 다른 애플리케이션이 사용할 수 있도록 제공하는 개념
공유가 아니라 제공하는 개념임
Content Provider를 직접 만드는 일은 거의 없을 것
앨범에서 사진가져오는 기능에서는 많이 사용할 수 있음

B 어플리케이션이 A 어플리케이션의 CP를 사용하고 싶다고 OS에 요청
OS는 A 어플리케이션의 CP에 사용할 기능을 호출
하지만 A 어플리케이션의 CP에 아무런 기능이 오버라이딩 되지 않았다면 아무런 일도 일어나지 않는다.


URI Authorities : 외부에서 다른 어플리케이션이 CP에 접근하기 위한 주소
OS에 요청할때 사용된다.
AndroidManifest.xml에도 provider가 등록된다.

CotentProvider에서 DB객체는 나중에 CotentProvider가 종료될때 OS가 DB객체를 알아서 닫아주기 때문에 close문을 써서 직접 닫아줄 필요가 없다.
lateinit으로 선언 후 onCreate문에 DB객체를 생성해서 다른 메서드에서 생성된 DB객체를 사용하는 방식
// 첫번째 어플
// DBhelper.kt
class DBHelper(context: Context) : SQLiteOpenHelper(context, "Test.db", null, 1) {
override fun onCreate(db: SQLiteDatabase?) {
val sql = """create table TestTable
| (idx integer primary key autoincrement,
| data1 integer not null,
| data2 real not null,
| data3 text not null,
| data4 date not null)
""".trimMargin()
// 쿼리문을 실행한다.
db?.execSQL(sql)
}
override fun onUpgrade(db: SQLiteDatabase?, oldVersion: Int, newVersion: Int) {
TODO("Not yet implemented")
}
}
// 첫번째 어플
// MainActivity.kt
class MainActivity : AppCompatActivity() {
lateinit var activityMainBinding: ActivityMainBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
activityMainBinding = ActivityMainBinding.inflate(layoutInflater)
setContentView(activityMainBinding.root)
activityMainBinding.apply {
button.setOnClickListener {
// 쿼리문
val sql = """
select idx, data1, data2, data3, data4
from TestTable
""".trimIndent()
// 쿼리 실행
val dbHelper = DBHelper(this@MainActivity)
val cursor = dbHelper.writableDatabase.rawQuery(sql, null)
while(cursor.moveToNext()){
// 컬럼 순서값을 가져온다.
val idx1 = cursor.getColumnIndex("idx")
val idx2 = cursor.getColumnIndex("data1")
val idx3 = cursor.getColumnIndex("data2")
val idx4 = cursor.getColumnIndex("data3")
val idx5 = cursor.getColumnIndex("data4")
// 데이터를 가져온다.
val idx = cursor.getInt(idx1)
val data1 = cursor.getInt(idx2)
val data2 = cursor.getDouble(idx3)
val data3 = cursor.getString(idx4)
val data4 = cursor.getString(idx5)
textView.apply {
text = "idx : ${idx}\n"
append("data1 : ${data1}\n")
append("data2 : ${data2}\n")
append("data3 : ${data3}\n")
append("data4 : ${data4}\n")
}
}
dbHelper.close()
}
}
}
}
// 첫번째 어플
// TestProvider.kt
class TestProvider : ContentProvider() {
// 데이터 베이스 접근 객체
lateinit var sqliteDatabase:SQLiteDatabase
// 삭제
// 두번째 : 조건절
// 세번째 : 조건절의 ?에 바인딩될 값
// 반환값 : 삭제된 행의 개수
override fun delete(uri: Uri, selection: String?, selectionArgs: Array<String>?): Int {
val cnt = sqliteDatabase.delete("TestTable",selection,selectionArgs)
return cnt
}
// ContentProvider의 authorities를 반환해준다.
// ContentProvider 사용하는 쪽에서 다수의 Provider를 사용하고 있다면
// 이를 구분하기 위한 용도로 사용한다.
override fun getType(uri: Uri): String? {
return uri.authority
}
// 첫번째 : authorities가 담긴 uri 객체
// 두번째 : 저장할 데이터가 담긴 객체
override fun insert(uri: Uri, values: ContentValues?): Uri? {
// 저장한다.
sqliteDatabase.insert("TestTable", null, values)
return uri
}
// ContentProvider가 생성될 때 자동으로 호출된다.
// 별로 할 작업이 없다.
// 데이터베이스에 접속하는 작업을 한다.
override fun onCreate(): Boolean {
val dbHelper = DBHelper(context!!)
sqliteDatabase = dbHelper.writableDatabase
return true
}
// select
// 첫 번째 : authorities 가 담긴 Uri 객체
// 두 번째 : 가져올 컬럼의 목록
// 세 번째 : 조건 절
// 네 번째 : 조건 절의 ?에 들어갈 값
// 다 섯번째 : 정렬 기준
// 반환 : 데이터를 접근할 수 있는 커서 객체
override fun query(
uri: Uri, projection: Array<String>?, selection: String?,
selectionArgs: Array<String>?, sortOrder: String?
): Cursor? {
val cursor = sqliteDatabase.query("TestTable", projection, selection, selectionArgs, null, null, sortOrder)
// contentprovider가 소멸될때 OS에서 자동으로 열린 db객체를 닫아주기 때문에
// 여기서 close할필요가 없음
// sqliteDatabase.close()
return cursor
}
// update
// 두 번째 : 저장할 데이터
// 세 번째 : 조건절
// 네 번째 : 조건절의 ?에 바인딩될 값 배열
// 반환 : 수정이 적용된 행의 개수
override fun update(
uri: Uri, values: ContentValues?, selection: String?,
selectionArgs: Array<String>?
): Int {
val cnt = sqliteDatabase.update("TestTable", values, selection, selectionArgs)
return cnt
}
}
// 두번째 어플
// MainActivity.kt
class MainActivity : AppCompatActivity() {
lateinit var activityMainBinding: ActivityMainBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
activityMainBinding = ActivityMainBinding.inflate(layoutInflater)
setContentView(activityMainBinding.root)
activityMainBinding.apply {
button.setOnClickListener{
// 저장할 데이터를 준비한다.
val sdf = SimpleDateFormat("yyyy-MM-dd", Locale.getDefault())
val now = sdf.format(Date())
// 저장할 데이터를 ContentValues에 담아준다.
val cv1 = ContentValues()
cv1.put("data1", 100)
cv1.put("data2", 11.11)
cv1.put("data3", "문자열1")
cv1.put("data4", now)
// ContentProvider 접근을 위한 이름을 가진 객체를 생성한다.
// ContentProvider 사용을 위해 권한을 부여해줘야 한다.
val uri = Uri.parse("content://kr.co.lion.testprovider")
// 저장요청을 수행한다.
// 저장 요청은 OS에 하게 되고 이 요청을 받은 OS는 해당 Conteont Provider의 insert 메서드를
// 호출해준다.
// Content Provider의 insert 메서드 내에 코드가 구현되어 있는데로 동작한다.
contentResolver.insert(uri, cv1)
textView.text = "저장완료"
}
button2.setOnClickListener {
val uri = Uri.parse("content://kr.co.lion.testprovider")
// 데이터를 가져온다.
// 두번째 : 가져올 컬럼 목록, null이면 모두 가져온다.
// 세번째 : 조건절
// 네번째 : 조건절 ?에 설정된 값 배열
// 다섯번째 : 정렬 기준 컬럼 목록
val cursor = contentResolver.query(uri,null,null,null,null)!!
while(cursor.moveToNext()){
// 컬럼 순서값을 가져온다.
val idx1 = cursor.getColumnIndex("idx")
val idx2 = cursor.getColumnIndex("data1")
val idx3 = cursor.getColumnIndex("data2")
val idx4 = cursor.getColumnIndex("data3")
val idx5 = cursor.getColumnIndex("data4")
// 데이터를 가져온다.
val idx = cursor.getInt(idx1)
val data1 = cursor.getInt(idx2)
val data2 = cursor.getDouble(idx3)
val data3 = cursor.getString(idx4)
val data4 = cursor.getString(idx5)
textView.apply {
text = "idx : ${idx}\n"
append("data1 : ${data1}\n")
append("data2 : ${data2}\n")
append("data3 : ${data3}\n")
append("data4 : ${data4}\n")
}
}
}
button3.setOnClickListener {
// 수정할 데이터를 담는다.
val cv1 = ContentValues()
cv1.put("data1", 1000)
// 조건절
val where = "idx = ?"
val args = arrayOf("1")
// 수정한다.
val uri = Uri.parse("content://kr.co.lion.testprovider")
contentResolver.update(uri,cv1,where,args)
textView.text = "수정 완료"
}
button4.setOnClickListener {
// 조건절
val where = "idx = ?"
val args = arrayOf("1")
// 삭제 요청한다.
val uri = Uri.parse("content://kr.co.lion.testprovider")
contentResolver.delete(uri, where, args)
textView.text = "삭제 완료"
}
}
}
}
// AndroidManifest.xml
<!-- 다른 애플리케이션이 가지고 있는 ContentProvider 사용을 위한 권한 -->
<!-- 모든 sql 쿼리 작업을 허용하도록 설정한다 -->
<uses-permission android:name="android.permission.QUERY_ALL_PACKAGES"
tools:ignore="QueryAllPackagesPermission"/>
두번째 어플을 켜서 insert 요청 버튼을 누르고

첫번째 어플을 다시 켜보면 값을 불러올 수 있다.

두번째 어플에서 select요청 버튼을 눌러도 값을 불러올 수 있다.

두번째 어플에서 update 버튼을 누르면 값이 수정된다.


두번째 어플에서 delete 버튼을 누르면 설정된 idx 값의 데이터가 삭제된다.

※ 출처 : 멋쟁이사자 앱스쿨 2기, 소프트캠퍼스