Press "Enter" to skip to content

[카테고리:] 안드로이드

ViewPager에서 탭 구현시 옆 탭을 미리 읽는 문제..

문제라고 하기보다는 현재 보이는 탭의 옆 탭을 미리 읽어둬서 탭 슬라이드로 넘어갈때 자연스럽게 넘어가게 하는 기술이라 보겠다.
(액티비티 형태의 프로그램만 만지작 하다가 요즘 프래그먼트 형태의 프로그램을 만지작하다보니 삽질의 연속이다.)

하지만 옆 탭이 로딩하는데 통신같은 상당한 시간이 걸리거나, 심지어 프로그래스다이얼로그까지 나타나는 탭이라면 골치가 아파온다.

왜냐! 현재 탭은 이미 화면에 나타나서 잘 보이고 사용자의 입력을 기다릴 타이밍인데, 

옆 탭을 미리 로딩하면서 현재화면에 프로그래스다이얼로그가 나타난다? 켁…

(이걸 해결하기 위해 몇시간을 삽질했다… ㅜㅡ 알고보면 이렇게 간단한 것을…. )
(물론, 미리 로딩하는 걸 안하게 프로그램하는 방밥이 있을~지도 모른다…..만 안찾아봤다.. ㅡㅡ)

이제 하나하나 스텝별로 짚고 넘어가 볼까요?

상황을 하나 가정해 본다.

탭이 로딩될때 스레드로 통신을 해서 데이터를 화면에 보여주는 것이라고 하자…

현재 사용자에게 보이는 탭 화면이 자신인지는 다음과 같이 판단한다.

TabActivity는 탭을 감싸고 있는 Activity

tabIdex는 자기 자신이 TabActivity에 붙는 순번(인덱스)

탭이 표시되는 클래스의 onCreate나 onResum이나 onCreateView 등등에 아래와 같은 코드를 넣을 수 있겠죠?

if ( TabActivity.getCurrentPagerPosition() == tabIdx )
{
    // 내가 보이고 있다!!! 여기다 프로그래스다이얼로그를 뿌려주면 되것다.
}
else
{
    // 내가 보이는 중이 아니라면 미리 읽어들이는 거니깐 프로그래스다이얼로그는 표시하지 말자
}

// 필요한 스레드 생성해서 돌리기
if ( thread == null )
{
  thread = new Thread();
  thread.start();
}
// 쓰레드 동작 안하는 중
else if ( thread.isAlive() == false )
{
  // 앗싸리 새로 생성해서 다시 실행하는게 안전할라나???
  thread.start();
}
// 쓰레드가 아직 동작하는 중
else
{
   // 아무것도 안해도 되것죠? 이미 스레드가 돌고 있으니깐.
}

이렇게 해놓으면 미리 읽어들이는 중에서는 프로그래스 다이얼로그가 표시되지 않는다.

만일 사용자가 화면을 휙휙휙 넘겨서 쓰레드가 끝나기 전에 해당 화면으로 오면??? 

프로그래스 다이얼로그가 표시되어야 하는데 표시되지 않고 사용자가 뭔가를 입력할 수 있는 상황이 된다.

어랏??? 이것도 쬐끔 위험한 일이다.. 그래서 막아보자~~~

다음 코드를 넣어주면 된다.

@Override
public void setUserVisibleHint(boolean isVisibleToUser) {
    super.setUserVisibleHint(isVisibleToUser);

         if ( isVisibleToUser )
        {
            if ( thread != null )
            {
                // 스레드가 동작중이면
                if ( thread.isAlive( ) )
                {
                    // 한번도 표시된 적이 없다면
                    if ( progressDialog == null )
                    {
                        // 조회중 표시
                        progressDialog = ProgressDialog.show( TabActivity, “통신중”, “조회중…”, true, false );
                    }
                    // 표시중이지 않으면
                    else if ( progressDialog.isShowing() == false )
                    {
                        // 조회중 표시
                        progressDialog = ProgressDialog.show( TabActivity, “통신중”, “조회중…”, true, false );
                    }
                }
            }
        }   
}

마지막으로 이 프로그래스 다이얼로그는 쓰레드 종료 시점에 다음 코드로 종료해 주면 되것다.

if ( progressDialog != null )
{
     if ( progressDialog.isShowing() )
     {
          progressDialog.dismiss();
     }
}

작업환경 : 안드로이드 스튜디오 1.1.0
참고자료 : http://stackoverflow.com/questions/9323279/how-to-test-if-a-fragment-view-is-visible-to-the-user

Bitmap too large to be uploaded into a texture 에러 메시지 대처방법

내가 경험한 메시지는 정확하게 이렇게 나온다.

W/OpenGLRenderer﹕ Bitmap too large to be uploaded into a texture (5100×4212, max=4096×4096)



안드로이드에서 ImageView 에다가 좀 해상도가 나가는(?) 이미지를 올리면 이런 메시지가 나오는 경우가 있다.

실제 이미지 사이즈는 4096×4096보다 훨씬 작은데도 말이다. 
(나는 1700×1404 이었다 절반도 안되는데 막 이레… ㅡㅡ;)


황당한 것은 롤리팝(5.0)이 올라간 넥서스 5에서 퍽퍽 난다…. 죽지는 않고 이미지가 안나오고 흰 화면만 나올 뿐이다.

돌아버리는 줄알았다.. 

똑같은 apk를 프로요나 진저브레드에 설치해서 돌려보면 잘 돌아간다.

아놔… 이 시키들 뭘 바꾼겨??? 라는 생각을 했지만… 뭐… 갸들도 합리적이라 생각해서 뭔가를 바꿧겠지…



원인을 알아보자…

drawable 폴더에 있는 나의 이미지 파일은 drawable-mdpi 에 넣은거랑 동일하다. 

왜??? 거기가 1dp == 1px 이니깐… 근데… 내 안드로이드 기기가 xxhdpi(1080×1920)이라면 mdpi 꺼를 확대해서 사용한다.

몇배??? 

3배!!! ㅡㅡ;;;;

나의 경우는 1700*3 x 1404*3 = 5100 x 4212 딱 맞다~ 에러 메시지에 나온 얼토당토 않은 초고해상도가 이렇게 나온 것이었다.

췟!!! 이게 뭐다냥…


이제 해결을 해보자

우선 완전 간단하게 해결 하는 방법 : 
/drawable 에 있는 이미지 파일을 /drawable-nodpi 폴더로 이동한다. 
폴더가 없으면 /drawable 와 같은 레벨에 만들면 된다. dpi를 적용하지 않는 이미지가 저장되는 파일들이다. 
당근 xxhdpi 단말기에서도 확대를 하지 않고 그대로 쓴다.

해결은 당장 되지만 이미지의 퀄리티는 쫌 떨어져 보인다.


쬐끔 복잡하게 해결 하는 방법 : 
drawable-mdpi, hdpi, xhdpi, xxhdpi 에다가 각각의 화면 단위에 맞게 이미지를 만들어 주는 것이다.
같은 이미지를 해상도를 달리하여 저장하는 작업이기 때문에 쫌 까다롭겠지만 괸찮은 퀄리티의 이미지를 보여줄 수 있다.

안드로이드에서 내가 Debug 모듈인지 체크하는 방법

ADT 17 이상에서

BuildConfig.DEBUG 상수값을 체크함으로 알 수 있다.

이 클래스는 컴파일시 generated 된다.

프로그래밍할때 다음과 같이 사용하면 편하다.

if ( BuildConfig.DEBUG )   
{  
  // Debug 일때 사용할 코드  
}  
else  
{  
  // Release 일때 사용할 코드  
}  

참고로 안드로이드 스튜디오에서 Run으로 실행시키나 Debug로 실행시키나 동일하게 BuildConfig.DEBUG 는 true 이다.

릴리즈 apk를 실행할때만 false이다.

안드로이드 BLE 프로그래밍

Bluetooth Low Energy 4.3부터 BLE central role을 지원하기 시작했다. 그리고 디바이스 검색과 서비스 검색 그리고 속성들의 읽기/쓰기를 지원한다. BLE를 통해 저전력으로 설계된 다양한 디바이스와통신이 가능해졌다.
Key Terms and Concepts

  • Generic Attribute Profile (GATT) – BLE link상에서 송수신 가능한 일반적인 사양을 말하며 모든 LE 애플리케이션프로파일은 GATT에 기반한다.
    • 블루투스 SIG는 많은 프로파일을 정의하였는데 하나의 프로파일은 앱에서 어떻게 동작할지에 대한 사양을 의미한다. 하나의 장치는 여러 프로파일을 구현할 수 있다. 
  • Attribute Protocol (ATT) – GATT는 ATT의 최상위 구현체이며 GATT/ATT로 참조되기도 한다. ATT는 BLE장치에서 동작하도록 최적화 되어 있다. 개개의 속성(Attribute)은 UUID를 가지며 128비트로 구성된다. ATT에 의해 부여된 속성은 특성과 서비스를 결정한다.
  • Characteristic – 하나의 특성(characteristic)은 하나의 값과 n개의 디스크립터를 포함하는데 디스크립터는 특성 값을 기술한다. 하나의 특성은 클래스와 유사한 타입으로 생각하면 된다. 
  • Descriptor – 디스크립터는 특성의 값을 기술한다.
  • Service – 하나의 서비스는 특성드의 집합니다. 예를 들어 “Heart Rate Monitor”라고 불리는 서비스를 가지고 있다면 그 서비스는 “heart rate measurement”같은 특성을 포함한다. GATT-based profile의 리스트와 서비스를 확인하려면 bluetooth.org를 방문하라.

Roles and Responsibilities이 문서에서 설명하는 역할과 책임은 Android장치가  BLE장치와 연동하는데 적용되는것들이다.

  • Central vs. peripheral – BLE 연결에 적용된다. central 역할은 scan, 게시검색(looking for advertisement), 그리고 peripheral역할은 게시를 만든다.
  • GATT server vs. GATT client – 디바이스가 연결된 이 후 서로 어떻게 대화하는지에 대해 정의한다. 

차이점을 이해하려면 안드로이드폰하나와 움직임을 감지하는 BLE장치를 가지고 있다고 가정 해보자. 폰은 central역할을 한다. BLE장치는 peripheral역할을 한다. 
폰과 BLE장치가 한번 연결이 되면 두 장치는 서로  GATT metadata를 주고 받는다. 주고받는 데이터에 따라 하나이상의 서버 액션이 있을 수 있다. 예를 들어 BLE장치가 폰에 센서정보를 전달하려고 할 수 있다. 이 때 장치는 서버로 동작한다. 장치가 폰으로부터 업데이트를 받고자 할수 있다 이 때는 폰이 서버로 동작한다.
이 문서에서 제공하는 샘플은 안드로이드 디바이스로 동작하는 앱으로 GATT client이다. 앱은 GATT server로부터 데이터를 가져오는데 그것은 Heart Rate Profile을 지원하는 BLE heart rate monitor이다.반면에 앱을 GATT 서버역할을 하도록 구현할 수 도 있다. BluetoothGattServer를 참조하라.
BLE permissions블루투스통신(연결요청, 연결수락, 데이터 전송)을 하려면 BLUETOOTH 퍼미션을 추가해야 함다.블루투스장치를 검색하고 설정을 조작하려면 BLUETOOTH_ADMIN 퍼미션을 추가해야 한다.BLUETOOTH_ADMIN은 BLUETOOTH퍼미션과 함께 정의되어야 한다.

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

블루투스 BLE만 사용할거라면 아래와 같이 manifest에 포함시켜야 한다.

<uses-feature android:name="android.hardware.bluetooth_le" android:required="true"/>

BLE를 지원하지 않는 앱으로 만들고자 한다면 여전히 동일한  feature를 추가해야 하며 다만 required=”false”로 하면 된다. 런타임에 BLE활성화 여부를 PackageManager.hasSystemFeature로 알아낼 수 있다.

// Use this check to determine whether BLE is supported on the device. Then
// you can selectively disable BLE-related features.
if (!getPackageManager().hasSystemFeature(PackageManager.FEATURE_BLUETOOTH_LE)) {
    Toast.makeText(this, R.string.ble_not_supported, Toast.LENGTH_SHORT).show();
    finish();
}

Setting Up BLE
BLE로 통신하기 전에 디바이스가 BLE를 지원하는지 확이해야 한다. 지원한다면 활성화되어야 한다. 만일  BLE를 지원하지 않는다면 BLE feature들을 비활성화해야 한다. 지원한다면 사용자에게 BT를 앱을 떠나지 않고 활성화하도록 유도해야 한다. 이 과정은 BluetoothAdapter를 사용하여 2단계로 가능하다.

1. BluetoothAdapter얻기

BluetoothAdapter는 블루투스관련 일부 또는 모든 블루투스 동작들을 필요로 한다. BluetoothAdapter는 디바이스자체의 BluetoothAdapter를 나타낸다.  전체시스템을 위한 하나의 어댑터가 있고 앱은 이 객체를 통해서 상호작용을 한다. 다음 코드조각은 어댑터를 얻는 방법을 보여준다.

  1. // Initializes Bluetooth adapter.
    final BluetoothManager bluetoothManager =
            (BluetoothManager) getSystemService(Context.BLUETOOTH_SERVICE);
    mBluetoothAdapter = bluetoothManager.getAdapter();

2. Bluetooth 활성화.다음에 블루투스를 활성화해야 한다. isEnabled()로 활성화여부를 확인 가능하다. false면 비활성화이다. 다음은 그 샘플이다.

  1. private BluetoothAdapter mBluetoothAdapter;

    // Ensures Bluetooth is available on the device and it is enabled. If not,
    // displays a dialog requesting user permission to enable Bluetooth.
    if (mBluetoothAdapter == null || !mBluetoothAdapter.isEnabled()) {
        Intent enableBtIntent = new Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE);
        startActivityForResult(enableBtIntent, REQUEST_ENABLE_BT);
    }

BLE 장치 찾기BLE장치를 찾으려면 startLeScan() 메소드를 호출한다. LeScanCallack이 파라메터로 호출된다. 이 메소드를 구현해야 하고 스캔결과를 받을 수 있다. 배터리 소모가 심심하기 때문에 다음의 가이드라인을 잘 지켜야 한다.

  • 원하는 디바이스를 찾으면 바로 스캔을 중단해야 한다.
  • 하나의 루프에서 스캔하지 말고 타임아웃을 적용해라. 디바이스가 범위안에서 벗어났을 수도 있고 그럴경우 배터리 는 골로 간다.

다음 코드는 스캔을 시작하고 중단하는 예이다.

/**
 * Activity for scanning and displaying available BLE devices.
 */
public class DeviceScanActivity extends ListActivity {

    private BluetoothAdapter mBluetoothAdapter;
    private boolean mScanning;
    private Handler mHandler;

    // Stops scanning after 10 seconds.
    private static final long SCAN_PERIOD = 10000;
    ...
    private void scanLeDevice(final boolean enable) {
        if (enable) {
            // Stops scanning after a pre-defined scan period.
            mHandler.postDelayed(new Runnable() {
                @Override
                public void run() {
                    mScanning = false;
                    mBluetoothAdapter.stopLeScan(mLeScanCallback);
                }
            }, SCAN_PERIOD);

            mScanning = true;
            mBluetoothAdapter.startLeScan(mLeScanCallback);
        } else {
            mScanning = false;
            mBluetoothAdapter.stopLeScan(mLeScanCallback);
        }
        ...
    }
...
}

특정타입의 페리퍼렁만 스캔하고자 한다면 startLeScan(UUID[], BluetoothAdapter.LeScanCallback)을 사용할 수 있다. UUID[]에는 앱에서 지원하고자 하는  GATT 서비스목록이 들어간다.다음코드 예를 보아라.

private LeDeviceListAdapter mLeDeviceListAdapter;
...
// Device scan callback.
private BluetoothAdapter.LeScanCallback mLeScanCallback =
        new BluetoothAdapter.LeScanCallback() {
    @Override
    public void onLeScan(final BluetoothDevice device, int rssi,
            byte[] scanRecord) {
        runOnUiThread(new Runnable() {
           @Override
           public void run() {
               mLeDeviceListAdapter.addDevice(device);
               mLeDeviceListAdapter.notifyDataSetChanged();
           }
       });
   }
};

BLE또는 Class BT장치를 검색할 수 는 있지만 두가지를 동시에 검색은 할 수 없다.
GATT 서버에 연결하기첫번째 연동을 위한 과정은 BLE디바이스에 연결하는것인데 좀더 자세히 말하면 디바이스의 GATT서버에 연결하는 것이다. connectGatt()메소드로 하면 된다. 이 메소드는 3개의 파라메터가 있는데 context, autoConnect, 그리고 BluetoothGattCallback이 있다. autoConnect는 검색되었을 때 자동으로 연결할지에 대한 파라메터다.

mBluetoothGatt = device.connectGatt(this, false, mGattCallback);

이 코드는 BLE device를 GATT 서버 호스트로 연결한다. 그리고 BluetoothGatt 인스턴스를 반환한다. 이 인스턴스로 GATT client를 운영한다.BluetoothGattCallback은 클라이언트에 연결상태나 client 운영에 대한 결과를 전달한다.
이 예제에서 BLE 앱은 하나의 액티비티가 연결하고 데이터를 표시하고 GATT 서비스와 특성들을 표시한다. 사용자 입력에 기반하여 BluetoothLeService로 불리는 서비스와 통신을 수행하도 Android BLE API를 통해 장치와 상호연동을 한다.

// A service that interacts with the BLE device via the Android BLE API.
public class BluetoothLeService extends Service {
    private final static String TAG = BluetoothLeService.class.getSimpleName();

    private BluetoothManager mBluetoothManager;
    private BluetoothAdapter mBluetoothAdapter;
    private String mBluetoothDeviceAddress;
    private BluetoothGatt mBluetoothGatt;
    private int mConnectionState = STATE_DISCONNECTED;

    private static final int STATE_DISCONNECTED = 0;
    private static final int STATE_CONNECTING = 1;
    private static final int STATE_CONNECTED = 2;

    public final static String ACTION_GATT_CONNECTED =
            "com.example.bluetooth.le.ACTION_GATT_CONNECTED";
    public final static String ACTION_GATT_DISCONNECTED =
            "com.example.bluetooth.le.ACTION_GATT_DISCONNECTED";
    public final static String ACTION_GATT_SERVICES_DISCOVERED =
            "com.example.bluetooth.le.ACTION_GATT_SERVICES_DISCOVERED";
    public final static String ACTION_DATA_AVAILABLE =
            "com.example.bluetooth.le.ACTION_DATA_AVAILABLE";
    public final static String EXTRA_DATA =
            "com.example.bluetooth.le.EXTRA_DATA";

    public final static UUID UUID_HEART_RATE_MEASUREMENT =
            UUID.fromString(SampleGattAttributes.HEART_RATE_MEASUREMENT);

    // Various callback methods defined by the BLE API.
    private final BluetoothGattCallback mGattCallback =
            new BluetoothGattCallback() {
        @Override
        public void onConnectionStateChange(BluetoothGatt gatt, int status,
                int newState) {
            String intentAction;
            if (newState == BluetoothProfile.STATE_CONNECTED) {
                intentAction = ACTION_GATT_CONNECTED;
                mConnectionState = STATE_CONNECTED;
                broadcastUpdate(intentAction);
                Log.i(TAG, "Connected to GATT server.");
                Log.i(TAG, "Attempting to start service discovery:" +
                        mBluetoothGatt.discoverServices());

            } else if (newState == BluetoothProfile.STATE_DISCONNECTED) {
                intentAction = ACTION_GATT_DISCONNECTED;
                mConnectionState = STATE_DISCONNECTED;
                Log.i(TAG, "Disconnected from GATT server.");
                broadcastUpdate(intentAction);
            }
        }

        @Override
        // New services discovered
        public void onServicesDiscovered(BluetoothGatt gatt, int status) {
            if (status == BluetoothGatt.GATT_SUCCESS) {
                broadcastUpdate(ACTION_GATT_SERVICES_DISCOVERED);
            } else {
                Log.w(TAG, "onServicesDiscovered received: " + status);
            }
        }

        @Override
        // Result of a characteristic read operation
        public void onCharacteristicRead(BluetoothGatt gatt,
                BluetoothGattCharacteristic characteristic,
                int status) {
            if (status == BluetoothGatt.GATT_SUCCESS) {
                broadcastUpdate(ACTION_DATA_AVAILABLE, characteristic);
            }
        }
     ...
    };
...
}

콜백이 호출되면 broadcastUpdate()를 호출한다. 여기서 데이터 파싱은 Bluetooth Heart Rate Measurement profile 사양에 맞춰서 수행하고 있다.

private void broadcastUpdate(final String action) {
    final Intent intent = new Intent(action);
    sendBroadcast(intent);
}

private void broadcastUpdate(final String action,
                             final BluetoothGattCharacteristic characteristic) {
    final Intent intent = new Intent(action);

    // This is special handling for the Heart Rate Measurement profile. Data
    // parsing is carried out as per profile specifications.
    if (UUID_HEART_RATE_MEASUREMENT.equals(characteristic.getUuid())) {
        int flag = characteristic.getProperties();
        int format = -1;
        if ((flag & 0x01) != 0) {
            format = BluetoothGattCharacteristic.FORMAT_UINT16;
            Log.d(TAG, "Heart rate format UINT16.");
        } else {
            format = BluetoothGattCharacteristic.FORMAT_UINT8;
            Log.d(TAG, "Heart rate format UINT8.");
        }
        final int heartRate = characteristic.getIntValue(format, 1);
        Log.d(TAG, String.format("Received heart rate: %d", heartRate));
        intent.putExtra(EXTRA_DATA, String.valueOf(heartRate));
    } else {
        // For all other profiles, writes the data formatted in HEX.
        final byte[] data = characteristic.getValue();
        if (data != null && data.length > 0) {
            final StringBuilder stringBuilder = new StringBuilder(data.length);
            for(byte byteChar : data)
                stringBuilder.append(String.format("%02X ", byteChar));
            intent.putExtra(EXTRA_DATA, new String(data) + "\n" +
                    stringBuilder.toString());
        }
    }
    sendBroadcast(intent);
}

이 이벤드들은 B roadcastReceiver에 의해 처리된다.

// Handles various events fired by the Service.
// ACTION_GATT_CONNECTED: connected to a GATT server.
// ACTION_GATT_DISCONNECTED: disconnected from a GATT server.
// ACTION_GATT_SERVICES_DISCOVERED: discovered GATT services.
// ACTION_DATA_AVAILABLE: received data from the device. This can be a
// result of read or notification operations.
private final BroadcastReceiver mGattUpdateReceiver = new BroadcastReceiver() {
    @Override
    public void onReceive(Context context, Intent intent) {
        final String action = intent.getAction();
        if (BluetoothLeService.ACTION_GATT_CONNECTED.equals(action)) {
            mConnected = true;
            updateConnectionState(R.string.connected);
            invalidateOptionsMenu();
        } else if (BluetoothLeService.ACTION_GATT_DISCONNECTED.equals(action)) {
            mConnected = false;
            updateConnectionState(R.string.disconnected);
            invalidateOptionsMenu();
            clearUI();
        } else if (BluetoothLeService.
                ACTION_GATT_SERVICES_DISCOVERED.equals(action)) {
            // Show all the supported services and characteristics on the
            // user interface.
            displayGattServices(mBluetoothLeService.getSupportedGattServices());
        } else if (BluetoothLeService.ACTION_DATA_AVAILABLE.equals(action)) {
            displayData(intent.getStringExtra(BluetoothLeService.EXTRA_DATA));
        }
    }
};

BLE 속성 읽기앱이 GATT 서버에 연결하고 서비스를 찾게 되면 속성을  읽고/쓸수 있게 된다. 다음 코드는 서비스와 특성들을 나열하여 표시해준다.

public class DeviceControlActivity extends Activity {
    ...
    // Demonstrates how to iterate through the supported GATT
    // Services/Characteristics.
    // In this sample, we populate the data structure that is bound to the
    // ExpandableListView on the UI.
    private void displayGattServices(List<BluetoothGattService> gattServices) {
        if (gattServices == null) return;
        String uuid = null;
        String unknownServiceString = getResources().
                getString(R.string.unknown_service);
        String unknownCharaString = getResources().
                getString(R.string.unknown_characteristic);
        ArrayList<HashMap<String, String>> gattServiceData =
                new ArrayList<HashMap<String, String>>();
        ArrayList<ArrayList<HashMap<String, String>>> gattCharacteristicData
                = new ArrayList<ArrayList<HashMap<String, String>>>();
        mGattCharacteristics =
                new ArrayList<ArrayList<BluetoothGattCharacteristic>>();

        // Loops through available GATT Services.
        for (BluetoothGattService gattService : gattServices) {
            HashMap<String, String> currentServiceData =
                    new HashMap<String, String>();
            uuid = gattService.getUuid().toString();
            currentServiceData.put(
                    LIST_NAME, SampleGattAttributes.
                            lookup(uuid, unknownServiceString));
            currentServiceData.put(LIST_UUID, uuid);
            gattServiceData.add(currentServiceData);

            ArrayList<HashMap<String, String>> gattCharacteristicGroupData =
                    new ArrayList<HashMap<String, String>>();
            List<BluetoothGattCharacteristic> gattCharacteristics =
                    gattService.getCharacteristics();
            ArrayList<BluetoothGattCharacteristic> charas =
                    new ArrayList<BluetoothGattCharacteristic>();
           // Loops through available Characteristics.
            for (BluetoothGattCharacteristic gattCharacteristic :
                    gattCharacteristics) {
                charas.add(gattCharacteristic);
                HashMap<String, String> currentCharaData =
                        new HashMap<String, String>();
                uuid = gattCharacteristic.getUuid().toString();
                currentCharaData.put(
                        LIST_NAME, SampleGattAttributes.lookup(uuid,
                                unknownCharaString));
                currentCharaData.put(LIST_UUID, uuid);
                gattCharacteristicGroupData.add(currentCharaData);
            }
            mGattCharacteristics.add(charas);
            gattCharacteristicData.add(gattCharacteristicGroupData);
         }
    ...
    }
...
}

GATT 통지 수신디바이스의 특성이 변경에 대한 통지를 앱이 알수 있다. 다음 코드는 setCharacteristicNotification()메소드로 어떻게 통지를 설정하는지 보여준다.

private BluetoothGatt mBluetoothGatt;
BluetoothGattCharacteristic characteristic;
boolean enabled;
...
mBluetoothGatt.setCharacteristicNotification(characteristic, enabled);
...
BluetoothGattDescriptor descriptor = characteristic.getDescriptor(
        UUID.fromString(SampleGattAttributes.CLIENT_CHARACTERISTIC_CONFIG));
descriptor.setValue(BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE);
mBluetoothGatt.writeDescriptor(descriptor);

하나의 특성에 대해 통지가 활성화되면 onCharacteristicChanged()가 장치에서 해당 특성정보가 변경이 되면 호출된다.

@Override
// Characteristic notification
public void onCharacteristicChanged(BluetoothGatt gatt,
        BluetoothGattCharacteristic characteristic) {
    broadcastUpdate(ACTION_DATA_AVAILABLE, characteristic);
}

클라이언트 앱의 종료BLE 장치를 사용하는 앱이 종료되면 반드시 close()를 호출하여 시스템이 관련 리소스를 반환하도록 해야 한다.

public void close() {
    if (mBluetoothGatt == null) {
        return;
    }
    mBluetoothGatt.close();
    mBluetoothGatt = null;
}

출처: https://samse.tistory.com/entry/android-Bluetooth-LE-programmingBLE [고 투 더 멘토]