레이블이 kotlin인 게시물을 표시합니다. 모든 게시물 표시

file.delete()를 사용했을 때 내가 의도한대로 동작하지 않는 경우가 있었다. file.delete()가 실패해서 false를 반환하는 원인은 여러가지가 있는데 몰랐던 사실을 알게 되어 원인 및 해결 방법을 간략하게 정리해보려고 합니다. 1. ...

file.delete()를 사용했을 때 내가 의도한대로 동작하지 않는 경우가 있었다.

file.delete()가 실패해서 false를 반환하는 원인은 여러가지가 있는데

몰랐던 사실을 알게 되어 원인 및 해결 방법을 간략하게 정리해보려고 합니다.








1. File.delete() Return False 원인

File.delete 메서드가 실패한(Return False) 원인에 대한 정보는 제공해 주지 않습니다.

삭제가 실패한 이유는 사용자가 직접 찾아야 합니다.

일반적으로 크게 아래 4가지가 원인이 될 수 있다고 생각하면 좋을 것 같습니다.


 - 권한 문제 (WRITE_EXTERNAL_STORAGE, READ_EXTERNAL_STORAGE)

 - 비어 있지 않은 디렉터리인 경우

 - 해당 파일이 실행되어 있는 경우

 - 해당 파일이 존재하지 않는 경우



저 같은 경우 비어 있지 않은 디렉터리를 삭제 시도했을 때 실패하는 상황이였어요.

(이번에 알게 된 사실...ㅠㅠ)

끝으로 비어있지 않은 디렉터리인 경우 삭제하는 방법을 알아보고 마무리 하겠습니다.








2. 디렉터리 삭제

비어있지 않은 디렉터리를 삭제하는 예시 입니다.

파일이 디렉터리인 경우 하위 파일들을 모두 비워주고 삭제하는 deleteFile() 메서드를 

작성해서 테스트해봤는데 잘 동작하네요 ㅎㅎ

val path = "/sdcard/Download/TEST" // 비어있지 않은 디렉터리
val file = File(path)
if (file.exists()) {
//file.delete() // return false
deleteFile(file) // return true
}


private fun deleteFile(file: File): Boolean {
if (file.isDirectory) {
val files = file.listFiles()
if (files != null) {
for (f in files) {
deleteFile(f)
}
}
}
return file.delete()
}







[Reference]

StackOverFlow





  ScrollView 또는 RecyclerView는 스크롤이 제공되는 View 입니다. 각각 스크롤 위치를 제어하는데 비슷하면서 조금 차이도 가지고 있어 함께 정리하였습니다.           1. ScrollView 스크롤 위치 제어 ScrollV...

 

ScrollView 또는 RecyclerView는 스크롤이 제공되는 View 입니다.

각각 스크롤 위치를 제어하는데 비슷하면서 조금 차이도 가지고 있어 함께 정리하였습니다.


 

 

 

 

 

1. ScrollView 스크롤 위치 제어

ScrollView 클래스의 "scrollTo", "smoothScrollTo" 메서드를 사용합니다.

해당 메서드를 사용하면 원하는 스크롤 위치로 제어가 가능합니다.

scrollTo는 즉시 이동시키고 smoothScrollTo는 부드럽게 이동시키는 차이를 가지고 있습니다.

사용 예시는 아래와 같습니다.

 

 

[스크롤 맨 위로 올리기]

//binding.scrollView.scrollTo(0, binding.scrollView.top) // 즉시 이동
binding.scrollView.smoothScrollTo(0, binding.scrollView.top) // 부드럽게 이동



[스크롤 맨 아래로 내리기]

//binding.scrollView.scrollTo(0, binding.scrollView.bottom) // 즉시 이동
binding.scrollView.smoothScrollTo(0, binding.scrollView.bottom) // 부드럽게 이동


 

[스크롤 특정 위치로 이동]

//binding.scrollView.scrollTo(0, position) // 즉시 이동
binding.scrollView.smoothScrollTo(0, position) // 부드럽게 이동

 

 

 

 

2. RecyclerView 스크롤 위치 제어

RecyclerView 클래스의 "scrollToPosition", "smoothScrollToPostion" 메서드를 사용합니다.

ScrollView와 다르게 RecyclerView 아이템 인덱스(Position) 위치를 인자로 사용됩니다.

마찬가지로 scrollToPosition는 즉시 이동, smoothScrollToPostion는 부드럽게 이동합니다.

사용 예시는 아래와 같습니다.

 

 

[스크롤 맨 위로 올리기]

//binding.rvItems.scrollToPosition(0) // 즉시 이동
binding.rvItems.smoothScrollToPosition(0) // 부드럽게 이동



[스크롤 맨 아래로 내리기]

val itemCount = recyclerViewAdapter.itemCount
if (itemCount > 0) {
//binding.rvItems.scrollToPosition(itemCount - 1) // 즉시 이동
binding.rvItems.smoothScrollToPosition(itemCount - 1) // 부드럽게 이동
}



[스크롤 특정 위치로 이동]

//binding.rvItems.scrollToPosition(position) // 즉시 이동
binding.rvItems.smoothScrollToPosition(position) // 부드럽게 이동

 

 

 

EditText를 사용하다보면 키보드를 내리고 싶은 경우가 있습니다. 예를 들면 위 그림과 같이 EditText 입력하고 검색 버튼을 누르면 텍스트 입력은 끝났으니 키보드가 내려갔으면 좋겠는데 계속해서 유지된 상태로 있습니다. 이와 같이 키보드를 내려...

EditText를 사용하다보면 키보드를 내리고 싶은 경우가 있습니다.

예를 들면 위 그림과 같이 EditText 입력하고 검색 버튼을 누르면

텍스트 입력은 끝났으니 키보드가 내려갔으면 좋겠는데 계속해서 유지된 상태로 있습니다.

이와 같이 키보드를 내려야하는 상황이 있을 수 있는데요,

간단한 예제로 남겨보려고 합니다.








1. EditText 키보드 내리기/올리기

아래와 같이 EditText 키보드를 내리는 동작은 아래와 같이 간단하게 구현 가능합니다.

private fun hideKeyboard(view: EditText) {
try {
val inputMethodManager = getSystemService(INPUT_METHOD_SERVICE) as InputMethodManager
inputMethodManager.hideSoftInputFromWindow(view.windowToken, 0)
} catch (e: Exception) {
e.printStackTrace()
}
}


추가로 원하는 EditText에 포커싱을 주고 키보드를 올려주는 동작도 아래와 같이 작성할 수 있습니다.

private fun showKeyboard(view: EditText) {
try {
view.requestFocus() // Focus to target editText

val inputMethodManager = getSystemService(INPUT_METHOD_SERVICE) as InputMethodManager
inputMethodManager.showSoftInput(view, 0)
} catch (e: Exception) {
e.printStackTrace()
}
}








2. 테스트 결과

MainActivity에서 몇 가지 버튼을 생성해서 간단하게 테스트해볼 수 있었습니다.

class MainActivity : AppCompatActivity() {

private lateinit var binding: ActivityMainBinding

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = DataBindingUtil.setContentView(this, R.layout.activity_main)

binding.btnSearch.setOnClickListener {
hideKeyboard(binding.etKeyword)
}

binding.btnShowKeyboard.setOnClickListener {
showKeyboard(binding.etKeyword)
}

binding.btnHideKeyboard.setOnClickListener {
hideKeyboard(binding.etKeyword)
}
}

...
}






날짜를 선택에 활용되는 DatePickerDialog 예제를 기록하려고 합니다. 해당 예제는 위 그림과 같이 시작 날짜 ~ 끝 날짜를 설정하는 내용을 담고 있습니다.             1. DatePickerDialog (kotlin) class ...

날짜를 선택에 활용되는 DatePickerDialog 예제를 기록하려고 합니다.

해당 예제는 위 그림과 같이 시작 날짜 ~ 끝 날짜를 설정하는 내용을 담고 있습니다.


 

 

 

 

 

 

1. DatePickerDialog (kotlin)

class MainActivity : AppCompatActivity() {
private lateinit var binding: ActivityMainBinding

private var startCalendar = Calendar.getInstance()
private var endCalendar = Calendar.getInstance()
private val dateFormat = SimpleDateFormat("yyyy-MM-dd")

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = DataBindingUtil.setContentView(this, R.layout.activity_main)

startCalendar.add(Calendar.MONTH, -1) // startCalendar 한달 전으로 설정

binding.btnStartDate.text = dateFormat.format(startCalendar.time)
binding.btnEndDate.text = dateFormat.format(endCalendar.time)

binding.btnStartDate.setOnClickListener {
// 날짜 선택 리스너 정의
val onDateSetListener = DatePickerDialog.OnDateSetListener { _, year, month, day ->
startCalendar.set(Calendar.YEAR, year)
startCalendar.set(Calendar.MONTH, month)
startCalendar.set(Calendar.DAY_OF_MONTH, day)

binding.btnStartDate.text = dateFormat.format(startCalendar.time)
}

// DatePickerDialog 생성, 날짜 선택 리스너 등록 및 날짜 설정
val datePickerDialog = DatePickerDialog(
this,
onDateSetListener,
startCalendar.get(Calendar.YEAR),
startCalendar.get(Calendar.MONTH),
startCalendar.get(Calendar.DAY_OF_MONTH)
)

// DatePickerDialog Show
datePickerDialog.show()
}

binding.btnEndDate.setOnClickListener {
// 날짜 선택 리스너 정의
val onDateSetListener = DatePickerDialog.OnDateSetListener { _, year, month, day ->
endCalendar.set(Calendar.YEAR, year)
endCalendar.set(Calendar.MONTH, month)
endCalendar.set(Calendar.DAY_OF_MONTH, day)

binding.btnEndDate.text = dateFormat.format(endCalendar.time)
}

// DatePickerDialog 생성, 날짜 선택 리스너 등록 및 날짜 설정
val datePickerDialog = DatePickerDialog(
this,
onDateSetListener,
endCalendar.get(Calendar.YEAR),
endCalendar.get(Calendar.MONTH),
endCalendar.get(Calendar.DAY_OF_MONTH)
)

// DatePickerDialog Show
datePickerDialog.show()
}
}
}

 

 

 

 

 

 

 

2. DatePickerDialog (java)

public class MainActivity extends AppCompatActivity {

private Calendar startCalendar = Calendar.getInstance();
private Calendar endCalendar = Calendar.getInstance();
private SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd");

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

Button btnStartDate = findViewById(R.id.btnStartDate);
Button btnEndDate = findViewById(R.id.btnStartDate);

startCalendar.add(Calendar.MONTH, -1); // startCalendar 한달 전으로 설정

btnStartDate.setText(dateFormat.format(startCalendar.getTime()));
btnEndDate.setText(dateFormat.format(endCalendar.getTime()));

btnStartDate.setOnClickListener(view -> {
// 날짜 선택 리스너 정의
DatePickerDialog.OnDateSetListener onDateSetListener = (v, year, month, day) -> {
startCalendar.set(Calendar.YEAR, year);
startCalendar.set(Calendar.MONTH, month);
startCalendar.set(Calendar.DAY_OF_MONTH, day);

btnStartDate.setText(dateFormat.format(startCalendar.getTime()));
};

// DatePickerDialog 생성, 날짜 선택 리스너 등록 및 날짜 설정
DatePickerDialog datePickerDialog = new DatePickerDialog(
this,
onDateSetListener,
startCalendar.get(Calendar.YEAR),
startCalendar.get(Calendar.MONTH),
startCalendar.get(Calendar.DAY_OF_MONTH)
);

// DatePickerDialog Show
datePickerDialog.show();
});

btnEndDate.setOnClickListener(view -> {
// 날짜 선택 리스너 정의
DatePickerDialog.OnDateSetListener onDateSetListener = (v, year, month, day) -> {
endCalendar.set(Calendar.YEAR, year);
endCalendar.set(Calendar.MONTH, month);
endCalendar.set(Calendar.DAY_OF_MONTH, day);

btnEndDate.setText(dateFormat.format(endCalendar.getTime()));
};

// DatePickerDialog 생성, 날짜 선택 리스너 등록 및 날짜 설정
DatePickerDialog datePickerDialog = new DatePickerDialog(
this,
onDateSetListener,
endCalendar.get(Calendar.YEAR),
endCalendar.get(Calendar.MONTH),
endCalendar.get(Calendar.DAY_OF_MONTH)
);

// DatePickerDialog Show
datePickerDialog.show();
});
}
}

 

 

 

 

3. layout 구성

<layout xmlns:android="http://schemas.android.com/apk/res/android">

<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"
android:orientation="horizontal">

<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="5dp"
android:gravity="center"
android:orientation="horizontal">

<Button
android:id="@+id/btnStartDate"
android:layout_width="160dp"
android:layout_height="wrap_content"
android:gravity="center"
android:textSize="17dp" />

<TextView
android:layout_width="20dp"
android:layout_height="wrap_content"
android:gravity="center"
android:text="~"
android:textSize="17dp" />

<Button
android:id="@+id/btnEndDate"
android:layout_width="160dp"
android:layout_height="wrap_content"
android:gravity="center"
android:textSize="17dp" />

</LinearLayout>
</LinearLayout>
</layout>

 

 

 

 

HTTPS 통신을 구현하다가 아래와 같은 오류를 보게 되었습니다. "CertPathValidatorException: Trust anchor for certification path not found" 이것저것 찾다보니까 몰랐던 내용...

HTTPS 통신을 구현하다가 아래와 같은 오류를 보게 되었습니다.

"CertPathValidatorException: Trust anchor for certification path not found"

이것저것 찾다보니까 몰랐던 내용들도 많고 해서

해당 오류에 대한 해결 방법을 간단하게 정리해보려고 합니다.


 

 

 

 

 

 

1. 원인

HTTP와 다르게 HTTPS는 SSL(Secure Socket Layer)이 더해져서 보안이 강화된 프로토콜입니다.

SSL을 활용한 통신은 클라이언트와 서버 간의 암호화된 통신을 하면서 외부로부터 보호하는 역할을 합니다.


HTTPS 통신을 하기 위해서는 SSL 인증서에 대해 확인하는 동작이 필요하며,

이는 클라이언트측 OkHttpClient을 통해 적용이 가능합니다.

이를 별도로 구현하지 않았을 경우 위와 같은 오류를 볼 수 있습니다..!


해당 오류없이 HTTPS 통신을 구현하는 방법은 크게 두가지로 나눠서 볼 수 있습니다.

 - 인증서 없이 HTTPS 연결 (안전하지 않은 방법)

 - 인증서를 활용하여 HTTPS 연결 (권장)

 


 

 

 

 

 

2. 인증서 없이 HTTPS 연결

https:// 도메인에 접속했을 때 아래와 같은 화면을 보신적이 있을겁니다!

이때 "위험을 감수하고 계속 진행" 또는 "안전하지 않음으로 이동"으로 접속하는 방법과 비슷하게 

인증서 없이 HTTPS 연결하는 방법으로보면 좋을 것 같습니다.



Retrofit 생성

fun getInstance(context: Context): Retrofit {
if (instance == null) {
// 안전하지 않음으로 HTTPS 연결 시도
val okHttpClient = unSafeOkHttpClient()

instance = Retrofit.Builder()
.baseUrl(serverUrl)
.addConverterFactory(GsonConverterFactory.create(gson))
.client(okHttpClient)
.build()
}
return instance!!
}


별도의 SSL 인증서 없이 우회하기

private fun unSafeOkHttpClient(): OkHttpClient {
val okHttpClient = OkHttpClient.Builder()
try {
val trustAllCerts: Array<TrustManager> = arrayOf(object : X509TrustManager {
override fun checkClientTrusted(chain: Array<out X509Certificate>?, authType: String?) {}
override fun checkServerTrusted(chain: Array<out X509Certificate>?, authType: String?) {}
override fun getAcceptedIssuers(): Array<X509Certificate> = arrayOf()
})

val sslContext = SSLContext.getInstance("SSL")
sslContext.init(null, trustAllCerts, SecureRandom())

if (trustAllCerts.isNotEmpty() && trustAllCerts.first() is X509TrustManager) {
okHttpClient.sslSocketFactory(sslContext.socketFactory, trustAllCerts.first() as X509TrustManager)
okHttpClient.hostnameVerifier { _, _ -> true }
}
} catch (e: Exception) {
e.printStackTrace()
}
return okHttpClient.build()
}

 

 

 

 

 

 

3. 인증서를 통해 HTTPS 접속하기

먼저 연결하고자하는 도메인의 인증서를 다운받아야합니다.

아래 화면과 같이 "인증서 보기"에서 다운받을 수 있습니다.




인증서 보기가 따로 없다면 아래 그림과 같이 주소창 왼쪽에 배치된 자물쇠 또는 뭔가 누를 수 있는 버튼을 클릭해서 다운 받을 수 있습니다.




다운받은 인증서를 raw에 배치시켜줍니다.

 

 


Retrofit 생성

fun getInstance(context: Context): Retrofit {
if (instance == null) {
// SSL 인증서로 HTTPS 연결 시도
val okHttpClient = sslOkHttpClient(context)

instance = Retrofit.Builder()
.baseUrl(serverUrl)
.addConverterFactory(GsonConverterFactory.create(gson))
.client(okHttpClient)
.build()
}
return instance!!
}


SSL 인증서 확인

private fun sslOkHttpClient(context: Context): OkHttpClient {
val okHttpClient = OkHttpClient.Builder()
try {
val cf = CertificateFactory.getInstance("X.509")
val caInput = context.resources.openRawResource(R.raw.my_cert)
var ca: Certificate? = null
try {
ca = cf.generateCertificate(caInput)
Log.d("[HTTP]", "ca=" + (ca as X509Certificate?)!!.subjectDN)
} catch (e: CertificateException) {
e.printStackTrace()
} finally {
caInput.close()
}

if (ca != null) {
val keyStoreType = KeyStore.getDefaultType()
val keyStore = KeyStore.getInstance(keyStoreType)
keyStore.load(null, null)
keyStore.setCertificateEntry("ca", ca)

val tmfAlgorithm = TrustManagerFactory.getDefaultAlgorithm()
val tmf = TrustManagerFactory.getInstance(tmfAlgorithm)
tmf.init(keyStore)

val sslContext = SSLContext.getInstance("TLS")
sslContext.init(null, tmf.trustManagers, null)

if (tmf.trustManagers.isNotEmpty() && tmf.trustManagers.first() is X509TrustManager) {
okHttpClient.sslSocketFactory(sslContext.socketFactory, tmf.trustManagers.first() as X509TrustManager)
okHttpClient.hostnameVerifier { _, _ -> true }
}
}
} catch (e: Exception) {
e.printStackTrace()
}
return okHttpClient.build()
}

 

 

 

 

 

 

 

3. 끝으로...(추가내용)

찾아보다가 해당 오류가 꼭 위와 같은이유로만 출력되는 오류는 아니였네요.

위의 내용은 제가 겪고 해결한 방법을 정리한 것이니 참고만 해주시고,

자세한 내용은 아래 공식문서를 확인해주세요!

Android Developer : 서버 인증서 확인과 관련된 일반적인 문제

 

그리고..

인증서 없이 연결했을 때와 인증서를 포함하여 연결했을 때의 차이를 확인하고싶었습니다.

대부분 인증서를 포함시키는걸 권장한다고는하는데...

정확히 어떤 차이가 생기는지를 알고 싶었지만 찾지 못했네요ㅠㅠ

혹시 차이를 알고 계신분이 있다면 댓글로 공유 부탁드립니다!!!

 







[Reference]

stackOverflow

Retrofit2로 SSL을 이용한 HTTPS 통신하기(Okhttp3)

HTTPS 통신 원리 쉽게 이해하기