2026/03/21

오늘의 이야기

이전 글에서 정리할 것처럼 java에서 kotlin으로 이전을 했습니다.  그러고 나서 보기 시작했는 데,  DefaultSharedPrefernces의 사용할 수 없는 환경으로 변경이 된 것을 알게 되었습니다. 


 


이전

prefs = PreferenceManager.getDefaultSharedPreferences(this)

 


이후

prefs = getSharedPreferences(packageName + "_preferences", MODE_PRIVATE)

기본 preferences 의 이름은 패키지 이름 뒤에 접미사가 달라붙어 있었습니다.   


 


이렇게 해서 또 하나 해소가 되었습니다.


 


환경설정



 





오늘의 이야기

이 글은 기존에 사용하던 java 프로젝트를 kotlin으로 전환해 보기 위해서 작성하고 있습니다. 이 글은 아래 링크의 개발자 페이지의 내용을 참고하고 있습니다.


 


https://developer.android.com/kotlin/add-kotlin?hl=ko#kts 



 


기존 앱에 Kotlin 추가  |  Android Developers


기존 앱에 Kotlin 추가 컬렉션을 사용해 정리하기 내 환경설정을 기준으로 콘텐츠를 저장하고 분류하세요. Android 스튜디오에서는 Kotlin을 완벽하게 지원하므로 기존 프로젝트에 Kotlin 파일을 추가


developer.android.com




 


먼저 gradle 파일부터 수정해 보겠습니다. 


 


buildscript {

ext { // 추가한 부분
compose_ui_version = '1.5.0-alpha04'
raamcosta_version = '1.9.42-beta'
kotlin_version = '1.8.22'
}
repositories {
google()
mavenCentral()
}
dependencies {
classpath 'com.android.tools.build:gradle:8.0.2'
classpath 'com.google.gms:google-services:4.3.15'
classpath 'com.google.firebase:firebase-crashlytics-gradle:2.9.7'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" // 추가한 부분
}
}

task clean(type: Delete) {
delete rootProject.buildDir
}

다음 모듈 수준의 gradle 파일 수정입니다.


import java.text.SimpleDateFormat

plugins {
id 'com.android.application'
id 'com.google.gms.google-services'
id 'com.google.firebase.crashlytics'
id 'kotlin-android' // 추가한 부분
}

android {
signingConfigs {
upload {
storeFile file(var)
storePassword var
keyAlias = var
keyPassword var
}
}
compileSdkVersion 34
namespace='com.n.......y2kakao'

defaultConfig {
applicationId "com.n......akao"
minSdkVersion 26
targetSdkVersion 34
versionCode 41
versionName '1.4.1'
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"

renderscriptTargetApi 18
renderscriptSupportModeEnabled true

}

sourceSets { // 추가한 부분
getByName("main") {
java.srcDir("src/main/kotlin")
}
}

compileOptions {
sourceCompatibility JavaVersion.VERSION_17
targetCompatibility JavaVersion.VERSION_17
}

buildTypes {
debug {
minifyEnabled false // 2022.06.23 true 가 되었더니, 압축이 되서 null 오류가 난다.
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
release {
minifyEnabled false // 2022.06.23 true 가 되었더니, 압축이 되서 null 오류가 난다.
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
viewBinding{
enabled = true
}

def archiveBuildType = ["release"]
namespace 'com.nari.notify2kakao'
applicationVariants.all { variant ->
variant.outputs.each { output ->
if (variant.buildType.name in archiveBuildType) {
def df = new SimpleDateFormat("yyyyMMdd")
df.setTimeZone(TimeZone.getDefault())
if (variant.versionName != null) {
String name = "Notify2Kakao_${df.format(new Date())}_${defaultConfig.versionCode}_${variant.versionName}.apk"
output.outputFileName = name
}
}
}
}
}

dependencies {

constraints { // 추가한 부분
implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.8.0") {
because("kotlin-stdlib-jdk7 is now a part of kotlin-stdlib")
}
implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.8.0") {
because("kotlin-stdlib-jdk8 is now a part of kotlin-stdlib")
}

implementation('androidx.work:work-runtime:2.7.1') {
because '''androidx.work:work-runtime:2.1.0 pulled from play-services-ads
has a bug using PendingIntent without FLAG_IMMUTABLE or
FLAG_MUTABLE and will fail in apps targeting S+.'''
}
}

implementation("androidx.core:core-ktx:1.10.1") // 추가한 부분
implementation("org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version") // 추가한 부분

implementation "com.kakao.sdk:v2-all:2.15.0" // 전체

implementation "com.google.android.play:core:1.10.3"
implementation 'androidx.appcompat:appcompat:1.6.1'
implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
implementation 'com.google.android.material:material:1.9.0'
implementation 'androidx.legacy:legacy-support-v4:1.0.0'
implementation 'androidx.preference:preference-ktx:1.2.0'
implementation 'com.google.gms:google-services:4.3.15'
implementation platform('com.google.firebase:firebase-bom:30.1.0')
implementation 'com.google.firebase:firebase-crashlytics'
implementation 'com.google.firebase:firebase-analytics'
testImplementation 'junit:junit:4.13.2'
androidTestImplementation 'androidx.test.ext:junit:1.1.5'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1'
implementation 'androidx.navigation:navigation-fragment-ktx:2.6.0'
implementation 'androidx.navigation:navigation-ui-ktx:2.6.0'
implementation 'com.google.android.gms:play-services-ads:22.2.0'
implementation 'com.google.android.gms:play-services-maps:18.1.0'
implementation 'androidx.mediarouter:mediarouter:1.4.0'
implementation 'com.google.guava:guava:29.0-android'

// 데이터 주고 받기
implementation 'com.squareup.retrofit2:retrofit:2.9.0'
implementation 'com.squareup.retrofit2:converter-gson:2.9.0'
implementation 'com.squareup.retrofit2:converter-simplexml:2.9.0'

}

 


실행하는 방법은 메뉴에서 Code 메뉴 아래에 보면 Convert Java File To Kotlin File 메뉴가 있는 데, 저는 아래 그림처럼 변환할 대상을 찾고 오른쪽 마우스 클릭해서 해 보도록 하겠습니다.


변환해 보기



 


이제 java 코드를 kotlin 으로 하나씩 변환해 보겠습니다.


 


변환하면서 쉽게 만나는 오류 표시



이름 그림과 같이 kotlin으로 변환된 뒤에 변수 처리 등에 오류 표시가 나오는 경우를 종종 만나게 됩니다.  사유는 해당 변수를 변환하면서 null 허용 상태를 만들어 가면서 변환을 하기 때문입니다. 선언된 변수를 찾아보겠습니다. 


 




이렇게 null 로 초기화를 해야만 하는 가 봅니다.  kotlin 에는 초기화를 나중에 하는 유용한 방법이 있는 데도 말입니다.  그래서 다음과 같이 변경을 해 주면 해결이 됩니다. 




이제 소스 코드에 표시 되었던 오류는 사라집니다.  다만, 실제로 source 코드에서는 해당 변수를 사용하기 전에 꼭 초기화를 해야 실행 시 오류가 발생하지 않습니다. 


 


java.lang.NullPointerException: Parameter specified as non-null is null: 

또한 java.lang.NullPointerException: Parameter specified as non-null is null 이런 오류를 만나게 되었습니다. 사유는 Recycle View을 이용해서 List에 들어 있는 항목들을 보여주는 화면들이 있는 데, View Adapter 등을 사용했었다면 만나게 될 것 같습니다. 


 


                 Process: com.nari.notify2kakao, PID: 17268
java.lang.NullPointerException: Parameter specified as non-null is null: method com.nari.notify2kakao.util.ViewStrValueAdapter.getView, parameter convertView
at com.nari.notify2kakao.util.ViewStrValueAdapter.getView(Unknown Source:2)
at android.widget.AbsListView.obtainView(AbsListView.java:2466)
at android.widget.ListView.makeAndAddView(ListView.java:2065)
at android.widget.ListView.fillDown(ListView.java:791)
at android.widget.ListView.fillFromTop(ListView.java:853)
at android.widget.ListView.layoutChildren(ListView.java:1836)
at android.widget.AbsListView.onLayout(AbsListView.java:2263)
at android.view.View.layout(View.java:24421)

 


아래 Adapter 예시와 같이 getView 의 리턴값이 View? 이 될 수 있도록 조치를 해 주시면 해소가 됩니다.


class ViewWithDrawMonthlyAdapter(oData: ArrayList<ViewWithDrawMonthly>) : BaseAdapter() {
private var viewWithDrawMonthlyls = ArrayList<ViewWithDrawMonthly>()

init {
viewWithDrawMonthlyls = oData
}

override fun getCount(): Int {
return viewWithDrawMonthlyls.size
}

override fun getItem(position: Int): Any {
return viewWithDrawMonthlyls[position]
}

override fun getItemId(position: Int): Long {
return position.toLong()
}

override fun getView(position: Int, containView: View?, parent: ViewGroup): View? {
var containView = containView
val context = parent.context
if (containView == null) {
val inflater =
context.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater
containView = inflater.inflate(R.layout.viewwithdrawmonthly_item, parent, false)
}
val chkTy = containView?.findViewById<CheckBox>(R.id.chkTy)
val monthlyDay = containView?.findViewById<TextView>(R.id.editMonthlyDay)
val remark = containView?.findViewById<TextView>(R.id.editRemark)
val outamt = containView?.findViewById<TextView>(R.id.editOutAmt2)
val textSplit = containView?.findViewById<TextView>(R.id.textSplit)
val textSplit1 = containView?.findViewById<TextView>(R.id.textSplit1)
val viewWithDrawMonthly = viewWithDrawMonthlyls[position]
if (chkTy != null) {
chkTy.isChecked = false
}
if ("Y" == viewWithDrawMonthly.chkTy) {
if (chkTy != null) {
chkTy.isChecked = true
}
}
if (monthlyDay != null) {
monthlyDay.text = viewWithDrawMonthly.getmonthlyDay()
}
if (remark != null) {
remark.text = viewWithDrawMonthly.remark
}
if (outamt != null) {
outamt.text = viewWithDrawMonthly.outAmt
}
return containView
}
}

kotlin에서 non-null 이 null 될 수 없어서 오류가 발생하기 때문이기도 합니다.


 


다음이야기는 뒤에...





오늘의 이야기



#스치니1000프로젝트 #재미 #행운기원 #Compose #Firebase

🎯 야 너 토요일마다 로또 확인하냐?
나도 맨날 "혹시나~" 하면서 봤거든 ㅋㅋ

근데 이제는 그냥 안 해
AI한테 맡겼어 🤖✨

그것도 구글 Gemini로다가!

그래서 앱 하나 만들었지
👉 "로또 예상번호 by Gemini" 🎱

AI가 분석해서 번호 딱! 뽑아줌
그냥 보고 참고만 하면 됨

재미로 해도 좋고…
혹시 모르는 거잖아? 😏


https://play.google.com/store/apps/details?id=com.billcorea.gptlotto1127




오늘의 이야기

https://v.daum.net/v/20230720150011670

"신입사원 평균이 50세, 이 무슨?".. 취업 전선도 점점 '고령화'  어쩌다

앞으로 10년 정도 더 지나면 취업시장에서 '젊은' 층을 보기가 쉽지 않을 것이란 관측입니다. 지난해 취업자 평균 나이가 46.8세였는데, 계속 연령대가 올라가 10년 후면 50살, 2050년이면 평균 연

v.daum.net


오늘은 기사 스크랩 하나를 올려 봅니다.  나이는 숫자??? ㅋ~  

생활이 필요한 지금도 그럴까요?  노력해 보아야 할 것 같아요. 50대 중반 이 나이에도 취업능 해야 합니다.  아직도 부양해야 하는 가족이 있어서요.

오늘도 열심히 살아야 할 것 같아요.

야경






오늘의 이야기


#스하리1000명프로젝트

스치니들!
내가 만든 이 앱은, 내 폰에 오는 알림 중에서 중요한 키워드가 있는 경우
등록해둔 친구에게 자동으로 전달해주는 앱이야 📲

예를 들어, 카드 결제 알림을 와이프나 자녀에게 보내주거나
이번 달 지출을 달력처럼 확인할 수도 있어!

앱을 함께 쓰려면 친구도 설치 & 로그인해줘야 해.
그래야 친구 목록에서 서로 선택할 수 있으니까~
서로 써보고 불편한 점 있으면 알려줘 🙏

👉 https://play.google.com/store/apps/details?id=com.nari.notify2kakao





오늘의 이야기

장마철이 끝나기는 하는 건지?  오늘은 어제와 다른 게 맑은 하늘에 더운 기온을 보냈습니다.


맑인 하늘



어제와는 다르게 하늘이 파란색을 보여 줍니다. 날씨도 무덥고요. 오락가락 날씨에  이제 그만 비는 내리지 않았으면 싶기도 하지만,  내일은 또 어떤 날이 될까는 걱정이 되기도 합니다. 


 


요 몇일 동안 폭우 때문에 힘들어 지신 분들께는 오늘 같은 날씨가 도움이 될까 싶기도 하고요.  후손들에게 돌려주어야 하는 지구가 요새는 엉망이 되어 가고 있다는 생각이 들기도 합니다. 


 


오늘도 무사히 ...

 


어린 시절에 버스나 택시등의 교통수단을 타면 유난히 많이 보이도 기도문 이었던 거 같습니다.  오늘도 잘 마무리가 되시길 기원드려요.


 





오늘의 이야기

활용앱 예시


휴대폰에 있는 사진 (이미지)을 백업해 보아야겠다는 요청을 받았습니다.  하지만, 그때는 방법을 잘 모르겠더군요. 그래서 일단 찾아본다고 했는 데, 그 이후에는 더 이상의 요구를 하지 않았습니다.  그래서 이왕 찾아보았던 정보를 이용해서 앱을 하나 만들어 보기로 했습니다. 
 
https://medium.com/@sendtosaeed2/android-fetch-all-files-from-local-storage-media-store-api-e9b9 14cd71e1

Android Fetch All Files From Local Storage (Media Store API) 🥲

Hi android devs! Hope you are all fine but not fine with the media store api, cause it is good but not good for us. In this article we will…

medium.com

 
처리하는 기술적인 부분에 대해서는 이 글이 참고가 되었습니다. 안드로이드가 External Storage에 대한 access 권한을 제한하기 전에 안드로이드 11 이전에는 READ_EXTERNAL_STORAGE, WRITE_EXTERNAL_STORAGE 등으로 카메라 앱으로 촬영한 사진등의 이미지를 접근해 백업을 하거나 하는 처리를 할 수 있었습니다.
 
하지만, 안드로이드가 업데이트를 하는 과정에서 여러 가지 보안절차가 강화되었기 때문에 그걸 활용할 수 없었습니다.
 
https://developer.android.com/training/data-storage/shared/media?hl=ko 

공유 저장소의 미디어 파일에 액세스  |  Android 개발자  |  Android Developers

DataStore는 로컬 데이터를 저장하는 최신 방법을 제공합니다. SharedPreferences 대신 DataStore를 사용해야 합니다. 자세한 내용은 DataStore 가이드를 참고하세요. 공유 저장소의 미디어 파일에 액세스 컬

developer.android.com

그래서 위 참조글에서 보았던 MediaStore API을 활용해 보기로 했습니다. 권한 설정 등의 제한 사항이 발견되었기 때문에 안드로이드 13을 타깃으로 설정했습니다. 
 

gradle
defaultConfig {
applicationId "com.billcorea.......t0710"
minSdk 33
targetSdk 34
versionCode 7
versionName "0.0.7"

testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
vectorDrawables {
useSupportLibrary true
}
}

minSdk 33으로 설정 했습니다. 아직 안드로이드 13까지 패치되지 않은 기기들이 많기는 하겠지만, 뭐 금방 다 옮겨 오지 않을까 싶습니다.  물론 지구상의 모든 안드로이드 기기를 대상으로 하겠다면 달라질 수 있습니다. 
 
다만, 한국에서 팔리는 기기등은 대부분 안드로이드 13으로 패치가 되었을 거라는 믿음(?)은 강하게 당겨 옵니다. 
 

manifest
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"
android:maxSdkVersion="32" />
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES"/>
<uses-permission android:name="android.permission.ACCESS_MEDIA_LOCATION"/>
<uses-permission android:name="com.google.android.gms.permission.AD_ID"/>

manifest에  등록한 권한 목록입니다. notify을 하기 위한 권한,  wifi 상태 확인을 위한 권한등은 기능설정을 위해서 필요한 부분이고, 
 
READ_MEDIA_IMAGES 가 필수 권한입니다.  READ_EXTERNAL_STOREAGE 은 안드로이드 API 32까지만 필요하므로 이제 지워도 될 것 같습니다.  이제 데이터를 읽어 오는 부분에 대한 코드를 작성해 보겠습니다. 
 

withContext(Dispatchers.IO) {

/**
* Android [ContentProvider]로 작업할 때 핵심 개념은
* "투영". 프로젝션은 공급자에게 요청할 열의 목록입니다.
* (정확히) SQL의 "SELECT ..." 절로 생각할 수 있습니다.
* 성명.
*
* 프로젝션을 제공하는 것은 _필수_가 아닙니다. 이 경우 `null`을 전달할 수 있습니다.
* [ContentResolver.query]에 대한 호출에서 `projection` 대신 요청하지만
* 필요한 것보다 더 많은 데이터는 성능에 영향을 미칩니다.
*
* 이 샘플에서는 몇 개의 데이터 열만 사용하므로
* 열의 하위 집합.
*/
/**
* Android [ContentProvider]로 작업할 때 핵심 개념은
* "투영". 프로젝션은 공급자에게 요청할 열의 목록입니다.
* (정확히) SQL의 "SELECT ..." 절로 생각할 수 있습니다.
* 성명.
*
* 프로젝션을 제공하는 것은 _필수_가 아닙니다. 이 경우 `null`을 전달할 수 있습니다.
* [ContentResolver.query]에 대한 호출에서 `projection` 대신 요청하지만
* 필요한 것보다 더 많은 데이터는 성능에 영향을 미칩니다.
*
* 이 샘플에서는 몇 개의 데이터 열만 사용하므로
* 열의 하위 집합.
*/
val projection = arrayOf(
MediaStore.Images.Media._ID,
MediaStore.Images.Media.DISPLAY_NAME,
MediaStore.Images.Media.DATE_ADDED,
)

/**
* `selection`은 SQL 문의 "WHERE ..." 절입니다. 가능하다
* 대신 `null`을 전달하여 이를 생략하면 모든 행이 반환됩니다.
* 이 경우 이미지를 촬영한 날짜를 기준으로 선택 항목을 사용하고 있습니다.
*
* 선택 항목에 `?`가 포함되어 있습니다. 이것은 변수를 나타냅니다.
* 다음 변수에 의해 제공됩니다.
*/
/**
* `selection`은 SQL 문의 "WHERE ..." 절입니다. 가능하다
* 대신 `null`을 전달하여 이를 생략하면 모든 행이 반환됩니다.
* 이 경우 이미지를 촬영한 날짜를 기준으로 선택 항목을 사용하고 있습니다.
*
* 선택 항목에 `?`가 포함되어 있습니다. 이것은 변수를 나타냅니다.
* 다음 변수에 의해 제공됩니다.
*/
val selection = "${MediaStore.Images.Media.DATE_ADDED} >= ?"

/**
* `selectionArgs`는 각 `?`에 대해 채워질 값 목록입니다.
* `선택`에서.
*/
/**
* `selectionArgs`는 각 `?`에 대해 채워질 값 목록입니다.
* `선택`에서.
*/
val selectionArgs = arrayOf(
dateToTimestamp(1, 1, 2000).toString()
)
selectionArgs.forEach {
Log.e("", "selectionArgs $it ${getDateTime(it)}")
}

/**
* Sort order to use. This can also be null, which will use the default sort
* order. For [MediaStore.Images], the default sort order is ascending by date taken.
*/
/**
* Sort order to use. This can also be null, which will use the default sort
* order. For [MediaStore.Images], the default sort order is ascending by date taken.
*/
val sortOrder = "${MediaStore.Images.Media.DATE_ADDED} DESC"

context.contentResolver.query(
MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
projection,
selection,
selectionArgs,
sortOrder
)?.use { cursor ->

/**
* 반환된 [Cursor]에서 데이터를 가져오려면
* 관심 있는 각 열과 일치하는 인덱스를 찾습니다.
*
* 두 가지 방법이 있습니다. 첫 번째는 방법을 사용하는 것입니다
* 열 ID를 찾을 수 없는 경우 -1을 반환하는 [Cursor.getColumnIndex]. 이것
* 코드가 요청할 열을 프로그래밍 방식으로 선택하는 경우에 유용합니다.
* 그러나 객체로 파싱하기 위해 단일 방법을 사용하고 싶습니다.
*
* 우리의 경우 원하는 열을 정확히 알고 있기 때문에
* 반드시 포함되어야 한다는 점(API 1에서 모두 지원되기 때문에)
* [Cursor.getColumnIndexOrThrow]를 사용합니다. 이 방법은
* [IllegalArgumentException] 명명된 열을 찾을 수 없는 경우.
*
* 두 경우 모두 이 방법이 느리지는 않지만 결과를 캐시하려고 합니다.
* 각 행에 대해 조회할 필요가 없도록 합니다.
*/
/**
* 반환된 [Cursor]에서 데이터를 가져오려면
* 관심 있는 각 열과 일치하는 인덱스를 찾습니다.
*
* 두 가지 방법이 있습니다. 첫 번째는 방법을 사용하는 것입니다
* 열 ID를 찾을 수 없는 경우 -1을 반환하는 [Cursor.getColumnIndex]. 이것
* 코드가 요청할 열을 프로그래밍 방식으로 선택하는 경우에 유용합니다.
* 그러나 객체로 파싱하기 위해 단일 방법을 사용하고 싶습니다.
*
* 우리의 경우 원하는 열을 정확히 알고 있기 때문에
* 반드시 포함되어야 한다는 점(API 1에서 모두 지원되기 때문에)
* [Cursor.getColumnIndexOrThrow]를 사용합니다. 이 방법은
* [IllegalArgumentException] 명명된 열을 찾을 수 없는 경우.
*
* 두 경우 모두 이 방법이 느리지는 않지만 결과를 캐시하려고 합니다.
* 각 행에 대해 조회할 필요가 없도록 합니다.
*/
val idColumn = cursor.getColumnIndexOrThrow(MediaStore.Images.Media._ID)
val dateModifiedColumn =
cursor.getColumnIndexOrThrow(MediaStore.Images.Media.DATE_ADDED)
val displayNameColumn =
cursor.getColumnIndexOrThrow(MediaStore.Images.Media.DISPLAY_NAME)

while (cursor.moveToNext()) {

// Here we'll use the column indexs that we found above.
val id = cursor.getLong(idColumn)
val dateModified =
Date(TimeUnit.SECONDS.toMillis(cursor.getLong(dateModifiedColumn)))
val displayName = cursor.getString(displayNameColumn)

/**
* 이것은 가장 까다로운 부분 중 하나입니다.
*
* 이미지에 액세스하고 있기 때문에(사용하여
* [MediaStore.Images.Media.EXTERNAL_CONTENT_URI], 이를 사용하겠습니다.
* 기본 URI로 이미지의 ID를 추가합니다.
*
* 이것은 [MediaStore.Video]로 작업할 때와 완전히 동일한 방법이며
* [MediaStore.Audio]도 마찬가지입니다. `Media.EXTERNAL_CONTENT_URI`가 무엇이든
* 아이템을 얻기 위한 쿼리가 기본이고, ID는 받을 문서입니다.
* 거기에 요청하십시오.
*/

/**
* 이것은 가장 까다로운 부분 중 하나입니다.
*
* 이미지에 액세스하고 있기 때문에(사용하여
* [MediaStore.Images.Media.EXTERNAL_CONTENT_URI], 이를 사용하겠습니다.
* 기본 URI로 이미지의 ID를 추가합니다.
*
* 이것은 [MediaStore.Video]로 작업할 때와 완전히 동일한 방법이며
* [MediaStore.Audio]도 마찬가지입니다. `Media.EXTERNAL_CONTENT_URI`가 무엇이든
* 아이템을 얻기 위한 쿼리가 기본이고, ID는 받을 문서입니다.
* 거기에 요청하십시오.
*/

val contentUri = ContentUris.withAppendedId(
MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
id
)

val image = MediaStoreImage(id, displayName, dateModified, contentUri)
images += image

// For debugging, we'll output the image objects we create to logcat.
// Log.e("", "Added image: $displayName $dateModified ${TimeUnit.SECONDS.toMillis(cursor.getLong(dateModifiedColumn))}")
}
}

이 코드는 android 개발자 페이지의 open 된 코드들 중에서 storeage-samples 안에 있는 MediaStore 프로젝트에서 가지고 왔음을 밝혀 둡니다.
 
https://github.com/android/storage-samples

GitHub - android/storage-samples: Multiple samples showing the best practices in storage APIs on Android.

Multiple samples showing the best practices in storage APIs on Android. - GitHub - android/storage-samples: Multiple samples showing the best practices in storage APIs on Android.

github.com

그걸 이용하면 이렇게 코드가 작성됩니다. 
 
여기에 다가 android 13에서 어떻게 적용할 것인 가 하는 부분에 대한 참조글은 처음에 인용한 글에서 참고했습니다. 
 
아무튼 이렇게 작성된 코드를 이용해서 앱 하나 만들어 playstore에 게시했습니다. ㅋ~
https://play.google.com/store/apps/details?id=com.billcorea.ftpclient0710 

사진백업 - Google Play 앱

NAS 로 내 폰의 사진을 백업해 드립니다. 자동으로 해 드릴 생각 이에요

play.google.com

 





오늘의 이야기


#스하리1000명프로젝트,
Terkadang sulit untuk berbicara dengan pekerja asing, bukan?
Saya membuat aplikasi sederhana yang membantu! Anda menulis dalam bahasa Anda, dan orang lain melihatnya dalam bahasa mereka.
Ini menerjemahkan secara otomatis berdasarkan pengaturan.
Sangat berguna untuk obrolan mudah. Lihatlah ketika Anda mendapat kesempatan!
https://play.google.com/store/apps/details?id=com.billcoreatech.multichat416




오늘의 이야기

장마속에서 맑은 하늘



장마 속에서 날 맑은 하늘...


 


어느 곳에서는 수마의 할큄에 모든 걸 잃어버리기도 했고...


 


세상을 떠나기도 했고...


 


가슴 아픈 사고가 있기도 했고...


 


오늘도 살아 있음에 감사하는 날이 되었기를...





오늘의 이야기

앱을 만들어 배포를 하다 보면 사용자가 업데이트를 놓치는 경우가 있을까 싶어서 앱을 실행할 때 자동으로 업데이트를 할 수 있도록 코드 구현을 하고 있습니다. 


 


물론 잠자는 시간에 충전을 하고 있다면 playstore 앱이 잘 알아서 업데이트를 해 주기도 합니다만, 그래도...


인앱 업데이트 deprecated



코드를 구현해 사용하다 보니 어느날 deprecated 되어 다는 이야기가 나옵니다. 안드로이드 버전이 올라가면서 나오는 현상이기는 하겠습니다만, 개발자 페이지를 찾아보겠습니다. 


 


개발자 페이지 설명



https://developer.android.com/reference/com/google/android/play/core/appupdate/AppUpdateManager



 


AppUpdateManager  |  Android Developers


Stay organized with collections Save and categorize content based on your preferences. Summary: Methods AppUpdateManager public interface AppUpdateManager com.google.android.play.core.appupdate.AppUpdateManager Manages operations that allow your app to ini


developer.android.com




 


어느 순간에 그걸 사용할 수 없다는 이야기가 나오고 그에 따르는 대응책을 알려 줍니다.  이제 그걸 따라해 보겠습니다.


 


변경후 코드 적용



 


코드가 한결 가벼워 졌습니다.  따라 하기도 수월해졌고요.


 


쉽게 구현해 보았습니다.





오늘의 이야기


#billcorea #운동동아리관리앱
🏸 Schneedle, aplikasi yang wajib dimiliki oleh klub bulu tangkis!
👉 Match Play – Rekam Skor & Temukan Lawan 🎉
Sempurna untuk di mana saja, sendirian, bersama teman, atau di klub! 🤝
Jika Anda suka bulu tangkis, cobalah

Buka aplikasi 👉 https://play.google.com/store/apps/details?id=com.billcorea.matchplay




오늘의 이야기

이전 글에서 정리할 것처럼 java에서 kotlin으로 이전을 했습니다.  그러고 나서 보기 시작했는 데,  DefaultSharedPrefernces의 사용할 수 없는 환경으로 변경이 된 것을 알게 되었습니다.    이전 prefs = Prefere...