The user 12345 does not meet the requirements to access device identifiers

안드로이드는 휴대폰을 구분할 수 있는 변경 불가능한 식별자(이하 편의상 UID, Unique identifier)를 제공해 왔다. 공식 문서에 언급된 API들은 아래와 같다.

Build나 TelephonyManager 클래스를 통해 제공되는 이 기능이 안드로이드 10(API29 혹은 Q OS)부터 개인정보 보호 강화 정책에 따라 추가 제약을 받는다고 한다. targetSdkVersion 기준 28까지는 READ_PHONE_STATE 권한을 가진 앱은 UID를 읽을 수 있으나, 29부터 READ_PRIVILEGED_PHONE_STATE 권한이 있어야 한다. 그러므로, TelephonyManager.getDeviceId() API를 각각 O OS와 Q OS에서 호출한다면 아래와 같이 동작할 것이다.

O OS + READ_PHONE_STATE 권한이 없을 때

Caused by: java.lang.SecurityException: getDeviceId: uid 10237 does not have android.permission.READ_PHONE_STATE.

O OS + READ_PHONE_STATE 권한이 있을 때

2019-12-31 14:59:56.528 32231-32231/me.sunphiz.android.myapplication2 D/sunphiz: deviceId= 230064929080355

Q OS + READ_PHONE_STATE 권한이 없을 때

Caused by: java.lang.SecurityException: getDeviceId: The user 10265 does not meet the requirements to access device identifiers.

Q OS + READ_PHONE_STATE 권한이 있을 때

2019-12-31 14:54:39.741 5812-5812/me.sunphiz.android.myapplication2 D/sunphiz: deviceId= null

Q OS + READ_PRIVILEGED_PHONE_STATE 권한이 있을 때

2019-12-31 14:59:56.528 32231-32231/me.sunphiz.android.myapplication2 D/sunphiz: deviceId= 230064929080355

그럼 READ_PRIVILEGED_PHONE_STATE 권한은 어떻게 얻을 수 있을까? 결론만 이야기하면, 다운로드 앱(구글 플레이 스토어 등에서 다운받아 설치한 앱)은 저 권한을 얻을 방법이 없다. 저 퍼미션이 선언된 시스템의 AndroidManifest.xml을 보면, protectionLevel이 signatureOrSystem이기 때문이다. 이 조건은 단말 제조사만 충족할 수 있으므로, 다운로드 앱은 targetSdkVersion 29부터 UID에 접근할 수 없다는 결론이 된다.

그럼 어떻게하란 말인가? UID를 이용하던 기능이 있다면, 아래 방법 중 하나를 적용해보자.

  1. targetSdkVersion을 28이하로 유지
  2. 광고 및 사용자 분석을 위한 고유 식별자‘로 대체 고려
  3. Android 식별자 사용 권장사항‘을 보고 기능 변경

그 밖에

UID는 변경이 불가능한 값이라 편리하다. 동시에, 내 서비스를 통해 값이 유출된다면 의도치 않은 사용자 피해가 생긴다. 조금 번거롭더라도 더 안전한 방향으로 개선하자.

참조

안드로이드의 다크 모드

다크 모드를 쉽게 설명하면, 어두운 배경에 밝은 글씨로 화면을 구성하는 것을 가리킨다. 오랫동안 다양한 서비스(이클립스, 인텔리제이 등)나 운영체제(윈도우 10, Mac OS 등)가 단순하게는 월페이퍼 변경 수준에서 복잡하게는 색상 구성(theme 혹은 appearance로 주로 표현)변경까지 다양한 방법으로 지원해 왔다. 스마트폰에서는 안드로이드의 리소스 관리에 한정자 값(qualifier value)으로 ‘night/nonight’가 API8에 포함되면서 기능을 사용할 방법이 열렸다. 본격적으로 삼성 갤럭시 One UI의 주요 기능으로써 P OS(API28)부터 지원을 시작한 후, 안드로이드(Android 10, Q OS 혹은 API29)아이폰(iOS 13)도 공식 지원하고 있다.

다크 모드에 설명이나 구현 방법에 대한 자료는 이미 인터넷에 충분하므로 여기서는, 다크 모드 자체에 대한 설명보다 ‘내 앱에도 적용해야 할까?’, 적용한다면 ‘내 앱에는 적용을 어떻게 할까?’라는 질문에 함께 고민해 보자.

해야 할까?

내 앱에 적용을 할지 고민하기 위해, 다크 모드의 장점을 먼저 살펴 보자. 보통 아래처럼 3가지를 꼽는다. 자세한 설명이 궁금하다면 이 글을 참고하자.

  1. 눈의 피로 개선
  2. 화면의 수면 연장
  3. 배터리 절약

세 꼭지 모두 좋은 말이지만, 앱 본연의 기능과 직접적 연관이 없다. 다크 모드가 적용된 기능이 불완전한 앱을 사용자가 좋아할까? 그럴리 없을 것이다. 그러니, 다크모드 보다는 기능을 우선하자. 다크 모드를 적용하기로 결정했다면 일반 모드(다크 모드를 사용하지 않을 때) 만큼 자연스럽게 제대로 만들자.

어떻게 할까?

다크 모드를 적용 하기로 결심했는가? 그럼, 다크 모드 적용 방법과 적용 범위에 따라 아래 같이 5가지로 분류해보자.

  • 강제 설정 기능 사용하기
    안드로이드 10은 어두운 테마 강제 설정 기능을 제공한다. 그러므로, 사용자의 핸드폰에 안드로이드 10이 설치되어 있다면 쉽게 다크 모드를 적용할 수 있다. 다만, 무엇이든 그렇듯이 알아서 되는 것이 내 입맛에 꼭 맞을리 없다. 안드로이드의 기능이니 꾸준히 나아지겠지만, 완벽을 기대하긴 어려울 것이다. 더 나은 다크 모드를 원한다면 테마와 스타일을 곁들이거나, 강제 설정 기능을 아예 활성화 하지 않고 다크 모드를 적용하길 권장한다.
    방법은 공식 가이드를 참고하자.
  • 적용하지 않기
    색상 구성이 서비스의 아이덴티티와 직결되는 경우라면, 전략적으로 적용하지 않을 수 있다.
    페이스북 앱이 검은색 바탕에 파란색 따봉을 띄울 때, 사용자가 어떻게 느낄지 상상이 잘 안된다. 또 백신처럼 기능에 치중하는 것이 미덕인 서비스라면, 사용자 요구가 커질 때 적용하는 것도 좋겠다.
  • 느낌만 살리기
    핸드폰이 다크 모드인 상태에서 실행 시, ‘로딩 중’ 같은 초기 화면에만 적용할 수 있다.
    슬랙(Slack)과 같은 앱이 한동안 이 형태로 다크 모드를 지원했다.
  • 중요 페이지만 적용하기
    메인 화면이나, 사용자가 주로 머무는 화면에 다크 모드를 적용하고 나머지는 그대로 둘 수 있다.
    구글 뉴스는 언론사와 같은 뉴스 제공자로부터 컨텐츠를 받아 큐레이션만 하고, 뉴스 선택 시 컨텐츠를 연결하는 방식으로 동작했다. 서비스 구조 상 컨텐츠 제어가 어렵다면 뉴스 목록 페이지만 다크 모드를 적용할 수 있다.
  • 완벽하게 적용하기
    장기적으로 대부분의 앱이 이렇게 할 것이다. 앞서 예로 든 슬랙이나 구글 뉴스도 과도기를 거쳐 현재는 완벽한 다크 모드를 지원한다. 다만, 이상적인 만큼 생각보다 품이 많이 든다.
    제조사 앱은 완벽하게 적용되어 있으며, 메이저 앱들 중 다수도 이미 완벽하게 지원하고 있다.
    적용 방법은 공식 문서를 참고하자.

그 밖에

  • 다크 모드를 지원하지 않는 낮은 OS가 설치된 휴대폰이라면, 다크 모드는 쓸 수 없다. 혹시, 삼성 휴대폰 사용자라면 갤럭시테마의 고대비테마를 통해 비슷한 경험과 장점을 누릴 수 있다.
  • 현재 Q OS의 안드로이드 OS 점유율은 2019년 5월 기준, 통계에 표시하지 않을 정도로 낮다. 강제 설정 기능만으로 다크 모드를 지원하면, 대부분의 사용자는 다크 모드를 경험할 수 없다.

참조

System 앱의 퍼미션 획득

설명하기 번거롭고 아무도 궁금해하지 않는 System 앱으로서 permission 획득(granted)하는 방법을 알아보자. 미리 이야기 할 것은, 안드로이드에 system이란 protectionLevel은 없다. signatureOrSystem이 있는데, 이 protectionLevel을 가진 permission의 획득 방법 두 가지 중 system 앱으로서 획득하는 방법에 대해 설명한다.

설명을 하기 위해서는 먼저 앱을 단말 내장 여부에 따라 구분해야 한다. 앱을 내장 여부에 따라 구분하는 일이 적어서 용어가 정착되지 않았는데, 여기서는 스토어/프리로드 앱으로 표현하겠다.

  • 스토어(store) 앱 : 단말에 내장되어 있지 않고, 단말 제조사나 구글의 앱 스토어에서 받아 설치(or 업데이트)할 수 있다. 일반 앱이나 서드파티 앱이라고도 부른다.
  • 프리로드(preload) 앱 : 단말에 내장되어 있고, 단말 제조사나 구글의 앱 스토어에서 업데이트 할 수도 있다.

프리로드 앱 중에서 특히 /system/priv-app/ 에 설치된 앱을 system 앱이라 한다. 혹은 프리빌리지드(privileged) 앱이라고도 부른다. 이 앱이 protectionLevel가 signatureOrSystem인 permission을 획득(granted)할 수 있다.

system 앱으로서 permission을 획득하는 방법을 설명했으니, permission에 영향을 줄 수 있는 system 앱의 특성을 살펴보자.

이를 위해서는 먼저 두 앱 간 설치/업데이트/삭제 시 차이점을 설명해야 한다. 스토어 앱은 최초 설치 시 /data/app/에 자리를 잡고, 업데이트 시에도 같은 위치에 덮어 씌운다. 삭제 시 업데이트 여부에 관계없이 /data/app/에 설치되었던 앱과 데이터가 사라진다.

프리로드 앱은 최초 설치 시 /system/priv-app/에 자리 잡고, 업데이트 시 /data/app/에 자리 잡는다. 삭제는 업데이트 여부에 따라 다르다. 최초 버전은 삭제할 수 없고, 업데이트 버전만 삭제할 수 있다. 삭제 시 /data/app/에 설치되었던 업데이트 버전의 앱이 사라진다.

이를 설명한 이유는 system 앱은 최초/업데이트 버전 여부에 따라 설치위치가 달라지고, 이에 따라 permission 획득도 달라질 수 있기 때문이다. 업데이트 버전에 <user-permission/> 선언을 신규추가한 경우 protectionLevel이 signatureOrSystem인 permission을 획득할 수 없다. 위에 설명한 것 처럼 업데이트 버전은 /system/priv-app/이 아닌 /data/app/에 설치되기 때문이다.

다만, 내장된 버전이 AndroidManifest의 <use-permission> 선언을 통해 이미 획득한 permission은 업데이트 버전도 선언만 하면 상속받는 것처럼 획득할 수 있다. 꼭 새 permission을 system 앱으로서 획득하고 싶다면 바이너리에 포함해야 한다.

그 밖에

  • 안드로이드의 permission에 4가지 protectionLevel에 대한 소개는 이 글을 확인하자.
  • 프리로드 앱은 system 앱 외에 다른 종류도 있다. 최초 부팅 시에 설치 되어있지만, 삭제 가능하며 단말 초기화(factory reset) 시 다시 설치된다. 이들은 /data/app/에 설치되므로 단말에 내장된 점을 제외하고 일반 앱과 같다.
  • 처음에 system앱은 /system/이하에 설치되는 앱을 말하며 system앱 모두가 권한을 가졌다. 그러나, /system/priv-app/에 설치되는 앱에만 권한을 주도록 정책이 축소&변경된다.  (아마도) 이 때 system앱과 구별하기 위해 previleged 앱이라 부르다 현재는 특별한 권한을 가진 앱을 가리킨다는 점에서 혼용하는 것 같다.

참조

 

startActivity가 안돼요

startActivity 메소드(startActivityForResult 메소드 포함)는 파라미터로 전달된 인텐트(Intent) 정보로 액티비티를 실행한다. 안드로이드 책 처음마다 이 메소드 소개는 빠지지 않는다. 그렇게 많이 사용하는데, 왜 안될까?

확인이 쉬운 것부터 살펴보자. 액티비티 호출을 위해 만드는 인텐트 유형은 명시적 인텐트와 암시적 인텐트로 나뉜다. 인텐트를 잘 만들었는지 살펴보자.

  • 명시적 인텐트는 호출할 대상 패키지와 패키지 속 액티비티 정보를 넣어야 한다. 명시적 인텐트를 통해 호출을 시도했으나 에러가 난다면 패키지나 액티비티 이름이 틀린 것은 아닌지 확인해보자.
  • 암시적 인텐트는 액션(action)을 이용하므로, 액션이 틀렸는지 확인하자.

intent에 넣은 정보가 맞는데도 안되는 경우도 있다. 그럼, 다음 경우도 생각해 보자.

  • 암시적 인텐트를 사용할 때 액션과 함께 전달하는 추가정보가 잘못 되었는지 확인하자.  예를 들면, AndroidManifest.xml에 액티비티가 처리할 수 있는 URL 스키마를 선언할 수 있다. 이 설정과 전달하는 url 데이터가 맞지 않다면 실행되지 않는다.
  • 퍼미션이 필요한지 확인하자. 안드로이드의 컴포넌트는 자신을 호출하는 앱이 퍼미션을 갖고(granted)있어야 실행하도록 강제할 수 있다. 퍼미션이 없다면 Exception이 발생한다.
  • 액티비티가 앱 외부에서 공개되지 않게 설정되었을 수도 있다. AndroidManifest.xml 내의 액티비티 컴포넌트 선언부의 android:exported 어트리뷰트가 true인지 확인하자. 참고로, 기본값이 true다.
  • intent에 넣은 정보가 맞는데도 실행이 안된다면 앱이 설치되었는지 확인하자. 앱 슬롯이나 셋팅 내 어플이케이션 목록을 통해 확인할 수 있다.
  • 설치가 되었는데도 실행되지 않는다면 앱이 비활성화 됐을 수 있다. 셋팅 내 어플이케이션 목록에서 앱을 찾아, 사용하지 않기(비활성화)로 둔 것은 아닌지 확인해보자.
  • 혹시, MAIN 액션과 LAUNCHER 카테고리를 가지는 일명 메인 액티비티가 실행되지 않는다면 이 글을 읽어보자.

그 밖에

액티비티를 실행할 때는 가능하다면 암시적 인텐트를 이용하자.

  • 같은 액션을 활용하는 모든 앱이 사용자에게 표시되므로 사용자에게 선호하는 앱을 이용할 수 있게 선택권을 줄 수 있다.
  • 호출할 액티비티의 패키지 이름이 바뀌더라도 명시적으로 컴포넌트를 선언하지 않기 때문에, ActivityNotFoundException과 같은 문제가 발생하지 않는다.
  • 액티비티 정보는 쉽게 알 수 없으니 명시적 인텐트가 안전하다고 생각하는 경우가 있다. dumpstate의 설치된 앱 정보 등을 통해 액티비티를 알아낼 방법은 다양하므로, 호출은 모두 가능하다. 잘못된 값이 전달될 위험은 값을 validation하여 대응하고, 사용 자체를 막고자 한다면 퍼미션을 활용하자.

참고

메인 액티비티 실행이 안돼요

일반적으로 앱은 아이콘이 있다. 아이콘을 누르면 진입점 역할의 액티비티가 실행된다. 이 액티비티를 편의상 메인 액티비티라고 부르자.

메인 액티비티를 만드는 법은 간단하다. AndroidManifest.xml에 선언된 액티비티 중 메인으로 만들고 싶은 것을 골라 아래와 같이 MAIN 액션LAUNCHER 카테고리가 포함된 인텐트 필터를 추가하면 된다.

<intent-filter>
     <action android:name="android.intent.action.MAIN" /> 
     <category android:name="android.intent.category.LAUNCHER" />
</intent-filter>

위와 같이 인텐트 필터를 선언하면, 런쳐는 메인 액티비티로 판단하여 앱 슬롯에 아이콘을 표시한다.

그럼, 코드로 메인 액티비티를 실행해보자. 내 앱이라면 패키지 이름과 컴포넌트 이름으로 명시적 호출도 가능하다. 여기서는 암시적 인텐트를 이용해 보자.

adb shell am start <패키지이름>

패키지 이름만 건네면 앱의 메인 액티비티를 찾아 실행하는 명령어다. 위 명령어로 앱들을 실행해보면 어떤 앱은 실행되고 어떤 앱은 실행되지 않는다. 왜 그럴까?

실행여부는 <intent-filter>에 android.intent.category.DEFAULT가 있어야 있느냐에 따라 달라진다. DEFAULT 카테고리가 있어야 패키지 이름만으로 실행할 수 있는 것이다.

숨겨진 액티비티도 아니고 아이콘이 노출된 앱인데 코드로 실행할 수는 없다니 이상하다. 게다가, 메인 액티비티는 공개된 액티비티이므로 android:exported=”true”도 강제라고 한다. 그럼 외부에서 실행하는 것을 보장하겠다는 뜻이 아닌가?

이런 앞뒤가 맞지 않는 상황 때문인지 안드로이드의 PackageManager 클래스는  API3부터 getLaunchIntentForPackage()라는 설명이 재미있는 API를 제공한다.

Returns a "good" intent to launch a front-door activity in a package. 
...(이하 생략)

메인 액티비티를 실행하는 “좋은” 인텐트를 만들어 반환한다고 API 설명이 되어 있다. 하지만, 이 API도 MAIN 액션, LAUNCHER 카테고리를 가진 액티비티를 찾아 반환하는 것이므로 여전히 인텐트 필터는 필요하다.

메인 액티지티를 만들 때 DEFAULT 카테고리도 잊지 말고 추가하도록 하자. 다른 앱이 내 앱을 한 번이라도 더 실행하도록 말이다.

그 밖에

  • 한 앱에서 MAIN, LAUNCHER 속성을 2개 이상의 액티비티에 동시에 선언할 수 있으며, 이 때 아이콘이 각각 생긴다.
  • 아이콘의 레이블은 액티비티의 레이블을 따라간다. 그러므로, 아이콘을 다르게 여러 개 만들 수도 있다.

참조