어떤 앱을 개발중이다. 그 앱에는 AlarmManager를 이용해서 매일 정해진 시각에 알람 통지 + 알람 사운드를 울려준다고 할때 
기 등록된 알람 시각 값이 SharedPreferences에 저장이 되어 있다. 
그리고 그렇게 등록된대로 정해진 시간에 알람이 울린다. 

그런데 개발 중이기 때문에 코드를 수정해 가면서 새롭게 앱을 Build and Run(맥에서 Control-R 혹은 Control-Shift-R) 시키면 
기 등록된 알람 데이터(SharedPreferences에 저장된 데이터)는 어떻게 될까? 
사라질까? 그대로 남아 있을까? 
즉 그렇게 Build And Run을 기키면 기존 등록된 알람 시간에 정상적으로 알람을 울려줄까?

즉 Android 시스템에 등록된 AlarmManager의 등록된 객체(등록된 알람 시간)는 어떻게 될까? 그대로 존재할까 아니면 사라질까?

물론 앱을 폰에서 삭제한 후에 다시 Build and Run을 시키면 당연히 저장되어 있던 알람 데이터는 삭제된다. 
이때는 해당 앱과 관계된 모든 데이터는 삭제된다.


그런데 폰에서 앱을 삭제하지 않고 수정된 코드를 테스트 및 확인하기 위해 Build and Run을 시키면 어떻게 될까? 
구글의 API Reference 문서에 나와있는지 모르겠으나 내가 테스트해 본 바로는 첫 번째 Build and Run 때에는 기존의 설정 데이터가 그대로 보존되었다. 
그러나 이것이 두 번, 세번 반복될때부터는 기존의 데이터들이 사라지게 된다. 
정확히 말하면 SharedPreferences에 저장된 데이터는 삭제가 되지 않는다. 
다만 Android 시스템에 등록되어 있던 AlarmManager의 등록된 객체가 사라진다는 것이다.


아무튼 결론은 Build and Run은 기존의 데이터 특히 AlarmManager에 대해 보장해 주지 않는다는 점이다. 

이렇게 Android 시스템에 등록된 AlarmManager 객체의 존재 여부를 확인할려면 adb 명령을 통해서 확인할 수 있다.


DOS 창(리눅스나 Mac OS의 경우 터미널 창)에서 
Android SDK가 설치된 경로의 platform-tools 폴더의 하위 경로로 이동해서(adb.exe가 있는 위치로 이동해서)
스마트폰을 PC와 연결한 후 아래 커맨더로 안드로이드 시스템에 등록된 AlarmManager의 정보를 확인할수 있다.

adb shell dumpsys alarm 

그러면 아래와 같은 복잡한 정보가 나온다.

    RTC_WAKEUP #0: Alarm{198895e type 0 when 1554422926096 com.sweettracker.smartparcel}
      tag=*walarm*:com.sweettracker.smartparcel/.halibut.HalibutSendAlarmReceiver
      type=0 whenElapsed=+17h46m43s806ms when=2019-04-05 09:08:46
      window=0 repeatInterval=0 count=0 flags=0x5
      operation=PendingIntent{481513f: PendingIntentRecord{251a79d com.sweettracker.smartparcel broadcastIntent}}
Batch{c31876b num=13 start=1116619814 end=1118823179 flgs=0xc}:
    ELAPSED_WAKEUP #12: Alarm{cdfc404 type 2 when 1116619814 com.google.android.gms}
      tag=*walarm*:com.google.android.gms.fido.authenticator.autoenroll.FIDO_KEY_VALIDITY_CHECK_DELAY_COMPLETE
      type=2 whenElapsed=+17h50m44s97ms when=+17h50m44s97ms
      window=+23h58m48s19ms repeatInterval=0 count=0 flags=0x0
      operation=PendingIntent{817dded: PendingIntentRecord{4961bdd com.google.android.gms startService}}
    ELAPSED_WAKEUP #11: Alarm{45e5238 type 2 when 1114137316 com.android.vending}
      tag=*walarm*:com.google.android.finsky.scheduler.FALLBACK3
      type=2 whenElapsed=+17h9m21s599ms when=+17h9m21s599ms
      window=+13h29m59s997ms repeatInterval=0 count=0 flags=0x0
      operation=PendingIntent{8864211: PendingIntentRecord{16720ff com.android.vending startService}}
    RTC_WAKEUP #10: Alarm{e07e04e type 0 when 1554418207741 com.example.exalarm_repeat}
      tag=*walarm*:com.example.exalarm_repeat/.AlarmReceiver
      type=0 whenElapsed=+16h28m5s451ms when=2019-04-05 07:50:07
      window=+12h38m59s998ms repeatInterval=0 count=0 flags=0x4
      operation=PendingIntent{a06756f: PendingIntentRecord{1faa808 com.example.exalarm_repeat broadcastIntent}}
    RTC_WAKEUP #9: Alarm{a15e422 type 0 when 1554416429513 com.hubizict.orientalhospital}
      tag=*walarm*:com.hubizict.orientalhospital/com.com.hubizict.orientalhospital.util.AlarmReceiver
      type=0 whenElapsed=+15h58m27s223ms when=2019-04-05 07:20:29
      window=+18h0m0s0ms repeatInterval=86400000 count=0 flags=0x0
      operation=PendingIntent{fa09fd1: PendingIntentRecord{82563d9 com.hubizict.orientalhospital broadcastIntent}}
    RTC_WAKEUP #8: Alarm{b20aeb3 type 0 when 1554414158867 com.hubizict.orientalhospital}
      tag=*walarm*:com.hubizict.orientalhospital/com.com.hubizict.orientalhospital.util.AlarmReceiver
      type=0 whenElapsed=+15h20m36s577ms when=2019-04-05 06:42:38
      window=+18h0m0s0ms repeatInterval=86400000 count=0 flags=0x0
      operation=PendingIntent{2767809: PendingIntentRecord{3bbc438 com.hubizict.orientalhospital broadcastIntent}}
    ELAPSED_WAKEUP #7: Alarm{3717f76 type 2 when 1092537187 com.android.vending}
      tag=*walarm*:com.google.android.finsky.scheduler.FALLBACK2
      type=2 whenElapsed=+11h9m21s470ms when=+11h9m21s470ms
      window=+8h59m59s911ms repeatInterval=0 count=0 flags=0x0
      operation=PendingIntent{76d6977: PendingIntentRecord{c06475d com.android.vending startService}}
    ELAPSED #6: Alarm{5883bd1 type 3 when 1086423185 com.google.android.gms}
      tag=*alarm*:com.google.android.gms.common.download.START
      type=3 whenElapsed=+9h27m27s468ms when=+9h27m27s468ms
      window=+8h59m59s994ms repeatInterval=0 count=0 flags=0x0
      operation=PendingIntent{1e15636: PendingIntentRecord{1f064f1 com.google.android.gms startService}}
    RTC #5: Alarm{5298f9c type 1 when 1554392921828 com.sec.android.app.sns3}
      tag=*alarm*:com.sec.android.app.sns.profile.ACTION_CHECK_OLD_DATA
      type=1 whenElapsed=+9h26m39s538ms when=2019-04-05 00:48:41
      window=+18h0m0s0ms repeatInterval=86400000 count=0 flags=0x0
      operation=PendingIntent{5b099c3: PendingIntentRecord{8cfdc97 com.sec.android.app.sns3 broadcastIntent}}
    ELAPSED_WAKEUP #4: Alarm{b1bdda5 type 2 when 1086297010 com.google.android.gms}
      tag=*walarm*:com.google.android.gms.auth.authzen.CHECK_REGISTRATION
      type=2 whenElapsed=+9h25m21s293ms when=+9h25m21s293ms
      window=+15h38m54s702ms repeatInterval=0 count=0 flags=0x0
      operation=PendingIntent{b03d57a: PendingIntentRecord{4cedd0f com.google.android.gms broadcastIntent}}

이상의 정보에서 위의 색상으로 표시된 내용을 보면 패키지가 com.example.exalarm_repeat로 되어 있는 앱에 의해서
RTC_WAKEUP형태로 등록되어 있고 알람 시각은 when=2019-04-05 07:50:07으로 등록되어 있음을 확인하게 된다. 
그런데 앱을 Build And Run을 반복하게 되면 안드로이드 시스템에 등록된 AlarmManager 객체(정보)가 사라진다는 것이다.
그러나 SharedPreferences에 저장된 값은 그대로 존속한다. 따라서 이로 인해 등록된 정보가 SharedPreferences에 있기 때문에 정해진 시간에 알람이 울리리라 생각하지만 실상은 이상과 같은 일들이 내부에서 벌어진 것이다.

 

  • 안드로이드 SDK Pie 업그레이드 폰에서 다음과 같은 에러가 발생할 경우 대처

java.lang.NoClassDefFoundError: Failed resolution of: Lorg/apache/http/message/HeaderGroup

 

(1) 수준의 build.gradle targetSdkVersion 28 것을 27 변경해 준다. 28에서 발생하는 문제이다. Api Level 28 Pie(Android version 9)이고 27 Oreo(Android version 8.1)이다. 

 

(2) Manifest.xml <application> 속성 아래에 다음 코드를 추가한다.

<application

    android:allowBackup="true"

    android:icon="@mipmap/ic_launcher"

    android:label="@string/app_name"

    android:roundIcon="@mipmap/ic_launcher_round"

    android:supportsRtl="true"

    android:theme="@style/AppTheme">

 

   <uses-library android:name=“org.apache.http.legacy” android:required=“false” />

 

…. />

 

구글의 설명을 들어보면 play-service map을 사용할때 발생하는 문제로 설명되어 있다.

 

If you are using com.google.android.gms:play-services-maps:16.0.0 or below and your app is targeting API level 28 (Android 9.0) or above, you must include the following declaration within the <application> element ofAndroidManifest.xml.

 

  <uses-library
     
android:name="org.apache.http.legacy"
     
android:required="false" />

This is handled for you if you are using com.google.android.gms:play-services-maps:16.1.0 and is not necessary if your app is targeting a lower API level.

안드로이드로 BLE(Bluetooth Low Energy) 개발을 하다보면 2대의 폰이 하나는 Peripheral(Slave) 모드로 다른 하나는 Central(Master) 모드로 동작하는 테스트 앱을 만들어야 할 때가 있다.

왜냐하면 Bluetooth Classic(혹은 Bluetooth Basic)과는 달리 BLE의 경우는 Peripheral 모드로 동작하는 기기가 없으면 Central에서 아무리 scan을 해 봐야 검색 자체가 되지 않기 때문이다.

참고로 스마트폰의 경우는 BLE로도 혹은 Bluetooth Classic으로 동작을 하는 '듀얼 모드'(혹은 Bluetooth Smart Ready)를 지원하기 때문에 이상의 작업 환경을 만드는 것이 가능하다.

또 참고로 BLE만을 지원하는 경우를 Bluetooth Smart 디바이스라고 하고 '싱글 모드'로 표현하기도 한다. 이 경우는 기존의 Bluetooth Classic 기기와는 호환되지 않는다.


여기서 BLE가 Peripheral 모드로 동작한다는 뜻은 다른 BLE 디바이스와 connection을 맺기 위해 advertising signal을 주기적으로 내보내는 역할을 하는 기기를 말하고 Central로 동작한다는 뜻은 다른 BLE가 내보내는 advertising signal를 주기적으로 스캔하는 디바이스를 말한다.

아무튼...


이렇게 Peripheral 모드로 동작하는 안드로이드 개발을 하다보면 아래 코드조각의 onStartFailure() 메소드에서 errorCode 1을 반환하는 경우를 만나게 된다.


        AdvertiseCallback advertisingCallback = new AdvertiseCallback(){

          @Override

          public void onStartSuccess(AdvertiseSettings settingsInEffect){

              Toast.makeText(getApplicationContext(), "Advertising 성공~ ", 1).show();

              super.onStartSuccess(settingsInEffect);

          }


          @Override

          public void onStartFailure(int errorCode){

              Log.e("BLE #######", "Advertising onStartFailure: "+errorCode); //여기서 errorCode가 1을 반환하는 에러


              //아래 Toast에서 errorCode가 1이 나오는데 API 문서를 보면

              //ADVERTISE_FAILED_DATA_TOO_LARGE 에러라고 설명이 되어 있다.

              //Failed to start advertising as the advertise data to be broadcasted is larger than 31 bytes.

              Toast.makeText(getApplicationContext(), "Advertising onStartFailure: "+errorCode, 1).show();

              super.onStartFailure(errorCode);

          }

        };


이 경우는 안드로이드 API 문서의 아래 설명과 같이 advertising의 데이터 길이가 31bytes를 넘어서 발생하는 문제이다.


"Failed to start advertising as the advertise data to be broadcasted is larger than 31 bytes."


BLE Peripheral 기기가 내보내는 advertising signal의 길이는 31byte로 한정되어 있는데 코드상에서 설정한 이 데이터의 크기가 31byte를 넘어서 발행한 경우이다.

아래 코드 조각이 advertising 데이터를 설정하는 코드이다.


        BluetoothLeAdvertiser advertiser = BluetoothAdapter.getDefaultAdapter().getBluetoothLeAdvertiser();


        AdvertiseSettings settings = new AdvertiseSettings.Builder()

                .setAdvertiseMode(AdvertiseSettings.ADVERTISE_MODE_LOW_LATENCY)

                .setTxPowerLevel(AdvertiseSettings.ADVERTISE_TX_POWER_HIGH)

                .setConnectable(false)

                .build();


        ParcelUuid pUuid = new ParcelUuid(UUID.fromString(getString(R.string.ble_uuid2)));


        AdvertiseData data = new AdvertiseData.Builder()

                .setIncludeDeviceName(true)

                .addServiceUuid(pUuid)

                .addServiceData(pUuid, "Data".getBytes(Charset.forName("UTF-8")))

                .build();


위의 AdvertiseData의 설정을 아래와 같이 수정해본다.


        AdvertiseData data = new AdvertiseData.Builder()

                .setIncludeDeviceName(true)

//                .addServiceUuid(pUuid)

//                .addServiceData(pUuid, "Data".getBytes(Charset.forName("UTF-8")))

                .build();


UUID와 Service Data를 advertising signal 데이터에 포함시키지 않도록 주석처리했다.

이제 문제가 해결되었을 것이고 다른 BLE에서 본 소스를 실행한 BLE를 정상적으로 잘 스캐닝 및 검색을 할수 있을 것이다.




FCM 안드로이드 앱 개발시 error: cannot access zzbgl, class file for com.google.android.gms.internal.zzbgl not found 에러 해법


Firebase Cloud Messaging 안드로이드 개발시 아래와 같은 에러를 만나는 경우가 있다.

이 에러는 라이브러리로 인한 에러인데 FCM에서 library로 인한 에러의 경우 컴파일 단계에서 원천적으로 되지 않는 경우가 발생한다.

남의 라이브러리를 사용할때마다 늘상 씨름을 한판하지 않고는 쉽게 넘어가지를 않는 것 같다.

com.google.firebase.messaging.FirebaseMessagingService를 상속받은 MyFirebaseMessagingService 클래스에서 아래와 같은 에러가 발생하면서 컴파일이 되지를 않는다.


D:\ExFCMTest\app\src\main\java\com\example\joe\exfcmtest\MyFirebaseMessagingService.java:29: error: cannot access zzbgl

        Map<String, String> data = remoteMessage.getData();

                                                ^

  class file for com.google.android.gms.internal.zzbgl not found

Note: D:\ExFCMTest\app\src\main\java\com\example\joe\exfcmtest\MyFirebaseMessagingService.java uses or overrides a deprecated API.

Note: Recompile with -Xlint:deprecation for details.

1 error

:app:compileDebugJavaWithJavac FAILED

:app:buildInfoGeneratorDebug


FAILURE: Build failed with an exception.


* What went wrong:

Execution failed for task ':app:compileDebugJavaWithJavac'.

> Compilation failed; see the compiler error output for details.


* Try:

Run with --stacktrace option to get the stack trace. Run with --info or --debug option to get more log output. Run with --scan to get full insights.


* Get more help at https://help.gradle.org


BUILD FAILED in 1s

18 actionable tasks: 7 executed, 11 up-to-date



이 에러가 발생했을 때의 앱 수준의 build.gradle(app/build.gradle)에 있는 dependencies의 내용이다.


dependencies {

    implementation fileTree(dir: 'libs', include: ['*.jar'])

    implementation 'com.android.support.constraint:constraint-layout:1.1.0'

    testImplementation 'junit:junit:4.12'

    androidTestImplementation 'com.android.support.test:runner:1.0.2'

    androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2'

    compile 'com.google.firebase:firebase-core:16.0.0'

    compile 'com.google.firebase:firebase-messaging:12.0.1'

}


이 문제는 앱 수준의 build.gradle의 dependencies의  


compile 'com.google.firebase:firebase-core:16.0.0'를 


compile 'com.google.firebase:firebase-core:11.8.0' 


로 바꾸었더니 정상적으로 compile이 되었다. 그래서 dependencies가 아래와 같은 모양을 이루었다.


dependencies {

    implementation fileTree(dir: 'libs', include: ['*.jar'])

    implementation 'com.android.support.constraint:constraint-layout:1.1.0'

    testImplementation 'junit:junit:4.12'

    androidTestImplementation 'com.android.support.test:runner:1.0.2'

    androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2'

    compile 'com.google.firebase:firebase-core:11.8.0'

    compile 'com.google.firebase:firebase-messaging:12.0.1'

}


이제 컴파일이 되어 Firebase Console 창에서 푸시 메시지를 해당 앱으로 날리니 이번에는 앱이 강제 종료가 된다. 아래와 같은 에러 메시지를 뿜으면서


06-06 18:40:14.223 13813-16293/com.example.joe.exfcmtest E/AndroidRuntime: FATAL EXCEPTION: pool-1-thread-1

    Process: com.example.joe.exfcmtest, PID: 13813

    java.lang.NoSuchMethodError: No static method zzamg()Lcom/google/android/gms/common/util/zzd; in class Lcom/google/android/gms/common/util/zzh; or its super classes (declaration of 'com.google.android.gms.common.util.zzh' appears in /data/app/com.example.joe.exfcmtest-1/split_lib_dependencies_apk.apk)

        at com.google.android.gms.internal.zzcim.<init>(Unknown Source)

        at com.google.android.gms.internal.zzcim.zzdx(Unknown Source)

        at com.google.android.gms.measurement.AppMeasurement.getInstance(Unknown Source)

        at com.google.firebase.messaging.zzd.zzde(Unknown Source)

        at com.google.firebase.messaging.zzd.zzc(Unknown Source)

        at com.google.firebase.messaging.zzd.zzf(Unknown Source)

        at com.google.firebase.messaging.FirebaseMessagingService.handleIntent(Unknown Source)

        at com.google.firebase.iid.zzc.run(Unknown Source)

        at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1133)

        at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:607)

        at java.lang.Thread.run(Thread.java:762)

06-06 18:40:14.287 13813-13813/com.example.joe.exfcmtest D/ViewRootImpl@c3f2698[MainActivity]: mHardwareRenderer.destroy()#4

    dispatchDetachedFromWindow

06-06 18:40:14.360 13813-13813/com.example.joe.exfcmtest D/InputTransport: Input channel destroyed: fd=70


이 문제는 라이브러리들의 버전이 일치하지 않아서 발생하는 문제이다. 위의 dependencies를 보면 아래와 같이 11.8.0과 12.0.1이 혼재되어 있다.


    compile 'com.google.firebase:firebase-core:11.8.0'

    compile 'com.google.firebase:firebase-messaging:12.0.1'


따라서     


compile 'com.google.firebase:firebase-messaging:12.0.1'를     


compile 'com.google.firebase:firebase-messaging:11.8.0'


으로 바꾸었다.

바꾸는건 12.0.1을 11.8.0으로 바꾸어서 타이핑해 주면 된다.


그래서 dependencies가 아래와 같이 되었다.


dependencies {

    implementation fileTree(dir: 'libs', include: ['*.jar'])

    implementation 'com.android.support.constraint:constraint-layout:1.1.0'

    testImplementation 'junit:junit:4.12'

    androidTestImplementation 'com.android.support.test:runner:1.0.2'

    androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2'

//    compile 'com.google.firebase:firebase-core:16.0.0' //error: cannot access zzbfm

    compile 'com.google.firebase:firebase-core:11.8.0' //O.K.

    compile 'com.google.firebase:firebase-messaging:11.8.0'

}


이제 컴파일도 정상적으로 되고 Firebase Console창에서 푸시 메시지를 날리면 정상적으로 잘 수신을 한다.

참고로 루트 수준의 build.gradle에 다음과 같이 구글의 maven 저장소를 추가해 준다.


allprojects {

    repositories {

        google()

        jcenter()

        maven {

            url "https://maven.google.com" //구글의 Maven repository

        }

    }

}


아래 사이트가 도움이 된다.


https://firebase.google.com/docs/android/setup?authuser=0#manually_add_firebase



FCM 안드로이드 앱을 만드는 중에 아래와 같은 에러가 발생하는 원인과 해결책


가정하기를 

 -. 패키지명 : com.example.joe.exfcmtest

 -. com.google.firebase.messaging.FirebaseMessagingService를 상속받을 클래스가 MyFirebaseMessagingService라고 할때


이러한 상황가운데서 아래와 같은 에러를 만난다면 


'com.example.joe.exfcmtest.MyFirebaseMessagingService' is not assignable to 'android.app.Service' less... (Ctrl+F1) 

Validates resource references inside Android XML files.


앱 수준의 build.gradle(프로젝트 수준의 build.gradle이 아님)에서 필요한 아래의 dependencies가 빠져 있어서의 문제다.


compile 'com.google.firebase:firebase-messaging:12.0.1'  (버전은 현재의 최신 버전과 다를수 있음)


이 문제는 참으로 황당하게도 구글의 Firebase SDK 추가에 대한 설명 자체에서 빼먹고 제시해 주지 않음으로 인해 만나게되는 황당한 에러이다.

Firebase 콘솔에서 제시한 대로 따라하다보면 아래 이미지에서 보듯이 compile 'com.google.firebase:firebase-messaging:12.0.1'를 포함시키라는 설명이 전혀 없다. 나쁜...




이로인해 Manifest 파일의 com.google.firebase.messaging.FirebaseMessagingService를 상속받을 Service 클래스에 대해 아래 그림과 같은 에러가 발생한다.


        <service android:name=".MyFirebaseMessagingService">

            <intent-filter>

                <action android:name="com.google.firebase.MESSAGING_EVENT" />

            </intent-filter>

        </service>




그리고 다음 에러




이 문제를 해결할려면 아래 그림에서 보는바와 같이 앱 수준의 build.gradle




compile 'com.google.firebase:firebase-messaging:12.0.1'를 dependencies에 포함시켜야 된다.




안드로이드는 너무 자주, 많이 바뀌는 통에...




안드로이드 6.0 마시멜로에서 변경된 중요 사항 중 블루투스, WiFi 권한 관련


블루투스 관련 앱이 정상적으로 검색과 연결이 잘 되던 것이 안드로이드 버전 6.0의 기기에서는 검색해 내지를 못하는 경험이 있을 것이다.

이것은 6.0에서의 정책상의 변화로 인해 발생하는 문제이다.

6.0에서 블루투스가 정상적으로 동작하기 위해서는 아래 권한이 Manifest에 선언되어 있으야만 한다.


ACCESS_FINE_LOCATION 혹은 ACCESS_COARSE_LOCATION 권한이 없으면 주변 WiFi / Bluetooth 디바이스를 발견할 수 없다.


위의 권한이 선언되어 있지 않으면 다음과 같은 메서드가 정상적으로 동작하지 않는다.


WifiManager.getScanResults()

BluetoothDevice.ACTION_FOUND

BluetoothLeScanner.startScan()

...


아래는 구글 안드로이드 API Reference 문서에서 소개하고 있는 내용이다.


Access to Hardware Identifier


To provide users with greater data protection, starting in this release, Android removes programmatic access to the device’s local hardware identifier for apps using the Wi-Fi and Bluetooth APIs. The WifiInfo.getMacAddress() and the BluetoothAdapter.getAddress() methods now return a constant value of 02:00:00:00:00:00.

To access the hardware identifiers of nearby external devices via Bluetooth and Wi-Fi scans, your app must now have the ACCESS_FINE_LOCATION or ACCESS_COARSE_LOCATION permissions:

Note: When a device running Android 6.0 (API level 23) initiates a background Wi-Fi or Bluetooth scan, the operation is visible to external devices as originating from a randomized MAC address.


https://developer.android.com/about/versions/marshmallow/android-6.0-changes.html





안드로이드에서 객체 정렬(sort) 하기


Arrays.sort() 메소드를 이용하면 배열 안의 원소들을 정렬할수 있다.

그러나 객체가 배열의 원소일때는 이 메소드로는 처리가 안된다.

정렬 대상이 객체(클래스의 객체)일 경우는 Comparable이라는 인터페이스를 통해서 가능하다.


이름(name)과 나이(age)라는 2개의 필드를 가진 People라는 클래스가 있다고 가정해 보자.

이 클래스를 정렬할려면 Comparable라는 인터페이스를 통해서 어떻게 구현하는지 살펴본다.


public class People implements Comparable<People>{

private String name;

private int age;

public People(String _name, int _age){

this.name = _name;

this.age = _age;

}

public String getName(){

return this.name;

}

public int getAge(){

return this.age;

}

public int compareTo(People _people){

//여긴 이름을 기준으로 정렬

//이건 내림 차순

//문자열은 이런 식으로 가능

// return this.name.compareTo(_people.name); 

//이건 오름차순

//문자열은 이런 식으로 가능

// return _people.name.compareTo(this.name);

//여기서부터는 나이를 기준으로 정렬

//이건 내림 차순

//숫자는 위의 compareTo()로 안되고 다음과 같이 처리 해야됨

if (this.age < _people.age){

// return -1; //이렇게 하면 내림 차순

return 1; //이렇게 하면 오름 차순

} else if (this.age == _people.age){

return 0;

} else {

// return 1; //이렇게 하면 내림 차순

return -1; //이렇게 하면 오름 차순

}

}

}


People이라는 객체를 사용하는 곳은 다음과 같다.

보통 Listview의 각 항목들을 보여줄때 이름을 기준으로나 혹은 나이를 기준으로 내림차순 혹은 오름 차순으로 보여주고자 할때 Comparable 인터페이스를 활용하면 간단히 처리할 수 있다.

여기서는 단지 화면의 TextView에 보여주는 기능을 구현해본다.


public class ExObjectSortActivity extends Activity {

private TextView txt;

private ArrayList<People> arrList;

@Override

protected void onCreate(Bundle savedInstanceState) {

super.onCreate(savedInstanceState);

setContentView(R.layout.ex_object_sort);

txt = (TextView)findViewById(R.id.txt);

arrList = new ArrayList<People>();

arrList.add(new People("홍길동", 15));

arrList.add(new People("고길동", 25));

arrList.add(new People("둘리", 12));

arrList.add(new People("챨리", 32));

arrList.add(new People("강감찬", 120));

String rt = "";

String str = "";

//기존 저장된 순서대로 출력

for(int i=0; i<arrList.size(); i++){

str += arrList.get(i).getName()+" - "

                                     +arrList.get(i).getAge()+"\n";

}

str += "\n\n\n";

//Comparable에서 지정된 대로(내림차순 혹은 오름차순) 정렬됨

Collections.sort(arrList);

//정렬된 결과를 출력하기

for(int i=0; i<arrList.size(); i++){

str += arrList.get(i).getName()+" - "

                                     +arrList.get(i).getAge()+"\n";

}

txt.setText(str);

}

}


ex_object_sort.xml 레이아웃은 다음과 같이 되어 있다.


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

    xmlns:tools="http://schemas.android.com/tools"

    android:layout_width="match_parent"

    android:layout_height="match_parent"

    android:layout_margin="20dp"

    tools:context="${relativePackage}.${activityClass}" >


    <TextView

        android:id="@+id/txt"

        android:layout_width="wrap_content"

        android:layout_height="wrap_content"

        android:textColor="#000000"

        android:textSize="20sp" />

</RelativeLayout>




자바 혹은 안드로이드를 개발할 때 로그 정보는 더 없이 유용한 정보이다.

난공불락 같아 보이는 에러도 로그를 출력해 보면 거의 대부분 문제를 해결하게 된다.

이클립스로 개발 한다면 로그 정보를 보는 방법은 매우 간단하다.

그런에 여기서는 로그의 정보를 파일로 저장하는 방법에 대해서 살펴본다.


우선 adb.exe가 설치되어 있는 위치로 이동해서 도스 창(커맨더 창)을 adb가 있는 위치에서 연다.

그리고 logcat을 저장하기 위해 사용할수 있는 명령어 형태는 다음과 같다.


Usage: logcat [options] [filterspecs]


options include:

  -s              Set default filter to silent.

                  Like specifying filterspec '*:s'

  -f <filename>   Log to file. Default to stdout

  -r [<kbytes>]   Rotate log every kbytes. (16 if unspecified). Requires -f

  -n <count>      Sets max number of rotated logs to <count>, default 4

  -v <format>     Sets the log print format, where <format> is one of:

                  brief process tag thread raw time threadtime long

  -c              clear (flush) the entire log and exit

  -d              dump the log and then exit (don't block)

  -t <count>      print only the most recent <count> lines (implies -d)

  -t '<time>'     print most recent lines since specified time (implies -d)

  -T <count>      print only the most recent <count> lines (does not imply -d)

  -T '<time>'     print most recent lines since specified time (not imply -d)

                  count is pure numerical, time is 'MM-DD hh:mm:ss.mmm'

  -g              get the size of the log's ring buffer and exit

  -b <buffer>     Request alternate ring buffer, 'main', 'system', 'radio',

                  'events', 'crash' or 'all'. Multiple -b parameters are

                  allowed and results are interleaved. The default is

                  -b main -b system -b crash.

  -B              output the log in binary.

  -S              output statistics.

  -G <size>       set size of log ring buffer, may suffix with K or M.

  -p              print prune white and ~black list. Service is specified as

                  UID, UID/PID or /PID. Weighed for quicker pruning if prefix

                  with ~, otherwise weighed for longevity if unadorned. All

                  other pruning activity is oldest first. Special case ~!

                  represents an automatic quicker pruning for the noisiest

                  UID as determined by the current statistics.

  -P '<list> ...' set prune white and ~black list, using same format as

                  printed above. Must be quoted.


filterspecs are a series of 

  <tag>[:priority]


where <tag> is a log component tag (or * for all) and priority is:


  V    Verbose

  D    Debug

  I    Info

  W    Warn

  E    Error

  F    Fatal

  S    Silent (supress all output)


'*' means '*:d' and <tag> by itself means <tag>:v

If not specified on the commandline, filterspec is set from ANDROID_LOG_TAGS.

If no filterspec is found, filter defaults to '*:I'

If not specified with -v, format is set from ANDROID_PRINTF_LOG

or defaults to "brief"


로그 정보에는 몇가지 종류가 있다.

v : 온갖 종류의 로그 전부다를 보여준다.

d, e, ...


그 중에서 여기서는 e에 대한 정보만을 출력하는 정보만을 파일로 저장해 본다.

다른 필터들(d, v, i...)도 동일한 방식으로 저장한다.

사용 방식은 다음과 같다.


Usage: logcat [options] [filterspecs]


따라서 다음과 같이 하면된다.


adb logcat *:e > D:\temp\kkk.txt


위와 같이 명령하면 D:\temp 폴더 아래에 kkk.txt라는 이름으로 로그 중 e(에러) 정보만을 파일로 저장한다.





BluetoothAdapter 클래스의 enable()라는 메소드를 통해서 Bluetooth를 강제적으로 활성화 할수 있으나

사용자로 하여금 선택할 수 있도록 하는 것이 더 바람직하다.


Google의 Android API Reference에서도 public boolean enable () 메소드에 대한 설명 가운데는

다음과 같이 사용자의 명백한 행동 없이

강제적으로 Bluetooth를 활성화시키지 말라고 얘기하고 있다.


Turn on the local Bluetooth adapter—do not use without explicit user action to turn on Bluetooth.


이때 Bluetooth를 활성화 하는 방법으로 Intent를 통해서 안드로이드 시스템에게 요청해서 

안드로이드 시스템이 사용자에게 다이얼로그 창을 띄워서 사용자가 선택하게 하고

사용자가 Bluetooth를 활성화하도록 선택을 하면 안드로이드 시스템이 Bluetooth를 

활성화 시키도록 하는 방법이 있다.

이때 BluetoothAdapter.ACTION_REQUEST_ENABLE라는 action을 Intent를 통해서

안드로이드 시스템에게 요청을 하면 된다.


Intent enableBtIntent = new Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE);

startActivity(enableBtIntent)라고 해도 되나 사용자가 어떤 선택을 했느냐에 따라서

그에 맞는 어떤 행동을 해야할 필요가 있기때문에 startActivityForResult()를 사용했다.

사용자의 선택 여부를 통보받는 곳이 protected void onActivityResult() 메소드를 통해서이다.


ENABLE_BT가 전역변수를 다음과 같이 선언되어 있다고 할 때

private static final int ENABLE_BT = 1;


아래는 안드로이드 시스템에게 Bluetooth 활성화 여부를 사용자에게 묻고

그 결과에 따라 활성화를 하도록 하는 코드이다.


startActivityForResult(enableBtIntent, ENABLE_BT);


사용자의 선택결과를 통보 받기 위해서는 onActivityResult()를 이용하면 된다.


@Override

protected void onActivityResult(int requestCode, int resultCode, Intent data){

if (requestCode == ENABLE_BT  

&& resultCode == Activity.RESULT_CANCELED){

Toast.makeText(this, "취소 했습니다", 1).show();

                finish();

} else if (requestCode == ENABLE_BT 

&& resultCode == Activity.RESULT_OK){

Toast.makeText(this, "블루투스를 활성화합니다.", 1).show();

  }

}




Android strings.xml에 HTML 코드 넣기

의료기 개발을 하면서 측정 결과를 공유하기 위해 

XML 형태로 전송하거나 

HTML 형태로 전송하거나 

혹은 PDF로 전송하는 등 다양한 형태의 파일로 

공유하는 작업을 하는 중에 

HTML형태의 파일로 저장하는 작업을 하면서 소스 코드 상에서 복잡한 HTML 코드를 집어 넣기에는 뭐시기해서 strings.xml을 이용하기 했다.

근데 문제는...


Android의 strings.xml에 HTML 코드를 그대로 넣으면 컴파일 단계에서 에러가 발생한다.


<string name="HtmlCSS">

<!DOCTYPE html>

<html>

<head>

<meta http-equiv="Content-Type" content="text/html; charset=utf-8">

<title>Meditab Results</title>

<style type="text/css">

body {

font-family: Arial, Verdana, sans-serif;

font-size: 90%;

color: #666;

background-color: #f8f8f8;}

table {

border-spacing: 0px; }

... 이하 생략 ...


이 문제를 해결할려면 다음과 같이 바꿔 주어야 된다.


   &lt;!DOCTYPE html&gt;

&lt;html&gt;

&lt;head&gt;

      &lt;meta http-equiv="Content-Type" content="text/html; charset=utf-8"&gt;

...이하 생략...


보통 번거로운일이 아니다. ;;;

이런 문제를 한방에 해결할수 있는 방법이 있다. 다음과 같이


<![CDATA[

  이 안에 HTML 코드를 있는 그대로 넣어주면 해결된다.

]]>


이때 string 항목에 속성을 하나 추가해 주어야 하는데 formatted="false"이다.

아래는 전체 예제이다.


<string name="HtmlCSS" formatted="false">

   <![CDATA[

   <!DOCTYPE html>

<html>

<head>

<meta http-equiv="Content-Type" content="text/html; charset=utf-8">

<title>Meditab Results</title>

<style type="text/css">

body {

font-family: Arial, Verdana, sans-serif;

font-size: 90%;

color: #666;

background-color: #f8f8f8;}

table {

border-spacing: 0px; }

th, td {

padding: 5px 30px 5px 10px;

border-spacing: 0px;

font-size: 90%;

margin: 0px;

}

th, td {

text-align: left;

background-color: #e0e9f0;

border-top: 1px solid #f1f8fe;

border-bottom: 1px solid #cbd2d8;

border-right: 1px solid #cbd2d8;}

tr.head th {

color: #fff;

background-color: #90b4d6;

border-bottom: 2px solid #547ca0;

border-right: 1px solid #749abe;

border-top: 1px solid #90b4d6;

text-align: center;

text-shadow: -1px -1px 1px #666666;

letter-spacing: 0.15em;}

td {

text-shadow: 1px 1px 1px #ffffff;

}

tr.even td, tr.even th {

background-color: #e8eff5;}

tr.head th:first-child {

-webkit-border-top-left-radius: 8px;

-moz-border-radius-topleft: 8px;

border-top-left-radius: 8px;}

tr.head th:last-child {

-webkit-border-top-right-radius: 8px;

-moz-border-radius-topright: 8px;

border-top-right-radius: 8px;}

fieldset {

width: 310px;

margin-top: 20px;

border: 1px solid #d6d6d6;

background-color: #ffffff;

line-height: 1.6em;}

legend {

font-style: italic;

color: #666666;}

.title {

float: left;

width: 160px;

clear: left;}

.submit {

width: 310px;

text-align: right;}

.results {

float: left;

}

</style>

</head>

<body>

<h1>Meditab Test Results</h1>

<h2>&nbsp;</h2>

<h2>개인정보 </h2>

<table>

<tr class="head">

<th>이름</th>

<th>측정일시</th>

<th>생년월일</th>

<th>성별</th>

<th>E-mail</th>

<th>진료담당</th>

</tr>

<tr> 

<th>홍길동</th>

<td>2016. 3. 5</td>

<td>1978. 5. 17</td>

<td>남</td>

<td>aslsd@naver.com</td>

<td>Dr. Park</td>

</tr>

</table>

<p />

<h2>&nbsp;</h2>

<h2>측정결과</h2>

<div >

<table class="results"> 

<tr class="head">

<th>ECG(심전도)</th>

<th>측정결과</th>

</tr>

<tr>

<td>Heart Rate</td>

<td>84</td>

</tr>

<tr>

<td>Respiratory Rate</td>

<td>19</td>

</tr>

<tr>

<td>ST Level(mV)</td>

<td>18</td>

</tr>

<tr>

<td>Arrythmia</td>

<td>Normal</td>

</tr>

</table>

<p />

<table class="results differ" > 

<tr class="head">

<th>NIBP(혈압)</th>

<th>측정결과</th>

</tr>

<tr>

<td>Systolic</td>

<td>120</td>

</tr>

<tr>

<td>Diastolic</td>

<td>84</td>

</tr>

<tr>

<td>Mean</td>

<td>96</td>

</tr>

<tr>

<td>&nbsp;</td>

<td>&nbsp;</td>

</tr>

</table>

<p />

<table class="results"> <!-- SPO2 -->

<tr class="head">

<th>SPO2(혈중 산소포화도)</th>

<th>측정결과</th>

</tr>

<tr>

<td>Saturation Value</td>

<td>100</td>

</tr>

<tr>

<td>Pulse Rate</td>

<td>84</td>

</tr>

<tr>

<td>&nbsp;</td>

<td>&nbsp;</td>

</tr>

<tr>

<td>&nbsp;</td>

<td>&nbsp;</td>

</tr>

</table>

<p />

<table class="results differ"> <!-- Temp -->

<tr class="head">

<th>Temp(체온)</th>

<th>측정결과</th>

</tr>

<tr>

<td>Temperature</td>

<td>36.3</td>


</tr>

<tr>

<td>&nbsp;</td>

<td>&nbsp;</td>

</tr>

<tr>

<td>&nbsp;</td>

<td>&nbsp;</td>

</tr>

<tr>

<td>&nbsp;</td>

<td>&nbsp;</td>

</tr>

</table>

</div>

<p/>

</body>

</html>

   ]]>

</string>




+ Recent posts