반응형

Night mode 의 각 모드에 대한 설명과 각 모드가 어떻게 동작하는지 예제를 만들어보았습니다.

 

먼저 Night mode는 아래와 같은 모드를 가지고 있습니다.

모드명 설명 참고
MODE_NIGHT_UNSPECIFIED 야간 모드에 대해 지정되지 않은 모드입니다. 기본 야간 모드를 사용하기 위해 이것은 주로 setLocalNightMode ()와 함께 사용되어 기본 야간 모드를 사용할 수 있도록합니다.

기본 모드와 로컬 야간 모드가 모두 이 값으로 설정된 경우에는MODE_NIGHT_FOLLOW_SYSTEM의 기본값이 적용됩니다.
MODE_NIGHT_FOLLOW_SYSTEM 시스템의 설정에 따라서 동작  
MODE_NIGHT_AUTO_TIME 시간(일출/일몰)에 따른 동작 Deprecated 됨
MODE_NIGHT_NO 동작안하도록 함  
MODE_NIGHT_YES 동작하도록 함  
MODE_NIGHT_AUTO_BATTERY 절전모드시 동작하도록 함  

실제 Night mode 동작 확인을 위해 각 모드별 버튼을 만들어서 버튼을 클릭한 이후 어떻게 동작하는지 확인합니다.

class MainActivity() : AppCompatActivity(){
    override fun onCreate(savedInstanceState: Bundle?) {
        val modeNightText = mapOf(-100 to "MODE_NIGHT_UNSPECIFIED", -1 to "MODE_NIGHT_FOLLOW_SYSTEM",
                0 to "MODE_NIGHT_AUTO_TIME", 1 to "MODE_NIGHT_NO",
                2 to "MODE_NIGHT_YES", 3 to "MODE_NIGHT_AUTO_BATTERY")

        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        val textModeNight : TextView = findViewById(R.id.tvModeNight)

        findViewById<View>(R.id.btnModeNightFollowSystem).setOnClickListener {
            AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM)
            textModeNight.text = modeNightText[AppCompatDelegate.getDefaultNightMode()]
        }
        findViewById<View>(R.id.btnModeNightAutoTime).setOnClickListener {
            AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_AUTO_TIME)
            textModeNight.text = modeNightText[AppCompatDelegate.getDefaultNightMode()]
        }
        findViewById<View>(R.id.btnModeNightNo).setOnClickListener{
            AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_NO)
            textModeNight.text = modeNightText[AppCompatDelegate.getDefaultNightMode()]
        }
        findViewById<View>(R.id.btnModeNightYes).setOnClickListener {
            AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_YES)
            textModeNight.text = modeNightText[AppCompatDelegate.getDefaultNightMode()]
        }
        findViewById<View>(R.id.btnModeNightAutoBattery).setOnClickListener {
            AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_AUTO_BATTERY)
            textModeNight.text = modeNightText[AppCompatDelegate.getDefaultNightMode()]
        }

        textModeNight.text = modeNightText[AppCompatDelegate.getDefaultNightMode()]
    }
}

 

나이트 모드별 버튼을 가지고 있고, 마지막 버튼 아래에는 현재 동작중인 모드가 무엇인지 테스트 표시됩니다.

 

시간(주/야), 절전여부, 시스템 설정(Night mode 여부)에 따른 동작 결과

주간 / 절전 x / 시스템 설정 (Night mode x)

 

주간 / 절전 x / 시스템 설정 (Night mode o)

 

주간 / 절전 o / 시스템 설정 (Night mode x)  

 

주간 / 절전 o / 시스템 설정 (Night mode o)

 

야간 / 절전 x / 시스템 설정 (Night mode x)

 

야간 / 절전 x / 시스템 설정 (Night mode o)

 

야간 / 절전 o / 시스템 설정 (Night mode x)

 

야간 / 절전 o / 시스템 설정 (Night mode o)

 

 

반응형

안드로이드 레이아웃 XML의 android:visibility 속성은 View를 보이거나 사라지게 하는 역할을 하며 3가지 상태(visible, invisible, gone)가 있습니다.

 

디폴트는 visible이라서 별도로 아래처럼 명시하지 않더라도 View는 보이게 됩니다.

다른 2가지 상태인 invisible과 gone은 View를 사라지게 합니다.

다만 이 둘의 차이는 invisible은 View 공간을 유지하면서 사리지고, gone은 View 공간을 유지하지 않고 사라집니다.

 

이게 무슨 의미가 있겠냐 싶지만 분명히 존재의 의미가 있고 필요하다는 것을 아래 예를 들어보도록 하겠습니다.

 

먼저 간단하게 아래와 같이 이미지 버튼으로 4개와 1개의 텍스트 뷰로 구성된 ConstraintLayout을 만들었습니다.

가운데 3개의 이미지 버튼들은 Spread 스타일의 Chain으로 만들어져 있습니다.

위쪽 이미지 버튼과 아래쪽 텍스트 뷰의 경우 각각 가운데 이미지 버튼들과 가장자리 위치의 중간되는 곳에 위치하고 있습니다.

 

정중앙 버튼의 android:visibility 속성을 invisible과 gone으로 변경해서 테스트해보겠습니다.

관찰 포인트는 가로로 버튼들이 균등하게 위치 여부,

                  상단 이미지 버튼과 하단의 텍스트뷰의 원래 위치 유지 여부 입니다.

 


#테스트 1 (android:visibility="invisible" 로 설정한 경우)

아래 왼쪽 그림에 정중간 버튼이 사라진 것이 확인되고, 오른쪽 그림(Blueprint)에는 정중간 버튼이 공간을 유지하고 있는 것이 확인됩니다. 다른 뷰와의 연결도 문제가 없고, 모든 뷰들의 위치도 동일합니다.

다만, 가운데 이미지 버튼들이 하나 사라졌음에도 나머지 2개의 버튼들이 다시 정렬되어서 균등하게 위치되지 않고 원래 자리를 유지하고 있습니다.

용도: 다른 뷰의 위치와 크기에 영향을 주지 않고 사라지게 할 목적인 경우에 유용


#테스트 2 (android:visibility="gone" 으로 설정한 경우)

마찬가지 중간 버튼이 사라졌지만, 오른쪽 그림(Blueprint)에는 원래 자리를 차지하고 있던 위치에 공간이 제거되었으며 다른 뷰들의 위치가 조금씩 이동된 것이 확인됩니다.

위치가 변경되면 안되는 상단의 이미지 버튼과 하단의 텍스트 뷰까지 조금씩 위치가 바뀌었습니다. (마지막에 추가 설명)

그렇지만, 가운데 이미지 버튼들의 경우 남아있던 나머지 2개가 다시 정렬되면서 균등하게 위차하고 있습니다.

용도: 사라지는 뷰와 연동해서 다른 뷰들의 위치나 크기 조절이 필요한 경우에 유용

상단의 이미지 버튼과 하단의 텍스트 뷰의 경우 위치가 이동이 되었는데 이유는 아래 그림을 참조하시면 됩니다.

정중앙 버튼(id=>imageButtonCenter)의 Bottom과 Parent의 Bottom 간의 중앙에 위치하도록 되어 있는데, 정중앙 버튼이 사라지면서 공간도 제거되고 연결된 것도 우측 이미지 버튼의 가운데를 가르키면서 전체적으로 위쪽으로 조금씩 이동하게 된 것입니다.

<TextView
    android:id="@+id/textView"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:text="Are you ready?"
    android:textAppearance="@style/TextAppearance.AppCompat.Large"
    app:layout_constraintBottom_toBottomOf="parent"
    app:layout_constraintLeft_toLeftOf="parent"
    app:layout_constraintRight_toRightOf="parent"
    app:layout_constraintTop_toBottomOf="@+id/imageButtonCenter" />

 

이제 어떤 경우에 View의 invisible 또는 gone을 사용하는지 이해가 되셨을 것으로 생각됩니다.

이번 글은 여기까지입니다. 읽어주셔서 감사합니다.

반응형

기본적으로 uses-permission과 사용하는 의미가 동일합니다. 즉, 앱에서 이러한 권한을 사용한다고 알려주는 것입니다.

 

먼저 AndroidManifest.xml 에서 아래 형식을 가지고 있습니다.

<uses-permission-sdk-23 android:name="string"
       
android:maxSdkVersion="integer" />

 

이전에는 uses-permission-sdk-m을 사용했지만, 현재 Deprecated 되어서 uses-permission-sdk-23을 사용합니다.

 

그냥 uses-permission을 사용하면 되는데 왜 uses-permission-sdk-23이 필요할까요?

 

그리고, uses-permission-sdk-22나 uses-permission-sdk-24는 왜 존재하지 않고 23만 존재할까요?

 

uses-permission-sdk-23은 API version 23 (Android 6.0) 이상이 설치된 디바이스에서 권한을 사용한다는 의미입니다.

 

이는 API version 23인 Android 6.0에서 새롭게 추가된 Runtime permission 때문입니다.

 

앱이 필요한 권한의 수준이 위험(Dangerous)인 경우 보안을 위해서 실행중 필요시 사용자에 권한 허용여부를 결정하도록 하는 기능입니다. 자세한 내용은 아래글 참조하세요.

2019/02/07 - 안드로이드 런타임 권한 확인 및 요청 처리하기 (Android Runtime permission check and request)

 

위험 권한을 사용한 새로운 기능을 추가했습니다. 그런데, 그 기능이 [Android 6.0 이상에만 유효하거나] 앱을 동작하는데 [필수적인 기능이 아니다]라고 한다면, Runtime permission을 지원하는 Android 6.0 이상에는 해당 기능 사용을 실행중 발생하는 권한 요청시 사용자가 결정할 수 있습니다. 

 

반면, 설치시 권한 승인을 해야 하는 Android 6.0 미만 디바이스에는 사용하지 않는데 권한 승인을 해줘야 하거나 필수적인 기능이 아님에도 강제로 추가됩니다.

 

이런 문제를 해결하기 위해서 [use-permission-sdk-23]을 사용하게 되면 Android 6.0 이상 디바이스에만 권한을 추가하며 그 미만 디바이스에서는 권한이 추가되지 않기에 설치시 권한 승인 및 강제로 기능이 추가되지 않습니다.

 

주로 Android 6.0 미만 디바이스까지 지원하는 앱에서 신규 기능을 추가하면서 겪게 될 수 있는 부분입니다.

 

참고로 maxSdkVersion의 경우 향후 최신 API 버전을 가진 디바이스에서 권한이 없어서 되는 경우에 그 직전 API 버전을 적어주면 됩니다.

 

여기까지 입니다. 도움이 되셨기를...

반응형

앱에서 시스템 설정 변경

 

앱(3rd party app.)에서 안드로이드 설정(Setting)을 읽어오거나 변경이 필요한 경우가 있습니다.

 

여기서 설정은 아래 그림처럼 설정 앱(com.android.settings, System app.) 또는 퀵세팅 타일의 항목을 말합니다.

 

이후 "앱"이라고 언급하면 3rd party app.으로 생각하시면 됩니다.

 

 

 

하나의 항목에는 이름(Name)과 값(Value)이 짝으로 기록되어집니다.

이는 SharedPreference의 키와 값이 짝으로 기록되는 데이터 저장방식과 유사합니다.

 

예로 들면 위 그림에서 설정 앱의 밝기 자동 조절 항목의 경우 이름과 (사용할 수 있는) 값은 아래와 같이 되어 있습니다.

/**
* Control whether to enable automatic brightness mode.
*/
public static final String SCREEN_BRIGHTNESS_MODE = "screen_brightness_mode";
/**
* SCREEN_BRIGHTNESS_MODE value for manual mode.
*/
public static final int SCREEN_BRIGHTNESS_MODE_MANUAL = 0;

/**
* SCREEN_BRIGHTNESS_MODE value for automatic mode.
*/
public static final int SCREEN_BRIGHTNESS_MODE_AUTOMATIC = 1;

그리고, 이 이름과 값은 아래 경로에 저장됩니다.

 

[Android 6.0 이전] 

 /data/data/com.android.providers.settings/databases/settings.db

 

[Android 6.0 이후]

/data/system/users/0/settings_system.xml

/data/system/users/0/settings_secure.xml

/data/system/users/0/settings_global.xml

 

앱에서 위와 같은 Database나 파일에 접근하기 위해서는 Content provider를 이용해야 합니다.

당연히 설정(Settings)에서도 이를 위한 SettingsProvider라는 Native Content provider가 존재합니다. 

 

하지만, 앱에서 설정에 접근하기 위해서 주로 사용하는 방법은 SettingsProvider의 설정 항목을 쿼리해서 참조할 수 있도록 만든 Settings 클래스(android.provider.settings)를 이용하는 것입니다.

 

Settings 클래스는 SettingsProvider에 대한 접근을 단순화해주는 클래스라고 보면 됩니다.

 

Settings-SettingsProvider 관련한 자세한 설명과 분석은 나중에 기회가 되면 따로 설명해 드리고 여기서는 Settings 클래스에 대해 간략한 설명만 하도록 하겠습니다.

 

NameValueTable 클래스

- 설정 항목의 데이터(이름과 값)를 데이터베이스나 파일에 기록하는 기본 클래스입니다.

 

Global, Secure, System 클래스

 - 설정 항목의 유형에 따라서 NameValueTable 클래스를 상속받은 Global, Secure, System 클래스가 사용됩니다.

 

Global의 경우 MultiUser(Android 4.2 버전에서 도입)와 관련됩니다. MultiUser를 위한 공용 설정 데이터 접근 용도입니다.

Secure의 경우 보안 관련한 설정 데이터 접근 용도입니다.

System의 경우 시스템과 관련한 설정 데이터 접근 용도입니다.

 

참고로 System 항목이었지만 이후에 Global 또는 Secure로 이동된 항목들도 많이 있습니다.

 

설정 항목 읽기

 

value = Settings.Global|Secure|System.getInt|getLong|getFloat|getString(ContentResolver cr, String name)

 

설정 항목 유형과 데이터 종류에 따라서 여러 조합으로 만들어집니다.

 

▶ 밝기 조절 모드 구하기

int brightnessMode = Settings.System.getInt(getContentResolver(), Settings.System.SCREEN_BRIGHTNESS_MODE);

 

▶ 위치 정보 모드 구하기

int locationMode = Settings.Secure.getInt(getContentResolver(), Settings.Secure.LOCATION_MODE);

 

▶ 개발자 옵션 - 디버깅할 앱 패키지명 구하기

String debugApp = Settings.Global.getString(getContentResolver(), Settings.Global.DEBUG_APP);

 

참고로 getInt, getLong, getFloat을 사용할 때 설정값이 존재하지 않는 경우에 대한 아래와 같은 에러 핸들링이 필요합니다.

try {
locationMode = Settings.Secure.getInt(getContentResolver(), Settings.Secure.LOCATION_MODE);
} cath (Settings.SettingNotFoundException e) {
// Error handling
}

또는 설정값이 존재하지 않는 경우의 디폴트 값을 세 번째 파라미터에 미리 정해줄 수도 있습니다.

int locationMode = Settings.Secure.getInt(getContentResolver(), Settings.Secure.LOCATION_MODE, Settings.Secure.LOCATION_MODE_NONE);

 

설정 쓰기 권한

 

<android.permission.WRITE_SETTINGS>

 

설정을 읽는 것과 달리 설정에 쓰는 것에는 권한이 필요하고 제한도 따릅니다.

"설정 쓰기" 권한 획득 없이 설정 항목에 기록하려고 하면 아래의 오류를 만나게 됩니다.

 

우선 작업은 AndroidManifest.xml 파일에 아래와 같이 "설정 쓰기" 권한을 넣어주는 것입니다.

<uses-permission android:name="android.permission.WRITE_SETTINGS"
tools:ignore="ProtectedPermissions"/>

안드로이드 6.0 이전까지는 이렇게만 해주면 앱의 설치단계에서 "설정 쓰기" 권한을 얻게 되어 설정 항목 변경이 가능합니다.

 

안드로이드 6.0 이후부터는 "설정 쓰기" 권한은 보호 수준이 서명 권한(Signature permission)이라서 플랫폼과 동일한 키로 서명된 앱만 정상 권한처럼 설치단계에서 권한을 얻을 수 있지만, 이렇게 동작할 수 있는 것은 시스템 앱의 경우에만 해당합니다.

 

이에 앱에서는 Settings 클래스의 ACTION_MANAGE_WRITE_SETTINGS 인텐트를 보내서 사용자의 승인을 요청합니다.

 

위험 권한(Dangerous permission)을 대상으로 하는 일반적인 런타임 권한 처리와는 차이가 있습니다.

 

일반적인 런타임 권한 요청에 대해서는 아래 글을 참조하시면 됩니다.

2019/02/07 - 안드로이드 런타임 권한 확인 및 요청 처리하기 (Android Runtime permission check and request)

 

 

 

런타임 권한 확인은 "checkSelfPermission()"이 사용되었는데, 설정 쓰기 권한 확인은 Settings.System.canWrite()를 이용합니다. 

 

아래 코드를 보면, Settings.System.canWrite()로 권한 확인해서 없는 경우에 토스트 메시지를 띄우고, 메시지가 사라지면서 시스템 설정 수정을 위한 액티비티로 넘어가게 되어 있습니다.

private boolean permissionCheckAndRequest() {
boolean permission;

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
permission = Settings.System.canWrite(getApplicationContext());
} else {
permission = ContextCompat.checkSelfPermission(getApplicationContext(), Manifest.permission.WRITE_SETTINGS)
== PackageManager.PERMISSION_GRANTED;
}

mHasWriteSettingsPermission = permission;

if (!permission) {
Toast.makeText(getApplicationContext(), "시스템 설정(밝기 조절 세팅, 화면 회전)을 변경하기 위해서 시스템 변경할 수" +
" 있는 권한이 필요합니다." +
"\n잠시 후에 시스템 설정 변경 창으로 이동합니다. 권한을 [허용]해주세요.", Toast.LENGTH_LONG).show();

new Handler().postDelayed(new Runnable() {
@Override
public void run() {
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.M) {
Intent intent = new Intent(Settings.ACTION_MANAGE_WRITE_SETTINGS);
intent.setData(Uri.parse("package:" + getPackageName()));
startActivityForResult(intent, WRITE_SETTINGS_PERMISSION_REQUEST_CODE);
} else {
ActivityCompat.requestPermissions(MainActivity.this, new String[]{Manifest.permission.WRITE_SETTINGS},
WRITE_SETTINGS_PERMISSION_REQUEST_CODE);
}
}
}, 3500);

return false;
}
return true;
}

아래 그림이 "설정 쓰기" 권한을 얻기 위해서 ACTION_MANAGE_WRITE_SETTINGS 인텐트를 보내면 전환되는 액티비티입니다.

단말 업체별 텍스트 등의 차이는 있습니다만 권한 활성화를 위한 스위치를 갖춘 UI로 되어 있는 것은 동일합니다.

 

 

권한 획득을 위해서 스위치 활성화하고 앱으로 복귀하면 onActivityResult()로 들어오는데 다시 한번 Settings.System.canWrite()를 실행해서 권한 여부를 확인합니다. (권한 획득하지 못한 경우는 종료처리)

@Override
protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {
super.onActivityResult(requestCode, resultCode, data);

boolean permission;
if (requestCode == WRITE_SETTINGS_PERMISSION_REQUEST_CODE) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
permission = Settings.System.canWrite(this);
} else {
permission = ContextCompat.checkSelfPermission(getApplicationContext(), Manifest.permission.WRITE_SETTINGS)
== PackageManager.PERMISSION_GRANTED;
}

mHasWriteSettingsPermission = permission;

if (permission) {
initBrightness();
mButtonRotation.setVisibility(View.VISIBLE);
} else {
Toast.makeText(getApplicationContext(), "시스템 설정(밝기 조절 세팅)을 변경을 위한 권한이 없어서 앱을 종료하였습니다",
Toast.LENGTH_LONG).show();
finish();
}
}
}

설정 항목 쓰기

 

Settings.Global|Secure|System.putInt|putLong|putFloat|putString(ContentResolver cr, String name,

                                                                                           int|long|float|String value)

 

설정 항목의 유형 중에 Global과 Secure는 시스템 앱에서만 쓰기가 가능합니다.

 

시스템 앱이 아닌 경우에 Global과 Secure 항목을 변경하려고 하면 아래와 같은 오류가 발생합니다.

이는 AndroidManifest.xml에 android.permission.WRITE_SECURE_SETTINGS를 추가하더라도 발생하는 현상입니다.

 

즉, 앱에서는 System 유형의 설정 항목만 쓰기가 가능합니다.

 

▶ 밝기 조절 모드를 수동으로 변경

    Settings.System.putInt(getContentResolver(), Settings.System.SCREEN_BRIGHTNESS_MODE,

                        Settings.System.SCREEN_BRIGHTNESS_MANUAL);

 

▶ 화면을 자동 회전 설정

    Settings.System.putInt(getContentResolver(), Settings.System.ACCELEROMETER_ROTATION, 1);

 

▶ 화면을 180도 회전

    Settings.System.putInt(getContentResolver(), Settings.System.USER_ROTATION, 2);

 

▶ 버튼음/터치 소리 활성화

    Settings.System.putInt(getContentResolver(), Settings.System.SOUND_EFFECTS_ENABLED, 1);

 

▶ 폰트 배율을 2로 조정

    Settings.System.putFloat(getContentResolver(), Settings.System.FONT_SCALE, 2.0f);

 

 

시스템 설정 변경을 이용한 화면 밝기 및 화면 방향 조절 예제

 

레이아웃 (activity_main.xml)

- 밝기의 설정값을 보여주는 TextView

- 화면 밝기 조절을 위한 SeekBar

- 화면 방향 조절을 위한 Button

 

 

<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout 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:background="@drawable/food_1898194_1280"
tools:context=".MainActivity">

<TextView
android:id="@+id/tvBrightness"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="8dp"
android:background="#40000000"
android:text="화면 밝기값"
android:textColor="@android:color/white"
android:textSize="20dp"
android:textStyle="bold"
android:visibility="gone"
app:layout_constraintBottom_toTopOf="@+id/tvBrightnessValue"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" />

<TextView
android:id="@+id/tvBrightnessValue"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="8dp"
android:background="#40000000"
android:text=""
android:textColor="@android:color/white"
android:textSize="20dp"
android:textStyle="bold"
android:visibility="gone"
app:layout_constraintBottom_toTopOf="@+id/BrightnessSeekBar"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" />

<SeekBar
android:id="@+id/BrightnessSeekBar"
android:layout_width="255dp"
android:layout_height="20dp"
android:max="255"
android:progress="1"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="0.75" />

<Button
android:id="@+id/btnRotation"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:text="화면 회전"
android:visibility="gone"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/BrightnessSeekBar" />
</android.support.constraint.ConstraintLayout>

자바 (MainActivity.java)

※설정 쓰기 권한 처리 소스는 위에서 언급되어 있습니다.

 

화면 밝기 조절을 위한 SeekBar 리스너 / 화면 방향 조절을 위한 Button 리스너 처리

 - changeScreenBrightness에 SeekBar로 조절한 값 전달

 - 화면 방향 조절 버튼은 누르면 바로 시스템 설정에 수정 반영 / 누를 때마다 방향이 변경되도록 처리

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mTextViewBrightnessValue = findViewById(R.id.tvBrightnessValue);

mSeekBarBrightness = findViewById(R.id.BrightnessSeekBar);
mSeekBarBrightness.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {
@Override
public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
mTextViewBrightnessValue.setText(String.valueOf(progress));
changeScreenBrightness(progress);
}

@Override
public void onStartTrackingTouch(SeekBar seekBar) {
}

@Override
public void onStopTrackingTouch(SeekBar seekBar) {
}
});

mButtonRotation = findViewById(R.id.btnRotation);
mButtonRotation.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Settings.System.putInt(getContentResolver(), Settings.System.USER_ROTATION, ++nRotationValue%4);
}
});
}

화면 밝기 조절

 - 넘어온 SeekBar 값으로 실제 화면 밝기 조절하는 코드

 - 넘어온 SeekBar 값을 화면 밝기 설정값으로 저장하는 코드(changeBrightnessSystemSetting)

private void changeScreenBrightness(int value) {
Window window = getWindow();
WindowManager.LayoutParams layoutParams = window.getAttributes();
layoutParams.screenBrightness = value * 1.0f / 255;
window.setAttributes(layoutParams);
changeBrightnessSystemSetting(value);
}

 

private void changeBrightnessSystemSetting(int value) {
    ContentResolver cResolver = getContentResolver();
    Settings.System.putInt(cResolver, Settings.System.SCREEN_BRIGHTNESS_MODE, Settings.System.SCREEN_BRIGHTNESS_MODE_MANUAL);
    Settings.System.putInt(cResolver, Settings.System.SCREEN_BRIGHTNESS, value);
}

화면 밝기 백업 / 복원

 - 앱 실행 전 화면 밝기 저장

 - 앱 종료 때 저장된 화면 밝기로 복원

@Override
protected void onResume() {
super.onResume();

if (!isFinishing() && permissionCheckAndRequest()) {
initBrightness();
initRotation();
}
}

@Override
protected void onPause() {
super.onPause();
if (mHasWriteSettingsPermission) {
restoreBrightnessSystemSetting();
}
}

private void backupBrightnessSystemSetting() {
mBrightnessModeBackup = Settings.System.getInt(getContentResolver(), Settings.System.SCREEN_BRIGHTNESS_MODE,
Settings.System.SCREEN_BRIGHTNESS_MODE_MANUAL);
mBrightnessValueBackup = Settings.System.getInt(getContentResolver(), Settings.System.SCREEN_BRIGHTNESS, 128);
Toast.makeText(getApplicationContext(), "[Screen brightness system setting]" +
"\n\nMode: " + ((mBrightnessModeBackup==Settings.System.SCREEN_BRIGHTNESS_MODE_AUTOMATIC) ?"Automatic":"Manual")
+ ((mBrightnessModeBackup==Settings.System.SCREEN_BRIGHTNESS_MODE_AUTOMATIC)?"":"\nValue: " + mBrightnessValueBackup),
Toast.LENGTH_LONG).show();
}

private void restoreBrightnessSystemSetting() {
Settings.System.putInt(getContentResolver(), Settings.System.SCREEN_BRIGHTNESS_MODE, mBrightnessModeBackup);
if (mBrightnessModeBackup == Settings.System.SCREEN_BRIGHTNESS_MODE_MANUAL) {
Settings.System.putInt(getContentResolver(), Settings.System.SCREEN_BRIGHTNESS, mBrightnessValueBackup);
}
}

메니페스트(AndroidManifest.xml)

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
package="kr.happydev.systemsettingcontrol">
<uses-permission android:name="android.permission.WRITE_SETTINGS"
tools:ignore="ProtectedPermissions"/>

<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:theme="@style/AppTheme">
<activity android:name=".MainActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN" />

<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>

</manifest>

프로젝트 압축파일

SystemSettingControl.zip

 

이번 글은 여기까지입니다. 읽어주셔서 감사합니다.

반응형

안드로이드 권한(Permission)은 시스템의 보안과 개인정보 보호를 위해 존재합니다.

 

이 권한은 Protection level로 구분된 일반 권한(Normal permissions)위험 권한(Dangerous permissions)으로 나눠집니다.

 

이것 외에도 시스템 앱(Preload 앱)에 사용될 수 있는 Protection level 종류가 더 있지만, 일반 앱 개발자의 경우 사용할 수 있는 것은 위의 두 가지 Protection level에 해당하는 권한들만 사용할 수 있습니다.

 

Protection level은 아래 링크를 참조하시길 바랍니다.

https://developer.android.com/reference/android/R.attr#protectionLevel

 

[일반 권한 / 위험 권한]

일반 권한

 

시스템이나 개인정보 보안 등의 위험을 초래하지 않기 때문에 앱에서 권한을 요청만 하면 자동으로 권한을 얻게 되어서 접근할 수 있습니다.

 

▶ 일반 권한 확인 방법

[Play 스토어에서 앱을 설치 시 확인 방법]

일반 권한만 존재하는 앱은 아래 왼쪽 그림과 같이 바로 다운로드 되면서 설치됩니다. (Android 5.1 이하 단말)

Play 스토어의 해당 앱 설명 하단에 보면 "앱 권한" 이 있는데 확인을 하면 아래 오른쪽 그림과 같이 나옵니다. (모든 단말)

 - 아래의 앱("또 다른 지식의 성전" 게임)에서는 "일반 권한"만 존재합니다.

 

 

[설치된 앱에서 확인 방법]

"앱 정보" 진입(설정-애플리케이션-해당 앱)후 "권한" 확인 시 아래 왼쪽 그림과 같이 "위험 권한"은 존재하지 않고, 빨간색으로 표시된 "옵션 더 보기"를 누르면 아래 오른쪽 그림과 같이 "일반 권한"이 존재하는 것을 확인할 수 있습니다.

 

 

위 앱의 네트워크 접근 권한은 인터넷 권한(android.permission.INTERNET)으로 레퍼런스에 확인 시 "일반 권한"임을 알 수 있습니다.

 


 

위험 권한

 

시스템이나 개인정보 보안 등의 위험을 초래할 수 있기 때문에 사용자가 앱이 요청한 권한을 확인하고 승인해야 비로소 앱에서 접근할 수 있습니다.

앱이 카메라, 블루투스, 지문인식, 네트워크, NFC 등 시스템의 장치를 사용하거나 메시지, 전화번호, 일정과 같은 민감한 사용자 정보를 이용하기 위해서는 사용자에게 권한 승인을 얻은 이후 접근할 수 있습니다.

앱은 사용자가 승인하지 않으면 카메라를 사용할 수 없고, 전화번호를 읽어 올 수도 없습니다.

 

▶ 위험 권한 확인 방법

[Play 스토어에서 앱을 설치 시 확인 방법]

위험 권한이 포함된 아래("Send Anywhere" 앱)의 경우 설치 시 사용자에게 권한에 대한 승인을 받는 창이 나옵니다.

(단말이 Android 5.1 이하인 경우 또는 단말이 Android 6.0 이상이지만 앱의 targetSdkVersion이 22 이하인 경우)

Android 6.0 이상의 단말에서 targetSdkVersion이 23 이상이면 "위험 권한"이 존재하더라도 권한 승인 창이 나오지 않고 바로 설치됩니다. 이것은 나중에 "런타임 권한 확인"이 가능하기 때문에 그렇게 동작하는 것입니다.

 

[설치된 앱에서 확인 방법]

앱 정보 진입(설정-애플리케이션-해당 앱)후 "권한" 확인 시 아래 왼쪽과 같이 앱에서 사용하는 "위험 권한" 목록이 나오고 여기에서 권한 설정 및 해제도 가능합니다. 아래 오른쪽은 앱을 최초 실행했을 때 "위험 권한" 사용 요청을 위한 런타임 권한 요청하는 것을 확인할 수 있습니다.

 

 

 

[런타임 권한 (Runtime permission)]  

Android 6.0에서 최초 도입이 되었습니다. 이는 앱 설치 시 권한 요청을 하는 것이 아니라, 앱 실행 중 권한이 필요할 때 요청하는 것을 의미하며 요청에 대해 거부도 할 수 있으며, 승인/거부 상황에 맞게 처리하는 루틴이 고려해야 합니다.

 

요청을 하는 대상은 "위험 권한" 항목만 해당합니다. "일반 권한"은 자동으로 권한을 부여받습니다.

 

Android 6.0에 도입되었지만, 앱에서 targetSdkVersion을 23 이상으로 해주어야 실제 런타임 권한으로 동작합니다.

 

몇 달 전에 구글에서는 신규 및 기존 앱 업데이트 시 targetSdkVersion을 26이상으로 변경해야 등록되도록 정책을 변경하였습니다.
(관련 링크:
https://support.google.com/googleplay/android-developer/answer/113469?hl=ko#targetsdk)

 

그러므로, 이제부터는 구글플레이에 등록해야 하는 신규 앱은 처음부터 런타임 권한이 지원하도록 만들어야 하고, 기존 등록된 앱이 아직 targetSdkVersion을 23 이상으로 올리지 않았다고 하면 향후 업데이트 대비해서 런타임 권한이 지원시 문제가 될 수 있는 부분이 있는지 미리 파악해두시는 것이 좋을 것 같습니다.

 

런타임 권한 지원하기

 

아래와 같은 순서도로 동작하는 런타임 권한을 지원하도록 하겠습니다.

꼭 이렇게 해야 하는 것은 아니고, 하나의 예를 보여드리기 위함입니다.

 

 

 

런타임 권한과 관련된 API

권한 부여 여부: checkSelfPermission()

권한 요청: requestPermissions()

권한 요청 콜백: onRequestPermissionsResult()

사용자에게 설명이 필요한 상황 확인: shouldShowRequestPermissionRationale()

 


 

◈런타임 권한 지원하기 실제 예

지금부터는 런타임 권한 지원을 어떻게 하면 되는지 예제를 보면서 설명드리겠습니다. 아래에 있는 예제를 사용할 것입니다.

2018/10/13 - 손전등 앱 만들기 #2 - Camera2 API 이용방법 (Flashlight app using Camera2 API)

 

#1 targetSdkVersion 변경에 따른 이상 동작 확인

위에 언급한 예제는 아래와 같이 targetSdkVersion이 21로 런타임 권한 지원이 되지 않습니다.

android {
compileSdkVersion 27
defaultConfig {
applicationId "com.example.help.permissionflashlight3"
minSdkVersion 21
targetSdkVersion 21

먼저 런타임 관련한 코드 수정 없이 targetSdkVersion을 23으로 바꾸면 어떻게 되는지 확인해보겠습니다.

 

앱에서 카메라 장치에 접근(open)하는 순간 Exception이 발생해서 강제종료(Force close)됩니다. 

 

이는 targetSdkVersion이 23으로 되면서 "위험 권한"에 해당하는 카메라의 경우 자동으로 권한을 부여받지 못하고, 런타임 권한 요청에 의한 권한을 부여받아야 하기에 최초의 경우 권한이 없는 상태입니다. 그래서, CameraService 연결하려고 하면 권한 검사를 통과하지 못해 ERROR_PERMISSION_DENIED 상태로 연결 거부(Reject)되었음을 전달해주며 그 결과 CameraManger에서 SecurityException을 throw해서 Exception이 발생하게 됩니다.

[CameraService.cpp]

 

// If it's not calling from cameraserver, check the permission.
if (callingPid != getpid() &&
!checkPermission(String16("android.permission.CAMERA"), clientPid, clientUid)) {
ALOGE("Permission Denial: can't use the camera pid=%d, uid=%d", clientPid, clientUid);
return STATUS_ERROR_FMT(ERROR_PERMISSION_DENIED,
"Caller \"%s\" (PID %d, UID %d) cannot open camera \"%s\" without camera permission",
clientName8.string(), clientUid, clientPid, cameraId.string());
}
[CameraManager.java]

 

public static void
throwAsPublicException(Throwable t) throws CameraAccessException {
if (t instanceof ServiceSpecificException) {
ServiceSpecificException e = (ServiceSpecificException) t;
int reason = CameraAccessException.CAMERA_ERROR;
switch(e.errorCode) {
case ICameraService.ERROR_DISCONNECTED:
reason = CameraAccessException.CAMERA_DISCONNECTED;
break;
case ICameraService.ERROR_DISABLED:
reason = CameraAccessException.CAMERA_DISABLED;
break;
case ICameraService.ERROR_CAMERA_IN_USE:
reason = CameraAccessException.CAMERA_IN_USE;
break;
case ICameraService.ERROR_MAX_CAMERAS_IN_USE:
reason = CameraAccessException.MAX_CAMERAS_IN_USE;
break;
case ICameraService.ERROR_DEPRECATED_HAL:
reason = CameraAccessException.CAMERA_DEPRECATED_HAL;
break;
case ICameraService.ERROR_ILLEGAL_ARGUMENT:
case ICameraService.ERROR_ALREADY_EXISTS:
throw new IllegalArgumentException(e.getMessage(), e);
case ICameraService.ERROR_PERMISSION_DENIED:
throw new SecurityException(e.getMessage(), e);
case ICameraService.ERROR_TIMED_OUT:
case ICameraService.ERROR_INVALID_OPERATION:
default:
reason = CameraAccessException.CAMERA_ERROR;
}
throw new CameraAccessException(reason, e.getMessage(), e);
} else if (t instanceof DeadObjectException) {
throw new CameraAccessException(CameraAccessException.CAMERA_DISCONNECTED,
"Camera service has died unexpectedly",
t);
} else if (t instanceof RemoteException) {
throw new UnsupportedOperationException("An unknown RemoteException was thrown" +
" which should never happen.", t);
} else if (t instanceof RuntimeException) {
RuntimeException e = (RuntimeException) t;
throw e;
}
}

SecurityException에 의한 Fatal exception 대응 코드

아래와 같이 try ~ catch 사용으로 예외처리

try {
mCameraManager.openCamera(mCameraId, new CameraDevice.StateCallback() {
...
}, null);
} catch (SecurityException e) {
e.printStackTrace();
}

#2 SecurityException의 root cause인 Runtime Permission 적용

SecurityException가 발생한 이유는 targetSdkVersion이 23으로 변경되었지만, RuntimePermission이 적용되어있지 않아서 Permission이 허용되지 않는 상태에서 문제가 된 API를 호출해서 발생하였습니다.

 

위의 순서도에 맞게 추가 작업한 부분입니다. (빨간색 코드)

 

최초 앱 실행 시 checkSelfPermission()을 호출해서 권한 부여 여부를 확인합니다.

권한을 이미 부여받은 상태이면, 원래의 액티비티(R.layout.activity_main)을 호출하고, 그렇지 않으면 권한 요청을 위한 새로 만든 액티비티(R.layout.activity_permission)을 호출하도록 하였습니다.

 

 

위의 권한 요청 액티비티(R.layout.activity_permission)에서 "요청하기" 버튼을 누르면 requestPermissions()를 호출해서 권한 요청을 하면 아래와 같은 "권한 요청 시스템 팝업"이 발생합니다.

 

 

위의 "권한 요청 시스템 팝업"에서 선택(거부/허용)한 결과는 onRequestPermissionsResult()로 콜백됩니다.

아래는 이와 관련된 기존 예제에서 추가된 코드입니다.

@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
if (requestCode == REQUEST_CAMERA) {
if (grantResults.length > 0) {
if (grantResults[0] == PackageManager.PERMISSION_GRANTED) {
recreate();
} else if (grantResults[0] == PackageManager.PERMISSION_DENIED) {
if (ActivityCompat.shouldShowRequestPermissionRationale(this, Manifest.permission.CAMERA)) {
Toast.makeText(MainActivity.this, "잠시 후에 다시 권한 요청을 합니다.\n플래시를 사용하기 위해서는" +
" [카메라]권한이 필요합니다.", Toast.LENGTH_LONG).show();
new Handler().postDelayed(new Runnable() {
@Override
public void run() {
ActivityCompat.requestPermissions(MainActivity.this,
new String[]{Manifest.permission.CAMERA},
REQUEST_CAMERA);
}
}, 3500);
} else {
Toast.makeText(MainActivity.this, "잠시 후에 앱 정보로 이동합니다. 권한 항목에서" +
" [카메라]를 허용해주세요.", Toast.LENGTH_LONG).show();
new Handler().postDelayed(new Runnable() {
@Override
public void run() {
Intent intent = new Intent();
intent.setAction(Settings.ACTION_APPLICATION_DETAILS_SETTINGS);
intent.setData(Uri.fromParts("package", getPackageName(), null));
startActivity(intent);
}
}, 3500);
}
}
}
} else {
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
}
}

콜백에서 권한이 승인(PERMISSION_GRANTED)으로 넘어오면 recreate()를 호출해서 액티비티를 종료시키고 다시 액티비티(=MainActivity)를 실행하도록 하면 checkSelfPermission()에선 권한 부여받은 것으로 처리되어서 원래의 액티비티(R.layout.activity_main)를 호출해서 targetSdkVersion 23 이전처럼 앱이 정상동작하게 됩니다.

 

콜백에서 권한이 거부(PERMISSION_DENIED)로 넘어온 이후 두 번째 권한 요청할 때부터는 "권한 요청 시스템 팝업"에 "다시 묻지 않음" 체크 버튼이 추가되어서 나옵니다. 그래서, 위의 경우에 아래 왼쪽처럼 토스트 메시지가 나오고, 토스트가 사라질 때쯤에 오른쪽과 같이 "권한 요청 시스템 팝업"이 발생합니다.

 

 

위의 "권한 요청 시스템 팝업"에서 거부를 하면 다시 콜백되어서 토스트 메시지와 "권한 요청 시스템 팝업"이 다시 나오게 됩니다. 거부한 경우에 계속 반복적으로 요청하는 루틴으로 되어 있습니다. 이건 순서도에 있는 것처럼 이렇게 동작하도록 시나리오를 만든 것입니다. 이 부분의 처리는 각자 생각한 시나리오에 맞게 적절히 변경해주시면 됩니다.

 

만약 "다시 묻지 않음"을 체크한 경우에는 아래 왼쪽처럼 "허용"은 비활성화되고, "거부"만 선택할 수 있게 됩니다.

이때 "거부"를 선택하게 되면, 콜백에서 아래 가운데처럼 토스트 메시지가 나오도록 처리하고, 토스트가 사라질 때쯤 오른쪽과 같이 "앱 정보" 화면으로 넘어가도록 처리되어 있습니다.

 

참고: 2018/11/02 - 애플리케이션 정보 (Application info.) 보기로 이동

 

  

 

아래 왼쪽과 같이 "앱 정보"의 권한 항목에서 "카메라"를 허용해주고 앱으로 돌아가서 "요청하기" 버튼을 누르면 권한이 허용된 상태이기 때문에 원래의 액티비티(R.layout.activity_main)로 넘어갑니다.

 

"앱 정보"의 "카메라"를 허용하지 않는 상태로 앱으로 돌아가서 "요청하기" 버튼을 누르면 "다시 묻지 않음"이 체크된 상태라서 "권한 요청" 시 "권한 요청 시스템 팝업"이 나오지 않고 곧바로 "앱 정보"를 호출(아래 오른쪽 참고)하게 됩니다.

 

 

 

앱 동작 영상

 

프로젝트 화일

PermissionFlashlight3_support_runtime_permission.zip

여기까지입니다. 읽어주셔서 감사합니다.

 

 

 

 

반응형

안녕하세요. Simple& Happy Dev입니다.

 

앱 개발할 때 ANR, App crash, Native crash 등 많은 오류를 만납니다.

 

오류 발생할 때 빠른 로그 확보는 유의미한 오류 발생 직전 로그를 많이 확보할 수 있도록 합니다.

 

이번 글에서는 오류 발생할 때 빠른 버그 리포팅을 위해서 어떤 준비를 하면 좋은지와 이전 오류 정보들을 보관하고 있는 tombstone과 dropbox를 알아보도록 하겠습니다.

 

빠른 버그 리포팅을 위한 준비

 

- 전원 메뉴에 [버그 신고 버튼 추가] 하기

"설정 - 개발자 옵션 - 버그 신고 바로가기"를 활성화하면 전원 버튼을 누르면 메뉴에 버그 리포트 생성할 수 있는 메뉴가 생깁니다.

 

오류 발생할 때 전원버튼을 누르고, "버그 보고서 작성 또는 버그 신고"를 누르면, 버그 리포트가 생성됩니다.

 

 

 

이는 오류 발생할 때 버그 리포트 생성 메뉴 이동 단계를 줄여줍니다.

 


 

- 삼성 단말의 경우 SYSDUMP로 버그 리포트 생성할 때 [NOTIBAR RUN DUMPSTATE: ON] 하기

 

 

오류 발생할 때 알림창을 내려서 "Run DumpState"를 누르면, 버그 리포트가 생성됩니다.

 

 

이것도 오류 발생시 버그 리포트 생성 메뉴 이동 단계를 줄여줍니다.

 


 

- 삼성 단말의 경우 SYSDUMP로 버그 리포트 생성할 때 [Modem Log 제외] 하기

 

 

일반적으로 앱 개발에서는 Modem 로그가 필요하지 않습니다. 불필요한 시간을 줄일 수 있습니다.

 

Sysdump에 관한 추가 설명은 아래 참고하세요.

2018/11/30 - Bugreport 및 Sysdump (Samsung) 사용 방법

 

 

이전 오류 정보가 기록된 tombstone과 dropbox

 

오류가 발생했으나 버그 리포트를 생성하지 않고 넘어간 경우라도 이후에 확인하는 방법이 있습니다.

 

[tombstone]

tombstone의 경우에 주로 native crash 발생할 때 정보(abort message, register, backtrace, stack, memory map)가 저장됩니다.

 

※native crash가 tombstone에 기록되는 과정은 아래 dropbox의 native crash 내용을 참조하시길 바랍니다.

 

#define MAX_TOMBSTONES  10
#define TOMBSTONE_DIR   "/data/tombstones"
#define TOMBSTONE_TEMPLATE (TOMBSTONE_DIR"/tombstone_%02d")

 

tombstones 디렉터리(/data/tombstones)는 eng 또는 userdebug 빌드된 단말에서만 접근이 가능합니다.

tombstones 디렉터리를 열어보면 tombstone_xx 파일들을 확인할 수 있습니다.

기본적으로 10개 파일이 저장되게 되어 있고, 10개 이후는 가장 오래된 파일부터 지워지면서 추가됩니다.

 

아래는 userdebug 빌드된 단말에서 보이는 tombstone 디렉터리입니다. user 빌드된 단말에서는 아래가 보이지 않습니다.

  

 

tombstone_xx 파일을 열면 아래처럼 abort message 및 register, backtrace, stack 등의 정보를 확인할 수 있습니다.

pid: 8671, tid: 8785, name: RenderThread  >>> com.sec.android.app.sbrowser <<<
signal 6 (SIGABRT), code -6 (SI_TKILL), fault addr --------
Abort message: 'Failed to set damage region on surface 0xcefee7e0, error=EGL_BAD_ACCESS'
    r0 00000000  r1 00002251  r2 00000006  r3 00000008
    r4 000021df  r5 00002251  r6 cbdb8f08  r7 0000010c
    r8 cbdb97d0  r9 00003e98  sl cbdb97f0  fp cbdb97e0
    ip 00000000  sp cbdb8ef8  lr f12a44d7  pc f12d580c  cpsr 200f0010
    d0  5f4441425f4c4741  d1  6765722065676143
    d2  0000000000000043  d3  000003e800000045
    d4  0000000000000008  d5  0000000000000000
    d6  00000000d1946020  d7  00000000d1946028
    d8  0000000000000000  d9  0000000000000000
    d10 0000000000000000  d11 0000000000000000
    d12 0000000000000000  d13 0000000000000000
    d14 0000000000000000  d15 0000000000000000
    d16 2e74736973726570  d17 2e6761742e676f6c
    d18 0000000000956d20  d19 00000000005d11e8
    d20 0000000000000000  d21 0000000000000000
    d22 0000000000000000  d23 0000000000000000
    d24 0000005b0000001b  d25 0000005b000000db
    d26 0000000000000000  d27 0000000000000000
    d28 0000000000000000  d29 0000000000000000
    d30 0000000000000000  d31 0000000000000000
    scr 20000013

backtrace:
    #00 pc 0004b80c  /system/lib/libc.so (tgkill+12)
    #01 pc 0001a4d3  /system/lib/libc.so (abort+54)
    #02 pc 00006683  /system/lib/liblog.so (__android_log_assert+154)
    #03 pc 0003a37d  /system/lib/libhwui.so (_ZN7android10uirenderer12renderthread10EglManager11damageFrameERKNS1_5FrameERK6SkRect+204)
    #04 pc 00038405  /system/lib/libhwui.so (_ZN7android10uirenderer12renderthread14OpenGLPipeline4drawERKNS1_5FrameERK6SkRectS8_RKNS0_12FrameBuilder13LightGeometry

EPNS0_16LayerUpdateQueueERKNS0_4RectEbRKNS0_15BakedOpRenderer9LightInfoERKNSt3__16vectorINS_2spINS0_10RenderNodeEEEN

SM_9allocatorISQ_EEEEPNS0_19FrameInfoVisualizerE+32)
    #05 pc 00036dd5  /system/lib/libhwui.so (_ZN7android10uirenderer12renderthread13CanvasContext4drawEv+144)
    #06 pc 00039457  /system/lib/libhwui.so (_ZN7android10uirenderer12renderthread13DrawFrameTask3runEv+138)
    #07 pc 0003e0a3  /system/lib/libhwui.so (_ZN7android10uirenderer12renderthread12RenderThread10threadLoopEv+166)
    #08 pc 0000d479  /system/lib/libutils.so (_ZN7android6Thread11_threadLoopEPv+144)
    #09 pc 000a2c69  /system/lib/libandroid_runtime.so (_ZN7android14AndroidRuntime15javaThreadShellEPv+80)
    #10 pc 00048a89  /system/lib/libc.so (_ZL15__pthread_startPv+24)
    #11 pc 0001b34f  /system/lib/libc.so (__start_thread+32)

stack:
         cbdb8eb8  ffffffdf
         cbdb8ebc  00000000
         cbdb8ec0  cbdb97e0
         cbdb8ec4  76d298d5  /dev/ashmem/dalvik-non moving space_4285_4285 (deleted)
         cbdb8ec8  000021df
         cbdb8ecc  f2ceeaab  /system/bin/app_process32 (sigprocmask+182)
         cbdb8ed0  c60348a0  [anon:libc_malloc]

 

tombstone 추출하는 방법

eng 또는 userdebug 빌드된 단말에서는 adb pull 명령으로 파일을 꺼낼 수 있습니다. (adb pull /data/tombstones)

 

user 빌드된 단말(양산 단말)에서는 "버그 리포트 생성"하면 아래처럼 버그 리포트에 포함돼 나옵니다.

 

[dropbox]

보통 logcat 같은 경우 정해진 크기의 링 버퍼에 로그가 저장되지만, dropbox는 파일로 저장되는 로그입니다.

 

주로 오류나 시스템에 영향을 줄 수 있는 상황을 기록하는 용도로 많이 사용됩니다.

strictmode violation, watchdog, wtf(What a Terrible Failure), netstats_error, lowmem 정보도 기록되지만 여기에서는 ANR과 Crash에 대한 것만 확인하도록 하겠습니다.

 

dropbox 디렉토리(/data/system/dropbox)도 eng 또는 userdebug 빌드된 단말에서만 접근이 가능합니다.

 

아래는 userdebug 빌드된 단말에서 보이는 dropbox 디렉터리입니다. user 빌드된 단말에서는 아래가 보이지 않습니다.

 

ANR

inputsystem에서는 입력 시 5초내 응답이 없을 경우 ANR 트리거 동작해서 AMS(ActivityManagerService)의 inputDispatchingTimeOut을 호출하고, 여기서 다시 AppErrors를 호출해서 ANR 다이얼로그를 화면에 표시하기 전에 Process 정보, ANR reason, 각 프로세스의 CPU 사용량, stack trace 등은 dropbox에 기록(addErrorToDropBox)하고 일부 정보는 로그로 출력합니다.

 

AppErrors.java

 

App Crash

Exception이 Throw 된 후 catch 되지 않으면 Runtime에서 uncaught exception으로 처리합니다.

uncaughtException()에서 AMS의 handleApplicationCrash() -> handleApplicationCrashInner() 이벤트 로그에 Crash 정보 출력 후 dropbox에 기록(addErrorToDropBox) 후 해당 앱을 강제 종료 처리합니다.

 

RuntimeInit.java

 

ActivityManagerService.java

 

 

Native crash

Native crash가 발생하면 커널은 해당 시그널을 보내고, linker 모듈의 debugger(클라이언트)에서 시그널(SIGABRT, SIGSEGV, SIGFPE 등)를 잡아서 debuggerd(데몬 서버)로 DEBUGGER_ACTION_CRASH 메시지를 보냅니다.

 

데몬 서버인 debuggerd에서는 DEBUGGER_ACTION_CRASH 메시지를 받아서 Native에서 보낸 정보를 읽어오고, AMS(ActivityManagerService) NativeCrashListener에 소켓 연결을 설정하고 tombstone를 호출해서 tombstone에 native crash에 대해서 덤프하도록 요청합니다. 이후 dump thread에 의해서 tombstone에 backtrace 외 정보들이 저장됩니다.

 

한편, debuggerd와 소켓 연결된 NativeCrashListener는 스레드를 상속받은 클래스인데, 애초부터 SystemServer에서 AMS로 NativeCrashListener의 인스턴스를 생성해서 스레드로 동작중인 상태였으며, debuggerd에서는 tombstone에 dump를 요청한 이후 NativeCrashListener에도 시그널을 전달해서 아래와 같이 dropbox에 기록하도록 요청합니다.

 

NativeCrashListener.java

 

ActivityManagerService.java

 

dropbox 추출하는 방법

eng 또는 userdebug 빌드된 단말에서는 adb pull 명령으로 파일을 꺼낼 수 있습니다. (adb pull /data/system/dropbox)

 

user 빌드된 단말에서는 tombstone에서 "버그 리포트 생성"을 해서 꺼낸 것처럼 dropbox를 꺼낼 수 없습니다.

단, 삼성 단말의 경우 "Sysdump로 버그 리포트 생성"하면 dropbox 디렉터리에 있는 파일들을 압축해서 내보내줍니다.

 

직전의 오류의 경우에 대해서는 아래 명령으로 꺼낼 수는 있습니다.

 

ANR

adb shell dumpsys dropbox --print system_app_anr > system_app_anr.txt

adb shell dumpsys dropbox --print data_app_anr > data_app_anr.txt

 

APP Crash

adb shell dumpsys dropbox --print system_app_crash > system_app_crash.txt 

adb shell dumpsys dropbox --print data_app_crash > data_app_crash.txt 

 

Native Crash

adb shell dumpsys dropbox --print system_app_native_crash > system_app_native_crash.txt 

adb shell dumpsys dropbox --print data_app_native_crash > data_app_native_crash.txt

 

여기까지입니다. 읽어주셔서 감사합니다.

+ Recent posts