문제점

bluetoothAdapter.startDiscovery() return false !!!

true로 반환 되어야 하는데 false 로 반환 되어 블루투스 검색을 할 수 없음

안드로이드 10 버전에서 위치 관련 보안이 강화 되었는데 블루투스 검색 할때 위치 관련 퍼미션이 필요하기 때문에 일어난 퍼미션 관련 이슈였습니다.

버전 별로 블루투스 검색 및 연결 관련 퍼미션을 알아보겠습니다.

기본적으로 블루투스 기능 ON , 위치 기능 ON 상태 여야 합니다.

블루투스 기능 OFF 상태일 때는 사용 가능 하게 팝업 띄어 주고 위치 기능 OFF 상태일때는 체크 해서 설정 화면으로 보내면됩니다.

targetSdkVersion 29 미만인 경우

AndroidManifest.xml 파일

<uses-permission android:name="android.permission.BLUETOOTH" />
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/>
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />

29 미만인 경우 위치 관련 퍼미션이 적용 되어 있을 경우 아래 퍼미션이 자동으로 추가 됩니다.

<uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION"/>

아래 퍼미션은 런타임 퍼미션 적용
Manifest.permission.ACCESS_COARSE_LOCATION

targetSdkVersion 29 인 경우

AndroidManifest.xml 파일 

<uses-permission android:name="android.permission.BLUETOOTH" />
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/>
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION"/>

아래 퍼미션은 런타임 퍼미션 적용

Manifest.permission.ACCESS_COARSE_LOCATION,
Manifest.permission.ACCESS_FINE_LOCATION,
Manifest.permission.ACCESS_BACKGROUND_LOCATION

퍼미션 승인 후 bluetoothAdapter. startDiscovery() 이 true로 반환되는걸 확인 할 수 있었습니다.

아래 이미지는 블루투스 연결 버튼 클릭 시 플로우 차트

API 호출 할 때 다른 터치를 막기 위해 센터 프로그레스를 보여주는 경우 여러 가지 방법이 있지만 그중 progressDialog 를 사용해보자~!

BaseActivity 에서 progressDialog 를 가지고 있고 필요할 때 showProgress() , hideProgress() 를 통해 보여주거나 감출수 있다.

abstract class BaseActivity : AppCompatActivity(){
    private var progressDialog: AppCompatDialog? = null

    fun showProgress() {
        if (this == null || isFinishing) {
            return
        }

        progressDialog?.apply {
            dismiss()
        }

        progressDialog = AppCompatDialog(this)
        progressDialog?.apply {
            setCancelable(false)
            window?.setBackgroundDrawable(ColorDrawable(Color.TRANSPARENT))
            setContentView(R.layout.progress_center_loading)
            show()
        }
    }

    fun hideProgress() {
        progressDialog?.dismiss()
    }
}

아래는 layout 코드

<?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"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:clickable="true">

    <ProgressBar
        style="?android:attr/indeterminateDrawable"
        android:layout_width="34dp"
        android:layout_height="34dp"
        android:indeterminate="true"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>

이 경우 장점은 BaseActivity 를 상속 받는 Activity 의 경우 showProgress , hideProgress 를 통해 간편하게 사용할 수 있다.

그리고 액티비티 layout에서 위의 layout 코드를 포함 하고 있지 않아도 된다.

단점은 필요 하지 않은 Activity 에서도 progressDialog 변수를 가지고 있을 수 있다.

다른 방법으로는 Application class 에서 가지고 있거나 싱글톤 클래스로 가지고 있어도 괜찮을것 같다.

데이터 바인딩라이브러리는 레이아웃의 UI 구성 요소를 프로그래밍 방식이 아닌 선언적 형식을 사용하여 앱의 데이터 소스에 바인딩 할 수있는 지원 라이브러리다.

<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:bind="http://schemas.android.com/tools"
    xmlns:tools="http://schemas.android.com/tools">
    <data>
        <import type="android.view.View" />
        <variable
            name="viewModel"
            type="패키지경로.ExampleViewModel" />
    </data>
    <androidx.constraintlayout.widget.ConstraintLayout
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:background="@{viewModel.isRedBackground ? @color/red : @color/white}">
    </androidx.constraintlayout.widget.ConstraintLayout>
</layout>

예를 들어 API 호출을 하고 받아온 값이 빨간색 배경색을 가져야 한다면 viewModel.isRedBackground (ObservableField Boolean 값) 데이터 변경 만으로 뷰가 빨간색 또는 흰색으로 변경 된다. 하지만 상태값이 빨간색 흰색 그외 초록색이 필요하다면 BindingAdapter를 통해서 구현할 수 있다.

android:background=”@{viewModel.isRedBackground ? @color/red : @color/white}”
–> bind:status=”@{viewModel.status}”

위의 코드에서 해당 줄만 변경한다. 그 다음 BindingAdapter를 만들어준다.

@BindingAdapter("bind:status")
fun bindBuyingStatusText(imageView: ImageView, status: String?) {
    when (status) {
        "빨간색" -> {
            imageView.setBackgroundColor(imageView.context.getColor(R.color.color_red))
        }
        "흰색" -> {
            imageView.setBackgroundColor(imageView.context.getColor(R.color.color_white))
        }
        else -> {  //초록색
            imageView.setBackgroundColor(imageView.context.getColor(R.color.color_green))
        }
    }
}

상태값이 2가지만 있는경우에도 BindingAdapter 사용해서 처리할수 있지만 굳이 사용할 필요는 없을것 같다.

집에 와서 안드로이드 단톡방 쭉쭉 보다가 어떤 분이 질문 올린 글을 봤다.
Rx 공부 해야지 해야지 생각 하다가 안하고 있었는데 마침 문제가 하나 생김 ㅎㅎ

문제는 RxKotlin 을 사용해서 입력창 세가지가 있는데 각각 입력창에서 입력 할 때마다 validation 체크를 하고 다 통과 되면 버튼을 활성화 시키고 싶다.

키워드는 combineLatest 이다. 아래의 TestCode 를 보자.

class CombineLatestTest {
    @Test
    fun test() {
        var isValid = false
        val emailSubject = BehaviorSubject.create<String>()
        val passwordSubject = BehaviorSubject.create<String>()
        val nickNameSubject = BehaviorSubject.create<String>()
        BehaviorSubject.combineLatest<String, String, String, Boolean>(
            emailSubject,
            passwordSubject,
            nickNameSubject,
            Function3<String, String, String, Boolean> { email, password, nickname ->
                return@Function3 email.isNotEmpty() && password.isNotEmpty() && nickname.isNotEmpty()
            }
        ).subscribe { result ->
            isValid = result
             println("활성화 여부 $isValid")
        }

        emailSubject.onNext("이메일 입력")
        assertEquals(isValid, false)

        passwordSubject.onNext("패스워드 입력")
        assertEquals(isValid, false)

        nickNameSubject.onNext("닉네임 입력")
        assertEquals(isValid, true)

        passwordSubject.onNext("")  //패스워드 제거
        assertEquals(isValid, false)

        passwordSubject.onNext("패스워드 입력")  //패스워드 다시 입력
        assertEquals(isValid, true)

    }

}


    결과는 
    활성화 여부 false
    활성화 여부 false
    활성화 여부 true
    활성화 여부 false
    활성화 여부 true


combineLatest 은 파라미터로 ObservableSource 를 받고 마지막에 리턴할 Function 을 받는다.
ObservableSource 갯수 제한은 9개 까지로 확인했다.
테스트 코드라서 isNotEmpty 로 처리 해뒀는데 저기 부분이 isValid 로 변경 되면 되겠다~

기본적으로 Android Studio 에서 프로젝트를 만들게 되면 기본적으로 Build Variants가 Debug 로 셋팅 되어 있다.

Android SDK 도구에서 생성된 디버그 인증서를 사용하여 앱에 자동으로 서명하기 때문이다.

안드로이드 앱을 출시 하기 위해서는 Release KeyStore 를 통해 서명 된 앱이 필요하다.

Release KeyStore 를 생성해서 서명한 앱을 출시 하고 난 뒤, Release 용 apk 가 설치 된 앱에서 Debug 모드로 빌드하게 되면 빌드 실패가 뜨고 삭제 후 설치 하시겠습니까? 라는 팝업이 뜬다.

이 이유는 Release apk 와 Debug apk 인증 정보가 다르기 때문이다.

이렇게 되면 삭제 후 설치는 할 수 있지만 Build Variants 가 변경될 때마다 다시 설치해야하는 번거러움도 있고 삭제하지 않고 디버깅 하고 싶은 상황에서 할 수 없게 된다.

이 상황을 해결 하기 위해서는 Release Keystore 로부터 Debug Keystore 를 만들면 된다.

keytool -importkeystore -v -srckeystore 릴리즈용 키스토어 파일이름 및 경로 -destkeystore 디버그용 키스토어 파일이름 및 경로 -srcstorepass 릴리즈용 키스토어 비밀번호 -deststorepass android -srcalias alias이름 -destalias androiddebugkey -srckeypass alias비밀번호 -destkeypass android

새로 만드는 Debug Keystore 를 Android Studio 기본 제공해주는 Debug Keystore 와 매칭 시켜주는 작업이다.
-destalias androiddebugkey , -deskkeypass android 는 Android SDK 도구에서 생성된 디버그 인증 정보와 동일하게 해서 나중에 별도 셋팅이 필요없다.

build.gradle 셋팅은 아래와 같이 하면 된다.

android {
 signingConfigs {
        release {
            storeFile file('릴리즈 키스토어 경로')
            storePassword '패스워드'
            keyAlias '릴리즈 alias'
            keyPassword '패스워드'
        }

        debug {
            storeFile file('디버그 키스토어 경로')
            //따로 정보 필요 없음
        }
    }
}