πŸ“ SeSAC의 'JetPackκ³Ό Kotlin을 ν™œμš©ν•œ Android App 개발' κ°•μ’Œλ₯Ό μ •λ¦¬ν•œ κΈ€ μž…λ‹ˆλ‹€.


πŸ“‡ ContentProvider κ°œμš”

κ΅¬κΈ€μ˜ κΈ°λ³Έ μ•± 연동을 μœ„ν•΄μ„œλŠ” ContentProvider λΌλŠ” μ»΄ν¬λ„ŒνŠΈκ°€ ν•„μš”ν•˜λ‹€.

  • μ»¨ν…νŠΈ ν”„λ‘œλ°”μ΄λ”λŠ” μ•±κ³Ό μ•± κ°„μ˜ 데이터 연동을 λͺ©μ μœΌλ‘œ ν•˜λŠ” μ»΄ν¬λ„ŒνŠΈμ΄λ‹€.

λ‚΄κ°€ λ§Œλ“  μ•±μ—μ„œ μ—°λ½μ²˜, 가러리 등에 μžˆλŠ” 데이터가 ν•„μš”λ‘œ ν•˜λŠ” κ²½μš°κ°€ μžˆλ‹€. λ‹Ήμ—°ν•˜κ²Œλ„ μ™ΈλΆ€ μ•±μ—μ„œ 데이터에 직접 μ ‘κ·Όν•˜λŠ” 것은 κΈˆμ§€λ˜μ–΄ μžˆλ‹€. κ·Έλž˜μ„œ 데이터λ₯Ό κ°€μ§€κ³  μžˆλŠ” 앱에 ContentProvider λ₯Ό λ§Œλ“€κ³ , 데이터λ₯Ό μ΄μš©ν•˜λŠ” μ™ΈλΆ€ μ•±μ—μ„œ ContentProvider 을 μ΄μš©ν•œλ‹€.

πŸ“‘ ContentProvider λ‚΄λΆ€ μ•±

class MyContentProvider : ContentProvider() {
	
    override fun delete(uri: Uri, selection: String?, selectionArgs: Array<String>?): Int { }
    
    override fun getType(uri: Uri): String? { }
    
    override fun insert(uri: Uri, values: ContentValues?): Uri? { }
    
    override fun onCreate(): Boolean { }
    
    override fun query(
    	uri: Uri, projection: Array<String>?, selection: String?,
        selectionArgs: Array<String>?, sortOrder: String?
    ): Cursor? { }
    
    override fun update(
    	uri: Uri, values: ContentValues?, selection: String?,
        selectionArgs: Array<String>?
    ): Int { }
  • μ»¨ν…νŠΈ ν”„λ‘œλ°”μ΄λ”λŠ” ContentProvider λ₯Ό 상속 λ°›μ•„ μž‘μ„±

  • API ν˜•νƒœλ§Œ Sql λ¬Έκ³Ό λΉ„μŠ·ν•œ 것 뿐이닀. νŒŒμΌμ„ read ν•˜λ“  DBλ₯Ό μ°Έμ‘°ν•˜λ“  ν•˜κ³  싢은 λŒ€λ‘œ κ΅¬ν˜„ν•˜λ©΄ λœλ‹€.

μ™ΈλΆ€ μ•±μ—μ„œ query() ν•¨μˆ˜λ₯Ό ν˜ΈμΆœν•˜λ©΄ λ‚΄λΆ€ μ•±μ˜ 데이터λ₯Ό μ „λ‹¬ν•œλ‹€. μ™ΈλΆ€ μ•±μ—μ„œ delete() ν•¨μˆ˜λ‘œ λ‚΄λΆ€ μ•±μ˜ 데이터λ₯Ό μ‚­μ œ ν•  μˆ˜λ„ μžˆλ‹€. 이것을 ν—ˆμš©ν•˜κ³  μ‹Άμ§€ μ•Šλ‹€λ©΄ κ·Έλƒ₯ ν•¨μˆ˜ λ‚΄λΆ€λ₯Ό κ΅¬ν˜„ν•˜μ§€ μ•ŠμœΌλ©΄ λœλ‹€. μ „μ μœΌλ‘œ 개발자 λ§ˆμŒμ΄λ‹€.

πŸ“Œ Manifest 등둝

<provider
	android:name=".MyContentProvider"
    android:authorities="com.example.provider"
    android:enabled="true"
    android:exported="true></provider>
  • AndroidManifest.xml νŒŒμΌμ— 등둝

μ»¨ν…νŠΈ ν”„λ‘œλ°”μ΄λ”λŠ” ν•˜λ‚˜μ˜ μ»΄ν¬λ„ŒνŠΈ 이닀. κ·Έλž˜μ„œ λ‹Ήμ—°νžˆ λ§€λ‹ˆνŽ˜μŠ€νŠΈ νŒŒμΌμ— 등둝해야 ν•œλ‹€.
<provider> νƒœκ·Έλ‘œ λ“±λ‘ν•œλ‹€.

❗ λ‹€λ₯Έ μ»΄ν¬λ„ŒνŠΈλŠ” <name> μ†μ„±λ§Œ ν•„μˆ˜μ§€λ§Œ, μ»¨ν…νŠΈ ν”„λ‘œλ°”μ΄λ”λŠ” <authorities> 속성도 ν•„μˆ˜μ΄λ‹€. 이 μ†μ„±κ°’μœΌλ‘œ μ‹λ³„λœλ‹€.

πŸ“‘ ContentProvider μ™ΈλΆ€ μ•±

  • μ»¨ν…νŠΈ ν”„λ‘œλ°”μ΄λ”λ₯Ό μ΄μš©ν•˜κΈ° μœ„ν•΄μ„œ μΈν…νŠΈλ₯Ό λ°œμƒμ‹œν‚€μ§€ μ•ŠλŠ”λ‹€.

  • μ»¨ν…νŠΈ ν”„λ‘œλ°”μ΄λ”λŠ” ν•„μš”ν•œ μˆœκ°„ μ‹œμŠ€ν…œμ—μ„œ μžλ™μœΌλ‘œ 생성.

  • μ»¨ν…νŠΈ ν”„λ‘œλ°”μ΄λ”λ₯Ό μ΄μš©ν•˜κ³ μž ν•˜λŠ” 앱은 query(), insert(), delete(), update() ν•¨μˆ˜λ§Œ 호좜.

4개의 μ»΄ν¬λ„ŒνŠΈ μ€‘μ—μ„œ μ»¨ν…νŠΈ ν”„λ‘œλ°”μ΄λ”λ§Œμ€ μΈν…νŠΈ λ§€μ»€λ‹ˆμ¦˜κ³Ό μ „ν˜€ 관련이 μ—†λ‹€. μ™œλƒν•˜λ©΄ μ‹œμŠ€ν…œμ—μ„œ μ»¨ν…νŠΈ ν”„λ‘œλ°”μ΄λ”μ™€ μΈν…νŠΈλ₯Ό λ³„κ°œλ‘œ κ΄€λ¦¬ν•˜κΈ° λ•Œλ¬Έμ΄λ‹€.

Aμ•±μ—μ„œ Bμ•±μ˜ μ»¨ν…νŠΈ ν”„λ‘œλ°”μ΄λ”λ₯Ό ν˜ΈμΆœν•˜λ©΄, Bμ•±μ˜ μ‹œμŠ€ν…œμ—μ„œ μžλ™μœΌλ‘œ μ»¨ν…νŠΈ ν”„λ‘œλ°”μ΄λ”λ₯Ό μƒμ„±ν•œλ‹€. 그리고 Aμ•±μ—μ„œ delete(), insert(), query(), update() ν•¨μˆ˜λ₯Ό ν˜ΈμΆœν•˜λ©΄ λœλ‹€.

πŸ“Œ Query Visibility

ν•˜μ§€λ§Œ μ»¨ν…νŠΈ ν”„λ‘œλ°”μ΄λ”λ₯Ό μ΄μš©ν•˜κΈ° μœ„ν•΄μ„œλŠ” μ΄μš©ν•˜λŠ” μͺ½μ˜ μ•±μ˜ 메인 ν™˜κ²½νŒŒμΌ 섀정이 ν•„μš”ν•˜λ‹€.

<queries>
	<!--	λ‘˜ 쀑 ν•˜λ‚˜λ§Œ μ„ μ–Έλ˜μ–΄ 있으면 λœλ‹€.-->
    <!--	<provider android:authorities="com.example.test.provider"/>-->
    <package android:name="com.example.test.outter"/>
</queries>
  • μ™ΈλΆ€ μ•±μ˜ μ»¨ν…νŠΈ ν”„λ‘œλ°”μ΄λ”λ₯Ό μ΄μš©ν•˜κ³ μž ν•œλ‹€λ©΄ ν•΄λ‹Ή 앱을 μ΄μš©ν•˜κΈ° μœ„ν•œ Query Visibility κ΄€λ ¨ μ„€μ •

  • μ»¨ν…νŠΈ ν”„λ‘œλ°”μ΄λ”λ₯Ό κ°€μ§€κ³  μžˆλŠ” μ•±μ˜ νŒ¨ν‚€μ§€λͺ…을 <package> νƒœκ·Έλ‘œ ν˜Ήμ€ μ»¨ν…νŠΈ ν”„λ‘œλ°”μ΄λ”μ˜ authorities λ¬Έμžμ—΄μ„ <provider> νƒœκ·Έλ‘œ μ„ μ–Έ

πŸ“Œ ContentResolver

contentResolver.query(
	Uri.parse("content://com.example.test.provider"),
    null, null, null, null)
  • μ‹œμŠ€ν…œμ˜ μ»¨ν…νŠΈ ν”„λ‘œλ°”μ΄λ”λ₯Ό μ΄μš©ν•˜κΈ° μœ„ν•œ 객체가 ContentResolver 객체

  • ContentResolver κ°μ²΄λŠ” contentResolver ν”„λ‘œνΌν‹°λ‘œ νšλ“ν•˜μ—¬ query(), insert(), update(), delete() ν•¨μˆ˜λ₯Ό 호좜

첫 번째 λ§€κ°œλ³€μˆ˜λ‘œ Uri κ°’, μ‹λ³„μžλ₯Ό μ€˜μ„œ μ–΄λŠ μ•±μ˜ μ»¨ν…νŠΈ ν”„λ‘œλ°”μ΄λ”λ₯Ό 호좜 ν• μ§€ μ§€μ •ν•œλ‹€.

❗ 이 URL 에 쑰건이 μžˆλ‹€.

  • μ»¨ν…νŠΈ ν”„λ‘œλ°”μ΄λ”λ₯Ό μ‹λ³„ν•˜κΈ° μœ„ν•œ Uri 객체

  • ν”„λ‘œν† μ½œμ€ content κ°€ λ˜μ–΄μ•Ό ν•˜κ³ , 도메인 뢀뢄은 μ»¨ν…νŠΈ ν”„λ‘œλ°”μ΄λ”μ˜ μ‹λ³„μž κ°€ λ˜μ–΄μ•Ό ν•œλ‹€.


πŸ“š Contacts App

μ£Όμ†Œλ‘μ„ μ΄μš©ν•˜κΈ° μœ„ν•΄μ„œλŠ” νΌλ―Έμ…˜ 선언이 ν•„μš”ν•˜λ‹€.

<uses-permission android:name="android.permission.READ_CONTACTS"/>
  • μ£Όμ†Œλ‘ 앱을 μ—°λ™ν•˜μ—¬ μ£Όμ†Œλ‘ λͺ©λ‘ 화면을 λ„μš°κΈ°

  • μœ μ €κ°€ μ„ νƒν•œ μ‚¬λžŒμ˜ μ „ν™”λ²ˆν˜Έ ν˜Ήμ€ 이메일 정보λ₯Ό νšλ“

μ£Όμ†Œλ‘ λͺ©λ‘ 화면은 μ£Όμ†Œλ‘ μ•±μ˜ μ•‘ν‹°λΉ„ν‹° κ°€ λœλ‹€. κ·Έλž˜μ„œ μ£Όμ†Œλ‘ 화면을 λ„μš°λŠ” μž‘μ—…μ€ μΈν…νŠΈλ₯Ό λ°œμƒμ‹œμΌœμ•Ό ν•œλ‹€.

μ„ νƒν•œ 데이터λ₯Ό νšλ“ν•˜λŠ” μž‘μ—…μ€ μ£Όμ†Œλ‘ μ•±μ˜ μ»¨ν…νŠΈ ν”„λ‘œλ°”μ΄λ” κ°€ λœλ‹€.

πŸ“Œ Intent

val intent = Intent(Intent.ACTION_PICK, ContactsContract.CommonDataKinds.Phone.CONTENT_URI)
requestActivity.launch(intent)
  • μ£Όμ†Œλ‘μ˜ λͺ©λ‘ 화면을 λ„μš°κΈ°

  • μΈν…νŠΈλ₯Ό λ°œμƒμ‹œμΌœμ•Ό ν•œλ‹€.

이 λ•Œ λ˜μ Έμ£ΌλŠ” URI 에 따라 좜λ ₯λ˜λŠ” 데이터가 달라진닀.

ContactsContract.Contacts.CONTENT_URI : λͺ¨λ“  μ‚¬λžŒ 좜λ ₯

ContactsContract.CommonDataKinds.Phone.CONTENT_URI : μ „ν™”λ²ˆν˜Έκ°€ μžˆλŠ” μ‚¬λžŒλ§Œ 좜λ ₯

ContactsContract.CommonDataKinds.Email.CONTENT_URI : 이메일 정보가 μžˆλŠ” μ‚¬λžŒλ§Œ 좜λ ₯
  • μ£Όμ†Œλ‘ 앱을 μ—°λ™ν•˜κΈ° μœ„ν•œ URL

πŸ“Œ ContentProvider

val cursor = contentResolver.query(
	it.data!!.data!!,	// 첫 번째 λ§€κ°œλ³€μˆ˜λ‘œ μ‹λ³„μžλ₯Ό 전달
    arrayOf<String>(
    	ContactsContract.CommonDataKinds.Phone.DISPLAY_NAME,	// 이름
    	ContactsContract.CommonKinds.Phone.NUMBER				// μ „ν™”λ²ˆν˜Έ
    ),
    null,
    null,
    null,
)
  • μ£Όμ†Œλ‘μ—μ„œ μ „λ‹¬ν•œ κ²°κ³ΌλŠ” URL λ¬Έμžμ—΄ ν˜•νƒœμ΄λ©° URL 의 맨 λ§ˆμ§€λ§‰ 단어(μœ„μ—μ„œλŠ” 1144) κ°€ μœ μ €κ°€ μ„ νƒν•œ μ‚¬λžŒμ˜ μ‹λ³„μž κ°’

  • μ‹λ³„μžλ₯Ό 쑰건으둜 μ£Όμ†Œλ‘ μ•±μ˜ 컨텐츠 ν”„λ‘œλ°”μ΄λ” 이용

μ„ νƒν•œ μœ μ € λ°μ΄ν„°λŠ” URL ν˜•νƒœλ‘œ λ„˜μ–΄μ˜¨λ‹€. μ™œλƒν•˜λ©΄ μœ μ €κ°€ κ°€μ§€λŠ” 데이터가 λ„ˆλ¬΄ 많기 λ•Œλ¬Έμ΄λ‹€. λ‹¨μˆœνžˆ μ „ν™”λ²ˆν˜Έλ§Œ μžˆλŠ” 것이 μ•„λ‹ˆλΌ 이메일, μ£Όμ†Œ, μ§‘ μ „ν™” λ“±μ˜ 정보도 있기 λ•Œλ¬Έμ΄λ‹€. κ·Έλž˜μ„œ 우리 μ•±μœΌλ‘œ λŒμ•„μ˜€λŠ” 것은 μ„ νƒν•œ μœ μ €μ˜ μ‹λ³„μž 만 λ„˜μ–΄μ˜€κ³ , 이것이 URL ν˜•νƒœμ΄λ‹€. 그리고 이 μ‹λ³„μž λ₯Ό μ΄μš©ν•΄μ„œ λ‚΄κ°€ μ›ν•˜λŠ” 데이터λ₯Ό λ‹€μ‹œ μ£Όμ†Œλ‘μ˜ μ»¨ν…νŠΈ ν”„λ‘œλ°”μ΄λ”μ— μš”μ²­ν•œλ‹€.

🧩 μ‹€μŠ΅ 예제

  • Manifest.xml
<uses-permission android:name="android.permission.READ_CONTACTS" />

  • activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:gravity="center"
    android:orientation="vertical"
    tools:context=".MainActivity">

    <TextView
        android:id="@+id/resultView"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:textSize="30dp"
        android:textStyle="bold" />

    <Button
        android:id="@+id/button"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Contacts App" />

</LinearLayout>


  • MainActivity.kt
package com.kotdev99.android.c63

class MainActivity : AppCompatActivity() {
	override fun onCreate(savedInstanceState: Bundle?) {
		super.onCreate(savedInstanceState)
		setContentView(R.layout.activity_main)

		val button = findViewById<Button>(R.id.button)
		val resultView = findViewById<TextView>(R.id.resultView)

		val requestActivity: ActivityResultLauncher<Intent> = registerForActivityResult(
			ActivityResultContracts.StartActivityForResult()
		) {
			// μ£Όμ†Œλ‘μ—μ„œ λ˜λŒμ•„ 였면 μ‹€ν–‰ λ˜λŠ” λΆ€λΆ„
			val cursor = contentResolver.query(
				it.data!!.data!!,   // ContentProvider 및 μœ μ €μ˜ μ‹λ³„μž
				arrayOf(
					ContactsContract.CommonDataKinds.Phone.DISPLAY_NAME,
					ContactsContract.CommonDataKinds.Phone.NUMBER
				),
				null,
				null,
				null
			)
			var name = "none"
			var phone = "none"
			if (cursor!!.moveToFirst()) {
				name = cursor?.getString(0).toString()
				phone = cursor?.getString(1).toString()
			}
			resultView.text = "name - $name, phone - $phone"
		}

		val permissionLauncher = registerForActivityResult(
			ActivityResultContracts.RequestPermission()
		) { isGranted ->
			// νΌλ―Έμ…˜ λ‹€μ΄μ–Όλ‘œκ·Έ μ’…λ£Œ ν›„ μ‹€ν–‰ λ˜λŠ” λΆ€λΆ„
			if (isGranted) {
				val intent =
					Intent(Intent.ACTION_PICK, ContactsContract.CommonDataKinds.Phone.CONTENT_URI)
				requestActivity.launch(intent)
			}
		}

		button.setOnClickListener {
			val status =
				ContextCompat.checkSelfPermission(this, "android.permission.READ_CONTACTS")
			if (status == PackageManager.PERMISSION_GRANTED) {
				// νΌλ―Έμ…˜ ν—ˆμš© μƒνƒœ μ‹œ λ°”λ‘œ μ•”μ‹œμ  μΈν…νŠΈ λ°œμƒμ‹œμΌœ μ‹λ³„μž νšλ“
				val intent =
					Intent(
						Intent.ACTION_PICK,
						ContactsContract.CommonDataKinds.Phone.CONTENT_URI
					)
				requestActivity.launch(intent)
			} else {
				// νΌλ―Έμ…˜ κ±°λΆ€ μƒνƒœ μ‹œ νΌλ―Έμ…˜ μš”μ²­
				permissionLauncher.launch("android.permission.READ_CONTACTS")
			}
		}
	}
}

πŸ“² κ²°κ³Ό


πŸ“š Gallery App

κ°€λŸ¬λ¦¬λ„ μ£Όμ†Œλ‘κ³Ό λΉ„μŠ·ν•˜λ‹€.

μΈν…νŠΈλ‘œ 가러리 μ•±μ˜ 사진 λͺ©λ‘ μ•‘ν‹°λΉ„ν‹°λ₯Ό λ„μš°κ³  -> μœ μ €κ°€ μ„ νƒν•œ μ‚¬μ§„μ˜ μ‹λ³„μž '만' κ°€μ Έμ˜€κ³  -> μ‹λ³„μž 값을 μ΄μš©ν•΄μ„œ μ»¨ν…νŠΈ ν”„λ‘œλ°”μ΄λ”λ₯Ό μ—°λ™ν•˜μ—¬ 데이터λ₯Ό νšλ“

val intent = Intent(Intent.ACTION_PICK, MedialStore.Images.Media.EXTERNAL_CONTENT_URI)
intent.type = "image/*"
requestActivity.launch(intent)
  • μΈν…νŠΈλ‘œ 가러리 μ•±μ˜ 이미지 λͺ©λ‘ 화면을 좜λ ₯

❗ OOM 문제

μš”μ¦˜ 카메라 μ„±λŠ₯이 λ„ˆλ¬΄ λ†’μ•„μ Έ μΉ΄λ©”λΌλ‘œ 찍은 이미지 μ‚¬μ΄μ¦ˆκ°€ λ„ˆλ¬΄ 크닀.

이러면 OOM 문제 κ°€ λ°œμƒ ν•  수 μžˆλ‹€. 화면에 좜λ ₯λ˜λŠ” μ‚¬μ΄μ¦ˆκ°€ μ•„λ‹ˆλΌ μ•„μ˜ˆ 데이터 μ‚¬μ΄μ¦ˆλ₯Ό μ€„μ—¬μ„œ λ‘œλ”©ν•΄μ•Ό ν•œλ‹€.

val option = BitmapFactory.Options()
option.inSampleSize = 5		// 1/5 둜 μ‚¬μ΄μ¦ˆ λ‹€μš΄
  • OOM (OutOfMemoryException) 문제 κ°€ λ°œμƒ ν•  수 μžˆλŠ” 상황

  • BitmapFactory.Option 객체의 inSampleSize 값을 μ§€μ •ν•΄ 데이터 μ‚¬μ΄μ¦ˆλ₯Ό μ€„μ—¬μ„œ λ‘œλ”©

πŸ“Œ ContentProvider

InputStream 을 μ΄μš©ν•΄μ„œ 가러리 μ•±μ—μ„œ 데이터λ₯Ό νšλ“ν•˜κ³ , νšλ“ν•œ 데이터λ₯Ό BitmapFactory 에 λ„˜κ²¨μ„œ 이미지 객체λ₯Ό λ§Œλ“ λ‹€.

var inputStream = contentResolver.openInputStream(it.data!!.data)
val bitmap = BitmapFactory.decodeStream(inputStream, null, option)
  • 가러리 μ•±μ˜ ContentProvider κ°€ μ œκ³΅ν•˜λŠ” InputStream 객체λ₯Ό νšλ“

  • InputStream 객체에 μ˜ν•΄ λ„˜μ–΄μ˜€λŠ” 데이터 이용

openInputStream 의 λ§€κ°œλ³€μˆ˜λ‘œ μ‹λ³„μžλ₯Ό ν”„λ‘œλ°”μ΄λ”μ—κ²Œ μ „λ‹¬ν•˜λ©΄μ„œ 데이터λ₯Ό 받을 수 μžˆλŠ” InputStream μš”μ²­

BitmapFactory μ—κ²Œ InputStream 을 λ„˜κ²¨ InputStream 으둜 λ„˜μ–΄μ˜€λŠ” λ°μ΄ν„°λ‘œ 이미지 객체 생성

🧩 μ‹€μŠ΅ 예제

  • activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:gravity="center"
    android:orientation="vertical"
    tools:context=".MainActivity">

    <ImageView
        android:id="@+id/imageView"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content" />

    <Button
        android:id="@+id/button"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Button" />
</LinearLayout>

  • MainActivity.kt
package com.kotdev99.android.c64

class MainActivity : AppCompatActivity() {
	override fun onCreate(savedInstanceState: Bundle?) {
		super.onCreate(savedInstanceState)
		setContentView(R.layout.activity_main)

		val imageView = findViewById<ImageView>(R.id.imageView)
		val button = findViewById<Button>(R.id.button)

		val launcher: ActivityResultLauncher<Intent> = registerForActivityResult(
			ActivityResultContracts.StartActivityForResult()
		) {
			// 가러리 μ•±μ—μ„œ λ˜λŒμ•„ 왔을 λ•Œ μ‹€ν–‰ λ˜λŠ” μ˜μ—­
			try {
				val option = BitmapFactory.Options()
				option.inSampleSize = 5

				// μ»¨ν…νŠΈ ν”„λ‘œλ°”μ΄λ”μ—κ²Œ μ‹λ³„μžμ— ν•΄λ‹Ή λ˜λŠ” 데이터 InputStream μš”μ²­
				val inputStream = contentResolver.openInputStream(it.data!!.data!!)
				val bitmap = BitmapFactory.decodeStream(inputStream, null, option)
				inputStream!!.close()
				bitmap?.let {
					imageView.setImageBitmap(bitmap)
				} ?: let {

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

		button.setOnClickListener {
			val intent = Intent(Intent.ACTION_PICK, MediaStore.Images.Media.EXTERNAL_CONTENT_URI)
			intent.type = "image/*"
			launcher.launch(intent)
		}
	}
}

πŸ“² κ²°κ³Ό


πŸ“š Call App

μ „ν™”λ₯Ό κ±Έκ±°λ‚˜ λ°›λŠ” 앱을 μ§€μΉ­ν•œλ‹€. λ„ˆλ¬΄ 잘 κ΅¬ν˜„λœ 앱이라 μ „ν™” κΈ°λŠ₯은 κ·Έλƒ₯ 이거 μ“°λ©΄ λœλ‹€.

1. νΌλ―Έμ…˜ ν•„μš”

2. μΈν…νŠΈμ˜ μ•‘μ…˜ λ¬Έμžμ—΄μ„ Intent.ACTION_CALL 둜 μ§€μ •

3. data μ •λ³΄μ˜ URL 은 tel: 으둜 μ„ μ–Έ (Call App 의 μ•‘ν‹°λΉ„ν‹°κ°€ tel: ν”„λ‘œν† μ½œ λͺ…μœΌλ‘œ 데이터λ₯Ό λ°›κΈ° λ•Œλ¬Έ)

4. data μ •λ³΄λ‘œ μ „ν™” 번호 λͺ…μ‹œ

🧩 μ‹€μŠ΅ 예제

  • Manifest.xml
<uses-permission android:name="android.permission.CALL_PHONE" />

  • activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:gravity="center"
    android:orientation="vertical"
    tools:context=".MainActivity">

    <EditText
        android:id="@+id/editView"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:inputType="phone" />

    <Button
        android:id="@+id/button"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="Call" />

</LinearLayout>

  • MainActivity.kt
package com.kotdev99.android.c65

class MainActivity : AppCompatActivity() {
	override fun onCreate(savedInstanceState: Bundle?) {
		super.onCreate(savedInstanceState)
		setContentView(R.layout.activity_main)

		val editView = findViewById<EditText>(R.id.editView)
		val button = findViewById<Button>(R.id.button)

		val permissionLauncher = registerForActivityResult(
			ActivityResultContracts.RequestPermission()
		) { isGranted ->
			if (isGranted) {
				val intent = Intent(Intent.ACTION_CALL, Uri.parse("tel:${editView.text}"))
				startActivity(intent)
			} else {
				Toast.makeText(this, "denied", Toast.LENGTH_SHORT).show()
			}
		}

		button.setOnClickListener {
			val status = ContextCompat.checkSelfPermission(this, "android.permission.CALL_PHONE")
			if (status == PackageManager.PERMISSION_GRANTED) {
				val intent = Intent(Intent.ACTION_CALL, Uri.parse("tel:${editView.text}"))
				startActivity(intent)
			} else {
				permissionLauncher.launch("android.permission.CALL_PHONE")
			}
		}
	}
}

πŸ“² κ²°κ³Ό


πŸ“š Camera App

카메라 앱을 μ—°λ™ν•˜μ—¬ 사진을 μ΄¬μ˜ν•˜κ³  κ²°κ³Όλ₯Ό 되돌렀 λ°›λŠ” 방법은 두 κ°€μ§€ 방법이 μžˆλ‹€.

  • 사진 데이터 νšλ“ 방법

  • 파일 곡유 방법

// 사진 데이터 νšλ“ 방법 

1. 카메라 앱은 사진을 file 둜 μ €μž₯ν•˜μ§€ μ•Šκ³  λ°μ΄ν„°λ§Œ λ„˜κΈ΄λ‹€.

2. κ·Έλž˜μ„œ 가러리 등에 남지 μ•ŠλŠ”λ‹€. 

3. 그런데 μ‚¬μ΄μ¦ˆκ°€ μž‘κ²Œ λ„˜μ–΄μ˜¨λ‹€. (OOM 문제 λ•Œλ¬ΈμΈ λ“―?)
// 파일 곡유 방법

1. μΈν…νŠΈλ₯Ό λ°œμƒμ‹œν‚¬ λ•Œ μ•±μ˜ νŒŒμΌμ •λ³΄λ₯Ό 같이 λ„˜κ²¨μ€€λ‹€. 

2. 카메라 앱이 사진을 μ΄¬μ˜ν•˜κ³  file 둜 μ €μž₯ν•œ ν›„ 성곡/μ‹€νŒ¨ μ—¬λΆ€λ§Œ λ„˜κ²¨μ€€λ‹€. (사진은 ν’€ λ°μ΄ν„°λ‘œ μ €μž₯λœλ‹€.)

3. 우리 앱은 μ €μž₯된 νŒŒμΌμ„ 읽어 듀인닀.

πŸ“Œ λ°μ΄ν„°λ§Œ νšλ“

val intent = Intent(MediaStore.ACTION_IMAGE_CAPTURE)
requestActivity.launch(intent)
val bitmap = it.data?.getExtras()?.get("data") as Bitmap
  • 사진 데이터 νšλ“ 방법

λ„˜μ–΄μ˜€λŠ” 데이터λ₯Ό κ·Έλƒ₯ Bitmap 객체둜 이용 ν•΄μ£Όλ©΄ λœλ‹€.

πŸ“Œ 곡유

// 파일 곡유 방법

1. μ•±μ—μ„œ 사진을 μ €μž₯ν•  νŒŒμΌμ„ λ§Œλ“ λ‹€.

2. 파일 정보λ₯Ό ν¬ν•¨ν•΄μ„œ μΈν…νŠΈλ₯Ό λ°œμƒμ‹œμΌœ 카메라 앱을 μ‹€ν–‰ μ‹œν‚¨λ‹€.

3. 카메라 μ•±μ—μ„œ 사진 촬영 ν›„ 촬영된 사진을 곡유된 νŒŒμΌμ— μ €μž₯을 ν•œλ‹€.

4. 카메라 앱이 μ’…λ£Œ λ˜λ©΄μ„œ 성곡 μ‹€νŒ¨λ₯Ό λ°˜ν™˜ν•œλ‹€.

5. μ•±μ—μ„œ νŒŒμΌμ„ 읽어 카메라 앱이 μ €μž₯ν•œ 사진 데이터λ₯Ό μ΄μš©ν•œλ‹€.

그런데 파일둜 μ΄μš©μ„ ν•  κ²½μš°μ—λŠ” 쑰금 μž‘μ—…μ„ ν•΄μ•Ό ν•œλ‹€. νŒŒμΌμ„ κ³΅μœ ν•˜κΈ° λ•Œλ¬Έμ— λ°œμƒν•˜λŠ” μž‘μ—…λ“€ 이닀.

πŸ“ XML 파일 생성

XML νŒŒμΌμ„ λ§Œλ“€μ–΄ μ€˜μ•Ό ν•œλ‹€.

<paths xmlns:android="http://schemas.android.com/apk/res/android">
	<external-path name="myfiles" path="Android/data/com.kotdev99.android.c66/files/Pictures"/>
</paths>    
  • FileProvider λ₯Ό μ΄μš©ν•˜λ €λ©΄ κ³΅μœ ν•˜κ³ μž ν•˜λŠ” 파일의 Uri 값을 μ€€λΉ„

  • path λŠ” κ·Έλƒ₯ νŒ¨ν‚€μ§€ λͺ…μœΌλ‘œ μž‘μ„±ν–ˆλ‹€.

πŸ“ Manifest 등둝

그리고 κ·Έ XML νŒŒμΌμ„ Manifest 에 등둝해 μ€˜μ•Ό ν•œλ‹€.

<provider
	android:name="androidx.core.content.FileProvider"
    android:authorities="com.kotdev99.android.c66.fileprovider"
    android:exported="false"
    android:grantUriPermissions="true">
    <meta-data
    	android:name="android.support.FILE_PROVIDER_PATHS"
        android:resource="@xml/file_paths"></meta-data>
</provider>
  • AndroidManifest.xml νŒŒμΌμ— 등둝

  • <provider> νƒœκ·Έλ‘œ λ“±λ‘ν•œλ‹€.

  • ν”„λ‘œλ°”μ΄λ”λ₯Ό λ§Œλ“œλŠ” 것이 μ•„λ‹ˆλΌ λΌμ΄λΈŒλŸ¬λ¦¬μ— μžˆλŠ” κ±Έ κ°€μ Έλ‹€ μ“Έ 뿐이닀.

  • μš°λ¦¬λŠ” resource 에 XML 파일 μ •λ³΄λ§Œ μ§€μ • ν•΄μ£Όλ©΄ λœλ‹€.

πŸ’¬ XML 파일 μž‘μ„±

πŸ“Œ 파일 생성

val file = File.createTempFile(
	"JPEG_${timeStamp}_",
    ".jpg",
    storageDir
)
filePath = file.absolutePath
  • 카메라 앱을 μ—°λ™ν•˜κΈ° μœ„ν•΄μ„œ 파일 생성

πŸ“Œ Uri 객체 생성

μƒμ„±λœ 파일의 μ‹λ³„μžλΌκ³  보면 λœλ‹€.

val photoURI: Uri = FileProvider.getUriForFile(
	this,
    "com.kotdev99.android.c66.fileprovider", file
)
val intent = Intent(MediaStore.ACTION_IMAGE_CAPTURE)
intent.putExtra(MediaStore.EXTRA_OUTPUT, photoURI);
fileRequestActivity.launch(intent)
  • νŒŒμΌμ„ κ³΅μœ ν•˜κΈ° μœ„ν•œ Uri 객체λ₯Ό λ§Œλ“€κ³  이 정보λ₯Ό μΈν…νŠΈμ˜ μ—‘μŠ€νŠΈλΌ λ°μ΄ν„°λ‘œ μ„€μ •

πŸ“Œ 파일 생성

파일 κ²½λ‘œμ—μ„œ 데이터λ₯Ό 뽑아 이미지 객체 νšλ“ν•œλ‹€.

val bitmap = BitmapFactory.decodeFile(filePath, option)
  • 파일 경둜λ₯Ό decodeFile() ν•¨μˆ˜μ— μ§€μ •ν•˜μ—¬ Bitmap 객체λ₯Ό νšλ“

🧩 μ‹€μŠ΅ 예제

DATA λ²„νŠΌμ€ 이미지 데이터λ₯Ό νšλ“ν•˜κ³ , FILE λ²„νŠΌμ€ 이미지 νŒŒμΌμ„ νšλ“ν•˜κ²Œλ” κ΅¬ν˜„ ν•΄λ³΄μž!

  • activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout 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"
    android:gravity="center"
    android:orientation="vertical"
    tools:context=".MainActivity">


    <androidx.cardview.widget.CardView
        android:layout_width="150dp"
        android:layout_height="150dp"
        app:cardCornerRadius="75dp"
        app:cardElevation="0dp">

        <ImageView
            android:id="@+id/imageView"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:scaleType="centerCrop" />
    </androidx.cardview.widget.CardView>

    <Button
        android:id="@+id/dataButton"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginTop="24dp"
        android:text="data" />

    <Button
        android:id="@+id/fileButton"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginTop="24dp"
        android:text="file" />

</LinearLayout>

  • file_path.xml

res/xml ν•˜μœ„μ— File 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/com.kotdev99.android.c66/files/Pictures" />
</paths>

  • Manifest.xml

<provider> νƒœκ·Έμ™€ <meta-data> νƒœκ·Έ 등둝

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

    <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/Theme.AndroidLab">

        <provider
            android:name="androidx.core.content.FileProvider"
            android:authorities="com.kotdev99.android.c66.file-provider"
            android:exported="false"
            android:grantUriPermissions="true">
            <meta-data
                android:name="android.support.FILE_PROVIDER_PATHS"
                android:resource="@xml/file_path" />
        </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>
        </activity>
    </application>

</manifest>

  • MainActivity.kt
package com.kotdev99.android.c66

class MainActivity : AppCompatActivity() {
	@SuppressLint("SimpleDateFormat")
	override fun onCreate(savedInstanceState: Bundle?) {
		super.onCreate(savedInstanceState)
		setContentView(R.layout.activity_main)

		val imageView = findViewById<ImageView>(R.id.imageView)
		val dataButton = findViewById<Button>(R.id.dataButton)
		val fileButton = findViewById<Button>(R.id.fileButton)

		val launcher: ActivityResultLauncher<Intent> = registerForActivityResult(
			ActivityResultContracts.StartActivityForResult()
		) {
			val bitmap = it.data?.extras?.get("data") as Bitmap
			bitmap?.let {
				imageView.setImageBitmap(bitmap)
			}
		}

		dataButton.setOnClickListener {
			val intent = Intent(MediaStore.ACTION_IMAGE_CAPTURE)
			launcher.launch(intent)
		}

		var filePath = ""
		val fileLauncher: ActivityResultLauncher<Intent> = registerForActivityResult(
			ActivityResultContracts.StartActivityForResult()
		) {
			val option = BitmapFactory.Options()
			option.inSampleSize = 3
			val bitmap = BitmapFactory.decodeFile(filePath, option)
			bitmap?.let {
				imageView.setImageBitmap(bitmap)
			}
		}

		fileButton.setOnClickListener {
			val timeStamp = SimpleDateFormat("yyyyMMdd_HHmm ss").format(Date())
			val storageDir = getExternalFilesDir(Environment.DIRECTORY_PICTURES)
			val file = File.createTempFile(
				"JPEG_${timeStamp}_",   // 파일λͺ…
				".jpg",
				storageDir
			)
			filePath = file.absolutePath
			val uri = FileProvider.getUriForFile(
				this,
				"com.kotdev99.android.c66.file-provider",
				file
			)
			val intent = Intent(MediaStore.ACTION_IMAGE_CAPTURE)
			intent.putExtra(MediaStore.EXTRA_OUTPUT, uri)
			fileLauncher.launch(intent)
		}
	}
}

πŸ“² κ²°κ³Ό

profile
응애 λ‚˜ μ•„κΈ° 뉴비

0개의 λŒ“κΈ€

Powered by GraphCDN, the GraphQL CDN